做安环能管控项目,最基础也最容易出错的一环就是抄表。工厂里的多功能电能表几乎清一色走 DL/T 645-2007 规约(老表是 1997 版,帧结构相似但数据标识不同,本文讲 2007 版),RS485 总线一条挂十几块表。很多人第一次接会被三个细节绊倒:地址字节序是反的、数据域整体加了 33H、电量值还要乘 CT/PT 倍率。这篇把协议帧、组帧解析代码、总线调度和能耗对账一次讲透。

一、RS485 组网:总线拓扑比代码更重要

抄表不稳,八成是物理层问题。组网硬规矩:

  • 手拉手菊花链拓扑,严禁星形分叉;总线两端各并一个 120Ω 终端电阻;
  • A/B 全总线统一:所有表的 A 接 A、B 接 B,中途换颜色是乱码和抄不到的主要原因;
  • 波特率先统一:老表默认 2400,新表多为 9600,8E1(8 数据位、偶校验、1 停止位)是 645 的标准串口格式——校验位是 Even,不是 None
  • 表地址台账:每块表的 12 位通信地址(表壳条码或表内地址)在安装时就录入配置,别用广播地址抄多表——广播帧所有表同时应答,总线直接撞车;
  • 一条总线挂 16 块表以内、总长 600 米以内最稳妥;超了加分段和中继。

二、协议帧结构:十个字节域逐个认

68H帧起始
A0~A5地址域6字节
68H帧起始
C控制码
L数据长度
DATA数据域(+33H)
CS校验和
16H帧结束

▲ 读数据请求帧 C=11H,正常应答 C=91H;异常应答 C=91H 且数据域为错误码(如 08H 无数据)。

三个最反直觉的约定,必须先记住:

  1. 地址域低字节在前、BCD 码、不足补 AAH。表号 000012345678,线上字节顺序是 78 56 34 12 00 00;6 位表号则高字节补 AA;
  2. 数据域每个字节发前 +33H、收后 −33H(规约叫"加 33H 处理",目的是让数据域避开 68H/16H 等帧界定符);
  3. 数据标识 DI3DI2DI1DI0 四字节,线上顺序 DI0 在前,同样每字节 +33H。

三、常用数据标识:抄表真正用到的就这几个

数据项DI3 DI2 DI1 DI0说明
(当前)组合有功总电能00 00 00 00最常用,正向+反向合计费的总示值
费率1~4 电能00 00 01~04 00对应尖、峰、平、谷四个费率(按表内费率表)
A/B/C 相电压02 01 01/02/03 00实时相电压,单位 V
A/B/C 相电流02 02 01/02/03 00实时相电流,单位 0.01A
总有功功率02 03 00 00实时功率,kW,带符号(负为反向送电)
总功率因数/频率02 05/06 00 00电能质量监测用
上1~12结算日电能04 00 01~0C 00电表冻结的历史月度底数,对账神器

四、C# 组帧与解析:50 行核心代码

// 组"读数据"请求帧(C=11H)。meterAddr: 12位表号字符串,di: 4字节标识(逻辑顺序)
static byte[] BuildReadFrame(string meterAddr, byte[] di)
{
    var bytes = new List<byte> { 0x68 };
    // 地址:BCD低字节在前,不足6字节补0xAA
    var addr = new byte[6];
    for (int i = 0; i < 6; i++)
        addr[i] = i * 2 + 2 <= meterAddr.Length
            ? Convert.ToByte(meterAddr.Substring(meterAddr.Length - 2 - i * 2, 2), 16)
            : (byte)0xAA;
    bytes.AddRange(addr);
    bytes.Add(0x68); bytes.Add(0x11);
    var data = new List<byte>();
    for (int i = 3; i >= 0; i--) data.Add((byte)(di[i] + 0x33)); // DI0先发, +33H
    bytes.Add((byte)data.Count); bytes.AddRange(data);
    byte cs = 0; for (int i = 1; i < bytes.Count; i++) cs += bytes[i];
    bytes.Add(cs); bytes.Add(0x16);
    return bytes.ToArray();
}

// 解析应答电能数据:数据域前4字节是回送的DI,其后为BCD数值(低字节在前)
static double ParseEnergy(byte[] data, int decimals)
{
    long raw = 0;
    for (int i = data.Length - 1; i >= 4; i--)  // 跳过4字节DI,逆序拼BCD
    {
        var b = (byte)(data[i] - 0x33);                // 减33H还原
        raw = raw * 100 + ((b >> 4) * 10 + (b & 0x0F));  // 每字节两位BCD
    }
    return raw / Math.Pow(10, decimals);               // decimals:小数位(常见2位)
}
小数位和倍率是两件事,别混。小数位由数据标识定义(电能常见 2 位小数,即 0.01kWh);倍率是计量回路的 CT 变比 × PT 变比(如 200/5 电流互感器=40 倍,低压无 PT 取 1)。实际一次侧电量 = 表内示值 × CT × PT。倍率写进每块表的配置台账,绝不在代码里写死。

五、抄表调度:轮询表 + 三档周期 + 失败退避

能耗采集和 PLC 轮询不同——电量是累计量,不怕漏一两次,怕的是把总线打瘫。我们的调度设计:

  • 三档周期:电压/电流/功率 5 分钟抄一次(实时监测够了),电能示值 15 分钟一次,结算日冻结值每月 1 号抄一次;
  • 串行不并发:总线同一时刻只发一帧,发完等应答(超时 600ms)再发下一帧,无应答重试 2 次,仍失败标记该表离线、跳过本轮——一块表掉线不能拖住全总线;
  • 唤醒前导:给休眠型表发帧前先发 2~4 个 FEH 唤醒字节;帧与帧之间留 30~50ms 总线空闲,符合规约的帧间静默要求;
  • 轮询周期天然错过整点(比如 xx:03、xx:18 抄),避免多台主机/多套系统同时抢总线。

六、能耗核算:示值差值法 + 月度冻结对账

能耗统计不用差值积分,直接示值做差:某时段用电量 = 时段末示值 − 时段初示值(乘倍率后)。三个工程细节:

  1. 换表/清零要登记底数:示值突降只可能是换表或清零,程序检测到示值回退自动标记,必须由管理员录入新表底数后才继续核算,不能算出负电量;
  2. 月度对账用表内冻结值:电表自己冻结的结算日示值是法律/结算意义上的"底数",上位机每月抄回,与自家 15 分钟示值链算出的月度合计比对,偏差超 0.5% 触发核查——这条能同时防抄读错误和表计异常;
  3. 尖峰平谷按费率抄:4 个费率电能分别抄回做分时报表,前提是电表内时段表与当地峰谷政策一致,投运前用电表菜单逐项核对时段;4 个费率之和应等于总电能,不等就是费率表配置错了。
水表、气表同理不同规。水表常用 CJ/T 188,燃气表也走 188 或 645 扩展,帧结构思路(地址+标识+BCD+校验)相通,差在校验算法和数据标识。先拿到设备厂家的点表/协议附页再写代码,规约给框架,厂家附页给每个标识的小数位和单位——能耗项目最值钱的资料就是那张点表。

能耗采集是典型的"慢系统":频率不高、数据不大,但口径必须绝对准确。协议解析写错一个字节序,整厂的能耗报表就全是错的。上线前的验收方法很朴素:选三块表,人工读表底数和系统抄回值对三天,完全一致再放开全量——抄表这件事,没有"差不多"。