跳转至

Modbus RTU 从站应用

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

一、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 RTU 从站应用,演示功能如下:

1、将 Air8000 配置为 Modbus RTU 从站模式

2、等待并且应答主站设备请求

2.2 注意事项

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

2、设备作为 modbus RTU 从站模式时,仅支持接收 modbus RTU 标准格式的请求报文

3、进行回应时也需要符合 modbus RTU 标准格式

三、演示硬件环境

1、Air8000 开发板一块

2、TYPE-C USB 数据线一根

3、USB-RS485 串口板

此处购买链接仅为推荐,如有问题请直接联系店家

Air8000 与 USB-RS485 串口板接线图如下:

四、演示软件环境

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

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

3. 脚本文件:Air8000 脚本文件

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

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

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

五、API 接口说明

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

六、代码示例介绍

6.1 模块示例代码解析说明

6.1.1 RTU 从站应用模块(对应 rtu_slave_manage.lua)

1. 调用 exmodbus 扩展库

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

local exmodbus = require("exmodbus")

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

本文章演示使用 Modbus RTU 从站,通信模式应固定为 exmodbus.RTU_SLAVE; 其余串口相关参数应根据实际情况进行填写,并且要与主站设备串口参数保持一致; RS485 芯片供电引脚、RS485 方向引脚等管脚需要根据自己所使用的开发板进行调整,演示使用的是 Air8000 整机开发板。

gpio.setup(16, 1)         -- Air8000 开发板 RS485 芯片供电引脚
local rs485_dir_gpio = 17 -- Air8000 开发板 RS485 方向引脚

-- 创建 RTU 从站配置参数
-- 说明:创建 RTU 从站时只需要配置如下参数即可
local rtu_slave_config = {
    -- 串口配置参数
    mode = exmodbus.RTU_SLAVE,       -- 通信模式
    uart_id = 1,                     -- UART 端口号
    baud_rate = 115200,              -- 波特率
    data_bits = 8,                   -- 数据位
    stop_bits = 1,                   -- 停止位
    parity_bits = uart.None,         -- 校验位
    byte_order = uart.LSB,           -- 字节顺序
    rs485_dir_gpio = rs485_dir_gpio, -- RS485 方向引脚
    rs485_dir_rx_level = 0,          -- RS485 接收方向电平
}

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 RTU 从站

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

-- 创建 RTU 从站实例
local rtu_slave = exmodbus.create(rtu_slave_config)

-- 判断从站是否创建成功
if not rtu_slave then
    log.info("exmodbus_test", "rtu_slave 创建失败")
else
    log.info("exmodbus_test", "rtu_slave 创建成功, 从站 ID 为", SLAVE_ID)
end

5. 定义主站请求处理回调函数,配置注册接口

在接收到主站设备请求数据时,exmodbus 扩展库只校验了 CRC,以及报文帧长度; 其他检查需要用户在脚本中自行做判断逻辑。

-- 定义主站请求处理回调函数
local function callback(request)
    log.info("exmodbus_test", "rtu_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


-- 注册主站请求处理回调函数
rtu_slave:on(callback)

log.info("从站回调函数已注册,开始监听主站请求...")

6.2 完整示例代码展示

6.2.1 RTU 从站应用模块(对应 rtu_slave_manage.lua)

--[[
@module  rtu_slave_manage
@summary RTU 从站应用模块
@version 1.0
@date    2025.12.12
@author  马梦阳
@usage
本功能模块演示的内容为:
1、将设备配置为 modbus RTU 从站模式
2、等待并且应答主站请求

注意事项:
1、该示例程序需要搭配 exmodbus 扩展库使用
2、设备作为 modbus RTU 从站模式时,仅支持接收 modbus RTU 标准格式的请求报文
3、进行回应时也需要符合 modbus RTU 标准格式

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

local exmodbus = require("exmodbus")

gpio.setup(16, 1)         -- Air8000 开发板 RS485 芯片供电引脚
local rs485_dir_gpio = 17 -- Air8000 开发板 RS485 方向引脚

-- 创建 RTU 从站配置参数
-- 说明:创建 RTU 从站时只需要配置如下参数即可
local rtu_slave_config = {
    -- 串口配置参数
    mode = exmodbus.RTU_SLAVE,       -- 通信模式
    uart_id = 1,                     -- UART 端口号
    baud_rate = 115200,              -- 波特率
    data_bits = 8,                   -- 数据位
    stop_bits = 1,                   -- 停止位
    parity_bits = uart.None,         -- 校验位
    byte_order = uart.LSB,           -- 字节顺序
    rs485_dir_gpio = rs485_dir_gpio, -- RS485 方向引脚
    rs485_dir_rx_level = 0,          -- RS485 接收方向电平
}

-- 当前从站地址(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

-- 创建 RTU 从站实例
local rtu_slave = exmodbus.create(rtu_slave_config)

-- 判断从站是否创建成功
if not rtu_slave then
    log.info("exmodbus_test", "rtu_slave 创建失败")
else
    log.info("exmodbus_test", "rtu_slave 创建成功, 从站 ID 为", SLAVE_ID)
end

-- 定义主站请求处理回调函数
local function callback(request)
    log.info("exmodbus_test", "rtu_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

-- 注册主站请求处理回调函数
rtu_slave:on(callback)

log.info("从站回调函数已注册,开始监听主站请求...")

七、操作步骤演示

1、搭建硬件环境

  • 将 USB-RS485 串口板与 Air8000 开发板进行连接
  • 将 USB-RS485 串口板 与 Air8000 开发板的 USB 端同时接在电脑上
  • 参考图见 演示硬件环境

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

  • 点击左上角的 “通道管理”按钮,在 “通道管理” 窗口界面选择对应的串口(USB-RS485 串口板与 Air8000 开发板进行 485 通信时的端口),点击对应串口后面的 “配置” 按钮,在 “串口参数配置” 窗口配置串口参数(要求与代码中调用 exmodbus.create 接口时填入的配置参数一致),操作流程图如下:
  • 点击左上角的 “添加设备”按钮,在 “添加设备” 窗口对通信设备参数进行配置,配置好后点击 “添加” 按钮,左侧栏即为添加后的效果,操作流程图如下:
  • 点击左侧的第一个主站(我这里显示为 “COM36-001”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增线圈 0-2,操作流程图如下,此时按照刚才操作,依次分别创建离散输入 0-2、保持寄存器 0-2、输入寄存器 0-2
  • 此时在摩尔信使上的配置操作已经完成,如果需要在摩尔信使上查看报文,那么操作流程图如下:

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

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

6、开机运行后 Luatools 工具上记录的日志如下,此时便开始等待主站发送请求

[2025-12-08 17:23:19.802][000000000.678] I/user.main RTU_SLAVE 001.000.000
[2025-12-08 17:23:19.806][000000000.711] Uart_ChangeBR 1338:uart1, 115200 115203 26000000 3611
[2025-12-08 17:23:19.809][000000000.711] I/user.exmodbus 串口 1 初始化成功,波特率 115200
[2025-12-08 17:23:19.811][000000000.712] I/user.exmodbus_test rtu_slave 创建成功, 从站 ID 为 1
[2025-12-08 17:23:19.813][000000000.712] I/user.exmodbus 已注册从站请求处理回调函数
[2025-12-08 17:23:19.816][000000000.712] I/user.从站回调函数已注册,开始监听主站请求...

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

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

9、开启轮询后 Luatools 工具与摩尔信使上的日志如下:

[2025-12-08 17:32:09.235][000000023.394] I/user.exmodbus_test rtu_slave 收到主站请求
[2025-12-08 17:32:09.239][000000023.394] I/user.exmodbus_test 读取成功,返回数据:  0, 0, 0
[2025-12-08 17:32:12.251][000000026.402] I/user.exmodbus_test rtu_slave 收到主站请求
[2025-12-08 17:32:12.255][000000026.402] I/user.exmodbus_test 读取成功,返回数据:  1, 1, 1
[2025-12-08 17:32:15.266][000000029.417] I/user.exmodbus_test rtu_slave 收到主站请求
[2025-12-08 17:32:15.269][000000029.418] I/user.exmodbus_test 读取成功,返回数据:  200, 201, 202
[2025-12-08 17:32:18.270][000000032.420] I/user.exmodbus_test rtu_slave 收到主站请求
[2025-12-08 17:32:18.273][000000032.420] I/user.exmodbus_test 读取成功,返回数据:  100, 101, 102
[2025-12-08 17:32:21.294][000000035.443] I/user.exmodbus_test rtu_slave 收到主站请求
[2025-12-08 17:32:21.296][000000035.444] I/user.exmodbus_test 读取成功,返回数据:  0, 0, 0
[2025-12-08 17:32:24.288][000000038.446] I/user.exmodbus_test rtu_slave 收到主站请求
[2025-12-08 17:32:24.290][000000038.447] I/user.exmodbus_test 读取成功,返回数据:  1, 1, 1
[2025-12-08 17:32:27.326][000000041.476] I/user.exmodbus_test rtu_slave 收到主站请求
[2025-12-08 17:32:27.329][000000041.477] I/user.exmodbus_test 读取成功,返回数据:  200, 201, 202

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

11、执行写入请求后 Luatools 工具与摩尔信使上的日志如下:

[2025-12-08 17:42:53.696][000000667.848] I/user.exmodbus_test rtu_slave 收到主站请求
[2025-12-08 17:42:53.704][000000667.848] I/user.exmodbus_test 写入成功,写入地址:  0 写入数据:  123

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

八、总结

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

九、硬件电路说明

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