主题
综合例程:采集第三方设备(三菱 FX / Modbus RTU / Modbus TCP)
让设备自己周期性去读第三方 PLC 或仪表,值自动落到本机的 D / M 或
data对象里, 梯形图、上位机、云端直接取用 —— 这是第七章四个「自动轮询」模块的通用做法。 本页把三条最常见的链路放在一起,并单独讲清一个高频误解:通道到底落在 D 还是 M (见第四节)。
一、先分清两件事
| 自动轮询(本页) | 按需读写(Pipe) | |
|---|---|---|
| 谁发起 | 固件后台线程按周期采 | 您的脚本每次调 read_xxx / write_xxx |
| 值放在哪 | 通道绑定好的 D / M / data 对象 | 函数返回值 |
| 适合 | 连续监控、要上云、要和梯形图联动 | 偶尔查一次、要拿每次通信的错误码 |
三条链路的分工是固定的:协议子设备(com_fx / com_modbus / com_dlt645)描述"读哪些点、放到哪", 串口父设备(com)负责"什么时候去读"。modbus(TCP)自己就是完整的主站,不需要 com 包装。
二、链路一:串口读三菱 FX 编程口
场景:COM1 挂一台 FX 系列 PLC(9600 / 7 数据位 / 偶校验 / 1 停止位,这是编程口的常规设置), 读它的 D0 到本机 D100,D1、D2 读进 data 对象并回写。
完整代码(可直接下载运行):
lua
-- ============================================================
-- 串口读三菱 FX 编程口
-- 子设备描述"读什么",父设备决定"多久读一次"
-- ============================================================
-- ---------- 1. 子设备:三菱编程口 ----------
local fx_state = data.creat(0) -- 通信状态对象(建议建)
local fx = com_fx.creat(fx_state) -- 第 1 参:状态回写位置
fx:add_channel("D0", com_fx.S16, 100, com_fx.READ_WRITE) -- 对方 D0 <-> 本机 D100
local d1 = data.creat(0)
local d2 = data.creat(0)
fx:add_channel("D1", com_fx.S16, d1, com_fx.READ_WRITE) -- 对方 D1 <-> d1 对象
fx:add_channel("D2", com_fx.S16, d2, com_fx.READ_WRITE) -- 对方 D2 <-> d2 对象
-- ---------- 2. 父设备:串口 ----------
local port = com.setup(1, 9600, com.EVEN, 7, 1) -- 编程口常用 7 位偶校验
if not port then
print("串口打开失败,检查端口号与波特率")
return
end
port:add_device(fx) -- 必须 add_device 之后再 start
port:set_time_lunxun(1000) -- 1 秒一轮
port:start() -- 后台线程开始自动采集
-- ---------- 3. 本机侧使用与回写 ----------
timer.start(10000, function()
print("D0=" .. plc.d(100) .. " D1=" .. d1:get_num() ..
" D2=" .. d2:get_num() .. " 状态=" .. fx_state:get_num())
if d2:get_num() > 100 then
d2:set(88) -- 对象值一变,下轮自动写到对方 D2
end
d1:set(1 + d1:get_num()) -- 累加后写回对方 D1
end)逐段要点:
add_device只认三种子设备对象:com_modbus/com_fx/com_dlt645的返回值。 传错类型会被静默忽略,不报错也不采集 —— 采不到数据时先回头查这一步。- 通道必须在
start之前加完,启动后再add_channel不保证生效。 READ_WRITE的含义是「本机值发生变化就写下去,否则读上来」,不是"一直写"。 所以同一个通道既能采集又能下发,不用分两条。- 状态对象不能省:
0通常表示成功,非 0 表示异常或超时(具体值随固件版本略有差异)。 没有它就分不清"对端确实是 0"和"根本没读到"。 - 波特率只接受
300/600/1200/2400/4800/9600/19200/38400/57600/76800/115200, 填别的值com.setup返回nil,现象和"端口不存在"一模一样。
三、链路二:串口读 Modbus RTU 仪表
场景:COM2 上一台 Modbus RTU 从站(地址 1),保持寄存器 400001 双向绑定到本机 D1, 400002 绑定到 data 对象。
完整代码(可直接下载运行):
lua
-- ============================================================
-- 串口读 Modbus RTU 仪表
-- ============================================================
-- ---------- 1. 子设备 ----------
local stata = data.creat(0) -- 通讯状态:0=成功,非 0=异常/超时
local modbus1 = com_modbus.creat(com_modbus.RTU, 1, stata) -- RTU 模式,从站地址 1
modbus1:add_channel(400001, com_modbus.S16, 1, com_modbus.READ_WRITE) -- -> 本机 D1
local v = data.creat(0)
modbus1:add_channel(400002, com_modbus.S16, v, com_modbus.READ_WRITE) -- -> 对象 v
modbus1:set_time_error(1000) -- 读写出错后的恢复时间
-- ---------- 2. 父设备 ----------
local port = com.setup(2, 9600, com.NONE, 8, 1) -- 要与从站通讯格式一致
if not port then
print("串口打开失败")
return
end
port:add_device(modbus1)
port:start()
-- ---------- 3. 取用与下发 ----------
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)逐段要点:
- 组合地址
xyyyyy从 1 开始数:400001是偏移 0;线圈第 1 点直接写1。 - ASCII 模式要改串口格式:
com_modbus.ASCII的从站是 7 位数据,com.setup第 4 参要填7,填 8 会通不了。 - 浮点、32 位数据读出来是乱码时,先查字序:国产设备多数是
CDAB, 用set_float_abcd(com_modbus.CDAB)。字序函数要和通道类型配对, 给S16通道调set_32_abcd不会有任何反应。 - 不要在同一个串口上再手写
send/recv(裸报文例程), 轮询线程和您的报文会互相插队。 - 上电瞬间串口/总线可能还没就绪,不要写
system.sleep(5000)去等 —— 脚本顶层的sleep是真阻塞,那 5 秒里所有回调都是死的(见并发模型概述)。 要延后就用一个单次定时器,或者直接不管:轮询线程自己会按set_time_error重试。
四、通道落点:位类型到 M,字类型到 D
add_channel 的 target 传数字时,落在 D 区还是 M 区由数据类型决定,不是固定 D 区:
| 通道数据类型 | 数字 target N 的含义 | 读写的是 |
|---|---|---|
BITE | 本机 M 元件号 | MN,一位 |
S16 / U16 | 本机 D 寄存器号 | DN,一个 |
S32 / U32 / FLOAT | 本机 D 寄存器号 | DN 和 DN+1,两个 |
DOUBLE | 本机 D 寄存器号 | DN 到 DN+3,四个 |
传 data 对象 | 与 D / M 无关 | 写到该对象 |
所以 add_channel(1, 100002, modbus.BITE, 21, modbus.READ) 是「读从站线圈 → M21」, 本机侧要用 plc.m(21) 取,而不是 plc.d(21)。
位和字用同一段编号会打架
把位通道写成 200、又把字通道写成 200,看起来不冲突(一个是 D200、一个是 M200), 但很容易被自己或接手的人误读。建议给位通道和字通道划开两段编号, 例如位统一用 M500 起、字统一用 D1000 起,一眼能看出类型。
五、链路三:网口读 Modbus TCP 从站
场景:设备通过网口连一台 Modbus TCP 从站(192.168.0.238:503), 读线圈 100001 / 100002 到本机 M20 / M21,另读保持寄存器 400001 到 D100。
完整代码(可直接下载运行):
lua
-- ============================================================
-- Modbus TCP 主站:一个对象一条 TCP 连接,连接上挂多个从站单元号也行
-- ============================================================
local dev = modbus.creat("192.168.0.238", 503) -- 只有 IP + 端口两个参数
if not dev then
print("建立对象失败")
return
end
-- 位通道:数字 target 落 M 区
dev:add_channel(1, 100001, modbus.BITE, 20, modbus.READ) -- 线圈 -> M20
dev:add_channel(1, 100002, modbus.BITE, 21, modbus.READ) -- 线圈 -> M21
-- 字通道:数字 target 落 D 区
dev:add_channel(1, 400001, modbus.U16, 100, modbus.READ) -- 保持寄存器 -> D100
-- 双向:本机改 D101 就会写下去
dev:add_channel(1, 400002, modbus.U16, 101, modbus.READ_WRITE)
dev:set_time_lunxun(1000) -- 轮询 1 秒
dev:set_time_recv(500) -- 单次应答等 500ms
dev:set_time_error(1000) -- 出错后 1 秒恢复
dev:start()
timer.start(2000, function()
print("M20=", plc.m(20), "M21=", plc.m(21),
"D100=", plc.d(100), "D101=", plc.d(101))
end)逐段要点:
modbus.creat只有两个参数:IP 和端口。它没有"模式"参数, 本身就是 TCP 主站(RTU 走串口,用com_modbus)。- 对象必须长期持有:用全局变量或本页这种脚本顶层的
local。 若写成函数里的局部变量,函数一返回对象就被回收,连接会莫名断掉。 - 通道要在
start之前全部加完。 close()是停止,停止后同一个对象不能再次start,要恢复通信必须重新creat。- 需要"读一次拿错误码"或按需单点读,用
pipe,不是本模块。
六、错误写法 vs 正确写法对照
下面这段在客户手上出现频率极高 —— 它是照着别的设备的协议手册写出来的, 一上来就报错,程序一行都不执行:
lua
-- 错误:凭空捏造的常量和函数名
modbusTcp1 = modbus.creat(modbus.TCP, "192.168.0.238", 503) -- 建立 MODBUS RTU 串口设备
modbusTcp1:set_channel(1, 100002, modbus.BITE, 21, modbus.READ)
modbusTcp1:set_channel(1, 100001, modbus.BITE, 20, modbus.READ)
modbusTcp1:start()
timer.start(500, function()
print(plc.m(20), plc.m(21))
end)| 问题 | 后果 | 正确做法 |
|---|---|---|
modbus.TCP 这个常量不存在 | 表达式求值为 nil,creat 第 1 参要的是字符串,直接 bad argument #1 | modbus.creat("192.168.0.238", 503),没有模式参数 |
| 传了 3 个参数,IP 被挤到第 2 位 | 端口当成 IP 用、真正的端口丢失 | 参数个数固定是两个:IP、端口 |
set_channel 这个函数不存在 | attempt to call field 'set_channel' (a nil value) | 叫 add_channel |
注释写"RTU 串口设备"、代码用 modbus | 后面有人照着注释改,改成串口参数就通不了 | RTU 用 com_modbus + com,TCP 用 modbus |
把 100002 读到 M21,注释也这么写 | 这一步其实是对的:位通道的数字 target 确实落 M 区 | 保持 add_channel(1, 100002, modbus.BITE, 21, modbus.READ) + plc.m(21) |
修正后的完整可运行版就是第五节那段。 值得留意:原作者对"落到 M21"的理解是正确的,错的是函数名和参数 —— 这类"业务想法对、API 名字凭印象编"的写法是脚本失败的第一大来源, 写完务必对着 API 速查表 逐个核一遍函数名。
七、注意事项汇总
add_device的参数类型不对时静默忽略,既不报错也不采:采不到数据先查这条。- 通道加在
start之后不保证生效,一律先加完通道再启动。 - 字序设置和通道类型不配对不会报错,只会"设置了没反应"。
- 32 位以上数据类型要占多个 D,相邻通道地址重叠时后者会被覆盖。
- 同一从站加重复通道会被拒绝:同一个从站、同一区、同一地址只能有一条。
- 不要拿
status当"数据是不是 0"用:它只说明通信成不成功,成功时数据完全可能就是 0。 - 不要为了等待硬件就绪在脚本顶层写
while ... do system.sleep(...) end: 这是阻塞主循环,会冻死所有回调。真要等,用定时器反复检查,见 并发模型概述。 - 从站是别人的 PLC 时,改
READ_WRITE通道等于直接改对方寄存器, 上线前务必确认地址表,避免误写运行中的设备。