简介:在电力系统自动化与智能电网场景中,通信规约的源代码对于设备互联与数据交互至关重要。这份RAR压缩包汇集了CDT规约、FDK规约与Modbus规约的完整工程实现,面向电力系统工程师、自动化集成人员以及希望深入理解规约细节的开发者,可用于二次开发、协议调试和系统集成。包内共181个文件,以源码文件为主,包括42个h头文件、37个cpp源文件,配合配套的ico图标、bmp位图、dll动态库及调试辅助文件,涵盖工程配置、资源定义和可执行程序,整体约2.28MB,结构紧凑便于快速查阅。目前已吸引498人学习下载。凭借源代码可学习报文组帧、数据解析、异常处理及维护工具的使用,压缩包中的comm_maintain文件集中展现了通信维护相关实现与调试思路,对解决电力系统现场通信问题具有直接参考价值。 干过变电站远动调试的朋友,应该都有过这种经历:半夜在后台机前抓报文,CDT的EB 90 EB 90 EB 90刷屏,就是等不到想要的那帧信息字;或者FDK(101)链路死活爬不上来,FCB一直翻转却收不到确认帧;再不然就是MODBUS从设备半天不上数,最后发现CRC高低字节反了。这三套规约,基本覆盖了电力远动通信从老站到新站、从间隔层到主站的大部分调试场景。这篇文章就把CDT规约、FDK规约和MODBUS规约的源代码结构、核心状态机以及我在现场攒下的坑,一次性理清楚。
纯搞应用层开发的朋友可能觉得规约代码又老又繁琐,但真到联调对点时你就知道,这几段代码就是你和调度主站之间唯一的桥梁。看懂它、改得动它,能帮你省掉大量现场蹲守的时间。
1. 三类规约的定位:为什么电力通信离不开它们
1.1 CDT、FDK(101)、MODBUS各自解决什么问题
很多人第一次接触这三类规约时会犯迷糊:又是CDT又是FDK又是MODBUS,到底该用哪个?其实它们对应的是电力通信里三个完全不同层面的需求。
CDT规约是循环式远动规约,国内大量老站和国产RTU设备都在用。它的核心特点是厂站端主动循环上送数据,主站被动接收,不需要主站频繁下发查询命令。这种“厂站主动上报”的机制在点对点通信里非常稳定,哪怕通道质量差一点,只要同步字能对齐,数据就能持续上送。
FDK规约,行业内习惯这么叫,其实就是IEC 60870-5-101规约(国内对应DL/T 634.5101)。它属于问答式远动规约,主站通过召唤命令去取数,厂站收到请求后再回送数据。跟CDT的“广播式”相反,101更像“点名回答”,链路层有完整的状态管理和超时重发机制,适合调度主站与厂站之间数据量大、可靠性要求高的场景。
MODBUS则是设备级通信的常客,在电力系统中大量用于智能电表、保护装置、温控器、在线监测装置等现场设备的接入。它结构简单,一帧地址加功能码就能读写寄存器,上位机软件和嵌入式设备都很容易实现。实际做变电站辅助设备监控时,MODBUS往往是打通不同厂家设备最省事的方案。
| 规约 | 通信方式 | 典型应用位置 | 特点 |
|---|---|---|---|
| CDT | 循环上送 | 老站RTU、国产远动装置 | 帧结构固定,实现简单,依赖通道稳定性 |
| FDK(101) | 问答式 | 调度主站与厂站通信 | 状态机完善,可靠性高,支持多种ASDU类型 |
| MODBUS | 主从问答 | 智能设备、电能表、传感器 | 通用性强,寄存器模型直观,调试快 |
1.2 搞清楚规约源代码能省下哪些时间
规约源代码的真正价值,不在于让你从零造一个轮子,而在于三件事:第一,排查问题时能直接看穿帧的每个字节含义,而不是拿着十六进制报文干瞪眼;第二,理解状态机是怎么流转的,调试101链路时不会被FCB、ACD这些位搞得晕头转向;第三,移植和适配时知道改哪里,比如换一个CPU平台、换一个串口中断方式,代码该怎么动。
我自己带过不少新人,最直观的感受是:会读规约源码的人,现场排查问题平均能快一半时间。不会读的人,遇到帧解析失败只能一遍遍抓包、重启设备,靠猜来定位问题。
2. 帧结构拆解:把三大规约的数据模型掰开看
2.1 CDT规约:循环式远动的帧组合
CDT的帧结构非常规整,用三个部分就能说清楚:同步字、控制字、信息字。
| 组成部分 | 长度 | 内容说明 |
|---|---|---|
| 同步字 | 6字节 | EB 90 EB 90 EB 90,用于帧头识别 |
| 控制字 | 6字节 | 帧类别、信息字数、源站址、目的站址、2字节校验码 |
| 信息字 | n×6字节 | 功能码、信息序号、信息体4字节(含校验) |
控制字的帧类别决定了这一帧装的是什么数据。上行方向常用:61H代表重要遥测帧(A帧),62H代表次要遥测帧(B帧),63H代表遥信帧(C帧),64H是电能脉冲帧,65H是事件顺序记录帧。下行方向还有遥控选择、执行、撤销等帧类别。现场调试时报文跟我对不上,十有八九是帧类别没匹配上。
信息字的结构也值得多说一句:每个信息字固定6字节,功能码标识数据类型,信息序号表示点号,信息体里才是真正的遥测值或遥信状态。CDT的校验不是普通的CRC16,而是基于特定生成多项式的BCH校验,计算范围是控制字前5字节或信息字前4字节。很多移植的代码跑不通,问题就出在校验的字节范围和字节序上。不同厂家实现会有差异,拿到源码后第一件事就是对着对端设备的规约说明核对校验逻辑。
2.2 FDK(101)规约:FT1.2链路层与ASDU
FDK(101)规约的帧格式分固定帧长和可变帧长两种,理解它们的区别是看懂101源码的前提。
固定帧长格式:启动符10H、控制域C、链路地址A、帧校验和CS、结束符16H,一共5字节。它用于链路建立、请求链路状态这类短命令,不携带应用数据。可变帧长格式则复杂一些:启动符68H、长度L、长度L重复、启动符68H、控制域C、链路地址A、ASDU数据、帧校验和CS、结束符16H。应用数据全在ASDU里。
ASDU是101规约的核心,里面包含类型标识、可变结构限定词、传送原因、公共地址、信息对象地址和信息元素。常用的类型标识有:1号单点遥信、3号双点遥信、11号标度化遥测、13号短浮点遥测、45号单点命令、46号双点命令、100号总召唤。做对点调试时,看到类型标识就能知道主站要什么数据。
控制域里的功能码也很关键:主站请求1级数据用10H,请求2级数据用0BH,从站回送数据用08H,确认帧用00H。链路状态从停止态到工作态,全靠这几个功能码一步步推进。这部分源码如果状态机写得乱,链路就永远建立不起来。
2.3 MODBUS规约:寄存器读写的一问一答
MODBUS相对简单,但在电力设备接入里出场率极高。RTU模式下,一帧报文就是地址码、功能码、数据、CRC16校验。常用功能码:01H读线圈、02H读离散输入、03H读保持寄存器、04H读输入寄存器、05H写单线圈、06H写单寄存器、0FH写多线圈、10H写多寄存器。
MODBUS最容易被坑的是寄存器地址偏移。很多设备说明书里写的是40001、40002这类PLC风格的寄存器号,但协议报文里的地址是从0开始的。比如说明书让你读40001,你报文里地址字段得填0,而不是40001。这个偏移问题在电力设备调试中几乎天天遇到,源代码里如果没有做地址转换,现场就会一直读错寄存器。
3. 源代码实现:状态机、调度与校验算法
3.1 规约代码的经典分层与数据结构
规约代码写得好不好,关键是分层。我一般把代码分成三层:物理层负责串口收发和字节缓冲;链路层负责帧同步、校验、状态机;应用层负责点表映射、数据入库、命令下发。分层清晰之后,换串口驱动不用动协议代码,换点表也不用动帧解析。
typedef struct { uint8_t buf[512]; uint16_t len; uint16_t rx_index; uint8_t sync_found; } serial_channel_t; typedef struct { uint8_t frame_type; uint8_t info_count; uint8_t src_addr; uint8_t dst_addr; uint8_t info_unit[MAX_INFO_CNT * 6]; uint16_t crc; } cdt_frame_t; typedef struct { uint8_t ctl; uint8_t link_addr; uint8_t asdu[256]; uint16_t asdu_len; } iec101_frame_t;这两个结构体是我在实际项目里常用的样子。CDT和101的帧都放在固定缓冲区里,避免使用动态内存分配,这样不仅代码简单,在嵌入式平台上也不会因为内存碎片出问题。缓冲区大小根据现场最大报文长度来定,CDT一般256字节足够,101的可变帧长最大也就255字节。
3.2 CDT循环发送调度实现
CDT的核心在一个“循环”上:设备上电后,要按周期把重要遥测、次要遥测、遥信轮流发出去。如果中间有事件发生(比如遥信变位),还得插入变位帧。这个调度逻辑用定时器就能实现。
void cdt_task_1s(void) { // 每个周期上送一帧重要遥测 cdt_send_frame(CDT_FRAME_A); // 次要遥测隔周期上送 if (b_frame_flag) { cdt_send_frame(CDT_FRAME_B); b_frame_flag = 0; } else { b_frame_flag = 1; } // 有遥信变位时优先插入事件帧 if (event_pending) { cdt_send_frame(CDT_FRAME_E); event_pending = 0; } }这段代码看起来简单,但有一个关键点:事件帧要“插空”发送,不能在A帧发送到一半时把缓冲区覆盖掉。所以在真正的工程实现里,我会用一个发送队列,把A帧、B帧、E帧都塞进队列,串口发送中断从队列里取数据,这样就不会出现帧交叠。CDT对时序要求高,帧与帧之间如果出现半个帧混在一起,对端直接丢帧。
3.3 101链路层状态机实现要点
101规约的从站侧状态机,我习惯用枚举来表示:停止态、未初始化态、工作态。主站发复位命令后从停止态进入未初始化态,再通过总召唤进入工作态。每个状态下能接收的控制域功能码不一样,这个逻辑用switch-case就能清晰实现。
typedef enum { LINK_STOP = 0, LINK_UNINIT, LINK_WORK } link_state_t; bool link_handle_frame(uint8_t *buf, uint16_t len) { // 先校验帧校验和CS,再检查链路地址 // 然后取出控制域功能码,按状态机分发 switch (link_state) { case LINK_STOP: if (func == FUNC_RESET_LINK) { link_state = LINK_UNINIT; link_ack(FUNC_ACK); } break; case LINK_UNINIT: if (func == FUNC_REQ_STATUS) { link_ack(LINK_STATUS_OK); } else if (func == FUNC_RESET_PROCESS) { link_state = LINK_WORK; link_ack(FUNC_ACK); } break; case LINK_WORK: if (func == FUNC_REQ_1LEVEL) { send_class1_asdu(); } else if (func == FUNC_REQ_2LEVEL) { send_class2_asdu(); } break; } return true; }实现101状态机时最容易犯的错是FCB位处理。主站每成功收到一帧确认,下一次请求就会把FCB翻转一下;如果从站收到的FCB跟上一次相同,说明链路可能丢帧,需要重发确认。源代码里如果没有维护一个last_fcb变量,链路质量差的时候就会出现“主站一直发、从站一直不应答”的死循环。
3.4 MODBUS RTU主从实现与CRC细节
MODBUS的实现核心就两个:一个是帧间隔判断,一个是CRC校验。RTU帧没有固定帧头,靠的是3.5个字符时间的静默间隔来切分帧。波特率9600时,3.5个字符大约是4毫秒,代码里用定时器量这个间隔,间隔到了就认为一帧数据收完。
uint16_t modbus_crc16(uint8_t *buf, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= buf[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 1) crc = (crc >> 1) ^ 0xA001; else crc >>= 1; } } return crc; }这段CRC代码是MODBUS RTU的标准算法,多项式是0xA001(也就是0x8005的反转形式),初值0xFFFF,结果低字节在前发送。我在现场见过好几次“CRC算不对”的情况,最后发现是代码里把初值写成了0x0000,或者算完没交换高低字节。这个小函数值得反复验证,最好拿报文里的CRC字段反推一遍。
4. 现场调试常见问题与排查实录
4.1 一张表说清高频故障
| 故障现象 | 可能原因 | 排查思路 |
|---|---|---|
| CDT搜不到同步字 | 波特率或字符极性不对 | 先抓串口原始数据,确认EB 90帧头存在 |
| CDT解码CRC报错 | BCH校验字节范围不对 | 对照规约文档,核对校验码计算范围 |
| 101链路反复断开 | t0/t1超时参数不合理 | 调大超时时间,观察FCB翻转是否一致 |
| 101总召唤无响应 | 从站未进入工作态或地址不匹配 | 逐帧确认链路状态机走到了哪一步 |
| MODBUS通信超时 | 从站地址或寄存器地址偏移错误 | 用MODBUS调试工具单帧读写,锁定偏移 |
| MODBUS CRC校验失败 | CRC初值、字节序错误 | 用计算器离线验算帧尾CRC |
| 遥信点号错位 | 信息体地址映射错 | 对照调度点表逐一核对偏移量 |
| 遥测值毛刺大 | 系数换算错误或符号位处理错 | 用当前实际值反推原始码值 |
这张表我打印出来贴在工位很久,现场遇到问题直接对着查,大部分能快速定位。
4.2 一次101链路调不通的完整排查过程
前两年做一个110kV站的新设备接入,101链路怎么都建立不起来。主站后台反复显示“通道中断”,我抓包看主站一直在发请求链路状态命令,但厂站端就是不应答。用串口助手直接插在厂站装置前,发现报文其实已经收到了,但控制域里的功能码和地址都被正确解析,就是不回确认帧。
后来查代码才发现,问题出在链路地址上。装置配置的链路地址是1,但主站下发的请求里地址字段写的是11,厂商的老代码里对地址处理做了偏移加10的操作,导致两边地址永远对不上。这种问题靠抓包根本看不出来,必须对照双方的点表和地址配置逐项核对。最后把装置地址改成11,链路立马就通了。从那以后我每次调试101,第一件事就是把主站侧和厂站侧的链路地址、公共地址、信息体地址三张表全部打印出来,逐个核对。
4.3 好用的调试辅助手段
调试规约代码,光靠眼睛看缓冲区肯定不行。我的习惯是三层工具配合用:第一层是串口抓包工具,直接看物理层的原始字节流;第二层是PC端的规约模拟软件,比如MODBUS Poll加MODBUS Slave,还有101主站/从站模拟器,专门用来验证状态机和帧格式;第三层是自己写的报文解析脚本,把抓到的报文按帧结构逐字节打印成可读字段,比纯十六进制直观得多。
个人建议代码里加一个debug_printf接口,把所有收发的原始报文按时间戳打到调试串口,同时打上解析后的字段值。这样现场出问题时,后台工程师不用到现场也能通过日志定位到具体哪一帧哪个字段不对。
5. 拿到源代码后怎么落地:移植与改造建议
5.1 移植前必须做好的三件事
第一件事是通读一遍规约标准和厂家的设备点表。CDT规约虽然是国标,但很多老设备的字节序、校验范围都有自己的一套解释;101规约的ASDU类型和传送原因在不同调度主站里也会有差异。不读文档直接改代码,等于盲人摸象。
第二件事是搭建离线仿真环境。把代码编译成一个PC上的命令行程序,用模拟主站软件对跑,先把功能链路全打通再上真实设备。这样做一次,后面到现场基本就是改改参数的事。
第三件事是确认目标平台的字节序和内存限制。很多嵌入式平台是大小端混合的,MODBUS的CRC和101的帧校验和都要按实际字节序处理。尽量用静态缓冲区,不要在中断里频繁动态分配内存,否则长时间运行后系统性能会越来越差。
5.2 让源码从“能跑”变成“好用”的优化方向
基础功能跑通之后,我的习惯是逐步加上这几样东西:报文日志功能(按时间、方向、类型存储,方便回溯)、链路质量统计(记录误码率、超时次数、重发次数)、配置化点表(把点号映射做成外部配置文件,不用改代码就能换站适配),以及多链路支持(比如同时跑CDT和101,做通道热切换)。
这些改造听起来多,但核心思想只有一个:让规约代码和具体站点配置解耦。源码本身只是通信工具,真正让它在不同现场都能用起来的,是配置和诊断能力。我见过很多项目,因为点表写死在代码里,换一个站就要重新编译一次,非常痛苦。把点表抽出来做成配置表之后,现场维护效率能提升一个量级。
我个人的体会是,规约源代码这东西,不怕旧,就怕没人真正读懂过。CDT的循环调度、101的状态机、MODBUS的CRC,看起来都是老技术,但每个现场的问题都藏在细节里。拿到代码先别急着编译下载,把帧格式、校验逻辑、状态流转这三条线走通,后面所有调试都会顺很多。
本文还有配套的精品资源,点击获取