第四章 数据链路层设计:帧结构定义、起始符与结束符、CRC校验算法实现、重传机制

数据链路层,说白了就是给原始比特流「穿上衣服」。裸数据在信道上跑,你根本分不清哪一段是有效信息,哪一段是噪声。我早年做工业总线时吃过这个亏——数据包粘在一起,解析器直接崩了。从那以后,我对帧结构设计就特别较真。

4.1 帧结构定义:给数据画个框

帧结构就像快递包裹。你得告诉接收方:包裹从哪开始、到哪结束、里面装了什么、有没有损坏。我习惯把帧分成五个部分:

字段 长度(字节) 说明
起始符 2 固定值 0xAA 0x55
长度域 2 从本字段后到CRC前的字节数
帧类型 1 0x01=数据帧,0x02=应答帧,0x03=重传帧
有效载荷 N 实际传输的数据,最大1024字节
CRC32 4 对长度域+帧类型+有效载荷计算
结束符 2 固定值 0x55 0xAA

你想想看,为什么起始符和结束符要设计成互补的?0xAA 是 10101010,0x55 是 01010101。这种交替模式在比特流中极难偶然出现。我在项目中遇到过,有些传感器在空闲时会输出全0或全1,如果起始符也是全0,那接收端就会频繁误触发。嗯,这里要注意。

4.2 起始符与结束符:边界守卫

起始符和结束符不只是两个固定字节那么简单。它们承担着「帧同步」的重任。我个人习惯在起始符后面加一个「逃逸字符」机制——如果有效载荷里出现了与起始符相同的字节,就在前面插入一个 0x7D 转义符,并把原字节异或 0x20。

核心原则:起始符和结束符在有效载荷中必须唯一。做不到唯一,就用转义。

举个例子:假设有效载荷里出现了 0xAA 0x55,发送端会把它变成 0x7D 0x8A 0x7D 0x75。接收端看到 0x7D,就知道下一个字节需要异或 0x20 还原。我曾经在一个视觉仿生项目中,因为没做转义处理,导致图像数据里连续出现 0xAA 0x55,接收端直接截断了帧——丢了一整张图。

避坑指南:我曾经在调试时发现,某些MCU的UART在空闲时会输出0xFF。如果结束符是0xFF,接收端就会在帧中间误判结束。所以结束符最好用0x55 0xAA这种非全1模式。

4.3 CRC校验算法实现:数据完整性守护神

CRC(循环冗余校验)不是简单的累加和。它用多项式除法来生成校验码,检错能力远强于奇偶校验。我推荐使用CRC32,多项式用 0x04C11DB7(IEEE标准)。

为什么不用CRC8或CRC16?视觉仿生系统传输的是图像数据,一帧可能几百KB。CRC8的碰撞概率太高,CRC16勉强够用,但CRC32能覆盖99.9999%以上的错误。说白了,多两个字节的代价,换一个安心。

下面是我常用的查表法实现,速度比逐位计算快10倍以上:

// CRC32 查表法实现
static uint32_t crc32_table[256];

void crc32_init_table(void) {
    uint32_t poly = 0x04C11DB7;
    for (uint32_t i = 0; i < 256; i++) {
        uint32_t crc = i << 24;
        for (int j = 0; j < 8; j++) {
            crc = (crc & 0x80000000) ? (crc << 1) ^ poly : (crc << 1);
        }
        crc32_table[i] = crc;
    }
}

uint32_t crc32_calc(uint8_t *data, uint32_t len) {
    uint32_t crc = 0xFFFFFFFF;
    for (uint32_t i = 0; i < len; i++) {
        uint8_t index = (crc >> 24) ^ data[i];
        crc = (crc << 8) ^ crc32_table[index];
    }
    return ~crc;
}

小技巧:初始化CRC寄存器为0xFFFFFFFF,最后取反。这样做的好处是,能检测出数据前面补0的错误。很多工程师忽略这一步,结果CRC的检错能力直接打折扣。

4.4 重传机制:丢了就补

CRC能检测错误,但不能纠正错误。一旦发现CRC校验失败,就得重传。我常用的策略是「超时重传+滑动窗口」。

超时重传很简单:发送端发出帧后,启动一个定时器。如果在规定时间内没收到ACK,就重传。超时时间怎么设?我建议用RTT(往返时间)的1.5倍。RTT可以通过历史数据动态估算。

滑动窗口呢?它允许发送端连续发多个帧,不用等每个帧的ACK。窗口大小我一般设4或8。窗口太大,重传代价高;窗口太小,信道利用率低。视觉仿生系统对实时性要求高,我建议窗口大小设为4,超时时间50ms。

重传策略对比:

  • 停等协议:发一帧等一帧。简单,但效率低。适合低速链路。
  • 回退N帧:窗口内任一帧超时,重传该帧及之后所有帧。实现简单,但浪费带宽。
  • 选择性重传:只重传丢失的帧。效率最高,但接收端需要缓存乱序帧。

我个人偏好选择性重传。虽然实现复杂一点,但在视觉仿生这种高带宽场景下,能省下不少带宽。我曾经在一个项目中,用停等协议传1080p图像,帧率只有15fps。换成选择性重传后,直接飙到30fps。

下面是我画的一个重传机制流程图,帮你理清逻辑:

重传机制流程图 发送端 接收端 发送帧N 启动超时定时器 接收帧N CRC校验 校验通过 校验失败 发送ACK 丢弃帧N ACK 收到ACK? 超时重传 发送下一帧 重传帧N 发送帧N+1

你看这个流程,发送端发出帧N后,启动定时器。接收端收到后做CRC校验,通过就回ACK,失败就丢弃。发送端如果在超时前收到ACK,就发下一帧;如果超时了还没收到ACK,就重传帧N。

这里有个细节:重传次数不能无限。我一般设3次重传上限,超过就上报链路异常。为什么是3次?你想想看,如果连续3次都失败,说明信道质量已经差到不可用了,再重传也是浪费带宽。

实战建议:重传定时器的超时时间不要设死。我习惯用指数退避——第一次超时50ms,第二次100ms,第三次200ms。这样做的好处是,在信道拥塞时不会加剧冲突。

好了,数据链路层的核心就这些。帧结构是骨架,CRC是体检医生,重传机制是急救队。三者配合好了,你的视觉仿生系统才能在嘈杂的信道里稳稳地跑起来。


公众号:蓝海资料掘金营,微信deep3321