全部手册
SIMTUTAI / DOCUMENTATION

开发手册

开发手册、使用视频,应用案例。

本节介绍 Modbus 四区读写、字内位和批量操作,并提供双从站及屏端从站示例。

核心功能

  • 四区地址与从站选择。
  • 单点、字内位、多字及批量读写。
  • 持续轮询、变量通知与通信检查。

位与字: 0/1 区每个地址为一个位,3/4 区每个地址为一个 16 位字。线圈单点用 get_bit/set_bit,连续 16 点用 get_uint16/set_uint16。

使用前更新屏端固件或模拟器,并加载最新导出的工程。

相关文档:5.2 寄存器读写 · 5.1 系统回调 · 主站配置 · 从站配置

1. 通信配置

1.1 数据区与地址

下表的读写权限指屏作为 Modbus Master 访问外部设备。vtype 是脚本参数,功能码是实际通信报文中的字段,两者不是同一编号。

数据区 vtype 单地址宽度 读功能码 写功能码
0 区 · 线圈 3 1 位 0x01 单点 0x05;多点 0x0F
1 区 · 离散输入 4 1 位 0x02 只读
3 区 · 输入寄存器 5 16 位/2 字节 0x04 只读
4 区 · 保持寄存器 6 16 位/2 字节 0x03 单字 0x06;多字 0x10

4 区配置强制多字写时,单字也使用 0x10。位区的值为 0/1;1/3 区的“只读”指主站访问外部设备,屏内部数据更新见“3.2 屏作为从站”。

0x224 是十六进制数值 548,不表示它必然属于“0 区”。例如 get_bit(3, 0x224) 访问线圈 548,get_uint16(6, 0x224) 访问保持寄存器 548;两者的数据区独立。

地址使用设备手册明确的协议数值地址。若手册明确“40001 对应报文地址 0”,才按该关系换算;不要把显示标签 40001 或 40101 直接当成所有设备通用的脚本地址。十进制 100 与十六进制 0x64 表示同一个地址。

运行库没有内置 VT_0x、VT_4x、LW 等常量。以下常量由示例自己定义:

lua 复制代码
local LW = 1
local MB_COIL = 3
local MB_DISCRETE = 4
local MB_INPUT = 5
local MB_HOLDING = 6

1.2 从站索引

先在工程通信设置中配置从站列表、各区地址范围、波特率、数据位、校验和停止位。界面的“从站01”对应 Lua 索引 0,“从站02”对应索引 1;各区“最大地址”须覆盖示例访问的完整跨度。本节示例假设配置两个从站:

工程顺序 Lua 从站索引 设备实际站号
第一个从站 0 1
第二个从站 1 7
lua 复制代码
select_slave(0)                   -- 访问列表中的第一个设备,实际站号 1
local a = get_uint16(6, 100)

select_slave(1)                   -- 访问第二个设备,实际站号 7
local b = get_uint16(6, 100)

select_slave(0)                   -- 明确恢复下一次业务所需的选择

select_slave 影响后续数值、位、字符串和字表读写,默认索引为 0。

  • 索引只接受已配置的 0~7。传入实际站号 7 不会自动选择列表中的第二个设备。
  • 非法选择抛 Lua 错误,并保留此前的选择。
  • LW/RW 始终是屏的本地数据,不随从站选择切换。

start_read 的第一个参数直接指定预读从站索引,不依赖最近一次 select_slave。处理 on_update(slave, vtype, addr) 的外部值时,应先 select_slave(slave),避免从另一个设备的缓存读取同号地址。

2. 读写说明

2.1 单点读写

lua 复制代码
local MB_COIL = 3
local MB_DISCRETE = 4

select_slave(0)
local coil = get_bit(MB_COIL, 0x224)          -- 只读取线圈 0x224
local input = get_bit(MB_DISCRETE, 0x224)     -- 只读取离散输入 0x224

set_bit(MB_COIL, 0x224, 1)                   -- FC05,置位这一点
set_bit(MB_COIL, 0x224, 0)                   -- FC05,复位这一点

get_bit 返回整数 0 或 1,set_bit 只接受整数 0 或 1。没有第三个“位号”参数:位元件的地址本身就是位号。主站不能 set_bit(4, ...) 写离散输入。

2.2 读取连续位

lua 复制代码
local base = 0x224
select_slave(0)
local coils = get_uint16(3, base)             -- 线圈 base~base+15
local inputs = get_uint16(4, base)            -- 离散输入 base~base+15

local first = coils & 1                      -- base 点
local next_point = (coils >> 1) & 1           -- base+1 点
local last = (coils >> 15) & 1                -- base+15 点

整组的第 k 位对应起始地址加 k;16 位线圈组不受“16 位整序交换”设置影响。例如线圈 0x224=0、0x225=1、其余 14 点为 0 时,get_bit(3, 0x224) 返回 0,get_uint16(3, 0x224) 返回 2。

整组写会改变后面的线圈

set_uint16(3, 0x224, 1) 使用 FC0F:0x224=1,随后 15 点=0。只改一个线圈应使用 set_bit。

两次整组读取的地址相差 1 时,窗口也只移动一个位;读取后面不重叠的一组,应把地址加 16。

普通数值、字设置和多状态控件也可能按配置读取整组;布尔位按钮和布尔条件的单点修复,不会把所有脚本数值访问都改成单点。

2.3 字读写

lua 复制代码
select_slave(0)
local measured = get_uint16(5, 100)           -- 3 区输入寄存器,后台 FC04
local setting = get_uint16(6, 100)            -- 4 区保持寄存器,后台 FC03
set_uint16(6, 100, 250)                      -- 正常单字设置 FC06,配置强制时 FC10

UINT16 范围为 0~65535。带符号的 16 位值使用 get_int16/set_int16,范围为 -32768~32767。主站不能用写函数更新外部 3 区。

2.4 读取字内位

lua 复制代码
local function read_word_bit(vtype, addr, bit_index)
    assert(bit_index >= 0 and bit_index <= 15)
    local word = get_uint16(vtype, addr)
    return (word >> bit_index) & 1
end

select_slave(0)
local alarm = read_word_bit(5, 100, 5)         -- 输入寄存器 100 的 bit5
local enabled = read_word_bit(6, 100, 3)      -- 保持寄存器 100 的 bit3

字内的位仍属于同一个寄存器,不能用地址 100+5 代替寄存器 100 的 bit5。

  • get_bit(5, 100) 和 get_bit(6, 100) 只读取逻辑字的 bit0。
  • set_bit(6, 100, 1) 通过缓存合并修改 bit0;主站基于已确认的保持寄存器缓存保留其他位,发送完整字写命令。
  • 字内位操作使用 FC06/10,不会发送线圈 FC05。

使用前先建立有效读回。 初始化缓存或 PLC 并发修改都可能使保位结果与设备当前字不一致,内置 set_bit 也不提供 PLC 侧原子掩码写。

2.5 修改字内位

lua 复制代码
local function write_holding_bit(addr, bit_index, value)
    assert(bit_index >= 0 and bit_index <= 15)
    assert(value == 0 or value == 1)
    local word = get_uint16(6, addr)
    local mask = 1 << bit_index
    local updated = value == 1 and (word | mask) or (word & (~mask))
    set_uint16(6, addr, updated & 0xFFFF)
end

select_slave(0)
write_holding_bit(100, 5, 1)                 -- 修改 bit5

local word = get_uint16(6, 100)
local field = (word >> 4) & 0x0F            -- 提取 bits4~7
local updated = (word & 0xFF0F) | (6 << 4)
set_uint16(6, 100, updated)                 -- 只在本次计算中改变 bits4~7

这个 Lua 辅助函数按“读本地字 → 改位 → 写完整字”执行,必须确保该字的缓存已经由有效通信建立。它不提供 PLC 侧原子掩码写;若 PLC 或其他主站同时修改同一个字,可能覆盖并发变化。当前此通路不发送 FC16(十六进制 0x16)掩码写命令。

控件若只需要控制一个字内位,应在地址属性中明确配置“开始位=结束位”。字地址默认字段 F0 表示整个字,写 0/1 会修改整个字;不能因为控件显示的是开关就认为它自动选择了某个字内位。

2.6 多字数值

lua 复制代码
select_slave(0)
local count = get_uint32(6, 110)             -- 占用保持寄存器 110、111
local temperature = get_float(5, 120)        -- 占用输入寄存器 120、121
set_uint32(6, 110, 70000)                   -- 2 字,FC10
set_float(6, 120, 12.5)                     -- 2 字,FC10

在 3/4 区,32 位数值占 2 个连续寄存器,64 位数值占 4 个;相邻业务地址要预留这一跨度。在 0/1 区调用 32/64 位数值接口,则覆盖连续 32/64 个点,并按对应的 32/64 位整序处理。数值字节序按工程的 16/32/64 位整数及浮点设置解释。示例假设设备与工程设置一致;不能只根据值“看起来不对”修改寄存器地址。

当前工程 Lua 使用 32 位有符号整数和单精度浮点数。get_uint32 不保证把大于 2147483647 的原始值作为正整数返回;set_uint32 的非负整数参数也受当前 Lua 可表达范围限制,不能直接用 0xFFFFFFFF 当作 4294967295 传入。64 位接口也不能扩大 Lua 的整数或精度范围。需要保存完整的较大无符号数时,在普通 16 位字区按字读取和处理,详见寄存器操作函数。

2.7 连续读取

当前没有原生 get_array。下面辅助函数返回从下标 1 开始的 Lua 表;循环读取的是本地缓存,并不是同步发送 N 次 Modbus 读请求。

lua 复制代码
local function read_words(slave_index, vtype, addr, count)
    assert(vtype == 5 or vtype == 6)
    assert(count >= 1 and addr >= 0 and addr + count <= 65536)
    select_slave(slave_index)
    local values = {}
    for i = 1, count do
        values[i] = get_uint16(vtype, addr + i - 1)
    end
    return values
end

-- 在 on_init 中预先登记:start_read(0, 6, 100, 3)
local values = read_words(0, 6, 100, 3)       -- 寄存器 100、101、102

这不是跨多个寄存器的原子快照;设备可能在不同帧之间更新数据。需要一组一致的业务数据时,应与设备约定快照寄存器、数据版本号或提交标记。

2.8 批量写字

lua 复制代码
select_slave(1)
set_array(6, 100, {250, 600, 3})            -- 写 100、101、102,FC10
-- 同义接口:set_uint16_ex(6, 100, {250, 600, 3})

set_array 与 set_uint16_ex 功能相同。构造字表时遵守以下规则:

  • 从下标 1 开始、没有空洞,长度为 1~2000 个 16 位字。
  • 每项使用 0~65535 的整数。超范围整数当前会截断,例如 -1 变为 65535、65536 变为 0;调用前应自行检查。
  • 表项提交原始字,不执行单值数值接口的 16 位整序交换。

以上示例假设 16 位整序为正常。使用交换整序时,应按设备原始字构造表,或逐个调用按工程整序处理的 set_uint16。

当前主站批量写按每帧最多 120 字 拆分;超过一帧不等于一次 PLC 事务。标准 FC10 上限为 123 字,120 是当前这个脚本批量通路的分帧上限。每站写队列同时受 10 条命令及字节容量限制:Lua 接受长表不代表所有分帧都已发送或执行;队列不足时本地值保留,运行日志记录 TX 未入队。

2.9 整组写线圈

lua 复制代码
select_slave(0)
set_array(3, 0x200, {0x0005, 0x8000})
-- 第一项:0x200~0x20F,其中 0x200、0x202 为 1,其余为 0
-- 第二项:0x210~0x21F,其中 0x21F 为 1,其余为 0

线圈字表的每项覆盖 16 个线圈,bit0 对应该组起始点;后一项的线圈地址加 16。{1, 0, 1} 表示 48 个线圈的三个打包字,不是三个线圈值。FC0F 标准上限为 1968 点,当前脚本批量通路每帧最多 120 组,即 1920 点。

2.10 逐点写线圈

lua 复制代码
select_slave(0)
local points = {1, 0, 1}
for i = 1, #points do
    set_bit(3, 0x224 + i - 1, points[i])
end

这提交三个单点 FC05 操作,范围严格为 0x224~0x226,不会为了凑 16 点而清零后面的 13 点。它是多条写命令,仍遵守有限队列和本地提交规则。大批量业务应分阶段提交并核对通信,不要在一个长循环里假定全部设备写入已经成功。

需要批量改 14 点时,也不要直接把最后两点补 0 后整组写入;可以逐点写,或在确认末尾两点有效缓存且允许整组覆盖时保留它们再打包。后者仍是缓存合并,不提供外部设备的原子性。

2.11 持续预读

lua 复制代码
start_read(slave_index, vtype, addr, count)

四个参数必须是整数。成功没有返回值,只登记持续轮询范围,读取数据仍使用 get_*。

参数 当前规则
slave_index 已配置的从站索引 0~7;不是任务编号、不是实际站号
vtype Modbus 的 3/4/5/6;LW/RW 是本地数据,不登记外部轮询
addr 起始数值地址 0~65535,还须落在工程相应区域范围内
count 1~65535 个桥接字,且 addr+count<=65536;vtype 3/4(0/1 区)每字登记 16 点,vtype 5/6(3/4 区)每字登记 1 个寄存器
lua 复制代码
function on_init()
    start_read(0, 3, 0x224, 1)              -- 线圈 0x224~0x233,共 16 点
    start_read(0, 4, 0x300, 2)              -- 离散输入 0x300~0x31F,共 32 点
    start_read(0, 5, 100, 4)                -- 输入寄存器 100~103
    start_read(0, 6, 100, 4)                -- 保持寄存器 100~103
    start_read(1, 6, 100, 4)                -- 第二个从站的保持寄存器
end

位区在地址末端按实际可用范围截取,不绕回地址 0。登记参数合法也不代表工程范围外的地址会被轮询;示例的完整范围应在工程中开放。登记成功不代表已有设备应答,也不能用初始化的零值判断在线。

2.12 轮询规则

  1. 当前页面控件按绑定地址登记通信需求;读取和脏检查续订所需地址。主站脚本 get_* 读取缓存时也登记相应范围。
  2. start_read 保存脚本的持续订阅,通常在 on_init 调用一次,用于不在当前页面上的变量。
  3. 协议状态机按工程参数、当前请求/应答、命令间隔和重试调度通信。一次 get_* 调用不等于立即发送一帧;多个读取可共享后台批量读。get_bit 返回目标一个位,并不保证后台读帧只包含这一点,后台可以同时更新邻近缓存。
  4. 同站、同区的邻近地址可批量读取,遵守工程“最多读取数”及“读取间隔数”。这两个数量在字区按字,在位区按 16 点组换算;“读取间隔数”用于合并未登记的间隔。标准读限制为位区 2000 点、字区 125 字;工程设置可进一步限制每帧数量。
  5. 有效响应先更新可读缓存,再刷新显示和排队 Lua 通知。相同值通常不重复通知;首次有效零值也会通知,同址尚未分发的多次变化可能合并。

订阅保留与清理:

  • 页面实际删除后的清表会恢复脚本预读及报警/采样的全局需求。
  • 最多保存 16 个不同的“从站索引+数据区+起始地址”订阅。
  • 同一起点再次 start_read 只更新数量;缩短后撤销旧尾部的脚本需求,其他控件仍在使用的地址可以继续轮询。
  • 脚本关闭或重载时清理旧订阅,并把默认从站恢复为 0。

容易混淆的时间与数量:

工程设置/接口 含义
最多读取数 限制每帧读数量:3/4 区按字;0/1 区按 16 点组换算
读取间隔数 合并时允许的未登记间隔:3/4 区按字;0/1 区按 16 点组换算,不是毫秒
间隔时间ms 通信调度的时间设置
超时时间ms 等待应答的超时时间
start_timer 间隔 Lua 定时回调周期

完整通信周期随变量数量、串口速率、应答和重试变化,不能承诺所有变量每 100 ms 都得到新值。当前没有注册 stop_read、stop_all_read、set_auto_read,也不支持 cycle/cycle_run/mode 轮询参数。

2.13 写入与通知

数值/位读写的参数错误、越界、只读或不支持的访问会抛 Lua 错误;set_* 正常执行 无返回值。

lua 复制代码
select_slave(0)
local ok, err = pcall(set_bit, 3, 0x224, 1)
if ok then
    print("本地写入已提交;继续核对设备应答/读回")
else
    print("本地写入失败", err)
end
阶段 实际含义
本地提交 校验通过后先更新 HMI 缓存和显示,再尝试发送;未连接 COM、无应答或队列不足时,本地输入也可以生效
设备执行 需要核对通信应答或有效读回;函数返回、立即读到新值、on_update 都不能作为执行确认
未入队 TX 未入队会记录日志,未发送的输入不会自动补发;恢复后按业务重新提交
读回同步 无同址待写时,有效读回使缓存重新与设备值同步
lua 复制代码
function on_update(slave, vtype, addr)
    if vtype == 6 and addr == 100 then
        select_slave(slave)
        print("保持寄存器缓存更新", slave, addr, get_uint16(vtype, addr))
    end
end

on_update 是可读缓存变化通知,不是逐条写 ACK 回调;多字值可能按各组成地址通知。回调内读写自己需要的设备前都应明确从站选择。当前 LW0x01A3 尚未统一提供有效在线状态,不能把该寄存器当作可靠的在线门控条件。

set_notify(0) 只关闭 DCBUS/DGUS 自动上传,不停止 Modbus 轮询或 PLC 写命令。set_bit(..., notify=0/2) 也不会停止主站写命令。需要停止定时写操作时,用业务开关或 stop_timer;不要把通知开关当通信停止开关。

3. 应用

3.1 双从站读写

工程配置

本例初始化时登记通信、清零 LW 命令和结果变量;每 500 ms 将缓存镜像到 LW。对外写入由画面按钮触发。

配置项 本例要求
从站列表 索引 0 → 站号 1;索引 1 → 站号 7
0 区线圈 两站均开放 0x224~0x233
1 区离散输入 两站均开放 0x300~0x30F
3/4 区寄存器 两站均开放 100~103
16 位整序 正常

画面地址与按钮命令

数值显示控件使用下表中的 LW 地址。两站镜像地址分开,业务地址相同。

显示内容 第一个从站 第二个从站
线圈 0x224 LW0x1100 LW0x1110
离散输入 0x300 LW0x1101 LW0x1111
输入寄存器 100 LW0x1102 LW0x1112
保持寄存器 100 LW0x1103 LW0x1113

数值输入绑定 LW0x1201~0x1203。字设置按钮向 LW0x1200 写入下列命令,脚本消费后自动清零;这四种命令都写第一个从站。

命令值 操作 使用的参数
1 置位线圈 0x224 无
2 复位线圈 0x224 无
3 写保持寄存器 100 LW0x1201
4 批量写保持寄存器 100~102 LW0x1201~0x1203

结果地址:LW0x1204。0=初始化;1=本地提交正常;2=脚本写入失败。它不能作为设备执行状态。

完整脚本

lua 复制代码
local LW = 1
local COIL, DISCRETE, INPUT, HOLDING = 3, 4, 5, 6
local COMMAND, PARAMETER, RESULT = 0x1200, 0x1201, 0x1204
local TIMER_VIEW = 1

local function mirror_value(slave, reader, vtype, addr, destination)
    select_slave(slave)
    local ok, value = pcall(reader, vtype, addr)
    if ok then
        set_uint16(LW, destination, value)
    end
end

function on_init()
    for slave = 0, 1 do
        start_read(slave, COIL, 0x224, 1)
        start_read(slave, DISCRETE, 0x300, 1)
        start_read(slave, INPUT, 100, 4)
        start_read(slave, HOLDING, 100, 4)
    end
    set_uint16(LW, COMMAND, 0)
    set_uint16(LW, RESULT, 0)
    start_timer(TIMER_VIEW, 500, 0, 0)
end

function on_timer(timer_id)
    if timer_id ~= TIMER_VIEW then
        return
    end
    for slave = 0, 1 do
        local base = 0x1100 + slave * 16
        mirror_value(slave, get_bit, COIL, 0x224, base)
        mirror_value(slave, get_bit, DISCRETE, 0x300, base + 1)
        mirror_value(slave, get_uint16, INPUT, 100, base + 2)
        mirror_value(slave, get_uint16, HOLDING, 100, base + 3)
    end
    select_slave(0)
end

function on_update(slave, vtype, addr)
    if vtype ~= LW or addr ~= COMMAND then
        return
    end
    local command = get_uint16(LW, COMMAND)
    if command == 0 then
        return
    end
    set_uint16(LW, COMMAND, 0)
    local ok, err = pcall(function()
        select_slave(0)
        if command == 1 then
            set_bit(COIL, 0x224, 1)
        elseif command == 2 then
            set_bit(COIL, 0x224, 0)
        elseif command == 3 then
            set_uint16(HOLDING, 100, get_uint16(LW, PARAMETER))
        elseif command == 4 then
            set_array(HOLDING, 100, {
                get_uint16(LW, PARAMETER),
                get_uint16(LW, PARAMETER + 1),
                get_uint16(LW, PARAMETER + 2)
            })
        else
            error("未知示例命令")
        end
    end)
    set_uint16(LW, RESULT, ok and 1 or 2)
    if not ok then
        print("Modbus 示例命令失败", err)
    end
end

LW 镜像只是最近可读缓存,不提供在线/数据时效标志。不要把“镜像值为 0”当成断线,也不要把点击后的镜像新值当成 PLC 写确认。工程已有同名回调、定时器或 LW 地址时,应合并业务并调整示例地址,不能用第二份同名全局函数覆盖原业务。

3.2 屏作为从站

从站模式下,Lua 操作的是屏内部的 0/1/3/4 区缓存,不向 PLC 发送主站请求。select_slave(0) 可用于统一示例写法,但不会选择外部设备;start_read 不会使从站主动发出查询。

lua 复制代码
function on_init()
    set_uint16(5, 100, 250)                 -- 本地提供输入寄存器,供外部主站 FC04 读取
    set_bit(4, 0x300, 1)                    -- 本地提供离散输入,供外部主站 FC02 读取
    set_uint16(6, 100, 9)                   -- 本地保持寄存器
    set_bit(3, 0x224, 1)                    -- 本地线圈
end

function on_update(slave, vtype, addr)
    if vtype == 6 and addr == 100 then
        print("屏内部保持寄存器变化", get_uint16(6, 100))
    end
end

“1 区、3 区只读”指外部 Modbus 主站通过协议的访问权限。屏内部 Lua 可以更新这些输入数据,以提供状态和测量值。外部主站仍只可用 FC05/0F 写线圈、FC06/10 写保持寄存器,不能通过写功能码写离散输入或输入寄存器。

从站模式同样区分单点 get_bit/set_bit 和 16 位整组数值访问;字节序按从站工程设置处理,on_update 也可能由本地脚本或控件写入触发。

4. 常见问题

4.1 功能码速查

功能码(十六进制) 十进制 作用 本节对应操作
0x01 1 读线圈 0 区后台轮询
0x02 2 读离散输入 1 区后台轮询
0x03 3 读保持寄存器 4 区后台轮询
0x04 4 读输入寄存器 3 区后台轮询
0x05 5 写一个线圈 set_bit(3, addr, 0/1)
0x06 6 写一个保持寄存器 正常单字 set_uint16(6, ...) 等
0x0F 15 写多个线圈 0 区整组 set_uint16、字表
0x10 16 写多个保持寄存器 多字数值、字表;配置强制时也可只写 1 字

注意“功能码 16”通常指十进制 16,即十六进制 0x10;十六进制 0x16 是另一条掩码写功能码。set_bit(6, ...) 修改字的 bit0 仍发字写,set_bit(3, ...) 才发线圈 FC05。

模拟器串口副屏 TX/RX 可以核对站号、功能码、起始地址、数量、字节数及应答。看到 0x224 后面一起变化时,先区分:实际线圈值是否改了、脚本是否使用了整组写、显示条件是否读了整组。只读取后台缓存的 get_* 没有“本次函数调用对应的独立读 ACK”。

4.2 通信检查

  1. 从站索引是否与设备站号混用,手册地址是否已换算。
  2. 各区地址范围是否覆盖完整跨度,16/32 位与浮点字节序是否一致。
  3. 字表是否按 16 位打包,单点业务是否误用了整组写。
  4. 是否把本地提交当成设备执行,通信日志是否有有效应答。
  5. 回调是否重复定义,是否加载了最新工程脚本。