Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hardware Integration: Modbus TCP

Modbus Overview

Modbus TCP is one of the most common industrial communication protocols. If you have used Modbus with TwinCAT or any other PLC, the concepts are the same — holding registers, input registers, coils, and discrete inputs.

In AutoCore, Modbus communication is handled by the autocore-modbus module. This module:

  1. Runs as a separate process
  2. Connects to your Modbus TCP devices
  3. Cyclically reads and writes registers
  4. Maps register data into shared memory

Your control program reads and writes Modbus data through variables, just like any other I/O.

Configuring a Modbus Device

Add the Modbus module to your project.json:

{
  "name": "modbus_example",
  "version": "0.1.0",
  "description": "Modbus TCP example",
  "control": {
    "enable": true,
    "source_directory": "./control",
    "entry_point": "main.rs",
    "signals": {
      "tick": {
        "source": "internal",
        "scan_rate_us": 10000
      }
    }
  },
  "modules": {
    "modbus": {
      "enabled": true,
      "args": ["service"],
      "config": {
        "devices": [
          {
            "name": "sensor_unit",
            "type": "modbus_tcp",
            "host": "192.168.1.100",
            "port": 502,
            "slave_id": 1,
            "scan_rate_ms": 100,
            "registers": [
              {
                "name": "temperature",
                "type": "input_register",
                "address": 0,
                "count": 1
              },
              {
                "name": "humidity",
                "type": "input_register",
                "address": 1,
                "count": 1
              },
              {
                "name": "setpoint",
                "type": "holding_register",
                "address": 0,
                "count": 1
              }
            ]
          }
        ]
      }
    }
  },
  "variables": {
    "temperature_raw": {
      "type": "u16",
      "direction": "input",
      "link": "modbus.sensor_unit.temperature",
      "description": "Raw temperature reading (0.1°C per count)"
    },
    "humidity_raw": {
      "type": "u16",
      "direction": "input",
      "link": "modbus.sensor_unit.humidity",
      "description": "Raw humidity reading (0.1% per count)"
    },
    "setpoint_raw": {
      "type": "u16",
      "direction": "command",
      "link": "modbus.sensor_unit.setpoint",
      "description": "Temperature setpoint (0.1°C per count)"
    }
  }
}

Key configuration fields:

FieldDescription
modules.modbus.enabledSet to true to enable the Modbus module
modules.modbus.argsMust include "service" for the Modbus module
config.devices[].nameA friendly name for the device (used in variable links)
config.devices[].hostIP address of the Modbus TCP device
config.devices[].portTCP port (usually 502)
config.devices[].slave_idModbus unit ID (1-247)
config.devices[].registers[].type"holding_register", "input_register", "coil", or "discrete_input"
config.devices[].registers[].addressThe register address (0-based)

Linking Variables to Modbus Registers

The "link" field in a variable definition connects it to a Modbus register. The format is:

modbus.<device_name>.<register_name>

For example, if your device is named "sensor_unit" and the register is named "temperature", the link is "modbus.sensor_unit.temperature".

The direction of the variable determines the data flow:

Variable DirectionModbus Behavior
"input"Module reads from the device and writes to shared memory. Your control program reads it.
"command" or "output"Your control program writes to shared memory. Module reads it and writes to the device.

Example: Reading a Temperature Sensor

This example reads a temperature and humidity sensor via Modbus TCP and converts the raw values to engineering units.

project.json variables (using the Modbus config above):

"variables": {
  "temperature_raw": {
    "type": "u16",
    "direction": "input",
    "link": "modbus.sensor_unit.temperature",
    "description": "Raw temperature (0.1°C per count)"
  },
  "humidity_raw": {
    "type": "u16",
    "direction": "input",
    "link": "modbus.sensor_unit.humidity",
    "description": "Raw humidity (0.1% per count)"
  },
  "temperature_degc": {
    "type": "f32",
    "direction": "status",
    "description": "Temperature in °C"
  },
  "humidity_pct": {
    "type": "f32",
    "direction": "status",
    "description": "Humidity in %"
  },
  "temp_alarm": {
    "type": "bool",
    "direction": "status",
    "description": "Temperature over limit"
  }
}

control/src/program.rs:

#![allow(unused)]
fn main() {
use autocore_std::{ControlProgram, TickContext};
use autocore_std::fb::Ton;
use std::time::Duration;
use crate::gm::GlobalMemory;

pub struct MyControlProgram {
    alarm_delay: Ton,
}

impl MyControlProgram {
    pub fn new() -> Self {
        Self {
            alarm_delay: Ton::new(),
        }
    }
}

impl ControlProgram for MyControlProgram {
    type Memory = GlobalMemory;

    fn initialize(&mut self, _mem: &mut Self::Memory) {
        log::info!("Temperature monitor started");
    }

    fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
        // Convert raw Modbus values to engineering units
        // The sensor sends temperature as integer tenths of a degree
        ctx.gm.temperature_degc = ctx.gm.temperature_raw as f32 / 10.0;
        ctx.gm.humidity_pct = ctx.gm.humidity_raw as f32 / 10.0;

        // Alarm if temperature exceeds 50°C for more than 5 seconds
        let over_limit = ctx.gm.temperature_degc > 50.0;
        ctx.gm.temp_alarm = self.alarm_delay.call(over_limit, Duration::from_secs(5));

        // Log every 10 seconds
        if ctx.cycle % 1000 == 0 {
            log::info!(
                "Temp: {:.1}°C, Humidity: {:.1}%, Alarm: {}",
                ctx.gm.temperature_degc,
                ctx.gm.humidity_pct,
                ctx.gm.temp_alarm
            );
        }
    }
}
}

Example: Controlling a VFD (Variable Frequency Drive)

This example shows how to control a VFD (motor drive) over Modbus TCP. Most VFDs use holding registers for speed setpoint and run/stop commands.

project.json (modules section):

"modules": {
  "modbus": {
    "enabled": true,
    "args": ["service"],
    "config": {
      "devices": [
        {
          "name": "vfd_01",
          "type": "modbus_tcp",
          "host": "192.168.1.50",
          "port": 502,
          "slave_id": 1,
          "registers": [
            { "name": "control_word",    "type": "holding_register", "address": 0 },
            { "name": "speed_setpoint",  "type": "holding_register", "address": 1 },
            { "name": "status_word",     "type": "input_register",   "address": 0 },
            { "name": "speed_feedback",  "type": "input_register",   "address": 1 },
            { "name": "current",         "type": "input_register",   "address": 2 }
          ]
        }
      ]
    }
  }
}

Variables:

"variables": {
  "vfd_control":     { "type": "u16", "direction": "command", "link": "modbus.vfd_01.control_word" },
  "vfd_speed_cmd":   { "type": "u16", "direction": "command", "link": "modbus.vfd_01.speed_setpoint" },
  "vfd_status":      { "type": "u16", "direction": "input",   "link": "modbus.vfd_01.status_word" },
  "vfd_speed_fb":    { "type": "u16", "direction": "input",   "link": "modbus.vfd_01.speed_feedback" },
  "vfd_current":     { "type": "u16", "direction": "input",   "link": "modbus.vfd_01.current" },
  "motor_run_cmd":   { "type": "bool", "direction": "command", "description": "Run motor from HMI" },
  "motor_speed_rpm": { "type": "f32",  "direction": "command", "description": "Speed setpoint in RPM" },
  "motor_running":   { "type": "bool", "direction": "status",  "description": "Motor is running" },
  "motor_rpm":       { "type": "f32",  "direction": "status",  "description": "Actual speed in RPM" },
  "motor_amps":      { "type": "f32",  "direction": "status",  "description": "Motor current in A" }
}

control/src/program.rs:

#![allow(unused)]
fn main() {
use autocore_std::{ControlProgram, TickContext};
use autocore_std::fb::RTrig;
use crate::gm::GlobalMemory;

pub struct MyControlProgram {
    run_trig: RTrig,
    stop_trig: RTrig,
}

impl MyControlProgram {
    pub fn new() -> Self {
        Self {
            run_trig: RTrig::new(),
            stop_trig: RTrig::new(),
        }
    }
}

impl ControlProgram for MyControlProgram {
    type Memory = GlobalMemory;

    fn initialize(&mut self, _mem: &mut Self::Memory) {
        log::info!("VFD control started");
    }

    fn process_tick(&mut self, ctx: &mut TickContext<Self::Memory>) {
        // VFD control word bits (typical for many VFDs):
        //   Bit 0: Run forward
        //   Bit 1: Run reverse
        //   Bit 2: Fault reset
        const RUN_FWD: u16 = 0x0001;

        // Convert HMI speed (RPM) to VFD setpoint
        // Many VFDs use 0-10000 = 0-100.00 Hz. Adjust for your drive.
        let max_rpm = 1800.0_f32;
        let max_freq_counts = 5000_u16; // 50.00 Hz = 1800 RPM for a 4-pole motor

        if ctx.gm.motor_run_cmd {
            ctx.gm.vfd_control = RUN_FWD;
            let speed_pct = (ctx.gm.motor_speed_rpm / max_rpm).clamp(0.0, 1.0);
            ctx.gm.vfd_speed_cmd = (speed_pct * max_freq_counts as f32) as u16;
        } else {
            ctx.gm.vfd_control = 0;
            ctx.gm.vfd_speed_cmd = 0;
        }

        // Convert feedback to engineering units
        ctx.gm.motor_running = (ctx.gm.vfd_status & 0x0001) != 0;
        ctx.gm.motor_rpm = (ctx.gm.vfd_speed_fb as f32 / max_freq_counts as f32) * max_rpm;
        ctx.gm.motor_amps = ctx.gm.vfd_current as f32 / 100.0; // 0.01A resolution
    }
}
}