LuatOS 是面向物联网设备的轻量级嵌入式 Lua 脚本运行框架 / 实时系统,基于 Lua 5.3 深度优化,用于 4G‑Cat.1、MCU 等物联网终端。LuatOS 开发特点是基于 Lua 脚本、协程多任务设计,当前LuatOS共有70多个核心库,30多个扩展库,1000多个应用demo示例,覆盖所有基础应用,开发方便。
一、概述
1.1 modbus 协议介绍及扩展库说明
Modbus 最初由 Modicon(现为施耐德电气旗下品牌)于 1979 年开发,是一种用于可编程逻辑控制器(PLC)之间通信的工业通信协议。由于其简单、开放、免费且易于实现,Modbus 已成为工业自动化领域最广泛使用的通信协议之一,被广泛应用于工业控制、楼宇自动化、能源管理、智能仪表等场景。
该协议采用主从架构,最初基于串行通信(如 Modbus RTU 和 Modbus ASCII),后扩展支持以太网传输(Modbus TCP)。Modbus 定义了四种数据对象:线圈、离散输入、输入寄存器和保持寄存器,并通过功能码实现设备间的数据交换。
LuatOS 提供了 exmodbus 扩展库,方便用户更加方便的管理和处理 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(0x0F):写多个线圈
- 典型用途:控制继电器、开关、指示灯等数字输出设备
- 实际应用举例:
- 控制继电器的吸合或断开
- 控制电机的启动或停止
- 控制阀门的开启或关闭
- 控制指示灯的点亮或熄灭
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(0x10):写多个保持寄存器
- 典型用途:存储配置参数、设定值、设备状态等可被主站设备修改的数据
- 实际应用举例:
- 写入方面:设置目标温度、设定电机转速、修改报警阈值、发送控制命令代码
- 读取方面:读取设备内部计算的参数、获取系统状态信息、读取预置的配方数据
1.3.5 数据类型一览表
| 数据类型(中文) | 数据类型(英文) | 存储单元 | 访问权限 | 功能码 | 典型用途 |
|---|---|---|---|---|---|
| 线圈 | Coils | 1 bit | 可读可写 | 01(0x01):读单个或多个线圈 05(0x05):写单个线圈 15(0x0F):写多个线圈 | 控制继电器、开关、指示灯等数字输出设备 |
| 离散输入 | Discrete Inputs | 1 bit | 只读 | 02(0x02):读单个或多个离散输入 | 读取限位开关、按钮、传感器等数字输入状态 |
| 输入寄存器 | Input Registers | 16 bit | 只读 | 04(0x04):读单个或多个输入寄存器 | 读取传感器温度、压力、电压等模拟量输入值 |
| 保持寄存器 | Holding Registers | 16 bit | 可读可写 | 03(0x03):读单个或多个保持寄存器 06(0x06):写单个保持寄存器 16(0x10):写多个保持寄存器 | 存储配置参数、设定值、设备状态等可被主站设备修改的数据 |
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) | 网关无法从目标设备(即最终的被访问从站)获得响应。这通常意味着目标设备离线或存在通信问题。 |
1.6 主/从站使用流程图
1.6.1 主站端

1.6.2 从站端

二、核心示例
1、核心示例是指:使用本库文件提供的核心 API,开发的基础业务逻辑的演示代码;
2、核心示例的作用是:帮助开发者快速理解如何使用本库,所以核心示例的逻辑都比较简单;
3、更加完整和详细的 demo,请参考 LuatOS 仓库 中各个产品目录下的 demo/modbus;
2.1 Modbus RTU 主站
演示场景:
1、将设备配置为 modbus RTU 主站模式;
2、对从站 1 进行 2 秒一次的读取保持寄存器 0-1 操作;
特别说明:
1、使用 Air8000 或者 Air780EPM 开发板可直接使用下列代码,只需要按照代码中注释说明打开对应代码即可;
2、对于其他主流模组(如 Air780EHx 系列、Air8101),若有官方开发板且设计有 485 接口时可以参考对应的硬件手册在代码中配置相关管脚即可实现 485 功能,若没有官方开发板或者官方开发板没有设计 485 接口时需要参考硬件手册选择合适的管脚并外挂 uart 转 RS485 模块才能实现 485 功能;
3、对 RS485 不了解的用户可以先到对应模组下 软件开发资料-外设接口-UART 文档进行学习,待熟悉之后再参考硬件手册进行配置更容易上手;
2.1.1 字段参数方式
lua
PROJECT = "RTU_MASTER"
VERSION = "001.000.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
local exmodbus = require("exmodbus")
-- 使用 Air8000 开发板测试打开这两个
gpio.setup(16, 1) -- RS485 芯片供电引脚
local rs485_dir_gpio = 17 -- RS485 方向引脚
-- 使用 Air780EPM 开发板测试打开这三个;
-- gpio.setup(1, 1) -- Air780EPM RS485 芯片供电引脚
-- gpio.setup(23, 1) -- Air780EPM vref 脚拉高
-- local rs485_dir_gpio = 24 -- Air780EPM RS485 方向引脚(V1.2 是 25,V1.3 是 24)
-- 创建 RTU 主站配置参数;
-- 说明:创建 RTU 主站时只需要配置如下参数即可;
local create_config = {
-- 串口配置参数;
mode = exmodbus.RTU_MASTER, -- 通信模式
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 接收方向电平
}
-- 初始化从站 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
}
-- 创建 RTU 主站实例
local rtu_master = exmodbus.create(create_config)
-- 判断主站是否创建成功并记录日志
if not rtu_master then
log.info("exmodbus_test", "rtu_master 创建失败")
else
log.info("exmodbus_test", "rtu_master 创建成功")
end
-- 读取从站 1 保持寄存器数据的函数
local function read_slave1_holding_registers()
log.info("exmodbus_test", "开始读取从站 1 保持寄存器 0-1 的值")
-- 执行读取操作
local read_result = rtu_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 task()
while true do
if rtu_master then
-- 每 2 秒调用一次读取函数
read_slave1_holding_registers()
else
log.info("exmodbus_test", "rtu_master 未创建,无法执行 read_slave1_holding_registers()")
end
sys.wait(2000)
end
end
-- 初始化任务
sys.taskInit(task)
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
2.1.2 原始帧方式
lua
PROJECT = "RTU_MASTER"
VERSION = "001.000.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
local exmodbus = require("exmodbus")
-- 使用 Air8000 开发板测试打开这两个
gpio.setup(16, 1) -- RS485 芯片供电引脚
local rs485_dir_gpio = 17 -- RS485 方向引脚
-- 使用 Air780EPM 开发板测试打开这三个;
-- gpio.setup(1, 1) -- Air780EPM RS485 芯片供电引脚
-- gpio.setup(23, 1) -- Air780EPM vref 脚拉高
-- local rs485_dir_gpio = 24 -- Air780EPM RS485 方向引脚(V1.2 是 25,V1.3 是 24)
-- 创建 RTU 主站配置参数;
-- 说明:创建 RTU 主站时只需要配置如下参数即可;
local create_config = {
-- 串口配置参数;
mode = exmodbus.RTU_MASTER, -- 通信模式
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 接收方向电平
}
-- 初始化从站 1 数据结构
-- 用于记录从站 1 保持寄存器 0-1 的值;
local slave1_data = {}
-- 配置读取从站 1 保持寄存器 0-1 的值;
local read_config = {
raw_request = string.char(
0x01, -- 从站地址
0x03, -- 功能码:读取保持寄存器
0x00, 0x00, -- 寄存器起始地址
0x00, 0x02, -- 寄存器数量
0xC4, 0x0B -- CRC16校验码
),
timeout = 1000 -- 超时时间 1000 ms
}
-- 创建 RTU 主站实例
local rtu_master = exmodbus.create(create_config)
-- 判断主站是否创建成功并记录日志
if not rtu_master then
log.info("exmodbus_test", "rtu_master 创建失败")
else
log.info("exmodbus_test", "rtu_master 创建成功")
end
-- 读取从站 1 保持寄存器数据的函数
local function read_slave1_holding_registers()
log.info("exmodbus_test", "开始读取从站 1 保持寄存器 0-1 的值")
-- 执行读取操作
local read_result = rtu_master:read(read_config)
-- 根据返回状态处理结果
if read_result.status == exmodbus.STATUS_SUCCESS then
local resp = read_result.raw_response
-- 特别说明:
-- 接下来的判断是针对 modbus RTU 标准响应格式的应答原始帧来解析的
-- 在实际项目中,应根据自己项目中的实际应答原始帧格式进行解析
-- 如果实际格式与此处演示的格式不一致,需要修改接下来的解析代码
-- 1. 检查总长度:必须为 9 字节(1 地址 + 1 功能码 + 1 字节数 + 4 数据 + 2 CRC)
if #resp ~= 9 then
log.info("exmodbus_test", "响应长度错误,期望 9 字节,实际:", #resp)
return
end
-- 2. 检查从站地址
if string.byte(resp, 1) ~= 0x01 then
log.info("exmodbus_test", "从站地址不匹配,收到:", string.byte(resp, 1))
return
end
-- 3. 检查功能码
local func_code = string.byte(resp, 2)
if func_code == 0x83 then
local exc_code = string.byte(resp, 3)
log.info("exmodbus_test", "从站返回异常响应,异常码:", exc_code)
return
elseif func_code ~= 0x03 then
log.info("exmodbus_test", "功能码错误,收到:", func_code)
return
end
-- 4. 检查字节数字段(应为 4)
local byte_count = string.byte(resp, 3)
if byte_count ~= 4 then
log.info("exmodbus_test", "字节数字段错误,期望 4,实际:", byte_count)
return
end
-- 5. 校验CRC
-- 计算前 7 字节的 CRC
local crc_calculated = crypto.crc16_modbus(resp:sub(1, 7))
-- 提取接收到的 CRC
local crc_received = string.unpack("<I2", resp, 8)
-- 比较 CRC
if crc_calculated ~= crc_received then
log.info("exmodbus_test", "CRC 校验错误,计算值:", crc_calculated, ",接收值:", crc_received)
return
end
-- 6. 解析寄存器数据(从第 4 字节开始,大端序)
local data1 = string.unpack(">I2", resp, 4) -- 寄存器 0,偏移 4
local data2 = string.unpack(">I2", resp, 6) -- 寄存器 1,偏移 6
-- 7. 记录数据
slave1_data[0] = data1
slave1_data[1] = data2
-- 8. 记录日志
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 的响应(超时)")
end
end
-- 定时任务函数:每 2 秒调用一次读取函数
local function task()
while true do
if rtu_master then
-- 每 2 秒调用一次读取函数
read_slave1_holding_registers()
else
log.info("exmodbus_test", "rtu_master 未创建,无法执行 read_slave1_holding_registers()")
end
sys.wait(2000)
end
end
-- 初始化任务
sys.taskInit(task)
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
2.2 Modbus RTU 从站
演示场景:
1、将设备配置为 modbus RTU 从站模式
2、等待并且应答主站请求
特别说明:
1、使用 Air8000 或者 Air780EPM 开发板均可直接使用下列代码,只需要按照代码中注释说明打开对应代码即可;
2、对于其他主流模组(如 Air780EHx 系列、Air8101),若有官方开发板且设计有 485 接口时可以参考对应的硬件手册在代码中配置相关管脚即可实现 485 功能,若没有官方开发板或者官方开发板没有设计 485 接口时需要参考硬件手册选择合适的管脚并外挂 uart 转 RS485 模块才能实现 485 功能;
3、对 RS485 不了解的用户可以先到对应模组下 软件开发资料-外设接口-UART 文档进行学习,待熟悉之后再参考硬件手册进行配置更容易上手;
lua
PROJECT = "RTU_SLAVE"
VERSION = "001.000.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
local exmodbus = require("exmodbus")
-- 使用 Air8000 开发板测试打开这两个
gpio.setup(16, 1) -- RS485 芯片供电引脚
local rs485_dir_gpio = 17 -- RS485 方向引脚
-- 使用 Air780EPM 开发板测试打开这三个;
-- gpio.setup(1, 1) -- Air780EPM RS485 芯片供电引脚
-- gpio.setup(23, 1) -- Air780EPM vref 脚拉高
-- local rs485_dir_gpio = 24 -- Air780EPM RS485 方向引脚(V1.2 是 25,V1.3 是 24)
-- 创建 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 = {}
for i = 0, request.reg_count - 1 do
local addr = request.start_addr + i
response[addr] = data_table[addr]
end
log.info("exmodbus_test", "读取成功,返回数据: ", table.concat(response, ", "))
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("从站回调函数已注册,开始监听主站请求...")
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
2.3 Modbus ASCII 主站(开发中)
2.4 Modbus ASCII 从站(开发中)
2.5 Modbus TCP 主站
演示场景:
1、将设备配置为 modbus TCP 主站模式
2、对从站 1 进行 2 秒一次的读取保持寄存器 0-1 操作
特别说明:
1、开发板和上位机需要接入同一路由下,开发板网卡驱动使用的是以太网;
2、使用 Air8000 开发板可直接使用下列代码;
3、对于其他主流模组(如 Air780Exx 系列、Air8101),若有官方开发板且设计有以太网口时可以参考对应的硬件手册在代码中配置相关管脚即可实现以太网功能,若没有官方开发板或者官方开发板没有设计以太网口时需要参考硬件手册选择合适的管脚并外挂以太网模块才能实现以太网功能;
4、对以太网不了解的用户可以先到对应模组下 软件开发资料-外设接口-以太网 文档进行学习,待熟悉之后再参考硬件手册进行配置更容易上手;
2.5.1 字段参数方式
main.lua
lua
PROJECT = "TCP_MASTER"
VERSION = "001.000.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
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
}
-- 创建 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 task()
while true do
if tcp_master then
-- 每 2 秒调用一次读取函数
read_slave1_holding_registers()
else
log.info("exmodbus_test", "tcp_master 未创建,无法执行 read_slave1_holding_registers()")
end
sys.wait(2000)
end
end
-- 初始化任务
sys.taskInit(task)
netdrv_eth_spi.lua
lua
--[[
@module netdrv_eth_spi
@summary "通过SPI外挂CH390H芯片的以太网卡"驱动模块
@version 1.0
@date 2025.12.18
@author 马梦阳
@usage
本文件为"通过SPI外挂CH390H芯片的以太网卡"驱动模块,核心业务逻辑为:
1、打开CH390H芯片供电开关;
2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
3、以太网卡的连接状态发生变化时,在日志中进行打印;
直接使用Air8000开发板硬件测试即可;
本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
]]
local exnetif = require "exnetif"
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_ETH then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
end
end
local function ip_lose_func(adapter)
if adapter == socket.LWIP_ETH then
log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
end
end
-- 以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
-- 各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
-- 以太网断网后,内核固件会产生一个"IP_LOSE"消息
-- 各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
--此处订阅"IP_READY"和"IP_LOSE"两种消息
--在消息的处理函数中,仅仅打印了一些信息,便于实时观察"通过SPI外挂CH390H芯片的以太网卡"的连接状态
--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
-- 配置SPI外接以太网芯片CH390H的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_ETH
-- 本demo使用Air8000开发板测试,开发板上的硬件配置为:
-- GPIO140为CH390H以太网芯片的供电使能控制引脚
-- 使用spi1,片选引脚使用GPIO12
-- 如果使用的硬件不是Air8000开发板,根据自己的硬件配置修改以下参数
local function netdrv_task_func()
exnetif.set_priority_order({
{
ETHERNET = {
pwrpin = 140,
tp = netdrv.CH390,
opts = { spi = 1, cs = 12 },
-- 此处设置为静态 IP 地址
-- 如果不设置为静态 IP 地址,默认会使用 DHCP 协议动态获取 IP 地址
static_ip = {
ipv4 = "192.168.1.183",
mark = "255.255.255.0",
gw = "192.168.1.1"
}
}
}
})
end
sys.taskInit(netdrv_task_func)
2.5.2 原始帧方式
main.lua
lua
PROJECT = "TCP_MASTER"
VERSION = "001.000.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
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
}
-- 创建 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 task()
while true do
if tcp_master then
-- 每 2 秒调用一次读取函数
read_slave1_holding_registers()
else
log.info("exmodbus_test", "tcp_master 未创建,无法执行 read_slave1_holding_registers()")
end
sys.wait(2000)
end
end
-- 初始化任务
sys.taskInit(task)
netdrv_eth_spi.lua
lua
--[[
@module netdrv_eth_spi
@summary "通过SPI外挂CH390H芯片的以太网卡"驱动模块
@version 1.0
@date 2025.12.18
@author 马梦阳
@usage
本文件为"通过SPI外挂CH390H芯片的以太网卡"驱动模块,核心业务逻辑为:
1、打开CH390H芯片供电开关;
2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
3、以太网卡的连接状态发生变化时,在日志中进行打印;
直接使用Air8000开发板硬件测试即可;
本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
]]
local exnetif = require "exnetif"
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_ETH then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
end
end
local function ip_lose_func(adapter)
if adapter == socket.LWIP_ETH then
log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
end
end
-- 以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
-- 各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
-- 以太网断网后,内核固件会产生一个"IP_LOSE"消息
-- 各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
--此处订阅"IP_READY"和"IP_LOSE"两种消息
--在消息的处理函数中,仅仅打印了一些信息,便于实时观察"通过SPI外挂CH390H芯片的以太网卡"的连接状态
--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
-- 配置SPI外接以太网芯片CH390H的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_ETH
-- 本demo使用Air8000开发板测试,开发板上的硬件配置为:
-- GPIO140为CH390H以太网芯片的供电使能控制引脚
-- 使用spi1,片选引脚使用GPIO12
-- 如果使用的硬件不是Air8000开发板,根据自己的硬件配置修改以下参数
local function netdrv_task_func()
exnetif.set_priority_order({
{
ETHERNET = {
pwrpin = 140,
tp = netdrv.CH390,
opts = { spi = 1, cs = 12 },
-- 此处设置为静态 IP 地址
-- 如果不设置为静态 IP 地址,默认会使用 DHCP 协议动态获取 IP 地址
static_ip = {
ipv4 = "192.168.1.183",
mark = "255.255.255.0",
gw = "192.168.1.1"
}
}
}
})
end
sys.taskInit(netdrv_task_func)
2.6 Modbus TCP 从站
演示场景:
1、将设备配置为 modbus TCP 从站模式;
2、等待并且应答主站请求;
特别说明:
1、开发板和上位机需要接入同一路由下,开发板网卡驱动使用的是以太网;
2、使用 Air8000 开发板可直接使用下列代码;
3、对于其他主流模组(如 Air780Exx 系列、Air8101),若有官方开发板且设计有以太网口时可以参考对应的硬件手册在代码中配置相关管脚即可实现以太网功能,若没有官方开发板或者官方开发板没有设计以太网口时需要参考硬件手册选择合适的管脚并外挂以太网模块才能实现以太网功能;
4、对以太网不了解的用户可以先到对应模组下 软件开发资料-外设接口-以太网 文档进行学习,待熟悉之后再参考硬件手册进行配置更容易上手;
main.lua
lua
PROJECT = "TCP_SLAVE"
VERSION = "001.000.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- 开启以太网 wan(默认使用静态 IP 地址)
require "netdrv_eth_spi"
local exmodbus = require("exmodbus")
-- 创建 TCP 从站配置参数
-- 说明:创建 TCP 从站时只需要配置如下参数即可
local tcp_slave_config = {
-- 串口配置参数
mode = exmodbus.TCP_SLAVE, -- 通信模式
slave_id = 1, -- 从站 ID
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 = {}
for i = 0, request.reg_count - 1 do
local addr = request.start_addr + i
response[addr] = data_table[addr]
end
log.info("exmodbus_test", "读取成功,返回数据: ", table.concat(response, ", "))
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("从站回调函数已注册,开始监听主站请求...")
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
netdrv_eth_spi.lua
lua
--[[
@module netdrv_eth_spi
@summary "通过SPI外挂CH390H芯片的以太网卡"驱动模块
@version 1.0
@date 2025.12.10
@author 马梦阳
@usage
本文件为"通过SPI外挂CH390H芯片的以太网卡"驱动模块,核心业务逻辑为:
1、打开CH390H芯片供电开关;
2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
3、以太网卡的连接状态发生变化时,在日志中进行打印;
直接使用Air8000开发板硬件测试即可;
本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
]]
local exnetif = require "exnetif"
local function ip_ready_func(ip, adapter)
if adapter == socket.LWIP_ETH then
-- 在位置1和2设置自定义的DNS服务器ip地址:
-- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
-- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
-- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
-- 如果使用专网卡,不要使用这两行代码;
-- 如果使用国外的网络,不要使用这两行代码;
socket.setDNS(adapter, 1, "223.5.5.5")
socket.setDNS(adapter, 2, "114.114.114.114")
log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
end
end
local function ip_lose_func(adapter)
if adapter == socket.LWIP_ETH then
log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
end
end
-- 以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
-- 各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
-- 以太网断网后,内核固件会产生一个"IP_LOSE"消息
-- 各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
--此处订阅"IP_READY"和"IP_LOSE"两种消息
--在消息的处理函数中,仅仅打印了一些信息,便于实时观察"通过SPI外挂CH390H芯片的以太网卡"的连接状态
--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)
-- 配置SPI外接以太网芯片CH390H的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_ETH
-- 本demo使用Air8000开发板测试,开发板上的硬件配置为:
-- GPIO140为CH390H以太网芯片的供电使能控制引脚
-- 使用spi1,片选引脚使用GPIO12
-- 如果使用的硬件不是Air8000开发板,根据自己的硬件配置修改以下参数
local function netdrv_task_func()
exnetif.set_priority_order({
{
ETHERNET = {
pwrpin = 140,
tp = netdrv.CH390,
opts = { spi = 1, cs = 12 },
-- 此处设置为静态 IP 地址
-- 如果不设置为静态 IP 地址,默认会使用 DHCP 协议动态获取 IP 地址
static_ip = {
ipv4 = "192.168.1.183",
mark = "255.255.255.0",
gw = "192.168.1.1"
}
}
}
})
end
sys.taskInit(netdrv_task_func)
三、常量详解
扩展库常量,合宙LuatOS扩展库中定义的、不可重新赋值或者修改的固定值;
3.1 通信模式常量
3.1.1 exmodbus.RTU_MASTER
lua
常量含义:modbus RTU 主站;
数据类型:number;
示例代码:-- 创建 modbus RTU 主站
local create_config = {
-- 串口配置参数;
mode = exmodbus.RTU_MASTER, -- 通信模式
uart_id = 1, -- UART 端口号
baud_rate = 115200, -- 波特率
data_bits = 8, -- 数据位
stop_bits = 1, -- 停止位
parity_bits = uart.None, -- 校验位
byte_order = uart.LSB, -- 字节顺序
rs485_dir_gpio = 23, -- RS485 方向引脚
rs485_dir_rx_level = 0, -- RS485 接收方向电平
}
local rtu_master = exmodbus.create(create_config)
3.1.2 exmodbus.RTU_SLAVE
lua
常量含义:modbus RTU 从站;
数据类型:number;
示例代码:-- 创建 modbus RTU 从站
local create_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 = 23, -- RS485 方向引脚
rs485_dir_rx_level = 0, -- RS485 接收方向电平
}
local rtu_slave = exmodbus.create(create_config)
3.1.3 exmodbus.ASCII_MASTER
lua
常量含义:modbus ASCII 主站;
数据类型:number;
示例代码:-- 创建 modbus ASCII 主站
local create_config = {
-- 串口配置参数;
mode = exmodbus.ASCII_MASTER, -- 通信模式
uart_id = 1, -- UART 端口号
baud_rate = 115200, -- 波特率
data_bits = 8, -- 数据位
stop_bits = 1, -- 停止位
parity_bits = uart.None, -- 校验位
byte_order = uart.LSB, -- 字节顺序
rs485_dir_gpio = 23, -- RS485 方向引脚
rs485_dir_rx_level = 0, -- RS485 接收方向电平
}
local ascii_master = exmodbus.create(create_config)
3.1.4 exmodbus.ASCII_SLAVE
lua
常量含义:modbus ASCII 从站;
数据类型:number;
示例代码:-- 创建 modbus ascii 从站
local create_config = {
-- 串口配置参数;
mode = exmodbus.ASCII_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 = 23, -- RS485 方向引脚
rs485_dir_rx_level = 0, -- RS485 接收方向电平
}
local rtu_slave = exmodbus.create(create_config)
3.1.5 exmodbus.TCP_MASTER
lua
常量含义:modbus TCP 主站(客户端);
数据类型:number;
示例代码:
3.1.6 exmodbus.TCP_SLAVE
lua
常量含义:modbus TCP 从站(服务器);
数据类型:number;
示例代码:-- 创建 modbus tcp 从站
local create_config = {
-- 网络配置参数;
mode = exmodbus.TCP_SLAVE, -- 通信模式
adapter = socket.LWIP_ETH, -- 网卡 ID:LwIP 协议栈的以太网卡
port = 6000, -- 监听本地端口
}
local tcp_slave = exmodbus.create(create_config)
3.2 数据类型常量
3.2.1 exmodbus.COIL_STATUS
lua
常量含义:线圈;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 线圈 0-1 的状态时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.COIL_STATUS, -- 寄存器类型:线圈
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
3.2.2 exmodbus.INPUT_STATUS
lua
常量含义:离散输入;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 离散输入 0-1 的状态时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.INPUT_STATUS, -- 寄存器类型:离散输入
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
3.2.3 exmodbus.INPUT_REGISTER
lua
常量含义:输入寄存器;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 1 输入寄存器 0-1 的值时,配置读命令的字段参数
local read_config = {
slave_id = 1, -- 从站地址 1
reg_type = exmodbus.INPUT_REGISTER, -- 寄存器类型:输入寄存器
start_addr = 0x0000, -- 起始地址 0
reg_count = 0x0002, -- 读取 2 个寄存器
timeout = 1000 -- 超时时间 1000 ms
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
3.2.4 exmodbus.HOLDING_REGISTER
lua
常量含义:保持寄存器;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
3.3 操作类型常量
3.3.1 exmodbus.READ_COILS
lua
常量含义:读线圈状态;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.READ_COILS then
log.info("exmodbus_test", "操作类型为读线圈状态")
-- 用户代码
end
end
modbus:on(callback)
3.3.2 exmodbus.READ_DISCRETE_INPUTS
lua
常量含义:读离散输入状态;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.READ_DISCRETE_INPUTS then
log.info("exmodbus_test", "操作类型为读离散输入状态")
-- 用户代码
end
end
modbus:on(callback)
3.3.3 exmodbus.READ_HOLDING_REGISTERS
lua
常量含义:读保持寄存器;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.READ_HOLDING_REGISTERS then
log.info("exmodbus_test", "操作类型为读保持寄存器")
-- 用户代码
end
end
modbus:on(callback)
3.3.4 exmodbus.READ_INPUT_REGISTERS
lua
常量含义:读输入寄存器;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.READ_INPUT_REGISTERS then
log.info("exmodbus_test", "操作类型为读输入寄存器")
-- 用户代码
end
end
modbus:on(callback)
3.3.5 exmodbus.WRITE_SINGLE_COIL
lua
常量含义:写单个线圈状态;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.WRITE_SINGLE_COIL then
log.info("exmodbus_test", "操作类型为写单个线圈状态")
-- 用户代码
end
end
modbus:on(callback)
3.3.6 exmodbus.WRITE_SINGLE_HOLDING_REGISTER
lua
常量含义:写单个保持寄存器;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.WRITE_SINGLE_HOLDING_REGISTER then
log.info("exmodbus_test", "操作类型为写单个保持寄存器")
-- 用户代码
end
end
modbus:on(callback)
3.3.7 exmodbus.WRITE_MULTIPLE_HOLDING_REGISTERS
lua
常量含义:写多个保持寄存器;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.WRITE_MULTIPLE_HOLDING_REGISTERS then
log.info("exmodbus_test", "操作类型为写多个保持寄存器")
-- 用户代码
end
end
modbus:on(callback)
3.3.8 exmodbus.WRITE_MULTIPLE_COILS
lua
常量含义:写多个线圈状态;
数据类型:number;
示例代码:-- 1. 创建从站
-- 2. 接收主站请求时,判断收到的请求属于哪种操作类型
local function callback(request)
log.info("exmodbus_test", "rtu_slave 收到主站请求")
if request.func_code == exmodbus.WRITE_MULTIPLE_COILS then
log.info("exmodbus_test", "操作类型为写多个线圈状态")
-- 用户代码
end
end
modbus:on(callback)
3.4 异常码常量
3.4.1 exmodbus.ILLEGAL_FUNCTION
lua
常量含义:不支持请求的功能码;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.ILLEGAL_FUNCTION then
log.info("exmodbus_test", "不支持请求的功能码")
end
end
3.4.2 exmodbus.ILLEGAL_DATA_ADDRESS
lua
常量含义:请求的数据地址无效或超出范围;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.ILLEGAL_DATA_ADDRESS then
log.info("exmodbus_test", "请求的数据地址无效或超出范围")
end
end
3.4.3 exmodbus.ILLEGAL_DATA_VALUE
lua
常量含义:请求的数据值无效;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.ILLEGAL_DATA_VALUE then
log.info("exmodbus_test", "请求的数据值无效")
end
end
3.4.4 exmodbus.SLAVE_DEVICE_FAILURE
lua
常量含义:从站在执行操作时发生内部错误;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.SLAVE_DEVICE_FAILURE then
log.info("exmodbus_test", "从站在执行操作时发生内部错误")
end
end
3.4.5 exmodbus.ACKNOWLEDGE
lua
常量含义:请求已接受,但需要长时间处理;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.ACKNOWLEDGE then
log.info("exmodbus_test", "请求已接受,但需要长时间处理")
end
end
3.4.6 exmodbus.SLAVE_DEVICE_BUSY
lua
常量含义:从站正忙,无法处理请求;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.SLAVE_DEVICE_BUSY then
log.info("exmodbus_test", "从站正忙,无法处理请求")
end
end
3.4.7 exmodbus.NEGATIVE_ACKNOWLEDGE
lua
常量含义:无法执行编程功能;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.NEGATIVE_ACKNOWLEDGE then
log.info("exmodbus_test", "无法执行编程功能")
end
end
3.4.8 exmodbus.MEMORY_PARITY_ERROR
lua
常量含义:内存奇偶校验错误;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.MEMORY_PARITY_ERROR then
log.info("exmodbus_test", "内存奇偶校验错误")
end
end
3.4.9 exmodbus.GATEWAY_PATH_UNAVAILABLE
lua
常量含义:网关路径不可用;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.GATEWAY_PATH_UNAVAILABLE then
log.info("exmodbus_test", "网关路径不可用")
end
end
3.4.10 exmodbus.GATEWAY_TARGET_NO_RESPONSE
lua
常量含义:网关目标设备无响应;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
if read_result.execption_code == exmodbus.GATEWAY_TARGET_NO_RESPONSE then
log.info("exmodbus_test", "网关目标设备无响应")
end
end
3.5 响应结果常量
3.5.1 exmodbus.STATUS_SUCCESS
lua
常量含义:收到响应数据且数据有效;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_SUCCESS then
log.info("exmodbus_test", "收到响应数据且数据有效")
-- 用户代码
end
3.5.2 exmodbus.STATUS_DATA_INVALID
lua
常量含义:收到响应数据但数据损坏/校验失败;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_DATA_INVALID then
log.info("exmodbus_test", "收到响应数据但数据损坏/校验失败")
-- 用户代码
end
3.5.3 exmodbus.STATUS_EXCEPTION
lua
常量含义:收到 modbus 标准异常响应;
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_EXCEPTION then
log.info("exmodbus_test", "收到 modbus 标准异常响应")
-- 用户代码
end
3.5.4 exmodbus.STATUS_TIMEOUT
lua
常量含义:无任何响应(超时);
数据类型:number;
示例代码:-- 1. 创建主站
-- 2. 读取从站 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
}
-- 3. 向主站 1 发送读取请求
local read_result = modbus:read(read_config)
-- 4. 判断从站响应状态
if read_result.status == exmodbus.STATUS_TIMEOUT then
log.info("exmodbus_test", "无任何响应(超时)")
-- 用户代码
end
四、函数详解
4.1 exmodbus.create(config)
功能
创建并返回一个新的 modbus 主/从站实例;
注意事项
1、创建实例时需要将串口或者网络参数填写完整并且正确;
参数
由于 rtu/ascii 与 tcp 需要配置不同的 config 配置文件,因此下方将会分别进行介绍:
config(创建 RTU/ASCII 实例时)
lua
> 参数含义:创建 RTU 或 ASCII 主/站实例时的配置文件,用于配置串口参数;
> 此参数为 table 类型,字段参数如下:
> {
> 参数含义:通信模式;
> 数据类型:number;
> 取值范围:
> exmodbus.RTU\_MASTER;
> exmodbus.RTU\_SLAVE;
> exmodbus.ASCII\_MASTER;
> exmodbus.ASCII\_SLAVE;
> 是否必选:必选;
> 参数示例:配置通信模式为 modbus RTU 主站;
> mode = exmodbus.RTU\_MASTER
> 参数名称: config.mode
>
> 参数含义:串口 ID,uart0 写 0,uart1 写 1,以此类推;
> 数据类型:number;
> 取值范围:最大值取决于设备;
> 是否必选:必选;
> 参数示例:配置串口号为 1;
> uart\_id = 1
> 参数名称: config.uart\_id
>
> 参数含义:波特率;
> 数据类型:number;
> 取值范围:可选择波特率表:{2000000,921600,460800,230400,115200,57600,38400,19200,9600,4800,2400};
> 是否必选:可选(默认 115200);
> 参数示例:配置波特率为 115200;
> baud\_rate = 115200
> 参数名称: config.baud\_rate
>
> 参数含义:数据位;
> 数据类型:number;
> 取值范围:7/8;
> 是否必选:可选(默认 8);
> 参数示例:配置数据位为 8;
> data\_bits = 8
> 参数名称: config.data\_bits
>
> 参数含义:停止位;
> 数据类型:number;
> 取值范围:0.5/1/1.5/2 等;
> 是否必选:可选(默认 1);
> 参数示例:配置停止位为 1;
> stop\_bits = 1
> 参数名称: config.stop\_bits
>
> 参数含义:校验位;
> 数据类型:number;
> 取值范围:参考uart api中的常量详解;
> uart.None;
> uart.Even;
> uart.Odd;
> 是否必选:可选(默认 uart.None);
> 参数示例:配置校验位为无校验;
> parity\_bits = uart.None
> 参数名称: config.parity\_bits
>
> 参数含义:字节顺序;
> 数据类型:number;
> 取值范围:参考uart api中的常量详解;
> uart.LSB
> uart.MSB
> 是否必选:可选(默认 uart.LSB);
> 参数示例:配置字节顺序为小端序;
> byte\_order = uart.LSB
> 参数名称: config.byte\_order
>
> 参数含义:RS485 方向 GPIO 引脚;
> 数据类型:number;
> 取值范围:根据设备选择可用的 GPIO;
> 是否必选:可选(默认 0xffffffff);
> 参数示例:配置 RS485 方向 GPIO 引脚为 23;
> rs485\_dir\_gpio = 23
> 参数名称: config.rs485\_dir\_gpio
>
> 参数含义:RS485 接收方向电平;
> 数据类型:number;
> 取值范围:0/1;
> 是否必选:可选(默认为 0);
> 参数示例:配置 RS485 接收方向电平为 0;
> rs485\_dir\_rx\_level = 0
> 参数名称: config.rs485\_dir\_rx\_level
>
> 参数含义:字符拼接超时时间(仅对 RTU/ASCII 模式有效),单位毫秒;
> 在数据拼接过程中,等待后续数据片段到达的最大时间间隔;
> 数据类型:number;
> 取值范围:暂无;
> 是否必选:可选(默认不开启)
> 参数示例:配置字符拼接超时时间为 50ms;
> concat\_timeout = 50
> 参数名称: config.concat\_timeout
> }
>
> 数据类型:table;
> 取值范围:暂无;
> 是否必选:是;
> 参数实例:创建 modbus RTU 主站
> local config = {
> mode = exmodbus.RTU\_MASTER, -- 通信模式:RTU 主站
> uart\_id = 1, -- 串口 ID:uart1
> baud\_rate = 115200, -- 波特率:115200
> data\_bits = 8, -- 数据位:8
> stop\_bits = 1, -- 停止位:1
> parity\_bits = uart.None, -- 校验位:无校验
> byte\_order = uart.LSB, -- 字节顺序:小端序
> rs485\_dir\_gpio = 23, -- RS485 方向转换 GPIO 引脚
> rs485\_dir\_rx\_level = 0 -- RS485 接收方向电平:0 为低电平,1 为高电平
> concat\_timeout = 50 -- 字符拼接超时时间:50 毫秒
> }
> local rtu\_master = exmodbus.create(config)
> \-- 创建 modbus ASCII 主站
> local config = {
> mode = exmodbus.ASCII\_MASTER, -- 通信模式:RTU 主站
> uart\_id = 1, -- 串口 ID:uart1
> baud\_rate = 115200, -- 波特率:115200
> data\_bits = 8, -- 数据位:8
> stop\_bits = 1, -- 停止位:1
> parity\_bits = uart.None, -- 校验位:无校验
> byte\_order = uart.LSB, -- 字节顺序:小端序
> rs485\_dir\_gpio = 23, -- RS485 方向转换 GPIO 引脚
> rs485\_dir\_rx\_level = 0 -- RS485 接收方向电平:0 为低电平,1 为高电平
> concat\_timeout = 50 -- 字符拼接超时时间:50 毫秒
> }
> local ascii\_master = exmodbus.create(config)
config(创建 TCP 实例时)
> 参数含义:创建主/站实例时的配置文件,用于配置串口参数或者网络参数;
> 此参数为 table 类型,字段参数如下:
> {
> 参数含义:通信模式;
> 数据类型:number;
> 取值范围:
> exmodbus.TCP\_MASTER;
> exmodbus.TCP\_SLAVE;
> 是否必选:必选;
> 参数示例:配置通信模式为 modbus TCP 主站;
> mode = exmodbus.TCP\_MASTER
> 参数名称: config.mode
>
> 参数含义:网卡 ID;
> 数据类型:number;
> 取值范围:参考socket api中的常量详解;
> 是否必选:可选;
> 参数示例:配置网卡 ID 为 LwIP 协议栈的以太网卡;
> adapter = socket.LWIP\_ETH
> 参数名称: config.adapter
>
> 参数含义:服务器地址;
> 数据类型:string;
> 取值范围:不可超过 191 字符;
> 是否必选:必选;
> 参数示例:配置服务器地址为 192.168.1.100;
> ip\_address = "192.168.1.100"
> 参数名称: config.ip\_address
>
> 参数含义:服务器端口号;
> 数据类型:number;
> 取值范围:暂无;
> 是否必选:必选;
> 参数示例:配置服务器端口号为 502;
> port = 502
> 参数名称: config.port
>
> 参数含义:是否是UDP,true表示UDP,false或者nil表示TCP;
> 数据类型:boolean;
> 取值范围:true/false;
> 是否必选:可选(默认 TCP);
> 参数示例:配置不使用 UDP 协议,使用 TCP 协议;
> is\_udp = false
> 参数名称: config.is\_udp
>
> 参数含义:是否为加密传输;
> 数据类型:boolean;
> 取值范围:true/false;
> 取值范围:可选
> 参数示例:配置不使用加密传输;
> is\_tls = false
> 参数名称: config.is\_tls
>
> 参数含义:连接空闲多长时间后,开始发送第一个 keepalive 探针报文;
> 数据类型:number;
> 取值范围:大于 0 的正整数;
> 是否必选:可选(默认为不开启)
> 参数示例:配置连接空闲 300 秒后,开始发送第一个 keepalive 探针报文;
> keep\_idle = 300
> 参数名称: config.keep\_idle
>
> 参数含义:发送第一个探针后,如果没收到 ACK 回复,间隔多久再发送下一个探针,单位为秒;
> 数据类型:number;
> 取值范围:大于 0 的正整数;
> 是否必选:可选;
> 参数示例:配置发送第一个探针后,如果没收到 ACK 回复,10 秒后再发送下一个探针;
> keep\_interval = 10
> 参数名称: config.keep\_interval
>
> 参数含义:总共发送多少次探针后,如果依然没有回复,则判定连接已断开;
> 数据类型:number;
> 取值范围:大于 0 的正整数;
> 是否必选:可选;
> 参数示例:配置总共发送 3 次探针后,如果依然没有回复,则判定连接已断开;
> keep\_cnt\_ = 3
> 参数名称: config.keep\_cnt
>
> 参数含义:TCP 模式下的服务器 ca 证书数据,UDP模式下的 PSK;
> 数据类型:string;
> 取值范围:暂无;
> 是否必选:可选;
> 参数示例:配置不验证 ca 证书或者 PSK;
> server\_cert = nil
> 参数名称: config.server\_cert
>
> 参数含义:TCP 模式下的客户端证书数据,UDP 模式下的 PSK-ID;
> 数据类型:string;
> 取值范围:暂无;
> 是否必选:可选;
> 参数示例:配置不验证 客户端证书 或者 PSK-ID;
> client\_cert = nil
> 参数名称: config.client\_cert
>
> 参数含义:TCP 模式下的客户端私钥加密数据;
> 数据类型:string;
> 取值范围:暂无;
> 是否必选:可选;
> 参数示例:配置不使用客户端私钥加密数据;
> client\_key = nil
> 参数名称: config.client\_key
>
> 参数含义:TCP 模式下的客户端私钥口令数据;
> 数据类型:string;
> 取值范围:暂无;
> 是否必选:可选;
> 参数示例:配置不使用客户端私钥口令数据;
> client\_password = nil
> 参数名称: config.client\_password
> }
>
> 数据类型:table;
> 取值范围:暂无;
> 是否必选:是;
> 参数实例:创建 modbus TCP 主站
> local config = {
> mode = exmodbus.TCP\_MASTER, -- 通信模式:TCP 主站
> adapter = socket.LWIP\_ETH, -- 网卡 ID:LwIP 协议栈的以太网卡
> ip\_address = "192.168.1.100", -- 服务器 IP 地址:192.168.1.100(主站:服务器 IP;从站:本地 IP,从站可以不用填此参数)
> port = 502, -- 服务器端口号:502(主站:服务器端口;从站:本地端口)
> is\_udp = false, -- 是否使用 UDP 协议:不使用 UDP 协议,false/nil 表示使用 TCP 协议
> is\_tls = false, -- 是否使用加密传输:不使用加密传输,false/nil 表示不使用加密
> keep\_idle = 300, -- 连接空闲多长时间后,开始发送第一个 keepalive 探针报文:300 秒
> keep\_interval = 10, -- 发送第一个探针后,如果没收到 ACK 回复,间隔多久再发送下一个探针:10 秒
> keep\_cnt = 3, -- 总共发送多少次探针后,如果依然没有回复,则判断连接已断开:3 次
> server\_cert = nil, -- TCP 模式下的服务器 CA 证书数据,UDP 模式下的 PSK:如果客户端不需要验证服务器证书,则设为 nil 或空着
> client\_cert = nil, -- TCP 模式下的客户端证书数据,UDP 模式下的 PSK-ID:如果服务器不需要验证客户端证书,则设为 nil 或空着
> client\_key = nil, -- TCP 模式下的客户端私钥加密数据:如果服务器不需要验证客户端私钥,则设为 nil 或空着
> client\_password = nil -- TCP 模式下的客户端私钥口令数据:如果服务器不需要验证客户端私钥口令,则设为 nil 或空着
> }
> local tcp\_master = exmodbus.create(config)
返回值
local mb = exmodbus.create(config)
mb
lua
含义说明:modbus 实例对象;
数据类型:table;
取值范围:暂无;
返回值示例:-- 创建 modbus RTU 主站
local create_config = {
-- 串口配置参数;
mode = exmodbus.RTU_MASTER, -- 通信模式:RTU 主站
uart_id = 1, -- 串口 ID:uart1
baud_rate = 115200, -- 波特率:115200
data_bits = 8, -- 数据位:8
stop_bits = 1, -- 停止位:1
parity_bits = uart.None, -- 校验位:无校验
byte_order = uart.LSB, -- 字节顺序:小端序
rs485_dir_gpio = 23, -- RS485 方向转换 GPIO 引脚
rs485_dir_rx_level = 0 -- RS485 接收方向电平:0 为低电平,1 为高电平
}
local rtu_master = exmodbus.create(config)
示例
lua
-- 创建 modbus RTU 主站
local create_config = {
-- 串口配置参数;
mode = exmodbus.RTU_MASTER, -- 通信模式:RTU 主站
uart_id = 1, -- 串口 ID:uart1
baud_rate = 115200, -- 波特率:115200
data_bits = 8, -- 数据位:8
stop_bits = 1, -- 停止位:1
parity_bits = uart.None, -- 校验位:无校验
byte_order = uart.LSB, -- 字节顺序:小端序
rs485_dir_gpio = 23, -- RS485 方向转换 GPIO 引脚
rs485_dir_rx_level = 0 -- RS485 接收方向电平:0 为低电平,1 为高电平
}
local rtu_master = exmodbus.create(config)
4.2 modbus:read(config)
功能
主站向从站发送读取操作请求(阻塞接口);
支持通过"字段参数方式"或"原始帧方式"传入 config 配置参数,传入格式详见下方 config 参数示例;
如果你需要发送的请求报文是符合 modbus 标准格式,可以使用"字段参数方式"或者"原始帧方式";
如果你需要发送的请求报文是非标准格式,必须使用"原始帧方式",使用"字段参数方式"会导致解析的数据不正确;
注意事项
1、在调用此接口之前,需要先确保对应实例对象有效;
2、请求范围需要符合 modbus 标准协议要求;
参数
由于该接口支持通过"字段参数方式"或"原始帧方式"传入 config 配置参数,因此下方将会分别进行介绍:
config(字段参数方式传入时)
lua
> 参数含义:向从站发送读取操作请求时的配置参数;
> 该参数为 table 类型,字段如下:
> {
> 参数含义:从站地址(ID);
> 数据类型:number;
> 取值范围:0 ~ 247(0 为广播地址);
> 是否必选:必选;
> 参数示例:-- 配置从站地址为 1;
> slave\_id = 1
> 参数名称: config.slave\_id
>
> 参数含义:寄存器类型(数据类型);
> 数据类型:number;
> 取值范围:
> exmodbus.COIL\_STATUS;
> exmodbus.INPUT\_STATUS;
> exmodbus.INPUT\_REGISTER;
> exmodbus.HOLDING\_REGISTER;
> 是否必选:必选;
> 参数示例:-- 配置寄存器类型(数据类型)为保持寄存器;
> reg\_type = exmodbus.HOLDING\_REGISTER
> 参数名称: config.reg\_type
>
> 参数含义:寄存器起始地址;
> 数据类型:number;
> 取值范围:0 ~ 65535;
> 是否必选:必选;
> 参数示例:-- 配置寄存器起始地址为 0;
> start\_addr = 0
> 参数名称: config.start\_addr
>
> 参数含义:寄存器数量;
> 数据类型:number;
> 取值范围:1 ~ 125;
> 是否必选:必选;
> 参数示例:配置寄存器数量为 2;
> reg\_count = 2
> 参数名称: config.reg\_count
>
> 参数含义:超时时间,单位毫秒;
> 数据类型:number;
> 取值范围:暂无;
> 是否必选:可选,默认为 1 秒;
> 参数示例:配置超时时间为 1 秒;
> timeout = 1000
> 参数名称: config.timeout
> }
>
> 数据类型:table;
> 取值范围:暂无;
> 是否必选:必选;
> 参数示例:--1. 创建主站
> \--2. 读取操作时的配置参数(字段参数模式)
> \-- 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
> \-- 超时时间为 1 秒(1000 毫秒)
> local read\_config = {
> slave\_id = 1, -- 从站地址
> reg\_type = exmodbus.HOLDING\_REGISTER, -- 寄存器类型
> start\_addr = 0, -- 寄存器起始地址
> reg\_count = 2, -- 寄存器数量
> timeout = 1000 -- 超时时间
> }
> \-- 3. 执行读取请求
> local read\_result = modbus:read(read\_config)
config(原始帧方式传入时)
> 参数含义:向从站发送读取操作请求时的配置参数;
> 该参数为 table 类型,字段如下:
> {
> 参数含义:原始请求帧;
> 数据类型:string;
> 取值范围:暂无;
> 是否必选:可选;
> 参数示例:-- 读取从站 1 保持寄存器 0-1 的值时,配置读命令原始请求帧
> \-- 超时时间为 1 秒(1000 毫秒)
> raw\_request = string.char( -- 原始报文帧
> 0x01, -- 从站地址
> 0x03, -- 功能码
> 0x00, 0x00, -- 寄存器起始地址
> 0x00, 0x02, -- 寄存器数量
> 0xC4, 0x0B -- CRC 16 校验
> )
> 参数名称: config.raw\_request
>
> 参数含义:超时时间,单位毫秒;
> 数据类型:number;
> 取值范围:暂无;
> 是否必选:可选,默认为 1 秒;
> 参数示例:配置超时时间为 1 秒;
> timeout = 1000
> 参数名称: config.timeout
> }
>
> 数据类型:table;
> 取值范围:暂无;
> 是否必选:必选;
> 参数示例:-- 1. 创建主站
> \-- 2. 读取操作时的配置参数(原始帧模式)
> \-- 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数
> \-- 超时时间为 1 秒(1000 毫秒)
> local read\_config = {
> raw\_request = string.char( -- 原始报文帧
> 0x01, -- 从站地址
> 0x03, -- 功能码
> 0x00, 0x00, -- 寄存器起始地址
> 0x00, 0x02, -- 寄存器数量
> 0xC4, 0x0B -- CRC 16 校验
> )
> timeout = 1000 -- 超时时间
> }
> \-- 3. 执行读取请求
> local read\_result = modbus:read(read\_config)
返回值
local result = modbus:read(config)
result
lua
含义说明:向从站发送读取请求后的结果数据;
返回值为 table 类型,字段如下:
{
-- 参数含义:响应结果状态码;
-- 数据类型:number;
-- 取值范围:
-- exmodbus.STATUS_SUCCESS;
-- exmodbus.STATUS_DATA_INVALID;
-- exmodbus.STATUS_EXCEPTION;
-- exmodbus.STATUS_TIMEOUT;
status
-- 参数含义:异常码;
-- 数据类型:number;
-- 取值范围:暂无(取决于 modbus 标准异常码);
execption_code
-- 参数含义:寄存器值;
-- 该参数为 table 类型,字段如下:
{
[0] = , -- 第一个寄存器数值,索引与寄存器地址一致,则寄存器地址为0时此处索引为0
[1] = , -- 第二个寄存器数值,索引与寄存器地址一致,则寄存器地址为1时此处索引为1
...
...
...
}
-- 数据类型:table;
-- 取值范围:1 ~ 125;
data
-- 参数含义:原始响应帧;
-- 数据类型:string;
-- 取值范围:暂无;
raw_response
}
数据类型:table
取值范围:暂无;
返回值示例:-- 1. 创建主站
-- 2. 执行读取请求
local result = modbus:read(read_config)
-- 3. 判断从站响应状态
if result.status == exmodbus.STATUS_SUCCESS then
log.info("收到响应数据且数据有效")
elseif result.status == exmodbus.STATUS_DATA_INVALID then
log.info("收到响应数据但数据损坏/校验失败")
elseif result.status == exmodbus.STATUS_EXCEPTION then
log.info("收到 modbus 标准异常响应")
elseif result.status == exmodbus.STATUS_TIMEROUT then
log.info("无任何响应(超时)")
end
示例
lua
-- 1. 创建主站
-- 2. 执行读取请求
local result = modbus:read(config)
-- 3. 判断从站响应状态
if result.status == exmodbus.STATUS_SUCCESS then
log.info("收到响应数据且数据有效")
elseif result.status == exmodbus.STATUS_DATA_INVALID then
log.info("收到响应数据但数据损坏/校验失败")
elseif result.status == exmodbus.STATUS_EXCEPTION then
log.info("收到 modbus 标准异常响应")
elseif result.status == exmodbus.STATUS_TIMEROUT then
log.info("无任何响应(超时)")
end
4.3 modbus:write(config)
功能
主站向从站发送写入操作请求(阻塞接口);
支持通过"字段参数方式"或"原始帧方式"传入 config 配置参数,传入格式详见下方 config 参数示例;
如果你需要发送的请求报文是符合 modbus 标准格式,可以使用"字段参数方式"或者"原始帧方式";
如果你需要发送的请求报文是非标准格式,必须使用"原始帧方式",使用"字段参数方式"会导致解析的数据不正确;
注意事项
1、在调用此接口之前,需要先确保对应实例对象有效;
2、请求范围需要符合 modbus 标准协议要求;
参数
由于该接口支持通过"字段参数方式"或"原始帧方式"传入 config 配置参数,因此下方将会分别进行介绍:
config(字段参数方式传入时)
lua
> 参数含义:向从站发送写入操作请求时的配置参数;
> 该参数为 table 类型,字段如下:
> {
> 参数含义:从站地址(ID);
> 数据类型:number;
> 取值范围:0 ~ 247(0 为广播地址);
> 是否必选:必选;
> 参数示例:-- 配置从站地址为 1;
> slave\_id = 1
> 参数名称: config.slave\_id
>
> 参数含义:寄存器类型(数据类型);
> 数据类型:number;
> 取值范围:
> exmodbus.COIL\_STATUS;
> exmodbus.INPUT\_STATUS;
> exmodbus.INPUT\_REGISTER;
> exmodbus.HOLDING\_REGISTER;
> 是否必选:必选;
> 参数示例:-- 配置寄存器类型(数据类型)为保持寄存器;
> reg\_type = exmodbus.HOLDING\_REGISTER
> 参数名称: config.reg\_type
>
> 参数含义:寄存器起始地址;
> 数据类型:number;
> 取值范围:0 ~ 65535;
> 是否必选:必选;
> 参数示例:-- 配置寄存器起始地址为 0;
> start\_addr = 0
> 参数名称: config.start\_addr
>
> 参数含义:寄存器数量;
> 数据类型:number;
> 取值范围:1 ~ 125;
> 是否必选:必选;
> 参数示例:-- 配置寄存器数量为 2;
> reg\_count = 2
> 参数名称: config.reg\_count
>
> 参数含义:寄存器值;
> 该参数为 table 类型,字段如下:
> {
> \[0\] = , -- 第一个寄存器数值,索引与寄存器地址一致,则寄存器地址为0时此处索引为0
> \[1\] = , -- 第二个寄存器数值,索引与寄存器地址一致,则寄存器地址为1时此处索引为1
> ...
> ...
> ...
> }
> 数据类型:table;
> 取值范围:1 ~ 125;
> 是否必选:必选;
> 参数示例:-- 配置第一个寄存器值为 123,第二个寄存器值为 345;
> data =
> {
> \[start\_addr\] = 123, -- 第一个寄存器值
> \[start\_addr + 1\] = 345, -- 第二个寄存器值
> }
> 参数名称: config.data
>
> 参数含义:控制写单个寄存器时使用写单个功能码还是写多个功能码;
> 数据类型:boolean;
> 取值范围:true(使用写多个功能码)/false(使用写单个功能码);
> 是否必选:可选;
> 参数示例:-- 配置写单个寄存器时使用写多个功能码
> force\_multiple = true
> 参数名称: config.force\_multiple
>
> 参数含义:超时时间,单位毫秒;
> 数据类型:number;
> 取值范围:暂无;
> 是否必选:可选,默认为 1 秒;
> 参数示例:-- 配置超时时间为 1 秒;
> timeout = 1000
> 参数名称: config.timeout
> }
>
> 数据类型:table;
> 取值范围:暂无;
> 是否必选:必选;
> 参数示例:-- 1. 创建主站
> \-- 2. 写入操作时的配置参数(字段参数模式)
> \-- 写入从站 1 保持寄存器 0-1 的值时,配置写命令的字段参数
> \-- 超时时间为 1 秒(1000 毫秒)
> local write\_config = {
> slave\_id = 1, -- 从站地址
> reg\_type = exmodbus.HOLDING\_REGISTER, -- 寄存器类型
> start\_addr = 0, -- 寄存器起始地址
> reg\_count = 2, -- 寄存器数量
> data = -- 寄存器值
> {
> \[0\] = 123, -- 第一个寄存器值
> \[1\] = 345, -- 第二个寄存器值
> },
> timeout = 1000 -- 超时时间
> }
> \-- 3. 执行写入请求
> local write\_result = modbus:write(write\_config)
config(原始帧方式传入时)
> 参数含义:向从站发送写入操作请求时的配置参数;
> 该参数为 table 类型,字段如下:
> {
> 参数含义:原始请求帧;
> 数据类型:string;
> 取值范围:暂无;
> 是否必选:可选;
> 参数示例:-- 写入从站 1 保持寄存器 0-1 的值时,配置写命令的原始请求帧
> raw\_request = string.char( -- 原始报文帧
> 0x01, -- 从站地址
> 0x10, -- 功能码
> 0x00, 0x00, -- 寄存器起始地址
> 0x00, 0x02, -- 寄存器数量
> 0x00, 0x7B, -- 第一个寄存器数值
> 0x01, 0x59, -- 第二个寄存器数值
> 0x24, 0x71 -- CRC 16 校验
> )
> 参数名称: config.raw\_request
>
> 参数含义:超时时间,单位毫秒;
> 数据类型:number;
> 取值范围:暂无;
> 是否必选:可选,默认为 1 秒;
> 参数示例:-- 配置超时时间为 1 秒;
> timeout = 1000
> 参数名称: config.timeout
> }
>
> 数据类型:table;
> 取值范围:暂无;
> 是否必选:必选;
> 参数示例:-- 1. 创建主站
> \-- 2. 写入操作时的配置参数(原始帧模式)
> \-- 写入从站 1 保持寄存器 0-1 的值时,配置写命令的字段参数
> \-- 超时时间为 1 秒(1000 毫秒)
> local write\_config = {
> raw\_request = string.char( -- 原始报文帧
> 0x01, -- 从站地址
> 0x10, -- 功能码
> 0x00, 0x00, -- 寄存器起始地址
> 0x00, 0x02, -- 寄存器数量
> 0x00, 0x7B, -- 第一个寄存器数值
> 0x01, 0x59, -- 第二个寄存器数值
> 0x24, 0x71 -- CRC 16 校验
> )
> timeout = 1000 -- 超时时间
> }
> \-- 3. 执行写入请求
> local write\_result = modbus:write(write\_config)
返回值
local result = modbus:write(config)
result
lua
含义说明:向从站发送写入请求后的结果;
返回值为 table 类型,字段如下:
{
-- 参数含义:响应结果状态码;
-- 数据类型:number;
-- 取值范围:
-- exmodbus.STATUS_SUCCESS
-- exmodbus.STATUS_DATA_INVALID
-- exmodbus.STATUS_EXCEPTION
-- exmodbus.STATUS_TIMEOUT
status
-- 参数含义:异常码;
-- 数据类型:number;
-- 取值范围:暂无;
execption_code
-- 参数含义:原始响应帧;
-- 数据类型:string;
-- 取值范围:暂无;
raw_response
}
数据类型:table
取值范围:暂无;
返回值示例:-- 1. 创建主站
-- 2. 执行写入请求
local result = modbus:write(write_config)
-- 3. 判断从站响应状态
if result.status == exmodbus.STATUS_SUCCESS then
log.info("收到响应数据且数据有效")
elseif result.status == exmodbus.STATUS_DATA_INVALID then
log.info("收到响应数据但数据损坏/校验失败")
elseif result.status == exmodbus.STATUS_EXCEPTION then
log.info("收到 modbus 标准异常响应")
elseif result.status == exmodbus.STATUS_TIMEROUT then
log.info("无任何响应(超时)")
end
示例
lua
-- 1. 创建主站
-- 2. 执行写入请求
local result = modbus:write(write_config)
-- 3. 判断从站响应状态
if result.status == exmodbus.STATUS_SUCCESS then
log.info("收到响应数据且数据有效")
elseif result.status == exmodbus.STATUS_DATA_INVALID then
log.info("收到响应数据但数据损坏/校验失败")
elseif result.status == exmodbus.STATUS_EXCEPTION then
log.info("收到 modbus 标准异常响应")
elseif result.status == exmodbus.STATUS_TIMEROUT then
log.info("无任何响应(超时)")
end
4.4 modbus:destroy()
功能
销毁已创建的主/从站示例对象;
注意事项
暂无;
参数
无;
返回值
无;
示例
4.5 modbus:on(callback)
功能
此接口仅限设备做从站时使用;
当收到主站请求数据时,通过 callback 通知应用脚本处理;
应用脚本处理完之后,在 callback 中通知返回值,告知 exmodbus 扩展库返回给主站;
注意事项
1、在调用此接口之前,需要先确保对应实例对象有效;
参数
callback
lua
参数含义:事件回调函数;格式为:
function callback(request)
-- 用户代码
end
该回调函数接收 requset 一个参数,request 参数说明如下:
-- 参数含义:主站发送请求时的请求数据;
-- 该参数为 table 类型,字段如下:
{
-- 参数含义:从站地址(ID);
-- 数据类型:number;
-- 取值范围:0 ~ 247(0 为广播地址);
slave_id
-- 参数含义:功能码;
-- 数据类型:number;
-- 取值范围:暂无;
func_code
-- 参数含义:寄存器类型(数据类型);
-- 数据类型:number;
-- 取值范围:
-- exmodbus.COIL_STATUS;
-- exmodbus.INPUT_STATUS;
-- exmodbus.INPUT_REGISTER;
-- exmodbus.HOLDING_REGISTER;
reg_type
-- 参数含义:寄存器起始地址;
-- 数据类型:number;
-- 取值范围:0 ~ 65535;
start_addr
-- 参数含义:寄存器数量;
-- 数据类型:number;
-- 取值范围:1 ~ 125;
reg_count
-- 参数含义:寄存器值;
-- 该参数为 table 类型,字段如下:
{
[0] = , -- 第一个寄存器数值,索引与寄存器地址一致,则寄存器地址为0时此处索引为0
[1] = , -- 第二个寄存器数值,索引与寄存器地址一致,则寄存器地址为1时此处索引为1
...
...
...
}
-- 数据类型:table;
-- 取值范围:1 ~ 125;
data
}
-- 数据类型:table;
-- 取值范围:暂无;
-- 是否必选:必选;
request
数据类型:function;
取值范围:暂无;
是否必选:必选;
参数示例:-- 1. 创建从站
-- 2. 注册主站请求处理回调函数
function callback(request)
-- 用户代码
end
-- 3. 注册主站请求处理回调函数
modbus:on(callback)
返回值
无;
示例
lua
-- 0. 初始化一些参数
-- 当前从站地址(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
-- 1. 创建从站
-- 2. 注册主站请求处理回调函数
local function callback(request)
log.info("exmodbus_test", "收到主站请求")
-- 检查从站 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 = {}
for i = 0, request.reg_count - 1 do
local addr = request.start_addr + i
response[addr] = data_table[addr]
end
log.info("exmodbus_test", "读取成功,返回数据: ", table.concat(response, ", "))
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
-- 3. 注册主站请求处理回调函数
modbus:on(callback)
五、模组支持说明
支持 LuatOS 开发的所有产品都支持 exmodbus 扩展库。
今天就分享到这里啦~~~