MaixCAM MaixPy 使用 Modbus 协议

Modbus 简介

Modbus 是一个应用层总线协议,传输层基于 UART 或者 TCP。
使用它可以实现一个总线上挂多个设备,实现一对多通信。

Modbus 和 Maix 应用通信协议的区别

  • Maix 应用通信协议

    • 通信方式:一对一通信。
    • 通信模式:全双工通信,双方都可以主动发送消息,实现更加实时的交互。
    • 数据灵活性:数据长度和类型没有限制,灵活支持多种数据结构。
    • MaixPy内置: MaixCAM MaixPy 内置了这个协议,部分应用默认数据输出用这个协议,在 MaixPy 生态中使用相同的协议有利于应用生态繁荣。
    • 应用场景:适用于一对一实时性要求高、需要双向数据传输的场景,比如 AI 推理结果传输和控制命令反馈,以及 Maix 应用信息读取和控制。
  • Modbus

    • 通信方式:总线协议,支持一对多通信。
    • 通信模式:只能由主机主动发起读写操作,从机的数据更新需要主机轮询获取,从机本质上可以看作具有多组寄存器的传感器。
    • 数据类型:数据以寄存器为单位,从机通过多个可读写或只读的寄存器实现数据交换。
    • 应用场景:适用于工业自动化中传感器或设备的数据采集与监控,尤其是在需要主从结构的情况下。

MaixCAM MaixPy 使用 Modbus

MaixPy 适配了 modbus 协议, 主机和从机模式均支持,RTU(UART)和 TCP 模式均支持,底层使用了开源项目 libmodbus

MaixCAM MaixPy 作为 Modbus 从机

作为从机时,可以将 MaixCAM 看作是一个有几组可读写寄存器的模块。

包含了几组寄存器,它们的区别就是值类型不同,以及有的能读写,有的只能读,寄存器组包括:

  • coils寄存器组: 布尔值,可读写。
  • discrete input:布尔值,可读不可写。
  • input registers:16bit int 值,可读不可写。
  • holding registers 16bit int 值,可读可写.

每组寄存器的地址和长度在从机初始化时自由指定,根据你的应用需求设置即可。

以下是例程,更多代码请看源码例程(examples/protocol/comm_modbus_xxx.py):

RTU(UART):

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

slave = modbus.Slave(
    modbus.Mode.RTU,    # modbus 模式选择为 rtu
    "/dev/ttyS0",       # 选择与主机通信的串口
    0x00, 10,           # coils 开始地址以及该组寄存器数量
    0x00, 10,           # discrete input 开始地址以及该组寄存器数量
    0x00, 10,           # input registers 开始地址以及该组寄存器数量
    0x00, 10,           # holding registers 开始地址以及该组寄存器数量
    115200, 1,          # 115200波特率, 默认为 8N1, 后面的 1 代表 rtu 的从机地址
    0, False            # tcp 端口号, 我们使用的是 rtu, 随便填写即可, 后者为 是否打印 debug 信息
)

"""
以 coils 寄存器组为例, 起始地址为 0x00, 数量为 10, 代表该从机 coils 寄存器组的地址范围为 0x00~0x09 共 10 个寄存器, 每个寄存器存储一个布尔值.
"""

# 读取当前 input registers 寄存器组里的所有值
# 寄存器内初始值均为 0
old_ir = slave.input_registers()
print("old ir: ", old_ir)

# 将列表内的值从引索2开始更新到 input registers 中
# 寄存器组内数值将变成 [0x00 0x00 0x22 0x33 0x44 0x00 0x00 0x00 0x00 0x00]
data : list[int] = [0x22, 0x33, 0x44]
slave.input_registers(data, 2)
# 读取, 打印验证
new_ir = slave.input_registers()
print("new ir:", new_ir)


while not app.need_exit():
    # 等待主机的读写操作
    if err.Err.ERR_NONE != slave.receive(2000): #timeout 2000ms
        continue

    # 获取主机的操作类型
    rtype = slave.request_type()
    # 如果主机想要读取 holding registers
    if rtype == modbus.RequestType.READ_HOLDING_REGISTERS:
        # 准备数据
        # 获取并查看当前 holding registers 内数据
        print("master read hr")
        hr = slave.holding_registers()
        print("now hr: ", hr)

        # 更新 holding registers 内所有数据
        hr = [x+1 for x in hr]
        print("now we make hr+1: ", hr)
        print("update hr")
        slave.holding_registers(hr)
    # else ... 处理其他操作

    # 自动处理主机请求, 自动更新寄存器值
    slave.reply()

TCP:

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

slave = modbus.Slave(
    modbus.Mode.TCP,    # 模式: TCP
    "",                 # 保持空即可
    0x00, 10,
    0x00, 10,
    0x00, 10,
    0x00, 10,
    0, 1,               # 我们使用的是 TCP 模式, 忽略波特率和rtu地址即可
    502, False         # TCP 端口号, 后者为 是否打印 debug 信息
)

### 以下代码与 RTU 部分一致

old_ir = slave.input_registers()
print("old ir: ", old_ir)
data : list[int] = [0x22, 0x33, 0x44]
slave.input_registers(data, 3)
new_ir = slave.input_registers()
print("new ir:", 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 read hr")
        hr = slave.holding_registers()
        print("now hr: ", hr)
        hr = [x+1 for x in hr]
        print("now we make hr+1: ", hr)
        print("update hr")
        slave.holding_registers(hr)

    slave.reply()

可以看到这里接收到来自主机的读取请求后调用slave.reply()就会自动回复主机要读取的数据了,以及这里展示了更改本身的寄存器值。

有关 Modbus API 的详细说明请看 Modbus API 文档.

MaixCAM MaixPy 作为 Modbus 主机

主机则可以主动读写从机的数据,例程(以源码例程examples/protocol/comm_modbus_xxx.py为准):

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


REGISTERS_START_ADDRESS = 0x00
REGISTERS_NUMBER = 10

RTU_SLAVE_ID = 1
RTU_BAUDRATE = 115200

def master_thread(*args):
    if pinmap.set_pin_function("A19", "UART1_TX") != err.Err.ERR_NONE:
        print("init uart1 failed!")
        exit(-1)
    if pinmap.set_pin_function("A18", "UART1_RX") != err.Err.ERR_NONE:
        print("init uart1 failed!")
        exit(-1)

    # modbus.set_master_debug(True)

    master = modbus.MasterRTU(
        "/dev/ttyS1",
        RTU_BAUDRATE
    )

    while not app.need_exit():
        hr = master.read_holding_registers(
            RTU_SLAVE_ID,
            REGISTERS_START_ADDRESS,
            REGISTERS_NUMBER,
            2000
        )
        if len(hr) == 0:
            continue
        print("Master read hr: ", hr)
        time.sleep(1)

master_thread(None)

可以看到这里用 串口1 作为主机从从机读取寄存器值。