Modbus TCP 从站应用
作者:马梦阳 | 最后修改:2026-04-10
一、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、将 Air8101 配置为 Modbus TCP 从站模式
2、等待并且应答主站设备请求
2.2 注意事项
1、本文章中所使用的示例程序需要搭配 exmodbus 扩展库使用,扩展库会在下方进行简单介绍
2、设备作为 modbus TCP 从站模式时,仅支持接收 modbus TCP 标准格式的请求报文
3、进行回应时也需要符合 modbus TCP 标准格式
2.3 特别说明
1、在 main.lua 中 require "netdrv_eth_spi" 模块,使用的是“通过 SPI 外挂 CH390H 芯片的以太网卡”
三、演示硬件环境
1、Air8101 核心板一块
2、AirETH_1000 配件板任意一块
3、TYPE-C USB 数据线一根
4、网线两根(一根开发板使用,一根电脑使用)
Air8101 核心板和 AirETH_1000 配件板的硬件接线方式为:
| Air8101核心板 | AirETH_1000配件板 |
| 59/3V3 | 3.3v |
| gnd | gnd |
| 28/DCLK | SCK |
| 54/DISP | CSS |
| 55/HSYN | SDO |
| 57/DE | SDI |
| 14/GPIO8 | INT |
Air8101 核心板与 AirETH_1000 配件板接线图为:

四、演示软件环境
1. 烧录工具:Luatools 下载调试工具
2. 本demo开发测试时使用的固件为Air8101 V2001 版本固件,本demo对固件版本没有什么特殊要求,所以你如果要测试本demo时,可以直接使用最新版本的内核固件;如果发现最新版本的内核固件测试有问题,可以使用我们开发本demo时使用的内核固件版本来对比测试;
3. 脚本文件:Air8101 脚本文件
4. 模拟工具:摩尔信使(MThings)官网(用于模拟 modbus 主站设备)
5. LuatOS 运行所需要的 lib 文件:使用 Luatools 烧录时,勾选 添加默认 lib 选项,使用默认 lib 脚本文件。
准备好软件环境之后,接下来查看 Air8101 核心板使用说明,将本篇文章中演示使用的项目文件烧录到 Air8101 核心板中。
五、API 接口说明
exmodbus 扩展库说明:https://docs.openluat.com/osapi/ext/exmodbus/
六、代码示例介绍
6.1 模块示例代码解析说明
6.1.1 TCP 从站应用模块(对应 tcp_slave_manage.lua)
1. 调用 exmodbus 扩展库
说明:搭配扩展库使用时需要 require 对应的库文件名,否则在运行时会出现错误
local exmodbus = require("exmodbus")
2. 配置创建从站所需要的参数
本文章演示使用 Modbus TCP 从站,通信模式应固定为 exmodbus.TCP_SLAVE; 网卡 ID 可以与主站设备不一致,只需要能和主站建立通信链接即可; 作为服务器时,允许只填写端口号,此时填的是本地端口号;
-- 创建 TCP 从站配置参数
-- 说明:创建 TCP 从站时只需要配置如下参数即可
local tcp_slave_config = {
-- 网络配置参数
mode = exmodbus.TCP_SLAVE, -- 通信模式:TCP 从站
adapter = socket.LWIP_ETH, -- 网卡 ID:LwIP 协议栈的以太网卡
port = 6000, -- 本地端口号:6000
}
3. 配置寄存器映射表,并初始化默认值
-- 当前从站地址(ID 号)
local SLAVE_ID = 1
-- 寄存器映射表(按类型组织)
local modbus_data = {
coils = {}, -- 线圈,可读可写,布尔值 (0/1)
inputs = {}, -- 输入状态,只读,布尔值 (0/1)
input_registers = {}, -- 输入寄存器,只读,16 位无符号整数
holding_registers = {} -- 保持寄存器,可读可写,16 位无符号整数
}
-- 初始化一些默认值,便于测试
for i = 0, 3 do
modbus_data.coils[i] = 0
modbus_data.inputs[i] = 1
modbus_data.input_registers[i] = 100 + i
modbus_data.holding_registers[i] = 200 + i
end
4. 创建 Modbus TCP 从站
调用 create 接口时应判断创建结果,防止后续调用无效实例对象。
-- 创建 TCP 从站实例
local tcp_slave = exmodbus.create(tcp_slave_config)
-- 判断从站是否创建成功
if not tcp_slave then
log.info("exmodbus_test", "tcp_slave 创建失败")
else
log.info("exmodbus_test", "tcp_slave 创建成功, 从站 ID 为", SLAVE_ID)
end
5. 定义主站请求处理回调函数,配置注册接口
在接收到主站设备请求数据时,exmodbus 扩展库只校验了报文帧长度; 其他检查需要用户在脚本中自行做判断逻辑。
-- 定义从站请求处理回调函数
local function callback(request)
log.info("exmodbus_test", "tcp_slave 收到主站请求")
-- 检查从站 ID 是否匹配
if request.slave_id ~= SLAVE_ID then
log.info("exmodbus_test", "从站 ID 不匹配,请求从站 ID 为", request.slave_id, ",当前从站 ID 为", SLAVE_ID)
return nil
end
-- 根据功能码和寄存器类型,匹配对应的数据表
local data_table = nil
local is_write = false -- 标记是否为写操作
-- 检查请求的功能码是否支持
if request.func_code == exmodbus.READ_COILS then -- 读线圈
data_table = modbus_data.coils
elseif request.func_code == exmodbus.READ_DISCRETE_INPUTS then -- 读离散输入
data_table = modbus_data.inputs
elseif request.func_code == exmodbus.READ_HOLDING_REGISTERS then -- 读保持寄存器
data_table = modbus_data.holding_registers
elseif request.func_code == exmodbus.READ_INPUT_REGISTERS then -- 读输入寄存器
data_table = modbus_data.input_registers
elseif request.func_code == exmodbus.WRITE_SINGLE_COIL or request.func_code == exmodbus.WRITE_MULTIPLE_COILS then -- 写单个/多个线圈
is_write = true
data_table = modbus_data.coils
elseif request.func_code == exmodbus.WRITE_SINGLE_HOLDING_REGISTER or request.func_code == exmodbus.WRITE_MULTIPLE_HOLDING_REGISTERS then -- 写单个/多个保持寄存器
is_write = true
data_table = modbus_data.holding_registers
else
-- 不支持的功能码
log.info("exmodbus_test", "不支持的功能码: ", request.func_code)
return exmodbus.ILLEGAL_FUNCTION
end
-- 检查数据地址是否有效
local end_addr = request.start_addr + request.reg_count - 1
-- 假设每种寄存器的最大地址是 3 (即 0 - 3)
if request.start_addr < 0 or end_addr > 3 then
log.info("exmodbus_test", "数据地址超出范围,起始地址为", request.start_addr, "结束地址为", end_addr)
return exmodbus.ILLEGAL_DATA_ADDRESS
end
-- 处理读取操作
if not is_write then
-- 构造响应数据表
local response = {}
local response_str = ""
for i = 0, request.reg_count - 1 do
local addr = request.start_addr + i
response[addr] = data_table[addr]
response_str = response_str .. (i > 0 and ", " or "") .. response[addr]
end
log.info("exmodbus_test", "读取成功,返回数据: ", response_str)
return response
end
-- 处理写入操作
if is_write then
-- 执行写入操作
for i = 0, request.reg_count - 1 do
local addr = request.start_addr + i
data_table[addr] = request.data[addr]
log.info("exmodbus_test", "写入成功,写入地址: ", addr, "写入数据: ", request.data[addr])
end
return {} -- 返回空表表示成功
end
end
-- 注册主站请求处理回调函数
tcp_slave:on(callback)
log.info("从站回调函数已注册,开始监听主站请求...")
6.2 完整示例代码展示
6.2.1 TCP 从站应用模块(对应 tcp_slave_manage.lua)
--[[
@module tcp_slave_manage
@summary TCP 从站应用模块
@version 1.0
@date 2026.01.04
@author 马梦阳
@usage
本功能模块演示的内容为:
1、将设备配置为 modbus TCP 从站模式
2、等待并且应答主站请求
注意事项:
1、该示例程序需要搭配 exmodbus 扩展库使用
2、设备作为 modbus TCP 从站模式时,仅支持接收 modbus TCP 标准格式的请求报文
3、进行回应时也需要符合 modbus TCP 标准格式
本文件没有对外接口,直接在 main.lua 中 require "tcp_slave_manage" 就可以加载运行;
]]
local exmodbus = require("exmodbus")
-- 创建 TCP 从站配置参数
-- 说明:创建 TCP 从站时只需要配置如下参数即可
local tcp_slave_config = {
-- 网络配置参数
mode = exmodbus.TCP_SLAVE, -- 通信模式:TCP 从站
adapter = socket.LWIP_ETH, -- 网卡 ID:LwIP 协议栈的以太网卡
port = 6000, -- 本地端口号:6000(主站:服务器端口;从站:本地端口)
}
-- 当前从站地址(ID 号)
local SLAVE_ID = 1
-- 寄存器映射表(按类型组织)
local modbus_data = {
coils = {}, -- 线圈,可读可写,布尔值 (0/1)
inputs = {}, -- 输入状态,只读,布尔值 (0/1)
input_registers = {}, -- 输入寄存器,只读,16 位无符号整数
holding_registers = {} -- 保持寄存器,可读可写,16 位无符号整数
}
-- 初始化一些默认值,便于测试
for i = 0, 3 do
modbus_data.coils[i] = 0
modbus_data.inputs[i] = 1
modbus_data.input_registers[i] = 100 + i
modbus_data.holding_registers[i] = 200 + i
end
-- 创建 TCP 从站实例
local tcp_slave = exmodbus.create(tcp_slave_config)
-- 判断从站是否创建成功
if not tcp_slave then
log.info("exmodbus_test", "tcp_slave 创建失败")
else
log.info("exmodbus_test", "tcp_slave 创建成功, 从站 ID 为", SLAVE_ID)
end
-- 定义主站请求处理回调函数
local function callback(request)
log.info("exmodbus_test", "tcp_slave 收到主站请求")
-- 检查从站 ID 是否匹配
if request.slave_id ~= SLAVE_ID then
log.info("exmodbus_test", "从站 ID 不匹配,请求从站 ID 为", request.slave_id, ",当前从站 ID 为", SLAVE_ID)
return nil
end
-- 根据功能码和寄存器类型,匹配对应的数据表
local data_table = nil
local is_write = false -- 标记是否为写操作
-- 检查请求的功能码是否支持
if request.func_code == exmodbus.READ_COILS then -- 读线圈
data_table = modbus_data.coils
elseif request.func_code == exmodbus.READ_DISCRETE_INPUTS then -- 读离散输入
data_table = modbus_data.inputs
elseif request.func_code == exmodbus.READ_HOLDING_REGISTERS then -- 读保持寄存器
data_table = modbus_data.holding_registers
elseif request.func_code == exmodbus.READ_INPUT_REGISTERS then -- 读输入寄存器
data_table = modbus_data.input_registers
elseif request.func_code == exmodbus.WRITE_SINGLE_COIL or request.func_code == exmodbus.WRITE_MULTIPLE_COILS then -- 写单个/多个线圈
is_write = true
data_table = modbus_data.coils
elseif request.func_code == exmodbus.WRITE_SINGLE_HOLDING_REGISTER or request.func_code == exmodbus.WRITE_MULTIPLE_HOLDING_REGISTERS then -- 写单个/多个保持寄存器
is_write = true
data_table = modbus_data.holding_registers
else
-- 不支持的功能码
log.info("exmodbus_test", "不支持的功能码: ", request.func_code)
return exmodbus.ILLEGAL_FUNCTION
end
-- 检查数据地址是否有效
local end_addr = request.start_addr + request.reg_count - 1
-- 假设每种寄存器的最大地址是 3 (即 0 - 3)
if request.start_addr < 0 or end_addr > 3 then
log.info("exmodbus_test", "数据地址超出范围,起始地址为", request.start_addr, "结束地址为", end_addr)
return exmodbus.ILLEGAL_DATA_ADDRESS
end
-- 处理读取操作
if not is_write then
-- 构造响应数据表
local response = {}
local response_str = ""
for i = 0, request.reg_count - 1 do
local addr = request.start_addr + i
response[addr] = data_table[addr]
response_str = response_str .. (i > 0 and ", " or "") .. response[addr]
end
log.info("exmodbus_test", "读取成功,返回数据: ", response_str)
return response
end
-- 处理写入操作
if is_write then
-- 执行写入操作
for i = 0, request.reg_count - 1 do
local addr = request.start_addr + i
data_table[addr] = request.data[addr]
log.info("exmodbus_test", "写入成功,写入地址: ", addr, "写入数据: ", request.data[addr])
end
return {} -- 返回空表表示成功
end
end
-- 注册主站请求处理回调函数
tcp_slave:on(callback)
log.info("从站回调函数已注册,开始监听主站请求...")
七、操作步骤演示
1、搭建硬件环境
- 将 TYPE-C USB 数据线一端接在 Air8101 核心板上,另一端接在电脑上
- 将 AirETH_1000 配件板与 Air8101 核心板相连接,网线接在配件板网口上,另一端接在路由器/交换机上
- 将另一根网线一端接在电脑网口上,另一端接在同一个路由器/交换机上
- 参考图见 演示硬件环境
2、在摩尔信使上配置模拟 TCP 主站设备环境
- 点击左上角的 “通道管理” 按钮,在 “通道管理” 窗口点击 “网络通道” 按钮,点击 NET000 通道后面的 “配置” 按钮,在 “网络参数配置” 窗口配置网络参数,操作流程如下:

- 点击左上角的 “添加设备”按钮,在 “添加设备” 窗口对通信设备参数进行配置,配置好后点击 “添加” 按钮,左侧栏即为添加后的效果,操作流程图如下:

- 点击左侧的第一个主站(我这里显示为 “NET000-001”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增线圈 0-2,操作流程图如下,此时按照刚才操作,依次分别创建离散输入 0-2、保持寄存器 0-2、输入寄存器 0-2

- 此时在摩尔信使上的配置操作已经完成,如果需要在摩尔信使上查看报文,那么操作流程图如下:

3、打开 Luatools 工具,根据要求烧录本次所需要的内核固件和脚本代码
4、烧录成功后,自动开机运行
5、此时需要等待客户端连接,连接成功后 Luatools 工具上的日志如下:
[2026-01-04 17:27:14.729] luat:U(21062):I/user.exmodbus TCP 从站已启动,监听端口: 6000
6、如果摩尔信使一直没有连接成功,则需要对网络通道进行重启,鼠标右击左上角 “通道” 下方的按钮,点击 “配置参数” 后会弹出 “网络参数配置” 窗口,此时直接点击确定,通道便已经重启,操作流程如下:

7、如下图所示,在摩尔信使上鼠标右击第一个主站,然后点击 “启动轮询”,此时上位机便会模拟主站设备开始执行轮询请求操作

8、如下图所示,如果需要修改轮询的间隔时间或者其他参数,先将滑动条滑到右边,然后鼠标左键双击对应参数即可修改

9、开启轮询后 Luatools 工具与摩尔信使上的日志如下:
[2026-01-04 17:28:27.416] luat:U(93750):I/user.exmodbus_test 读取成功,返回数据: 0, 0, 0
[2026-01-04 17:28:30.443] luat:U(96769):I/user.exmodbus_test tcp_slave 收到主站请求
[2026-01-04 17:28:30.443] luat:U(96769):I/user.exmodbus_test 读取成功,返回数据: 1, 1, 1
[2026-01-04 17:28:33.465] luat:U(99792):I/user.exmodbus_test tcp_slave 收到主站请求
[2026-01-04 17:28:33.465] luat:U(99792):I/user.exmodbus_test 读取成功,返回数据: 200, 201, 202
[2026-01-04 17:28:36.482] luat:U(102811):I/user.exmodbus_test tcp_slave 收到主站请求
[2026-01-04 17:28:36.482] luat:U(102812):I/user.exmodbus_test 读取成功,返回数据: 100, 101, 102

10、如下图所示,如果需要执行写入请求,需要先在执行可写操作的对应区块行的指令处鼠标左键双击填入要写入的数值,然后在鼠标右键双击该数值,最后点击下发写指令

11、执行写入请求后 Luatools 工具与摩尔信使上的日志如下:
[2026-01-04 17:32:11.049] luat:U(317384):I/user.exmodbus_test tcp_slave 收到主站请求
[2026-01-04 17:32:11.049] luat:U(317385):I/user.exmodbus_test 写入成功,写入地址: 0 写入数据: 123

12、如下图所示,在摩尔信使上鼠标右击第一个主站,然后点击 “停止轮询”,此时上位机便不会再执行轮询请求操作

八、总结
本文章演示 Air8101 作为 Modbus TCP 从站如何与主站设备进行 Modbus 通信。