4. 链路层协议设计:HDLC帧结构、CRC校验算法选择(CRC8/16/32)、帧同步与逃逸字符处理
链路层协议,说白了就是给数据包穿上“防弹衣”。
我做了这么多年嵌入式通信,发现很多新手工程师最容易栽跟头的地方,恰恰就是链路层。你想想看,物理层把比特流传过来了,但怎么知道一帧从哪里开始、到哪里结束?数据传错了怎么发现?遇到和帧头一样的字节怎么办?
这些问题,HDLC协议都给出了经典的解决方案。今天我们就来拆解一下。
4.1 HDLC帧结构:数据包的“集装箱”
HDLC(高级数据链路控制)是一种面向比特的链路层协议。它的帧结构非常规整,像集装箱一样,每个部分都有明确分工。
标准的HDLC帧由以下几个字段组成:
| 字段 | 长度 | 说明 |
|---|---|---|
| 标志(Flag) | 8 bit | 固定为 0x7E,表示帧的开始和结束 |
| 地址(Address) | 8/16 bit | 标识站地址,我习惯用8位,够用 |
| 控制(Control) | 8/16 bit | 帧类型(信息帧/监控帧/无编号帧) |
| 信息(Information) | 可变 | 真正的载荷数据 |
| 帧校验(FCS) | 8/16/32 bit | CRC校验值 |
| 标志(Flag) | 8 bit | 0x7E,帧结束 |
嗯,这里要注意:两个Flag之间的所有内容,才是完整的一帧。接收端只要检测到0x7E,就知道一帧开始了。
核心要点:Flag字段0x7E是帧同步的关键。但问题来了——如果数据里也出现了0x7E怎么办?这就引出了逃逸字符处理。
4.2 帧同步与逃逸字符处理:别让0x7E捣乱
我在项目中遇到过最头疼的问题,就是数据里恰好出现了0x7E。接收端一看,哎?又一个Flag?于是帧就提前结束了,后面的数据全乱套。
HDLC的解决方案是“字节填充法”,也叫逃逸字符处理。具体做法:
- 发送端:遇到0x7E,替换成0x7D 0x5E
- 发送端:遇到0x7D,替换成0x7D 0x5D
- 接收端:遇到0x7D,把后面的字节异或0x20还原
说白了,就是给特殊字符加个“反斜杠”。
我曾经在一个无人机数传项目里,因为没处理好逃逸字符,导致遥控指令偶尔丢失。排查了三天,最后发现是某个遥测数据包里的0x7E把帧同步搞崩了。从那以后,我每次设计链路层,第一件事就是检查逃逸逻辑。
实战技巧:逃逸处理一定要在CRC计算之后做。先算CRC,再加逃逸。接收端先解逃逸,再验CRC。顺序错了,校验就废了。
4.3 CRC校验算法选择:CRC8/16/32怎么选?
CRC(循环冗余校验)是链路层的“体检医生”。它用多项式除法算出一个校验值,接收端用同样的方法算一遍,对比就知道数据有没有被篡改。
选哪种CRC,取决于你的应用场景。我列个表,你一看就明白:
| 算法 | 校验位长度 | 检测能力 | 适用场景 |
|---|---|---|---|
| CRC-8 | 8 bit | 能检测所有单比特错误、双比特错误 | 短数据包、传感器节点、低功耗设备 |
| CRC-16 | 16 bit | 能检测所有单/双比特错误、奇数个错误 | 工业总线、Modbus、一般地面站链路 |
| CRC-32 | 32 bit | 能检测所有长度≤32的突发错误 | 文件传输、卫星通信、高可靠性场景 |
我个人习惯:地面站通信链路,至少用CRC-16。如果数据包长度超过256字节,我建议直接上CRC-32。别省那点计算量,数据错了重传更费时间。
为什么会这样?因为CRC-16的漏检率大约是1/65536,而CRC-32是1/4.3e9。你想想看,无人机在天上飞,一个误码导致执行了错误指令,后果是什么?
避坑指南:我曾经在一个项目里用了CRC-8,结果在强电磁干扰环境下,误码率飙升到1%。CRC-8根本扛不住,三天两头丢包。后来换成CRC-16,问题立刻解决。所以,别在可靠性上省钱。
4.4 知识体系核心逻辑
下面这张图,是我自己总结的链路层设计核心逻辑。你看一遍,基本就掌握了:
4.5 代码示例:CRC-16计算与逃逸处理
光说不练假把式。我贴一段实际项目里用过的代码,你感受一下:
// CRC-16计算(多项式0x8005,初始值0x0000)
uint16_t crc16_calc(uint8_t *data, uint16_t len) {
uint16_t crc = 0x0000;
for (uint16_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0x8005;
} else {
crc >>= 1;
}
}
}
return crc;
}
// 逃逸字符处理(发送端)
uint16_t hdlc_escape(uint8_t *src, uint16_t src_len, uint8_t *dst) {
uint16_t dst_len = 0;
for (uint16_t i = 0; i < src_len; i++) {
if (src[i] == 0x7E) {
dst[dst_len++] = 0x7D;
dst[dst_len++] = 0x5E;
} else if (src[i] == 0x7D) {
dst[dst_len++] = 0x7D;
dst[dst_len++] = 0x5D;
} else {
dst[dst_len++] = src[i];
}
}
return dst_len;
}
个人经验:CRC查表法比直接计算快很多。如果MCU资源允许,我建议预生成一个256字节的查表数组。在STM32F103上,查表法比逐位计算快了将近10倍。
4.6 总结
链路层设计,说白了就是三件事:
- 帧结构:用Flag把数据包框起来,让接收端知道从哪里开始、到哪里结束
- 逃逸处理:防止数据里的0x7E和0x7D捣乱,用字节填充法解决
- CRC校验:选对算法(至少CRC-16),确保数据完整性
嗯,这些基础打牢了,后面讲重传机制、流量控制的时候,你才能游刃有余。
可可精品资料库