Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

4.4 CRC引擎与Safety Queue——数学与工程的双打

场景:当 RAM 的某个 bit 被宇宙射线击中

这不是科幻:海拔每升高 1000 米,中子通量增加约一倍。在海拔 3000 米的盘山公路上,你的 ECU 的 SRAM 存储着从传感器队列传来的转向角数据,包含 12 个历史采样点。一个中子击中某个 SRAM 位元,就能把 0x1A3F 变成 0x0A3F,驱动电机将执行一个比真实值偏差 4096 个单位的错误角度。

你已经知道了 E2E 怎么保护正在传输的数据。但已经到达 ECU 内部、暂存在 RAM 中的数据呢?CRC 引擎对数据做数学签名,Safety_Queue 把签名嵌入数据结构,两把锁一起锁住 RAM 中的每个字节。

一、Crc_32.c:从数学定义到工程代码

打开 SafeLib/Crc/src/Crc_32.c,232 行。第一印象,三种模式的编译切换:

#ifndef Crc_32_Mode
#define Crc_32_Mode 0
#endif

#if Crc_32_Mode == CRC_32_HARDWARE
#error "Crc_32_Mode is set to CRC_32_HARDWARE which isn't supported"
#endif

Arctic Core 不支持硬件 CRC 加速(但留了接口)。两个软件模式:

  • CRC_32_RUNTIME:逐位计算,用于验证查表的正确性
  • CRC_32_TABLE:256 项预计算表,性能优化

IEEE-802.3 的参数定义:

#define Crc_32_StartValue    0xFFFFFFFFU
#define Crc_32_Polynomial    0x04C11DB7U
#define Crc_32_Xor           0xFFFFFFFFU

二、运行时 CRC:reflect 的艺术

IEEE-802.3 CRC32 有一个反直觉的设计,数据要先按位反射(reflect),结果也要反射。看实现:

static INLINE uint32 reflectResult(uint32 data)
{
    uint32 reflection = 0x00000000U;
    uint8 bit;
    uint32 tmpData = data;

    for (bit = 0; bit < 32; bit++) {
        if ((tmpData & 0x01U) != 0) {
            reflection |= (1UL << (31UL - bit));
        }
        tmpData = tmpData >> 1;
    }
    return reflection;
}

static INLINE uint8 reflectInData(uint8 data)
{
    uint8 reflection = 0x00;
    uint8 bit;
    uint32 tmpData = data;

    for (bit = 0; bit < 8; bit++) {
        if ((tmpData & 0x01U) != 0) {
            reflection |= (uint8)(1U << (7U-bit));
        }
        tmpData = (tmpData >> 1U);
    }
    return reflection;
}

reflectResult 把 32-bit 输入按位反转:bit0→bit31, bit1→bit30,…。reflectInData 把 8-bit 数据按字节内反转:bit0→bit7,…。这是 LSB-first CRC 算法要求的预处理,与直觉中的“高位先除“相反。

逐位计算的 CRC32:

static uint32 calculateCRC32(const uint8* message,
    uint32 nBytes, uint32 start)
{
    uint32 remainder = reflectResult(start);
    uint8  bit;
    const uint32  topbit = 0x80000000U;

    if (message != NULL_PTR) {
        for (uint32 byte = 0; byte < nBytes; byte++) {
            uint32 reflectedData = reflectInData(*message);
            remainder ^= (reflectedData << 24u);
            message++;

            for (bit = 8; bit > 0; bit--) {
                if ((remainder & topbit) != 0) {
                    remainder = (remainder << 1u)
                        ^ Crc_32_Polynomial;
                }
                else {
                    remainder = (remainder << 1u);
                }
            }
        }
    }
    return reflectResult(remainder);
}

算法流程:

  1. 反射起始值reflectResult(start)
  2. 每个字节:反射输入移到高位XOR 到余数
  3. 8 轮 bit-by-bit:最高位为 1 则左移并 XOR 多项式,否则只左移
  4. 反射最终余数返回

这个版本不是生产用的(它是 O(n×8)),它存在的意义是验证查表版本的正确性,运行时用查表,单元测试用逐位计算,两者交叉比对。

三、查表 CRC:256个魔术数字

生产代码用的是 CRC_32_TABLE 模式。256 个 uint32 预先计算好(每 8 个一组是表中的一行):

uint32 Crc_CalculateCRC32(const uint8* Crc_DataPtr,
    uint32 Crc_Length, uint32 Crc_StartValue32,
    boolean Crc_IsFirstCall)
{
    uint32 crc = 0;
#if Crc_32_Mode == CRC_32_TABLE
    static const uint32 Crc_32_Tab[] = {
        0x00000000U,0x77073096U,0xEE0E612CU,0x990951BAU,
        0x076DC419U,0x706AF48FU,0xE963A535U,0x9E6495A3U,
        0x0EDB8832U,0x79DCB8A4U,0xE0D5E91EU,0x97D2D988U,
        0x09B64C2BU,0x7EB17CBDU,0xE7B82D07U,0x90BF1D91U,
        // ... 256 entries total ...
        0xB40BBE37U,0xC30C8EA1U,0x5A05DF1BU,0x2D02EF8D };
#endif

    if (Crc_DataPtr != NULL_PTR) {
        crc = (TRUE == Crc_IsFirstCall) ?
            Crc_32_StartValue :
            (Crc_StartValue32 ^ Crc_32_Xor);

#if Crc_32_Mode == CRC_32_TABLE
        for(uint32 byte = 0; byte < Crc_Length; byte++) {
            crc = ((crc >> 8) & 0x00FFFFFFU)
                ^ Crc_32_Tab[(crc ^ *Crc_DataPtr) & 0xFFU];
            Crc_DataPtr++;
        }
#endif
        crc = crc ^ Crc_32_Xor;
    }
    return crc;
}

一行代码就完成了一个字节的 CRC 更新:

crc = ((crc >> 8) & 0x00FFFFFFU) ^ Crc_32_Tab[(crc ^ *Crc_DataPtr) & 0xFFU];

这是 CRC32 查表算法的经典形式,表索引 = (crc XOR data_byte) & 0xFF,新 crc = (crc >> 8) XOR 表值。O(n) 复杂度,1 字节/次迭代。

Crc_IsFirstCall 参数是连续 CRC 计算的关键:第一次调用用标准 StartValue(0xFFFFFFFF),后续调用用传入的 Crc_StartValue32 ^ Crc_32_Xor 抵消上一次的最终 XOR。这样可以将一个大数据块的 CRC 分多次调用计算,中间结果保持一致性。Safety_Queue 就利用了这一点:它分两次计算队列结构体的 CRC 和数据缓冲区的 CRC。

四、Safety_Queue.c:双 CRC 锁

打开 Safety_Queue.c,首先注意到的是模板化的 API:

Queue_ReturnType Safety_Queue_Init(Safety_Queue_t *queue,
    void *buffer, uint8 max_count, size_t dataSize,
    cmpFunc compare_func)
{
    SYS_CALL_SuspendOSInterrupts();
    // ... validation ...
    queue->bufStart = buffer;
    queue->bufEnd = (char *) buffer + (max_count * dataSize);
    queue->head = queue->bufStart;
    queue->tail = queue->bufStart;
    queue->dataSize = dataSize;
    queue->count = 0u;
    queue->max_count = max_count;
    queue->compare_func = *compare_func;

    queue->bufferCrc = Crc_CalculateCRC8(queue->bufStart,
            queue->dataSize * queue->max_count, 0u, 1u);
    queue->isInit = TRUE;

    size_t without_buffer_size =
        (char*) &(queue->queueCrc) - (char*) queue;
    queue->queueCrc = Crc_CalculateCRC8((void*) queue,
            without_buffer_size, 0u, 0u);

    SYS_CALL_ResumeOSInterrupts();
    return QUEUE_E_OK;
}

注意 (char*) &(queue->queueCrc) - (char*) queue 这个指针运算,它计算出从结构体头部到 queueCrc 字段之间的字节数。这样 CRC 计算只覆盖 queueCrc 之前的字段(queueCrc 本身不参与),避免了“CRC 覆盖自己“的递归问题。这是两个 CRC 的“管辖范围“:

  1. bufferCrc:覆盖整个数据缓冲区(dataSize * max_count 字节)
  2. queueCrc:覆盖结构体元数据(head、tail、count、bufferCrc… 但不包括 queueCrc 自己)

五、Add 操作:写前验证

每次 Safety_Queue_Add 的第一步不是写入数据,而是验证全完整性

Queue_ReturnType Safety_Queue_Add(Safety_Queue_t *queue,
    void const *dataPtr)
{
    SYS_CALL_SuspendOSInterrupts();
    // ... NULL/init checks ...

    uint8 currBufferCrc = Crc_CalculateCRC8(queue->bufStart,
            queue->dataSize * queue->max_count, 0u, 1u);

    size_t without_buffer_size =
        (char*) &(queue->queueCrc) - (char*) queue;
    uint8 currQueueCrc = Crc_CalculateCRC8((void*) queue,
            without_buffer_size, 0u, 0u);

    if ((queue->bufferCrc != currBufferCrc)
            || (queue->queueCrc != currQueueCrc)) {
        SYS_CALL_ResumeOSInterrupts();
        return QUEUE_E_CRC_ERR;
    }

    // Queue is full check
    if (queue->count == queue->max_count) {
        queue->bufFullFlag = TRUE;
        queue->queueCrc = Crc_CalculateCRC8((void*) queue,
            without_buffer_size, 0u, 1u);
        SYS_CALL_ResumeOSInterrupts();
        return QUEUE_E_FULL;
    }

    MEMCPY(queue->head, dataPtr, queue->dataSize);
    queue->head = (char *) queue->head + queue->dataSize;

    if (queue->head == queue->bufEnd) {
        queue->head = queue->bufStart;  // Wrap-around
    }
    queue->count++;

    // Update CRC values
    queue->bufferCrc = Crc_CalculateCRC8(queue->bufStart,
            queue->dataSize * queue->max_count, 0u, 1u);
    queue->queueCrc = Crc_CalculateCRC8((void*) queue,
            without_buffer_size, 0u, 1u);

    SYS_CALL_ResumeOSInterrupts();
    return QUEUE_E_OK;
}

操作序列:

  1. 关中断SYS_CALL_SuspendOSInterrupts):防止 check-then-act 竞态
  2. CRC 完整性校验:数据损坏直接返回 QUEUE_E_CRC_ERR
  3. 满检查,满时返回 QUEUE_E_FULL 并更新 queueCrc(bufFullFlag 变了)
  4. MEMCPY 写入数据
  5. 更新 head / count / wrap-around
  6. 重新计算两组 CRC 并存档
  7. 开中断

Next 操作有相同模式,还在出队时额外检测 bufFullFlag,如果出队时 flag 为 TRUE,表示之前满过,返回 QUEUE_E_LOST_DATA 告知调用者可能有数据丢失。

六、Peek 操作:CRC 保护的无副作用读

Peek 提供了不修改队列状态的数据读取:

Queue_ReturnType Safety_Queue_Peek(Safety_Queue_t const *queue,
    void *dataPtr)
{
    SYS_CALL_SuspendOSInterrupts();
    // validation, CRC check...
    if ((queue->bufferCrc != currBufferCrc)
            || (queue->queueCrc != currQueueCrc)) {
        SYS_CALL_ResumeOSInterrupts();
        return QUEUE_E_CRC_ERR;
    }
    if (queue->count == 0u) {
        SYS_CALL_ResumeOSInterrupts();
        return QUEUE_E_NO_DATA;
    }

    MEMCPY((void*) dataPtr, queue->tail, queue->dataSize);

    SYS_CALL_ResumeOSInterrupts();
    return QUEUE_E_OK;
}

Peek 之后队列的 CRC 不需要更新,因为没有修改任何数据。但读之前仍然执行了完整的 CRC 验证。这确保了即使读取操作静默发生,返回的数据仍然可信。

七、误差类型学:返回码即安全信号

Safety_Queue 定义的返回码映射了一个完整的安全状态空间:

返回码含义安全影响
QUEUE_E_OK操作成功继续
QUEUE_E_NULL空指针调用者代码错误
QUEUE_E_NO_INIT未初始化调用时序错误
QUEUE_E_CRC_ERRCRC校验失败硬件故障
QUEUE_E_FULL队列满可能丢数据
QUEUE_E_NO_DATA队列空无数据可读
QUEUE_E_LOST_DATA出队时发现历史丢帧数据窗口有空洞
QUEUE_E_ALREADY_INIT重复初始化逻辑错误
QUEUE_E_TRUE / QUEUE_E_FALSEContains 结果搜索答案

QUEUE_E_CRC_ERR 是最关键的错误,它意味着 RAM 中的数据静默损坏了。调用者收到这个错误后通常应该:

  1. 记录 DEM 事件(硬件故障证据)
  2. 重新初始化队列(清空损坏数据)
  3. 如果反复发生→触发安全降级

Safety_Queue 的双 CRC 设计解决了一个经典难题:如何检测 RAM 数据的静默损坏而不依赖硬件 ECC?答案是对元数据和数据分别做 CRC8 签名,每次操作前验证、操作后更新。CRC8 只有 256 分之一的理论碰撞概率,但结合数据长度和实际内存访问模式,检测覆盖率远超 99.9%,而且 CRC 计算是 O(n) 无分支操作,对于数十到数百字节的典型队列大小,开销在微秒级。

本篇小结

  1. CRC32 引擎支持两种模式:CRC_32_RUNTIME(逐位计算,O(n×8),用于单元测试交叉验证)和 CRC_32_TABLE(256 项预计算查表,O(n),生产模式),通过 Crc_IsFirstCall 参数支持分多次连续计算大数据块。
  2. IEEE-802.3 CRC32 要求数据先按位反射(reflect)再计算,结果再反射,reflect 操作精确到每个 bit 的逐位反转,这是 LSB-first 算法区别于直觉“高位先除“的核心。
  3. Safety_Queue 采用双 CRC8 保护策略:bufferCrc 覆盖整个数据缓冲区,queueCrc 覆盖结构体元数据(head/tail/count 等),前者检测数据静默损坏,后者检测元数据篡改,两者互不重叠。
  4. 每次 Add/Next/Peek 操作前先执行完整 CRC 验证、操作后重新计算并更新 CRC,期间关中断防止竞态;CRC 校验失败返回 QUEUE_E_CRC_ERR,调用者应记录 DEM 事件并触发安全降级。
  5. Safety_Queue 的返回码映射了完整的安全状态空间(OK/NULL/NO_INIT/CRC_ERR/FULL/NO_DATA/LOST_DATA/ALREADY_INIT),其中 QUEUE_E_LOST_DATA 通过 bufFullFlag 检测历史溢出,填补了“满→出队→数据空洞“的检测盲区。

【下集预告】: 数据保护做得再严密,如果存储数据的 SRAM 本身就有物理缺陷呢?下一节进入 RamTst.c 的 561 行,看 March X 算法如何在四遍升序/降序遍历中,用精心选择的读写模式逐一检测 stuck-at-0、stuck-at-1、耦合故障和地址线解码错误。它的设计哲学和本节的双 CRC 如出一辙:你无法修复一个损坏的存储单元,但你必须在上电后 0.3 秒内发现它,然后拒绝启动这台 ECU。