Forráskód Böngészése

add:增加 Air8000 modbus rtu master/slave、tcp slave 示例代码

马梦阳 3 hónapja
szülő
commit
2f2c4a28f9

+ 149 - 0
module/Air8000/demo/modbus/rtu_master/main.lua

@@ -0,0 +1,149 @@
+--[[
+@module  main
+@summary LuatOS 用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+
+本 demo 演示的核心功能为:
+1、将设备配置为 modbus RTU 主站模式
+2、与从站 1 和 从站 2 进行通信
+    1. 对从站 1 进行 2 秒一次的读取保持寄存器 0-1 操作
+    2. 对从站 2 进行 4 秒一次的写入保持寄存器 0-1 操作
+3、读取温湿度传感器数据
+    1. 配置 modbus RTU 主站,读取温湿度传感器数据
+    2. 每 2 秒读取一次传感器数据并解析温度和湿度值
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、在 main.lua 中 require "param_field" 模块,可以演示标准 modbus RTU 请求报文格式的使用方式
+3、在 main.lua 中 require "raw_frame" 模块,可以演示非标准 modbus RTU 请求报文格式的使用方式
+4、在 main.lua 中 require "temp_hum_sensor" 模块,可以演示读取485温湿度传感器数值的使用方式
+5、require "param_field"、require "raw_frame" 和 require "temp_hum_sensor",不要同时打开,否则功能会有冲突
+
+特别说明:
+关于 RTU 报文,exmodbus 扩展库支持通过 字段参数 或 原始帧 两种方式进行配置
+这两种配置方式本质都由用户将其放入 table 中在调用接口时传入,区别如下:
+1、字段参数方式
+    这种方式需要用户将请求报文进行解析后,将其放入 table 中,例如:
+    读取请求:
+    local config = {
+        slave_id = 1,                         -- 从站地址
+        reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
+        start_addr = 0x0000,                  -- 寄存器起始地址
+        reg_count = 0x0002,                   -- 寄存器数量
+        timeout = 1000                        -- 超时时间
+    }
+    写入请求:
+    local config = {
+        slave_id = 2,                         -- 从站地址
+        reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
+        start_addr = 0x0000,                  -- 寄存器起始地址
+        reg_count = 0x0002,                   -- 寄存器数量
+        data = {
+            [start_addr] = 0x0012,            -- 寄存器 0 的值
+            [start_addr + 1] = 0x0034,        -- 寄存器 1 的值
+        }
+        force_multiple = true, -- 是否强制使用多个寄存器写入操作(写多个线圈功能码:0x0F;写多个寄存器功能码:0x10)
+        timeout = 1000                        -- 超时时间
+    }
+    
+2、原始帧方式
+    这种方式只需要用户将原始请求报文放入 table 中,例如:
+    读取请求:
+    local config = {
+        raw_request = string.char(
+            0x01,       -- 从站地址
+            0x03,       -- 功能码:读取保持寄存器
+            0x00, 0x00, -- 寄存器起始地址
+            0x00, 0x02, -- 寄存器数量
+            0xC4, 0x0B  -- CRC16校验码
+        )
+        timeout = 1000  -- 超时时间
+    }
+    写入请求:
+    local config = {
+        raw_request = string.char(
+            0x02,       -- 从站地址
+            0x10,       -- 功能码:写入保持寄存器
+            0x00, 0x00, -- 寄存器起始地址
+            0x00, 0x02, -- 寄存器数量
+            0x04,       -- 字节数量
+            0x00, 0x12, -- 寄存器 0 的值
+            0x00, 0x34, -- 寄存器 1 的值
+            0x5D, 0x39  -- CRC16校验码
+        )
+        timeout = 1000  -- 超时时间
+    }
+如果你需要发送的请求报文是符合 modbus RTU 标准格式,可以使用 字段参数 或者 原始帧 方式
+如果你需要发送的请求报文是非标准格式,必须使用 原始帧 方式,使用 字段参数 方式会导致解析的数据不正确
+更多说明参考本目录下的 readme.md 文件;
+]]
+
+--[[
+必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
+PROJECT:项目名,ascii string类型
+        可以随便定义,只要不使用,就行
+VERSION:项目版本号,ascii string类型
+        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
+            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
+            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
+        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+PROJECT = "RTU_MASTER"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- 以下三种方式只能选择其中一种打开,否则会导致功能冲突
+
+-- 加载 RTU 主站应用模块(字段参数方式)
+require "param_field"
+
+-- 加载 RTU 主站应用模块(原始帧方式)
+-- require "raw_frame"
+
+-- 加载温湿度传感器模块
+-- require "temp_hum_sensor"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 159 - 0
module/Air8000/demo/modbus/rtu_master/param_field.lua

@@ -0,0 +1,159 @@
+--[[
+@module  param_field
+@summary RTU 主站应用模块(字段参数方式)
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+本功能模块演示的内容为:
+1、将设备配置为 modbus RTU 主站模式
+2、与从站 1 和 从站 2 进行通信
+    1. 对从站 1 进行 2 秒一次的读取保持寄存器 0-1 操作
+    2. 对从站 2 进行 4 秒一次的写入保持寄存器 0-1 操作
+        可通过修改字段参数 force_multiple 为 true 来强制使用写多个功能码(写多个线圈功能码:0x0F;写多个寄存器功能码:0x10)
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、本功能模块只适合使用标准 modbus RTU 请求报文格式的用户
+3、如果你使用的是非标准 modbus RTU 请求报文格式,请参考 raw_frame 功能模块
+
+本文件没有对外接口,直接在 main.lua 中 require "param_field" 就可以加载运行;
+]]
+
+local exmodbus = require("exmodbus")
+
+
+gpio.setup(16, 1)         -- Air8000 开发板 RS485 芯片供电引脚
+local rs485_dir_gpio = 17 -- Air8000 开发板 RS485 方向引脚
+
+
+-- 创建 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
+}
+
+
+-- 初始化从站 2 数据结构;
+local slave2_data = {
+    data1 = 123,
+    data2 = 456
+}
+
+-- 定义从站 2 保持寄存器的起始地址;
+local start_addr = 0x0000
+
+-- 写入从站 2 保持寄存器 0-1 的值时,配置写命令的字段参数;
+local write_config = {
+    slave_id = 2,                                            -- 从站地址 2
+    reg_type = exmodbus.HOLDING_REGISTER,                    -- 寄存器类型:保持寄存器
+    start_addr = start_addr,                                 -- 起始地址 0
+    reg_count = 0x0002,                                      -- 写入 2 个寄存器
+    data = {
+        [start_addr] = slave2_data.data1,                    -- 第一个寄存器值
+        [start_addr + 1] = slave2_data.data2,                -- 第二个寄存器值
+    },                                                       -- 写入寄存器值
+    force_multiple = true,                                   -- 强制使用写多个功能码
+                                                                -- 设置为 true 时,写单个或多个线圈时强制功能码为 0x0F,写单个或多个保持寄存器时强制功能码为 0x10
+                                                                -- 设置为 false 时,写单个线圈时功能码为 0x05,写单个保持寄存器时功能码为 0x06,写多个线圈时功能码为 0x0F,写多个保持寄存器时功能码为 0x10
+    timeout = 1000                                           -- 超时时间 1000 ms
+}
+
+
+-- 创建 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 write_slave2_holding_registers()
+
+    log.info("exmodbus_test", "开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为", slave2_data.data1, ",寄存器 1 数值为", slave2_data.data2)
+
+    -- 执行写入操作
+    local write_result = rtu_master:write(write_config)
+
+    -- 根据返回状态处理结果
+    if write_result.status == exmodbus.STATUS_SUCCESS then
+        log.info("exmodbus_test", "成功写入从站 2 保持寄存器 0-1 的值")
+    elseif write_result.status == exmodbus.STATUS_DATA_INVALID then
+        log.info("exmodbus_test", "收到从站 2 的响应数据但数据损坏/校验失败")
+    elseif write_result.status == exmodbus.STATUS_EXCEPTION then
+        log.info("exmodbus_test", "收到从站 2 的 modbus 标准异常响应,异常码为", write_result.execption_code)
+    elseif write_result.status == exmodbus.STATUS_TIMEOUT then
+        log.info("exmodbus_test", "未收到从站 2 的响应(超时)")
+    end
+end
+
+-- 定时任务函数:每 2 秒调用一次读取函数,每 4 秒调用一次写入函数
+local function task()
+
+    local count = 0 -- 计数器
+
+    while true do
+        if rtu_master then
+            -- 每 2 秒调用一次读取函数
+            read_slave1_holding_registers()
+            if count == 0 then
+                -- 每 4 秒调用一次写入函数
+                write_slave2_holding_registers()
+            end
+            count = (count + 1) % 2
+        else
+            log.info("exmodbus_test", "rtu_master 未创建,无法执行 read_slave1_holding_registers()")
+        end
+        sys.wait(2000)
+    end
+end
+
+-- 初始化任务
+sys.taskInit(task)

+ 257 - 0
module/Air8000/demo/modbus/rtu_master/raw_frame.lua

@@ -0,0 +1,257 @@
+--[[
+@module  raw_frame
+@summary RTU 主站应用模块(原始帧方式)
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+本功能模块演示的内容为:
+1、将设备配置为 modbus RTU 主站模式
+2、与从站 1 和 从站 2 进行通信
+    1. 对从站 1 进行 2 秒一次的读取保持寄存器 0-1 操作
+    2. 对从站 2 进行 4 秒一次的写入保持寄存器 0-1 操作
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、本功能模块只适合使用非标准 modbus RTU 请求报文格式的用户
+3、如果你使用的是标准 modbus RTU 请求报文格式,请参考 param_field 功能模块
+
+本文件没有对外接口,直接在 main.lua 中 require "raw_frame" 就可以加载运行;
+]]
+
+local exmodbus = require("exmodbus")
+
+
+gpio.setup(16, 1)         -- Air8000 开发板 RS485 芯片供电引脚
+local rs485_dir_gpio = 17 -- Air8000 开发板 RS485 方向引脚
+
+
+-- 创建 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
+}
+
+
+-- 配置写入从站 2 保持寄存器 0-1 的值;
+local write_config = {
+    raw_request = string.char(
+        0x02,           -- 从站地址
+        0x10,           -- 功能码:写入保持寄存器
+        0x00, 0x00,     -- 寄存器起始地址
+        0x00, 0x02,     -- 寄存器数量
+        0x04,           -- 字节数量
+        0x00, 0x12,     -- 寄存器 0 的值
+        0x00, 0x34,     -- 寄存器 1 的值
+        0x5D, 0x39      -- 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 write_slave2_holding_registers()
+
+    log.info("exmodbus_test", "开始写入从站 2 保持寄存器 0-1 的值")
+
+    -- 执行写入操作
+    local write_result = rtu_master:write(write_config)
+
+    -- 根据返回状态处理结果
+    if write_result.status == exmodbus.STATUS_SUCCESS then
+        local resp = write_result.raw_response
+
+        -- 特别说明:
+        -- 接下来的判断是针对 modbus RTU 标准响应格式的应答原始帧来解析的
+        -- 在实际项目中,应根据自己项目中的实际应答原始帧格式进行解析
+        -- 如果实际格式与此处演示的格式不一致,需要修改接下来的解析代码
+
+        -- 1. 检查总长度:必须为 8 字节(1 地址 + 1 功能码 + 2 起始地址 + 2 寄存器数量 + 2 CRC)
+        if #resp ~= 8 then
+            log.info("exmodbus_test", "响应长度错误,期望 8 字节,实际:", #resp)
+            return
+        end
+
+        -- 2. 检查从站地址
+        if string.byte(resp, 1) ~= 0x02 then
+            log.info("exmodbus_test", "从站地址不匹配,收到:", string.byte(resp, 1))
+            return
+        end
+
+        -- 3. 检查功能码
+        local func_code = string.byte(resp, 2)
+        if func_code == 0x90 then
+            local exc_code = string.byte(resp, 3)
+            log.info("exmodbus_test", "从站返回异常响应,异常码:", exc_code)
+            return
+        elseif func_code ~= 0x10 then
+            log.info("exmodbus_test", "功能码错误,收到:", func_code)
+            return
+        end
+
+        -- 4. 检查起始地址(应为 0x0000)
+        local start_addr = string.unpack(">I2", resp, 3)
+        if start_addr ~= 0x0000 then
+            log.info("exmodbus_test", "起始地址不匹配,收到:", start_addr)
+            return
+        end
+
+        -- 5. 检查寄存器数量(应为 0x0002)
+        local reg_count = string.unpack(">I2", resp, 5)
+        if reg_count ~= 0x0002 then
+            log.info("exmodbus_test", "寄存器数量错误,期望 0x0002,实际:", reg_count)
+            return
+        end
+
+        -- 6. 校验CRC
+        -- 计算前 6 字节的 CRC
+        local crc_calculated = crypto.crc16_modbus(resp:sub(1, 6))
+        -- 提取接收到的 CRC
+        local crc_received = string.unpack("<I2", resp, 7)
+        -- 比较 CRC
+        if crc_calculated ~= crc_received then
+            log.info("exmodbus_test", "CRC 校验错误,计算值:", crc_calculated, ",接收值:", crc_received)
+            return
+        end
+
+        log.info("exmodbus_test", "成功写入从站 2 保持寄存器 0-1")
+    elseif write_result.status == exmodbus.STATUS_TIMEOUT then
+        log.info("exmodbus_test", "未收到从站 2 的响应(超时)")
+    end
+end
+
+-- 定时任务函数:每 2 秒调用一次读取函数,每 4 秒调用一次写入函数
+local function task()
+
+    local count = 0 -- 计数器
+
+    while true do
+        if rtu_master then
+            -- 每 2 秒调用一次读取函数
+            read_slave1_holding_registers()
+            if count == 0 then
+                -- 每 4 秒调用一次写入函数
+                write_slave2_holding_registers()
+            end
+            count = (count + 1) % 2
+        else
+            log.info("exmodbus_test", "rtu_master 未创建,无法执行 read_slave1_holding_registers()")
+        end
+        sys.wait(2000)
+    end
+end
+
+
+-- 初始化任务
+sys.taskInit(task)

+ 444 - 0
module/Air8000/demo/modbus/rtu_master/readme.md

@@ -0,0 +1,444 @@
+## 演示模块概述
+
+1、main.lua:主程序入口;
+
+2、param_field.lua:RTU 主站应用模块(字段参数方式);
+
+3、raw_frame.lua:RTU 主站应用模块(原始帧方式);
+
+4、temp_hum_sensor.lua:485温湿度传感器读取模块;
+
+## 演示功能概述
+
+本 demo 演示的核心功能为:
+
+1、将设备配置为 modbus RTU 主站模式
+
+2、与从站 1 和 从站 2 进行通信
+
+- 对从站 1 进行 2 秒一次的读取保持寄存器 0-1 操作
+- 对从站 2 进行 4 秒一次的写入保持寄存器 0-1 操作
+
+3、读取温湿度传感器数据
+
+- 配置 modbus RTU 主站,读取温湿度传感器数据
+- 每 2 秒读取一次传感器数据并解析温度和湿度值
+
+
+
+注意事项:
+
+1、该示例程序需要搭配 exmodbus 扩展库使用
+
+2、在 main.lua 中 require "param_field" 模块,可以演示标准 modbus RTU 请求报文格式的使用方式
+
+3、在 main.lua 中 require "raw_frame" 模块,可以演示非标准 modbus RTU 请求报文格式的使用方式
+
+4、在 main.lua 中 require "temp_hum_sensor" 模块,可以演示读取485温湿度传感器数值的使用方式
+
+5、require "param_field"、require "raw_frame" 和 require "temp_hum_sensor",不要同时打开,否则功能会有冲突
+
+
+
+特别说明:
+
+关于 RTU 报文,exmodbus 扩展库支持通过 字段参数 或 原始帧 两种方式进行配置
+
+这两种配置方式本质都由用户将其放入 table 中在调用接口时传入,区别如下:
+
+1、字段参数方式
+
+这种方式需要用户将请求报文进行解析后,将其放入 table 中,例如:
+
+```lua
+-- 读取请求:
+local config = {
+    slave_id = 1,                         -- 从站地址
+    reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
+    start_addr = 0x0000,                  -- 寄存器起始地址
+    reg_count = 0x0002,                   -- 寄存器数量
+    timeout = 1000                        -- 超时时间
+}
+
+-- 写入请求:
+local config = {
+    slave_id = 2,                         -- 从站地址
+    reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
+    start_addr = 0x0000,                  -- 寄存器起始地址
+    reg_count = 0x0002,                   -- 寄存器数量
+    data = {
+        [start_addr] = 0x0012,            -- 寄存器 0 的值
+        [start_addr + 1] = 0x0034,        -- 寄存器 1 的值
+    }
+    force_multiple = true, -- 是否强制使用多个寄存器写入操作(写多个线圈功能码:0x0F;写多个寄存器功能码:0x10)
+    timeout = 1000                        -- 超时时间
+}
+```
+
+2、原始帧方式
+
+这种方式只需要用户将原始请求报文放入 table 中,例如:
+
+```Lua
+-- 读取请求:
+local config = {
+    raw_request = string.char(
+        0x01,       -- 从站地址
+        0x03,       -- 功能码:读取保持寄存器
+        0x00, 0x00, -- 寄存器起始地址
+        0x00, 0x02, -- 寄存器数量
+        0xC4, 0x0B  -- CRC16校验码
+    )
+    timeout = 1000  -- 超时时间
+}
+
+-- 写入请求:
+local config = {
+    raw_request = string.char(
+        0x02,       -- 从站地址
+        0x10,       -- 功能码:写入保持寄存器
+        0x00, 0x00, -- 寄存器起始地址
+        0x00, 0x02, -- 寄存器数量
+        0x04,       -- 字节数量
+        0x00, 0x12, -- 寄存器 0 的值
+        0x00, 0x34, -- 寄存器 1 的值
+        0x5D, 0x39  -- CRC16校验码
+    )
+    timeout = 1000  -- 超时时间
+}
+```
+
+如果你需要发送的请求报文是符合 modbus RTU 标准格式,可以使用 字段参数 或者 原始帧 方式
+
+如果你需要发送的请求报文是非标准格式,必须使用 原始帧 方式,使用 字段参数 方式会导致解析的数据不正确
+
+## 演示硬件环境
+
+1、Air8000 开发板一块
+
+2、TYPE-C USB数据线一根
+
+3、USB-RS485 串口板
+
+> 此处购买链接仅为推荐,如有问题请直接联系店家
+
+- 购买链接:https://e.tb.cn/h.7YHZDX57ex5G68h?tk=AvE4fsurFy8
+
+4、气体浓度变送器(RS485 版)
+
+> 如果你是小白,建议直接购买同款变送器,由于不同型号的温湿度模块默认的参数也会有所区别
+
+- 购买链接:https://e.tb.cn/h.71oGcXUfrc46J83?tk=RJ1WfsuBfuY
+
+Air8000 与 USB-RS385 串口板接线图如下:
+
+![](https://docs.openluat.com/cdn/image/Air8000_rs485.png)
+
+Air8000 与气体浓度变送器(RS-485 版)接线图如下:
+
+![](https://docs.openluat.com/cdn/image/Air8000_RS485.jpg)
+
+## 演示软件环境
+
+1、[Luatools下载调试工具](https://docs.openluat.com/air8000/luatos/common/download/)
+
+2、[Air8000 V2018 版本](https://docs.openluat.com/air8000/luatos/firmware/)(理论上最新版本固件也可以,如果使用最新版本的固件不可以,可以烧录 V2018-1 固件对比验证)
+
+3、[摩尔信使(MThings)官网](https://www.gulink.cn/)(用于模拟 modbus 从站设备)
+
+## 演示核心步骤
+
+### RTU 主站应用模块(字段参数方式,对应 param_field.lua)
+
+1、搭建硬件环境
+
+- 将 USB-RS485 串口板与 Air8000 开发板进行连接
+- 将 USB-RS485 串口板 与 Air8000 开发板的 USB 端同时接在电脑上
+- 参考图见 演示硬件环境
+
+2、在摩尔信使上配置模拟 RTU 从站设备环境
+
+- 点击左上角的 “通道管理”按钮,在 “通道管理” 窗口选择对应的串口(USB-RS485 串口板与 Air8000 开发板进行 485 通信时的端口),点击对应串口后面的 “配置” 按钮,在 “串口参数配置” 窗口配置串口参数(要求与代码中调用 exmodbus.create 接口时填入的配置参数一致),操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/1.png)
+
+- 点击左上角的 “添加设备”按钮,在 “添加设备” 窗口对通信设备参数进行配置,配置好后点击 “添加” 按钮,左侧栏即为添加后的效果,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/2.png)
+
+- 点击左侧的第一个从站(我这里显示为 “COM36-001”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增保持寄存器 0 和 保持寄存器 1,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/3.png)
+
+- 点击左侧的第二个从站(我这里显示为 “COM36-002”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增保持寄存器 0 和 保持寄存器 1,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/4.png)
+
+- 此时在摩尔信使上的配置操作已经完成,如果需要在摩尔信使上查看报文,那么操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/5.png)
+
+3、调整软件代码
+
+- 打开 require "param_field" ,注释掉 require "raw_frame" 和 require "temp_hum_sensor",操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/modbus/1.png)
+
+4、打开 Luatools 工具,根据要求烧录本次所需要的内核固件和脚本代码
+
+5、烧录成功后,自动开机运行
+
+6、开机运行后 Luatools 工具上记录的日志如下:
+
+```
+[2025-12-08 14:14:56.464][000000000.394] I/user.main RTU_MASTER 001.000.000
+[2025-12-08 14:14:56.468][000000000.425] Uart_ChangeBR 1338:uart1, 115200 115203 26000000 3611
+[2025-12-08 14:14:56.472][000000000.426] I/user.exmodbus 串口 1 初始化成功,波特率 115200
+[2025-12-08 14:14:56.477][000000000.426] I/user.exmodbus_test rtu_master 创建成功
+[2025-12-08 14:14:56.481][000000000.427] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:14:57.124][000000001.433] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:14:57.128][000000001.433] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:14:58.135][000000002.437] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+[2025-12-08 14:15:00.139][000000004.438] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:01.139][000000005.441] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:15:03.144][000000007.442] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:04.140][000000008.445] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:15:04.146][000000008.446] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:05.149][000000009.449] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+[2025-12-08 14:15:07.139][000000011.450] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:08.146][000000012.453] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:15:10.148][000000014.454] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:11.154][000000015.457] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:15:11.165][000000015.458] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:12.160][000000016.461] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+[2025-12-08 14:15:14.150][000000018.462] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:15.158][000000019.465] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:15:17.166][000000021.466] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:18.160][000000022.469] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:15:18.163][000000022.470] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:18.177][000000022.486] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 14:15:20.178][000000024.487] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:20.207][000000024.503] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 14:15:22.197][000000026.504] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:22.228][000000026.519] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 14:15:22.231][000000026.520] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:22.235][000000026.534] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 14:15:24.224][000000028.534] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:24.255][000000028.550] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 14:15:26.246][000000030.551] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:26.257][000000030.569] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 14:15:26.265][000000030.570] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:26.275][000000030.583] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 14:15:28.284][000000032.584] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:28.289][000000032.598] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 14:15:30.292][000000034.599] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:30.309][000000034.614] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 14:15:30.322][000000034.615] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:30.334][000000034.634] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值
+```
+
+7、如下图所示,鼠标右键点击 “通道” 下方的按钮,当我们把摩尔信使上由上位机与 Air8000 之间的串口通道关闭后,此时 Air8000 在发送请求时便会收不到响应,Luatools 工具上显示的日志如下:
+
+![](https://docs.openluat.com/cdn/image/MThings/6.png)
+
+```
+[2025-12-08 14:14:56.481][000000000.427] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:14:57.124][000000001.433] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:14:57.128][000000001.433] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:14:58.135][000000002.437] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+```
+
+8、如下图所示,鼠标右键点击 “通道” 下方的按钮,当我们把摩尔信使上由上位机与 Air8000 之间的串口通道打开后,此时 Air8000 在发送请求时便会接收到响应,Luatools 工具与摩尔信使上显示的日志如下:
+
+![](https://docs.openluat.com/cdn/image/MThings/7.png)
+
+```
+[2025-12-08 14:15:18.163][000000022.470] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:18.177][000000022.486] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 14:15:20.178][000000024.487] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:20.207][000000024.503] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+```
+
+![](https://docs.openluat.com/cdn/image/MThings/8.png)
+
+9、关于 Air8000 执行读取和写入请求后,摩尔信使上位机的数值变化如下图所示:
+
+![](https://docs.openluat.com/cdn/image/MThings/9.png)
+
+![](https://docs.openluat.com/cdn/image/MThings/10.png)
+
+### RTU 主站应用模块(原始帧方式,对应 raw_frame.lua)
+
+1、搭建硬件环境
+
+- 将 USB-RS485 串口板与 Air8000 开发板进行连接
+- 将 USB-RS485 串口板 与 Air8000 开发板的 USB 端同时接在电脑上
+- 参考图见 演示硬件环境
+
+2、在摩尔信使上配置模拟 RTU 从站设备环境
+
+- 点击左上角的 “通道管理”按钮,在 “通道管理” 窗口选择对应的串口(USB-RS485 串口板与 Air8000 开发板进行 485 通信时的端口),点击对应串口后面的 “配置” 按钮,在 “串口参数配置” 窗口配置串口参数(要求与代码中调用 exmodbus.create 接口时填入的配置参数一致),操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/1.png)
+
+- 点击左上角的 “添加设备”按钮,在 “添加设备” 窗口对通信设备参数进行配置,配置好后点击 “添加” 按钮,左侧栏即为添加后的效果,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/2.png)
+
+- 点击左侧的第一个从站(我这里显示为 “COM36-001”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增保持寄存器 0 和 保持寄存器 1,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/3.png)
+
+- 点击左侧的第二个从站(我这里显示为 “COM36-002”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增保持寄存器 0 和 保持寄存器 1,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/4.png)
+
+- 此时在摩尔信使上的配置操作已经完成,如果需要在摩尔信使上查看报文,那么操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/5.png)
+
+3、调整软件代码
+
+- 打开 require "raw_frame" ,注释掉 require "param_field" 和 require "temp_hum_sensor",操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/modbus/2.png)
+
+4、打开 Luatools 工具,根据要求烧录本次所需要的内核固件和脚本代码
+
+5、烧录成功后,自动开机运行
+
+6、开机运行后 Luatools 工具上记录的日志如下:
+
+```
+[2025-12-08 15:14:13.691][000000000.703] I/user.main RTU_MASTER 001.000.000
+[2025-12-08 15:14:13.695][000000000.736] Uart_ChangeBR 1338:uart1, 115200 115203 26000000 3611
+[2025-12-08 15:14:13.699][000000000.737] I/user.exmodbus 串口 1 初始化成功,波特率 115200
+[2025-12-08 15:14:13.702][000000000.737] I/user.exmodbus_test rtu_master 创建成功
+[2025-12-08 15:14:13.706][000000000.738] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:14.595][000000001.743] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 15:14:14.600][000000001.743] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:15.602][000000002.747] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+[2025-12-08 15:14:17.591][000000004.747] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:18.591][000000005.750] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 15:14:20.591][000000007.750] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:21.602][000000008.754] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 15:14:21.611][000000008.754] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:22.607][000000009.758] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+[2025-12-08 15:14:24.603][000000011.758] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:25.612][000000012.762] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 15:14:27.618][000000014.762] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:28.616][000000015.766] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 15:14:28.620][000000015.766] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:29.623][000000016.770] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+[2025-12-08 15:14:31.615][000000018.770] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:31.646][000000018.790] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:33.633][000000020.790] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:33.651][000000020.808] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:33.656][000000020.808] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:33.662][000000020.821] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1
+[2025-12-08 15:14:35.670][000000022.821] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:35.682][000000022.839] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:37.692][000000024.839] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:37.699][000000024.854] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:37.705][000000024.854] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:37.716][000000024.870] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1
+[2025-12-08 15:14:39.717][000000026.870] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:39.748][000000026.887] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:41.737][000000028.887] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:41.753][000000028.902] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:41.755][000000028.902] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:41.758][000000028.916] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1
+[2025-12-08 15:14:43.765][000000030.916] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:43.795][000000030.934] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:45.788][000000032.934] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:45.795][000000032.949] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:45.801][000000032.949] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:45.812][000000032.964] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1
+```
+
+7、如下图所示,鼠标右键点击 “通道” 下方的按钮,当我们把摩尔信使上由上位机与 Air8000 之间的串口通道关闭后,此时 Air8000 在发送请求时便会收不到响应,Luatools 工具上显示的日志如下:
+
+![](https://docs.openluat.com/cdn/image/MThings/6.png)
+
+```
+[2025-12-08 15:14:13.706][000000000.738] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:14.595][000000001.743] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 15:14:14.600][000000001.743] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:15.602][000000002.747] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+```
+
+8、如下图所示,鼠标右键点击 “通道” 下方的按钮,当我们把摩尔信使上由上位机与 Air8000 之间的串口通道打开后,此时 Air8000 在发送请求时便会接收到响应,Luatools 工具与摩尔信使上显示的日志如下:
+
+> 程序设计为每隔 2 秒执行一次读取,每隔 4 秒执行一次写入,在日志上呈现出现就是先执行两次读取再执行一次写入
+
+![](https://docs.openluat.com/cdn/image/MThings/7.png)
+
+```
+[2025-12-08 15:14:31.615][000000018.770] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:31.646][000000018.790] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:33.633][000000020.790] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:33.651][000000020.808] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:33.656][000000020.808] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:33.662][000000020.821] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1
+```
+
+![](https://docs.openluat.com/cdn/image/MThings/11.png)
+
+9、关于 Air8000 执行读取和写入请求后,摩尔信使上位机的数值变化如下图所示:
+
+![](https://docs.openluat.com/cdn/image/MThings/9.png)
+
+![](https://docs.openluat.com/cdn/image/MThings/10.png)
+
+### 485 温湿度传感器读取模块(对应 temp_hum_sensor.lua)
+
+1、搭建硬件环境
+
+- 将气体浓度变送器(RS-485 版)的 '+' 和 '-' 与供电设备(稳压电源等)进行连接,供电范围 8V ~ 36V DC
+
+- 将气体浓度变送器(RS-485 版)与 Air8000 开发板进行连接
+- 将 Air8000 开发板的 USB 端接在电脑上
+- 参考图见 演示硬件环境
+
+2、了解气体浓度变送器(RS-485 版)
+
+- 该变送器模块上电后默认输出数据,从站地址默认为 1,波特率默认为 9600
+- 温度传感器数值通过保持寄存器地址 0x001E 输出,输出数据为 16 位有符号整数(-0x7FFF ~ +0x7FFF),
+  - 数据范围为 -40℃ ~ +85℃,分辨率为 0.1℃
+  - 注:寄存器值为 235,实际温度值为 235 * 0.1 = 23.5
+- 湿度传感器对应保持寄存器地址 0x001F 输出,输出数据为 16 位无符号整数(0 ~ 0xFFFF)
+  - 数据范围为 0%RH ~ 85%RH,分辨率为 0.1%RH
+  - 注:寄存器值为 653,实际湿度值为 653 * 0.1 = 65.3
+
+3、调整软件代码
+
+- 打开 require "temp_hum_sensor" ,注释掉 require "raw_frame" 和 require "param_field" ,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/modbus/3.png)
+
+4、打开 Luatools 工具,根据要求烧录本次所需要的内核固件和脚本代码,为气体浓度变送器(RS-485 版)进行供电(提前通电也可以)
+
+5、烧录成功后,自动开机运行
+
+6、开机运行后 Luatools 工具上记录的日志如下:
+
+```
+[2025-12-09 22:11:43.077][000000000.588] I/user.main RTU_MASTER 001.000.000
+[2025-12-09 22:11:43.092][000000000.620] Uart_ChangeBR 1338:uart1, 9600 9600 26000000 43333
+[2025-12-09 22:11:43.101][000000000.621] I/user.exmodbus 串口 1 初始化成功,波特率 9600
+[2025-12-09 22:11:43.112][000000000.621] I/user.temp_hum_sensor RTU 主站创建成功
+[2025-12-09 22:11:43.123][000000000.622] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:43.138][000000000.724] I/user.temp_hum_sensor 读取成功,温度为 16.70000 ℃,湿度为 83.20000 %RH
+[2025-12-09 22:11:43.154][000000002.724] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:43.166][000000002.761] I/user.temp_hum_sensor 读取成功,温度为 16.80000 ℃,湿度为 82.90000 %RH
+[2025-12-09 22:11:43.172][000000004.761] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:43.178][000000004.793] I/user.temp_hum_sensor 读取成功,温度为 16.60000 ℃,湿度为 83.50000 %RH
+[2025-12-09 22:11:43.388][000000006.794] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:43.399][000000006.828] I/user.temp_hum_sensor 读取成功,温度为 16.90000 ℃,湿度为 82.70000 %RH
+[2025-12-09 22:11:45.096][000000008.828] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:45.128][000000008.858] I/user.temp_hum_sensor 读取成功,温度为 17.00000 ℃,湿度为 82.30000 %RH
+[2025-12-09 22:11:47.116][000000010.858] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:47.148][000000010.890] I/user.temp_hum_sensor 读取成功,温度为 16.80000 ℃,湿度为 83.00000 %RH
+[2025-12-09 22:11:49.156][000000012.890] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:49.188][000000012.920] I/user.temp_hum_sensor 读取成功,温度为 16.70000 ℃,湿度为 83.40000 %RH
+```

+ 129 - 0
module/Air8000/demo/modbus/rtu_master/temp_hum_sensor.lua

@@ -0,0 +1,129 @@
+--[[
+@module  temp_hum_sensor
+@summary 485温湿度传感器读取模块
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+本功能模块演示的内容为:
+1、将设备配置为 modbus RTU 主站模式
+2、读取485接口的温湿度传感器数据
+3、每 2 秒读取一次传感器数据并解析温度和湿度值
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、参考对应传感器手册,配置从站地址、寄存器地址等参数
+
+特别说明:
+1、本示例演示使用的是中盛科技的气体浓度变送器(RS485 版),
+    该变送器模块上电后默认输出数据,从站地址为 1,波特率为 9600
+2、温度传感器数值通过保持寄存器地址 0x001E 输出,输出数据为 16 位有符号整数(-0x7FFF ~ +0x7FFF),
+    数据范围为 -40℃ ~ +85℃,分辨率为 0.1℃
+    注:寄存器值为 235,实际温度值为 235 * 0.1 = 23.5
+3、湿度传感器数值通过保持寄存器地址 0x001F 输出,输出数据为 16 位无符号整数(0 ~ 0xFFFF),
+    数据范围为 0%RH ~ 85%RH,分辨率为 0.1%RH
+    注:寄存器值为 653,实际湿度值为 653 * 0.1 = 65.3
+
+本文件没有对外接口,直接在 main.lua 中 require "temp_hum_sensor" 就可以加载运行;
+]]
+
+local exmodbus = require("exmodbus")
+
+
+-- Air8000 开发板硬件配置
+gpio.setup(16, 1)         -- RS485 芯片供电引脚
+local rs485_dir_gpio = 17 -- RS485 方向引脚
+
+
+-- 创建 RTU 主站配置参数
+local create_config = {
+    -- 串口配置参数;
+    mode = exmodbus.RTU_MASTER,      -- 通信模式:RTU主站
+    uart_id = 1,                     -- UART 端口号:1
+    baud_rate = 9600,                -- 波特率:9600(根据传感器手册调整)
+    data_bits = 8,                   -- 数据位:8
+    stop_bits = 1,                   -- 停止位:1
+    parity_bits = uart.None,         -- 校验位:无
+    byte_order = uart.LSB,           -- 字节顺序:LSB(低位优先)
+    rs485_dir_gpio = rs485_dir_gpio, -- RS485 方向引脚:17
+    rs485_dir_rx_level = 0,          -- RS485 接收方向电平:0
+}
+
+
+-- 初始化传感器数据结构
+-- 用于记录传感器的温度和湿度值
+local sensor_data = {
+    temperature = 0, -- 温度值
+    humidity = 0     -- 湿度值
+}
+
+-- 配置读取温湿度传感器的参数
+local read_config = {
+    slave_id = 1,                         -- 从站地址:1
+    reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
+    start_addr = 0x001E,                  -- 起始地址:0x001E(温度寄存器)
+    reg_count = 0x0002,                   -- 读取 2 个寄存器:温度和湿度
+    timeout = 1000                        -- 超时时间 1000 ms
+}
+
+
+-- 创建 RTU 主站实例
+local rtu_master = exmodbus.create(create_config)
+
+-- 判断主站是否创建成功并记录日志
+if not rtu_master then
+    log.info("temp_hum_sensor", "RTU 主站创建失败")
+else
+    log.info("temp_hum_sensor", "RTU 主站创建成功")
+end
+
+
+-- 读取温湿度传感器数据的函数
+local function read_temp_humidity()
+
+    log.info("temp_hum_sensor", "开始读取温湿度传感器数据")
+
+    -- 执行读取操作
+    local read_result = rtu_master:read(read_config)
+
+    -- 根据返回状态处理结果
+    if read_result.status == exmodbus.STATUS_SUCCESS then
+        -- 读取原始寄存器值
+        local temp_raw = read_result.data[read_config.start_addr]
+        local humi_raw = read_result.data[read_config.start_addr + 1]
+
+        -- 处理温度值的符号位
+        if temp_raw > 0x7FFF then
+            temp_raw = temp_raw - 0x10000
+        end
+
+        -- 解析温度和湿度值
+        -- 这里假设温度和湿度都是16位整数,单位分别为0.1℃和0.1%RH
+        sensor_data.temperature = temp_raw / 10.0
+        sensor_data.humidity = humi_raw / 10.0
+
+        log.info("temp_hum_sensor", "读取成功,温度为", sensor_data.temperature, "℃,湿度为", sensor_data.humidity, "%RH")
+    elseif read_result.status == exmodbus.STATUS_DATA_INVALID then
+        log.info("temp_hum_sensor", "收到传感器响应数据但数据损坏/校验失败")
+    elseif read_result.status == exmodbus.STATUS_EXCEPTION then
+        log.info("temp_hum_sensor", "收到传感器异常响应,标准异常码为", read_result.execption_code)
+    elseif read_result.status == exmodbus.STATUS_TIMEOUT then
+        log.info("temp_hum_sensor", "未收到传感器的响应(超时)")
+    end
+end
+
+
+-- 定时任务函数:每 2 秒读取一次温湿度数据
+local function task()
+    while true do
+        if rtu_master then
+            read_temp_humidity()
+        else
+            log.info("temp_hum_sensor", "RTU主站未创建,无法读取传感器数据")
+        end
+        sys.wait(2000)
+    end
+end
+
+-- 初始化任务
+sys.taskInit(task)

+ 79 - 0
module/Air8000/demo/modbus/rtu_slave/main.lua

@@ -0,0 +1,79 @@
+--[[
+@module  main
+@summary LuatOS 用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+
+本 demo 演示的核心功能为:
+1、将设备配置为 modbus RTU 从站模式
+2、等待并且应答主站请求
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、设备作为 modbus RTU 从站模式时,仅支持接收 modbus RTU 标准格式的请求报文
+3、进行回应时也需要符合 modbus RTU 标准格式
+
+更多说明参考本目录下的 readme.md 文件;
+]]
+
+--[[
+必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
+PROJECT:项目名,ascii string类型
+        可以随便定义,只要不使用,就行
+VERSION:项目版本号,ascii string类型
+        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
+            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
+            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
+        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+PROJECT = "RTU_SLAVE"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+
+-- 加载 RTU 从站应用模块(字段参数方式)
+require "rtu_slave_manage"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 128 - 0
module/Air8000/demo/modbus/rtu_slave/readme.md

@@ -0,0 +1,128 @@
+## 演示模块概述
+
+1、main.lua:主程序入口;
+
+2、rtu_slave_manage.lua:RTU 从站应用模块;
+
+## 演示功能概述
+
+本 demo 演示的核心功能为:
+
+1、将设备配置为 modbus RTU 从站模式
+
+2、等待并且应答主站请求
+
+
+
+注意事项:
+
+1、该示例程序需要搭配 exmodbus 扩展库使用
+
+2、设备作为 modbus RTU 从站模式时,仅支持接收 modbus RTU 标准格式的请求报文
+
+3、进行回应时也需要符合 modbus RTU 标准格式
+
+## 演示硬件环境
+
+1、Air8000 开发板一块
+
+2、TYPE-C USB数据线一根
+
+3、USB-RS485 串口板
+
+![](https://docs.openluat.com/cdn/image/Air8000_rs485.png)
+
+## 演示软件环境
+
+1、[Luatools下载调试工具](https://docs.openluat.com/air8000/luatos/common/download/)
+
+2、[Air8000 V2018 版本](https://docs.openluat.com/air8000/luatos/firmware/)(理论上最新版本固件也可以,如果使用最新版本的固件不可以,可以烧录 V2018-1 固件对比验证)
+
+3、[摩尔信使(MThings)官网](https://www.gulink.cn/)(用于模拟 modbus 主站设备)
+
+## 演示核心步骤
+
+1、搭建硬件环境
+
+- 将 USB-RS485 串口板与 Air8000 开发板进行连接
+- 将 USB-RS485 串口板 与 Air8000 开发板的 USB 端同时接在电脑上
+- 参考图见 演示硬件环境
+
+2、在摩尔信使上配置模拟 RTU 主站设备环境
+
+- 点击左上角的 “通道管理”按钮,在 “通道管理” 窗口选择对应的串口(USB-RS485 串口板与 Air8000 开发板进行 485 通信时的端口),点击对应串口后面的 “配置” 按钮,在 “串口参数配置” 窗口配置串口参数(要求与代码中调用 exmodbus.create 接口时填入的配置参数一致),操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/1.png)
+
+- 点击左上角的 “添加设备”按钮,在 “添加设备” 窗口对通信设备参数进行配置,配置好后点击 “添加” 按钮,左侧栏即为添加后的效果,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/20.png)
+
+- 点击左侧的第一个主站(我这里显示为 “COM36-001”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增线圈 0-2,操作流程图如下,此时按照刚才操作,依次分别创建离散输入 0-2、保持寄存器 0-2、输入寄存器 0-2
+
+  ![](https://docs.openluat.com/cdn/image/MThings/21.png)
+
+- 此时在摩尔信使上的配置操作已经完成,如果需要在摩尔信使上查看报文,那么操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/22.png)
+
+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、如下图所示,在摩尔信使上鼠标右击第一个主站,然后点击 “启动轮询”,此时上位机便会模拟主站设备开始执行轮询请求操作
+
+![](https://docs.openluat.com/cdn/image/MThings/23.png)
+
+8、如下图所示,如果需要修改轮询的间隔时间或者其他参数,先将滑动条滑到右边,然后鼠标左键双击对应参数即可修改
+
+![](https://docs.openluat.com/cdn/image/MThings/24.png)
+
+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
+[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
+[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 读取成功,返回数据:  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 读取成功,返回数据:  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
+[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
+[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 读取成功,返回数据:  201, 202
+```
+
+![](https://docs.openluat.com/cdn/image/MThings/25.png)
+
+10、如下图所示,如果需要执行写入请求,需要先在执行可写操作的对应区块行的指令处鼠标左键双击填入要写入的数值,然后在鼠标右键双击该数值,最后点击下发写指令
+
+![](https://docs.openluat.com/cdn/image/MThings/26.png)
+
+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
+```
+
+![](https://docs.openluat.com/cdn/image/MThings/27.png)
+
+12、如下图所示,在摩尔信使上鼠标右击第一个主站,然后点击 “停止轮询”,此时上位机便不会再执行轮询请求操作
+
+![](https://docs.openluat.com/cdn/image/MThings/28.png)

+ 147 - 0
module/Air8000/demo/modbus/rtu_slave/rtu_slave_manage.lua

@@ -0,0 +1,147 @@
+--[[
+@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 = {}
+        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("从站回调函数已注册,开始监听主站请求...")

+ 81 - 0
module/Air8000/demo/modbus/tcp_slave/main.lua

@@ -0,0 +1,81 @@
+--[[
+@module  main
+@summary LuatOS 用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+
+本 demo 演示的核心功能为:
+1、将设备配置为 modbus TCP 从站模式
+2、等待并且应答主站请求
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、设备作为 modbus TCP 从站模式时,仅支持接收 modbus TCP 标准格式的请求报文
+3、进行回应时也需要符合 modbus TCP 标准格式
+
+更多说明参考本目录下的 readme.md 文件;
+]]
+
+--[[
+必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
+PROJECT:项目名,ascii string类型
+        可以随便定义,只要不使用,就行
+VERSION:项目版本号,ascii string类型
+        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
+            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
+            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
+        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+PROJECT = "TCP_SLAVE"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- 开启以太网wan(默认使用静态 IP 地址)
+require "netdrv_eth_spi"
+
+-- 加载 TCP 从站应用模块(字段参数方式)
+require "tcp_slave_manage"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 81 - 0
module/Air8000/demo/modbus/tcp_slave/netdrv_eth_spi.lua

@@ -0,0 +1,81 @@
+--[[
+@module  netdrv_eth_spi
+@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
+@version 1.0
+@date    2025.12.12
+@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)

+ 130 - 0
module/Air8000/demo/modbus/tcp_slave/readme.md

@@ -0,0 +1,130 @@
+## 演示模块概述
+
+1、main.lua:主程序入口;
+
+2、tcp_slave_manage.lua:TCP 从站应用模块;
+
+3、netdrv_eth_spi.lua:“通过SPI外挂CH390H芯片的以太网卡”驱动模块;
+
+## 演示功能概述
+
+本功能模块演示的内容为:
+
+1、将设备配置为 modbus TCP 从站模式
+
+2、等待并且应答主站请求
+
+
+
+注意事项:
+
+1、该示例程序需要搭配 exmodbus 扩展库使用
+
+2、设备作为 modbus TCP 从站模式时,仅支持接收 modbus TCP 标准格式的请求报文
+
+3、进行回应时也需要符合 modbus TCP 标准格式
+
+## 演示硬件环境
+
+1、Air8000 开发板一块
+
+2、TYPE-C USB数据线一根
+
+3、网线两根(一根开发板使用,一根电脑使用)
+
+![](https://docs.openluat.com/cdn/image/Air8000_tcp1.png)
+
+## 演示软件环境
+
+1、[Luatools下载调试工具](https://docs.openluat.com/air8000/luatos/common/download/)
+
+2、[Air8000 V2018 版本](https://docs.openluat.com/air8000/luatos/firmware/)(理论上最新版本固件也可以,如果使用最新版本的固件不可以,可以烧录 V2018-1 固件对比验证)
+
+3、[摩尔信使(MThings)官网](https://www.gulink.cn/)(用于模拟 modbus 主站设备)
+
+## 演示核心步骤
+
+1、搭建硬件环境
+
+- 将 TYPE-C USB 数据线一端接在 Air8000 开发板上,另一端接在电脑上
+- 将网线一端接在 Air8000 开发板网口上,另一端接在路由器/交换机上
+
+- 将另一根网线一端接在电脑网口上,另一端接在同一个路由器/交换机上
+
+- 参考图见 演示硬件环境
+
+2、在摩尔信使上配置模拟 TCP 主站设备环境
+
+- 点击左上角的 “通道管理” 按钮,在 “通道管理” 窗口点击 “网络通道” 按钮,点击 NET000 通道后面的 “配置” 按钮,在 “网络参数配置” 窗口配置网络参数,操作流程如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/40.png)
+
+- 点击左上角的 “添加设备”按钮,在 “添加设备” 窗口对通信设备参数进行配置,配置好后点击 “添加” 按钮,左侧栏即为添加后的效果,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/30.png)
+
+- 点击左侧的第一个主站(我这里显示为 “NET000-001”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增线圈 0-2,操作流程图如下,此时按照刚才操作,依次分别创建离散输入 0-2、保持寄存器 0-2、输入寄存器 0-2
+
+  ![](https://docs.openluat.com/cdn/image/MThings/31.png)
+
+- 此时在摩尔信使上的配置操作已经完成,如果需要在摩尔信使上查看报文,那么操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/32.png)
+
+4、打开 Luatools 工具,根据要求烧录本次所需要的内核固件和脚本代码
+
+5、烧录成功后,自动开机运行
+
+6、此时需要等待客户端连接,连接成功后 Luatools 工具上的日志如下:
+
+```
+[2025-12-09 14:53:09.670][000000154.965] I/user.exmodbus TCP 从站已启动,监听端口: 6000
+```
+
+7、如果摩尔信使一直没有连接成功,则需要对网络通道进行重启,鼠标右击左上角 “通道” 下方的按钮,点击 “配置参数” 后会弹出 “网络参数配置” 窗口,此时直接点击确定,通道便已经重启,操作流程如下:
+
+![](https://docs.openluat.com/cdn/image/MThings/33.png)
+
+8、如下图所示,在摩尔信使上鼠标右击第一个主站,然后点击 “启动轮询”,此时上位机便会模拟主站设备开始执行轮询请求操作
+
+![](https://docs.openluat.com/cdn/image/MThings/34.png)
+
+9、如下图所示,如果需要修改轮询的间隔时间或者其他参数,先将滑动条滑到右边,然后鼠标左键双击对应参数即可修改
+
+![](https://docs.openluat.com/cdn/image/MThings/35.png)
+
+10、开启轮询后 Luatools 工具与摩尔信使上的日志如下:
+
+```
+[2025-12-09 15:06:14.069][000000631.817] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:06:14.071][000000631.817] I/user.exmodbus_test 读取成功,返回数据:  0, 0
+[2025-12-09 15:06:17.100][000000634.844] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:06:17.102][000000634.844] I/user.exmodbus_test 读取成功,返回数据:  1, 1
+[2025-12-09 15:06:20.118][000000637.858] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:06:20.125][000000637.859] I/user.exmodbus_test 读取成功,返回数据:  201, 202
+[2025-12-09 15:06:23.141][000000640.881] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:06:23.146][000000640.881] I/user.exmodbus_test 读取成功,返回数据:  101, 102
+[2025-12-09 15:06:26.169][000000643.914] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:06:26.171][000000643.915] I/user.exmodbus_test 读取成功,返回数据:  0, 0
+[2025-12-09 15:06:29.194][000000646.936] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:06:29.198][000000646.937] I/user.exmodbus_test 读取成功,返回数据:  1, 1
+```
+
+![](https://docs.openluat.com/cdn/image/MThings/36.png)
+
+11、如下图所示,如果需要执行写入请求,需要先在执行可写操作的对应区块行的指令处鼠标左键双击填入要写入的数值,然后在鼠标右键双击该数值,最后点击下发写指令
+
+![](https://docs.openluat.com/cdn/image/MThings/37.png)
+
+12、执行写入请求后 Luatools 工具与摩尔信使上的日志如下:
+
+```
+[2025-12-09 15:11:48.599][000000966.338] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:11:48.601][000000966.339] I/user.exmodbus_test 写入成功,写入地址:  0 写入数据:  123
+```
+
+![](https://docs.openluat.com/cdn/image/MThings/38.png)
+
+13、如下图所示,在摩尔信使上鼠标右击第一个主站,然后点击 “停止轮询”,此时上位机便不会再执行轮询请求操作
+
+![](https://docs.openluat.com/cdn/image/MThings/39.png)

+ 139 - 0
module/Air8000/demo/modbus/tcp_slave/tcp_slave_manage.lua

@@ -0,0 +1,139 @@
+--[[
+@module  tcp_slave_manage
+@summary TCP 从站应用模块
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+本功能模块演示的内容为:
+1、将设备配置为 modbus TCP 从站模式
+2、等待并且应答主站请求
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、设备作为 modbus TCP 从站模式时,仅支持接收 modbus TCP 标准格式的请求报文
+3、进行回应时也需要符合 modbus TCP 标准格式
+
+本文件没有对外接口,直接在 main.lua 中 require "tcp_slave_manage" 就可以加载运行;
+]]
+
+local exmodbus = require("exmodbus")
+
+
+-- 创建 TCP 从站配置参数
+-- 说明:创建 TCP 从站时只需要配置如下参数即可
+local tcp_slave_config = {
+    -- 网络配置参数
+    mode = exmodbus.TCP_SLAVE, -- 通信模式:TCP 从站
+    adapter = socket.LWIP_ETH, -- 网卡 ID:LwIP 协议栈的以太网卡
+    port = 6000,               -- 本地端口号:6000(主站:服务器端口;从站:本地端口)
+}
+
+
+-- 当前从站地址(ID 号)
+local SLAVE_ID = 1
+
+
+-- 寄存器映射表(按类型组织)
+local modbus_data = {
+    coils = {},            -- 线圈,可读可写,布尔值 (0/1)
+    inputs = {},           -- 输入状态,只读,布尔值 (0/1)
+    input_registers = {},  -- 输入寄存器,只读,16 位无符号整数
+    holding_registers = {} -- 保持寄存器,可读可写,16 位无符号整数
+}
+
+
+-- 初始化一些默认值,便于测试
+for i = 0, 3 do
+    modbus_data.coils[i] = 0
+    modbus_data.inputs[i] = 1
+    modbus_data.input_registers[i] = 100 + i
+    modbus_data.holding_registers[i] = 200 + i
+end
+
+
+-- 创建 TCP 从站实例
+local tcp_slave = exmodbus.create(tcp_slave_config)
+
+-- 判断从站是否创建成功
+if not tcp_slave then
+    log.info("exmodbus_test", "tcp_slave 创建失败")
+else
+    log.info("exmodbus_test", "tcp_slave 创建成功, 从站 ID 为", SLAVE_ID)
+end
+
+
+-- 定义主站请求处理回调函数
+local function callback(request)
+    log.info("exmodbus_test", "tcp_slave 收到主站请求")
+
+    -- 检查从站 ID 是否匹配
+    if request.slave_id ~= SLAVE_ID then
+        log.info("exmodbus_test", "从站 ID 不匹配,请求从站 ID 为", request.slave_id, ",当前从站 ID 为", SLAVE_ID)
+        return nil
+    end
+
+    -- 根据功能码和寄存器类型,匹配对应的数据表
+    local data_table = nil
+    local is_write = false -- 标记是否为写操作
+
+    -- 检查请求的功能码是否支持
+    if request.func_code == exmodbus.READ_COILS then -- 读线圈
+        data_table = modbus_data.coils
+    elseif request.func_code == exmodbus.READ_DISCRETE_INPUTS then -- 读离散输入
+        data_table = modbus_data.inputs
+    elseif request.func_code == exmodbus.READ_HOLDING_REGISTERS then -- 读保持寄存器
+        data_table = modbus_data.holding_registers
+    elseif request.func_code == exmodbus.READ_INPUT_REGISTERS then -- 读输入寄存器
+        data_table = modbus_data.input_registers
+    elseif request.func_code == exmodbus.WRITE_SINGLE_COIL or request.func_code == exmodbus.WRITE_MULTIPLE_COILS then -- 写单个/多个线圈
+        is_write = true
+        data_table = modbus_data.coils
+    elseif request.func_code == exmodbus.WRITE_SINGLE_HOLDING_REGISTER or request.func_code == exmodbus.WRITE_MULTIPLE_HOLDING_REGISTERS then -- 写单个/多个保持寄存器
+        is_write = true
+        data_table = modbus_data.holding_registers
+    else
+        -- 不支持的功能码
+        log.info("exmodbus_test", "不支持的功能码: ", request.func_code)
+        return exmodbus.ILLEGAL_FUNCTION
+    end
+
+    -- 检查数据地址是否有效
+    local end_addr = request.start_addr + request.reg_count - 1
+
+    -- 假设每种寄存器的最大地址是 3 (即 0 - 3)
+    if request.start_addr < 0 or end_addr > 3 then
+        log.info("exmodbus_test", "数据地址超出范围,起始地址为", request.start_addr, "结束地址为", end_addr)
+        return exmodbus.ILLEGAL_DATA_ADDRESS
+    end
+
+    -- 处理读取操作
+    if not is_write then
+        -- 构造响应数据表
+        local response = {}
+        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("从站回调函数已注册,开始监听主站请求...")