"配方点了下发,画面显示成功,设备跑出来的还是上一套参数"——这种事故在现场基本都能追到同一个根因:上位机只管"写出去",没人确认 PLC 真的"收到且认下了"。配方下发和采集监控是两回事:采集错一个点顶多画面闪一下,配方写错一组参数就是整批废品甚至设备事故。可靠的配方下发要凑齐四样东西:一张对得上的地址映射表、一笔原子的批量写、一组握手机制字、一套回读校验流程。这篇把这套机制完整画出来。

第一步:映射表是合同,双方各执一份

配方下发的第一原则:上位机软件里不出现任何一个魔法地址。所有"配方参数 ↔ PLC 寄存器"的对应关系全部落在一张映射表里,上位机照表写、PLC 工程师照表编程,调试时对着表逐行核对。我们用 Modbus 保持寄存器(4xxxxx)做配方区,地址连续排布,方便功能码 16 一笔写完。以一条热处理产线为例:

参数名寄存器地址类型量程/单位说明
■ 握手区(40090 ~ 40094)
下发请求字 Req16位40090UInt160/1HMI 写 1 = 请求下发;PLC 处理完自动清 0
校验回执字 Ack16位40091UInt160~40 空闲 / 1 校验中 / 2 校验OK已生效 / 3 校验失败 / 4 设备忙拒绝
配方版本号 Ver16位40092UInt160~65535每次下发 +1,HMI 与 PLC 比对版本一致才算完成
校验和 ChkSum16位40093UInt16配方区所有寄存器的累加和(mod 65536),PLC 侧重算比对
故障码 ErrCode16位40094UInt160~校验失败时填第几个参数不合法,画面直接红字提示
■ 配方数据区(40100 起,地址连续)
一区设定温度2寄存器40100~40101Float320.0~400.0 ℃float 占两个寄存器,字序见下文坑点
二区设定温度2寄存器40102~40103Float320.0~400.0 ℃同上
保温时间1寄存器40104UInt160~600 min整数,缩放系数 1
升温速率1寄存器40105UInt160.1~10.0 ℃/s×10 定点:写 35 表示 3.5,避免 float 对字序
目标真空度2寄存器40106~40107Float320.0~1.0 MPa
冷却风扇延时1寄存器40108UInt160~600 s

▲ 这张表就是合同:上位机按地址写,PLC 按地址读,任何一方改地址都要双方签字改表。数据区地址必须连续,中间不留洞,这是功能码 16 一笔原子写的前提。

最经典的坑:float 字序。32 位浮点在 Modbus 里占两个寄存器,而两个寄存器、寄存器内两个字节的排列顺序,不同品牌 PLC 不一样——ABCD(大端)、CDAB(字交换)、BADC、DCBA 四种都有人用。温度写 180.0,PLC 里读到 2.6e-38 这种离谱值,99% 是字序错了。两个办法:要么拿浮点数 1.0(hex 3F800000)写一笔,让 PLC 侧读出来核对字序;要么干脆避开 float,用"整数+缩放系数"定点传输(比如上表的升温速率 3.5 写成 35)。定点法土是土,永远不会有字序问题,现场越土的招越稳。

第二步:停机互锁,设备跑着的时候禁止下发

下发前必须先问 PLC 一句"现在能不能收"。我们在映射表约定一个只读状态区(采集用),上位机下发前先读:运行中、有未复位报警、自动模式下,下发按钮直接置灰,连请求字都不让写。这个检查放在上位机侧是第一道门,PLC 程序里也要再写一遍(设备运行时即使收到请求字也回 Ack=4 拒绝)——上位机的互锁防误操作,PLC 的互锁防上位机异常,两道门一个都不能省。

第三步:握手状态机,"写出去"不等于"生效了"

核心机制就五个握手字。下发不是"写参数"一个动作,而是一个双方来回确认的状态机:

① 空闲读 Ack = 0确认 PLC 就绪、设备停机、上次下发已收尾
② 写数据功能码16一笔写40100~40108 整段连续写入,附校验和与新版本号
③ 请求写 Req = 1数据全部到位后才置请求,顺序绝不能反
④ PLC校验轮询 AckPLC 重算校验和、逐参数查量程,Ack=1 停留约几十毫秒
⑤ 生效Ack = 2校验通过:PLC 拷贝到生效配方区,清 Req,回写当前 Ver
⑤'Ack = 3 / 4失败:读 ErrCode 报出第几个参数非法,或设备忙,全部回退

这里有两个顺序细节,都是现场用事故换来的:一是先写数据、最后置 Req——PLC 看到 Req=1 才开始校验,如果你先置请求再写数据,PLC 读到的是半新半旧的参数,校验和碰巧还能对上旧值;二是上位机必须等 Ack=2 并核对版本号,而不是写完就算成功。Req 是"我请你干活",Ack 才是"我干完了",少了这个回执,画面上的"下发成功"就是自欺欺人。

为什么还要校验和 + 版本号双保险?校验和防"传输途中个别寄存器写错/丢写"(通信干扰、网关异常都可能发生),PLC 重算一遍对不上直接 Ack=3;版本号防"时序错乱"——操作员连发两次不同配方,Ack 回来时分不清是哪次的结果。每次下发版本号递增,回执里版本号对不上本次,就判超时重来。这两个字各防一类故障,不冲突。

第四步:回读比对,让 PLC 自己念一遍

Ack=2 之后还有最后一道:上位机把数据区整段回读一次,和本地配方逐寄存器比对。这道关防的是"PLC 校验逻辑本身有 bug"或"写生效区时映射错了地址"——PLC 说它收下了,那让它把收到的念出来对一遍。回读一致,本次下发才算真正闭环。下面是完整的 C# 下发时序骨架:

public RecipeResult Download(Recipe r)
{
    // ① 前置互锁:设备必须停机、无报警、Ack=0(空闲)
    var st = _plc.ReadHolding(StatusAddr, 4);   // 读状态区
    if (st.Running || st.Alarm || _plc.Read(40091) != 0)
        return RecipeResult.BusyOrRunning;      // 按钮置灰的服务端兜底

    ushort newVer = (ushort)(_plc.Read(40092) + 1);
    var regs = BuildRegisters(r, newVer);      // 映射表:参数→寄存器数组,含缩放、字序
    ushort chk = (ushort)(regs.Sum(x => x) % 65536);
    SetChecksum(regs, chk);

    // ② 数据先行:功能码16,整段40100~40108一笔写完(原子)
    _plc.WriteMultiple(40100, regs);
    _plc.Write(40093, chk);    // 校验和
    _plc.Write(40092, newVer); // 版本号

    // ③ 最后置请求位
    _plc.Write(40090, 1);

    // ④ 轮询回执:500ms一次,最长等10秒
    for (int i = 0; i < 20; i++)
    {
        Thread.Sleep(500);
        ushort ack = _plc.Read(40091);
        if (ack == 2 && _plc.Read(40092) == newVer) goto Verify;
        if (ack == 3) return RecipeResult.CheckFail(_plc.Read(40094));
        if (ack == 4) return RecipeResult.PlcBusy;
    }
    return RecipeResult.Timeout;   // PLC没回应,记录日志并人工介入

Verify:
    // ⑤ 回读比对:让PLC把收到的念一遍
    var echo = _plc.ReadHolding(40100, regs.Length);
    if (!echo.SequenceEqual(regs))
        return RecipeResult.EchoMismatch;      // 罕见但必须报:通信/PLC逻辑异常

    _log.Info($"配方 {r.Code} v{newVer} 下发并校验生效");
    return RecipeResult.Ok;
}

收尾:三个容易被忘掉的细节

  • 掉电保持:配方数据区和版本号寄存器在 PLC 侧必须设为掉电保持,设备重启后跑的还是上次确认过的配方,而不是清零后的默认值;
  • 下发全程禁止切自动:从置 Req 到 Ack=2 这几秒,PLC 程序里锁定模式切换,防止参数校验半截设备启动;
  • 每次下发写审计日志:谁发的、配方名、版本号、结果,全部落库——制药/食品行业这本来就是合规要求,其他行业留着也便于追溯废品批次。

配方下发这套东西,说白了就是把"我写你收"的口头约定,变成映射表对账 + 批量原子写 + 请求回执状态机 + 回读复核的书面流程。如果你现在的系统下发完只弹了个"成功"提示框,建议做个最小验证:下发一套参数后断电重启 PLC,起来看跑的是不是这套值——大概率能测出问题。