跳转至

01 Modbus应用

一、MODBUS 概述

Modbus 是一种广泛应用于工业自动化领域的串行通信协议,由 Modicon 公司(现为施耐德电气旗下)于 1979 年推出,主要用于可编程逻辑控制器(PLC)与工业设备之间的通信。其设计简单、开放、易于实现,成为工业通信的事实标准。

1. 1 核心特点

主从架构:由主设备(Master)发起请求,从设备(Slave)响应,支持单主或多从模式。

典型应用:一台工控机(主)与多个传感器/执行器(从)通信。

1.2 常用协议变体

Modbus RTU:基于二进制编码,通过串行接口(RS-232/RS-485)传输,紧凑高效。

Modbus ASCII:使用 ASCII 字符编码,可读性强但效率低。

Modbus TCP/IP:基于以太网,适用于现代工业网络。

协议类型对比:

特性
Modbus RTU
Modbus ASCII
Modbus TCP/IP
传输介质
RS-232/RS-485
RS-232/RS-485
以太网P
帧起始/结束标志
静默时间(3.5字符间隔)
冒号:(起始),CRLF(结束)
TCP包头(事务标识符)
校验方式
CRC-16
LRC(纵向冗余校验)
无(依赖TCP校验)

1.3 数据模型

1.3.1 Modbus 定义四种数据类型,每种通过不同功能码访问:

线圈(Coils):可读可写的布尔量(功能码 01 读,05 写单个,15 写多个)。

离散输入(Discrete Inputs):只读布尔量(功能码 02 读)。

保持寄存器(Holding Registers):可读可写的 16 位整数(功能码 03 读,06 写单个,16 写多个)。

输入寄存器(Input Registers):只读的 16 位整数(功能码 04 读)。

1.3.2 简单报文结构

请求帧:功能码 + 数据地址 + 数据长度 + CRC 校验(RTU)或 TCP 头(Modbus TCP)。

响应帧:功能码 + 返回数据 + 校验。

1.4 优缺点

1.4.1 优点

  • 开放免费,兼容性强。
  • 在多种电气接口(RS232、RS485)及多种通信介质(以太网,串行电路,蓝牙,wifi 等)中运行 。
  • 报文帧简单紧凑。

1.4.2 缺点

  • 无内置安全机制(需依赖网络隔离或加密层)。
  • 仅支持基础数据类型(需扩展协议处理浮点数等)。

二、演示功能概述

本篇文章演示的内容为:通过 RTU、ASCII 和 TCP 三种常用协议,Air8000 开发板作为主站(客户端)与从站连接通讯的过程,或开发板作为从站(服务器)与主站连接通讯的过程。

三、准备硬件环境

3.1 硬件准备

参考:硬件环境清单第二章节内容,准备以及组装好硬件环境。

485/232 转 USB:

网线:

3.2 开发板组合演示

3.2.1 RTU 协议和 ASCII 协议测试连接

3.2.2 TCP 协议测试连接

四、准备软件环境

在开始实践本示例之前,先筹备一下软件环境:

  1. Luatools 工具

2. LuatOS-SoC_V2006_Air8000_LVGL;此页面有新版本固件的话选用最新版本固件。

3.LuatOS 需要的脚本和资源文件:https://gitee.com/openLuat/LuatOS/tree/master/module/Air8000/demo/modbus

  1. lib 脚本文件:使用 Luatools 烧录时,勾选 添加默认 lib 选项,使用默认 lib 脚本文件;

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

5.上位机下位机模拟软件

5.1 API 接口介绍

https://docs.openluat.com/osapi/core/modbus/

5.2 代码解析

5.2.1 RTU

5.2.1.1 master_rtu

1.初始化通讯串口

local uartid = 1        -- 根据实际设备选取不同的uartid
local uart485Pin = 24   -- 用于控制485接收和发送的使能引脚
gpio.setup(1, 1)        --打开电源(开发板485供电脚是gpio1,用开发板测试需要开机初始化拉高gpio1)
uart.setup(uartid, 115200, 8, 1, uart.NONE, uart.LSB, 1024, uart485Pin, 0, 2000)

2.Modbus 主站创建

mb_rtu = modbus.create_master(modbus.MODBUS_RTU, uartid,2000)

3.添加从站

mb_slave1 = modbus.add_slave(mb_rtu, 1)

4.创建数据区和通信消息

slave1_msg1_buf = zbuff.create(1)
mb_slave1_msg1 = modbus.create_msg(mb_rtu, mb_slave1, modbus.REGISTERS, modbus.READ, 0, 10, slave1_msg1_buf)
slave1_msg1_buf:clear()

5.启动 Modbus 设备

modbus.master_start(mb_rtu)

6.定时状态检查

sys.timerLoopStart(function()
    local status = modbus.get_all_slave_state(mb_rtu)
    log.info("modbus", status)
end, 5000)

sys.timerLoopStart(function()
    local status = modbus.get_slave_state(mb_slave1)
    log.info("modbus1", status)
end, 5000)

7.数据读取并转化为 json

-- 获取从站1的状态,每1秒获取一次数据并转换为JSON
sys.timerLoopStart(function()
    -- 检查从站状态
    local status = modbus.get_slave_state(mb_slave1)
    if status == 0 then  -- 0表示正常
        -- 读取缓冲区数据
        slave1_msg1_buf:seek(0)  -- 重置指针到起始位置

        -- 读取4个寄存器的值(每个寄存器2字节)
        local reg1 = slave1_msg1_buf:readU16()
        local reg2 = slave1_msg1_buf:readU16()
        local reg3 = slave1_msg1_buf:readU16()
        local reg4 = slave1_msg1_buf:readU16()

        -- 创建数据表
        local data = {
            addr = 1,  -- 从站地址
            fun = 3,   -- 功能码03
            reg1 = reg1 ,  -- 假设原始数据需要除以10得到实际值
            reg2 = reg2 ,
            reg3 = reg3 ,
            reg4 = reg4 ,
            timestamp = os.time()  -- 添加时间戳
        }

        -- 转换为JSON
        local json_str = json.encode(data)
        log.info("Modbus数据转JSON:", json_str)

    else
        log.warn("从站1状态异常:", status)
    end
end, 1000)
5.2.1.2 slave_rtu

1.初始化设置

local uartid = 1        -- 根据实际设备选取不同的uartid
local uart485Pin = 24   -- 用于控制485接收和发送的使能引脚
gpio.setup(1, 1)        --打开电源(开发板485供电脚是gpio1,用开发板测试需要开机初始化拉高gpio1)
uart.setup(uartid, 115200, 8, 1, uart.NONE, uart.LSB, 1024, uart485Pin, 0, 2000)

2.Modbus 从站创建:创建了一个 RTU 模式的 Modbus 从站,添加了两个数据块:保持寄存器区和线圈区

local slave_id = 1
mb_rtu_s = modbus.create_slave(modbus.MODBUS_RTU, slave_id, uartid)

-- 添加一块寄存器内存区
registers = zbuff.create(1)
modbus.add_block(mb_rtu_s, modbus.REGISTERS, 0, 32, registers)
registers:clear()

-- 创建线圈数据区
ciols = zbuff.create(1)
modbus.add_block(mb_rtu_s, modbus.CIOLS, 0, 32, ciols)
ciols:clear()

3.启动 modbus 从站

modbus.slave_start(mb_rtu_s)

4.数据更新

local counter = 0
-- 修改和读取modbus值
function modify_data()
    counter = counter + 1  
    -- 写入寄存器数据 (16位无符号整数)
    registers:seek(0)
    for i=0,31 do
        registers:writeU16((counter + i) % 65536)  -- 写入递增数字,限制在0-65535
    end  
    -- 写入线圈数据 (1位布尔值)
    ciols:seek(0)
    for i=0,31 do
        ciols:writeU8((counter + i) % 2)  -- 交替写入0和1
    end

    -- 读取并打印部分数据用于调试
    registers:seek(0)
    ciols:seek(0)
    log.info("registers:", registers:readU16(), registers:readU16(), registers:readU16(), registers:readU16(), registers:readU16())
    log.info("ciols    :", ciols:readU8(), ciols:readU8(), ciols:readU8(), ciols:readU8(), ciols:readU8())
end

5.2.1 ASCII

5.2.2.1 master_ascii

1.初始化通讯串口

local uartid = 1        -- 根据实际设备选取不同的uartid
local uart485Pin = 24   -- 用于控制485接收和发送的使能引脚
gpio.setup(1, 1)        --打开电源(开发板485供电脚是gpio1,用开发板测试需要开机初始化拉高gpio1)
uart.setup(uartid, 115200, 8, 1, uart.NONE, uart.LSB, 1024, uart485Pin, 0, 2000)

2.Modbus 主站创建

mb_ascii = modbus.create_master(modbus.MODBUS_ASCII, uartid,3000,2000,1,5000)

3.添加从站

mb_slave1 = modbus.add_slave(mb_ascii, 1)

4.创建数据区和通信消息

slave1_msg1_buf = zbuff.create(1)
mb_slave1_msg1 = modbus.create_msg(mb_ascii, mb_slave1, modbus.REGISTERS, modbus.READ, 0, 10, slave1_msg1_buf)
slave1_msg1_buf:clear()

5.启动 Modbus 设备

modbus.master_start(mb_ascii)

6.定时状态检查

sys.timerLoopStart(function()
    local status = modbus.get_all_slave_state(mb_ascii)
    log.info("modbus", status)
end, 5000)

sys.timerLoopStart(function()
    local status = modbus.get_slave_state(mb_slave1)
    log.info("modbus1", status)
end, 5000)

7.数据读取并转化为 json

-- 获取从站1的状态,每1秒获取一次数据并转换为JSON
sys.timerLoopStart(function()
    -- 检查从站状态
    local status = modbus.get_slave_state(mb_slave1)
    if status == 0 then  -- 0表示正常
        -- 读取缓冲区数据
        slave1_msg1_buf:seek(0)  -- 重置指针到起始位置

        -- 读取4个寄存器的值(每个寄存器2字节)
        local reg1 = slave1_msg1_buf:readU16()
        local reg2 = slave1_msg1_buf:readU16()
        local reg3 = slave1_msg1_buf:readU16()
        local reg4 = slave1_msg1_buf:readU16()

        -- 创建数据表
        local data = {
            addr = 1,  -- 从站地址
            fun = 3,   -- 功能码03
            reg1 = reg1 ,  -- 假设原始数据需要除以10得到实际值
            reg2 = reg2 ,
            reg3 = reg3 ,
            reg4 = reg4 ,
            timestamp = os.time()  -- 添加时间戳
        }

        -- 转换为JSON
        local json_str = json.encode(data)
        log.info("Modbus数据转JSON:", json_str)

    else
        log.warn("从站1状态异常:", status)
    end
end, 1000)
5.2.2.2 slave_ascii

1.初始化设置

local uartid = 1        -- 根据实际设备选取不同的uartid
local uart485Pin = 24   -- 用于控制485接收和发送的使能引脚
gpio.setup(1, 1)        --打开电源(开发板485供电脚是gpio1,用开发板测试需要开机初始化拉高gpio1)
uart.setup(uartid, 115200, 8, 1, uart.NONE, uart.LSB, 1024, uart485Pin, 0, 2000)

2.Modbus 从站创建:创建了一个 RTU 模式的 Modbus 从站,添加了两个数据块:保持寄存器区和线圈区

local slave_id = 1
mb_ascii_s = modbus.create_slave(modbus.MODBUS_ASCII, slave_id, uartid)

-- 添加一块寄存器内存区
registers = zbuff.create(1)
modbus.add_block(mb_ascii_s, modbus.REGISTERS, 0, 32, registers)
registers:clear()

-- 创建线圈数据区
ciols = zbuff.create(1)
modbus.add_block(mb_ascii_s, modbus.CIOLS, 0, 32, ciols)
ciols:clear()

3.启动 modbus 从站

modbus.slave_start(mb_ascii_s)

4.数据更新

local counter = 0
-- 修改和读取modbus值
function modify_data()
    counter = counter + 1  
    -- 写入寄存器数据 (16位无符号整数)
    registers:seek(0)
    for i=0,31 do
        registers:writeU16((counter + i) % 65536)  -- 写入递增数字,限制在0-65535
    end  
    -- 写入线圈数据 (1位布尔值)
    ciols:seek(0)
    for i=0,31 do
        ciols:writeU8((counter + i) % 2)  -- 交替写入0和1
    end

    -- 读取并打印部分数据用于调试
    registers:seek(0)
    ciols:seek(0)
    log.info("registers:", registers:readU16(), registers:readU16(), registers:readU16(), registers:readU16(), registers:readU16())
    log.info("ciols    :", ciols:readU8(), ciols:readU8(), ciols:readU8(), ciols:readU8(), ciols:readU8())
end

5.2.3 TCP

5.2.3.1 master_tcp

1.初始化

gpio.setup(20, 1)  --打开lan供电

2.SPI 和以太网驱动初始化

require "lan"  -- 实际网络配置在lan.lua中

-- lan.lua中的关键代码:
netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spiid=0, cs=8})  -- 初始化CH390以太网控制器(SPI0,片选GPIO8)
sys.wait(3000)  -- 等待3秒确保硬件就绪
local ipv4, mark, gw = netdrv.ipv4(socket.LWIP_ETH, "192.168.4.1", "255.255.255.0", "192.168.4.1")  -- 设置静态IP
dhcps.create({adapter=socket.LWIP_ETH})  -- 启动DHCP服务器
dnsproxy.setup(socket.LWIP_ETH, socket.LWIP_ETH)  -- 启用DNS代理
netdrv.napt(socket.LWIP_ETH)  -- 启用NAPT(网络地址端口转换)

3.创建主站

mb_tcp = modbus.create_master(
    modbus.MODBUS_TCP,     -- TCP模式
    socket.LWIP_ETH,       -- 以太网接口
    3000,                  -- 通信间隔时间(ms)
    1,                     -- 超时重试次数
    5000                   -- 断线重连间隔(ms)
)

4.添加从站

mb_slave1 = modbus.add_slave(mb_tcp, 1, "192.168.4.100", 6000)

5.创建数据请求消息

-- 为从站1创建读取保持寄存器的请求(地址0开始,读10个寄存器)
slave1_msg1_buf = zbuff.create(1)  -- 创建缓冲区
mb_slave1_msg1 = modbus.create_msg(
    mb_tcp, mb_slave1, 
    modbus.REGISTERS,     -- 寄存器类型
    modbus.READ,          -- 读操作
    0,                    -- 起始地址
    10,                   -- 寄存器数量
    slave1_msg1_buf       -- 存储数据的缓冲区
)

6.启动 Modbus 主站

modbus.master_start(mb_tcp)
log.info("start modbus master")

7.从站状态监控

-- 每5秒检查所有从站状态
sys.timerLoopStart(function()
    local status = modbus.get_all_slave_state(mb_tcp)
    log.info("modbus", status)
end, 5000)

-- 每5秒检查从站1单独状态
sys.timerLoopStart(function()
    local status = modbus.get_slave_state(mb_slave1)
    log.info("modbus1", status)
end, 5000)

8.数据处理

addvar = 0
function modify_data()
    -- 读取从站1的寄存器数据
    slave1_msg1_buf:seek(0)
    log.info("slave1 reg: ", 
        slave1_msg1_buf:readU16(), slave1_msg1_buf:readU16(),
        slave1_msg1_buf:readU16(), slave1_msg1_buf:readU16()
    )
end
sys.timerLoopStart(modify_data, 1000)
5.2.3.2 slave_tcp

1.网络硬件初始化

log.info("ch390", "打开LDO供电")
gpio.setup(20, 1)  --打开lan供电
require "lan"

2.SPI 和网络驱动初始化

local result = spi.setup(0, nil, 0, 0, 8, 25600000)
netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spiid=0,cs=8})

3.等待网络连接

while netdrv.link(socket.LWIP_ETH) ~= true do
    sys.wait(100)
end

4.IP 地址配置

local ipv4,mark, gw = netdrv.ipv4(socket.LWIP_ETH, "192.168.4.1", "255.255.255.0", "192.168.4.1")

5.DHCP 和 DNS 服务

dhcps.create({adapter=socket.LWIP_ETH})
dnsproxy.setup(socket.LWIP_ETH, socket.LWIP_ETH)
netdrv.napt(socket.LWIP_ETH)

6.系统监控任务

sys.taskInit(function()
    sys.waitUntil("IP_READY")
    while 1 do
        sys.wait(300000)
        log.info("lua", rtos.meminfo())
        log.info("sys", rtos.meminfo("sys"))
    end
end)

7.创建 Modbus TCP 从站

local slave_id = 1
mb_tcp_s = modbus.create_slave(modbus.MODBUS_TCP, slave_id, 6000, socket.LWIP_ETH)

8.创建数据存储区

registers = zbuff.create(1)
modbus.add_block(mb_tcp_s, modbus.REGISTERS, 0, 32, registers)
registers:clear()

ciols = zbuff.create(1)
modbus.add_block(mb_tcp_s, modbus.CIOLS, 0, 32, ciols)
ciols:clear()

9.启动 Modbus 从站

modbus.slave_start(mb_tcp_s)
log.info("start modbus slave")

10.定时数据更新

local counter = 0
function modify_data()
    counter = counter + 1
    -- 更新寄存器数据
    registers:seek(0)
    for i=0,31 do
        registers:writeU16((counter + i) % 65536)
    end
    -- 更新线圈数据
    ciols:seek(0)
    for i=0,31 do
        ciols:writeU8((counter + i) % 2)
    end
    -- 日志输出部分数据
    registers:seek(0)
    ciols:seek(0)
    log.info("registers:", registers:readU16(), registers:readU16(), registers:readU16(), registers:readU16(), registers:readU16())
    log.info("ciols    :", ciols:readU8(), ciols:readU8(), ciols:readU8(), ciols:readU8(), ciols:readU8())
end
sys.timerLoopStart(modify_data,1000)

六、运行结果展示

6.1 RTU 协议运行结果

6.1.1 modbus.master_rtu

6.1.1.1 完整代码展示
-- LuaTools需要PROJECT和VERSION这两个信息
PROJECT = "modbus_master_rtu"
VERSION = "1.0.0"
log.style(1)
log.info("main", PROJECT, VERSION)

-- 引入必要的库文件(lua编写), 内部库不需要require
sys = require("sys")

--初始化通讯串口
local uartid = 1        -- 根据实际设备选取不同的uartid
local uart485Pin = 17   -- 用于控制485接收和发送的使能引脚
gpio.setup(16, 1)        --打开电源(开发板485供电脚是gpio16,用开发板测试需要开机初始化拉高gpio16)
uart.setup(uartid, 115200, 8, 1, uart.NONE, uart.LSB, 1024, uart485Pin, 0, 2000)--用户如果接的是自己的下位机,请将uart.setup传入的参数成自己下位机的配置参数

-- 创建主站设备,RTU模式
-- 设置通讯间隔时间,主站将按每隔 设置时间 的频率向从站问询数据(默认100ms),当添加了多个从站后,主站向每个从站问询的时间间隔将叠加
-- 设置通讯超时时间和消息发送超时重发次数,当主站未在 设置的时间 内接收到从站数据,将向从站再次发送问询(问询次数按设置的 消息超时重发次数 发送,默认1)
-- 设置断线重连时间间隔,当从站与主站断连后,主站将在设置时间内重新连接从站(默认5000ms)
mb_rtu = modbus.create_master(modbus.MODBUS_RTU, uartid,3000,2000,1,5000)

-- 为主站添加从站,从站ID为1,可使用modbus.add_slave(master_handler, slave_id)接口添加多个从站,最多可以添加247个
mb_slave1 = modbus.add_slave(mb_rtu, 1)
-- -- 为主站添加从站,从站ID为2
-- mb_slave2 = modbus.add_slave(mb_rtu, 2)

-- 为从站1创建数据存储区,并创建通讯消息,默认为自动loop模式
slave1_msg1_buf = zbuff.create(1)
mb_slave1_msg1 = modbus.create_msg(mb_rtu, mb_slave1, modbus.REGISTERS, modbus.READ, 0, 10, slave1_msg1_buf)
slave1_msg1_buf:clear()

-- -- 为从站1创建数据存储区,并创建通讯消息,如需要使用手动模式,须在这里设置为手动模式
-- slave1_msg1_buf = zbuff.create(1)
-- mb_slave1_msg1 = modbus.create_msg(mb_rtu, mb_slave1, modbus.REGISTERS, modbus.READ, 0, 10, slave1_msg1_buf,1,modbus.EXEC)
-- slave1_msg1_buf:clear()

-- -- 为从站2创建数据存储区,并创建通讯消息,如设置多个从站,需要给每个从站创建数据储存区
-- slave2_msg1_buf = zbuff.create(1)
-- mb_slave2_msg1 = modbus.create_msg(mb_rtu, mb_slave2, modbus.REGISTERS,  modbus.READ, 0, 10, slave2_msg1_buf)
-- slave2_msg1_buf:clear()

-- 启动Modubs设备
modbus.master_start(mb_rtu)

-- -- 设置通讯间隔时间,设置后主站将按每隔 设置时间 的频率向从站问询数据,当添加了多个从站后,主站向每个从站问询的时间间隔将叠加
-- modbus.set_comm_interval_time(mb_rtu, 3000)

-- -- 设置通讯超时时间,当主站未在 设置的时间 内接收到从站数据,将向从站再次发送问询(问询次数按设置的 消息超时重发次数 发送)
-- modbus.set_comm_timeout(mb_rtu, 2000)

-- -- 设置消息发送失败、超时重发次数,如果主站在设置超时时间内未接收到数据,将按设置次数问询数据
-- modbus.set_comm_resend_count(mb_rtu,2)

-- -- 设置消息通讯周期,搭配modbus.create_master/modbus.set_comm_interval_time(mb_rtu, 3000)设置通讯时间使用,若设置通讯周期为2次,将在2倍的通讯时间后向从站问询数据
-- modbus.set_msg_comm_period(mb_slave1_msg1, 2)

-- 获取所有从站状态,如果所有从站状态为正常,返回true,其他情况返回false,将在每隔5秒的时间获取所有从站状态,并在日志中打印状态(仅方便调试使用,量产时可删除)
sys.timerLoopStart(function()
    local status = modbus.get_all_slave_state(mb_rtu)
    log.info("modbus", status)
end, 5000)

-- 获取从站1的状态,每隔5秒获取从站状态并在日志打印出来(仅方便调试使用,量产时可删除)
sys.timerLoopStart(function()
    local status = modbus.get_slave_state(mb_slave1)
    log.info("modbus1", status)
end, 5000)

-- -- 获取从站2的状态,每隔5秒获取从站状态并在日志打印出来(仅方便调试使用,量产时可删除)
-- sys.timerLoopStart(function()
--     local status = modbus.get_slave_state(mb_slave2)
--     log.info("modbus2", status)
-- end, 5000)

-- -- 每隔5秒执行一次mb_slave1_msg1消息,使用modbus.exec(master_handler, msg_handler)接口须先在modbus.set_msg_comm_period(msg_handler, comm_period)接口中设置为手动模式;成功返回true,其他情况返回false
-- sys.timerLoopStart(function()
--     local status=modbus.exec(mb_rtu, mb_slave1_msg1)
--     log.info("msg",status)
-- end,5000)

-- -- 测试删除一个从站对象,并删除与之相关的通讯消息句柄。需在主站停止时(modbus.master_stop)执行该操作,否则无效。
-- -- 将在3分钟后删除从站1(主站已关闭),删除与之相关的通讯消息句柄,并在5秒后重启主站,可以观察从站是否删除成功。
-- sys.timerStart(function()
--     local status = modbus.remove_slave(mb_rtu, mb_slave1)
--     log.info("modbus", "slave1 remove after 3 minutes")
--     log.info("remove", status)

-- -- 移除从站后,5秒后重新启动Modbus主站
--     sys.timerStart(function()
--         modbus.master_start(mb_rtu)
--         log.info("modbus", "Modbus master restarted after slave removal")
--     end, 5000)
-- end, 180000) 

-- 获取从站1的状态,每1秒获取一次数据并转换为JSON
sys.timerLoopStart(function()
    -- 检查从站状态
    local status = modbus.get_slave_state(mb_slave1)
    if status == 0 then  -- 0表示正常
        -- 读取缓冲区数据
        slave1_msg1_buf:seek(0)  -- 重置指针到起始位置

        -- 读取4个寄存器的值(每个寄存器2字节)
        local reg1 = slave1_msg1_buf:readU16()
        local reg2 = slave1_msg1_buf:readU16()
        local reg3 = slave1_msg1_buf:readU16()
        local reg4 = slave1_msg1_buf:readU16()

        -- 创建数据表
        local data = {
            addr = 1,  -- 从站地址
            fun = 3,   -- 功能码03
            reg1 = reg1 ,  -- 假设原始数据需要除以10得到实际值
            reg2 = reg2 ,
            reg3 = reg3 ,
            reg4 = reg4 ,
            timestamp = os.time()  -- 添加时间戳
        }

        -- 转换为JSON
        local json_str = json.encode(data)
        log.info("Modbus数据转JSON:", json_str)

    else
        log.warn("从站1状态异常:", status)
    end
end, 1000)


-- -- 将在主站开启2分钟后停止modbus主站
-- sys.timerStart(function()
--     modbus.master_stop(mb_rtu)
--     log.info("modbus", "Modbus stopped after 2 minutes")
-- end, 120000) 

-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
6.1.1.2 效果展示

1.打开 MThings(第四章中 第五项提到的上位机下位机软件 摩尔信使),界面如下。

2.点击右上角进入通道管理。

3.点击通道管理。

4.进入后可以看到 4 个 com 口,选择 485 通道的 com 口进行配置。

由设备管理器可知 485 通道是 com20

5.点入配置,按照代码中 uart.setup 设置参数后,点击确定即可,设置完毕后在通道管理界面配置的通道显示属于正常状态,用户如果接的是自己的下位机,请将 uart.setup 传入的参数成自己下位机的配置参数。

6.返回初始界面,点击添加设备。

7.通道选择刚刚配置过的 485 通道 COM20,设备类型是模拟从机,地址是添加从站的 id。

8.增加数据配置,数据条目是寄存器的数量。

点击确定后可以看到增加了 4 个条目,双击数值一栏,然后再双击固定值一栏即可按需选择模拟数据的方法

9.数据配置完成后,在主界面可以看到数值按照设置的通讯时间间隔开始变换,在日志中也可以看到收到的数据,modbus 连接和通讯成功。

6.1.2 modbus.slave_rtu

6.1.2.1 完整代码展示
-- LuaTools需要PROJECT和VERSION这两个信息
PROJECT = "modbus_slave_rtu"
VERSION = "1.0.0"
log.style(1)
log.info("main", PROJECT, VERSION)

-- 引入必要的库文件(lua编写), 内部库不需要require
sys = require("sys")

--初始化通讯串口
local uartid = 1        -- 根据实际设备选取不同的uartid
local uart485Pin = 17   -- 用于控制485接收和发送的使能引脚
gpio.setup(16, 1)        --打开电源(开发板485供电脚是gpio1,用开发板测试需要开机初始化拉高gpio1)
uart.setup(uartid, 115200, 8, 1, uart.NONE, uart.LSB, 1024, uart485Pin, 0, 2000)--用户如果接的是自己的下位机,请将uart.setup传入的参数成自己下位机的配置参数

-- 创建从站设备,可选择RTU、ASCII、TCP,此demo仅用作测试RTU和ASCII。
local slave_id = 1
mb_rtu_s = modbus.create_slave(modbus.MODBUS_RTU, slave_id, uartid)

-- 添加一块寄存器内存区
registers = zbuff.create(1)
modbus.add_block(mb_rtu_s, modbus.REGISTERS, 0, 32, registers)
registers:clear()

-- 创建线圈数据区
ciols = zbuff.create(1)
modbus.add_block(mb_rtu_s, modbus.CIOLS, 0, 32, ciols)
ciols:clear()

-- 启动modbus从站
modbus.slave_start(mb_rtu_s)

local counter = 0
-- 修改和读取modbus值
function modify_data()
    counter = counter + 1  
    -- 写入寄存器数据 (16位无符号整数)
    registers:seek(0)
    for i=0,31 do
        registers:writeU16((counter + i) % 65536)  -- 写入递增数字,限制在0-65535
    end  
    -- 写入线圈数据 (1位布尔值)
    ciols:seek(0)
    for i=0,31 do
        ciols:writeU8((counter + i) % 2)  -- 交替写入0和1
    end

    -- 读取并打印部分数据用于调试
    registers:seek(0)
    ciols:seek(0)
    log.info("registers:", registers:readU16(), registers:readU16(), registers:readU16(), registers:readU16(), registers:readU16())
    log.info("ciols    :", ciols:readU8(), ciols:readU8(), ciols:readU8(), ciols:readU8(), ciols:readU8())
end
sys.timerLoopStart(modify_data,1000)

-- -- 测试停止modbus从站,将在从站启动两分钟后关闭
-- sys.timerStart(function()
--     modbus.slave_stop(mb_rtu_s)
--     log.info("Modbus", "2分钟时间到,停止Modbus从站")
-- end, 2 * 60 * 1000)  -- 2分钟(单位:毫秒)

-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
6.1.2.2 效果展示

1.打开 MThings(第四章中 第五项提到的上位机下位机软件 摩尔信使),界面如下。

2.点击右上角进入通道管理。

3.点击通道管理。

4.进入后可以看到 4 个 com 口,选择 485 通道的 com 口进行配置。

由设备管理器可知 485 通道是 com20

5.点入配置,按照代码中 uart.setup 设置参数后,选择 RTU 传输协议,点击确定即可,设置完毕后在通道管理界面配置的通道显示属于正常状态。用户如果接的是自己的下位机,请将 uart.setup 传入的参数成自己下位机的配置参数。

6.返回初始界面,点击添加设备。

7.通道选择刚刚配置过的 485 通道 COM20,设备类型是模拟主站,地址是创建从站的 id。

8.增加数据配置,根据需要选择读取的寄存器与线圈数据。

9.添加成功后,双击数值栏就可以获取目前从站的数据了,开发板做从站和主站连接通讯成功。

6.2 ASCII 协议运行结果

6.2.1 modbus.master_ascii

6.2.1.1 完整代码展示
-- LuaTools需要PROJECT和VERSION这两个信息
PROJECT = "modbus_master_ascii"
VERSION = "1.0.0"
log.style(1)
log.info("main", PROJECT, VERSION)

-- 引入必要的库文件(lua编写), 内部库不需要require
sys = require("sys")

--初始化通讯串口
local uartid = 1        -- 根据实际设备选取不同的uartid
local uart485Pin = 17   -- 用于控制485接收和发送的使能引脚
gpio.setup(16, 1)        --打开电源(开发板485供电脚是gpio16,用开发板测试需要开机初始化拉高gpio16)
uart.setup(uartid, 115200, 8, 1, uart.NONE, uart.LSB, 1024, uart485Pin, 0, 2000)--用户如果接的是自己的下位机,请将uart.setup传入的参数成自己下位机的配置参数

-- 创建主站设备,ASCII模式
-- 设置通讯间隔时间,主站将按每隔 设置时间 的频率向从站问询数据(默认100ms),当添加了多个从站后,主站向每个从站问询的时间间隔将叠加
-- 设置通讯超时时间和消息发送超时重发次数,当主站未在 设置的时间 内接收到从站数据,将向从站再次发送问询(问询次数按设置的 消息超时重发次数 发送,默认1)
-- 设置断线重连时间间隔,当从站与主站断连后,主站将在设置时间内重新连接从站(默认5000ms)
mb_ascii = modbus.create_master(modbus.MODBUS_ASCII, uartid,2000,2000,1,5000)

-- 为主站添加从站,从站ID为1,可使用modbus.add_slave(master_handler, slave_id)接口添加多个从站,最多可以添加247个
mb_slave1 = modbus.add_slave(mb_ascii, 1)
-- -- 为主站添加从站,从站ID为2
-- mb_slave2 = modbus.add_slave(mb_ascii, 2)

-- 为从站1创建数据存储区,并创建通讯消息,默认为自动loop模式
slave1_msg1_buf = zbuff.create(1)
mb_slave1_msg1 = modbus.create_msg(mb_ascii, mb_slave1, modbus.REGISTERS, modbus.READ, 0, 10, slave1_msg1_buf)
slave1_msg1_buf:clear()

-- -- 为从站1创建数据存储区,并创建通讯消息,如需要使用手动模式,须在这里设置为手动模式
-- slave1_msg1_buf = zbuff.create(1)
-- mb_slave1_msg1 = modbus.create_msg(mb_ascii, mb_slave1, modbus.REGISTERS, modbus.READ, 0, 10, slave1_msg1_buf,1,modbus.EXEC)
-- slave1_msg1_buf:clear()

-- -- 为从站2创建数据存储区,并创建通讯消息,如设置多个从站,需要给每个从站创建数据储存区
-- slave2_msg1_buf = zbuff.create(1)
-- mb_slave2_msg1 = modbus.create_msg(mb_ascii, mb_slave2, modbus.REGISTERS,  modbus.READ, 0, 10, slave2_msg1_buf)
-- slave2_msg1_buf:clear()

-- 启动Modubs设备
modbus.master_start(mb_ascii)

-- -- 设置通讯间隔时间,设置后主站将按每隔 设置时间 的频率向从站问询数据,当添加了多个从站后,主站向每个从站问询的时间间隔将叠加
-- modbus.set_comm_interval_time(mb_ascii, 3000)

-- -- 设置通讯超时时间,当主站未在 设置的时间 内接收到从站数据,将向从站再次发送问询(问询次数按设置的 消息超时重发次数 发送)
-- modbus.set_comm_timeout(mb_ascii, 2000)

-- -- 设置消息发送失败、超时重发次数,如果主站在设置超时时间内未接收到数据,将按设置次数问询数据
-- modbus.set_comm_resend_count(mb_ascii,2)

-- -- 设置消息通讯周期,搭配modbus.create_master/modbus.set_comm_interval_time(mb_rtu, 3000)设置通讯时间使用,若设置通讯周期为2次,将在2倍的通讯时间后向从站问询数据
-- modbus.set_msg_comm_period(mb_slave1_msg1, 2)

-- 获取所有从站状态,如果所有从站状态为正常,返回true,其他情况返回false,将在每隔5秒的时间获取所有从站状态,并在日志中打印状态(仅方便调试使用,量产时可删除)
sys.timerLoopStart(function()
    local status = modbus.get_all_slave_state(mb_ascii)
    log.info("modbus", status)
end, 5000)

-- 获取从站1的状态,每个5秒获取从站状态并在日志打印出来(仅方便调试使用,量产时可删除)
sys.timerLoopStart(function()
    local status = modbus.get_slave_state(mb_slave1)
    log.info("modbus1", status)
end, 5000)

-- -- 获取从站2的状态,每个5秒获取从站状态并在日志打印出来(仅方便调试使用,量产时可删除)
-- sys.timerLoopStart(function()
--     local status = modbus.get_slave_state(mb_slave2)
--     log.info("modbus2", status)
-- end, 5000)

-- -- 每个5秒执行一次mb_slave1_msg1消息,使用modbus.exec(master_handler, msg_handler)接口须先在modbus.set_msg_comm_period(msg_handler, comm_period)接口中设置为手动模式;成功返回true,其他情况返回false
-- sys.timerLoopStart(function()
--     local status=modbus.exec(mb_ascii, mb_slave1_msg1)
--     log.info("msg",status)
-- end,5000)

-- -- 测试删除一个从站对象,并删除与之相关的通讯消息句柄。需在主站停止时(modbus.master_stop)执行该操作,否则无效。
-- -- 将3分钟后删除从站1(主站已关闭),删除与之相关的通讯消息句柄,并在5秒后重启主站,可以观察从站是否删除成功。
-- sys.timerStart(function()
--     local status = modbus.remove_slave(mb_ascii, mb_slave1)
--     log.info("modbus", "slave1 remove after 3 minutes")
--     log.info("remove", status)

-- -- 移除从站后,5秒后重新启动Modbus主站
--     sys.timerStart(function()
--         modbus.master_start(mb_ascii)
--         log.info("modbus", "Modbus master restarted after slave removal")
--     end, 5000)
-- end, 180000) 

-- 获取从站1的状态,每1秒获取一次数据并转换为JSON
sys.timerLoopStart(function()
    -- 检查从站状态
    local status = modbus.get_slave_state(mb_slave1)
    if status == 0 then  -- 0表示正常
        -- 读取缓冲区数据
        slave1_msg1_buf:seek(0)  -- 重置指针到起始位置

        -- 读取4个寄存器的值(每个寄存器2字节)
        local reg1 = slave1_msg1_buf:readU16()
        local reg2 = slave1_msg1_buf:readU16()
        local reg3 = slave1_msg1_buf:readU16()
        local reg4 = slave1_msg1_buf:readU16()

        -- 创建数据表
        local data = {
            addr = 1,  -- 从站地址
            fun = 3,   -- 功能码03
            reg1 = reg1 ,  -- 假设原始数据需要除以10得到实际值
            reg2 = reg2 ,
            reg3 = reg3 ,
            reg4 = reg4 ,
            timestamp = os.time()  -- 添加时间戳
        }

        -- 转换为JSON
        local json_str = json.encode(data)
        log.info("Modbus数据转JSON:", json_str)

    else
        log.warn("从站1状态异常:", status)
    end
end, 1000)


-- -- 将在主站开启2分钟后停止modbus主站
-- sys.timerStart(function()
--     modbus.master_stop(mb_ascii)
--     log.info("modbus", "Modbus stopped after 2 minutes")
-- end, 120000) 

-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
6.2.1.2 效果展示

1.打开 MThings(第四章中 第五项提到的上位机下位机软件 摩尔信使),界面如下。

2.点击右上角进入通道管理。

3.点击通道管理。

4.进入后可以看到 4 个 com 口,选择 485 通道的 com 口进行配置。

由设备管理器可知 485 通道是 com20

5.点入配置,按照代码中 uart.setup 设置参数后,选择 ASCII 传输协议,点击确定即可,设置完毕后在通道管理界面配置的通道显示属于正常状态。用户如果接的是自己的下位机,请将 uart.setup 传入的参数成自己下位机的配置参数。

6.返回初始界面,点击添加设备。

7.通道选择刚刚配置过的 485 通道 COM20,设备类型是模拟从机,地址是添加从站的 id。

8.增加数据配置,数据条目是寄存器的数量。

点击确定后可以看到增加了 4 个条目,双击数值一栏,然后再双击固定值一栏即可按需选择模拟数据的方法

9.数据配置完成后,在主界面可以看到数值按照设置的通讯时间间隔开始变换,在日志中也可以看到收到的数据,modbus 连接和通讯成功。

6.2.2 modbus.slave_ascii

6.2.2.1 完整代码展示
-- LuaTools需要PROJECT和VERSION这两个信息
PROJECT = "modbus_slave_rtu"
VERSION = "1.0.0"
log.style(1)
log.info("main", PROJECT, VERSION)

-- 引入必要的库文件(lua编写), 内部库不需要require
sys = require("sys")

--初始化通讯串口
local uartid = 1        -- 根据实际设备选取不同的uartid
local uart485Pin = 17   -- 用于控制485接收和发送的使能引脚
gpio.setup(16, 1)        --打开电源(开发板485供电脚是gpio16,用开发板测试需要开机初始化拉高gpio16)
uart.setup(uartid, 115200, 8, 1, uart.NONE, uart.LSB, 1024, uart485Pin, 0, 2000)--用户如果接的是自己的下位机,请将uart.setup传入的参数成自己下位机的配置参数

-- 创建从站设备,可选择RTU、ASCII、TCP,此demo仅用作测试RTU和ASCII。
local slave_id = 1
mb_rtu_s = modbus.create_slave(modbus.MODBUS_RTU, slave_id, uartid)

-- 添加一块寄存器内存区
registers = zbuff.create(1)
modbus.add_block(mb_rtu_s, modbus.REGISTERS, 0, 32, registers)
registers:clear()

-- 创建线圈数据区
ciols = zbuff.create(1)
modbus.add_block(mb_rtu_s, modbus.CIOLS, 0, 32, ciols)
ciols:clear()

-- 启动modbus从站
modbus.slave_start(mb_rtu_s)

local counter = 0
-- 修改和读取modbus值
function modify_data()
    counter = counter + 1  
    -- 写入寄存器数据 (16位无符号整数)
    registers:seek(0)
    for i=0,31 do
        registers:writeU16((counter + i) % 65536)  -- 写入递增数字,限制在0-65535
    end  
    -- 写入线圈数据 (1位布尔值)
    ciols:seek(0)
    for i=0,31 do
        ciols:writeU8((counter + i) % 2)  -- 交替写入0和1
    end

    -- 读取并打印部分数据用于调试
    registers:seek(0)
    ciols:seek(0)
    log.info("registers:", registers:readU16(), registers:readU16(), registers:readU16(), registers:readU16(), registers:readU16())
    log.info("ciols    :", ciols:readU8(), ciols:readU8(), ciols:readU8(), ciols:readU8(), ciols:readU8())
end
sys.timerLoopStart(modify_data,1000)

-- -- 测试停止modbus从站,将在从站启动两分钟后关闭
-- sys.timerStart(function()
--     modbus.slave_stop(mb_rtu_s)
--     log.info("Modbus", "2分钟时间到,停止Modbus从站")
-- end, 2 * 60 * 1000)  -- 2分钟(单位:毫秒)

-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!
6.2.2.2 效果展示

1.打开 MThings(第四章中 第五项提到的上位机下位机软件 摩尔信使),界面如下。

2.点击右上角进入通道管理。

3.点击通道管理。

4.进入后可以看到 4 个 com 口,选择 485 通道的 com 口进行配置。

由设备管理器可知 485 通道是 com20

5.点入配置,按照代码中 uart.setup 设置参数后,选择 ASCII 传输协议,点击确定即可,设置完毕后在通道管理界面配置的通道显示属于正常状态。用户如果接的是自己的下位机,请将 uart.setup 传入的参数成自己下位机的配置参数。

6.返回初始界面,点击添加设备。

7.通道选择刚刚配置过的 485 通道 COM20,设备类型是模拟主站,地址是创建从站的 id。

8.增加数据配置,根据需要选择读取的寄存器与线圈数据。

9.添加成功后,双击数值栏就可以获取目前从站的数据了,开发板做从站和主站连接通讯成功。

6.3 TCP 协议运行结果

6.3.1 modbus.master_tcp

6.3.1.1 完整代码展示

mian.lua

-- LuaTools需要PROJECT和VERSION这两个信息
PROJECT = "modbus_master_tcp"
VERSION = "1.0.0"
log.style(1)
log.info("main", PROJECT, VERSION)

-- sys库是标配
_G.sys = require("sys")
_G.sysplus = require("sysplus")

--初始化网络
log.info("ch390", "打开LDO供电")
gpio.setup(140, 1)  --打开lan供电

mcu.hardfault(0) -- 死机后停机,一般用于调试状态
require "lan"

-- 创建主站设备,TCP模式
-- 设置连接方式为socket.LWIP_ETH
-- 设置通讯间隔时间,主站将按每隔 设置时间 的频率向从站问询数据(默认100ms),当添加了多个从站后,主站向每个从站问询的时间间隔将叠加
-- 设置通讯超时时间和消息发送超时重发次数,当主站未在 设置的时间 内接收到从站数据,将向从站再次发送问询(问询次数按设置的 消息超时重发次数 发送,默认1)
-- 设置断线重连时间间隔,当从站与主站断连后,主站将在设置时间内重新连接从站(默认5000ms)
mb_tcp = modbus.create_master(modbus.MODBUS_TCP, socket.LWIP_ETH,3000,1,5000)

-- 为主站添加从站,从站ID为1,ip地址为 1192.168.4.100,端口号为 6000,最多可添加247个从站
mb_slave1 = modbus.add_slave(mb_tcp, 1, "192.168.4.100", 6000)
-- 为主站添加从站,从站ID为2,ip地址为 192.168.4.100,端口号为 6001
-- mb_slave2 = modbus.add_slave(mb_tcp, 2, "192.168.4.100", 6001)

-- 为从站1创建数据存储区,并创建通讯消息,默认为自动loop模式
slave1_msg1_buf = zbuff.create(1)
mb_slave1_msg1 = modbus.create_msg(mb_tcp, mb_slave1, modbus.REGISTERS, modbus.READ, 0, 10, slave1_msg1_buf)
slave1_msg1_buf:clear()

-- -- 为从站1创建数据存储区,并创建通讯消息,如需要使用手动模式,须在这里设置为手动模式
-- slave1_msg1_buf = zbuff.create(1)
-- mb_slave1_msg1 = modbus.create_msg(mb_tcp, mb_slave1, modbus.REGISTERS, modbus.READ, 0, 10, slave1_msg1_buf,1,modbus.EXEC)
-- slave1_msg1_buf:clear()

-- 为从站2创建数据存储区,并创建通讯消息
-- slave2_msg1_buf = zbuff.create(1)
-- mb_slave2_msg1 = modbus.create_msg(mb_tcp, mb_slave2, modbus.REGISTERS, modbus.WRITE, 0, 10, slave2_msg1_buf)
-- slave2_msg1_buf:clear()

-- 启动Modubs设备
modbus.master_start(mb_tcp)
log.info("start modbus master")

-- -- 设置通讯间隔时间,设置后主站将按每隔 设置时间 的频率向从站问询数据,当添加了多个从站后,主站向每个从站问询的时间间隔将叠加
-- modbus.set_comm_interval_time(mb_tcp,3000)

-- -- 设置通讯超时时间,当主站未在 设置的时间 内接收到从站数据,将向从站再次发送问询(问询次数按设置的 消息超时重发次数 发送)
-- modbus.set_comm_timeout(mb_tcp, 3000)

-- -- 设置消息发送失败、超时重发次数,如果主站在设置超时时间内未接收到数据,将按设置次数问询数据
-- modbus.set_comm_resend_count(mb_tcp,1)

-- 设置断线重连时间间隔,若从站与主站断连,主站将在设置时间内重新连接从站
-- modbus.set_comm_reconnection_time(mb_tcp, 5000)

-- -- 设置消息通讯周期,搭配modbus.create_master/modbus.set_comm_interval_time(mb_tcp,3000)设置通讯时间使用,若设置通讯周期为2次,将在2倍的通讯时间后向从站问询数据
-- modbus.set_msg_comm_period(mb_slave1_msg1, 2)

-- 获取所有从站状态,如果所有从站状态为正常,返回true,其他情况返回false,将在每隔5秒的时间获取所有从站状态,并在日志中打印状态(仅方便调试使用,量产时可删除)
sys.timerLoopStart(function()
    local status = modbus.get_all_slave_state(mb_tcp)
    log.info("modbus", status)
end, 5000)

-- 获取从站1的状态,每个5秒获取从站状态并在日志打印出来(仅方便调试使用,量产时可删除)
sys.timerLoopStart(function()
    local status = modbus.get_slave_state(mb_slave1)
    log.info("modbus1", status)
end, 5000)

-- -- 获取从站2的状态,每个5秒获取从站状态并在日志打印出来(仅方便调试使用,量产时可删除)
-- sys.timerLoopStart(function()
--     local status = modbus.get_slave_state(mb_slave2)
--     log.info("modbus2", status)
-- end, 5000)

-- -- 每个5秒执行一次 mb_slave1_msg1 消息,使用modbus.exec(master_handler, msg_handler)接口须先在modbus.set_msg_comm_period(msg_handler, comm_period)接口中设置为手动模式;成功返回true,其他情况返回false
-- sys.timerLoopStart(function()
--     local status=modbus.exec(mb_tcp, mb_slave1_msg1)
--     log.info("msg",status)
-- end,5000)

-- -- 测试删除一个从站对象,并删除与之相关的通讯消息句柄。需在主站停止时(modbus.master_stop)执行该操作,否则无效。
-- -- 将在3分钟后删除从站2(主站已关闭),删除与之相关的通讯消息句柄,并在5秒后重启主站,可以观察从站是否删除成功。
-- sys.timerStart(function()
--     local status = modbus.remove_slave(mb_tcp, mb_slave2)
--     log.info("modbus", "slave remove after 3 minutes")
--     log.info("remove", status)

--     -- 移除从站后,5秒后重新启动Modbus主站
--     sys.timerStart(function()
--         modbus.master_start(mb_tcp)
--         log.info("modbus", "Modbus master restarted after slave removal")
--     end, 5000)
-- end, 180000) 

-- 获取从站1的状态,每1秒获取一次数据并转换为JSON
sys.timerLoopStart(function()
    -- 检查从站状态
    local status = modbus.get_slave_state(mb_slave1)
    if status == 0 then  -- 0表示正常
        -- 读取缓冲区数据
        slave1_msg1_buf:seek(0)  -- 重置指针到起始位置

        -- 读取4个寄存器的值(每个寄存器2字节)
        local reg1 = slave1_msg1_buf:readU16()
        local reg2 = slave1_msg1_buf:readU16()
        local reg3 = slave1_msg1_buf:readU16()
        local reg4 = slave1_msg1_buf:readU16()

        -- 创建数据表
        local data = {
            addr = 1,  -- 从站地址
            fun = 3,   -- 功能码03
            reg1 = reg1 ,  -- 假设原始数据需要除以10得到实际值
            reg2 = reg2,
            reg3 = reg3,
            reg4 = reg4,
            timestamp = os.time()  -- 添加时间戳
        }

        -- 转换为JSON
        local json_str = json.encode(data)
        log.info("Modbus数据转JSON:", json_str)

    else
        log.warn("从站1状态异常:", status)
    end
end, 1000)

-- -- 将在主站开启2分钟后停止modbus主站
-- sys.timerStart(function()
--     modbus.master_stop(mb_tcp)
--     log.info("modbus", "Modbus stopped after 2 minutes")
-- end, 120000) 

-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!

lan.lua

-- 引入必要的库文件(lua编写), 内部库不需要require
sys = require("sys")
sysplus = require("sysplus")

dhcps = require "dhcpsrv"
dnsproxy = require "dnsproxy"

sys.taskInit(function ()
    -- sys.wait(3000)
    local result = spi.setup(
        0,--串口id
        nil,
        0,--CPHA
        0,--CPOL
        8,--数据宽度
        25600000--,--频率
        -- spi.MSB,--高低位顺序    可选,默认高位在前
        -- spi.master,--主模式     可选,默认主
        -- spi.full--全双工       可选,默认全双工
    )
    log.info("main", "open",result)
    if result ~= 0 then--返回值为0,表示打开成功
        log.info("main", "spi open error",result)
        return
    end

    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spiid=0,cs=8})
    sys.wait(3000)
    local ipv4,mark, gw = netdrv.ipv4(socket.LWIP_ETH, "192.168.4.1", "255.255.255.0", "192.168.4.1")
    log.info("ipv4", ipv4,mark, gw)
    while netdrv.link(socket.LWIP_ETH) ~= true do
        sys.wait(100)
    end
    dhcps.create({adapter=socket.LWIP_ETH})

    -- dnsproxy.setup(socket.LWIP_ETH, socket.LWIP_GP)
    -- netdrv.napt(socket.LWIP_GP)

    dnsproxy.setup(socket.LWIP_ETH, socket.LWIP_ETH)
    netdrv.napt(socket.LWIP_ETH)
end)

sys.taskInit(function()
    -- sys.waitUntil("IP_READY")
    while 1 do
        sys.wait(300000)
        -- log.info("http", http.request("GET", "http://httpbin.air32.cn/bytes/4096", nil, nil, {adapter=socket.LWIP_ETH}).wait())
        log.info("lua", rtos.meminfo())
        log.info("sys", rtos.meminfo("sys"))
        -- log.info("psram", rtos.meminfo("psram"))
    end
end)
6.3.1.2 效果展示

1.打开 MThings(第四章中 第五项提到的上位机下位机软件 摩尔信使),界面如下。

2.点击右上角进入通道管理。

3.点击通道管理。

4.进入后选择网络通道,然后进行网络参数配置。

5.连接模式选择 tcp 服务器,本地 ip 在把脚本烧录后可以看到开发板分配的 ip,端口号设置为代码中 mb_slave1 = modbus.add_slave(mb_tcp, 1, "192.168.4.100", 6000)创建从站的端口号。

6.返回初始界面,点击添加设备。

7.通道选择刚刚配置的 NET001 网络通道,设备类型是模拟从机,地址是创建从站的 id。

8.增加数据配置,根据需要选择寄存器或线圈数据。

9.点击确定后可以看到增加了 4 个条目,双击数值一栏,然后再双击固定值一栏即可按需选择模拟数据的方法。

10.设置完成后则可以看到模拟从机数据开始变化,日志上显示开发板获取数据,主站与从站连接通讯成功。

6.3.2 modbus.slave_tcp

6.3.2.1 完整代码展示

main.lua

-- LuaTools需要PROJECT和VERSION这两个信息
PROJECT = "modbus_slave_tcp"
VERSION = "1.0.0"
log.style(1)
log.info("main", PROJECT, VERSION)

-- sys库是标配
_G.sys = require("sys")
_G.sysplus = require("sysplus")

log.info("ch390", "打开LDO供电")
gpio.setup(140, 1)  --打开lan供电
require "lan"

-- 创建从站设备,可选择RTU、ASCII、TCP,此demo仅用作测试TCP。设置该从站端口号为6000,网卡适配器序列号为socket.LWIP_ETH。
local slave_id = 1
mb_tcp_s = modbus.create_slave(modbus.MODBUS_TCP, slave_id, 6000, socket.LWIP_ETH)

-- 创建寄存器数据区
registers = zbuff.create(1)
modbus.add_block(mb_tcp_s, modbus.REGISTERS, 0, 32, registers)
registers:clear()
-- 创建线圈数据区
ciols = zbuff.create(1)
modbus.add_block(mb_tcp_s, modbus.CIOLS, 0, 32, ciols)
ciols:clear()

-- 启动modbus从站
modbus.slave_start(mb_tcp_s)
log.info("start modbus slave")

local counter = 0
-- 修改和读取modbus值
function modify_data()
    counter = counter + 1
    -- 写入寄存器数据 (16位无符号整数)
    registers:seek(0)
    for i=0,31 do
        registers:writeU16((counter + i) % 65536)  -- 写入递增数字,限制在0-65535
    end
    -- 写入线圈数据 (1位布尔值)
    ciols:seek(0)
    for i=0,31 do
        ciols:writeU8((counter + i) % 2)  -- 交替写入0和1
    end

    -- 读取并打印部分数据用于调试
    registers:seek(0)
    ciols:seek(0)
    log.info("registers:", registers:readU16(), registers:readU16(), registers:readU16(), registers:readU16(), registers:readU16())
    log.info("ciols    :", ciols:readU8(), ciols:readU8(), ciols:readU8(), ciols:readU8(), ciols:readU8())
end
sys.timerLoopStart(modify_data,1000)

-- -- 测试停止modbus从站,从站将在开启两分钟后关闭
-- sys.timerStart(function()
--     modbus.slave_stop(mb_rtu_s)
--     log.info("Modbus", "2分钟时间到,停止Modbus从站")
-- end, 120000)

-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!

lan.lua

-- 引入必要的库文件(lua编写), 内部库不需要require
sys = require("sys")
sysplus = require("sysplus")

dhcps = require "dhcpsrv"
dnsproxy = require "dnsproxy"

sys.taskInit(function ()
    -- sys.wait(3000)
    local result = spi.setup(
        0,--串口id
        nil,
        0,--CPHA
        0,--CPOL
        8,--数据宽度
        25600000--,--频率
        -- spi.MSB,--高低位顺序    可选,默认高位在前
        -- spi.master,--主模式     可选,默认主
        -- spi.full--全双工       可选,默认全双工
    )
    log.info("main", "open",result)
    if result ~= 0 then--返回值为0,表示打开成功
        log.info("main", "spi open error",result)
        return
    end

    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spiid=0,cs=8})
    sys.wait(3000)
    local ipv4,mark, gw = netdrv.ipv4(socket.LWIP_ETH, "192.168.4.1", "255.255.255.0", "192.168.4.1")
    log.info("ipv4", ipv4,mark, gw)
    while netdrv.link(socket.LWIP_ETH) ~= true do
        sys.wait(100)
    end
    dhcps.create({adapter=socket.LWIP_ETH})

    -- dnsproxy.setup(socket.LWIP_ETH, socket.LWIP_GP)
    -- netdrv.napt(socket.LWIP_GP)

    dnsproxy.setup(socket.LWIP_ETH, socket.LWIP_ETH)
    netdrv.napt(socket.LWIP_ETH)

    -- netdrv.dhcp(socket.LWIP_ETH, true)
end)

sys.taskInit(function()
    sys.waitUntil("IP_READY")
    while 1 do
        sys.wait(300000)
        -- log.info("http", http.request("GET", "http://httpbin.air32.cn/bytes/4096", nil, nil, {adapter=socket.LWIP_ETH}).wait())
        log.info("lua", rtos.meminfo())
        log.info("sys", rtos.meminfo("sys"))
        -- log.info("psram", rtos.meminfo("psram"))
    end
end)
6.3.2.2 效果展示

1.打开 MThings(第四章中 第五项提到的上位机下位机软件 摩尔信使),界面如下。

2.点击右上角进入通道管理。

3.点击通道管理。

4.进入后选择网络通道,然后进行网络参数配置。

5.连接模式选择 tcp 客户端,本地 ip 在把脚本烧录后可以看到开发板分配的 ip,目标 ip 是开发板的 IP,为 192.168.4.1,目标端口号为脚本中 mb_tcp_s = modbus.create_slave(modbus.MODBUS_TCP, slave_id, 6000, socket.LWIP_ETH)设置的。

6.返回初始界面,点击添加设备。

7.通道选择刚刚配置的 NET001 网络通道,设备类型是模拟主站,地址是创建从站的 id。

8.增加数据配置,根据需要选择寄存器或线圈数据。

9.点击数值栏获取到从站寄存器和线圈的数据,主站与从站连接与通讯成功。

七、总结

本教程演示了 Air8000 modbus 在 RTU、ASCII 和 TCP 三种协议下的使用过程,请根据具体场景选择您需要的 demo 即可。

八、常见问题

1.删除从站地址和句柄位删除失败

删除失败可能是主站为关闭,删除从站需要在主站关闭后,删除后可再次打开主站观察从站是否删除成功。

2.开发板用 TCP 协议,如何修改开发板与 PC 端的连接方式

在 lan.lua 中修改 socket API