DL/T 645—2007《多功能电能表通信协议》的跨平台 C 语言实现

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

一次事务的生命周期:

  1. dlt645_master_build_*() 组帧,并调用 dlt645_master_start()。
  2. start() 解析请求帧本身,记下期望地址 (广播则置 silent)。
  3. poll() 在 TX 态分片调用 port.send(),发完转 WAIT。
  4. WAIT 态循环 port.recv(),逐字节喂给流式接收器;帧完成后校验:
    • 功能码必须匹配 (ctrl & 0x1F == m->func),否则丢弃继续等;
    • 非通配场景下地址必须匹配;
    • 异常帧 → DLT645_ERR_SLAVE 立即结束;
    • 后续帧标志 置位且是读路径 → 自动发 12H 续读(见 §6.2)。
  5. 两类超时: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() 的分派逻辑:

  1. 丢弃**方向位为"应答"**的帧(避免主从回环里自激)。
  2. 取明文数据域。
  3. 地址:广播放行,否则 addr_match(AA 通配)。
  4. 按功能码分派到各 handle_*;未知功能码回"其它错误" D1H。
  5. 广播校时/广播冻结永不应答。
  6. 有应答且注册了 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. 从哪开始读代码

给第一次接触本仓库的读者的建议路径:

  1. README.md ------ 跑起来,看主站阻塞读 30 行示例。
  2. tests/test_link.c ------ 最短的端到端全貌:FIFO 链路 + 300B 续读。
  3. src/dlt645_frame.c ------ 组帧/解帧/流式接收状态机,协议的核心。
  4. src/dlt645_master.c 的 dlt645_master_poll() ------ 事务状态机与续帧。
  5. src/dlt645_slave.c 的 dlt645_slave_handle_frame() ------ 服务端分派。
  6. src/dlt645_di.c ------ 目录匹配与逐级放宽。
  7. tools/gen_di_table.py ------ 目录从何而来。

12. 仓库

https://github.com/wqs-base/libDLT645.git

相关推荐
老王爱玩车3 小时前
工具函数——清空输入缓冲区
c语言·开发语言·学习
wuminyu4 小时前
JEP491中synchronized关键字引发的平台线程钉住解决方法简介
java·linux·c语言·jvm·c++
phltxy6 小时前
C 语言文件操作:让数据从内存走向持久化
c语言·网络·数据库
大圣编蚕7 小时前
C语言怎么写小游戏?从零开始做一个贪吃蛇
c语言·开发语言
Chen—LSN7 小时前
数据结构——拿捏复杂度
java·c语言·开发语言·数据结构·经验分享·笔记·算法
Chen_harmony7 小时前
六、排序详解
c语言·数据结构·算法·排序算法
她说彩礼65万8 小时前
C语言 单引号与双引号
java·服务器·c语言
dtq04248 小时前
数据结构 - 树的概念
c语言·数据结构·学习·算法
山下梅子酒2258 小时前
洛谷-入门-B2075
c语言