1. 一句话定位
一个无动态内存、无 OS 依赖 的 DL/T 645-2007 协议栈,主站(客户端)与
从站(电表)双角色同库 ,以非阻塞状态机 为核心、阻塞封装 为便利层,
通过函数指针 HAL 与物理层解耦。
适合的场景:电能表网关、采集终端(从站仿真)、嵌入式表计、PC 侧调试工具。
不适合的场景:需要线程安全、需要厂商私有协议语义解析、需要超越标准重传的
复杂流控的系统(见 §10 非目标)。
2. 30 秒速览
| 维度 | 取值 |
|---|---|
| 语言标准 | C99(CMAKE_C_EXTENSIONS OFF,严格标准) |
| 内存 | 全部静态/栈分配,无 malloc,无全局可变状态 |
| 依赖 | 核心 src/ 零外部依赖;port/ 仅串口参考实现 |
| 角色 | 主站 + 从站 |
| 并发模型 | 单事务、可重入、非阻塞;线程安全由调用方负责 |
| 编译期上限 | DLT645_MAX_DATA = 200(读)、DLT645_WRITE_MAX_DATA = 50(写) |
| DI 目录 | 700 条,由标准附录 A 经 tools/gen_di_table.py 生成 |
| 构建 | CMake ≥ 3.10;支持 find_package / pkg-config / add_subdirectory / 裸链接 |
| 测试 | 3 个独立 main() 程序,含主从内存回环集成测试 |
代码规模(不含生成表与 build/):
| 部分 | 文件 | 行数(约) |
|---|---|---|
| 公共头 | include/dlt645/*.h |
900 |
| 核心实现 | src/dlt645_{codec,frame,master,slave,di}.c |
1 700 |
| 生成表 | src/dlt645_di_table.inc |
705 |
| 串口适配 | port/dlt645_serial.c |
273 |
| 测试 | tests/* |
590 |
| 生成脚本 | tools/gen_di_table.py |
226 |
3. DL/T 645-2007 协议速览
3.1 链路层帧格式
0xFE x4 (前导,可选) 唤醒/同步,接收方可忽略
┌──────┬──────────┬──────┬──────┬──────┬──────────┬──────┬──────┐
│ 68H │ A0..A5 │ 68H │ C │ L │ DATA │ CS │ 16H │
└──────┴──────────┴──────┴──────┴──────┴──────────┴──────┴──────┘
1 6 1 1 1 L 字节 1 1
- 地址 A0...A5 :6 字节 BCD,低字节在前 。
A0是最低两位十进制数字。 - 控制码 C :
D7= 传输方向(0 主站→从站,1 从站→主站)D6= 异常标志D5= 后续帧标志("还有数据")D4..D0= 功能码
- 长度 L:数据域字节数,读数据最大 200。
- 数据域 :每字节
+0x33后上线,接收时-0x33还原。 - CS :从第一个
68H起到数据域末字节的模 256 纵向和。 16H:帧结束,接收端据此确认整帧边界。
3.2 本库支持的功能码
| 功能 | 请求 C | 正常应答 C | 异常应答 C |
|---|---|---|---|
| 广播校时 | 08H |
无应答 | 无 |
| 读数据 | 11H |
91H |
D1H |
| 读后续数据 | 12H |
92H(末帧)/ B2H(续) |
D2H |
| 读通信地址 | 13H |
93H |
(不答) |
| 写数据/编程 | 14H |
94H |
D4H |
| 写通信地址 | 15H |
95H |
(不答) |
| 冻结 | 16H |
96H |
D6H |
| 更改通信速率 | 17H |
97H |
D7H |
| 修改密码 | 18H |
98H |
D8H |
| 最大需量清零 | 19H |
99H |
D9H |
| 电表清零 | 1AH |
9AH |
DAH |
| 事件清零 | 1BH |
9BH |
DBH |
异常应答的数据域是 1 字节错误信息字 (DLT645_ERRBIT_*),例如
0x02 = 无请求数据、0x04 = 密码错。
3.3 地址语义(三个特殊值)
| 值 | 含义 | 本库行为 |
|---|---|---|
999999999999H |
广播 | 从站应答抑制;主站标记 silent 不等回复 |
AAAAAAAAAAAAH |
通配(缩位寻址) | 高位 AA 匹配任意电表;应答回真实地址 |
| 具体 12 位 | 点对点 | 正常匹配 |
主站侧 dlt645_addr_parse() 把字符串按十进制数值 解释,b[0] 存最低两位。
因此 "123456789012" 解析为 b = {12,90,78,56,34,12},与电表面板读取的
数字顺序一致。
3.4 数据标识(DI)
DI 是 4 字节 DI3 DI2 DI1 DI0,线上 DI0 在前(小端) 。本库用
uint32_t 存为 DI3<<24 | ... | DI0,十六进制打印出来与标准附录 A 一致。
以"读正向有功总电能"0x00010000 为例,测试里逐字节断言的完整帧:
68 12 90 78 56 34 12 68 11 04 33 33 34 33 68 16
│ └──── 地址 ────┘ │ │ └── 数据域 ──┘ │ └ 结束
│ │ │ 00+33 01+33 └ 校验和
└ 起始 │ └ 长度 04 00+33 00+33
└ 二次起始 控制码 11
4. 架构总览
4.1 分层
#mermaid-svg-iCnrNM8gWn2pcPYx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iCnrNM8gWn2pcPYx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iCnrNM8gWn2pcPYx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iCnrNM8gWn2pcPYx .error-icon{fill:#552222;}#mermaid-svg-iCnrNM8gWn2pcPYx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iCnrNM8gWn2pcPYx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iCnrNM8gWn2pcPYx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iCnrNM8gWn2pcPYx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iCnrNM8gWn2pcPYx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iCnrNM8gWn2pcPYx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iCnrNM8gWn2pcPYx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iCnrNM8gWn2pcPYx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iCnrNM8gWn2pcPYx .marker.cross{stroke:#333333;}#mermaid-svg-iCnrNM8gWn2pcPYx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iCnrNM8gWn2pcPYx p{margin:0;}#mermaid-svg-iCnrNM8gWn2pcPYx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iCnrNM8gWn2pcPYx .cluster-label text{fill:#333;}#mermaid-svg-iCnrNM8gWn2pcPYx .cluster-label span{color:#333;}#mermaid-svg-iCnrNM8gWn2pcPYx .cluster-label span p{background-color:transparent;}#mermaid-svg-iCnrNM8gWn2pcPYx .label text,#mermaid-svg-iCnrNM8gWn2pcPYx span{fill:#333;color:#333;}#mermaid-svg-iCnrNM8gWn2pcPYx .node rect,#mermaid-svg-iCnrNM8gWn2pcPYx .node circle,#mermaid-svg-iCnrNM8gWn2pcPYx .node ellipse,#mermaid-svg-iCnrNM8gWn2pcPYx .node polygon,#mermaid-svg-iCnrNM8gWn2pcPYx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iCnrNM8gWn2pcPYx .rough-node .label text,#mermaid-svg-iCnrNM8gWn2pcPYx .node .label text,#mermaid-svg-iCnrNM8gWn2pcPYx .image-shape .label,#mermaid-svg-iCnrNM8gWn2pcPYx .icon-shape .label{text-anchor:middle;}#mermaid-svg-iCnrNM8gWn2pcPYx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iCnrNM8gWn2pcPYx .rough-node .label,#mermaid-svg-iCnrNM8gWn2pcPYx .node .label,#mermaid-svg-iCnrNM8gWn2pcPYx .image-shape .label,#mermaid-svg-iCnrNM8gWn2pcPYx .icon-shape .label{text-align:center;}#mermaid-svg-iCnrNM8gWn2pcPYx .node.clickable{cursor:pointer;}#mermaid-svg-iCnrNM8gWn2pcPYx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iCnrNM8gWn2pcPYx .arrowheadPath{fill:#333333;}#mermaid-svg-iCnrNM8gWn2pcPYx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iCnrNM8gWn2pcPYx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iCnrNM8gWn2pcPYx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iCnrNM8gWn2pcPYx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iCnrNM8gWn2pcPYx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iCnrNM8gWn2pcPYx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iCnrNM8gWn2pcPYx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iCnrNM8gWn2pcPYx .cluster text{fill:#333;}#mermaid-svg-iCnrNM8gWn2pcPYx .cluster span{color:#333;}#mermaid-svg-iCnrNM8gWn2pcPYx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iCnrNM8gWn2pcPYx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iCnrNM8gWn2pcPYx rect.text{fill:none;stroke-width:0;}#mermaid-svg-iCnrNM8gWn2pcPYx .icon-shape,#mermaid-svg-iCnrNM8gWn2pcPYx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iCnrNM8gWn2pcPYx .icon-shape p,#mermaid-svg-iCnrNM8gWn2pcPYx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iCnrNM8gWn2pcPYx .icon-shape .label rect,#mermaid-svg-iCnrNM8gWn2pcPYx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iCnrNM8gWn2pcPYx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iCnrNM8gWn2pcPYx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iCnrNM8gWn2pcPYx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} port(可选参考实现)
libdlt645(核心,零 OS 依赖)
实现
注册
应用:网关 / 表计 / 调试工具
dlt645_master
事务引擎
dlt645_slave
请求处理
dlt645_frame
组帧 / 解帧 / 流式接收
dlt645_di
DI 目录
dlt645_codec
BCD / 地址 / 时间 / 校验
dlt645_types
类型 / 常量 / 状态码
dlt645_port
HAL 结构体
dlt645_serial
Windows / POSIX 串口 8E1
物理层:UART / TCP / 内存 FIFO / 仿真
4.2 依赖规则(重要)
- 依赖单向向下 :
master/slave → frame → codec → types。
di只被主站侧用来解释/命名结果,从站数据结构无需登记 DI。 - 核心不碰 OS :
src/与include/里没有任何<windows.h>/
<termios.h>/ 线程 / 时间系统调用,时间与 I/O 全部经dlt645_port_t。 port/dlt645_serial.c是唯一碰 OS 的文件,且是可选的独立静态库。- 唯一的外部可变状态是 DI 用户扩展表(一个指针 + 计数),见 §8。
5. 模块详解
5.1 dlt645_types ------ 契约层
集中定义协议常量、功能码、错误位、状态码、地址类型、时间类型。
DLT645_MAX_FRAME 由各部分拼出,是理解内存账本的起点:
DLT645_MAX_FRAME = 1(68) + 6(addr) + 1(68) + 1(ctrl) + 1(len)
+ 200(data) + 1(cs) + 1(16) = 212 字节
所有函数返回 dlt645_status_t,dlt645_strerror() 给出可读信息;不使用
errno、不设全局错误状态。
5.2 dlt645_codec ------ 编解码原子操作
提供协议最碎的字节操作,单独可测:
- BCD :
bcd_to_bin/bin_to_bcd,非 BCD 半字节返回0xFF。 - 地址 :
addr_parse/addr_format/addr_broadcast/addr_wildcard
/addr_match(后者实现AA通配匹配)。 - DI :
di_make/di_write(小端上线) /di_read(还原为规范序)。 - BCD 数据项 :
bcd_decode/bcd_encode/bcd_format。符号位在
最高有效字节的 bit7 ,bcd_format输出如"123456.78"/"-0012.5"。 - 时间 :
time_to_broadcast(ss mm hh DD MM YY)与通用 5/6 字节
ddhhmm日期时间。 - 杂项 :
checksum(模 256 和)、data_encode/data_decode(±0x33)。
5.3 dlt645_frame ------ 组帧、解帧与流式接收
编码 dlt645_frame_encode() 接收明文数据域 ,函数内部完成 +0x33、
算校验、可选加 4 个 0xFE 前导。调用方永远不用手动变换。
整帧解码 dlt645_frame_decode() 要求缓冲区恰好是一帧(首尾对齐),
做长度一致性、68H/16H 边界和校验和检查。
流式接收 dlt645_rx_parser_t 是关键设计:UART 是字节流,帧边界随时
可能出现,所以用一个 8 状态机逐字节推进,且丢帧后能自恢复。
#mermaid-svg-lvnNbtbeuUrMhHc6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lvnNbtbeuUrMhHc6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lvnNbtbeuUrMhHc6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lvnNbtbeuUrMhHc6 .error-icon{fill:#552222;}#mermaid-svg-lvnNbtbeuUrMhHc6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lvnNbtbeuUrMhHc6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lvnNbtbeuUrMhHc6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lvnNbtbeuUrMhHc6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lvnNbtbeuUrMhHc6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lvnNbtbeuUrMhHc6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lvnNbtbeuUrMhHc6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lvnNbtbeuUrMhHc6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lvnNbtbeuUrMhHc6 .marker.cross{stroke:#333333;}#mermaid-svg-lvnNbtbeuUrMhHc6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lvnNbtbeuUrMhHc6 p{margin:0;}#mermaid-svg-lvnNbtbeuUrMhHc6 defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-lvnNbtbeuUrMhHc6 g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-lvnNbtbeuUrMhHc6 g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-lvnNbtbeuUrMhHc6 g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-lvnNbtbeuUrMhHc6 g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-lvnNbtbeuUrMhHc6 g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-lvnNbtbeuUrMhHc6 .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-lvnNbtbeuUrMhHc6 .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-lvnNbtbeuUrMhHc6 .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-lvnNbtbeuUrMhHc6 .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-lvnNbtbeuUrMhHc6 .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-lvnNbtbeuUrMhHc6 .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-lvnNbtbeuUrMhHc6 .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-lvnNbtbeuUrMhHc6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lvnNbtbeuUrMhHc6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lvnNbtbeuUrMhHc6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lvnNbtbeuUrMhHc6 .edgeLabel .label text{fill:#333;}#mermaid-svg-lvnNbtbeuUrMhHc6 .label div .edgeLabel{color:#333;}#mermaid-svg-lvnNbtbeuUrMhHc6 .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-lvnNbtbeuUrMhHc6 .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-lvnNbtbeuUrMhHc6 .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-lvnNbtbeuUrMhHc6 .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-lvnNbtbeuUrMhHc6 .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-lvnNbtbeuUrMhHc6 .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lvnNbtbeuUrMhHc6 .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lvnNbtbeuUrMhHc6 #statediagram-barbEnd{fill:#333333;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lvnNbtbeuUrMhHc6 .cluster-label,#mermaid-svg-lvnNbtbeuUrMhHc6 .nodeLabel{color:#131300;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-lvnNbtbeuUrMhHc6 .note-edge{stroke-dasharray:5;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram-note text{fill:black;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram-note .nodeLabel{color:black;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagram .edgeLabel{color:red;}#mermaid-svg-lvnNbtbeuUrMhHc6 #dependencyStart,#mermaid-svg-lvnNbtbeuUrMhHc6 #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-lvnNbtbeuUrMhHc6 .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lvnNbtbeuUrMhHc6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 0x68
收满 6 字节
0x68
非 0x68(若本身是 0x68 则重新开始)
L > 0
L == 0
L 超限(ERR_FORMAT)
收满 L 字节
校验和匹配
校验失败(ERR_CHECKSUM)
0x16
非 0x16
下一字节
IDLE
ADDR
START2
CTRL
LEN
DATA
CS
END
COMPLETE
设计要点:
IDLE状态静默吞掉0xFE前导和其它噪声 ,直到遇到0x68。- 任何中途失败都调用
rx_start_or_idle():如果出错的那个字节本身是
0x68,就地以它开新帧,避免把一个真正的帧头当噪声丢掉。 dlt645_rx_feed()返回OK表示"还在拼帧或拼完了",返回
ERR_CHECKSUM/ERR_FORMAT表示丢弃了一帧但仍继续找下一帧。
dlt645_frame_t.data 保存的是**已变换(含 +0x33)**的线上字节;
取明文必须走 dlt645_frame_data()。这是一个刻意的、README 里也强调的坑。
5.4 dlt645_port ------ 4 个函数指针的 HAL
c
typedef struct dlt645_port {
void *user;
int (*send)(void *, const uint8_t *, size_t, uint32_t timeout_ms);
int (*recv)(void *, uint8_t *, size_t, uint32_t timeout_ms); /* 非阻塞 */
uint32_t (*now_ms)(void *);
void (*delay_ms)(void *, uint32_t);
} dlt645_port_t;
recv必须非阻塞 (无数据返回 0),这是非阻塞引擎的前提;
timeout_ms只是给阻塞实现的提示,可忽略。now_ms单调毫秒;delay_ms只在阻塞封装里用。- 未使用的回调可置
NULL,但对应能力会返回DLT645_ERR_UNSUPPORTED。 - 这个结构也让测试极其轻量 :
tests/test_link.c用两个环形 FIFO 和一个
自增"时钟"就搭出完整主从链路,无需串口。
5.5 dlt645_master ------ 事务引擎
主站是单事务状态机:
#mermaid-svg-8N42B2cfwDHjobGZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8N42B2cfwDHjobGZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8N42B2cfwDHjobGZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8N42B2cfwDHjobGZ .error-icon{fill:#552222;}#mermaid-svg-8N42B2cfwDHjobGZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8N42B2cfwDHjobGZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8N42B2cfwDHjobGZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8N42B2cfwDHjobGZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8N42B2cfwDHjobGZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8N42B2cfwDHjobGZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8N42B2cfwDHjobGZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8N42B2cfwDHjobGZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8N42B2cfwDHjobGZ .marker.cross{stroke:#333333;}#mermaid-svg-8N42B2cfwDHjobGZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8N42B2cfwDHjobGZ p{margin:0;}#mermaid-svg-8N42B2cfwDHjobGZ defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-8N42B2cfwDHjobGZ g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-8N42B2cfwDHjobGZ g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-8N42B2cfwDHjobGZ g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-8N42B2cfwDHjobGZ g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-8N42B2cfwDHjobGZ g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-8N42B2cfwDHjobGZ .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-8N42B2cfwDHjobGZ .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-8N42B2cfwDHjobGZ .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-8N42B2cfwDHjobGZ .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-8N42B2cfwDHjobGZ .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-8N42B2cfwDHjobGZ .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-8N42B2cfwDHjobGZ .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-8N42B2cfwDHjobGZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8N42B2cfwDHjobGZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8N42B2cfwDHjobGZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8N42B2cfwDHjobGZ .edgeLabel .label text{fill:#333;}#mermaid-svg-8N42B2cfwDHjobGZ .label div .edgeLabel{color:#333;}#mermaid-svg-8N42B2cfwDHjobGZ .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-8N42B2cfwDHjobGZ .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-8N42B2cfwDHjobGZ .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-8N42B2cfwDHjobGZ .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-8N42B2cfwDHjobGZ .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-8N42B2cfwDHjobGZ .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8N42B2cfwDHjobGZ .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8N42B2cfwDHjobGZ #statediagram-barbEnd{fill:#333333;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8N42B2cfwDHjobGZ .cluster-label,#mermaid-svg-8N42B2cfwDHjobGZ .nodeLabel{color:#131300;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-8N42B2cfwDHjobGZ .note-edge{stroke-dasharray:5;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram-note text{fill:black;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram-note .nodeLabel{color:black;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagram .edgeLabel{color:red;}#mermaid-svg-8N42B2cfwDHjobGZ #dependencyStart,#mermaid-svg-8N42B2cfwDHjobGZ #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-8N42B2cfwDHjobGZ .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8N42B2cfwDHjobGZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} build_*/start
发送完且非广播
广播(不等应答)
收到无关帧 / 部分帧
超时且有重试
收到匹配应答
后续帧标志置位 → 自动发 12H
重试耗尽(ERR_TIMEOUT)/ 异常应答(ERR_SLAVE)
下次 build
IDLE
TX
WAIT
DONE
一次事务的生命周期:
dlt645_master_build_*()组帧,并调用dlt645_master_start()。start()解析请求帧本身,记下期望地址 (广播则置silent)。poll()在TX态分片调用port.send(),发完转WAIT。WAIT态循环port.recv(),逐字节喂给流式接收器;帧完成后校验:- 功能码必须匹配 (
ctrl & 0x1F == m->func),否则丢弃继续等; - 非通配场景下地址必须匹配;
- 异常帧 →
DLT645_ERR_SLAVE立即结束; - 后续帧标志 置位且是读路径 → 自动发
12H续读(见 §6.2)。
- 功能码必须匹配 (
- 两类超时:
byte_timeout_ms(帧间字节停顿 → 重置接收器)、
response_timeout_ms(整体无应答 → 重试或ERR_TIMEOUT)。
多帧续读的地址处理 是一处细节:首个读请求可能用 AA 通配地址,
而从站应答回真实地址 。主站在收到带后续标志的应答时,把
expect_addr 更新为应答里的真实地址,之后所有 12H 都发给这个具体地址,
并切换 m->func = 0x12 以便匹配后续应答。这是"缩位寻址 + 自动续帧"能
协同工作的关键。
阻塞封装 dlt645_master_run() 只是 poll() + delay_ms(1) 的循环,
并带一个总超时;核心本身从不 sleep。
独立构造函数 (dlt645_build_*_frame)不依赖引擎状态,便于测试与工具。
5.6 dlt645_slave ------ 请求处理
从站用回调式数据模型 (dlt645_slave_model_t),库不拥有业务数据:
| 回调 | 语义 |
|---|---|
on_read(user, di, &data, &len) |
返回模型持有的数据指针,下次调用前有效 |
on_write(user, di, data, len) |
写入明文数据 |
on_auth(user, pa, pwd, op) |
权限 + 密码 + 操作者代码鉴权 |
on_write_addr / on_broadcast_time / on_freeze / on_change_baud / on_change_pwd / on_clear |
各类控制功能 |
dlt645_slave_handle_frame() 的分派逻辑:
- 丢弃**方向位为"应答"**的帧(避免主从回环里自激)。
- 取明文数据域。
- 地址:广播放行,否则
addr_match(AA通配)。 - 按功能码分派到各
handle_*;未知功能码回"其它错误"D1H。 - 广播校时/广播冻结永不应答。
- 有应答且注册了
on_event时通知一次,便于埋点/日志。
单帧应答长度上限的处理 :读数据每个应答最多 200 - 4 = 196 字节
(4 字节 DI);续帧最多 200 - 5 = 195 字节(多一个帧序号)。超出的部分
从站记录 follow_di/follow_pos,等主站发 12H 时从上次位置继续。
写地址的特殊性 :修改通信地址的应答从新地址发出 ,然后库把
s->addr 更新为新地址。
5.7 dlt645_di ------ 数据标识目录
目录由 tools/gen_di_table.py 从标准 PDF 附录 A 生成,产物两份:
tools/dlt645_di_catalog.json:700 条完整目录(机器可读)。src/dlt645_di_table.inc:被#include进dlt645_di.c的 C 表
(所以构建时不要单独编译它)。
为什么用"掩码通配"而不是全展开? 费率 1...63、结算日、数据块组合
全展开会有数万条。生成器把范围行压成一条 mask 记录(范围字节在
mask 里清零),例如"当前/上N日 组合有功总电能"共享一条通配项。
匹配算法 (dlt645_di.c):
best_match()选掩码置位最多(最具体)的条目;用户表在 同具体度
下优先 (>=),因此既能新增也能覆盖内置项。search()做逐级放宽 :精确 → 忽略 DI0(结算日/数据块)→ 忽略 DI1
(费率)→ 两者都忽略。这样附录 A 只示例性列出的"上 5 结算日"等也能解析。dlt645_di_format_name()用lookup_ex()判断该 DI 是否落在通配范围,
自动补(费率N)/(上N结算日)/(当前)/(数据块)后缀。
5.8 dlt645_serial ------ 可选串口适配
唯一平台相关代码,同一份 dlt645_serial.c 用条件编译同时支持 Windows 与
POSIX。行设置固定为 DL/T 645 要求的 8 数据位 + 偶校验 + 1 停止位(8E1) 。
dlt645_serial_port() 把它包装成 dlt645_port_t。有自己 UART 驱动的项目
可以完全忽略它。
6. 关键流程
6.1 单帧点对点读
dlt645_slave dlt645_master 应用 dlt645_slave dlt645_master 应用 #mermaid-svg-OtKYhAYQ3MVEuRcz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OtKYhAYQ3MVEuRcz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OtKYhAYQ3MVEuRcz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OtKYhAYQ3MVEuRcz .error-icon{fill:#552222;}#mermaid-svg-OtKYhAYQ3MVEuRcz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OtKYhAYQ3MVEuRcz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OtKYhAYQ3MVEuRcz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OtKYhAYQ3MVEuRcz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OtKYhAYQ3MVEuRcz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OtKYhAYQ3MVEuRcz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OtKYhAYQ3MVEuRcz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OtKYhAYQ3MVEuRcz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OtKYhAYQ3MVEuRcz .marker.cross{stroke:#333333;}#mermaid-svg-OtKYhAYQ3MVEuRcz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OtKYhAYQ3MVEuRcz p{margin:0;}#mermaid-svg-OtKYhAYQ3MVEuRcz .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OtKYhAYQ3MVEuRcz text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-OtKYhAYQ3MVEuRcz .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-OtKYhAYQ3MVEuRcz .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-OtKYhAYQ3MVEuRcz .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-OtKYhAYQ3MVEuRcz .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-OtKYhAYQ3MVEuRcz #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-OtKYhAYQ3MVEuRcz .sequenceNumber{fill:white;}#mermaid-svg-OtKYhAYQ3MVEuRcz #sequencenumber{fill:#333;}#mermaid-svg-OtKYhAYQ3MVEuRcz #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-OtKYhAYQ3MVEuRcz .messageText{fill:#333;stroke:none;}#mermaid-svg-OtKYhAYQ3MVEuRcz .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OtKYhAYQ3MVEuRcz .labelText,#mermaid-svg-OtKYhAYQ3MVEuRcz .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-OtKYhAYQ3MVEuRcz .loopText,#mermaid-svg-OtKYhAYQ3MVEuRcz .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-OtKYhAYQ3MVEuRcz .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-OtKYhAYQ3MVEuRcz .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-OtKYhAYQ3MVEuRcz .noteText,#mermaid-svg-OtKYhAYQ3MVEuRcz .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-OtKYhAYQ3MVEuRcz .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OtKYhAYQ3MVEuRcz .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OtKYhAYQ3MVEuRcz .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OtKYhAYQ3MVEuRcz .actorPopupMenu{position:absolute;}#mermaid-svg-OtKYhAYQ3MVEuRcz .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-OtKYhAYQ3MVEuRcz .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OtKYhAYQ3MVEuRcz .actor-man circle,#mermaid-svg-OtKYhAYQ3MVEuRcz line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-OtKYhAYQ3MVEuRcz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} looppoll build_read(addr, DI)68 addr 68 11 04 <DI+0x33> cs 16handle_frame → on_read()68 addr 68 91 L <DI+数据> cs 16on_frame(m, frame, user)on_done(m, DLT645_OK, user)
应用在 on_frame 里用 dlt645_frame_data() 取明文,再配合
dlt645_di_lookup() + dlt645_bcd_format() 得到数值与单位。
6.2 多帧续读(数据 > 196 字节)
tests/test_link.c 用 300 字节数据验证了完整路径:
Slave Master Slave Master #mermaid-svg-0u4wZWfx724GziM2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0u4wZWfx724GziM2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0u4wZWfx724GziM2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0u4wZWfx724GziM2 .error-icon{fill:#552222;}#mermaid-svg-0u4wZWfx724GziM2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0u4wZWfx724GziM2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0u4wZWfx724GziM2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0u4wZWfx724GziM2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0u4wZWfx724GziM2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0u4wZWfx724GziM2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0u4wZWfx724GziM2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0u4wZWfx724GziM2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0u4wZWfx724GziM2 .marker.cross{stroke:#333333;}#mermaid-svg-0u4wZWfx724GziM2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0u4wZWfx724GziM2 p{margin:0;}#mermaid-svg-0u4wZWfx724GziM2 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0u4wZWfx724GziM2 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-0u4wZWfx724GziM2 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0u4wZWfx724GziM2 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-0u4wZWfx724GziM2 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-0u4wZWfx724GziM2 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-0u4wZWfx724GziM2 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-0u4wZWfx724GziM2 .sequenceNumber{fill:white;}#mermaid-svg-0u4wZWfx724GziM2 #sequencenumber{fill:#333;}#mermaid-svg-0u4wZWfx724GziM2 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-0u4wZWfx724GziM2 .messageText{fill:#333;stroke:none;}#mermaid-svg-0u4wZWfx724GziM2 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0u4wZWfx724GziM2 .labelText,#mermaid-svg-0u4wZWfx724GziM2 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-0u4wZWfx724GziM2 .loopText,#mermaid-svg-0u4wZWfx724GziM2 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-0u4wZWfx724GziM2 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-0u4wZWfx724GziM2 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-0u4wZWfx724GziM2 .noteText,#mermaid-svg-0u4wZWfx724GziM2 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-0u4wZWfx724GziM2 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0u4wZWfx724GziM2 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0u4wZWfx724GziM2 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-0u4wZWfx724GziM2 .actorPopupMenu{position:absolute;}#mermaid-svg-0u4wZWfx724GziM2 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-0u4wZWfx724GziM2 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-0u4wZWfx724GziM2 .actor-man circle,#mermaid-svg-0u4wZWfx724GziM2 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-0u4wZWfx724GziM2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 更新 expect_addr 为真实地址func 切到 12H 若仍有剩余则回 B2H,主站继续发 12H 11H 读 DI91H|0x20 (B1H) DI + 196B ← 后续标志 D5 置位12H DI seq=192H DI + 剩余 104B + seq ← 余量 ≤ 195,不再置 D5,事务结束
从站侧:handle_read_data 首次切 196 字节并把余量记入 follow_pos;
handle_read_follow 从 follow_pos 继续,more 决定 0xB2(续)或
0x92(末)。主站侧:每收到一个带续标志的帧就自动 12H,回调会被
多次触发 ------应用需按顺序把各帧的数据域累加(测试里 on_frame 正是这么
做的,续帧还要去掉尾部帧序号)。
6.3 异常应答
从站 build_error() 用 ctrl_error_of(req) 置 D6,数据域放 1 字节错误位。
主站识别 D6 后直接以 DLT645_ERR_SLAVE 结束事务,错误细节(如"无请求
数据")需要应用自己在 on_frame 里读那个字节。这也是 test_read_no_data
覆盖的场景。
6.4 广播
请求地址为 99..99 时,主站在 start() 里置 silent=1:发完即
DLT645_OK,不进 WAIT。从站侧对应命令(校时、广播冻结)功能照做但
不应答。这是标准语义,也是"广播不产生应答风暴"的保证。
7. 内存与资源模型
无动态内存意味着所有上限都是编译期常量,资源可静态核算:
| 对象 | 组成 | 量级 |
|---|---|---|
DLT645_MAX_FRAME |
起始+地址+起始+控制+长度+200+校验+结束 | 212 B |
dlt645_frame_t |
地址 6 + 控制 1 + 长度 1 + 数据 200 | 208 B |
dlt645_rx_parser_t |
状态 + dlt645_frame_t + 索引 + 校验 |
≈ 216 B |
dlt645_master_t |
port + cfg + 回调 + tx[216] + rx + 事务状态 |
≈ 0.5 KB |
dlt645_slave_t |
port + model + addr + rx + tx[216] + 续帧状态 |
≈ 0.5 KB |
| DI 目录 | 700 条只读表(编译进 .rodata) |
由 .inc 决定 |
要点:
- 主/从同一实例同时只能有一笔事务 (单事务模型)。并发/排队由调用方
负责;需要多路并发就多开几个实例,或自行加锁。 - 从站数据是模型持有 的
指针 + 长度,库只拷贝、不拥有,所以应用
必须保证回调返回的指针在下一次调用前有效。 - 缓冲不会为了超长数据而放大:读路径靠多帧续读 ,而非更大的
L。
8. 扩展点:自定义 DI
主站解释数据依赖 DI 目录。厂商私有标识或需要覆盖内置命名时,注册一张
应用自有的静态数组即可(库不拷贝、不分配):
c
static const dlt645_di_info_t my_di[] = {
/* 新增厂商自定义标识 */
DLT645_DI_DEFINE(0x0A000001u, "厂商自定义电能", "kWh",
"XXXXXX.XX", 4, 2, DLT645_DI_READ, DLT645_DI_CAT_OTHER),
/* 覆盖内置条目(同等具体度用户优先) */
DLT645_DI_DEFINE(0x00010000u, "正向有功总电能(厂家重定义)", "kWh",
"XXXXXX.XX", 4, 2,
DLT645_DI_READ | DLT645_DI_WRITE, DLT645_DI_CAT_ENERGY),
/* 带通配掩码:mask 为 0 的位表示"任意值" */
DLT645_DI_ENTRY(0x0B000000u, 0xFFFF00FFu, "厂商自定义总电能", "kWh",
"XXXXXX.XX", 4, 2,
DLT645_DI_READ | DLT645_DI_WILD_RATE, DLT645_DI_CAT_ENERGY),
};
dlt645_di_set_user_table(my_di, sizeof(my_di) / sizeof(my_di[0]));
- 数组必须整个使用期存活 (用
static最稳妥)。 mask为1的位必须与di相等,为0的位表示该字节任意。- 仅主站侧解释 用到目录;从站数据由
on_read提供,无需登记。 - 非线程安全,请在启动阶段注册。
9. 工程化
9.1 构建与测试
sh
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failure
迭代单个测试(MinGW gcc + Ninja;dlt645_di_table.inc 是被 include 的,
不要编译):
sh
gcc -std=c99 -Wall -Wextra -Iinclude/dlt645 tests/test_link.c src/*.c -o build/t.exe
ctest --test-dir build -R test_link --output-on-failure
测试是独立 main() 程序 + tests/test_util.h 里的
CHECK/CHECK_EQ_INT/CHECK_EQ_MEM/TEST_REPORT 宏,无测试框架:
| 测试 | 覆盖 |
|---|---|
test_frame |
BCD、地址、DI 编解码、日期时间、组帧/解帧、坏校验、流式接收 |
test_link |
主↔从内存回环:300B 多帧续读、异常应答、读通信地址 |
test_di |
DI 目录查询、命名、用户扩展表 |
9.2 作为依赖使用
安装后同时提供 CMake package 与 pkg-config:
cmake
find_package(dlt645 0.1 CONFIG REQUIRED)
target_link_libraries(app PRIVATE dlt645::dlt645)
# 需要串口适配器时:
target_link_libraries(app PRIVATE dlt645::dlt645_serial)
也支持 add_subdirectory / FetchContent,或直接把 src/*.c 与
include/dlt645 加入裸工程。注意 include 路径是 include/dlt645 ,
头之间按裸名互相包含。
9.3 DI 目录再生成
sh
python tools/gen_di_table.py docs/多功能电能表通信协议.pdf
需要 PyMuPDF(import pymupdf 或旧名 fitz)。会同时重写
tools/dlt645_di_catalog.json 与 src/dlt645_di_table.inc。
.inc 是生成物,不要手改;要覆盖/扩展用 §8 的用户表。
10. 设计取舍
以下决策贯穿实现,了解它们能避免误用与重复讨论:
| 取舍 | 理由 | 代价 / 影响 |
|---|---|---|
| 主站与从站同库、共用链路段 | 组帧/解帧/BCD/地址只有一份实现,回环可自测 | 二进制略大;单角色项目可只链接需要的 src |
| 非阻塞状态机为核心,阻塞 API 只是轮询封装 | 核心可在中断/裸机主循环跑,无阻塞无线程 | 阻塞封装依赖 now_ms/delay_ms |
| HAL 用函数指针,串口只做参考实现 | 核心零 OS 依赖、易移植 | 使用者需提供 4 个回调,且语义必须严格 |
| C99 + 无动态内存 + 固定缓冲 | 嵌入式友好、内存可预测、无碎片 | 上限编译期确定;超长数据靠续帧 |
| 单事务模型(无内部队列) | 状态机简单、RAM 固定 | 同实例同时仅一笔事务,并发由调用方负责 |
| DI 目录用掩码通配而非全展开 | 全展开会有数万条 | 名称是"基项名 + 后缀",非逐值精确;查找逐级放宽 |
| 地址字符串按十进制数值解释 | 与"低字节在前"及面板显示一致 | 上游给"线路字节序"时需直接填 b[] |
| 从站数据为 model-owned 的指针+长度 | 库不复制、不拥有,省大缓冲 | 回调指针须在下次调用前有效 |
| 校验只用标准定义的偶校验 + 模 256 和 | 保证与现网电表互操作 | 检错弱于 CRC,依赖重试与超时 |
| 自动续帧仅限读数据 | 12H 是读路径的规范行为,写/控制无此语义 |
读请求可能多次 on_frame,调用方按序累加 |
错误统一用 dlt645_status_t |
无全局状态、可重入、易测试 | 调用方需检查返回值,不用 errno |
明确的非目标
- 线程安全:请自行加锁。
- 内置重传之外的流控:无滑动窗口/背压。
- 多主机仲裁:链路假设单一主站。
- 非标准厂家扩展标识的语义解析:库只给目录与命名,语义由应用定。
11. 从哪开始读代码
给第一次接触本仓库的读者的建议路径:
README.md------ 跑起来,看主站阻塞读 30 行示例。tests/test_link.c------ 最短的端到端全貌:FIFO 链路 + 300B 续读。src/dlt645_frame.c------ 组帧/解帧/流式接收状态机,协议的核心。src/dlt645_master.c的dlt645_master_poll()------ 事务状态机与续帧。src/dlt645_slave.c的dlt645_slave_handle_frame()------ 服务端分派。src/dlt645_di.c------ 目录匹配与逐级放宽。tools/gen_di_table.py------ 目录从何而来。