倍速链、转盘线上,工装板(托盘)一圈圈地转,板上装的是什么、属于哪个工单、前面漏了哪道工序——靠人盯迟早出事。给工装板贴一张纸质流程卡,油污一糊就废;靠工位记忆型号,混线生产时必错。RFID 解决的正是"看不见的流动":载码体随板循环、读写头各站自动识别,上位机据此做型号校验、工序防错和路由分流。但 RFID 项目翻车的也多——频段选错、金属上读不到、相邻工位串读、写卡与落库不一致——这篇把这些坑一次讲透。

一、频段选型:工装板流转 90% 选 HF,别一上来就 UHF

HF 高频 13.56MHz(ISO 15693)
  • 工业读头有效距离通常 3~15cm,场界清晰,到位才读到、离开即断开,天然不串读
  • 抗干扰好,金属工装板配抗金属载码体稳定可用
  • 载码体便宜、无源免维护,读写头 RS485/以太网接入成熟
  • 工装板/托盘逐站识别的首选
UHF 超高频 860~960MHz(EPC C1G2)
  • 读距 1~10 米,支持多标签批量盘点(通道门、整箱出入库)
  • 金属、液体环境调谐敏感,标签位置和贴法极讲究
  • 远距离 = 双刃剑,容易读到隔壁工位、隔壁料架的标签
  • 适合仓储/物流批量读,逐工位精确识别反而难做
对比项HF ISO 15693UHF EPC Gen2
典型用途工装板工位识别、刀具/夹具管理仓库通道门、栈板盘点、整车物流
识别边界精确,与到位传感器配合模糊,需功率/RSSI/定向天线收敛
金属环境抗金属标签后稳定必须专用抗金属标签并打样验证
单标签成本几元~十几元几毛~几元(但读写头贵)

另有 LF 125kHz(EM 卡)只读、读距几厘米,老式低成本工装线还有存量,新项目不建议。原则:逐工位、要精确边界选 HF;要一次读一整托/一整箱选 UHF。拿不准就带着工装板和产品实物去读写头厂家现场打样,读距和稳定性以实测为准。

二、硬件构成与载码体数据结构

一套工位识别 = 载码体(嵌在工装板内的螺钉式/贴片式/PCB 式标签,金属板务必选抗金属款)+ 读写头(固定在工位下方/侧方,RS485、以太网或 IO-Link 接上位机/PLC)+ 安装支架与到位传感器。载码体芯片内部分两个关键区域:

区域内容用法
UID / TID出厂全球唯一序列号,只读不可改(15693 为 8 字节)直接当工装板身份证,永远不用写
用户区按块组织,每块 4 字节,容量依芯片(常见几十至上百字节)写板号/简码,块号与字节序按芯片手册对齐
关键设计原则:标签里只放一个 ID,业务数据全部放数据库。工装板绑定什么产品、哪个工单、走过哪些工位,全部以 TID 为键存在上位机库里。往标签用户区塞 SN、型号、工单号是新手做法——工单一变就要逐块改写,写失败就是卡库不一致,且容量根本不够。标签是钥匙,数据库才是档案

三、读写头对接:统一抽象,隔离厂家协议

读写头接入常见三种:厂家 DLL/SDK(USB 或串口读写器)、Modbus RTU/TCP(读写头作从站,寄存器映射盘点结果)、工业总线(PROFINET/EtherNet/IP 直接进 PLC,上位机再从 PLC 取)。上位机侧定义统一接口,换厂家只换实现:

/// <summary>RFID读写头统一抽象:无论SDK还是Modbus,业务层只依赖此接口</summary>
public interface IRfidReader : IDisposable
{
    void Connect(string conn);
    // 盘点场内标签,返回TID列表(ISO15693 UID十六进制字符串)
    IReadOnlyList<string> Inventory();
    // 读单块/写单块:block为块号,data固定4字节(具体以芯片手册为准)
    byte[] ReadBlock(string tid, int block);
    bool WriteBlock(string tid, int block, byte[] data);
}

// 串口命令型读写头的典型实现(各厂命令帧不同,此处演示帧结构骨架)
public class SerialRfidReader : IRfidReader
{
    private SerialPort _sp;
    public void Connect(string port)
    {
        _sp = new SerialPort(port, 115200, Parity.None, 8, StopBits.One)
              { ReadTimeout = 800, WriteTimeout = 800 };
        _sp.Open();
    }

    public IReadOnlyList<string> Inventory()
    {
        // 帧:帧头 + 命令字(盘点) + 长度 + 校验;返回帧里按标签数切出各TID
        byte[] resp = Transact(new byte[] { 0xAA, 0x01, 0x00, 0x00, 0xDD });
        return ParseTids(resp)
              .Select(b => Convert.ToHexString(b))
              .ToList();
    }

    public bool WriteBlock(string tid, int block, byte[] data)
    {
        if (data.Length != 4) throw new ArgumentException("块长度必须为4字节");
        byte[] resp = Transact(BuildWriteFrame(tid, block, data));
        return resp[2] == 0x00;   // 状态字0=写入ACK,但ACK后仍建议回读校验
    }

    private byte[] Transact(byte[] frame) { /* 加校验-发送-读整帧-超时重发1次,略 */ }
}

走 Modbus 的读写头更简单:盘点结果通常映射在保持寄存器里(TID 占若干连续寄存器、状态字 1 个),用 119 篇讲过的 Modbus 主站轮询即可,不必引厂家 SDK;总线直接进 PLC 的方案则让 PLC 把读到的 TID 搬进 DB,上位机通过 S7/OPC UA 读取。

四、工装板全生命周期:绑定→读校验→解绑→回流

1

上线绑定产品SN扫码,读载码体TID,库内建立 板↔SN↔工单↔型号 绑定

2

各站读校验每工位读TID查档案:型号对不对、工序顺序对不对、该不该走本分支

3

下线解绑成品下线,过站完结,解除板与SN绑定,产品档案保留

4

空板回流空工装板沿回板线回上线口,等待下一次绑定循环

闭环关键:空板回流到上线口时系统必须确认其处于"未绑定"状态——防止上一轮异常下线的残绑定把新产品挂到旧 SN 上。

public class PalletService
{
    // ① 上线绑定:扫产品SN + 读板TID + 落库(标签只认TID,不写业务数据)
    public async Task BindAsync(string sn, string workOrder)
    {
        string tid = await ReadStableTidAsync(TimeSpan.FromSeconds(2));
        var pallet = await _db.Pallets.FirstAsync(p => p.Tid == tid);

        if (pallet.State != PalletState.Free)
            throw new InvalidOperationException(
                $"工装板{tid}仍绑定旧产品{pallet.Sn},禁止上线(需异常解绑)");

        var product = await _trace.GetBySnAsync(sn);
        pallet.Sn = sn; pallet.WorkOrder = workOrder;
        pallet.Model = product.Model; pallet.State = PalletState.Bound;
        pallet.BindAt = DateTime.Now;
        await _db.SaveChangesAsync();
    }

    // ② 工位读校验:本工位只放行"档案允许经过本站"的板
    public async Task<GateResult> VerifyAtStationAsync(string stationCode)
    {
        string tid = await ReadStableTidAsync(TimeSpan.FromSeconds(2));
        var pallet = await _db.Pallets.FirstOrDefaultAsync(p => p.Tid == tid);
        if (pallet == null || pallet.State != PalletState.Bound)
            return GateResult.Ng("未绑定的工装板,停线");

        var route = _routes[pallet.Model];
        if (route.Model != pallet.Model)
            return GateResult.Ng("型号与工位路由不符,疑似混线");
        if (!route.CanEnter(stationCode, pallet.PassedStations))
            return GateResult.Ng("跳站/回流错序,禁止过站");  // 防错逻辑同121篇过站闸门

        pallet.PassedStations.Add(stationCode);
        await _db.SaveChangesAsync();
        return GateResult.Ok(pallet.Sn, pallet.Model);
    }

    // ③ 下线解绑:状态回Free,TID不变,载码体直接可循环
    public async Task UnbindAsync(string tid)
    {
        var pallet = await _db.Pallets.FirstAsync(p => p.Tid == tid);
        pallet.State = PalletState.Free; pallet.Sn = null;
        pallet.PassedStations.Clear();
        await _db.SaveChangesAsync();
    }
}

五、漏读、抖动与串读:读得稳比读得到更重要

RFID 现场三大不稳定:到位瞬间场还没建立导致首次盘点空、板移动中 TID 时有时无、相邻工位读写头互相读到。软件和安装两端同时收敛:

// 稳定读取去抖:连续N次盘点到同一个唯一TID才采信;
// 场内出现2个以上TID直接判异常(工装板背靠背或串读)
private async Task<string> ReadStableTidAsync(TimeSpan timeout)
{
    var deadline = DateTime.Now + timeout;
    string last = null; int hits = 0;
    while (DateTime.Now < deadline)
    {
        var tids = _reader.Inventory();
        if (tids.Count > 1)
            throw new Exception("场内多标签:检查工装板间距或读写头串读");
        if (tids.Count == 1 && tids[0] == last)
        {
            if (++hits >= 3) return last;   // 连续3次一致,采信
        }
        else { last = tids.FirstOrDefault(); hits = 0; }
        await Task.Delay(60);
    }
    throw new TimeoutException("读标超时:标签损坏/位置偏移/读写头故障");
}
问题安装/硬件手段软件手段
首读漏读读写头装在到位传感器后方,板停稳才处于场区中心到位信号触发后延时 100~200ms 再盘点,失败短重试
读值抖动标签安装位置全板统一,读写头距离按手册固定连续多次同一 TID 去抖后采信
相邻工位串读降低发射功率、加屏蔽/吸波材料、窄场天线、读写头间距拉开到位传感器门控,非到位时段不盘点;多 TID 报警
金属工装板必须抗金属标签(带隔离层/螺钉式),实物打样建立标签档案,连续读不到的板自动报修

六、必须写用户区时:卡库一致性怎么保证

少数场景(板要离开本线上外协、跨产线读取)必须把简码写进载码体。写操作遵循"先落库、再写卡、写后回读校验、失败补偿"

public async Task WriteBoardCodeAsync(string tid, int block, byte[] code)
{
    // 1) 库内先记"待写"状态——即便此刻断电,重启补偿任务也知道这张卡该写什么
    await _db.InsertPendingWriteAsync(tid, block, code, WriteState.Pending);

    bool ack = await Task.Run(() => _reader.WriteBlock(tid, block, code));
    if (!ack) { await MarkRetryAsync(tid); throw new Exception("写卡无ACK"); }

    // 2) 回读比对:ACK不等于真的写进去(场弱、标签寿命、干扰都可能假成功)
    var back = _reader.ReadBlock(tid, block);
    if (!back.SequenceEqual(code))
    { await MarkRetryAsync(tid); throw new Exception("写后回读不一致"); }

    // 3) 卡、库两边都确认后才置完成
    await _db.SetWriteStateAsync(tid, WriteState.Done);
}

// 后台补偿服务:周期扫描 Pending/Retry 记录重写并回读;
// 超过重试次数告警并把该工装板锁定,禁止继续流转。
反过来的顺序(先写卡后落库)有个致命窗口:卡写成功、写库前断电,重启后库里没有这张卡的新码,谁也说不清。先落库留痕,卡上数据永远可以靠补偿任务重放补齐。载码体写寿命约十万次量级,频繁改写同一批块会提前损耗——这也是业务数据尽量不写卡的另一个理由。

七、RFID 与条码激光码:不是替代,是分工

  • 产品单品标识用条码/激光 DPM(113、124 篇):成本低、可视可抄、供应商和客户端都能读,随产品出厂;
  • 工装载具循环标识用 RFID:可擦写复用、不怕油污遮挡、不需要对准视线,随板留厂循环;
  • 两者在上线工位"相认"(扫产品 SN + 读板 TID 绑定),之后各工位既能读到板的档案,产品过站记录仍挂在单品 SN 下——这正是 121 篇追溯主链在输送线上的落地形态。

八、上线验收清单

  1. 读距实测:工装板以线速通过,各工位连续 200 次无漏读、无误读,背靠背两板不出现多 TID;
  2. 串读验证:相邻工位同时通电,板停在任一站时只有本站读写头读到,隔壁站零误触发;
  3. 绑定闭环:上线绑定后各站型号/工序校验准确,混放异型号板能被拦截停线;
  4. 回流检查:异常下线的板未解绑时重新上线,系统拒绝绑定并提示旧 SN,残绑定不会污染新产品;
  5. 写卡一致:若启用用户区写入,拔电/断网注入故障后补偿任务能补齐,回读 100% 一致;
  6. 运维兜底:坏板读不到时报警明确(区分"无板"与"有板读不到"),标签档案支持更换载码体后重新登记。

RFID 项目的成败七分在现场、三分在软件:频段和标签按实物打样、读写头场界靠安装收敛,上位机则守住稳定读取去抖、只认 TID、业务入库、卡库双写留痕、解绑闭环这五条。做到这些,每一块工装板走到哪个工位、载着什么产品、漏没漏工序,系统比线长还清楚——这就是"看不见的流动"被彻底管住的样子。