CRC8 校验原理与实现详解(附可交互动画演示)

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 → 商 1,异或 G(x)
  2. 被除数最高位为 0 → 商 0,只移位
  3. 每轮消掉最高位,直到剩余 < 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>

怎么看这个动画

  1. 点 重置,寄存器全 0。
  2. 点 单步 :第一帧是"当前字节异或进寄存器",日志会打印 crc ^= byte。
  3. 继续单步:每帧显示寄存器 8 位,最高位红色高亮 。若最高位为 1,操作是 (crc<<1)^0x07;为 0 则 crc<<1。
  4. 8 步后进入下一字节,重复。
  5. 全部字节处理完,显示最终 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 比简单累加和强的原因------有代数结构保证,不靠运气。


九、工程避坑

  1. 参数必须收发一致:poly/init/反射/xorout 四项任一不同,结果就不同。协议文档要写清"CRC-8/ATM"而非只写"CRC8"。
  2. 明确 CRC 覆盖范围:是全部载荷,还是含 ID/DLC?两端必须一致。
  3. 与 CAN 硬件 CRC 区分:CAN 帧自带 CRC15 由 bxCAN 硬件完成,软件层加的 CRC8 是应用层额外校验,两者不冲突。
  4. 查表法 vs 逐位法:逐位法代码小、适合低频;查表法快 8 倍、适合高频。参数运行时可变时只能用逐位法。
  5. 先过自检向量再上板 :"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 等嵌入式平台的通信校验。

相关推荐
CappuccinoRose1 小时前
拖拽交互体系
前端·javascript·css·html·交互·1024程序员节
Mortalbreeze2 小时前
0 - 1 BFS 算法详解:从基本原理到经典题目建模
数据结构·c++·算法·图论·宽度优先
进化中的刀盾兽4 小时前
从零实现 C++ Json-Rpc(十):把消息交给真实连接——MuduoConnection 与 ConnectionFactory
c++·rpc
青瓦梦滋10 小时前
WebSocket协议
linux·服务器·网络·c++·websocket·网络协议·http
CodeYp201916 小时前
第15讲:二叉搜索树
开发语言·c++·二叉搜索树
pipixia2003316 小时前
C++继承面试四连问:内存叠加、私有继承、sizeof、权限重映射
c++·面试·嵌入式·继承·面向对象·对象模型
牵猫散步的鱼儿19 小时前
C++ 内存分区
c++
choumin19 小时前
C++ 标准容器为什么不能存引用
c++·容器·引用类型