CRC8 校验原理与实现详解(附可交互动画演示)
前言
在嵌入式通信(CAN、串口、I2C、1-Wire)中,CRC8 是最常见的轻量级数据完整性校验。很多人会"抄一段代码能用",但说不清 if (crc & 0x80) crc = (crc << 1) ^ poly; 这一行到底在干什么。
本文从数学原理 讲到逐位实现 ,再配一个可单步播放的 HTML 动画,让你亲眼看到 CRC 寄存器每一位的翻转过程。读完后,任何 CRC 变体(不同 poly/init/反射/xorout)你都能看懂。
一、CRC8 的本质:GF(2) 上的多项式除法
CRC 不是哈希,也不是累加和。它的数学定义是:
把数据看成 GF(2) 上的多项式,乘以 x⁸ 后对生成多项式 G(x) 做模 2 除法,取余数。
1. 数据 = 多项式
一个字节 0b10110001,每一位是多项式系数:
0b10110001 = x⁷ + x⁵ + x⁴ + 1
多个字节依次排列,构成更长的多项式。例如 0x01 0x02:
0x01 = 0000_0001 → x⁰
0x02 = 0000_0010 → x¹
拼成 16 位 → x⁸ + x¹
2. GF(2) 运算:加法 = 异或
普通加法有进位,GF(2) 没有:
1 + 1 = 0 1 + 0 = 1 0 + 0 = 0
这正是 XOR 的真值表。所以在 GF(2) 里:
- 加法 = 减法 = 异或
- 没有进位、没有借位
这就是 CRC 代码里全是 ^、没有 +/- 的原因。
3. 生成多项式 G(x)
CRC8 的除数是 8 次多项式。以 poly = 0x07 为例:
0x07 = 0b0000_0111 → x² + x + 1
完整 G(x) = x⁸ + x² + x + 1 = 0b1_0000_0111 = 0x107
常见变体:
| 名称 | poly | 初值 | 反射 | xorout | 典型用途 |
|---|---|---|---|---|---|
| CRC-8/ATM | 0x07 | 0x00 | 否 | 0x00 | 通用 |
| CRC-8/SAE-J1850 | 0x1D | 0xFF | 否 | 0xFF | 汽车 CAN 应用层 |
| CRC-8/AUTOSAR | 0x2F | 0xFF | 否 | 0xFF | AUTOSAR |
| CRC-8/MAXIM | 0x31 | 0x00 | 是 | 0x00 | DS18B20、1-Wire |
二、模 2 长除法怎么做
CRC 计算就是一次模 2 长除法:
被除数 = 数据 × x⁸ (数据左移 8 位,末尾补 8 个 0)
除数 = G(x)
余数 = CRC8
以 data = 0x01、G(x) = 0x107 为例:
1
_________
0x107 ) 0x100
0x107
-----
0x007
规则(GF(2) 版):
- 被除数最高位为 1 → 商 1,异或 G(x)
- 被除数最高位为 0 → 商 0,只移位
- 每轮消掉最高位,直到剩余 < 8 位
余数 0x07 就是 CRC 值。
三、逐位算法:把长除法变成流水线
长除法要一次性拿到完整被除数,但数据是逐字节来的。逐位算法把除法"流水线化":
寄存器 crc 存"当前部分余数"
每处理一位:
crc <<= 1 // 移入下一位(长除法对齐)
if (最高位 == 1)
crc ^= poly // 减 G(x) 低 8 位(消最高位)
循环 8 次 = 处理完一个字节。
为什么异或的是 0x07 不是 0x107
crc << 1 时,第 8 位(值 0x100)已移出 8 位寄存器被丢弃,所以:
(crc << 1) ^ 0x107 ≡ (crc << 1) ^ 0x07
G(x) 最高位的 1 在移位时自动抵消。这就是代码里 poly 只存低 8 位的原因。
完整代码(C)
c
static uint8_t crc8(const uint8_t* data, size_t len) {
const uint8_t poly = 0x07;
uint8_t crc = 0x00;
for (size_t i = 0; i < len; ++i) {
crc ^= data[i];
for (int j = 0; j < 8; ++j) {
if (crc & 0x80)
crc = (crc << 1) ^ poly;
else
crc <<= 1;
}
}
return crc;
}
四、交互式动画演示
把下面代码存为 crc8-demo.html,浏览器打开即可单步观看每一位的翻转。改 DATA 数组可换数据。
html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>CRC8 逐位动画演示</title>
<style>
body { font-family: monospace; background:#1e1e1e; color:#ddd; padding:24px; }
h2 { color:#4ec9b0; }
.row { margin:10px 0; font-size:20px; }
.bits { display:inline-flex; gap:4px; }
.bit { width:34px; height:34px; line-height:34px; text-align:center;
border:1px solid #555; border-radius:4px; background:#2d2d2d; }
.bit.msb { border-color:#e06c75; color:#e06c75; }
.bit.on { background:#4ec9b0; color:#111; font-weight:bold; }
.op { color:#dcdcaa; margin-left:16px; }
.data { letter-spacing:2px; color:#ce9178; }
button { padding:6px 14px; margin-right:8px; background:#0e639c; color:#fff;
border:none; border-radius:4px; cursor:pointer; }
button:disabled { background:#444; cursor:default; }
#log { margin-top:16px; max-height:300px; overflow-y:auto; font-size:13px;
color:#999; border-top:1px solid #333; padding-top:8px; }
</style>
</head>
<body>
<h2>CRC-8/ATM 逐位动画 (poly=0x07, init=0x00)</h2>
<div class="row">数据: <span class="data" id="dataStr"></span></div>
<div class="row">当前字节: <span id="curByte" class="data">-</span></div>
<div class="row">CRC 寄存器: <span class="bits" id="crcBits"></span>
<span class="op" id="op"></span></div>
<div class="row">
<button id="btnStep">单步</button>
<button id="btnAuto">自动播放</button>
<button id="btnReset">重置</button>
</div>
<div id="log"></div>
<script>
const POLY = 0x07;
const DATA = [0x01, 0x02]; // 想换数据改这里
let crc, byteIdx, bitIdx, state, timer;
const logEl = document.getElementById('log');
function bitsOf(v) {
return [...Array(8)].map((_,i) => (v >> (7-i)) & 1);
}
function renderBits(el, v) {
el.innerHTML = '';
bitsOf(v).forEach((b,i) => {
const d = document.createElement('span');
d.className = 'bit' + (i===0 ? ' msb':'') + (b ? ' on':'');
d.textContent = b;
el.appendChild(d);
});
}
function log(s) {
const p = document.createElement('div');
p.textContent = s;
logEl.appendChild(p);
logEl.scrollTop = logEl.scrollHeight;
}
function render() {
document.getElementById('dataStr').textContent =
DATA.map(b => '0x'+b.toString(16).padStart(2,'0')).join(' ');
document.getElementById('curByte').textContent =
byteIdx < DATA.length ? '0x'+DATA[byteIdx].toString(16).padStart(2,'0') : '完成';
renderBits(document.getElementById('crcBits'), crc);
}
function reset() {
clearInterval(timer); timer=null;
crc = 0x00; byteIdx = 0; bitIdx = 0; state = 'XOR';
logEl.innerHTML = '';
document.getElementById('op').textContent = 'init = 0x00';
log('开始: crc = 0x00');
render();
document.getElementById('btnAuto').disabled = false;
document.getElementById('btnStep').disabled = false;
}
function step() {
if (byteIdx >= DATA.length) {
document.getElementById('op').textContent =
'最终 CRC = 0x'+crc.toString(16).padStart(2,'0').toUpperCase();
log('完成: CRC = 0x'+crc.toString(16).padStart(2,'0').toUpperCase());
clearInterval(timer); timer=null;
return;
}
if (state === 'XOR') {
const before = crc, b = DATA[byteIdx];
crc ^= b;
log(`字节 ${byteIdx}: crc ^= 0x${b.toString(16).padStart(2,'0')} ` +
`0x${before.toString(16).padStart(2,'0')} -> 0x${crc.toString(16).padStart(2,'0')}`);
document.getElementById('op').textContent = 'crc ^= byte';
state = 'BIT'; bitIdx = 0;
render();
return;
}
const msb = (crc & 0x80) ? 1 : 0;
const before = crc;
let op;
if (msb) { crc = ((crc << 1) & 0xFF) ^ POLY; op = 'MSB=1: (crc<<1)^0x07'; }
else { crc = (crc << 1) & 0xFF; op = 'MSB=0: crc<<1'; }
log(` 位 ${bitIdx}: 0x${before.toString(16).padStart(2,'0')} ${op} -> 0x${crc.toString(16).padStart(2,'0')}`);
document.getElementById('op').textContent = op;
render();
bitIdx++;
if (bitIdx >= 8) { byteIdx++; state = 'XOR'; }
}
document.getElementById('btnStep').onclick = step;
document.getElementById('btnReset').onclick = reset;
document.getElementById('btnAuto').onclick = () => {
if (timer) return;
timer = setInterval(step, 400);
document.getElementById('btnAuto').disabled = true;
};
reset();
</script>
</body>
</html>
怎么看这个动画
- 点 重置,寄存器全 0。
- 点 单步 :第一帧是"当前字节异或进寄存器",日志会打印
crc ^= byte。 - 继续单步:每帧显示寄存器 8 位,最高位红色高亮 。若最高位为 1,操作是
(crc<<1)^0x07;为 0 则crc<<1。 - 8 步后进入下一字节,重复。
- 全部字节处理完,显示最终 CRC。
把 DATA 改成 [0x01] 单字节,单步走一遍,对照下面手算表逐帧核对。
五、手算对照:data = 0x01
| 步 | crc 进入 | 最高位 | 操作 | 结果 |
|---|---|---|---|---|
| 异或 | 0x00 ^ 0x01 | --- | crc ^= byte |
0x01 |
| 1 | 0b0000_0001 | 0 | <<1 |
0x02 |
| 2 | 0b0000_0010 | 0 | <<1 |
0x04 |
| 3 | 0b0000_0100 | 0 | <<1 |
0x08 |
| 4 | 0b0000_1000 | 0 | <<1 |
0x10 |
| 5 | 0b0001_0000 | 0 | <<1 |
0x20 |
| 6 | 0b0010_0000 | 0 | <<1 |
0x40 |
| 7 | 0b0100_0000 | 0 | <<1 |
0x80 |
| 8 | 0b1000_0000 | 1 | (0x80<<1)^0x07 |
0x07 |
前 7 步最高位为 0 只移位,第 8 步最高位为 1 才异或 0x07。结果 0x07,与长除法一致。
六、验证:标准自检向量
CRC-8/ATM 对 "123456789" 的标准结果是 0xF4。写完代码务必先跑这个向量:
c
#include <assert.h>
const uint8_t check[] = {'1','2','3','4','5','6','7','8','9'};
assert(crc8(check, 9) == 0xF4);
把动画里的 DATA 改成:
js
const DATA = [0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39];
自动播放到结尾,显示 0xF4 即验证通过。
其它变体的自检值(同样对 "123456789"):
| 变体 | 期望值 |
|---|---|
| CRC-8/ATM (0x07) | 0xF4 |
| CRC-8/SAE-J1850 (0x1D) | 0x4B |
| CRC-8/AUTOSAR (0x2F) | 0xDF |
| CRC-8/MAXIM (0x31, 反射) | 0xA1 |
七、init 和 xorout 是什么
纯模 2 除法只由数据和 G(x) 决定。实际协议还加两个"调味":
- init(初值) :除法前寄存器设为该值。
0x00从零开始;0xFF用于避免前导零不影响 CRC。 - xorout(结果异或) :最后余数再异或常数。
0xFF常见于汽车协议,不改变检错能力,只改变结果分布。
反射(refin/refout)则是把数据/多项式按位反转,对应"低位先传输"的协议(如 1-Wire)。反射版移位方向改为右移,poly=0x31 反射后是 0x8C。
八、为什么 CRC 能检错
传输出错相当于被除数多了错误多项式 E(x)。接收方余数变为:
原余数 ^ (E(x)·x⁸ mod G(x))
只要 E(x) 不是 G(x) 的倍数,余数就变,错误被检出。G(x) 选得好(如 0x07 含 x+1 因子)可保证:
- 所有单比特错误必检出
- 所有双比特错误必检出(8 位 CRC 范围内)
- 大多数突发错误检出
这是 CRC 比简单累加和强的原因------有代数结构保证,不靠运气。
九、工程避坑
- 参数必须收发一致:poly/init/反射/xorout 四项任一不同,结果就不同。协议文档要写清"CRC-8/ATM"而非只写"CRC8"。
- 明确 CRC 覆盖范围:是全部载荷,还是含 ID/DLC?两端必须一致。
- 与 CAN 硬件 CRC 区分:CAN 帧自带 CRC15 由 bxCAN 硬件完成,软件层加的 CRC8 是应用层额外校验,两者不冲突。
- 查表法 vs 逐位法:逐位法代码小、适合低频;查表法快 8 倍、适合高频。参数运行时可变时只能用逐位法。
- 先过自检向量再上板 :
"123456789" → 0xF4通过再集成,能省掉大量排查时间。
十、总结
CRC8 = 数据多项式 × x⁸ 对 G(x) 做模 2 除法取余。GF(2) 里减=异或,所以逐位算法就是"最高位为 1 就异或 G(x) 低 8 位,为 0 就移位",8 次处理完一个字节。动画里每一次
<<1和^0x07都是长除法在寄存器上的实时演算。
理解了这个对应关系,if (crc & 0x80) crc = (crc << 1) ^ poly; 就不再是魔法,而是一行可解释的除法步骤。
本文代码基于 CRC-8/ATM(poly=0x07, init=0x00, 不反射, xorout=0x00),可直接用于 STM32 等嵌入式平台的通信校验。