数据的守护神:CRC 校验算法的极致优化

Evek Golden Lv4

CRC (循环冗余校验) 是嵌入式系统中数据完整性的最后一道防线。无论是固件升级 (OTA) 时的文件校验,还是 Modbus 通信的数据包检查,都离不开它。

1. 原理简述

CRC 的数学本质是模二除法
把数据看作一个巨大的二进制多项式 $D(x)$,用一个生成多项式 $G(x)$ 去除它,得到的余数 $R(x)$ 就是 CRC 校验码。
模二运算中,加减法等同于**异或 (XOR)**,没有进位借位,这使得它非常适合硬件实现。

2. 软实现之道:时间 vs 空间

在没有硬件 CRC 外设的单片机上,我们通常有两种软件实现方式。

2.1 按位移位法 (Bit-by-Bit) —— 省 Flash,费 CPU

这是最符合数学定义的方法。对每个字节的每一位进行计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
uint8_t CRC8_Slow(uint8_t *data, size_t len) {
uint8_t crc = 0;
while (len--) {
crc ^= *data++;
for (int i = 0; i < 8; i++) {
if (crc & 0x80)
crc = (crc << 1) ^ 0x07; // 0x07 是生成多项式
else
crc <<= 1;
}
}
return crc;
}
  • 优点:代码极短,Flash 占用极小。
  • 缺点:每个字节要循环 8 次,处理 1KB 数据需要 8KB 次循环,慢如蜗牛。

2.2 查表法 (Table-Driven) —— 费 Flash,省 CPU

我们可以预先计算出所有可能的字节 (0x00 ~ 0xFF) 经过 CRC 计算后的结果,存入一个 256 字节的数组(对于 CRC16/32 则是更大)。

1
2
3
4
5
6
7
8
9
10
11
// 预先算好的表
static const uint8_t crc_table[256] = { 0x00, 0x07, 0x0E, ... };

uint8_t CRC8_Fast(uint8_t *data, size_t len) {
uint8_t crc = 0;
while (len--) {
// 直接查表,O(1) 复杂度
crc = crc_table[crc ^ *data++];
}
return crc;
}
  • 优点:速度飞快,比移位法快 8 倍以上。
  • 缺点:CRC8 需 256 字节 ROM,CRC32 需 1KB ROM。

3. C++ 黑魔法:编译期生成表

手算或复制粘贴 256 个数到代码里是很蠢的。万一换了个多项式,整个表都要重算。
在 C++17 中,我们可以利用 constexpr 让编译器在编译阶段帮我们算出这个表!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <uint8_t Poly>
struct CRC8_Table {
uint8_t table[256];

constexpr CRC8_Table() : table{} {
for (int i = 0; i < 256; ++i) {
uint8_t crc = i;
for (int bit = 0; bit < 8; ++bit) {
if (crc & 0x80)
crc = (crc << 1) ^ Poly;
else
crc <<= 1;
}
table[i] = crc;
}
}
};

// 编译期生成,零运行时开销,零代码维护成本
constexpr auto crc_table = CRC8_Table<0x07>().table;

生成的汇编代码中,crc_table 就是一段已经算好的常量数据。

总结

  • 资源极度受限(Flash < 4KB):用移位法。
  • 追求速度(大部分场景):用查表法。
  • 追求优雅:用 C++ constexpr 生成查表。
  • Title: 数据的守护神:CRC 校验算法的极致优化
  • Author: Evek Golden
  • Created at : 2024-04-05 22:33:00
  • Updated at : 2026-06-12 08:57:02
  • Link: https://blog.cocodemo.uno/posts/crc7j2w/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments