Skip to content

modbus — Modbus TCP 主站(自动轮询) ​

模块名:modbus(别名 com_modbus 指向串口版,见 6.2)

以 Modbus TCP 主站身份连一台从站,把一批寄存器按固定周期自动轮询到 PLC 的 D 寄存器或 data 对象里。配好通道调用 start 之后就有独立线程在后台跑,脚本不用管收发。

和 pipe.setup("modbus.tcp", ...) 的区别:

  • 本模块:先声明通道,start 后后台自动轮询,值落到 D 区。适合「一直采一批点」。
  • pipe:每次调用 read_u16 之类的方法才发一帧,同步等回复。适合「按需读一下」或写控制命令。

modbus 模块的后台自动轮询与 pipe 的按需读取,怎么选

除 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.READ0只读:从站 → 本地
modbus.READ_WRITE1读写:本地值变化时写下去,否则读上来

本模块没有 WRITE 常量。要往从站写,用 READ_WRITE。

数据类型

常量值占用寄存器说明
modbus.BITE0—位(线圈 / 离散输入)
modbus.S161116 位有符号
modbus.U162116 位无符号
modbus.S323232 位有符号
modbus.U324232 位无符号
modbus.FLOAT52单精度浮点
modbus.DOUBLE64双精度浮点

字节序

用于常量(值)
set_16_abmodbus.AB (0)、modbus.BA (1)
set_32_abcd / set_float_abcdmodbus.ABCD (0)、modbus.CDAB (1)、modbus.BADC (2)、modbus.DCBA (3)
set_double_abcdefghmodbus.ABCDEFGH (0)、modbus.GHEFCDAB (1)、modbus.BADCFEHG (2)、modbus.HGFEDCBA (3)

字节序设置与通道数据类型必须配对

四个设置函数各自保存独立的字节序,只在解析对应数据类型的通道时被读取,互不影响:

设置函数生效的数据类型默认值
set_16_abS16、U16AB
set_32_abcdS32、U32CDAB
set_float_abcdFLOATCDAB
set_double_abcdefghDOUBLEGHEFCDAB

通道声明成 S16 却去调 set_32_abcd,是最常见的「设置了没反应」原因 —— S16 通道只会读 set_16_ab 的设置。 只需设置实际用到的类型那一项;设置不匹配的类型不会报错,也不会生效。

Modbus 区号(组合地址的首位)

区号值含义功能码
DO0线圈(可读写位)01 / 05 / 0F
DI1离散输入(只读位)02
AI3输入寄存器(只读字)04
VW4保持寄存器(可读写字)03 / 06 / 10

modbus.creat ​

lua
modbus.creat(ip, port) --> device

创建一个 Modbus TCP 主站对象。两个参数都必填,不填会报参数错误。

参数

#参数类型必填默认值说明
1ipstring是—从站(服务器)IP
2portinteger是—端口,标准 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 连接上访问多个单元号。

参数

#参数类型必填默认值说明
1slaveinteger是—从站单元号,取低 8 位
2modbus_addrinteger是—6 位组合地址,形如 400001:首位是区号(0/1/3/4),后 5 位是从 1 开始的点号
3data_typeinteger是—modbus.S16 等数据类型常量
4targetinteger | data是—值的落点编号。落点由数据类型决定:BITE 写进 M 区,字类型(S16/U16/S32/U32/FLOAT/DOUBLE)写进 D 区;传 data 对象则与 D/M 无关
5rwinteger是—modbus.READ 或 modbus.READ_WRITE

返回值

类型说明
integer1 添加成功,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 连接并启动后台轮询线程。所有通道和参数都配好之后调用一次。

参数

无参数(冒号语法自动传入对象)。

返回值

类型说明
integer1 轮询线程已启动,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)

设置轮询间隔,即一轮通道跑完后到下一轮开始的等待时间。

参数

#参数类型必填默认值说明
1msinteger是—间隔毫秒数

返回值

无返回值。

说明

  • 设太小会占满网络和 CPU,一般 200–1000ms 足够。

dev:set_time_recv ​

lua
dev:set_time_recv(ms)

设置发出请求后等应答的超时。

参数

#参数类型必填默认值说明
1msinteger是—超时毫秒数

返回值

无返回值。


lua
dev:set_time_relink(ms)

设置 TCP 断开后重连的间隔。

参数

#参数类型必填默认值说明
1msinteger是—间隔毫秒数

返回值

无返回值。

说明

  • 设太小时对方掉电会导致密集重连;建议 3000–10000ms。

dev:set_time_error ​

lua
dev:set_time_error(ms)

设置通信出错后,下一次重试前的延时。

参数

#参数类型必填默认值说明
1msinteger是—延时毫秒数

返回值

无返回值。


dev:set_16_ab ​

lua
dev:set_16_ab(order)

设置 16 位整数的字节序。

参数

#参数类型必填默认值说明
1orderinteger是—modbus.AB 或 modbus.BA

返回值

无返回值。


dev:set_32_abcd ​

lua
dev:set_32_abcd(order)

设置 32 位整数(S32/U32)的寄存器与字节顺序。

参数

#参数类型必填默认值说明
1orderinteger是modbus.CDABABCD / 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 通道)的寄存器与字节顺序。

参数

#参数类型必填默认值说明
1orderinteger是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)

设置双精度浮点的顺序。

参数

#参数类型必填默认值说明
1orderinteger是—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() 一次;启动后加通道不生效。

验证方法:

  1. 下载重启,D100~D107、D120 出现仪表数据。
  2. 拔掉仪表网线,线程按 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(是“方法不存在”,不是连不上),看到这两个报错就是脚本版本对不上。

LPPLC 工业物联网云平台 · 软硬件与云平台全栈自研