stm32/mcu/hw-crc.md
硬件 CRC 单元:一次实验看懂循环冗余校验
硬件 CRC 单元:一次实验看懂循环冗余校验
2026-09-15 实测验证:洋桃 1 号开发板(STM32F103C8T6),demo47_CRC 改造版,串口每秒输出
CRC a=56327939 b=C527FC48 c=5A,Keil MDK 5.25.3.0 编译 0 Error 0 Warning,STM32CubeProgrammer CLI 2.23.0 SWD 烧录通过。
#一、元信息
| 字段 | 值 |
|---|---|
| 芯片/平台 | STM32F103C8T6(洋桃 1 号开发板,标准库) |
| 外设类型 | 硬件 CRC32 单元(AHB 总线外设,多项式 0x4C11DB7,初始值 0xFFFFFFFF) |
| 验证状态 | ✅ 已验证(2026-09-15) |
| 验证环境 | 洋桃1号开发板 / Keil MDK 5.25.3.0(0 Error 0 Warning)/ STM32CubeProgrammer CLI 2.23.0(SWD 烧录)/ 2026-09-15 |
| 驱动方式 | 官方 demo47_CRC 改造版(crc.c),结果经 USART1 printf 每秒输出 |
#二、实验平台与改造:demo47_CRC
- 官方原版的局限:demo47_CRC 把计算结果只放在调试器变量里——接上调试器才能看到,脱机运行什么都观察不到;
- 我们的改造:把结果改成串口 printf 每秒输出一次,接个 USB-TTL 就能实时观察,也让"每秒结果都一样"这件事变得肉眼可验证;
- 顺带一课:官方 demo 是"裸"工程,要补上板级串口驱动才能真正用 printf。
给任意官方 demo 补板级串口驱动,三步:
- 把
usart.c / usart.h拷进工程(含时钟、引脚、USART1 初始化与 printf 重定向); - 把文件注册进 Keil 工程文件(.uvproj)——不注册就不参与编译,头文件找得到、函数链接不到;
- main 里
USART1_Init()后即可 printf。
#三、STM32 硬件 CRC 速览
| 函数/操作 | 作用 |
|---|---|
| RCC_AHBPeriph_CRC | 使能 CRC 单元的 AHB 时钟:RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC, ENABLE) |
| CRC_ResetDR() | 复位清态:把滚动状态清回初始值 0xFFFFFFFF,新一轮计算前必须先清态 |
| CRC_CalcCRC(x) | 喂一个 32 位字:把 x "搅拌"进滚动状态,并返回搅拌后的当前 CRC 值(可连续喂,逐次累积) |
| CRC_CalcBlockCRC(p, n) | 整块计算:一次性喂入 n 个 32 位字,返回最终值 |
| CRC_SetIDRegister() / CRC_GetIDRegister() | 独立 8 位 IDR 寄存器:与 CRC 计算无关,可当 1 字节"临时便签"自由读写 |
核心调用顺序:
cRCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC, ENABLE); /* 1. 开时钟 */
CRC_ResetDR(); /* 2. 清态回初值 */
a = CRC_CalcCRC(0x12345678); /* 3. 逐个喂字,返回累积 CRC */
a = CRC_CalcCRC(0x23456789);
a = CRC_CalcCRC(0x34567890); /* a = 最后一次返回值 */
CRC_ResetDR();
b = CRC_CalcBlockCRC(y, 3); /* 4. 整块一次算完 */
CRC_SetIDRegister(0x5A); /* 5. IDR 便签:写 0x5A */
c = CRC_GetIDRegister(); /* 回读 0x5A → 寄存器通路 OK */
#四、实测数据(串口每秒输出)
USART1 调试串口,每秒输出一行:
txtCRC a=56327939 b=C527FC48 c=5A
| 输出 | 来源 | 含义 |
|---|---|---|
| a=56327939 | 对 x[]={0x12345678, 0x23456789, 0x34567890} 逐个 CRC_CalcCRC() 喂入 | 最后一次返回的累积 CRC(十进制显示,即 0x035B7F03) |
| b=C527FC48 | 对 y[]={0x87654321, 0x98765432, 0x09876543} 用 CRC_CalcBlockCRC() 整块计算 | 同一算法、另一份数据的 CRC(十六进制显示) |
| c=5A | IDR 寄存器写 0x5A 再回读 | 写进什么读出什么,寄存器读写通路正常 |
#五、关键问答
- Q1:为什么 a 每秒都一样?
因为 CRC 是确定性算法:相同输入序列 + 相同初态 = 相同输出。每次输出前都 CRC_ResetDR() 清回初值,喂的字也一字不差,结果自然每秒一致。确定性正是 CRC 能当校验码的前提——发送方、接收方各算一遍,对得上就是好数据。
- Q2:为什么 a≠b?
因为它们根本不是同一份数据:a 算的是 x 数组,b 算的是 y 数组。同一个数,逐个喂和整块算结果完全一样(把 y 数组逐个喂,得到的值就是 b);不同数据算出不同值,CRC 才有"发现损坏"的能力。
- Q3:a 和 b 能直接比大小吗?
不能拿数值直接比——一个按十进制打印、一个按十六进制打印(a=56327939 即 0x035B7F03)。它们本来就来自两份不同的数据,比较没有意义。
#六、CRC 应用一览
| 场景 | 用的 CRC | 说明 |
|---|---|---|
| Modbus RTU | CRC16 | 工控串口总线标准帧尾校验 |
| CAN 总线 | 帧内建 CRC | 控制器硬件自动计算/校验,帧尾 15 位 |
| 以太网 | CRC32(FCS) | 每个以太网帧尾 4 字节帧校验序列 |
| bootloader | 常用 CRC32 | 跳转 App 前校验 App 区完整性,防止跑到半个固件上变砖 |
| EEPROM / Flash 配置块 | CRC8/16/32 | 上电校验配置数据,坏了回默认值 |
| ZIP / PNG 等文件格式 | CRC32 | 格式内部自带的完整性校验 |
#七、边界:CRC 能做什么、不能做什么
- 只能发现意外损坏(线噪、位翻转、写坏的 Flash),不能纠错——发现错了只能重传/重读;要"就地纠错"得上 ECC / 海明码(NAND Flash、内存 ECC 那一套);
- 防不了恶意篡改:算法公开、无密钥,攻击者可以连数据带 CRC 一起改好;防篡改要用密码学哈希(SHA-256)+ 签名(ECDSA/RSA);
- 一句话:CRC 管"坏了没有",签名管"是不是我发的"。
#八、CRC 在 OTA 里的位置
- 下载固件(HTTPS/TLS 通道)→ 写入 Flash 备用区;
- 下载完先做 CRC / 哈希校验,不过关直接丢弃,绝不碰正在运行的固件;
- 校验通过 → 在元数据里标记有效;
- 复位 → bootloader 再验一次 CRC/签名 → 通过则跳转新固件;启动失败或再校验不过 → 回滚旧固件。
CRC 只解决"下载坏没坏"。完整 OTA 还需要固件签名(ECDSA/RSA)防伪造、TLS 防通道窃听篡改——安全启动就是"CRC 之外的那几层"。
#九、参考库/链接
- STM32F103 参考手册(RM0008):CRC 章节(多项式 0x4C11DB7、数据寄存器 DR、独立 IDR)
- 洋桃1号官方例程:
demo47_CRC(本页改造对象)
#十、相关阅读
- 定时器与中断:1 秒心跳与五阶段状态机——每秒输出一次这种定时节奏用它实现
- STM32 实战复盘:一次烧录验证串起的整条工具链——UID 防伪实战,L3 方案的代码区 CRC 自校验正是本页外设的用武之地
#11、更新日志
📝共 1 条记录
| 日期 | 类型 | 更新内容 |
|---|---|---|
| 2026-09-15 | 新增 | 首次建档(实测验证版) |