VisualHMI - Modbus一主多从
本章节将介绍主从模式下(Modbus/FX2N/FX3U),读写PLC/驱动器的寄存器
以Modbus 为例,项目创建两个从站,第1个从站的站号为10,第2个从站的站号为20,如下所示:

- Lua读PLC/驱动器的寄存器
- Lua写PLC/驱动器的寄存器
- Lua批量写寄存器
适用范围:VisualHMI - HMI&M&Dx系列
例程下载链接:ViusalHMI - 一主多从(点击下载)
1. Lua API说明
1.1.start_read(index,vtype, addr,quantity,cycle,clcye_run,mode)
主机模式下周期性自动读取寄存器的通信控制函数,start_read 是 HMI 在 主机模式(如 Modbus Master、FX2N 主站)下的后台自动轮询机制,用于周期性向从机设备发起读取请求。
📊 参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
index |
number | ✅ | 任务索引号:(0~127),用于后续 stop_read(index) 停止该任务 |
vtype |
number | ✅ | 数据类型:如 VT_3x, VT_4x |
addr |
number | ✅ | 寄存器地址 |
quantity |
number | ✅ | 读取数量:1 ~ 120 个寄存器 |
cycle |
number | ❌ | 选填,轮询周期倍数(默认 0 → 每周期都读) |
cycle_run |
number | ❌ | 选填,在周期内的第几次执行(0-based,需 < cycle) |
mode |
number | ❌ | 选填,读取模式: 0 = 持续周期读取(默认) 1 = 仅读一次(需先调用 create_resp_que()) |
⚠️ 注意:
- 该函数无返回值;
- 实际数据需通过
get_uint16、get_float等函数从本地缓存读取; - 仅在主机模式下有效(如 Modbus RTU/FX3U)。
💡 示例
以Modbus RTU Master模式为例
1.1.1自动后台轮询
- 调用
start_read后,HMI 系统将该读取任务加入通信调度队列; - 每个通信周期,系统自动发送 Modbus 请求;
- 返回数据自动写入 HMI 内存镜像区;
- 脚本通过
get_xx(VT_4x, addr)直接读缓存,无通信延迟。
1.1.2.cycle 与 cycle_run 的调度逻辑
HMI 将通信周期划分为多个“子周期”,用于区分高/低频任务:
| 调用示例 | 行为说明 |
|---|---|
start_read(0, VT_4x, 0x1000, 10) |
每个通信周期都读 4x1000~1009 |
start_read(1, VT_4x, 0x2000, 10, 3, 2) |
每 3 个周期读一次,且在第 3 个周期(即 cycle_run=2,0-based)执行 |
🔄 调度时序示例:
1周期 #0 (0ms) → 读 0x1000(高频)
2周期 #1 (50ms) → 读 0x1000
3周期 #2 (100ms) → 读 0x1000 + 读 0x2000(低频任务触发)
4周期 #3 (150ms) → 读 0x1000
5周期 #4 (200ms) → 读 0x1000
6周期 #5 (250ms) → 读 0x1000 + 读 0x2000
7...
💡 设计目的:
- 高频变量(如速度、温度)→
cycle=0(每周期读)- 低频变量(如累计量、状态字)→
cycle=5, cycle_run=4(每 5 周期读一次)→ 降低总线负载,提升系统实时性
1.2.stop_read(index)
停止后台周期性读取任务,stop_read(index) 是 HMI 系统在主机模式(如 Modbus Master、FX2N 主站)下提供的通信任务管理函数,用于停止由 start_read 启动的后台周期性读取任务。调用后,HMI 将立即从通信调度队列中移除该任务,不再向从机设备发送对应的读取指令,但本地缓存中的数据保持不变(保留最后一次成功读取的值)。
📊 参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
index |
number | 任务索引号(0 ~ 127):必须与 start_read 中注册的 index 一致 |
✅ 核心用途:动态控制通信负载、释放无效轮询、实现按需读取策略。
1.3.stop_all_read()
停止所有脚本启动的后台读取任务,stop_all_read() 是 HMI 系统在主机模式(如 Modbus RTU/TCP Master、FX2N 主站等)下提供的全局通信任务停止函数,用于一次性停止所有由 start_read 启动的后台周期性读取任务
✅ 核心用途:
- 快速释放所有脚本注册的通信资源;
- 适用于系统复位、模式切换、紧急停机等场景;
- 避免逐个调用
stop_read(index)的繁琐操作。
1.4.set_auto_read(en)
控制“画面绑定变量”自动读取的全局开关,set_auto_read(en) 是 HMI 系统在主机模式(如 Modbus Master、FX2N 主站)下提供的全局通信策略控制函数,用于启用或禁用画面控件所绑定寄存器的自动轮询机制。
📊 参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
en |
number | 使能标志: 1 = 启用画面绑定变量的自动读取(默认) 0 = 禁止自动读取,仅依赖 start_read 等脚本控制 |
✅ 核心用途:
- 精细控制通信行为,将“画面自动读取”与“脚本主动读取”解耦;
- 在高性能或低带宽场景下,完全由脚本通过
start_read接管数据更新; - 避免系统自动生成冗余或冲突的读取指令。
1.5.select_slave(slave_id)
HMI 在多从机通信模式(如 Modbus RTU、FX2N 等总线协议)下,通过select_slave,临时指定从站索引,使得后续的 get_xxx() / set_xxx() 操作时,访问指定从机寄存器。
✅ 核心作用:切换指定从站,访问对应从站寄存器。
📊 参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
slave_id |
number | 从机索引 0 起始,对应工程中配置的从机列表顺序,非 Modbus 站号 |

| 术语 | 含义 | 示例 |
|---|---|---|
| 从机索引 (slave_id) | HMI 内部从机列表的数组下标(0, 1, 2...) | 0 = 第一个从机 |
| Modbus 站号 (Slave Address) | 从机设备的物理通信地址 | 10, 33 |
💡 映射关系由 HMI 工程配置决定:
slave_id = 0→ 站号 = 10slave_id = 1→ 站号 = 33
2.Lua 读寄存器
2.1.Lua 脚本
Lua脚本读取从机寄存器值,通常用于条件判断、计算和等。步骤如下所示
- 在脚本获取该寄存器时,确保后台有轮询请求,保证获取的值是最新的
- 获取寄存器前,要需要判断从机是否在线
- 获取寄存器前,需选择对应的从机
--读取保存寄存器
function mb_read_reg_03(slave, addr)
local onlineState = get_uint16(VT_LW,0x01A3) -- 获取从机在线状态
if ((onlineState >> slave) & 0x01) == 0x01 -- 读取从机在线状态
then
select_slave(slave)-- 选择从机
return get_uint16(VT_4x, addr)--获取寄存器值
else
return false
end
end
假设需要读取从机1/从机2的电量、电量,并计算总电压电量。如下所示
function on_run(screen)
local Energy1 = mb_read_reg_03(0, 0x0000)
local Energy2 = mb_read_reg_03(1, 0x0000)
if Energy1 ~= false and Energy2 ~= false
then
set_uint16(VT_LW, 0x1000, Energy1 + Energy2)
end
local vol1 = mb_read_reg_03(0, 0x0001)
local vol2 = mb_read_reg_03(1, 0x0001)
if vol1 ~= false and vol2 ~= false
then
set_uint16(VT_LW, 0x1001, vol1 + vol2)
end
end
2.2.运行预览
创建虚拟串口对,用Modbus slave和虚拟屏联机,如下所示,总电压、总电压实时变化:

3.Lua 写寄存器
3.1.Lua 脚本
Lua脚本写从机寄存器值,通常在某个条件下或计算后,设置对应寄存器。步骤如下所示
- 获取寄存器前,要需要判断从机是否在线
- 获取寄存器前,需选择对应的从机
--写单个保存寄存器
function mb_write_reg_06(slave, addr, value)
local onlineState = get_uint16(VT_LW,0x01A3)
if ((onlineState >> slave) & 0x01) == 0x01
then
feed_dog()
select_slave(slave)
set_uint16(VT_4x, addr, value)
return true
else
return false
end
end
假设一个称重业务,一共有三种校准模式,如下所示
- 零点校准:实重 = 57
- 砝码校准:实重 = 砝码重量
- 实物校准:实重=标定系数*砝码重量 // 保存码
function on_update(slave,vtype,addr)
if vtype == VT_LW
then
if addr == 0x1004
then
local mode = get_uint16(VT_LW, 0x1003) -- 校准
if mode == 0
then
mb_write_reg_06(0, 0x1002, 57)
elseif mode == 1
then
--实重 = 砝码重量
print('val == 1')
local weightVal = get_uint16(VT_LW, 0x1005) -- 砝码重量
mb_write_reg_06(0, 0x1002, weightVal)
elseif mode == 2
then
--if (标定系数>0) && (保存码>0)
--then
-- 实重=标定系数*砝码重量 // 保存码
--end
local coeffi = get_uint16(VT_LW, 0x1006) --标定系数
local saveCode = get_uint16(VT_LW, 0x1007) --保存码
if coeffi > 0 and saveCode > 0
then
local weightVal = get_uint16(VT_LW, 0x1005) -- 砝码重量
mb_write_reg_06(0, 0x1002, (saveCode*weightVal)//coeffi )
end
end
end
end
end
3.2.运行预览
创建虚拟串口对,用Modbus slave和虚拟屏联机,如下所示,选择不同的校准方式和校准参数,校准结果实时变化:

4.Lua 批量写线圈
4.1.Lua 脚本
VisualHMI批量写线圈最小单位是短整型,16个线圈对齐。本章节封装了批量写线圈的函数,直接调用以下接口
mb_write_coil_15(slave_id, addr, coilsTb)
- slave_id:从站号索引
- addr:起始地址
- coilsTb:线圈值,字表
[!note|tip:注意] mb_write_coil_15 该函数为自行封装api,非系统api
假设一个业务逻辑,线圈起始地址为0x0000,设置连续14个线圈值,如下所示
| coil | c0 | c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c9 | c10 | c11 | c12 | c13 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| val | 1 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 0 |
按钮按下,执行批量设置寄存器逻辑,如下所示:
--批量写多个线圈
function mb_write_coil_15 (slave,addr,coils)
if ((addr + #coils) > 0xFFFF ) or (#coils > 120) or (#coils <= 0)
then
return
end
local onlineState = get_uint16(VT_LW,0x01A3)
if ((onlineState >> slave) & 0x01) == 0x01
then
feed_dog()
select_slave(slave)
local cnt = (#coils // 16) + (((#coils % 16) >= 0 ) and 1 or 0) -- 总共个数,16个线圈对齐
local all = cnt*16
local idx = (#(coils) + 1)
if all > 120 --标准modbus 最大读写连续寄存器<=120
then
return
end
for i = idx, all --16bit对齐,获取实际对齐部分线圈的值
do
local ret = mb_read_coil_01(slave, ((addr + i) - 1))
if ret == false
then
return false
end
coils[#coils + 1] = ret
end
local val = {}
for i = 1, cnt --合并16位数据
do
val[i] = 0x0000
for j = 0, 15
do
val[i] = set_valbit(val[i], j, coils[(i - 1)*16 + j + 1])
end
end
set_array(VT_0x, addr, val)
return true
else
return false
end
end
function on_update(slave,vtype,addr)
if vtype == VT_LW
then
if addr == 0x1004
then
......
elseif addr == 0x2000
then
local val = get_uint16(VT_LW, 0x2000) -- 批量写
if val == 1
then
--批量写入0x0000~0x000D, 14个寄存器 5B1A
mb_write_coil_15(0, 0x0000, {1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0 })
....
end
end
end
end
4.2.运行预览
创建虚拟串口对,用Modbus slave和虚拟屏联机,如下所示,点击批量写线圈按钮,0x0000~0x000D线圈对应变化

5.Lua 批量写寄存器
5.1.Lua 脚本
VisualHMI批量写保持寄存器最小单位是16位数据。本章节封装了批量写寄存器的函数,直接调用以下接口
mb_write_reg_16(slave_id, addr, regsTb)
- slave_id:从站号索引
- addr:起始地址
- regsTb:寄存器值,字表
[!note|tip:注意] mb_write_reg_16 该函数为自行封装api,非系统api
--批量写多个保存寄存器
function mb_write_reg_16 (slave,addr,regs)
local onlineState = get_uint16(VT_LW,0x01A3)
if ((onlineState >> slave) & 0x01) == 0x01
then
feed_dog()
select_slave(slave)
set_array(VT_4x, addr, regs)
return true
else
return false
end
end
function on_update(slave,vtype,addr)
if vtype == VT_LW
then
if addr == 0x1004
then
......
elseif addr == 0x2000
then
local val = get_uint16(VT_LW, 0x2000) -- 批量写
if val == 1
then
....
elseif val == 2
then
--批量写入0x1000~0x1006, 7个寄存器
mb_write_reg_16(1, 0x1000, {0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007})
end
end
end
end
5.2.运行预览
创建虚拟串口对,用Modbus slave和虚拟屏联机,如下所示,点击批量写寄存器按钮,0x1000~0x1006寄存器对应变化
