主题
com_modbus — 串口 Modbus 主站
模块名:com_modbus
作为串口 Modbus 主站(RTU / ASCII)采集从站数据。它本身不碰串口,只描述「一个从站有哪些通道」,必须用 com.add_device 挂到一个 com 对象上,再由 com.start 的轮询线程驱动。通道的值直接读写到 PLC 的 D 寄存器或 data 对象,脚本里不需要自己发报文、算 CRC。
除 creat 之外的函数在库表和对象元表里都注册了,com_modbus.add_channel(dev, ...) 和 dev:add_channel(...) 等价。
函数总览
库级函数
| 函数 | 作用 |
|---|---|
com_modbus.creat(work_mode, device_addr, [data_state]) | 创建一个 Modbus 从站对象 |
对象方法
| 方法 | 作用 |
|---|---|
dev:add_channel | 增加一条采集/写入通道 |
dev:set_16_ab | 设置 16 位整数的字节序 |
dev:set_32_abcd | 设置 32 位整数的字节序 |
dev:set_float_abcd | 设置单精度浮点的字节序 |
dev:set_double_abcdefgh | 设置双精度浮点的字节序 |
dev:set_time_recv | 设置本从站的接收超时 |
dev:set_time_error | 设置出错后的重试延时 |
常量
工作模式(creat 第 1 参)
| 常量 | 值 | 说明 |
|---|---|---|
com_modbus.RTU | 0 | Modbus RTU(二进制帧,常用) |
com_modbus.ASCII | 1 | Modbus ASCII |
读写属性(add_channel 最后一个参数)
| 常量 | 值 | 说明 |
|---|---|---|
com_modbus.READ | 0 | 只读:从站 → 本地 |
com_modbus.READ_WRITE | 1 | 读写:本地值变化时写下去,否则读上来 |
com_modbus.WRITE | 2 | 只写:本地 → 从站 |
数据类型
| 常量 | 值 | 占用寄存器 | 说明 |
|---|---|---|---|
com_modbus.BITE | 0 | — | 位(线圈 / 离散输入) |
com_modbus.S16 | 1 | 1 | 16 位有符号 |
com_modbus.U16 | 2 | 1 | 16 位无符号 |
com_modbus.S32 | 3 | 2 | 32 位有符号 |
com_modbus.U32 | 4 | 2 | 32 位无符号 |
com_modbus.FLOAT | 5 | 2 | 单精度浮点 |
com_modbus.DOUBLE | 6 | 4 | 双精度浮点 |
字节序
| 用于 | 常量(值) |
|---|---|
set_16_ab | com_modbus.AB (0)、com_modbus.BA (1) |
set_32_abcd / set_float_abcd | com_modbus.ABCD (0)、com_modbus.CDAB (1)、com_modbus.BADC (2)、com_modbus.DCBA (3) |
set_double_abcdefgh | com_modbus.ABCDEFGH (0)、com_modbus.GHEFCDAB (1)、com_modbus.BADCFEHG (2)、com_modbus.HGFEDCBA (3) |
字节序设置与通道数据类型必须配对
四个设置函数各自保存独立的字节序,只在解析对应数据类型的通道时被读取,互不影响:
设置函数 生效的数据类型 默认值 set_16_abS16、U16ABset_32_abcdS32、U32CDABset_float_abcdFLOATCDABset_double_abcdefghDOUBLEGHEFCDAB通道声明成
S16却去调set_32_abcd,是最常见的「设置了没反应」原因 —— S16 通道只会读set_16_ab的设置。 只需设置实际用到的类型那一项;设置不匹配的类型不会报错,也不会生效。
Modbus 区号(用于 5 参形式的 add_channel,也是组合地址的首位)
| 区号 | 值 | 含义 |
|---|---|---|
| DO | 0 | 线圈(可读写位) |
| DI | 1 | 离散输入(只读位) |
| AI | 3 | 输入寄存器(只读字) |
| VW | 4 | 保持寄存器(可读写字) |
com_modbus.creat
lua
com_modbus.creat(mode, addr, [status]) --> device创建一个 Modbus 从站对象。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | mode | integer | 是 | — | com_modbus.RTU 或 com_modbus.ASCII |
| 2 | addr | integer | 是 | — | 从站地址,取低 8 位 |
| 3 | status | integer | data | 否 | — | 通信状态回写位置。传数字 = D 寄存器编号,传 data 对象 = 写到该对象。省略则不回写状态 |
返回值
| 类型 | 说明 |
|---|---|
| userdata | Modbus 从站对象。必须用变量接住,否则可能被 GC 回收 |
示例
lua
local state = data.creat(0)
local dev1 = com_modbus.creat(com_modbus.RTU, 1, state) -- RTU,从站地址 1说明
status用来判断这台从站是否通信正常,建议一并配置,否则采不到数据时无法区分是从站掉线还是数值确实为 0。
实用例程
lua
modbus_state=data.creat(0)--连接状态标志对象
dianlu=data.creat(0)--电流对象
--zongliangh=data.creat(0)--总量x100对象
--zongliangl=data.creat(0)--总量x1对象
meter=com_modbus.creat(com_modbus.RTU,3,modbus_state)--建立MODBUS子设备,数字是设备地址
--meter:set_32_abcd(com_modbus.)--32位数编码方式
--meter:set_float_abcd(com_modbus.ABCD)
meter:set_time_recv(2000)--接收时间
meter:add_channel(400001,com_modbus.S16,dianlu,com_modbus.READ)--加入采集通道,数字是寄存器D的地址,400001是要采集的MODBUS设备寄存器
--meter:add_channel(400001,com_modbus.S16,zongliangh,com_modbus.READ)--加入采集通道
--meter:add_channel(400001,com_modbus.FLOAT,zongliangh,com_modbus.READ)--加入采集通道
--meter:add_channel(400004,com_modbus.FLOAT,zongliangl,com_modbus.READ)--加入采集通道
--meter:add_channel(400006,com_modbus.FLOAT,liuliang,com_modbus.READ)--加入采集通道
serial=com.setup(1,9600,com.NONE,8,1)--建立串口父设备,前面 1是串口号
serial:add_device(meter)--加入子设备
serial:set_time_lunxun(2000)--设定轮询时间
serial:set_timeout(2000)--接收超时时间
serial:start()--启动串口数据采集
ds1=timer.start(5000,function()
print("dianlu:",dianlu)--plc.d(100))
plc.d(120,dianlu)
end
)把上面的地址、主题、引脚替换成你项目里的即可。
dev:add_channel
lua
-- 组合地址形式(4 参)
dev:add_channel(modbus_addr, data_type, target, rw) --> 0 | 1
-- 分离形式(5 参)
dev:add_channel(area, offset, data_type, reg, rw) --> 0 | 1给从站增加一条通道。两种写法等价,选一种用。
参数(组合地址形式)
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | modbus_addr | integer | 是 | — | 6 位组合地址,形如 400001:首位是区号(0/1/3/4),后 5 位是从 1 开始的点号。内部换算为 区号 = addr/100000、偏移 = addr%100000 - 1 |
| 2 | data_type | integer | 是 | — | com_modbus.S16 等数据类型常量 |
| 3 | target | integer | data | 是 | — | 值的落点编号。落点由数据类型决定:BITE 写进 M 区,字类型(S16/U16/S32/U32/FLOAT/DOUBLE)写进 D 区;传 data 对象则与 D/M 无关 |
| 4 | rw | integer | 是 | — | com_modbus.READ / READ_WRITE / WRITE |
参数(分离形式)
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | area | integer | 是 | — | 区号:0=DO、1=DI、3=AI、4=VW |
| 2 | offset | integer | 是 | — | 区内偏移,从 0 开始 |
| 3 | data_type | integer | 是 | — | 数据类型常量 |
| 4 | reg | integer | 是 | — | 落点编号,只接受数字(此形式不接受 data 对象)。BITE 时是 M 编号,字类型时是 D 编号 |
| 5 | rw | integer | 是 | — | 读写属性 |
返回值
| 类型 | 说明 |
|---|---|
| integer | 1 添加成功,0 添加失败。参数个数不是 4 或 5 时返回 nil |
示例
lua
-- 读保持寄存器 40001-40002 的浮点 → data 对象
dev1:add_channel(400001, com_modbus.FLOAT, temp, com_modbus.READ)
-- 读保持寄存器 40010 的 16 位有符号 → D100
dev1:add_channel(400010, com_modbus.S16, 100, com_modbus.READ)
-- 写线圈 00001(本地 M200 → 从站)
dev1:add_channel(1, 100000 + 1 - 1, com_modbus.BITE, 200, com_modbus.WRITE)
-- 分离形式:保持寄存器偏移 9(= 40010)→ D100
dev1:add_channel(4, 9, com_modbus.S16, 100, com_modbus.READ)说明
- 组合地址是「从 1 开始」的习惯写法:
400001对应寄存器偏移0。用分离形式时偏移直接从0数。 BITE类型只能配 DO(0 区)或 DI(1 区);字类型配 AI(3 区)或 VW(4 区)。- 位和字的落点不在同一个区:
BITE通道填200落的是M200,字类型填200落的是D200。 用plc.d(200)去读一条BITE通道永远读不到,要写plc.m(200)。 两条通道用了同一个编号也会打架(一个占 M 一个占 D,反而看不出来),所以建议位类通道集用一个 M 段、字类通道用一个 D 段,互不重叠。 - 一个从站可以加多条通道,轮询时按添加顺序依次访问。
dev:set_16_ab
lua
dev:set_16_ab(order)设置 16 位整数的字节序。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | order | integer | 是 | com_modbus.AB | com_modbus.AB 或 com_modbus.BA |
返回值
无返回值。
说明
- 只对
S16/U16通道生效。S32/U32/FLOAT/DOUBLE通道不受它影响。 - 反过来也一样:
S16通道的字节序只能用这个函数改,调set_32_abcd是无效的。
dev:set_32_abcd
lua
dev:set_32_abcd(order)设置 32 位整数(S32/U32)的寄存器与字节顺序。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | order | integer | 是 | com_modbus.CDAB | ABCD / CDAB / BADC / DCBA |
返回值
无返回值。
说明
32 位数据读出来数值离谱(比如温度显示成几万),十有八九是这里没设对。多数国产仪表用
CDAB。只对
S32/U32通道生效,对FLOAT不生效(浮点用set_float_abcd)。「设置了没反应」的排查:先确认
add_channel的数据类型确实是S32或U32。lua-- ❌ 通道是 S16,set_32_abcd 永远不会被读取 dev:set_32_abcd(com_modbus.BADC) dev:add_channel(400001, com_modbus.S16, v0, com_modbus.READ) -- ✅ 情况一:数据本来是 16 位,只是高低字节反了 dev:set_16_ab(com_modbus.BA) dev:add_channel(400001, com_modbus.S16, v0, com_modbus.READ) dev:add_channel(400002, com_modbus.S16, v1, com_modbus.READ) -- ✅ 情况二:数据确实是 32 位,通道类型也要一起改 dev:set_32_abcd(com_modbus.BADC) dev:add_channel(400001, com_modbus.S32, v0, com_modbus.READ) -- 注意 S32 占用 400001+400002 两个寄存器,不能再单独注册 400002
dev:set_float_abcd
lua
dev:set_float_abcd(order)设置单精度浮点(FLOAT)的寄存器与字节顺序。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | order | integer | 是 | com_modbus.CDAB(不设置时的默认) | com_modbus.ABCD / com_modbus.CDAB / com_modbus.BADC / com_modbus.DCBA,取值规则同 set_32_abcd,但存的是独立的一份设置 |
返回值
无返回值。
示例
lua
temp = data.creat(0)
dev = com_modbus.creat(com_modbus.RTU, 1)
dev:set_float_abcd(com_modbus.DCBA) -- 该表的浮点是字内也倒过来
dev:add_channel(400001, com_modbus.FLOAT, temp, com_modbus.READ)说明
- 只对
FLOAT通道生效,与set_32_abcd是两个互不相干的设置。同一个从站上既有U32又有FLOAT通道时,两个函数都要调。 - 字节序在每帧解码时现读,
com:start之后调用也有效,下一轮询即用新顺序。 - 传四个常量以外的数值不会报错,但解码时四个分支都匹配不上,该从站的
FLOAT通道不再被更新。「浮点值不动了」先检查这里。
dev:set_double_abcdefgh
lua
dev:set_double_abcdefgh(order)设置双精度浮点(DOUBLE)的顺序。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | order | integer | 是 | com_modbus.GHEFCDAB | ABCDEFGH / GHEFCDAB / BADCFEHG / HGFEDCBA |
返回值
无返回值。
说明
- 只对
DOUBLE通道生效。DOUBLE占用连续 4 个寄存器。
dev:set_time_recv
lua
dev:set_time_recv(ms)设置本从站的应答等待超时。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | ms | integer | 是 | — | 超时毫秒数 |
返回值
无返回值。
说明
- 这是每个从站独立的超时,比
com.set_timeout更精细。慢速仪表可以单独加大,不影响同总线上的其它设备。
dev:set_time_error
lua
dev:set_time_error(ms)设置本从站通信出错后,下一次重试前的延时。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | ms | integer | 是 | — | 延时毫秒数 |
返回值
无返回值。
说明
- 设大一点可以避免某台掉线设备反复超时、拖慢整条总线的轮询节奏。
常见用法
一条 RS485 挂两台仪表,值写进 D 区给梯形图用
lua
-- 1 号:温控表,读浮点温度
local st1 = data.creat(0)
local d1 = com_modbus.creat(com_modbus.RTU, 1, st1)
d1:set_float_abcd(com_modbus.CDAB) -- 国产表常见字序
d1:set_time_recv(500)
d1:add_channel(400001, com_modbus.FLOAT, 100, com_modbus.READ) -- → D100-D101
-- 2 号:电表,读 32 位电量
local st2 = data.creat(0)
local d2 = com_modbus.creat(com_modbus.RTU, 2, st2)
d2:set_32_abcd(com_modbus.CDAB)
d2:add_channel(400101, com_modbus.U32, 110, com_modbus.READ) -- → D110-D111
local port = com.setup(1, 9600, com.NONE, 8, 1)
port:add_device(d1)
port:add_device(d2)
port:set_time_lunxun(1000)
port:start()
timer.start(2000, function()
print("温度:", plc.df(100), "在线:", st1:get_num())
print("电量:", plc.dd(110), "在线:", st2:get_num())
end)把 PLC 计算结果写到变频器
lua
local vfd = com_modbus.creat(com_modbus.RTU, 3)
vfd:add_channel(400001, com_modbus.U16, 300, com_modbus.WRITE) -- D300 → 从站 40001
port:add_device(vfd)注意事项
- 字序设置容易出错:32 位和浮点读出乱码时可先尝试
CDAB,再依次尝试其它三种。 - 字序设置函数要和通道数据类型配对:
set_16_ab→S16/U16、set_32_abcd→S32/U32、set_float_abcd→FLOAT、set_double_abcdefgh→DOUBLE。给S16通道调set_32_abcd不会生效——这是「设置了没反应」的头号原因。 - 组合地址从 1 开始(
400001= 偏移 0),分离形式从 0 开始,两者不要混用。 - 建议传入
creat的第 3 个status参数,用于判断从站是否在线。 - 从站对象和
data对象都要用变量长期持有,被 GC 回收后采集会失效。 - 轮询启动后不要再对同一串口手动
send,会打乱报文时序。
例程
每个例程都是可直接下载运行的完整脚本:
com_modbus只描述「从站有哪些通道」,挂到com并start后由后台线程轮询,脚本注册完即结束。 API 只用本页已文档化的函数。统一模板「场景 / 完整代码 / 要点 / 验证方法 / 注意事项」。
例程 1:轮询一台温控表的浮点温度(含字节序与在线状态)
适用:LP3201 / LP3202 / LP3203 / LP3222 / LP3232 / LP3288 / 软PLC模拟器 | 依赖模块:com_modbus com data timer plc | 难度:★☆☆
场景:现场一台 Modbus RTU 温控表(地址 1),浮点温度放在保持寄存器 40001。希望自动轮询到 D100,并知道它是否在线。
完整代码(可直接下载运行):
lua
local st = data.creat(0) -- 在线状态回写位
local dev1 = com_modbus.creat(com_modbus.RTU, 1, st)
dev1:set_float_abcd(com_modbus.CDAB) -- 国产表常见字序
dev1:set_time_recv(500) -- 本从站应答超时
dev1:add_channel(400001, com_modbus.FLOAT, 100, com_modbus.READ) -- 温度 → D100-D101
local com1 = com.setup(1, 9600, com.NONE, 8, 1)
com1:add_device(dev1)
com1:set_time_lunxun(1000)
com1:start()
timer.start(2000, function()
print("温度:", plc.df(100), "在线:", st:get_num())
end)要点:
creat第 3 参st回写该从站的通信状态码:0=通讯成功、1=报文异常、2=通信超时(与本页例程 3 的stata一致)。判"在线"写st:get_num() == 0; 写成== 1会把"报文异常"当成在线。- 浮点占 2 个寄存器,
plc.df(100)一次读出。 com_modbus不碰串口,必须挂到com并com.start才会轮询。
验证方法:
- 下载重启,D100-D101 出现温度值,
st:get_num()= 0(通讯成功)。 - 拔掉表电源,
st变 2(通信超时);插回后自动恢复。
注意事项:
- 字节序要配对数据类型:
set_float_abcd只对FLOAT通道生效;S16/U16用set_16_ab,S32/U32用set_32_abcd。 - 组合地址
400001从 1 开始(= 偏移 0);分离形式add_channel(4, 0, ...)的偏移从 0 开始。 - 从站对象
dev1和data对象st都要用变量长期持有,被 GC 回收后采集失效。
例程 2:一条 RS485 挂两台仪表(温度 + 电量),各自字节序
适用:同上 | 依赖模块:com_modbus com data timer plc | 难度:★★☆
场景:同一总线挂温控表(地址 1,浮点温度)和电表(地址 2,32 位无符号电量)。两台表字序可能不同,要分别设置;其中电表较慢,单独加大应答超时。
完整代码(可直接下载运行):
lua
local st1 = data.creat(0)
local st2 = data.creat(0)
local d1 = com_modbus.creat(com_modbus.RTU, 1, st1)
d1:set_float_abcd(com_modbus.CDAB)
d1:set_time_recv(500)
d1:add_channel(400001, com_modbus.FLOAT, 100, com_modbus.READ) -- 温度 → D100-D101
local d2 = com_modbus.creat(com_modbus.RTU, 2, st2)
d2:set_32_abcd(com_modbus.CDAB)
d2:set_time_recv(800) -- 慢表单独加大超时,不拖慢总线
d2:add_channel(400101, com_modbus.U32, 110, com_modbus.READ) -- 电量 → D110-D111
local port = com.setup(1, 9600, com.NONE, 8, 1)
port:add_device(d1)
port:add_device(d2)
port:set_time_lunxun(1000)
port:start()
timer.start(2000, function()
print("温度:", plc.df(100), "在线:", st1:get_num())
print("电量:", plc.dd(110), "在线:", st2:get_num())
end)要点:
- 两台从站地址不同,各自
creat独立对象;字节序各自set_*互不影响。 - 32 位数据用
plc.dd(110)读;U32占 D110-D111 两个寄存器。 set_time_recv是每从站独立的超时,慢表加大不影响同总线其它设备。
验证方法:
- D100-D101 出温度,D110-D111 出电量;两台
st都为 1。 - 把
set_32_abcd改成ABCD后电量变成乱码,说明该表字序是CDAB(再改回来)。
注意事项:
- 32 位占两个连续寄存器,不要再单独注册
400102,否则会和400101重叠。 - 给
S16通道调set_32_abcd是无效操作(常见「设置了没反应」)。
例程 3:把本地计算结果写下去(WRITE 通道控制变频器)
适用:同上 | 依赖模块:com_modbus com plc | 难度:★★☆
场景:梯形图/其它逻辑算好变频器频率给定,放在 D300,希望自动下发到 3 号从站的保持寄存器 40001。用 WRITE 通道即可,轮询线程会检测 D300 变化并下发,脚本不用自己拼报文。
完整代码(可直接下载运行):
lua
-- D300 = 频率给定(由梯形图或别的脚本写);自动下发到 3 号从站 40001
local vfd = com_modbus.creat(com_modbus.RTU, 3)
vfd:add_channel(400001, com_modbus.U16, 300, com_modbus.WRITE) -- D300 → 从站 40001
local port = com.setup(1, 9600, com.NONE, 8, 1)
port:add_device(vfd)
port:set_time_lunxun(500)
port:start()
-- 之后只要 plc.d(300) 变化,轮询线程会自动下发;脚本无需其它代码要点:
WRITE通道:本地 D 变化 → 自动下发;READ_WRITE双向(既读上来又在下发时写下去)。- 脚本只需配置一次,无需手动
send。
验证方法:
- 把 D300 改成 50(变频器频率给定),用 Modbus 主站工具读 3 号从站 40001,应同步变成 50。
- 改
READ_WRITE后,从站 40001 被外部改写时,D300 也会跟着变化。
注意事项:
WRITE通道只写不读,想要回读状态要用READ_WRITE。- 轮询启动后不要再对该串口手动
send,会打乱报文时序。 - 从站对象
vfd要长期持有,被 GC 回收后下发停止。
例程 4:现场常用脚本 com_modbus.LPLUA(原文)
适用:同上 | 依赖模块:com_modbus com data timer plc | 难度:★☆☆
场景:读 400001 到本机 D1、读 400002 到数据对象 v,1 秒打印一次;v 超过 100 那么自动回写 88。 这是现场流传最广的一份入门脚本(文件 com_modbus.LPLUA),当前固件上可直接运行,原文收录如下。
完整代码(com_modbus.LPLUA 原文,可直接下载运行):
lua
system.sleep(5000)
--建立子设备
stata=data.creat(0)--通讯状态,0=成功,1=异常回应,2=通讯超时
modbus1=com_modbus.creat(com_modbus.RTU,1,stata)--RTU/ASCII模式,设备地址
--加入通道,数据存放到D区
modbus1:add_channel(400001,com_modbus.S16,1,com_modbus.READ_WRITE)--modbus地址,数据类型,本机存放位置为数字时为D或M区,读写类型
--加入通道,数据存放在对象中
v=data.creat(0)--建立数据接收对象
modbus1:add_channel(400002,com_modbus.S16,v,com_modbus.READ_WRITE)
--设定子设备参数
modbus1:set_time_error(1000)--设定读写错误时的恢复时间
--建立父设备
com1=com.setup(2,9600,com.NONE,8,1)--打开串口父设备,注意这里要与子设备通讯格式相同,比如ascii设备应该设定为7位数据
com1:add_device(modbus1)--在串口父设备中加入子设备
com1:start()--启动串口父设备
ds=timer.start(1000,function()
--输出读取到的值
print("400001=".. plc.d(1).." 400002=".. v:get_num().."通讯状态=".. stata:get_num())
--数据改变时会自动写入到设备
if v:get_num()>100 then
v:set(88)
end
end)要点:
com_modbus.creat的第三个参数stata是可选的,传了才能拿到通讯状态(0成功 /1异常回应 /2超时)。 不传就只能靠数据是否变化判断链路好坏,现场排查会很盲目。add_channel的第三个参数是数字时落 D 区(这里是400001 → D1,用plc.d(1)读); 传data对象(如v)则不占用任何寄存器。两种写法在同一个脚本里可以混用。- 本模块三种读写属性都有:
READ只读、WRITE只写(见例程 3)、READ_WRITE双向。 原脚本用的是READ_WRITE,既能把从站的值读上来,也能在本地值变化时写下去。 - 顶层那句
system.sleep(5000)是早期为了「等串口就绪」写的,它会把主线程冻住 5 秒(期间所有 定时器/回调都不跑)。一次性启动影响不大,但不要照抄到需要持续响应的场合, 区别见 并发模型概述。
验证方法:用 Modbus 仿真从站把 400001 改成 123,日志里 400001=123 跟随变化;把 400002 改成 150, 下一轮从站 400002 应被回写成 88;拔掉 RS485 后 通讯状态 变 2。
注意事项:com.setup(2, ...) 的口号要与实际接线一致;stata:get_num() 一直是 2 基本就是 波特率 / 校验位 / 从站地址不对;子设备对象 modbus1 必须是全局或长期持有,被回收后轮询会停。