MaixCAM MaixPy Using Modbus Protocol

Introduction to Modbus

Modbus is an application-layer bus protocol that operates over UART or TCP as the transport layer.
It allows multiple devices to connect to a single bus, enabling one-to-many communication.

Differences Between Modbus and Maix Application Communication Protocol

  • Maix Application Communication Protocol:

    • Communication Type: One-to-one communication.
    • Communication Mode: Full-duplex, allowing both parties to actively send messages for more real-time interaction.
    • Data Flexibility: No restrictions on data length or type, supporting flexible data structures.
    • MaixCAM MaixPy integrated: By default some MaixCAM/MaixPy APP impleted this protocol, you can directly use it, and use same protocol is good for MaixCAM/MaixPy ecosystem health.
    • Application Scenarios: Suitable for one-to-one scenarios requiring high real-time performance and bidirectional data transmission, such as AI inference result transmission and control command feedback. And communicate with MaixCAM MaixPy's applications.
  • Modbus:

    • Communication Type: A bus protocol supporting one-to-many communication.
    • Communication Mode: Only the master can initiate read/write operations. The slave's data updates are obtained by the master's polling mechanism, and slaves can essentially be regarded as sensors with multiple groups of registers.
    • Data Type: Data is organized into registers. Slaves have multiple readable/writable or read-only registers for data exchange.
    • Application Scenarios: Suitable for industrial automation scenarios where data collection and monitoring of sensors or devices are needed, especially in master-slave structured systems.

Using Modbus with MaixCAM MaixPy

MaixPy supports the Modbus protocol, including both master and slave modes, as well as RTU (UART) and TCP modes.
The implementation is based on the open-source project libmodbus.

MaixCAM as a Modbus Slave

When acting as a slave, MaixCAM can be seen as a module with several groups of readable/writable registers.

The registers include the following types, which differ in value type and read/write permissions:

  • coils Registers: Boolean values, readable and writable.
  • discrete input Registers: Boolean values, readable but not writable.
  • input registers: 16-bit integer values, readable but not writable.
  • holding registers: 16-bit integer values, readable and writable.

The address and length of each register group can be freely specified during slave initialization based on application requirements.

Below are examples. For more code, refer to the source examples (examples/protocol/comm_modbus_xxx.py).

RTU (UART):

from maix.comm import modbus
from maix import app, err

slave = modbus.Slave(
    modbus.Mode.RTU,    # Set Modbus mode to RTU
    "/dev/ttyS0",       # Specify the UART port for communication with the master
    0x00, 10,           # Start address and number of registers for coils
    0x00, 10,           # Start address and number of registers for discrete input
    0x00, 10,           # Start address and number of registers for input registers
    0x00, 10,           # Start address and number of registers for holding registers
    115200, 1,          # Baud rate of 115200, default 8N1; the last `1` is the slave address for RTU
    0, False            # TCP port (irrelevant for RTU), debug flag
)

"""
Example: Coils register group
Start address: 0x00
Number: 10
This means the coils register group ranges from 0x00 to 0x09, with each register storing a Boolean value.
"""

# Read all values from the input registers group
old_ir = slave.input_registers()
print("Old input registers:", old_ir)

# Update values in the input registers group starting from index 2
# New register values: [0x00, 0x00, 0x22, 0x33, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00]
data: list[int] = [0x22, 0x33, 0x44]
slave.input_registers(data, 2)

# Read and verify updated values
new_ir = slave.input_registers()
print("New input registers:", new_ir)

while not app.need_exit():
    # Wait for the master's read/write operation
    if err.Err.ERR_NONE != slave.receive(2000):  # Timeout: 2000ms
        continue

    # Determine the type of operation requested by the master
    rtype = slave.request_type()
    if rtype == modbus.RequestType.READ_HOLDING_REGISTERS:
        print("Master requested to read holding registers")
        hr = slave.holding_registers()
        print("Current holding registers:", hr)

        # Update holding registers with new values
        hr = [x + 1 for x in hr]
        slave.holding_registers(hr)

    # Automatically handle the master's request and update register values
    slave.reply()

TCP:

from maix.comm import modbus
from maix import app, err

slave = modbus.Slave(
    modbus.Mode.TCP,    # Set mode to TCP
    "",                 # Leave blank for TCP mode
    0x00, 10,           # Start address and number of registers for coils
    0x00, 10,           # Start address and number of registers for discrete input
    0x00, 10,           # Start address and number of registers for input registers
    0x00, 10,           # Start address and number of registers for holding registers
    0, 1,               # TCP port; the last parameter is the debug flag
)

# The following code is identical to the RTU example

old_ir = slave.input_registers()
print("Old input registers:", old_ir)

data: list[int] = [0x22, 0x33, 0x44]
slave.input_registers(data, 2)
new_ir = slave.input_registers()
print("New input registers:", new_ir)

while not app.need_exit():
    if err.Err.ERR_NONE != slave.receive(2000):  # Timeout: 2000ms
        continue

    rtype = slave.request_type()
    if rtype == modbus.RequestType.READ_HOLDING_REGISTERS:
        print("Master requested to read holding registers")
        hr = slave.holding_registers()
        print("Current holding registers:", hr)

        hr = [x + 1 for x in hr]
        print("Updated holding registers:", hr)
        slave.holding_registers(hr)

    slave.reply()

As shown above, calling slave.reply() after receiving a read request from the master automatically replies with the requested data. The example also demonstrates how to modify the register values on the slave side.

For detailed information on the Modbus API, refer to the Modbus API Documentation.

MaixCAM MaixPy as Modbus Master

As a master, MaixCAM can actively read and write data from/to slaves. Below is an example (refer to the source example examples/protocol/comm_modbus_xxx.py for more details):

from maix import pinmap, app, err, time, thread
from maix.comm import modbus

REGISTERS_START_ADDRESS = 0x00  # Start address for registers
REGISTERS_NUMBER = 10           # Number of registers to read

RTU_SLAVE_ID = 1                # Slave ID
RTU_BAUDRATE = 115200           # Baud rate for UART communication

def master_thread(*args):
    # Initialize UART1 for Modbus communication
    if pinmap.set_pin_function("A19", "UART1_TX") != err.Err.ERR_NONE:
        print("Failed to initialize UART1 TX!")
        exit(-1)
    if pinmap.set_pin_function("A18", "UART1_RX") != err.Err.ERR_NONE:
        print("Failed to initialize UART1 RX!")
        exit(-1)

    # Optional: Enable debugging for Modbus master
    # modbus.set_master_debug(True)

    # Create a Modbus master instance with RTU mode
    master = modbus.MasterRTU(
        "/dev/ttyS1",    # UART device for communication
        RTU_BAUDRATE     # Baud rate
    )

    while not app.need_exit():
        # Read holding registers from the slave
        hr = master.read_holding_registers(
            RTU_SLAVE_ID,              # Slave ID
            REGISTERS_START_ADDRESS,   # Starting address of registers
            REGISTERS_NUMBER,          # Number of registers to read
            2000                       # Timeout in milliseconds
        )

        # Check if the read operation was successful
        if len(hr) == 0:
            continue

        # Print the read data
        print("Master read holding registers:", hr)

        # Wait for 1 second before the next read
        time.sleep(1)

# Start the master thread
master_thread(None)

This example demonstrates using UART1 as the master to read register values from a slave device.