主题
modbus — Modbus TCP 主站(自动轮询)
模块名:modbus(别名 com_modbus 指向串口版,见 6.2)
以 Modbus TCP 主站身份连一台从站,把一批寄存器按固定周期自动轮询到 PLC 的 D 寄存器或 data 对象里。配好通道调用 start 之后就有独立线程在后台跑,脚本不用管收发。
和 pipe.setup("modbus.tcp", ...) 的区别:
- 本模块:先声明通道,
start后后台自动轮询,值落到 D 区。适合「一直采一批点」。 - pipe:每次调用
read_u16之类的方法才发一帧,同步等回复。适合「按需读一下」或写控制命令。
除 creat 之外的函数在库表和对象元表里都注册了,modbus.start(dev) 和 dev:start() 等价。
函数总览
库级函数
| 函数 | 作用 |
|---|---|
modbus.creat(ip, port) | 创建一个 Modbus TCP 主站对象 |
对象方法
| 方法 | 作用 |
|---|---|
dev:add_channel | 增加一条采集/写入通道 |
dev:start | 启动连接与轮询线程 |
dev:close | 停止轮询并断开(旧固件有坑,见说明) |
dev:set_time_lunxun | 设置轮询间隔 |
dev:set_time_recv | 设置应答超时 |
dev:set_time_relink | 设置断线重连间隔 |
dev:set_time_error | 设置出错后的重试延时 |
dev:set_16_ab | 设置 16 位整数字节序 |
dev:set_32_abcd | 设置 32 位整数字节序 |
dev:set_float_abcd | 设置单精度浮点字节序 |
dev:set_double_abcdefgh | 设置双精度浮点字节序 |
常量
读写属性(add_channel 最后一个参数)
| 常量 | 值 | 说明 |
|---|---|---|
modbus.READ | 0 | 只读:从站 → 本地 |
modbus.READ_WRITE | 1 | 读写:本地值变化时写下去,否则读上来 |
本模块没有
WRITE常量。要往从站写,用READ_WRITE。
数据类型
| 常量 | 值 | 占用寄存器 | 说明 |
|---|---|---|---|
modbus.BITE | 0 | — | 位(线圈 / 离散输入) |
modbus.S16 | 1 | 1 | 16 位有符号 |
modbus.U16 | 2 | 1 | 16 位无符号 |
modbus.S32 | 3 | 2 | 32 位有符号 |
modbus.U32 | 4 | 2 | 32 位无符号 |
modbus.FLOAT | 5 | 2 | 单精度浮点 |
modbus.DOUBLE | 6 | 4 | 双精度浮点 |
字节序
| 用于 | 常量(值) |
|---|---|
set_16_ab | modbus.AB (0)、modbus.BA (1) |
set_32_abcd / set_float_abcd | modbus.ABCD (0)、modbus.CDAB (1)、modbus.BADC (2)、modbus.DCBA (3) |
set_double_abcdefgh | modbus.ABCDEFGH (0)、modbus.GHEFCDAB (1)、modbus.BADCFEHG (2)、modbus.HGFEDCBA (3) |
字节序设置与通道数据类型必须配对
四个设置函数各自保存独立的字节序,只在解析对应数据类型的通道时被读取,互不影响:
设置函数 生效的数据类型 默认值 set_16_abS16、U16ABset_32_abcdS32、U32CDABset_float_abcdFLOATCDABset_double_abcdefghDOUBLEGHEFCDAB通道声明成
S16却去调set_32_abcd,是最常见的「设置了没反应」原因 —— S16 通道只会读set_16_ab的设置。 只需设置实际用到的类型那一项;设置不匹配的类型不会报错,也不会生效。
Modbus 区号(组合地址的首位)
| 区号 | 值 | 含义 | 功能码 |
|---|---|---|---|
| DO | 0 | 线圈(可读写位) | 01 / 05 / 0F |
| DI | 1 | 离散输入(只读位) | 02 |
| AI | 3 | 输入寄存器(只读字) | 04 |
| VW | 4 | 保持寄存器(可读写字) | 03 / 06 / 10 |
modbus.creat
lua
modbus.creat(ip, port) --> device创建一个 Modbus TCP 主站对象。两个参数都必填,不填会报参数错误。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | ip | string | 是 | — | 从站(服务器)IP |
| 2 | port | integer | 是 | — | 端口,标准 Modbus TCP 是 502 |
返回值
| 类型 | 说明 |
|---|---|
| userdata | 主站对象。必须用全局或模块级变量接住,被 GC 回收会断开连接 |
示例
lua
dev = modbus.creat("192.168.1.100", 502)说明
- 这里只记下地址,真正建连接是在
start里。
实用例程
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
dev:add_channel(slave, modbus_addr, data_type, target, rw) --> 0 | 1增加一条通道。可以给同一个对象加不同 slave 的通道,一条 TCP 连接上访问多个单元号。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | slave | integer | 是 | — | 从站单元号,取低 8 位 |
| 2 | modbus_addr | integer | 是 | — | 6 位组合地址,形如 400001:首位是区号(0/1/3/4),后 5 位是从 1 开始的点号 |
| 3 | data_type | integer | 是 | — | modbus.S16 等数据类型常量 |
| 4 | target | integer | data | 是 | — | 值的落点编号。落点由数据类型决定:BITE 写进 M 区,字类型(S16/U16/S32/U32/FLOAT/DOUBLE)写进 D 区;传 data 对象则与 D/M 无关 |
| 5 | rw | integer | 是 | — | modbus.READ 或 modbus.READ_WRITE |
返回值
| 类型 | 说明 |
|---|---|
| integer | 1 添加成功,0 添加失败 |
示例
lua
-- 保持寄存器 40001-40002 的浮点 → D100
dev:add_channel(1, 400001, modbus.FLOAT, 100, modbus.READ)
-- 输入寄存器 30010 的 16 位有符号 → D110
dev:add_channel(1, 300010, modbus.S16, 110, modbus.READ)
-- 线圈 00001 → data 对象,双向
run = data.creat(0)
dev:add_channel(1, 1, modbus.BITE, run, modbus.READ_WRITE)说明
- 组合地址从 1 开始:
400001对应寄存器偏移0(内部换算区号 = addr/100000、偏移 = addr%100000 - 1)。线圈 00001 写成1即可。 BITE只能配 0 区或 1 区;字类型配 3 区或 4 区。- 本地落点跟着数据类型走:
BITE通道填200落的是M200(用plc.m(200)读写), 字类型填200落的才是D200。这是现场最容易错的一处:脚本里plc.d(200)永远读不到一条位通道。 - 添加的通道会被内部排序并合并成连续块,地址相邻的点会打包进同一帧,不用自己凑。
dev:start
lua
dev:start() --> 0 | 1建立 TCP 连接并启动后台轮询线程。所有通道和参数都配好之后调用一次。
参数
无参数(冒号语法自动传入对象)。
返回值
| 类型 | 说明 |
|---|---|
| integer | 1 轮询线程已启动,0 启动失败 |
示例
lua
if dev:start() == 1 then print("轮询已启动") end说明
- 返回
1只代表线程起来了,不代表连上了从站。连不上时线程会在后台按set_time_relink的间隔反复重连,判断通信是否正常要看值有没有更新或status落点。 - 一条通道都没加时返回
0且不启动线程,不会有任何报错。先add_channel再start。 - 只调一次。实现里没有"已启动"标志,对同一对象重复
start会再起一条轮询线程,同一批通道被两个线程反复读写。 - 通道必须在这之前加完,
start之后add_channel不会进入本轮采集计划。
dev:close
lua
dev:close() --> 0 | 1停止后台轮询线程。
参数
无参数(冒号语法自动传入对象)。
返回值
| 类型 | 说明 |
|---|---|
| integer | 恒为 1(内部 stop() 流程总会返回成功;即使从未 start 过也返回 1) |
示例
lua
dev:close() -- 停止轮询线程
dev = nil -- 之后交给 GC 释放对象说明
- 会阻塞:
close要等后台轮询线程真正退出才返回,最长约一个「重连间隔 + 轮询间隔」的时长(秒级),别在时间敏感的回调里调它。 - 当前实现在
close时不主动关闭底层 socket,只停线程;TCP 连接本身留给对端或协议栈自然断开。要彻底回收对象,让变量置nil后等 GC。 close之后同一个对象不能再用start拉起来(内部启动标志被清掉,start返回0)。要恢复通信必须重新modbus.creat建新对象、重加通道。- 本函数的实现直到 2026-08-18 才修复——在那之前编译的旧固件里
close被错写成调用start,调用它不仅不会停止轮询、反而会再启动一次。若手上设备表现是「close 越关越忙」,就是固件太旧,联系升级。
dev:set_time_lunxun
lua
dev:set_time_lunxun(ms)设置轮询间隔,即一轮通道跑完后到下一轮开始的等待时间。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | ms | integer | 是 | — | 间隔毫秒数 |
返回值
无返回值。
说明
- 设太小会占满网络和 CPU,一般 200–1000ms 足够。
dev:set_time_recv
lua
dev:set_time_recv(ms)设置发出请求后等应答的超时。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | ms | integer | 是 | — | 超时毫秒数 |
返回值
无返回值。
dev:set_time_relink
lua
dev:set_time_relink(ms)设置 TCP 断开后重连的间隔。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | ms | integer | 是 | — | 间隔毫秒数 |
返回值
无返回值。
说明
- 设太小时对方掉电会导致密集重连;建议 3000–10000ms。
dev:set_time_error
lua
dev:set_time_error(ms)设置通信出错后,下一次重试前的延时。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | ms | integer | 是 | — | 延时毫秒数 |
返回值
无返回值。
dev:set_16_ab
lua
dev:set_16_ab(order)设置 16 位整数的字节序。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | order | integer | 是 | — | modbus.AB 或 modbus.BA |
返回值
无返回值。
dev:set_32_abcd
lua
dev:set_32_abcd(order)设置 32 位整数(S32/U32)的寄存器与字节顺序。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | order | integer | 是 | modbus.CDAB | ABCD / CDAB / BADC / DCBA |
返回值
无返回值。
说明
- 32 位数值读出来离谱,先试
CDAB。 - 只对
S32/U32通道生效,对FLOAT不生效(浮点用set_float_abcd)。 - 「设置了没反应」的排查:先确认
add_channel的数据类型确实是S32或U32;若通道是S16/U16,应改用set_16_ab。注意S32占用连续 2 个寄存器。
dev:set_float_abcd
lua
dev:set_float_abcd(order)设置单精度浮点(FLOAT 通道)的寄存器与字节顺序。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | order | integer | 是 | modbus.CDAB(不设置时的默认) | modbus.ABCD / modbus.CDAB / modbus.BADC / modbus.DCBA |
返回值
无返回值。
示例
lua
dev = modbus.creat("192.168.1.100", 502)
dev:set_float_abcd(modbus.ABCD) -- 该从站的浮点是高字在前
dev:add_channel(1, 400001, modbus.FLOAT, 100, modbus.READ)
dev:start()说明
- 只对
FLOAT通道生效,与set_32_abcd(管S32/U32)是两个互不相干的设置;同一条连接上两种通道都有时,两个函数都要调。 - 字节序是在每帧解析时现读的,
start之后调用也有效,下一轮解析即用新顺序。 - 传四个常量以外的数值不会报错,但解析时四个分支都匹配不上,
FLOAT通道从此不再被更新——数值会停在最后一次解码的结果上。「浮点值突然不动了」先检查这里。
dev:set_double_abcdefgh
lua
dev:set_double_abcdefgh(order)设置双精度浮点的顺序。
参数
| # | 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|---|
| 1 | order | integer | 是 | — | ABCDEFGH / GHEFCDAB / BADCFEHG / HGFEDCBA |
返回值
无返回值。
常见用法
采一台仪表的 10 个点到 D 区,交给梯形图和组态用
lua
dev = modbus.creat("192.168.1.100", 502)
dev:set_32_abcd(modbus.CDAB)
dev:set_float_abcd(modbus.CDAB)
dev:set_time_lunxun(500)
dev:set_time_recv(1000)
dev:set_time_relink(5000)
-- 保持寄存器 40001 起 8 个 U16 → D100~D107
for i = 0, 7 do
dev:add_channel(1, 400001 + i, modbus.U16, 100 + i, modbus.READ)
end
-- 40021-40022 的浮点温度 → D120
dev:add_channel(1, 400021, modbus.FLOAT, 120, modbus.READ)
-- 线圈 00001 双向:梯形图置位 M200 就写下去
dev:add_channel(1, 1, modbus.BITE, 200, modbus.READ_WRITE)
dev:start()
timer.start(2000, function()
print("D100 =", plc.d(100), "温度 =", plc.df(120))
end)一条连接访问多个单元号
lua
dev = modbus.creat("192.168.1.200", 502)
dev:add_channel(1, 400001, modbus.U16, 100, modbus.READ) -- 单元号 1
dev:add_channel(2, 400001, modbus.U16, 101, modbus.READ) -- 单元号 2
dev:add_channel(3, 400001, modbus.U16, 102, modbus.READ) -- 单元号 3
dev:start()注意事项
- 对象必须长期持有。局部变量出作用域被 GC 回收后连接就断了,用全局变量。
close只可用一次:它会真正停止轮询线程(2026-08-18 起;旧固件行为相反,见方法说明),且关闭后同一对象无法再start,恢复通信要重新creat。- 通道要在
start之前加完。启动后才add_channel不保证生效。 READ_WRITE是「值变了就写」:本地 D 寄存器数值发生变化时写到从站,否则读上来。想纯粹下发命令也用它,本模块没有只写模式。- 32 位和浮点数据乱码时应先检查字节序,多数国产设备是
CDAB。 - 字序设置函数要和通道数据类型配对:
set_16_ab→S16/U16、set_32_abcd→S32/U32、set_float_abcd→FLOAT、set_double_abcdefgh→DOUBLE。给S16通道调set_32_abcd不会生效,是「设置了没反应」的头号原因。 - 需要按需单次读取、或需要获取每次通信的错误码时,应使用
pipe而非本模块。
例程
每个例程都是可直接下载运行的完整脚本:
creat建对象、加通道、start启动后台轮询,脚本注册完即结束。 本模块add_channel是 5 个参数(含从站单元号),与串口版com_modbus(4 参)不同。统一模板「场景 / 完整代码 / 要点 / 验证方法 / 注意事项」。
例程 1:采一台仪表的多个点到 D 区
适用:LP3201 / LP3202 / LP3203 / LP3222 / LP3232 / LP3288 / 软PLC模拟器 / 上位机 | 依赖模块:modbus timer plc | 难度:★☆☆
场景:一台 Modbus TCP 仪表(IP 192.168.1.100,端口 502),把 8 个 U16 + 1 个浮点温度自动轮询到 D 区,交给梯形图/组态用。
完整代码(可直接下载运行):
lua
dev = modbus.creat("192.168.1.100", 502) -- 两参必填
dev:set_32_abcd(modbus.CDAB)
dev:set_float_abcd(modbus.CDAB)
dev:set_time_lunxun(500)
dev:set_time_recv(1000)
dev:set_time_relink(5000)
-- 保持寄存器 40001 起 8 个 U16 → D100~D107
for i = 0, 7 do
dev:add_channel(1, 400001 + i, modbus.U16, 100 + i, modbus.READ)
end
-- 40021-40022 浮点温度 → D120
dev:add_channel(1, 400021, modbus.FLOAT, 120, modbus.READ)
dev:start() -- 配完通道后只调一次
timer.start(2000, function()
print("D100 =", plc.d(100), "温度 =", plc.df(120))
end)要点:
modbus.creat(ip, port)两个参数都必填。add_channel是 5 参:(slave, modbus_addr, data_type, target, rw)——比串口版多了从站单元号slave。- 先
add_channel再加完、最后start()一次;启动后加通道不生效。
验证方法:
- 下载重启,D100~D107、D120 出现仪表数据。
- 拔掉仪表网线,线程按
set_time_relink重连;恢复后自动继续。
注意事项:
- 组合地址 从 1 开始:
400001= 偏移 0;线圈00001直接写1即可。 - 字节序要和通道数据类型配对:
set_32_abcd→S32/U32,set_float_abcd→FLOAT;给S16调set_32_abcd不会生效(「设置了没反应」头号原因)。 start()只调一次;重复调用会再起一条轮询线程,同一批通道被两个线程反复读写。
例程 2:一条 TCP 连接访问多个单元号
适用:同上 | 依赖模块:modbus | 难度:★☆☆
场景:同一条 TCP 连上三台不同单元号的从站,各自采一个值到不同 D 区。
完整代码(可直接下载运行):
lua
dev = modbus.creat("192.168.1.200", 502)
dev:add_channel(1, 400001, modbus.U16, 100, modbus.READ) -- 单元号 1 → D100
dev:add_channel(2, 400001, modbus.U16, 101, modbus.READ) -- 单元号 2 → D101
dev:add_channel(3, 400001, modbus.U16, 102, modbus.READ) -- 单元号 3 → D102
dev:start()要点:
- 同一对象上给不同
slave加通道,轮询线程会在一个连接上依次访问多台从站。 slave取低 8 位。
验证方法:改各从站的 40001,对应 D100/D101/D102 同步变化。
注意事项:slave 超出 8 位范围会被截断;地址越界时行为按固件实现(多为静默失败),先核对从站实际寄存器范围。
例程 3:把本机结果写下去(READ_WRITE 下发控制)
适用:同上 | 依赖模块:modbus plc | 难度:★★☆
场景:本机算好的频率给定放 D300,自动下发到 1 号从站保持寄存器 40050。
完整代码(可直接下载运行):
lua
-- 本机 D300 = 频率给定,自动下发到 1 号从站 40050(双向同步)
dev = modbus.creat("192.168.1.100", 502)
dev:add_channel(1, 400050, modbus.U16, 300, modbus.READ_WRITE) -- 双向
dev:start()
-- D300 变 → 自动写下去;从站改 40050 → D300 同步要点:
- 本模块没有
WRITE常量,要下发用READ_WRITE(值变了才写下去,否则读上来)。 - 脚本只需配置一次,下发由轮询线程自动完成。
验证方法:把 D300 改成 50,用 Modbus 主站工具读从站 40050 应同步成 50;反方向改 40050,D300 也变。
注意事项:纯下发命令也得用 READ_WRITE;start() 只调一次。
例程 4:读线圈到 M 区(现场脚本 LP.LPLUA)
适用:同上 | 依赖模块:modbus timer plc | 难度:★☆☆
场景:从 Modbus TCP 从站读线圈 100001 / 100002,落到本机 M20 / M21,每 500ms 打印一次。 这是现场流传很广的一份脚本(文件 LP.LPLUA),但它用的是早期版本的接口,在当前固件上直接 运行会报错,所以原文与当前写法一起给出,手上是这份文件的改两行就能跑。
现场脚本原文(旧接口,当前固件跑不通):
lua
modbusTcp1=modbus.creat(modbus.TCP,"192.168.0.238",503) --建立 MODBUS RTU 串口设备
modbusTcp1:set_channel(1,100002,modbus.BITE,21,modbus.READ)-- -- 加 入 通 道 读 100002 到 m21
modbusTcp1:set_channel(1,100001,modbus.BITE,20,modbus.READ)-- -- 加 入 通 道 读 100001 到 m20
modbusTcp1:start() --启动轮询
timer.start(500,function()
print(plc.m(20),plc.m(21))
end)当前固件的等价写法(可直接下载运行):
lua
modbusTcp1 = modbus.creat("192.168.0.238", 503) -- 只有 ip + 端口,没有 modbus.TCP 常量
modbusTcp1:add_channel(1, 100002, modbus.BITE, 21, modbus.READ) -- set_channel 已改名为 add_channel
modbusTcp1:add_channel(1, 100001, modbus.BITE, 20, modbus.READ)
modbusTcp1:start()
timer.start(500, function()
print(plc.m(20), plc.m(21)) -- 位通道落 M 区,用 plc.m() 取
end)要点:
- 旧脚本要改的就两处:
modbus.creat去掉第一个参数modbus.TCP(本模块只有 TCP 一种, 固件里没有这个常量);set_channel改成add_channel。 503不是标准端口(标准是502),按从站实际配置填。modbus.BITE是位类型,第 4 个参数20/21落的是M20/M21,原脚本用plc.m()读是对的。
验证方法:用从站仿真工具把 100001 置 1,日志里第二个值变 1;在上位机看 M20 同步变 1。
注意事项:把这条通道当成 D 区去 plc.d(20) 读,永远得 0;沿用旧接口的 set_channel 或 modbus.TCP 会报 attempt to call a nil value(是“方法不存在”,不是连不上),看到这两个报错就是脚本版本对不上。