Skip to content

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(...) 等价。

Modbus RTU 自动轮询:一条 RS485 总线挂多个从站,按间隔自动遍历所有通道

函数总览 ​

库级函数 ​

函数作用
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.RTU0Modbus RTU(二进制帧,常用)
com_modbus.ASCII1Modbus ASCII

读写属性(add_channel 最后一个参数)

常量值说明
com_modbus.READ0只读:从站 → 本地
com_modbus.READ_WRITE1读写:本地值变化时写下去,否则读上来
com_modbus.WRITE2只写:本地 → 从站

数据类型

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

字节序

用于常量(值)
set_16_abcom_modbus.AB (0)、com_modbus.BA (1)
set_32_abcd / set_float_abcdcom_modbus.ABCD (0)、com_modbus.CDAB (1)、com_modbus.BADC (2)、com_modbus.DCBA (3)
set_double_abcdefghcom_modbus.ABCDEFGH (0)、com_modbus.GHEFCDAB (1)、com_modbus.BADCFEHG (2)、com_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 区号(用于 5 参形式的 add_channel,也是组合地址的首位)

区号值含义
DO0线圈(可读写位)
DI1离散输入(只读位)
AI3输入寄存器(只读字)
VW4保持寄存器(可读写字)

com_modbus.creat ​

lua
com_modbus.creat(mode, addr, [status]) --> device

创建一个 Modbus 从站对象。

参数

#参数类型必填默认值说明
1modeinteger是—com_modbus.RTU 或 com_modbus.ASCII
2addrinteger是—从站地址,取低 8 位
3statusinteger | data否—通信状态回写位置。传数字 = D 寄存器编号,传 data 对象 = 写到该对象。省略则不回写状态

返回值

类型说明
userdataModbus 从站对象。必须用变量接住,否则可能被 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

给从站增加一条通道。两种写法等价,选一种用。

参数(组合地址形式)

#参数类型必填默认值说明
1modbus_addrinteger是—6 位组合地址,形如 400001:首位是区号(0/1/3/4),后 5 位是从 1 开始的点号。内部换算为 区号 = addr/100000、偏移 = addr%100000 - 1
2data_typeinteger是—com_modbus.S16 等数据类型常量
3targetinteger | data是—值的落点编号。落点由数据类型决定:BITE 写进 M 区,字类型(S16/U16/S32/U32/FLOAT/DOUBLE)写进 D 区;传 data 对象则与 D/M 无关
4rwinteger是—com_modbus.READ / READ_WRITE / WRITE

参数(分离形式)

#参数类型必填默认值说明
1areainteger是—区号:0=DO、1=DI、3=AI、4=VW
2offsetinteger是—区内偏移,从 0 开始
3data_typeinteger是—数据类型常量
4reginteger是—落点编号,只接受数字(此形式不接受 data 对象)。BITE 时是 M 编号,字类型时是 D 编号
5rwinteger是—读写属性

返回值

类型说明
integer1 添加成功,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 位整数的字节序。

参数

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

参数

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

参数

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

参数

#参数类型必填默认值说明
1orderinteger是com_modbus.GHEFCDABABCDEFGH / GHEFCDAB / BADCFEHG / HGFEDCBA

返回值

无返回值。

说明

  • 只对 DOUBLE 通道生效。DOUBLE 占用连续 4 个寄存器。

dev:set_time_recv ​

lua
dev:set_time_recv(ms)

设置本从站的应答等待超时。

参数

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

返回值

无返回值。

说明

  • 这是每个从站独立的超时,比 com.set_timeout 更精细。慢速仪表可以单独加大,不影响同总线上的其它设备。

dev:set_time_error ​

lua
dev:set_time_error(ms)

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

参数

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

返回值

无返回值。

说明

  • 设大一点可以避免某台掉线设备反复超时、拖慢整条总线的轮询节奏。

常见用法 ​

一条 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 才会轮询。

验证方法:

  1. 下载重启,D100-D101 出现温度值,st:get_num() = 0(通讯成功)。
  2. 拔掉表电源,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 是每从站独立的超时,慢表加大不影响同总线其它设备。

验证方法:

  1. D100-D101 出温度,D110-D111 出电量;两台 st 都为 1。
  2. 把 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。

验证方法:

  1. 把 D300 改成 50(变频器频率给定),用 Modbus 主站工具读 3 号从站 40001,应同步变成 50。
  2. 改 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 必须是全局或长期持有,被回收后轮询会停。

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