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 通信。