3、IEC60870-5-101链路层编程:串口通信基础、CRC校验实现、链路层状态机编程
好,咱们今天来啃一块硬骨头——IEC 101的链路层编程。说实话,很多做电力通信的工程师,一提到链路层就头疼。为什么?因为串口通信本身就不太稳定,再加上CRC校验、状态机这些概念,确实容易让人绕晕。
但我个人觉得,只要把基础打牢,链路层其实没那么可怕。咱们今天就从串口通信基础开始,一步步把CRC校验和状态机都讲透。
3.1 串口通信基础:别小看这根线
串口通信,说白了就是一根线发数据,一根线收数据。IEC 101协议在串口上跑,用的是RS-232或RS-485标准。我刚开始做项目时,总觉得串口通信太简单了,结果在调试现场吃了不少亏。
先看几个关键参数:
| 参数 | 典型值 | 说明 |
|---|---|---|
| 波特率 | 9600、19200 | 电力系统常用,别用太高,容易丢包 |
| 数据位 | 8位 | IEC 101固定8位 |
| 停止位 | 1位 | 也可以2位,但1位够用 |
| 校验位 | 偶校验 | 很多老设备要求偶校验 |
嗯,这里要注意:波特率不是越高越好。我见过有人把波特率设到115200,结果在强电磁干扰环境下,数据全乱了。电力系统里,9600是黄金选择。
3.2 CRC校验实现:别让数据出错
CRC校验,全称是循环冗余校验。IEC 101用的是CRC-16,多项式是x^16 + x^12 + x^5 + 1。说白了,就是给数据包加个“指纹”,接收方用同样的算法算一遍,指纹对得上就说明数据没被篡改。
我曾经在一个变电站项目里,因为CRC实现有bug,导致遥测数据偶尔跳变。排查了三天,最后发现是查表法里的多项式写错了。从那以后,我写CRC代码都会反复核对多项式。
下面是我常用的CRC-16实现,查表法,效率高:
// CRC-16查表法实现
// 多项式:0x8005 (x^16 + x^12 + x^5 + 1)
uint16_t crc16_table[256];
void crc16_init_table() {
for (int i = 0; i < 256; i++) {
uint16_t crc = i << 8;
for (int j = 0; j < 8; j++) {
if (crc & 0x8000)
crc = (crc << 1) ^ 0x8005;
else
crc = crc << 1;
}
crc16_table[i] = crc;
}
}
uint16_t crc16_calc(uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF; // 初始值
for (uint16_t i = 0; i < len; i++) {
uint8_t index = (crc >> 8) ^ data[i];
crc = (crc << 8) ^ crc16_table[index];
}
return crc; // 不需要取反
}
3.3 链路层状态机编程:让通信有序进行
链路层状态机,说白了就是管理通信流程的“交通警察”。IEC 101的链路层有几种状态:空闲态、发送态、接收态、等待确认态。为什么需要状态机?因为串口通信是半双工的,同一时间只能发或收,状态机就是用来协调这个的。
我画了一张状态机图,方便你理解:
状态机的实现,我习惯用switch-case结构,清晰又好维护。下面是一个简化版:
typedef enum {
LINK_IDLE, // 空闲态
LINK_SEND, // 发送态
LINK_RECV, // 接收态
LINK_WAIT_ACK // 等待确认态
} LinkState;
LinkState current_state = LINK_IDLE;
void link_state_machine(uint8_t event) {
switch (current_state) {
case LINK_IDLE:
if (event == EVENT_SEND_REQ) {
// 有数据要发送
uart_send_frame();
current_state = LINK_SEND;
} else if (event == EVENT_RECV_START) {
// 收到起始字符
uart_recv_frame();
current_state = LINK_RECV;
}
break;
case LINK_SEND:
if (event == EVENT_SEND_DONE) {
// 发送完成,等待确认
start_timer(TIMEOUT_100MS);
current_state = LINK_WAIT_ACK;
}
break;
case LINK_RECV:
if (event == EVENT_RECV_DONE) {
// 接收完成,校验CRC
if (crc16_calc(rx_buf, rx_len) == 0) {
process_frame();
}
current_state = LINK_IDLE;
}
break;
case LINK_WAIT_ACK:
if (event == EVENT_ACK_RECV) {
// 收到确认
stop_timer();
current_state = LINK_IDLE;
} else if (event == EVENT_TIMEOUT) {
// 超时,重发
uart_send_frame();
start_timer(TIMEOUT_100MS);
// 重发次数限制,超过则报错
}
break;
}
}
3.4 实战中的坑与经验
最后,分享几个我在项目中踩过的坑:
- 串口缓冲区溢出:我曾经在接收数据时,没及时清空缓冲区,结果下一帧数据来了,把上一帧覆盖了。解决方案是用环形缓冲区。
- CRC计算范围搞错:IEC 101的CRC只算数据部分,不包括起始字符和长度。我一开始没注意,调试了两天才发现。
- 状态机死锁:有一次,设备发送数据后没收到确认,状态机卡在等待确认态,再也不动了。后来加了超时机制,问题解决。
嗯,链路层编程就是这样,细节决定成败。你只要把串口通信、CRC校验、状态机这三个基础打牢,IEC 101的链路层就没什么能难住你的了。