简介:DLMS协议库.rar 面向智能电表、水表通信开发及协议移植人员,提供一套精简的DLMS协议实现代码,用于理解对象模型、报文交互与通信流程,并服务于嵌入式项目中的协议对接与一致性验证。压缩包共2个文件、约41KB,由1个C源文件和1个头文件组成,分别承载接口声明与核心实现,体积虽小但便于查阅结构、裁剪逻辑和对照调试。相关代码已在欧盟及东南亚多个国家的电表、水表中批量应用,并通过CCT测试、取得DLMS证书,同时满足MID、KEMA认证和最新CTT测试规范,可作为认证准备与协议测试的参考样本。其内容覆盖DLMS协会蓝皮书、绿皮书、黄皮书所关注的核心机制,适合开发者结合规范梳理服务接口、数据对象与通信状态。目前已有1107人学习,适合需要快速熟悉DLMS框架、积累协议实现经验的中高级嵌入式开发者收藏研读。
1. 一台电表的应答为什么“时有时无”:DLMS 协议库能替你做什么
现场联调最让人摸不着头脑的一幕,就是同一块表、同一根 485 线,上一条命令回了,下一条就没影。抓包看着字节都对,程序却解不出电量——十有八九是 DLMS 帧里的 FCS 算错、转义漏了,或者 OBIS 寻址少了一个字节。这份 DLMS 协议库处理的就是这种事:把 HDLC 成帧、A-XDR 编解码、COSEM 对象寻址、关联认证这些重复劳动封装成一组 C 接口,你只需要调用一个“读属性”的函数。它不适合想研究协议规范的人,适合已经拿着表、要在一两周内把采集程序跑起来的一线开发者;从完整性看,算得上值得珍藏的通信协议库。
2. 协议栈定位与技术选型:DLMS 库在抄表链路里的边界在哪里
读源码前先得把边界划清楚,否则会写出一堆“看起来能跑、换个表就失灵”的代码。DLMS/COSEM 是一套分层的计量通信协议,从上到下依次是应用层(COSEM 对象与 APDU)、表示层(A-XDR 编码)、链路层(HDLC 或 Wrapper),再往下才是 RS-485、红外、TCP 这类物理通道。这份库覆盖的是前三层,第四层要你自己按实际硬件去接线。
2.1 分层:一条命令要穿过四层
以最典型的“读正向有功总电能”为例,一次成功交互在协议上的实际路径是这样的:应用层先确定要读哪个对象,也就是 class_id=3(Register)、instance_id=1.0.1.8.0.255、attribute_id=2(value);表示层把这三个字段按 A-XDR 打包成 Get-Request APDU;链路层再给它套上 HDLC 帧头、地址、控制字、HCS/FCS,中间还要插一段 LLC 头(通常是E6 E6 00)告诉对端“上层是 xDLMS”;最后串口把这些字节一个个推出去。
很多新手在这里出错,是因为直接把 APDU 往串口塞,忘了 LLC 和 HDLC 两段。对端收到后,它按 HDLC 解析,会因为控制字节对不上而直接丢帧,现象就是“发出去了、没回应、也不报错”。库的价值就在这:dlms_client_*这一层调完,帧的拼装、转义、校验、重发、超时全都是内部做完的。
分层还有个实际好处是换通道不用改业务代码。同一套 COSEM 对象访问逻辑,底下从 HDLC 换成 Wrapper(TCP 上跑 DLMS,多为集中器与主站之间的场景),上层调用几乎不变,只是 link 结构体里的 type 字段换一下,端口号和地址填法跟着变。
2.2 解包之后:目录结构与模块职责
压缩包解开,目录一般呈现为“源码 + 例程 + 文档 + 移植层”四大块的结构,我按职责列一遍,方便你按图索骥去找要改的地方:
dlms/ ├── src/ │ ├── hdlc.c / hdlc.h HDLC 成帧、FCS、转义、地址与控制字 │ ├── wrapper.c TCP/UDP 上的 Wrapper 头封装 │ ├── axdr.c / axdr.h A-XDR 基本类型编解码与长度前缀 │ ├── apdu.c Get/Set/Action 请求应答的组装与解析 │ ├── cosem.c COSEM 类表:Register/Profile/Clock 等 │ ├── obis.c OBIS 六字节与压缩形式的互转 │ ├── association.c SNRM/AARQ/AARE/Release 会话管理 │ ├── security.c LLS 口令与 HLS 挑战应答 │ └── client.c / server.c 客户端主流程 / 从站应答桩 ├── port/ │ ├── port_serial.c 串口收发与波特率切换 │ └── port_timer.c 超时与重发计时 ├── examples/ │ ├── read_register.c 最小读表例程 │ └── virtual_meter.c 虚拟从站,离线自测用 └── docs/ 帧格式说明与对象清单| 模块 | 主要数据结构 | 职责 | 改动频率 |
|---|---|---|---|
| hdlc | hdlc_ctx_t | 帧同步、FCS、字节转义、分段 | 低 |
| axdr | dlms_value_t | 类型标签分派、大端取值 | 中(扩类型时) |
| cosem/obis | cosem_obj_t | 对象寻址与属性映射 | 高(换表就要改) |
| association | dlms_client_t | 建链、认证、释放 | 低 |
| port | 函数指针 | 收发与计时 | 高(换板子就要改) |
这张表是我判断“该不该动它”的依据:hdlc和axdr属于协议骨架,除非你对端设备真的实现了私有扩展,否则不建议改动;cosem/obis和port才是日常要动的地方。我把这条经验总结成一句话:骨架不动,两头改。
2.3 编译链与依赖:纯 C 的取舍
库本身基本不依赖第三方,常见做法是只使用 C99 标准库的stdint.h、string.h、stdlib.h,把内存分配做成可配置——要么用静态数组池,要么注册malloc/free钩子。这个取舍对嵌入式很重要:很多计量 MCU 的 RAM 只有几十 KB,动态分配一多就碎片化,跑几天就崩。我在板子上一般把接收缓冲开成 512 字节的静态数组,发送缓冲 256 字节,够跑单帧 I 帧和普通的 Get-Response。
编译有两种常用方式:
# 方式一:直接编静态库,主程序链接 gcc -c src/*.c -Iinclude -DPORT_LINUX -O2 ar rcs libdlms.a *.o gcc examples/read_register.c -Iinclude -L. -ldlms -o read_register # 方式二:带调试信息跑虚拟从站,先把协议跑通再接硬件 gcc -g -O0 src/*.c examples/virtual_meter.c port/port_serial.c -Iinclude -o vmeter-DPORT_LINUX这类宏是给port层做条件编译用的:Linux 下走 termios,裸机上走寄存器。你要移植到新平台,第一步就是port_serial.c里那两个函数——serial_open()和serial_write(),把它实现完,剩下的协议代码几乎不需要改动。这也是我比较看重这个库的原因:移植点集中,不用在几千行代码里找依赖。
3. 核心链路拆解:一条 Get-Request 从组帧到取值
这一章是重点,按“帧 → APDU → 对象 → 会话”的顺序走一遍,每一步都落到代码上。如果你只想先跑通,可以先跳到第 4 章,回头再看这里。
3.1 HDLC 成帧、转义与 FCS 校验
HDLC 帧结构是固定套路:起始标志0x7E,接着帧格式字段、目的地址、源地址、控制字、头校验 HCS、LLC 头、APDU 载荷、帧校验 FCS、结束标志0x7E。其中 FCS 用的是 CRC-16/X.25,多项式0x1021反序成0x8408,初值0xFFFF,结果取反后低字节先发。
/* hdlc.c —— FCS 计算,很多"偶发丢帧"就是这里移位方向写反了 */ static uint16_t hdlc_fcs16(const uint8_t *buf, uint16_t len) { uint16_t fcs = 0xFFFF; while (len--) { fcs ^= *buf++; for (int i = 0; i < 8; i++) fcs = (fcs & 0x0001) ? ((fcs >> 1) ^ 0x8408) : (fcs >> 1); } return (uint16_t)(~fcs); /* 取反后低字节先发 */ } /* 转义:0x7E、0x7D 和所有小于 0x20 的字节都要逃,方法一律是 ^0x20 */ static uint16_t hdlc_escape(const uint8_t *in, uint16_t in_len, uint8_t *out) { uint16_t n = 0; for (uint16_t i = 0; i < in_len; i++) { if (in[i] == 0x7E || in[i] == 0x7D || in[i] < 0x20) { out[n++] = 0x7D; out[n++] = (uint8_t)(in[i] ^ 0x20); } else { out[n++] = in[i]; } } return n; }hdlc_fcs16只有一个参数需要关注:len是参与校验的字节数,注意它不包括起始和结束标志,但包括帧格式、地址、控制字和载荷。HCS 单独算一次,覆盖范围只到控制字为止,这是最容易漏的一步——漏了 HCS,对端会在链路层直接把帧丢掉,你的抓包工具看到的却是“帧完整”。
hdlc_escape的两个条件不能少任何一个。只转义0x7E,那么数据里恰好出现0x7D时,接收端会把它当成转义引导符,吞掉下一个字节,表现为“长度对不上、偶发错帧”;而不转义小于0x20的控制字符,在某些串口驱动里会被当成流控或特殊字符吃掉。我曾在一个现场排查了三天,最后发现是地址里有个0x0D没转义。
组帧时还有个细节是长度字段的位宽:帧格式字段0xA0表示用 2 字节表示帧长,载荷短的话可以用 1 字节的0xA0变体。写的时候建议统一用 2 字节,省得临界长度上出问题。
3.2 APDU 与 A-XDR:类型标签决定怎么切字节
APDU 是应用层报文,Get-Request 的字节序列是:C0 01 81 <invoke-id> 05 <class-id> <instance-id> <attr-id> 00。逐段解释一下:C0是 Get-Request 的标签,01表示 normal(普通读,区别于02的 next 分块读),81是 invoke-id-and-priority,后面那一字节是递增的调用号,用来把应答和请求对上;05是 cosem-attribute-descriptor 的标签,然后依次是类 ID、实例 ID、属性 ID,最后的00表示不带选择性访问。
实例 ID 这里有个坑:完整的 OBIS 是六字节,但当 A=1、B=0、F=255 时可以压缩成两字节(只保留 C 和 E),比如 1.0.1.8.0.255 发出去就是01 08。我以前就是这样出错的——手动拼包时按六字节发,对端理解成别的对象,返回的应答看着正常,数值却是另一个量测点。
应答回来的是 A-XDR 编码的数据,必须按类型标签分派解析:
/* axdr.c —— 按标签解数据,标签错一位后面全乱 */ int axdr_decode(const uint8_t *in, uint16_t in_len, uint16_t *pos, dlms_value_t *out) { if (*pos >= in_len) return -1; uint8_t tag = in[(*pos)++]; switch (tag) { case 0x00: /* null */ out->type = DLMS_NULL; return 0; case 0x06: /* double-long-unsigned,4 字节大端 */ if (*pos + 4 > in_len) return -1; out->type = DLMS_U32; out->u32 = ((uint32_t)in[*pos] << 24) | ((uint32_t)in[*pos + 1] << 16) | ((uint32_t)in[*pos + 2] << 8) | (uint32_t)in[*pos + 3]; *pos += 4; return 0; case 0x09: { /* octet-string,长度前缀 + 内容 */ uint8_t len = in[(*pos)++]; if (*pos + len > in_len) return -1; out->type = DLMS_OCTETS; out->octets.ptr = &in[*pos]; out->octets.len = len; *pos += len; return 0; } case 0x11: /* unsigned,1 字节 */ out->type = DLMS_U32; out->u32 = in[(*pos)++]; return 0; default: return -2; /* 未支持的类型,交上层记账 */ } }函数签名里pos是读写游标,这个设计很关键:一个 Get-Response 可能带多个结果,或者本身是个 array,递归解的时候全靠游标推进,不能靠返回指针。返回-2表示类型不认识,这个分支千万别直接return 0当成功——我见过因为忽略它,把 octet-string 当数字读,最后电量显示成几亿度。
还有一类常见编码是0x01array 和0x02structure,这俩都是“长度前缀 + N 个元素”,数组元素本身又是完整的一段 A-XDR,所以必须递归调用自己。写的时候记得对长度做上限保护,否则对端发个长度 200、实际只有 3 字节的畸形帧,你的解包会越界。
3.3 OBIS 与 COSEM:class_id + attribute_id 的寻址表
COSEM 把每一类数据抽象成“类”,每个对象由 OBIS 唯一定位,属性下标决定读的是值、单位还是标度。实际项目里最常碰的几个类如下:
| class_id | 类名 | 典型 OBIS 实例 | 常用属性 |
|---|---|---|---|
| 1 | Data | 0.0.42.0.0.255(设备逻辑名) | 2 = value |
| 3 | Register | 1.0.1.8.0.255(正向有功总) | 2 = value, 3 = scaler_unit |
| 4 | Extended Register | 1.0.1.8.1.255 | 2 = value, 5 = capture_time |
| 7 | Profile Generic | 1.0.99.1.0.255(负荷曲线) | 2 = buffer, 7 = entries |
| 8 | Clock | 0.0.1.0.0.255 | 2 = time |
读 Register 拿到的不只是一个数,属性 3 里的scaler_unit是个 structure,里面第一个是 scale(整数),第二个是 unit(枚举)。真实电量 = 原始值 × 10^scale。这一步不算,结果就差着量级,也是电量读出巨大数值的主要原因。
Profile Generic 的读法和普通属性不一样,要用带选择性访问的 Get-Request,指定起始索引和条数,返回的是一个 array of structure。这里建议先读属性 7(entries)拿到总条数,再分段读,一次拉几百条会超过单帧长度被对端分成多帧,你还得实现分块应答拼接。库里有dlms_get_profile()这类封装,具体函数名各版本略有差异,翻一下cosem.c里 Profile 那一段就知道。
3.4 安全层:LLS 与 HLS 往哪插
会话建立时,AARQ 里的mechanism-name决定用哪种认证。最常见的是 LLS(低等级安全),本质就是把口令当 octet-string 塞进 calling-authentication-value,实现成本几乎为零:
dlms_client_t cli; dlms_link_t link = { .type = DLMS_LINK_HDLC, .addr_client = 0x10, /* 客户端地址,抓包里的源地址 */ .addr_server = 0x01, /* 表地址,低地址先发 */ .baudrate = 9600, /* 老表多是 9600,新表常见 2400 或 9600 */ }; dlms_client_init(&cli, &link, port_serial_write, port_serial_read); dlms_client_set_lls(&cli, "00000001"); /* 对口令,不是明文密码文件 */ int rc = dlms_client_connect(&cli); /* SNRM -> UA -> AARQ -> AARE */ if (rc != DLMS_OK) { printf("associate failed: %d\n", rc); /* 负值基本是链路,正值多是认证 */ return rc; }dlms_client_set_lls的第二个参数要按对端口令的实际长度传,别硬补零;有些实现对长度为 0 的口令会直接拒绝。返回值约定建议自己统一:负数表示链路层失败(超时、FCS 错),正数表示会话层失败(认证失败、一致性块不满足),这样排障时看符号就知道往哪层查。
HLS(高等级安全)要复杂一些:服务端先下发一段随机挑战(stoc),客户端用事先约定好的密钥按算法算出应答(ctos)回过去,部分实现还要再多一轮。算法一般是 MD5、SHA-1 或 HMAC 系列,密钥由主站侧下发。这一步要求你确认三件事,缺一个都过不去:密钥是不是按十六进制解出来的、挑战是几段、算法是哪一个。现场最常出现的情况是密钥正确、算法用错,然后表现为“AARE 返回认证失败”却查不出原因。
4. 跑起来:从编译到用虚拟表验证第一帧
协议库这类东西,最怕一上来就接真表。我的习惯是先让它在没有硬件的环境下跑通一轮完整交互,再去现场。
4.1 环境准备与最小可运行工程
Windows 上装个 gcc 环境就能编,Linux 一条命令的事。编译前确认两件事:include/目录加到搜索路径里,port层选对平台宏。下面是最小可运行例程的主干:
#include "dlms_client.h" #include "obis.h" #define OBIS_ACTIVE_ENERGY 0x010800FFUL /* 1.0.1.8.0.255,正向有功总 */ int main(void) { dlms_client_t cli; dlms_link_t link = { DLMS_LINK_HDLC, 0x10, 0x01, 9600 }; if (dlms_client_init(&cli, &link, port_serial_write, port_serial_read) != DLMS_OK) return -1; if (dlms_client_connect(&cli) != DLMS_OK) /* 建链 + 关联 */ return -2; uint32_t raw = 0; int rc = dlms_get_u32(&cli, 3, OBIS_ACTIVE_ENERGY, 2, &raw); if (rc == DLMS_OK) { int8_t scale = 0; dlms_get_scaler(&cli, 3, OBIS_ACTIVE_ENERGY, &scale); /* 读属性 3 */ printf("energy = %.3f kWh\n", raw * pow(10, scale)); } dlms_client_disconnect(&cli); /* DISC -> DM */ return rc; }dlms_get_u32的五个参数分别是:客户端句柄、class_id、OBIS 压缩值、attribute_id、输出缓冲。OBIS 那个宏是把 C 和 E 两字节拼成0x0108再补00FF后缀,读的时候按库里的obis_encode()反查一下更稳妥,别自己手拼。
编译跑起来如果串口没接东西,会卡在dlms_client_connect里等到超时。想先验证逻辑,把port_serial_write/read换成虚拟从站的收发函数即可,见 4.3。
4.2 建立关联:SNRM、AARQ 与一致性块
一次完整的会话建立是四步:发 SNRM 协商链路参数(最大信息长度、窗口大小),收到 UA 后链路才算通;接着发 AARQ 提出应用层关联,里面带 xDLMS-InitiateRequest,声明自己要用的服务;对端回 AARE,里面有个 conformance block,告诉你它支持哪些服务。
一致性块(conformance)常被忽略,但它决定你能不能读某些对象。比如你想用get的 multiple references 一次读多个属性,位没协商上,AARE 表面成功,后续请求全被拒。我在代码里一般加一段自检:
uint32_t conf = dlms_client_conformance(&cli); if (!(conf & DLMS_CONF_GET)) /* 连最基本的 get 都没协商上 */ return -3; if ((conf & DLMS_CONF_MULTIPLE_REFS) == 0) printf("warn: multiple references unsupported, fallback to single get\n");DLMS_CONF_*这组宏对应一致性块里的各个位,具体掩码位置照库里头文件抄,别自己数位数。这段自检的用处是把“后面莫名失败”提前暴露成“建链时就知道能力不足”,省掉大量猜谜时间。
此外 SNRM 里协商的最大信息长度也要留意。默认值太小(有些实现默认 128),你会频繁遇到一帧装不下、被拆成多帧的情况,而分块重组是另一套逻辑。把dlms_client_set_max_info_len(&cli, 512)显式设一下,多数表的支持都不成问题。
4.3 用虚拟从站做回环验证
包里通常带一个virtual_meter.c,它扮演从站,在你本机内存里造几个 COSEM 对象,收发走本地回环或者命名管道。用法是把客户端和它编到一个进程里,一收一发直接对接,跳过所有硬件:
# 虚拟从站与客户端跑在一个进程,业务侧不用改一行 gcc -g src/*.c examples/virtual_meter.c examples/read_register.c -Iinclude -o loopback ./loopback --server-addr 1 --client-addr 16 --obis 1.0.1.8.0.255跑通的标准是日志里能看到四件事:SNRM/UA 完成、AARQ/AARE 完成、Get-Response 里 result 字段是00(成功)、最终打印的电量数值合理。这四步任何一个没到,就说明协议栈内部还有问题,先别接硬件。
虚拟从站还有个更实用的功能:故意返回畸形数据。比如把某个对象的类型标签改成0x09却只发两字节,看客户端会不会越界;把 FCS 改错一位,看重发机制是否按预期触发。这套破坏性测试做完,再去现场会省下大把时间。
4.4 移植:串口与定时器两个钩子
真正换板子做的时候,要动的只有port层。串口那边要实现的是一个“收够一帧返回”的函数,以及一个“按字节发”的函数。收的难点在帧边界,建议用状态机:等待0x7E,进入帧内,遇到0x7D置转义标志,遇到下一个0x7E结束;同时用空闲超时兜底(一帧内超过 20 ms 没有新字节就认为帧结束),这比只靠长度判断稳。
定时器那边给它一个毫秒级时间源就行,dlms_client内部靠它算重发间隔和应答超时。我一般把应答超时设成 1200 ms、重发 2 次,485 上跑 9600 波特率时这个值比较稳;红外口慢,得放宽到 3000 ms。这两个参数在dlms_client_set_timing()里改,别去改内部宏。
5. 避坑与排查:联调阶段最费时间的五个问题
下面这几条都是从实际现场整理出来的,按现象、原因、解决三段写,能对上号就别绕远路。
5.1 链路层:偶发丢帧与“半帧”
现象:连续读 100 次,成功 80 多次,失败的没有任何规律;抓包工具显示帧完整,对端就是不回。
原因:一是 FCS 计算范围错了,把起始标志或者 LLC 头算进去了;二是转义不完整,数据里恰好出现0x7D被对端当成引导符;三是半双工侧收发方向切换太快,最后一字节还没走完就切到接收,末位被削掉。
解决:把hdlc_fcs16单独写个单测,用一组已知帧验证;转义函数用全字节遍历测一遍,0x00到0xFF都过一遍再拼帧;方向切换加 1~2 ms 延时,或者读串口的 TC(发送完成)标志而不是只等 TXE。
现象:一上电前几帧能通,跑几分钟后彻底静默。
原因:接收缓冲没清零,跨帧累积;或者帧长度字段被上一帧残留影响,游标越走越偏。
解决:每次收到起始标志就把游标和缓冲长度重置,别依赖“上次收到的长度”。
5.2 会话层:认证过了却读不到数据
现象:AARE 返回成功,紧接着的第一个 Get-Request 被拒,返回的 result 是0x01(hardware fault)或0x03(read-write denied)。
原因:LLS 口令对上了但关联上下文里没有对应的访问权限;有些表要求先读 Association LN 对象(class 15)里的object_list,按里面的 access 权限来判;还有一种是一直用同一个 invoke-id,被对端判为重放。
解决:invoke-id 每次请求都递增,回绕后从 1 重新开始;确认关联时用的是不是正确的 association 对象(公共客户端和读表客户端是两个不同的实例);把 AARE 里返回的result-source-diagnostic打出来,它能直接告诉你到底被拒在哪一步。
现象:建链时好时坏,AARQ 经常超时。
原因:SNRM 的 UA 还没收完就开始发 AARQ,两帧在同一段总线上撞了。
解决:严格串行,收到 UA 再发 AARQ;中间留 50 ms 间隔,别图快。
5.3 数据层:电量读出天文数字
现象:读回来的值是4294967295或者一个几亿度的数。
原因:一是把 octet-string 当数字解了,四字节内容拼出来正好是上限值;二是没乘 scaler,原始值直接当 kWh 用;三是字节序搞反,大端值按小端读。
解决:先看类型标签再取数,0x06才能按 U32 解,0x09一律按缓冲区处理;scaler 一定要读属性 3 并参与计算;大端小端的判断写个已知值(比如0x01020304)的自测用例,别猜。
现象:负荷曲线读出来条数是 0,或者一堆 0x00。
原因:Profile Generic 要用选择性访问,直接读属性 2 拿到的是空 buffer;或者读的起始索引从 0 开始,而协议里是从 1 开始。
解决:先读属性 7(entries)确认条数,再用带 from/to entry 的选择性访问去拉;索引从 1 起算。
6. 二次开发:把库接到自己的采集程序里
直接改库源码是最不明智的做法,我一般是在外面包一层采集适配,把“表参数、重试策略、数据落地”从协议层剥离开。这样换表、换协议版本时只改适配层。
6.1 加一层采集适配
适配层的核心是一张点位表加一个批量读函数。点位表描述“要读什么、怎么换算、存到哪”,批量读函数负责串行调度和失败重试:
typedef struct { uint8_t class_id; uint32_t obis; /* 压缩后的 OBIS,按库的 obis_encode() 生成 */ uint8_t attr_id; int8_t scaler; /* 从属性 3 读回来的,缓存住免得每次重读 */ uint32_t last_val; uint8_t fail_cnt; } meter_point_t; int collect_once(dlms_client_t *cli, meter_point_t *pts, int n) { int ok = 0; for (int i = 0; i < n; i++) { uint32_t v = 0; int rc = dlms_get_u32(cli, pts[i].class_id, pts[i].obis, pts[i].attr_id, &v); if (rc == DLMS_OK) { pts[i].last_val = v; pts[i].fail_cnt = 0; ok++; } else if (++pts[i].fail_cnt >= 3) { dlms_client_disconnect(cli); /* 连续失败就重建会话 */ dlms_client_connect(cli); /* 比死等超时恢复得快 */ } } return ok; }fail_cnt这个计数的意义在于区分“偶发丢帧”和“链路真断了”。前两三次失败只管重试,累计到三次就重建会话——现场经验是,绝大多数通信卡死靠一次断链重连能救回来,比在超时里反复等效率高得多。
改造时还有两个点值得做:一是把dlms_get_scaler的结果缓存进点位表,这个值基本不变,每次重读白白多一轮交互;二是把读到的原始值和采集时间一起落库,出了问题能回溯是哪一轮的数据异常,而不是只看到一个错误数值。
6.2 上线前的验证清单
| 验证项 | 方法 | 不通过的典型表现 |
|---|---|---|
| FCS 与转义 | 单测遍历0x00~0xFF拼帧再解 | 偶发丢帧、对端不回 |
| 会话建立 | 连续建链释放 200 次 | 中途 AARQ 超时 |
| 断线恢复 | 拔插 485 线 20 次 | 恢复后一直超时 |
| 类型健壮性 | 喂畸形标签与超长长度域 | 内存越界、数据错乱 |
| 数值正确性 | 与表计面板读数对拍 | 差 10^n 倍或字节序反 |
这张表我每次交付前都跑一遍,尤其第三项。链路恢复后客户端内部状态没重置,是这类库最常见的问题,多半要在断链回调里把 context 整个清一遍,而不是只关串口。释放内存方面也要注意,A-XDR 解出来的 octet-string 一般是指向接收缓冲的切片,不拥有内存,别去free它。
从那以后我每次接新表,都强制先跑一遍“单点读 + 断链重连”的快速验证,再去纠结数据准不准。协议这东西,能通和能用是两件事,先把通的路径固定下来,剩下的都是细枝末节。希望帮到你。
本文还有配套的精品资源,点击获取