做安环能管控项目,最基础也最容易出错的一环就是抄表。工厂里的多功能电能表几乎清一色走 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 无数据)。
三个最反直觉的约定,必须先记住:
- 地址域低字节在前、BCD 码、不足补 AAH。表号 000012345678,线上字节顺序是 78 56 34 12 00 00;6 位表号则高字节补 AA;
- 数据域每个字节发前 +33H、收后 −33H(规约叫"加 33H 处理",目的是让数据域避开 68H/16H 等帧界定符);
- 数据标识 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 抄),避免多台主机/多套系统同时抢总线。
六、能耗核算:示值差值法 + 月度冻结对账
能耗统计不用差值积分,直接示值做差:某时段用电量 = 时段末示值 − 时段初示值(乘倍率后)。三个工程细节:
- 换表/清零要登记底数:示值突降只可能是换表或清零,程序检测到示值回退自动标记,必须由管理员录入新表底数后才继续核算,不能算出负电量;
- 月度对账用表内冻结值:电表自己冻结的结算日示值是法律/结算意义上的"底数",上位机每月抄回,与自家 15 分钟示值链算出的月度合计比对,偏差超 0.5% 触发核查——这条能同时防抄读错误和表计异常;
- 尖峰平谷按费率抄:4 个费率电能分别抄回做分时报表,前提是电表内时段表与当地峰谷政策一致,投运前用电表菜单逐项核对时段;4 个费率之和应等于总电能,不等就是费率表配置错了。
水表、气表同理不同规。水表常用 CJ/T 188,燃气表也走 188 或 645 扩展,帧结构思路(地址+标识+BCD+校验)相通,差在校验算法和数据标识。先拿到设备厂家的点表/协议附页再写代码,规约给框架,厂家附页给每个标识的小数位和单位——能耗项目最值钱的资料就是那张点表。
能耗采集是典型的"慢系统":频率不高、数据不大,但口径必须绝对准确。协议解析写错一个字节序,整厂的能耗报表就全是错的。上线前的验收方法很朴素:选三块表,人工读表底数和系统抄回值对三天,完全一致再放开全量——抄表这件事,没有"差不多"。
