跳转至

Modbus TCP 主站应用

作者:马梦阳 | 最后修改:2026-04-09

一、Modbus 协议概述

1.1 Modbus 协议介绍

Modbus 最初由 Modicon(现为施耐德电气旗下品牌)于 1979 年开发,是一种用于可编程逻辑控制器(PLC)之间通信的工业通信协议。由于其简单、开放、免费且易于实现,Modbus 已成为工业自动化领域最广泛使用的通信协议之一,被广泛应用于工业控制、楼宇自动化、能源管理、智能仪表等场景。

该协议采用主从架构,最初基于串行通信(如 Modbus RTU 和 Modbus ASCII),后扩展支持以太网传输(Modbus TCP)。Modbus 定义了四种数据对象:线圈、离散输入、输入寄存器和保持寄存器,并通过功能码实现设备间的数据交换。

1.2 Modbus 三种常见通信协议模式

1.2.1 Modbus RTU

传输介质:RS485/RS232 串行通信

编码格式:二进制编码

完整报文:[从站地址][功能码][数据][CRC16 校验]

  • 向单个保持寄存器写入数据:

  • 请求报文格式:[从站地址][功能码][寄存器起始地址 寄存器值][CRC16 校验]

  • 举例:01 06 00 00 00 01 48 0A
    • 01 :从站地址
    • 06 :向单个保持寄存器写入数据功能码
    • 00 00 :寄存器起始地址
    • 00 01 :写入的单个寄存器数据
    • 48 0A :CRC16 校验值
  • 响应报文格式:[从站地址][功能码][寄存器起始地址 寄存器值][CRC16 校验]
  • 举例:01 06 00 00 00 01 48 0A
    • 01 :从站地址
    • 06 :向单个保持寄存器写入数据功能码
    • 00 00 :寄存器起始地址
    • 00 01 :写入的单个寄存器数据
    • 48 0A :CRC16 校验值
  • 异常响应报文格式:[从站地址][功能码][异常码][CRC16 校验]
  • 举例:01 86 02 83 A0
    • 01 :从站地址
    • 86 :原功能码为 06,最高位为 1 表示异常响应
    • 02 :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的)
    • 83 A0 :CRC16 校验值
  • 向多个保持寄存器写入数据:

  • 请求报文格式:[从站地址][功能码][寄存器起始地址 寄存器数量 寄存器值字节数 寄存器值][CRC16 校验]

  • 举例:01 10 00 00 00 02 04 00 01 00 05 62 6C
    • 01 :从站地址
    • 10 :写多个寄存器功能码
    • 00 00 :寄存器起始地址
    • 00 02 :要写入的寄存器数量
    • 04 :寄存器值的字节数
    • 00 01 :写入的第一个寄存器值
    • 00 05 :写入的第二个寄存器值
    • 62 6C :CRC16 校验值
  • 响应报文格式:[从站地址][功能码][寄存器起始地址 寄存器数量][CRC16 校验]
  • 举例:01 10 00 00 00 02 41 C8
    • 01 :从站地址
    • 10 :写多个寄存器功能码
    • 00 00 :寄存器起始地址
    • 00 02 :要写入的寄存器数量
    • 41 C8 :CRC16 校验值
  • 异常响应报文格式:[从站地址][功能码][异常码][CRC16 校验]
  • 举例:01 90 02 CD C1
    • 01 :从站地址
    • 90 :原功能码为 10,最高位为 1 表示异常响应
    • 02 :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的)
    • CD C1 :CRC16 校验值
  • 读取保持寄存器数据:

  • 请求报文格式:[从站地址][功能码][寄存器起始地址 寄存器数量][CRC16 校验]

  • 举例:01 03 00 00 00 02 C4 0B
    • 01 :从站地址
    • 03 :读单/多个保持寄存器功能码
    • 00 00 :寄存器起始地址
    • 00 02 :要读取的寄存器数量
    • C4 0B :CRC16 校验值
  • 响应报文格式:[从站地址][功能码][寄存器值字节数 寄存器值][CRC16 校验]
  • 举例:01 03 04 00 01 00 05 6B F0
    • 01 :从站地址
    • 03 :读单/多个保持寄存器功能码
    • 04 :寄存器值字节数
    • 00 01 :读取的第一个寄存器值
    • 00 05 :读取的第二个寄存器值
    • 6B F0 :CRC16 校验值
  • 异常响应报文格式:[从站地址][功能码][异常码][CRC16 校验]
  • 举例:01 83 02 C0 F1
    • 01 :从站地址
    • 83 :原功能码为 03,最高位为 1 表示异常响应
    • 02 :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的)
    • C0 F1 :CRC16 校验值

1.2.2 Modbus ASCII

传输介质:RS485/RS232 串行通信

编码格式:ASCII 字符编码

完整报文:[:][从站地址][功能码][数据][LRC 校验][\r\n]

  • 向单个保持寄存器写入数据:

  • 报文格式:[:][从站地址][功能码][寄存器起始地址 寄存器值][LRC 校验][\r\n]

  • 举例::010600000001B8\r\n
    • ‘:’ :起始符,HEX 值为 3A
    • ‘0’ ‘1’ :从站地址,HEX 值为 30 31
    • ‘0’ ‘6’ :向单个保持寄存器写入数据功能码,HEX 值为 30 36
    • ‘0’ ‘0’ ‘0’ ‘0’ :寄存器起始地址,HEX 值为 30 30 30 30
    • ‘0’ ‘0’ ‘0’ ‘1’ :写入的单个寄存器数据,HEX 值为 30 30 30 31
    • ‘B’ ‘8’ :LRC 校验值,HEX 值为 42 38
    • ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
  • 响应报文格式:[:][从站地址][功能码][寄存器起始地址 寄存器值][LRC 校验][\r\n]
  • 举例::010600000001B8\r\n
    • ‘:’ :起始符,HEX 值为 3A
    • ‘0’ ‘1’ :从站地址,HEX 值为 30 31
    • ‘0’ ‘6’ :向单个保持寄存器写入数据功能码,HEX 值为 30 36
    • ‘0’ ‘0’ ‘0’ ‘0’ :寄存器起始地址,HEX 值为 30 30 30 30
    • ‘0’ ‘0’ ‘0’ ‘1’ :写入的单个寄存器数据,HEX 值为 30 30 30 31
    • ‘B’ ‘8’ :LRC 校验值,HEX 值为 42 38
    • ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
  • 异常响应报文格式:[:][从站地址][功能码][异常码][LRC 校验][\r\n]
  • 举例::018602CF\r\n
    • ‘:’ :起始符,HEX 值为 3A
    • ‘0’ ‘1’ :从站地址,HEX 值为 30 31
    • ‘8’ ‘6’ :原功能码为 06,最高位为 1 表示异常响应,HEX 值为 38 36
    • ‘0’ ‘2’ :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的),HEX 值为 30 36
    • ‘C’ ‘F’ :LRC 校验值,HEX 值为 43 46
    • ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
  • 向多个保持寄存器写入数据:

  • 请求报文格式:[:][从站地址][功能码][寄存器起始地址 寄存器数量 寄存器值字节数 寄存器值][LRC 校验][\r\n]

  • 举例::0110000000020400010005D2\r\n
    • ‘:’ :起始符,HEX 值为 3A
    • ‘0’ ‘1’ :从站地址,HEX 值为 30 31
    • ‘1’ ‘0’ :写多个寄存器功能码,HEX 值为 31 30
    • ‘0’ ‘0’ ‘0’ ‘0’ :寄存器起始地址,HEX 值为 30 30 30 30
    • ‘0’ ‘0’ ‘0’ ‘2’ :要写入的寄存器数量,HEX 值为 30 30 30 32
    • ‘0’ ‘4’ :寄存器值的字节数,HEX 值为 30 34
    • ‘0’ ‘0’ ‘0’ ‘1’ :写入的第一个寄存器值,HEX 值为 30 30 30 31
    • ‘0’ ‘0’ ‘0’ ‘5’ :写入的第二个寄存器值,HEX 值为 30 30 30 35
    • ‘D’ ‘2’ :LRC 校验值,HEX 值为 44 32
    • ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
  • 响应报文格式:[:][从站地址][功能码][寄存器起始地址 寄存器数量][LRC 校验][\r\n]
  • 举例::011000000002BC\r\n
    • ‘:’ :起始符,HEX 值为 3A
    • ‘0’ ‘1’ :从站地址,HEX 值为 30 31
    • ‘1’ ‘0’ :写多个寄存器功能码,HEX 值为 31 30
    • ‘0’ ‘0’ ‘0’ ‘0’ :寄存器起始地址,HEX 值为 30 30 30 30
    • ‘0’ ‘0’ ‘0’ ‘2’ :要写入的寄存器数量,HEX 值为 30 30 30 32
    • ‘B’ ‘C’ :LRC 校验值,HEX 值为 42 43
    • ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
  • 异常响应报文格式:[:][从站地址][功能码][异常码][LRC 校验][\r\n]
  • 举例::019002D4\r\n
    • ‘:’ :起始符,HEX 值为 3A
    • ‘0’ ‘1’ :从站地址,HEX 值为 30 31
    • ‘9’ ‘0’ :原功能码为 10,最高位为 1 表示异常响应,HEX 值为 39 30
    • ‘0’ ‘2’ :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的),HEX 值为 30 36
    • ‘D’ ‘4’ :LRC 校验值,HEX 值为 44 34
    • ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
  • 读取保持寄存器数据:

  • 请求报文格式:[:][从站地址][功能码][寄存器起始地址 寄存器数量][LRC 校验][\r\n]

  • 举例::010300000002BA\r\n
    • ‘:’ :起始符,HEX 值为 3A
    • ‘0’ ‘1’ :从站地址,HEX 值为 30 31
    • ‘0’ ‘3’ :读单/多个保持寄存器功能码,HEX 值为 30 33
    • ‘0’ ‘0’ ‘0’ ‘0’ :寄存器起始地址,HEX 值为 30 30 30 30
    • ‘0’ ‘0’ ‘0’ ‘2’ :要读取的寄存器数量,HEX 值为 30 30 30 32
    • ‘B’ ‘A’ :LRC 校验值,HEX 值为 42 41
    • ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
  • 响应报文格式:[:][从站地址][功能码][寄存器值字节数 寄存器值][LRC 校验][\r\n]
  • 举例::0103040001000552\r\n
    • ‘:’ :起始符,HEX 值为 3A
    • ‘0’ ‘1’ :从站地址,HEX 值为 30 31
    • ‘0’ ‘3’ :读单/多个保持寄存器功能码,HEX 值为 30 33
    • ‘0’ ‘4’ :寄存器值字节数,HEX 值为 30 34
    • ‘0’ ‘0’ ‘0’ ‘1’ :读取的第一个寄存器值,HEX 值为 30 30 30 31
    • ‘0’ ‘0’ ‘0’ ‘5’ :读取的第二个寄存器值,HEX 值为 30 30 30 35
    • ‘5’ ‘2’ :LRC 校验值,HEX 值为 35 32
    • ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A
  • 异常响应报文格式:[:][从站地址][功能码][异常码][LRC 校验][\r\n]
  • 举例::018302D2\r\n
    • ‘:’ :起始符,HEX 值为 3A
    • ‘0’ ‘1’ :从站地址,HEX 值为 30 31
    • ‘8’ ‘3’ :原功能码为 03,最高位为 1 表示异常响应,HEX 值为 38 33
    • ‘0’ ‘2’ :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的),HEX 值为 30 36
    • ‘D’ ‘2’ :LRC 校验值,HEX 值为 44 32
    • ‘\r’ ‘\n’ :结束符,HEX 值为 0D 0A

1.2.3 Modbus TCP

传输介质:以太网等

编码格式:二进制编码格式

完整报文:[MBAP 头][功能码][数据]

  • 向单个保持寄存器写入数据:

  • 请求报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][寄存器起始地址 寄存器值]

  • 举例:00 01 00 00 00 06 01 06 00 00 00 01
    • 00 01 :事务标识符
    • 00 00 :协议标识符(modbus 固定为 00 00)
    • 00 06 :后续字节数
    • 01 :从站地址
    • 06 :向单个保持寄存器写入数据功能码
    • 00 00 :寄存器起始地址
    • 00 01 :写入的单个寄存器数据
  • 响应报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][寄存器起始地址 寄存器值]
  • 举例:00 01 00 00 00 06 01 06 00 00 00 01
    • 00 01 :事务标识符
    • 00 00 :协议标识符(modbus 固定为 00 00)
    • 00 06 :后续字节数
    • 01 :从站地址
    • 06 :向单个保持寄存器写入数据功能码
    • 00 00 :寄存器起始地址
    • 00 01 :写入的单个寄存器数据
  • 异常响应报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][异常码]
  • 举例:00 01 00 00 00 03 01 86 02
    • 00 01 :事务标识符
    • 00 00 :协议标识符(modbus 固定为 00 00)
    • 00 03 :后续字节数
    • 01 :从站地址
    • 86 :原功能码为 06,最高位为 1 表示异常响应
    • 02 :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的)
  • 向多个保持寄存器写入数据:

  • 请求报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][寄存器起始地址 寄存器数量 寄存器值字节数 寄存器值]

  • 举例:00 01 00 00 00 0B 01 10 00 00 00 02 04 00 01 00 05
    • 00 01 :事务标识符
    • 00 00 :协议标识符(modbus 固定为 00 00)
    • 00 0B :后续字节数
    • 01 :从站地址
    • 10 :写多个寄存器功能码
    • 00 00 :寄存器起始地址
    • 00 02 :要写入的寄存器数量
    • 04 :寄存器值的字节数
    • 00 01 :写入的第一个寄存器值
    • 00 05 :写入的第二个寄存器值
  • 响应报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][寄存器起始地址 寄存器数量]
  • 举例:00 01 00 00 00 06 01 10 00 00 00 02
    • 00 01 :事务标识符
    • 00 00 :协议标识符(modbus 固定为 00 00)
    • 00 06 :后续字节数
    • 01 :从站地址
    • 10 :写多个寄存器功能码
    • 00 00 :寄存器起始地址
    • 00 02 :要写入的寄存器数量
  • 异常响应报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][异常码]
  • 举例:00 01 00 00 00 03 01 90 02
    • 00 01 :事务标识符
    • 00 00 :协议标识符(modbus 固定为 00 00)
    • 00 03 :后续字节数
    • 01 :从站地址
    • 90 :原功能码为 10,最高位为 1 表示异常响应
    • 02 :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的)
  • 读取保持寄存器数据:

  • 请求报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][寄存器起始地址 寄存器数量]

  • 举例:00 01 00 00 00 06 01 03 00 00 00 02
    • 00 01 :事务标识符
    • 00 00 :协议标识符(modbus 固定为 00 00)
    • 00 06 :后续字节数
    • 01 :从站地址
    • 03 :读单/多个保持寄存器功能码
    • 00 00 :寄存器起始地址
    • 00 02 :要读取的寄存器数量
  • 响应报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][寄存器值字节数 寄存器值]
  • 举例:00 01 00 00 00 07 01 03 04 00 01 00 05
    • 00 01 :事务标识符
    • 00 00 :协议标识符(modbus 固定为 00 00)
    • 00 07 :后续字节数
    • 01 :从站地址
    • 03 :读单/多个保持寄存器功能码
    • 04 :寄存器值的字节数
    • 00 01 :读取的第一个寄存器值
    • 00 05 :读取的第二个寄存器值
  • 异常响应报文格式:[事务标识符 协议标识符 后续字节数 从站地址][功能码][异常码]
  • 举例:00 01 00 00 00 03 01 83 02
    • 00 01 :事务标识符
    • 00 00 :协议标识符(modbus 固定为 00 00)
    • 00 03 :后续字节数
    • 01 :从站地址
    • 83 :原功能码为 03,最高位为 1 表示异常响应
    • 02 :非法数据地址(请求中指定的数据地址是从站设备不允许的或不存在的)

1.3 Modbus 四种基本数据类型

注:Modbus 协议中通常将这四类称为“数据对象”(Data Objects),但在工程实践中常简称为“数据类型”,本文沿用此习惯以增强可读性。

Modbus 协议定义了四种基本类型(线圈、离散输入、输入寄存器、保持寄存器)。采取这样的划分是基于工业控制系统中常见的硬件接口特性、数据访问需求以及通信效率的综合考量。通过将数据按照功能、读写属性和物理意义进行分类,Modbus 在保持协议间接性的同时,可以有效映射真实设备的输入输出行为,为不同厂商设备之间的互操作性提供了清晰、一致的数据模型基础。

1.3.1 线圈(Coils)

  • 中文别名:数字输出、开关输出、DO(Digital Output)
  • 存储单元:单比特(1 bit)
  • 数值范围:两种状态:0(OFF)或 1(ON)
  • 访问权限:可读可写
  • 功能码:

  • 01(0x01):读单个或多个线圈

  • 05(0x05):写单个线圈
  • 15(0x15):写多个线圈
  • 典型用途:控制继电器、开关、指示灯等数字输出设备
  • 实际应用举例:

  • 控制继电器的吸合或断开

  • 控制电机的启动或停止
  • 控制阀门的开启或关闭
  • 控制指示灯的点亮或熄灭

1.3.2 离散输入(Discrete Inputs)

  • 中文别名:数字输入、开关输入、DI(Digital Input)
  • 存储单元:单比特(1 bit)
  • 数值范围:两种状态:0(OFF)或 1(ON)
  • 访问权限:只读
  • 功能码:

  • 02(0x02):读单个或多个离散输入

  • 典型用途:读取限位开关、按钮、传感器等数字输入状态
  • 实际应用举例:

  • 读取一个按钮是否被按下

  • 检测一个限位开关是否触发
  • 判断门磁传感器是否报警(门 开/关)
  • 查看故障报警信号的状态

1.3.3 输入寄存器(Input Registers)

  • 中文别名:模拟量输入、只读寄存器、AI(Analog Input)
  • 存储单元:16 位(2 bytes)
  • 数值范围:0 ~ 65535(无符号)或者 -32768 ~ 32767(有符号,非协议标准,需要主/从站设备配合实现)
  • 访问权限:只读
  • 功能码:

  • 04(0x04):读单个或多个输入寄存器

  • 典型用途:读取传感器温度、压力、电压等模拟量输入值
  • 实际应用举例:

  • 读取温度传感器的测量值(如 25.4℃)

  • 读取压力传感器的实际压力
  • 读取流量计的当前流量
  • 读取设备运行的累计时间(通常由设备内部维护,只供读取)

1.3.4 保持寄存器(Holding Registers)

  • 中文别名:模拟量输出、读写寄存器、AO(Analog Output)
  • 存储单元:16 位(2 bytes)
  • 数值范围:0 ~ 65535(无符号)或者 -32768 ~ 32767(有符号,非协议标准,需要主/从站设备配合实现)
  • 访问权限:可读可写
  • 功能码:

  • 03(0x03):读单个或多个保持寄存器

  • 06(0x06):写单个保持寄存器
  • 16(0x16):写多个保持寄存器
  • 典型用途:存储配置参数、设定值、设备状态等可被主站设备修改的数据
  • 实际应用举例:

  • 写入方面:设置目标温度、设定电机转速、修改报警阈值、发送控制命令代码

  • 读取方面:读取设备内部计算的参数、获取系统状态信息、读取预置的配方数据

1.3.5 数据类型一览表

数据类型(中文)
数据类型(英文)
存储单元
访问权限
功能码
典型用途
线圈
Coils
1 bit
可读可写
01(0x01):读单个或多个线圈
05(0x05):写单个线圈
15(0x15):写多个线圈
控制继电器、开关、指示灯等数字输出设备
离散输入
Discrete Inputs
1 bit
只读
02(0x02):读单个或多个离散输入
读取限位开关、按钮、传感器等数字输入状态
输入寄存器
Input Registers
16 bit
只读
04(0x04):读单个或多个输入寄存器
读取传感器温度、压力、电压等模拟量输入值
保持寄存器
Holding Registers
16 bit
可读可写
03(0x03):读单个或多个保持寄存器
06(0x06):写单个保持寄存器
16(0x16):写多个保持寄存器
存储配置参数、设定值、设备状态等可被主站设备修改的数据

1.4 常用基础功能码

部分功能码并非标配,具体需要看设备厂商是否支持。

功能名称
功能码 (十进制)
功能码 (十六进制)
描述
读取离散输入
02
02
读取只读的物理开关量输入点
读取线圈
01
01
读取可读可写的开关量输出点
写单个线圈
05
05
写入单个开关量输出点
写多个线圈
15
0F
写入多个连续的开关量输出点
读取输入寄存器
04
04
读取只读的模拟量输入或数据
读取保持寄存器
03
03
读取可读可写的模拟量输出或数据存储区
写单个寄存器
06
06
写入单个寄存器值
写多个寄存器
16
10
写入多个连续的寄存器值
读/写多个寄存器
23
17
在一个请求中同时执行读和写寄存器操作
屏蔽写寄存器
22
16
对寄存器进行“与”/“或”掩码操作,用于修改特定位

1.5 异常响应

异常响应时从站返回:响应码 + 异常码

报文格式:[响应码][异常码]

1.5.1 响应码

格式:0x80 + 功能码

举例:0x83(读保持寄存器异常响应码)

1.5.2 异常码

异常码 (十六进制)
名称
产生原因举例
01
非法功能
(ILLEGAL FUNCTION)

从站设备不支持请求报文中的功能码。例如,向一个只支持读保持寄存器(03)的传感器发送读线圈(01)命令。
02
非法数据地址
(ILLEGAL DATA ADDRESS)
请求中指定的数据地址是从站设备不允许的或不存在的。例如,试图读取一个起始地址为999的保持寄存器,但该设备只有0-99的寄存器。
03
非法数据值
(ILLEGAL DATA VALUE)
请求数据字段中的值对于从站设备来说是无效的。例如,在写多个寄存器(16)时,请求的寄存器数量为0,或者字节计数字节与后续数据不匹配。
04
服务器(从站)设备故障
(SERVER DEVICE FAILURE)
从站设备在处理请求的过程中发生了不可恢复的错误。这是一个通用错误,通常表示设备内部故障,例如存储器故障、软件异常等。
05
确认
(ACKNOWLEDGE)
从站设备已接受请求并正在处理中,但需要很长的处理时间。主站设备应稍后重新查询,而不是再次发送相同请求。
06
服务器(从站)设备忙
(SERVER DEVICE BUSY)
从站设备正忙于处理长时间命令。主站设备应当稍后重试相同的请求。
08
存储器奇偶校验错误
(MEMORY PARITY ERROR)
从站设备尝试读取记录文件,但检测到存储器中存在奇偶校验错误。主站设备可以重试请求,但从站设备可能需要进行检修。
0A
网关路径不可用
(GATEWAY PATH UNAVAILABLE)
通常与网关设备相关,表示网关配置错误或无法处理请求。
0B
网关目标设备响应失败
(GATEWAY TARGET DEVICE FAILED TO RESPOND)
网关无法从目标设备(即最终的被访问从站)获得响应。这通常意味着目标设备离线或存在通信问题。

二、演示功能概述

2.1 演示功能介绍

本篇文章主要演示 Modbus TCP 主站应用,演示功能如下:

1、将 Air780EPM 配置为 Modbus TCP 主站模式

2、与从站地址为 1 和 2 的从设备进行通信

  • 对从站地址为 1 的从设备进行每 2 秒一次的读取保持寄存器 0-1 操作
  • 对从站地址为 2 的从设备进行每 4 秒一次的写入保持寄存器 0-1 操作

2.2 注意事项

1、本文章中所使用的示例程序需要搭配 exmodbus 扩展库使用,扩展库会在下方进行简单介绍

2、在 main.lua 代码文件中 require "param_field" 模块时,可以演示标准 Modbus TCP 请求报文格式的使用方式

3、在 main.lua 代码文件中 require "raw_frame" 模块时,可以演示非标准 Modbus TCP 请求报文格式的使用方式

4、require "param_field" 和 require "raw_frame" 不要同时打开,否则功能会有冲突

2.3 特别说明

关于 TCP 报文,exmodbus 扩展库支持通过 字段参数 或 原始帧 两种方式进行配置

这两种配置方式本质都由用户将其放入 table 中在调用接口时传入,区别如下:

1、字段参数方式

这种方式需要用户将请求报文进行解析后,将其放入 table 中,例如:

读取请求:
local config = {
    slave_id = 1,                         -- 从站地址
    reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
    start_addr = 0x0000,                  -- 寄存器起始地址
    reg_count = 0x0002,                   -- 寄存器数量
    timeout = 1000                        -- 超时时间
}

写入请求:
local config = {
    slave_id = 2,                         -- 从站地址
    reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
    start_addr = 0x0000,                  -- 寄存器起始地址
    reg_count = 0x0002,                   -- 寄存器数量
    data = {
        [start_addr] = 0x0012,            -- 寄存器 0 的值
        [start_addr + 1] = 0x0034,        -- 寄存器 1 的值
    }
    force_multiple = true, -- 是否强制使用多个寄存器写入操作(写多个线圈功能码:0x0F;写多个寄存器功能码:0x10)
    timeout = 1000                        -- 超时时间
}

2、原始帧方式

这种方式只需要用户将原始请求报文放入 table 中,例如:

读取请求:
local config = {
    raw_request = string.char(
        0x00, 0x01, -- 事务标识符
        0x00, 0x00, -- 协议标识符
        0x00, 0x06, -- 长度
        0x01,       -- 单元标识符(从站地址)
        0x03,       -- 功能码:读取保持寄存器
        0x00, 0x00, -- 寄存器起始地址
        0x00, 0x02  -- 寄存器数量
    ),
    timeout = 1000  -- 超时时间 1000 ms
}

写入请求:
local config = {
    raw_request = string.char(
        0x00, 0x02, -- 事务标识符
        0x00, 0x00, -- 协议标识符
        0x00, 0x0B, -- 长度
        0x02,       -- 单元标识符(从站地址)
        0x10,       -- 功能码:写入保持寄存器
        0x00, 0x00, -- 寄存器起始地址
        0x00, 0x02, -- 寄存器数量
        0x04,       -- 字节数量
        0x00, 0x12, -- 寄存器 0 的值
        0x00, 0x34  -- 寄存器 1 的值
    ),
    timeout = 1000  -- 超时时间 1000 ms
}

如果你需要发送的请求报文是符合 modbus TCP 标准格式,可以使用 字段参数 或者 原始帧 方式

如果你需要发送的请求报文是非标准格式,必须使用 原始帧 方式,使用 字段参数 方式会导致解析的数据不正确

三、演示硬件环境

1、Air780EPM V1.3 开发板一块

2、TYPE-C USB 数据线一根

3、网线两根(一根开发板使用,一根电脑使用)

四、演示软件环境

1. 烧录工具:Luatools 下载调试工具

2. 本demo开发测试时使用的固件为LuatOS-SoC_V2018_Air780EPM 1号固件,本demo对固件版本没有什么特殊要求,所以你如果要测试本demo时,可以直接使用最新版本的内核固件Air780EPM固件Air780EHM固件;如果发现最新版本的内核固件测试有问题,可以使用我们开发本demo时使用的内核固件版本来对比测试;

3. 脚本文件:Air780EPM 脚本文件

4. 模拟工具:摩尔信使(MThings)官网(用于模拟 modbus 从站设备)

5. LuatOS 运行所需要的 lib 文件:使用 Luatools 烧录时,勾选 添加默认 lib 选项,使用默认 lib 脚本文件。

准备好软件环境之后,接下来查看 Air780EPM V1.3 开发板使用说明,将本篇文章中演示使用的项目文件烧录到 Air780EPM V1.3 开发板中。

五、API 接口说明

exmodbus 扩展库说明:https://docs.openluat.com/osapi/ext/exmodbus/

六、代码示例介绍

6.1 各模块代码解析说明

6.1.1 TCP 主站应用模块(字段参数方式,对应 param_field.lua)

1. 调用 exmodbus 扩展库

说明:搭配扩展库使用时需要 require 对应的库文件名,否则在运行时会出现错误

local exmodbus = require("exmodbus")

2. 配置创建主站所需要的参数

本文章演示使用 Modbus TCP 主站,通信模式应固定为 exmodbus.TCP_MASTER; 网卡 ID 可以与从站不一致,只需要能够连上服务器进行通信即可; IP 地址和端口号填的是需要连接的服务器 IP 地址和端口号,不要填写错误,否则会连接失败;

-- 创建 TCP 主站配置参数
-- 说明:创建 TCP 主站时只需要配置如下参数即可
local create_config = {
    -- 网络参数配置
    mode = exmodbus.TCP_MASTER,   -- 通信模式:TCP主站
    adapter = socket.LWIP_ETH,    -- 网卡 ID:LwIP 协议栈的以太网卡
    ip_address = "192.168.1.100", -- 从站 IP 地址
    port = 6000,                  -- 从站端口号
}

3. 配置主站向从站 1 进行读取请求的请求参数

定义 slave1_data 用于读取成功后记录读取的数值; 在配置读命令的字段参数时要符合 Modbus 协议规范,不可超出范围、不可请求无效地址等等,若参数配置不正确时扩展库也会输出错误日志进行提醒; 超时时间参数是可选项,不填时默认为 1000 毫秒(1 秒)。

-- 初始化从站 1 数据结构
-- 用于记录从站 1 保持寄存器 0-1 的值
local slave1_data = {}

-- 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数;
local read_config = {
    slave_id = 1,                         -- 从站地址 1
    reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
    start_addr = 0x0000,                  -- 起始地址 0
    reg_count = 0x0002,                   -- 读取 2 个寄存器
    timeout = 1000                        -- 超时时间 1000 ms
}

4. 配置主站向从站 2 进行写入请求的请求参数

定义 slave2_data 用于模拟向从站 2 写入的数值; 在配置写命令的字段参数时要符合 Modbus 协议规范,不可超出范围、不可请求无效地址等等,若参数配置不正确时扩展库也会输出错误日志进行提醒; data 字段参数是一个数组类型,其内部通过键值对的方式进行存储,每一个键值对代表一个数据类型的数值,每一个键表示一个数据类型的地址,例如写入保持寄存器 0-1,则 data 中有两个键值对,这两个键值对的键分别为 0 和 1; force_multiple 字段参数可用于控制向从站 2 发送写入请求时报文中的功能码,如果从站设备不支持 05(写单个线圈)或 06(写单个保持寄存器)功能码时,可以将 force_multiple 设置为 true,此时 exmodbus 扩展库在组包时便会使用 0F(写单个/多个线圈)或 10(写单个/多个保持寄存器)功能码; 超时时间参数是可选项,不填时默认为 1000 毫秒(1 秒)。

-- 初始化从站 2 数据结构;
local slave2_data = {
    data1 = 123,
    data2 = 456
}

-- 定义从站 2 保持寄存器的起始地址;
local start_addr = 0x0000

-- 写入从站 2 保持寄存器 0-1 的值时,配置写命令的字段参数;
local write_config = {
    slave_id = 2,                                            -- 从站地址 2
    reg_type = exmodbus.HOLDING_REGISTER,                    -- 寄存器类型:保持寄存器
    start_addr = start_addr,                                 -- 起始地址 0
    reg_count = 0x0002,                                      -- 写入 2 个寄存器
    data = {
        [start_addr] = slave2_data.data1,                    -- 第一个寄存器值
        [start_addr + 1] = slave2_data.data2,                -- 第二个寄存器值
    },                                                       -- 写入寄存器值
    force_multiple = true,                                   -- 强制使用写多个功能码
                                                                -- 设置为 true 时,写单个或多个线圈时强制功能码为 0x0F,写单个或多个保持寄存器时强制功能码为 0x10
                                                                -- 设置为 false 时,写单个线圈时功能码为 0x05,写单个保持寄存器时功能码为 0x06,写多个线圈时功能码为 0x0F,写多个保持寄存器时功能码为 0x10
    timeout = 1000                                           -- 超时时间 1000 ms
}

5. 创建 Modbus TCP 主站

调用 create 接口时应判断创建结果,防止后续调用无效实例对象。

-- 创建 TCP 主站实例
local tcp_master = exmodbus.create(create_config)

-- 判断主站是否创建成功并记录日志
if not tcp_master then
    log.info("exmodbus_test", "tcp_master 创建失败")
else
    log.info("exmodbus_test", "tcp_master 创建成功")
end

6. 配置读取函数

读取成功后的数值会存在 read_result.data 字段参数内,该字段参数内也是通过键值对的方式存储数值,其中键为对应数据类型的地址,例如读取保持寄存器 0,则键为 0,值为该数据类型的数值。

-- 读取从站 1 保持寄存器数据的函数
local function read_slave1_holding_registers()

    log.info("exmodbus_test", "开始读取从站 1 保持寄存器 0-1 的值")

    -- 执行读取操作
    local read_result = tcp_master:read(read_config)

    -- 根据返回状态处理结果
    if read_result.status == exmodbus.STATUS_SUCCESS then
        slave1_data.data1 = read_result.data[read_config.start_addr]
        slave1_data.data2 = read_result.data[read_config.start_addr + 1]
        log.info("exmodbus_test", "成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为", slave1_data.data1, ",寄存器 1 数值为", slave1_data.data2)
    elseif read_result.status == exmodbus.STATUS_DATA_INVALID then
        log.info("exmodbus_test", "收到从站 1 的响应数据但数据损坏/校验失败")
    elseif read_result.status == exmodbus.STATUS_EXCEPTION then
        log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
    elseif read_result.status == exmodbus.STATUS_TIMEOUT then
        log.info("exmodbus_test", "未收到从站 1 的响应(超时)")
    end
end

7. 配置写入函数

-- 写入从站 2 保持寄存器数据的函数
local function write_slave2_holding_registers()

    log.info("exmodbus_test", "开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为", slave2_data.data1, ",寄存器 1 数值为", slave2_data.data2)

    -- 执行写入操作
    local write_result = tcp_master:write(write_config)

    -- 根据返回状态处理结果
    if write_result.status == exmodbus.STATUS_SUCCESS then
        log.info("exmodbus_test", "成功写入从站 2 保持寄存器 0-1 的值")
    elseif write_result.status == exmodbus.STATUS_DATA_INVALID then
        log.info("exmodbus_test", "收到从站 2 的响应数据但数据损坏/校验失败")
    elseif write_result.status == exmodbus.STATUS_EXCEPTION then
        log.info("exmodbus_test", "收到从站 2 的 modbus 标准异常响应,异常码为", write_result.execption_code)
    elseif write_result.status == exmodbus.STATUS_TIMEOUT then
        log.info("exmodbus_test", "未收到从站 2 的响应(超时)")
    end
end

8. 配置定时任务函数并进行初始化

示例代码中设计为每 2 秒执行一次读取操作,每 4 秒执行一次写入操作。

-- 定时任务函数:每 2 秒调用一次读取函数,每 4 秒调用一次写入函数
local function task()

    local count = 0 -- 计数器

    while true do
        if tcp_master then
            -- 每 2 秒调用一次读取函数
            read_slave1_holding_registers()
            if count == 0 then
                -- 每 4 秒调用一次写入函数
                write_slave2_holding_registers()
            end
            count = (count + 1) % 2
        else
            log.info("exmodbus_test", "tcp_master 未创建,无法执行 read_slave1_holding_registers()")
        end
        sys.wait(2000)
    end
end

-- 初始化任务
sys.taskInit(task)

6.1.2 TCP 主站应用模块(原始帧方式,对应 raw_frame.lua)

1. 调用 exmodbus 扩展库

说明:搭配扩展库使用时需要 require 对应的库文件名,否则在运行时会出现错误

local exmodbus = require("exmodbus")

2. 配置创建主站所需要的参数

本文章演示使用 Modbus TCP 主站,通信模式应固定为 exmodbus.TCP_MASTER; 网卡 ID 可以与从站的不一致,只需要能够连上服务器进行通信即可; IP 地址和端口号填的是需要连接的服务器 IP 地址和端口号,不要填写错误,否则会连接失败;

-- 创建 TCP 主站配置参数
-- 说明:创建 TCP 主站时只需要配置如下参数即可
local create_config = {
    -- 网络参数配置
    mode = exmodbus.TCP_MASTER,   -- 通信模式:TCP主站
    adapter = socket.LWIP_ETH,    -- 网卡 ID:LwIP 协议栈的以太网卡
    ip_address = "192.168.1.100", -- 从站 IP 地址
    port = 6000,                  -- 从站端口号
}

3. 配置主站向从站 1 进行读取请求的请求参数

定义 slave1_data 用于读取成功后记录读取的数值; raw_request 字段参数用于填写读取请求时的原始报文帧,在填写时要符合 Modbus TCP 报文协议规范,exmodbus 扩展库只负责执行请求操作,并将请求到的原始报文帧进行返回,用户需要自己在脚本代码中进行解析判断; 超时时间参数是可选项,不填时默认为 1000 毫秒(1 秒)。

-- 初始化从站 1 数据结构
-- 用于记录从站 1 保持寄存器 0-1 的值;
local slave1_data = {}

-- 配置读取从站 1 保持寄存器 0-1 的值;
local read_config = {
    raw_request = string.char(
        0x00, 0x01, -- 事务标识符
        0x00, 0x00, -- 协议标识符
        0x00, 0x06, -- 长度
        0x01,       -- 单元标识符(从站地址)
        0x03,       -- 功能码:读取保持寄存器
        0x00, 0x00, -- 寄存器起始地址
        0x00, 0x02  -- 寄存器数量
    ),
    timeout = 1000  -- 超时时间 1000 ms
}

4. 配置主站向从站 2 进行写入请求的请求参数

raw_request 字段参数用于填写写入请求时的原始报文帧,在填写时要符合 Modbus TCP 报文协议规范,exmodbus 扩展库只负责执行请求操作,并将请求到的原始报文帧进行返回,用户需要自己在脚本代码中进行解析判断; 超时时间参数是可选项,不填时默认为 1000 毫秒(1 秒)。

-- 配置写入从站 2 保持寄存器 0-1 的值;
local write_config = {
    raw_request = string.char(
        0x00, 0x02, -- 事务标识符
        0x00, 0x00, -- 协议标识符
        0x00, 0x0B, -- 长度
        0x02,       -- 单元标识符(从站地址)
        0x10,       -- 功能码:写入保持寄存器
        0x00, 0x00, -- 寄存器起始地址
        0x00, 0x02, -- 寄存器数量
        0x04,       -- 字节数量
        0x00, 0x12, -- 寄存器 0 的值
        0x00, 0x34  -- 寄存器 1 的值
    ),
    timeout = 1000  -- 超时时间 1000 ms
}

5. 创建 Modbus TCP 主站

调用 create 接口时应判断创建结果,防止后续调用无效实例对象。

-- 创建 TCP 主站实例
local tcp_master = exmodbus.create(create_config)

-- 判断主站是否创建成功并记录日志
if not tcp_master then
    log.info("exmodbus_test", "tcp_master 创建失败")
else
    log.info("exmodbus_test", "tcp_master 创建成功")
end

6. 配置读取函数

读取成功后的原始报文帧会存在 read_result.raw_response 字段参数内,用户需要自己在脚本代码中做解析。

-- 读取从站 1 保持寄存器数据的函数
local function read_slave1_holding_registers()
    log.info("exmodbus_test", "开始读取从站 1 保持寄存器 0-1 的值")

    -- 执行读取操作
    local read_result = tcp_master:read(read_config)

    -- 根据返回状态处理结果
    if read_result.status == exmodbus.STATUS_SUCCESS then
        local resp = read_result.raw_response

        -- 特别说明:
        -- 接下来的判断是针对 modbus TCP 标准响应格式的应答原始帧来解析的
        -- 在实际项目中,应根据自己项目中的实际应答原始帧格式进行解析
        -- 如果实际格式与此处演示的格式不一致,需要修改接下来的解析代码

        -- 1. 检查总长度:必须为 13 字节(7 MBAP头 + 1 功能码 + 1 字节数 + 4 数据)
        if #resp ~= 13 then
            log.info("exmodbus_test", "响应长度错误,期望 13 字节,实际:", #resp)
            return
        end

        -- 2. 检查事务标识符是否与请求一致
        local req_trans_id = string.unpack(">I2", read_config.raw_request, 1)
        local resp_trans_id = string.unpack(">I2", resp, 1)
        if req_trans_id ~= resp_trans_id then
            log.info("exmodbus_test", "事务标识符不一致,期望:", req_trans_id, "实际:", resp_trans_id)
            return
        end

        -- 3. 检查协议标识符是否为 0x0000
        if string.unpack(">I2", resp, 3) ~= 0x0000 then
            log.info("exmodbus_test", "协议标识符错误,期望 0x0000,实际:", string.unpack(">I2", resp, 3))
            return
        end

        -- 4. 检查单元标识符(从站地址)是否与请求一致
        local req_unit_id = string.byte(read_config.raw_request, 7)
        local resp_unit_id = string.byte(resp, 7)
        if req_unit_id ~= resp_unit_id then
            log.info("exmodbus_test", "单元标识符不一致,期望:", req_unit_id, "实际:", resp_unit_id)
            return
        end

        -- 5. 检查功能码是否与请求一致
        local req_func_code = string.byte(read_config.raw_request, 8)
        local resp_func_code = string.byte(resp, 8)
        if req_func_code ~= resp_func_code then
            log.info("exmodbus_test", "功能码不一致,期望:", req_func_code, "实际:", resp_func_code)
            return
        end

        -- 6. 检查字节数字段是否正确
        local byte_count = string.byte(resp, 9)
        if byte_count ~= 4 then
            log.info("exmodbus_test", "字节数字段错误,期望 4 字节,实际:", byte_count)
            return
        end

        -- 7. 解析寄存器数据(从第 10 字节开始,大端序)
        local data1 = string.unpack(">I2", resp, 10) -- 寄存器 0,偏移 10
        local data2 = string.unpack(">I2", resp, 12) -- 寄存器 1,偏移 12

        -- 8. 记录数据
        slave1_data[0] = data1
        slave1_data[1] = data2

        -- 9. 记录日志
        log.info("exmodbus_test", "成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为", slave1_data[0], ",寄存器 1 数值为", slave1_data[1])
    elseif read_result.status == exmodbus.STATUS_TIMEOUT then
        log.info("exmodbus_test", "未收到从站 1 的响应(超时)")
    elseif read_result.status == exmodbus.STATUS_PARAM_INVALID then
        log.info("exmodbus_test", "读取从站 1 参数无效")
    end
end

7. 配置写入函数

写入成功后的原始报文帧会存在 read_result.raw_response 字段参数内,用户需要自己在脚本代码中做解析。

-- 写入从站 2 保持寄存器数据的函数
local function write_slave2_holding_registers()

    log.info("exmodbus_test", "开始写入从站 2 保持寄存器 0-1 的值")

    -- 执行写入操作
    local write_result = tcp_master:write(write_config)

    -- 根据返回状态处理结果
    if write_result.status == exmodbus.STATUS_SUCCESS then
        local resp = write_result.raw_response

        -- 特别说明:
        -- 接下来的判断是针对 modbus TCP 标准响应格式的应答原始帧来解析的
        -- 在实际项目中,应根据自己项目中的实际应答原始帧格式进行解析
        -- 如果实际格式与此处演示的格式不一致,需要修改接下来的解析代码

        -- 1. 检查总长度:必须为 12 字节(7 MBAP头 + 1 功能码 + 2 起始地址 + 2 寄存器数量)
        if #resp ~= 12 then
            log.info("exmodbus_test", "响应长度错误,期望 12 字节,实际:", #resp)
            return
        end

        -- 2. 检查事务标识符是否与请求一致
        local req_trans_id = string.unpack(">I2", write_config.raw_request, 1)
        local resp_trans_id = string.unpack(">I2", resp, 1)
        if req_trans_id ~= resp_trans_id then
            log.info("exmodbus_test", "事务标识符不一致,期望:", req_trans_id, "实际:", resp_trans_id)
            return
        end

        -- 3. 检查协议标识符是否为 0x0000
        if string.unpack(">I2", resp, 3) ~= 0x0000 then
            log.info("exmodbus_test", "协议标识符错误,期望 0x0000,实际:", string.unpack(">I2", resp, 3))
            return
        end

        -- 4. 检查单元标识符(从站地址)是否与请求一致
        local req_unit_id = string.byte(write_config.raw_request, 7)
        local resp_unit_id = string.byte(resp, 7)
        if req_unit_id ~= resp_unit_id then
            log.info("exmodbus_test", "单元标识符不一致,期望:", req_unit_id, "实际:", resp_unit_id)
            return
        end

        -- 5. 检查功能码是否与请求一致
        local req_func_code = string.byte(write_config.raw_request, 8)
        local resp_func_code = string.byte(resp, 8)
        if req_func_code ~= resp_func_code then
            log.info("exmodbus_test", "功能码不一致,期望:", req_func_code, "实际:", resp_func_code)
            return
        end

        -- 6. 检查起始地址是否与请求一致
        local req_start_addr = string.unpack(">I2", write_config.raw_request, 9)
        local resp_start_addr = string.unpack(">I2", resp, 9)
        if req_start_addr ~= resp_start_addr then
            log.info("exmodbus_test", "起始地址不一致,期望:", req_start_addr, "实际:", resp_start_addr)
            return
        end

        -- 7. 检查寄存器数量是否与请求一致
        local req_reg_count = string.unpack(">I2", write_config.raw_request, 11)
        local resp_reg_count = string.unpack(">I2", resp, 11)
        if req_reg_count ~= resp_reg_count then
            log.info("exmodbus_test", "寄存器数量不一致,期望:", req_reg_count, "实际:", resp_reg_count)
            return
        end

        log.info("exmodbus_test", "成功写入从站 2 保持寄存器 0-1")
    elseif write_result.status == exmodbus.STATUS_TIMEOUT then
        log.info("exmodbus_test", "未收到从站 2 的响应(超时)")
    elseif write_result.status == exmodbus.STATUS_PARAM_INVALID then
        log.info("exmodbus_test", "写入从站 2 参数无效")
    end
end

8. 配置定时任务函数并进行初始化

示例代码中设计为每 2 秒执行一次读取操作,每 4 秒执行一次写入操作。

-- 定时任务函数:每 2 秒调用一次读取函数,每 4 秒调用一次写入函数
local function task()

    local count = 0 -- 计数器

    while true do
        if tcp_master then
            -- 每 2 秒调用一次读取函数
            read_slave1_holding_registers()
            if count == 0 then
                -- 每 4 秒调用一次写入函数
                write_slave2_holding_registers()
            end
            count = (count + 1) % 2
        else
            log.info("exmodbus_test", "tcp_master 未创建,无法执行 read_slave1_holding_registers()")
        end
        sys.wait(2000)
    end
end

-- 初始化任务
sys.taskInit(task)

6.2 完整示例代码展示

6.2.1 TCP 主站应用模块(字段参数方式,对应 param_field.lua)

--[[
@module  param_field
@summary TCP 主站应用模块(字段参数方式)
@version 1.0
@date    2025.12.30
@author  马梦阳
@usage
本功能模块演示的内容为:
1、将设备配置为 modbus TCP 主站模式
2、与从站 1 和 从站 2 进行通信
    1. 对从站 1 进行 2 秒一次的读取保持寄存器 0-1 操作
    2. 对从站 2 进行 4 秒一次的写入保持寄存器 0-1 操作

注意事项:
1、该示例程序需要搭配 exmodbus 扩展库使用
2、本功能模块适合使用标准 modbus TCP 请求报文格式的用户
3、如果你使用的是非标准 modbus TCP 请求报文格式,请参考 raw_frame 功能模块

本文件没有对外接口,直接在 main.lua 中 require "param_field" 就可以加载运行;
]]

local exmodbus = require("exmodbus")

-- 创建 TCP 主站配置参数
-- 说明:创建 TCP 主站时只需要配置如下参数即可
local create_config = {
    -- 网络参数配置
    mode = exmodbus.TCP_MASTER,   -- 通信模式:TCP主站
    adapter = socket.LWIP_ETH,    -- 网卡 ID:LwIP 协议栈的以太网卡
    ip_address = "192.168.1.100", -- 从站 IP 地址
    port = 6000,                  -- 从站端口号
}

-- 初始化从站 1 数据结构
-- 用于记录从站 1 保持寄存器 0-1 的值
local slave1_data = {}

-- 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数;
local read_config = {
    slave_id = 1,                         -- 从站地址 1
    reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
    start_addr = 0x0000,                  -- 起始地址 0
    reg_count = 0x0002,                   -- 读取 2 个寄存器
    timeout = 1000                        -- 超时时间 1000 ms
}

-- 初始化从站 2 数据结构;
local slave2_data = {
    data1 = 123,
    data2 = 456
}

-- 定义从站 2 保持寄存器的起始地址;
local start_addr = 0x0000

-- 写入从站 2 保持寄存器 0-1 的值时,配置写命令的字段参数;
local write_config = {
    slave_id = 2,                                            -- 从站地址 2
    reg_type = exmodbus.HOLDING_REGISTER,                    -- 寄存器类型:保持寄存器
    start_addr = start_addr,                                 -- 起始地址 0
    reg_count = 0x0002,                                      -- 写入 2 个寄存器
    data = {
        [start_addr] = slave2_data.data1,                    -- 第一个寄存器值
        [start_addr + 1] = slave2_data.data2,                -- 第二个寄存器值
    },                                                       -- 写入寄存器值
    force_multiple = true,                                   -- 强制使用写多个功能码
                                                                -- 设置为 true 时,写单个或多个线圈时强制功能码为 0x0F,写单个或多个保持寄存器时强制功能码为 0x10
                                                                -- 设置为 false 时,写单个线圈时功能码为 0x05,写单个保持寄存器时功能码为 0x06,写多个线圈时功能码为 0x0F,写多个保持寄存器时功能码为 0x10
    timeout = 1000                                           -- 超时时间 1000 ms
}

-- 创建 TCP 主站实例
local tcp_master = exmodbus.create(create_config)

-- 判断主站是否创建成功并记录日志
if not tcp_master then
    log.info("exmodbus_test", "tcp_master 创建失败")
else
    log.info("exmodbus_test", "tcp_master 创建成功")
end

-- 读取从站 1 保持寄存器数据的函数
local function read_slave1_holding_registers()

    log.info("exmodbus_test", "开始读取从站 1 保持寄存器 0-1 的值")

    -- 执行读取操作
    local read_result = tcp_master:read(read_config)

    -- 根据返回状态处理结果
    if read_result.status == exmodbus.STATUS_SUCCESS then
        slave1_data.data1 = read_result.data[read_config.start_addr]
        slave1_data.data2 = read_result.data[read_config.start_addr + 1]
        log.info("exmodbus_test", "成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为", slave1_data.data1, ",寄存器 1 数值为", slave1_data.data2)
    elseif read_result.status == exmodbus.STATUS_DATA_INVALID then
        log.info("exmodbus_test", "收到从站 1 的响应数据但数据损坏/校验失败")
    elseif read_result.status == exmodbus.STATUS_EXCEPTION then
        log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
    elseif read_result.status == exmodbus.STATUS_TIMEOUT then
        log.info("exmodbus_test", "未收到从站 1 的响应(超时)")
    end
end

-- 写入从站 2 保持寄存器数据的函数
local function write_slave2_holding_registers()

    log.info("exmodbus_test", "开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为", slave2_data.data1, ",寄存器 1 数值为", slave2_data.data2)

    -- 执行写入操作
    local write_result = tcp_master:write(write_config)

    -- 根据返回状态处理结果
    if write_result.status == exmodbus.STATUS_SUCCESS then
        log.info("exmodbus_test", "成功写入从站 2 保持寄存器 0-1 的值")
    elseif write_result.status == exmodbus.STATUS_DATA_INVALID then
        log.info("exmodbus_test", "收到从站 2 的响应数据但数据损坏/校验失败")
    elseif write_result.status == exmodbus.STATUS_EXCEPTION then
        log.info("exmodbus_test", "收到从站 2 的 modbus 标准异常响应,异常码为", write_result.execption_code)
    elseif write_result.status == exmodbus.STATUS_TIMEOUT then
        log.info("exmodbus_test", "未收到从站 2 的响应(超时)")
    end
end

-- 定时任务函数:每 2 秒调用一次读取函数,每 4 秒调用一次写入函数
local function task()

    local count = 0 -- 计数器

    while true do
        if tcp_master then
            -- 每 2 秒调用一次读取函数
            read_slave1_holding_registers()
            if count == 0 then
                -- 每 4 秒调用一次写入函数
                write_slave2_holding_registers()
            end
            count = (count + 1) % 2
        else
            log.info("exmodbus_test", "tcp_master 未创建,无法执行 read_slave1_holding_registers()")
        end
        sys.wait(2000)
    end
end

-- 初始化任务
sys.taskInit(task)

6.2.2 TCP 主站应用模块(原始帧方式,对应 raw_frame.lua)

--[[
@module  raw_frame
@summary TCP 主站应用模块(原始帧方式)
@version 1.0
@date    2025.12.30
@author  马梦阳
@usage
本功能模块演示的内容为:
1、将设备配置为 modbus TCP 主站模式
2、与从站 1 和 从站 2 进行通信
    1. 对从站 1 进行 2 秒一次的读取保持寄存器 0-1 操作
    2. 对从站 2 进行 4 秒一次的写入保持寄存器 0-1 操作

注意事项:
1、该示例程序需要搭配 exmodbus 扩展库使用
2、本功能模块只适合使用非标准 modbus TCP 请求报文格式的用户
3、如果你使用的是标准 modbus TCP 请求报文格式,请参考 param_field 功能模块

本文件没有对外接口,直接在 main.lua 中 require "raw_frame" 就可以加载运行;
]]

local exmodbus = require("exmodbus")

-- 创建 TCP 主站配置参数
-- 说明:创建 TCP 主站时只需要配置如下参数即可
local create_config = {
    -- 网络参数配置
    mode = exmodbus.TCP_MASTER,   -- 通信模式:TCP主站
    adapter = socket.LWIP_ETH,    -- 网卡 ID:LwIP 协议栈的以太网卡
    ip_address = "192.168.1.100", -- 从站 IP 地址
    port = 6000,                  -- 从站端口号
}

-- 初始化从站 1 数据结构
-- 用于记录从站 1 保持寄存器 0-1 的值;
local slave1_data = {}

-- 配置读取从站 1 保持寄存器 0-1 的值;
local read_config = {
    raw_request = string.char(
        0x00, 0x01, -- 事务标识符
        0x00, 0x00, -- 协议标识符
        0x00, 0x06, -- 长度
        0x01,       -- 单元标识符(从站地址)
        0x03,       -- 功能码:读取保持寄存器
        0x00, 0x00, -- 寄存器起始地址
        0x00, 0x02  -- 寄存器数量
    ),
    timeout = 1000  -- 超时时间 1000 ms
}

-- 配置写入从站 2 保持寄存器 0-1 的值;
local write_config = {
    raw_request = string.char(
        0x00, 0x02, -- 事务标识符
        0x00, 0x00, -- 协议标识符
        0x00, 0x0B, -- 长度
        0x02,       -- 单元标识符(从站地址)
        0x10,       -- 功能码:写入保持寄存器
        0x00, 0x00, -- 寄存器起始地址
        0x00, 0x02, -- 寄存器数量
        0x04,       -- 字节数量
        0x00, 0x12, -- 寄存器 0 的值
        0x00, 0x34  -- 寄存器 1 的值
    ),
    timeout = 1000  -- 超时时间 1000 ms
}

-- 创建 TCP 主站实例
local tcp_master = exmodbus.create(create_config)

-- 判断主站是否创建成功并记录日志
if not tcp_master then
    log.info("exmodbus_test", "tcp_master 创建失败")
else
    log.info("exmodbus_test", "tcp_master 创建成功")
end

-- 读取从站 1 保持寄存器数据的函数
local function read_slave1_holding_registers()
    log.info("exmodbus_test", "开始读取从站 1 保持寄存器 0-1 的值")

    -- 执行读取操作
    local read_result = tcp_master:read(read_config)

    -- 根据返回状态处理结果
    if read_result.status == exmodbus.STATUS_SUCCESS then
        local resp = read_result.raw_response

        -- 特别说明:
        -- 接下来的判断是针对 modbus TCP 标准响应格式的应答原始帧来解析的
        -- 在实际项目中,应根据自己项目中的实际应答原始帧格式进行解析
        -- 如果实际格式与此处演示的格式不一致,需要修改接下来的解析代码

        -- 1. 检查总长度:必须为 13 字节(7 MBAP头 + 1 功能码 + 1 字节数 + 4 数据)
        if #resp ~= 13 then
            log.info("exmodbus_test", "响应长度错误,期望 13 字节,实际:", #resp)
            return
        end

        -- 2. 检查事务标识符是否与请求一致
        local req_trans_id = string.unpack(">I2", read_config.raw_request, 1)
        local resp_trans_id = string.unpack(">I2", resp, 1)
        if req_trans_id ~= resp_trans_id then
            log.info("exmodbus_test", "事务标识符不一致,期望:", req_trans_id, "实际:", resp_trans_id)
            return
        end

        -- 3. 检查协议标识符是否为 0x0000
        if string.unpack(">I2", resp, 3) ~= 0x0000 then
            log.info("exmodbus_test", "协议标识符错误,期望 0x0000,实际:", string.unpack(">I2", resp, 3))
            return
        end

        -- 4. 检查单元标识符(从站地址)是否与请求一致
        local req_unit_id = string.byte(read_config.raw_request, 7)
        local resp_unit_id = string.byte(resp, 7)
        if req_unit_id ~= resp_unit_id then
            log.info("exmodbus_test", "单元标识符不一致,期望:", req_unit_id, "实际:", resp_unit_id)
            return
        end

        -- 5. 检查功能码是否与请求一致
        local req_func_code = string.byte(read_config.raw_request, 8)
        local resp_func_code = string.byte(resp, 8)
        if req_func_code ~= resp_func_code then
            log.info("exmodbus_test", "功能码不一致,期望:", req_func_code, "实际:", resp_func_code)
            return
        end

        -- 6. 检查字节数字段是否正确
        local byte_count = string.byte(resp, 9)
        if byte_count ~= 4 then
            log.info("exmodbus_test", "字节数字段错误,期望 4 字节,实际:", byte_count)
            return
        end

        -- 7. 解析寄存器数据(从第 10 字节开始,大端序)
        local data1 = string.unpack(">I2", resp, 10) -- 寄存器 0,偏移 10
        local data2 = string.unpack(">I2", resp, 12) -- 寄存器 1,偏移 12

        -- 8. 记录数据
        slave1_data[0] = data1
        slave1_data[1] = data2

        -- 9. 记录日志
        log.info("exmodbus_test", "成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为", slave1_data[0], ",寄存器 1 数值为", slave1_data[1])
    elseif read_result.status == exmodbus.STATUS_TIMEOUT then
        log.info("exmodbus_test", "未收到从站 1 的响应(超时)")
    elseif read_result.status == exmodbus.STATUS_PARAM_INVALID then
        log.info("exmodbus_test", "读取从站 1 参数无效")
    end
end

-- 写入从站 2 保持寄存器数据的函数
local function write_slave2_holding_registers()

    log.info("exmodbus_test", "开始写入从站 2 保持寄存器 0-1 的值")

    -- 执行写入操作
    local write_result = tcp_master:write(write_config)

    -- 根据返回状态处理结果
    if write_result.status == exmodbus.STATUS_SUCCESS then
        local resp = write_result.raw_response

        -- 特别说明:
        -- 接下来的判断是针对 modbus TCP 标准响应格式的应答原始帧来解析的
        -- 在实际项目中,应根据自己项目中的实际应答原始帧格式进行解析
        -- 如果实际格式与此处演示的格式不一致,需要修改接下来的解析代码

        -- 1. 检查总长度:必须为 12 字节(7 MBAP头 + 1 功能码 + 2 起始地址 + 2 寄存器数量)
        if #resp ~= 12 then
            log.info("exmodbus_test", "响应长度错误,期望 12 字节,实际:", #resp)
            return
        end

        -- 2. 检查事务标识符是否与请求一致
        local req_trans_id = string.unpack(">I2", write_config.raw_request, 1)
        local resp_trans_id = string.unpack(">I2", resp, 1)
        if req_trans_id ~= resp_trans_id then
            log.info("exmodbus_test", "事务标识符不一致,期望:", req_trans_id, "实际:", resp_trans_id)
            return
        end

        -- 3. 检查协议标识符是否为 0x0000
        if string.unpack(">I2", resp, 3) ~= 0x0000 then
            log.info("exmodbus_test", "协议标识符错误,期望 0x0000,实际:", string.unpack(">I2", resp, 3))
            return
        end

        -- 4. 检查单元标识符(从站地址)是否与请求一致
        local req_unit_id = string.byte(write_config.raw_request, 7)
        local resp_unit_id = string.byte(resp, 7)
        if req_unit_id ~= resp_unit_id then
            log.info("exmodbus_test", "单元标识符不一致,期望:", req_unit_id, "实际:", resp_unit_id)
            return
        end

        -- 5. 检查功能码是否与请求一致
        local req_func_code = string.byte(write_config.raw_request, 8)
        local resp_func_code = string.byte(resp, 8)
        if req_func_code ~= resp_func_code then
            log.info("exmodbus_test", "功能码不一致,期望:", req_func_code, "实际:", resp_func_code)
            return
        end

        -- 6. 检查起始地址是否与请求一致
        local req_start_addr = string.unpack(">I2", write_config.raw_request, 9)
        local resp_start_addr = string.unpack(">I2", resp, 9)
        if req_start_addr ~= resp_start_addr then
            log.info("exmodbus_test", "起始地址不一致,期望:", req_start_addr, "实际:", resp_start_addr)
            return
        end

        -- 7. 检查寄存器数量是否与请求一致
        local req_reg_count = string.unpack(">I2", write_config.raw_request, 11)
        local resp_reg_count = string.unpack(">I2", resp, 11)
        if req_reg_count ~= resp_reg_count then
            log.info("exmodbus_test", "寄存器数量不一致,期望:", req_reg_count, "实际:", resp_reg_count)
            return
        end

        log.info("exmodbus_test", "成功写入从站 2 保持寄存器 0-1")
    elseif write_result.status == exmodbus.STATUS_TIMEOUT then
        log.info("exmodbus_test", "未收到从站 2 的响应(超时)")
    elseif write_result.status == exmodbus.STATUS_PARAM_INVALID then
        log.info("exmodbus_test", "写入从站 2 参数无效")
    end
end

-- 定时任务函数:每 2 秒调用一次读取函数,每 4 秒调用一次写入函数
local function task()

    local count = 0 -- 计数器

    while true do
        if tcp_master then
            -- 每 2 秒调用一次读取函数
            read_slave1_holding_registers()
            if count == 0 then
                -- 每 4 秒调用一次写入函数
                write_slave2_holding_registers()
            end
            count = (count + 1) % 2
        else
            log.info("exmodbus_test", "tcp_master 未创建,无法执行 read_slave1_holding_registers()")
        end
        sys.wait(2000)
    end
end

-- 初始化任务
sys.taskInit(task)

七、操作步骤演示

7.1 TCP 主站应用模块(字段参数方式,对应 param_field.lua)

1、搭建硬件环境

  • 将 TYPE-C USB 数据线一端接在 Air780EPM 开发板上,另一端接在电脑上
  • 将网线一端接在 Air780EPM 开发板网口上,另一端接在路由器/交换机上
  • 将另一根网线一端接在电脑网口上,另一端接在同一个路由器/交换机上
  • 参考图见 演示硬件环境

2、在摩尔信使上配置模拟 TCP 从站设备环境

  • 点击左上角的 “通道管理” 按钮,在 “通道管理” 窗口点击 “网络通道” 按钮,点击 NET000 通道后面的 “配置” 按钮,在 “网络参数配置” 窗口配置网络参数,操作流程如下:
  • 点击左上角的 “添加设备”按钮,在 “添加设备” 窗口对通信设备参数进行配置,配置好后点击 “添加” 按钮,左侧栏即为添加后的效果,操作流程图如下:
  • 点击左侧的第一个从站(我这里显示为 “NET000-001”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增保持寄存器 0 和 保持寄存器 1,操作流程图如下:
  • 点击左侧的第二个从站(我这里显示为 “NET000-002”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增保持寄存器 0 和 保持寄存器 1,操作流程图如下:
  • 此时在摩尔信使上的配置操作已经完成,如果需要在摩尔信使上查看报文,那么操作流程图如下:

3、调整软件代码

  • 打开 require "param_field" ,注释掉 require "raw_frame" ,操作流程图如下:
  • 在 ”param_field.lua“ 文件中修改对应的 IP 地址和端口号(与上位机保持一致)

4、打开 Luatools 工具,根据要求烧录本次所需要的内核固件和脚本代码

5、烧录成功后,自动开机运行

6、开机运行后 Luatools 工具上记录的日志如下:

[2025-12-30 13:51:30.910][000000002.003] I/user.exmodbus 连接服务器成功
[2025-12-30 13:51:31.226][000000002.334] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:31.235][000000002.347] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 13:51:33.240][000000004.276] D/mobile cid1, state0
[2025-12-30 13:51:33.251][000000004.277] D/mobile bearer act 0, result 0
[2025-12-30 13:51:33.257][000000004.277] D/mobile NETIF_LINK_ON -> IP_READY
[2025-12-30 13:51:33.261][000000004.278] I/user.dnsproxy 开始监听
[2025-12-30 13:51:33.268][000000004.347] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:33.271][000000004.365] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 13:51:33.276][000000004.365] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
[2025-12-30 13:51:33.280][000000004.379] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值
[2025-12-30 13:51:33.284][000000004.399] D/mobile TIME_SYNC 0
[2025-12-30 13:51:35.267][000000006.379] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:35.280][000000006.392] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 13:51:37.273][000000008.392] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:37.289][000000008.404] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 13:51:37.296][000000008.405] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
[2025-12-30 13:51:37.300][000000008.417] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值
[2025-12-30 13:51:38.518][000000009.636] I/user.exmodbus 连接断开
[2025-12-30 13:51:39.300][000000010.418] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:39.307][000000010.419] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 13:51:39.312][000000010.420] I/user.exmodbus_test 未收到从站 1 的响应(超时)
[2025-12-30 13:51:41.318][000000012.421] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:41.327][000000012.422] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 13:51:41.338][000000012.423] I/user.exmodbus_test 未收到从站 1 的响应(超时)
[2025-12-30 13:51:41.348][000000012.424] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
[2025-12-30 13:51:41.355][000000012.425] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 13:51:41.361][000000012.426] I/user.exmodbus_test 未收到从站 2 的响应(超时)
[2025-12-30 13:51:43.321][000000014.426] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:43.327][000000014.427] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 13:51:43.332][000000014.428] I/user.exmodbus_test 未收到从站 1 的响应(超时)
[2025-12-30 13:51:43.525][000000014.639] D/socket connect to 192.168.1.100,6000
[2025-12-30 13:51:43.528][000000014.640] D/net adapter 4 connect 192.168.1.100:6000 TCP
[2025-12-30 13:51:45.315][000000016.429] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:45.321][000000016.430] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 13:51:45.325][000000016.431] I/user.exmodbus_test 未收到从站 1 的响应(超时)
[2025-12-30 13:51:45.330][000000016.432] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
[2025-12-30 13:51:45.335][000000016.433] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 13:51:45.340][000000016.434] I/user.exmodbus_test 未收到从站 2 的响应(超时)
[2025-12-30 13:51:46.527][000000017.647] I/user.exmodbus 连接服务器成功
[2025-12-30 13:51:47.318][000000018.434] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:47.334][000000018.446] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 13:51:49.340][000000020.447] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:49.351][000000020.459] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 13:51:49.361][000000020.460] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
[2025-12-30 13:51:49.370][000000020.472] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值
[2025-12-30 13:51:51.361][000000022.473] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:51.368][000000022.485] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 13:51:53.382][000000024.486] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:53.388][000000024.498] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 13:51:53.392][000000024.499] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
[2025-12-30 13:51:53.396][000000024.511] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值

7、如下图所示,鼠标右键点击 “通道” 下方的按钮,当我们把摩尔信使上由上位机与 Air780EPM 之间的网络通道关闭后,此时 Air780EPM 在发送请求时便会收不到响应,Luatools 工具上显示的日志如下:

[2025-12-30 13:51:38.518][000000009.636] I/user.exmodbus 连接断开
[2025-12-30 13:51:39.300][000000010.418] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:39.307][000000010.419] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 13:51:39.312][000000010.420] I/user.exmodbus_test 未收到从站 1 的响应(超时)
[2025-12-30 13:51:41.318][000000012.421] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:41.327][000000012.422] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 13:51:41.338][000000012.423] I/user.exmodbus_test 未收到从站 1 的响应(超时)
[2025-12-30 13:51:41.348][000000012.424] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
[2025-12-30 13:51:41.355][000000012.425] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 13:51:41.361][000000012.426] I/user.exmodbus_test 未收到从站 2 的响应(超时)

8、如下图所示,鼠标右键点击 “通道” 下方的按钮,当我们把摩尔信使上由上位机与 Air780EPM 之间的网络通道打开后,此时 Air780EPM 在发送请求时便会接收到响应,Luatools 工具与摩尔信使上显示的日志如下:

程序设计为每隔 2 秒执行一次读取,每隔 4 秒执行一次写入

[2025-12-30 13:51:46.527][000000017.647] I/user.exmodbus 连接服务器成功
[2025-12-30 13:51:47.318][000000018.434] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:47.334][000000018.446] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 13:51:49.340][000000020.447] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 13:51:49.351][000000020.459] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 13:51:49.361][000000020.460] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
[2025-12-30 13:51:49.370][000000020.472] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值

9、关于 Air780EPM 执行读取和写入请求后,摩尔信使上位机的数值变化如下图所示:

7.2 TCP 主站应用模块(原始帧方式,对应 raw_frame.lua)

1、搭建硬件环境

  • 将 TYPE-C USB 数据线一端接在 Air780EPM 开发板上,另一端接在电脑上
  • 将网线一端接在 Air780EPM 开发板网口上,另一端接在路由器/交换机上
  • 将另一根网线一端接在电脑网口上,另一端接在同一个路由器/交换机上
  • 参考图见 演示硬件环境

2、在摩尔信使上配置模拟 TCP 从站设备环境

  • 点击左上角的 “通道管理” 按钮,在 “通道管理” 窗口点击 “网络通道” 按钮,点击 NET000 通道后面的 “配置” 按钮,在 “网络参数配置” 窗口配置网络参数,操作流程如下:
  • 点击左上角的 “添加设备”按钮,在 “添加设备” 窗口对通信设备参数进行配置,配置好后点击 “添加” 按钮,左侧栏即为添加后的效果,操作流程图如下:
  • 点击左侧的第一个从站(我这里显示为 “NET000-001”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增保持寄存器 0 和 保持寄存器 1,操作流程图如下:
  • 点击左侧的第二个从站(我这里显示为 “NET000-002”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增保持寄存器 0 和 保持寄存器 1,操作流程图如下:
  • 此时在摩尔信使上的配置操作已经完成,如果需要在摩尔信使上查看报文,那么操作流程图如下:

3、调整软件代码

  • 打开 require "raw_frame" ,注释掉 require "param_field" ,操作流程图如下:
  • 在 ”raw_frame.lua“ 文件中修改对应的 IP 地址和端口号(与上位机保持一致)

4、打开 Luatools 工具,根据要求烧录本次所需要的内核固件和脚本代码

5、烧录成功后,自动开机运行

6、开机运行后 Luatools 工具上记录的日志如下:

[2025-12-30 14:06:14.956][000000020.343] I/user.exmodbus 连接服务器成功
[2025-12-30 14:06:14.971][000000020.367] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:14.987][000000020.380] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 14:06:14.998][000000020.380] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
[2025-12-30 14:06:15.003][000000020.393] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1
[2025-12-30 14:06:16.000][000000022.394] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:17.005][000000022.405] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 14:06:19.007][000000024.406] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:19.024][000000024.417] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 14:06:19.029][000000024.417] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
[2025-12-30 14:06:19.038][000000024.434] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1
[2025-12-30 14:06:19.887][000000025.289] I/user.exmodbus 连接断开
[2025-12-30 14:06:21.037][000000026.435] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:21.060][000000026.436] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 14:06:21.064][000000026.437] I/user.exmodbus_test 未收到从站 1 的响应(超时)
[2025-12-30 14:06:23.037][000000028.437] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:23.047][000000028.438] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 14:06:23.055][000000028.439] I/user.exmodbus_test 未收到从站 1 的响应(超时)
[2025-12-30 14:06:23.060][000000028.439] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
[2025-12-30 14:06:23.067][000000028.440] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 14:06:23.074][000000028.441] I/user.exmodbus_test 未收到从站 2 的响应(超时)
[2025-12-30 14:06:24.890][000000030.292] D/socket connect to 192.168.1.100,6000
[2025-12-30 14:06:24.900][000000030.293] D/net adapter 4 connect 192.168.1.100:6000 TCP
[2025-12-30 14:06:25.045][000000030.441] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:25.056][000000030.442] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 14:06:25.073][000000030.443] I/user.exmodbus_test 未收到从站 1 的响应(超时)
[2025-12-30 14:06:27.038][000000032.443] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:27.046][000000032.444] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 14:06:27.051][000000032.445] I/user.exmodbus_test 未收到从站 1 的响应(超时)
[2025-12-30 14:06:27.056][000000032.445] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
[2025-12-30 14:06:27.059][000000032.446] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 14:06:27.064][000000032.447] I/user.exmodbus_test 未收到从站 2 的响应(超时)
[2025-12-30 14:06:27.896][000000033.299] I/user.exmodbus 连接服务器成功
[2025-12-30 14:06:29.052][000000034.447] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:29.056][000000034.459] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 14:06:31.063][000000036.460] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:31.068][000000036.471] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 14:06:31.071][000000036.471] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
[2025-12-30 14:06:31.076][000000036.483] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1
[2025-12-30 14:06:33.078][000000038.483] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:33.095][000000038.494] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0

7、如下图所示,鼠标右键点击 “通道” 下方的按钮,当我们把摩尔信使上由上位机与 Air780EPM 之间的网络通道关闭后,此时 Air780EPM 在发送请求时便会收不到响应,Luatools 工具上显示的日志如下:

[2025-12-30 14:06:19.887][000000025.289] I/user.exmodbus 连接断开
[2025-12-30 14:06:21.037][000000026.435] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:21.060][000000026.436] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 14:06:21.064][000000026.437] I/user.exmodbus_test 未收到从站 1 的响应(超时)
[2025-12-30 14:06:23.037][000000028.437] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:23.047][000000028.438] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 14:06:23.055][000000028.439] I/user.exmodbus_test 未收到从站 1 的响应(超时)
[2025-12-30 14:06:23.060][000000028.439] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
[2025-12-30 14:06:23.067][000000028.440] E/user.exmodbus TCP 连接未建立或已断开,无法发送请求
[2025-12-30 14:06:23.074][000000028.441] I/user.exmodbus_test 未收到从站 2 的响应(超时)

8、如下图所示,鼠标右键点击 “通道” 下方的按钮,当我们把摩尔信使上由上位机与 Air780EPM 之间的网络通道打开后,此时 Air780EPM 在发送请求时便会接收到响应,Luatools 工具与摩尔信使上显示的日志如下:

程序设计为每隔 2 秒执行一次读取,每隔 4 秒执行一次写入

[2025-12-30 14:06:27.896][000000033.299] I/user.exmodbus 连接服务器成功
[2025-12-30 14:06:29.052][000000034.447] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:29.056][000000034.459] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 14:06:31.063][000000036.460] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
[2025-12-30 14:06:31.068][000000036.471] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
[2025-12-30 14:06:31.071][000000036.471] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
[2025-12-30 14:06:31.076][000000036.483] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1

9、关于 Air780EPM 执行读取和写入请求后,摩尔信使上位机的数值变化如下图所示:

八、总结

本文章演示 Air780EPM 作为 Modbus TCP 主站如何与从站设备进行 Modbus 通信。

九、硬件电路说明

合宙 Air780Exx 系列模组管脚详细说明