尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32硬件CRC外设详解:原理、寄存器、HAL库与踩坑实战

STM32硬件CRC外设详解:原理、寄存器、HAL库与踩坑实战 做嵌入式通信和固件升级的工程师对CRC校验一定不陌生。串口帧、以太网帧、OTA固件包、Flash里的关键数据区都离不开它。很多人的第一反应是写个软件查表法几十行代码搞定。但如果你用的是STM32其实不用这么麻烦——芯片内部就带了一个硬件CRC计算外设运算速度快不占CPU还能配合DMA自动搬运数据。这篇应用笔记就围绕STM32的CRC外设把原理、寄存器、标准库和HAL库的写法以及我实际调板时踩过的几个坑一次讲清楚。正在做通信协议、固件升级、数据完整性校验的开发者可以直接照着用刚接触STM32的朋友也可以按图索骥。1. CRC外设能干什么为什么不用软件算1.1 CRC校验的基本原理CRCCyclic Redundancy Check循环冗余校验本质上是一种多项式除法。把要校验的数据看成一个大整数除以一个约定的生成多项式得到的余数就是CRC校验值。接收方收到数据后做同样的除法如果余数相同就认为数据没有出错。这个“除法”不是普通算术除法而是模2除法不借位、不进位本质上就是按位异或。以CRC-32为例生成多项式是0x04C11DB7虽然叫“32位”但完整多项式实际是33位的0x104C11DB7最高位固定为1所以描述时通常省略。计算时数据左移32位后与多项式做模2除法余数就是32位的CRC结果。这和小学数学里的除法求余数非常像被除数除以除数得到一个商和一个余数余数小于除数。CRC的区别在于这里的运算规则从“借位减法”换成了“异或”每步的余数也不是普通余数而是一个能够捕捉到数据位翻转的“指纹”。选一个好的多项式哪怕数据中间只有1个bit发生翻转CRC结果也能以极高概率变化。1.2 软件CRC与硬件CRC的取舍软件查表法是经典的CRC实现方式提前把256个字节的CRC表算好放内存里每个字节查一次表、异或几次速度不慢而且灵活——多项式、初始值、反射位都可以随便改。比如Modbus RTU的CRC16、XBee的CRC16、SD卡的CRC7都可以用同一个查表框架去适配。但软件CRC有一个问题它占用CPU时间。对一个几十KB的固件包做CRC校验就算查表法再快也有几千上万次循环。在Bootloader里做OTA升级时这一步会明显拉长升级时间在高速通信里如果每收到一帧都要软件算一遍CRCCPU的负担会直线上升。STM32的CRC外设把这件事变成“写数据、读结果”。你把要校验的数据按字写入数据寄存器硬件电路自己完成移位、异或、比较算完直接读结果整个过程CPU几乎不参与。配合DMA甚至可以把一整块内存自动送进CRC外设算完再中断通知你。速度上和软件查表法不是一个量级的。这是我个人实际的感触早年在串口转WiFi模块的固件升级里每次升级要校验80KB的固件包软件CRC跑一次大概要十几毫秒用户虽然感知不明显但那个“升级中”的进度条每次走到最后都会卡顿一下。换成硬件CRC后整个校验几乎不占时间体感上顺畅很多。1.3 适用范围与限制硬件CRC非常适合这几类场景Flash固件包校验尤其是OTA升级场景内存中固定长度数据块的完整性校验通信帧的CRC生成与校验前提是帧数据先缓存到内存连续区域系统启动时对关键配置块做完整性检测防止配置被意外改写但也要说清楚它的几个限制。第一STM32的CRC外设是按32位数据宽度为主设计的虽然新系列支持8位、16位输入格式但和逐字节到达的串口流不太合拍。第二它默认算的是CRC-32如果你想算CRC16或CRC8需要新系列芯片才能配置多项式位宽老一些的F1系列不行。第三硬件CRC的计算参数一旦配置好就固定了如果需要同一个项目里兼容多种CRC算法要么来回改寄存器要么准备两套方案。所以我的建议是如果数据已经在内存连续缓冲区里优先用硬件CRC如果数据是一个字节一个字节从外设挤进来、且每收完一帧都要立刻算校验那还是老老实实用软件查表法别为了省事硬上硬件外设。2. 从寄存器看STM32硬件CRC的设计逻辑2.1 核心寄存器一览STM32的CRC外设寄存器不多理解起来不复杂。F1系列很简单就三个寄存器F4、F7、L4等新系列增加了可配置项。先看一张总览表寄存器F1系列F4/F7/L4等新系列作用CRRESET位RESET、POLYSIZE、REV_IN、REV_OUT控制计算复位、多项式位宽、输入输出数据反转DR有有数据寄存器写入数据参与计算读出CRC结果IDR有有独立数据寄存器不参与CRC计算可临时存数据INIT无有设置CRC初值POL无有设置生成多项式访问方式32位32位所有寄存器均按32位访问这个表相当关键。如果你在F1上写过CRC计算换到F4上就会发现寄存器多了一倍如果反过来从F4移植到F1就得把可配置参数全部转换成F1的固定默认值。2.2 关键配置位与它的实际含义先看CR寄存器。RESET位最简单写1就把CRC计算单元复位回到初始状态。F1系列复位后CRC寄存器自动装载0xFFFFFFFFF4系列则装载INIT寄存器指定的值。POLYSIZE位用于选择多项式位宽00表示32位01表示16位10表示8位。选择16位或8位后POL寄存器里只需要填写对应的低16位或低8位多项式比如CRC-16/CCITT的多项式0x1021CRC-8的多项式0x07。要注意的是POLYSIZE只影响内部运算位宽DR寄存器依然按32位访问。REV_IN和REV_OUT的作用容易让人困惑。REV_IN控制输入数据的位反转方式00不反转01按字节内反转10按半字内反转11按全字内反转。REV_OUT则是一个单独的位决定CRC结果输出前是否按位反转。这个功能是为了兼容各种CRC标准。很多软件工具算CRC时都默认做了反射如果硬件这边不配置对应的反转结果必然对不上。INIT寄存器设置计算起始值。复位后自动装载INIT值每次RESET后也一样。POL寄存器设置多项式。如果设置了DefaultPolynomialUse为默认则使用0x04C11DB7否则使用CRCPolynomial成员指定的值。2.3 F1固定配置与F4可配置的差异这一点值得单独拿出来说。STM32F1的CRC外设非常“朴素”只有默认的CRC-32多项式固定0x04C11DB7初值固定0xFFFFFFFF输入输出不反转结果不异或。这个参数组合正好对应CRC-32/MPEG-2标准而不是以太网常用的IEEE 802.3 CRC-32。F4及之后的系列通过POL、INIT、REV_IN、REV_OUT这些寄存器理论上可以配置出绝大多数常见CRC算法。比如想算CRC-16/CCITTPOLYSIZE配成01POL填0x1021INIT填0xFFFF再根据是否需要反射配置REV_IN和REV_OUT。但这里藏着一个很现实的坑如果你早期在F1上用硬件CRC后来换到F4想当然认为“STM32硬件CRC算出来的值都一样”那就错了。F1的固定配置只对应CRC-32/MPEG-2F4如果手动改成IEEE CRC-32的反射参数结果就完全不同。同一份数据两个芯片算出的CRC值不一样这在多芯片通信场景里极容易出问题。所以项目里一定要把CRC参数明确写清楚最好做成宏定义防止踩到不同型号的差异。3. 标准库与HAL库实操两种方式都给你走一遍3.1 CubeMX配置步骤用CubeMX配置CRC外设非常快不需要任何引脚分配。以STM32F407为例在Pinout Configuration里找到Computing选中CRC如果使用默认CRC-32/MPEG-2参数不需要改任何配置直接生成代码如果需要自定义参数在CRC Parameter Settings里修改Polynomial Size、CRC Polynomial (Hex)、CRC Initial Value (Hex)、Input Data Format、Reverse Output Bits等选项这里有一个细节CubeMX里默认的Input Data Format是Words也就是按32位处理。如果你的数据是uint8_t数组建议先把数据拼成uint32_t数组再传入或者改用HAL库的Bytes格式。格式选错CRC结果天差地别后面章节我会展开说。生成代码后CRC外设的时钟已经使能初始化函数MX_CRC_Init会自动调用直接写计算逻辑就行。3.2 HAL库计算单个数据块HAL库把CRC封装得很简单核心就是两个函数HAL_CRC_Calculate和HAL_CRC_Accumulate。拿最常见的场景来演示一组uint8_t数据长度随便我要算它的CRC-32/MPEG-2。#include crc.h #define BUFFER_SIZE 64 uint8_t buffer[BUFFER_SIZE]; uint32_t crc_result; /* CRC外设已在MX_CRC_Init中完成初始化 */ crc_result HAL_CRC_Calculate(hcrc, buffer, BUFFER_SIZE);就这么简单。HAL_CRC_Calculate内部做了这么几件事如果配置了8位或16位输入格式自动把byte/halfword打包成字复位CRC计算单元逐个写入数据寄存器返回DR寄存器的值这里有个小重点HAL_CRC_Calculate每次调用都会复位计算单元所以可以多次调用每次计算的都是独立结果不受之前数据影响。如果使用F1系列的标准外设库代码也是这个模式RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC, ENABLE); /* 计算单个32位数据 */ CRC_ResetDR(); uint32_t crc CRC_CalcCRC(0x12345678); /* 计算连续缓冲区 */ uint32_t crc2 CRC_CalcBlockCRC((uint32_t*)buffer, BUFFER_SIZE / 4);注意标准库的CRC_CalcBlockCRC是按32位长度计算传入的BufferLength是指32位字的个数不是字节数这个容易搞错。3.3 HAL_CRC_Calculate与HAL_CRC_Accumulate的区别HAL_CRC_Calculate和HAL_CRC_Accumulate表面看都在算CRC行为却完全不同这是很多人忽略的细节。Calculate每次都会先复位CRC单元再写入数据。它适合“算一份数据得到一个独立结果”的场景。Accumulate不复位把新数据接着当前CRC状态继续算。它适合分段计算同一份数据的情况比如数据分几批到达或者内存不连续时把几段分别送进去最后得到一个整体的CRC。下面是HAL库里典型调用方式的对比/* 分段累加计算 */ HAL_CRC_Accumulate(hcrc, segment1, len1); HAL_CRC_Accumulate(hcrc, segment2, len2); HAL_CRC_Accumulate(hcrc, segment3, len3); /* 此时DR寄存器里的值是segment1segment2segment3拼接后的整体CRC */ uint32_t whole_crc HAL_CRC_Read_CRC(hcrc);这里就出现一个非常容易踩的坑如果你在Accumulate之前忘了先复位CRC单元而刚好之前又算过别的东西那么这次累加的结果会包含上一次的残留状态。正确做法是在第一段数据前先执行一次复位可以调用HAL_CRC_Calculate处理第一段也可以直接设置CR寄存器的RESET位。3.4 直接操作寄存器的参考代码有些老项目不用HAL库也不想用标准库函数直接操作寄存器最直接。以F4为例计算一段32位数据的CRC/* 使能CRC时钟 */ RCC-AHB1ENR | RCC_AHB1ENR_CRCEN; /* CRC外设默认配置为CRC-32/MPEG-2 */ /* 复位计算单元装载初始值0xFFFFFFFF */ CRC-CR | CRC_CR_RESET; /* 写入数据假设数据是32位对齐的数组 */ uint32_t data[4] {0x12345678, 0x9ABCDEF0, 0x11223344, 0x55667788}; for (int i 0; i 4; i) { CRC-DR data[i]; } /* 读取计算结果 */ uint32_t crc CRC-DR;这段代码在F4上直接可用。F1的寄存器更少把CRC-CR | CRC_CR_RESET替换成CRC_ResetDR()同样成立。直接操作寄存器的好处是心里有数逻辑全部透明出了问题也容易排查。4. DMA、中断与批量数据的实战配合4.1 用DMA批量计算大块内存CRCCRC外设和DMA是一对好搭档。要计算一块几KB甚至几百KB的数据DMA自动把内存数据搬进CRC的DR寄存器搬完触发传输完成中断你只需要在回调里读一次CRC结果。HAL库提供了现成的HAL_CRC_Calculate_DMA函数/* 配置DMA传输完成后结果在CRC_DR里自动更新 */ HAL_CRC_Calculate_DMA(hcrc, (uint32_t*)buffer, BUFFER_SIZE / 4);使用DMA有几个要点第一内存地址要32位对齐。如果传入一个uint8_t数组但地址没对齐DMA传输会触发总线错误。最简单的办法是把缓冲区定义成ALIGN_32BYTES或者直接用uint32_t数组。第二DMA传输长度单位是字还是字节取决于你配置DMA请求时采用的位宽。用HAL库时HAL_CRC_Calculate_DMA内部会按配置的InputDataFormat做转换但DMA自身还是以32位传输为主。如果数据长度不是4的倍数最后几个字节需要自己处理可以先补零凑成32位也可以先软算一小段再加进去。第三传输完成回调里读CRC结果不要在传输进行中读。搞过DMA的人应该都有经验搬一半去读数据拿到的什么都不是。实际调试中我还发现DMA模式下的时钟使能顺序也要注意。必须先使能CRC时钟再使能DMA时钟否则有可能出现第一次计算CRC结果全0的诡异现象。4.2 与串口通信结合的完整校验流程串口通信是最常见的CRC使用场景之一。工程里常见做法是发送方用软件CRC算出帧尾接收方用STM32硬件CRC也算一遍然后比对。假设通信协议是帧头2字节 数据区N字节 CRC324字节小端。发送方用PC或者另一块板子的软件CRC计算出CRC加在帧尾接收方用STM32硬件CRC对帧头数据区CRC32整段计算。理论上如果CRC正确整段计算出来的CRC结果应该是一个固定值比如0x00000000或者0xC704DD7B取决于CRC标准。但STM32硬件CRC默认没有输出异或对于MPEG-2标准整段数据含CRC在内算出来的结果并不是0而是一个固定魔术值。这里就必须严格匹配参数发送方用什么多项式、什么初值、什么反射、什么字节序接收方必须一模一样否则算出来的结果永远对不上。我在项目里实测下来的做法是接收方先把整帧数据含CRC全部缓存到内存等帧收完后再调用一次HAL_CRC_Calculate把结果和一个预先测好的期望值比较。这个期望值不是算出来的是在协议定稿时用标准CRC工具算一次作为常量写进代码。这样即使PC端用了不常见的CRC变体两边也能保证一致。4.3 OTA固件包校验的实战思路OTA固件包校验是硬件CRC最能发挥价值的地方。Bootloader收到新固件需要先确认固件包没被篡改、没传错再写进应用区。整包做一次CRC校验软件算和硬件算时间差距非常大。以512KB固件为例软件查表法在我的测试环境里大约需要80毫秒到100毫秒硬件CRC配合DMA只需要几毫秒差距十倍以上。在需要保证设备不能长时间无响应的OTA场景里这个时间差是实打实的体验差异。具体做法固件包在打包时生成CRC32校验值随包下发。Bootloader下载完成后对整个固件缓冲区调用硬件CRC计算与校验值比对。计算前记得复位CRC单元用累加模式分块算也行但分块逻辑要处理好边界。这里还要提一个我正在用的额外用法应用区固件启动前对固件头部关键字段做一次CRC校验判断固件是否完整。这样启动时如果有硬件损伤或者Flash意外改写可以提前发现而不是等程序跑飞了再去排查。5. 踩坑记录和PC端CRC对不上的各种原因5.1 第一个坑默认参数不是IEEE CRC-32这个坑几乎每个用STM32硬件CRC的人都会踩一次。你兴冲冲地把STM32硬件CRC的结果和PC端工具对了一下发现完全不一样然后开始怀疑人生。原因很简单STM32 CRC外设的默认配置输出的是CRC-32/MPEG-2而不是大多数软件工具默认的IEEE 802.3 CRC-32。两者的多项式一样都是0x04C11DB7但初值、反射、结果异或完全不同。参数项CRC-32 (IEEE 802.3)CRC-32/MPEG-2 (STM32默认)多项式0x04C11DB70x04C11DB7初始值0xFFFFFFFF0xFFFFFFFF输入反射是否输出反射是否输出异或0xFFFFFFFF0x00000000所以如果你在PC端用Python的binascii.crc32或者zlib的crc32去和STM32默认硬件CRC对比得出的结果永远不一样。这不是芯片算错了而是算法参数不匹配。解决办法有两个一是PC端也改成CRC-32/MPEG-2参数计算二是把STM32 CRC外设配成IEEE的参数在F4上把REV_IN配成按字节反转、REV_OUT置1再让HAL库在结果上做一次异或0xFFFFFFFF。在F1上就麻烦一点因为F1不支持反射配置只能让PC端迁就芯片。5.2 第二个坑每个新数据块前必须复位有次调试同事跑过来问我为什么CRC算第二次就开始出错了第一次算的明明是对的。看了代码他用的是HAL库的HAL_CRC_Accumulate但在算第二块数据时忘了先复位CRC单元。Accumulate的本意是在上一次结果上继续累加如果不复位它会“继承”上一次的CRC状态。就算你用HAL_CRC_Calculate如果HAL库里没做复位早期版本某些处理方式不一样同样会出现类似问题。直接操作寄存器时的规则更简单每开始计算一段新的独立数据必须先置位RESET位。不管是用标准库的CRC_ResetDR()还是直接写CRC-CR | CRC_CR_RESET这一步都不能省。这里建议把“计算一块数据”封装成一个函数函数第一行就复位避免调用方遗忘。我自己的代码风格是uint32_t crc32_calc(const uint8_t *buf, uint32_t len) { CRC-CR | CRC_CR_RESET; /* 按字写入数据处理尾部不对齐字节 */ ... return CRC-DR; }这样所有外部调用都拿到的是一段独立数据的完整结果谁也不可能踩到脏状态。5.3 第三个坑字节序与打包顺序字节序问题在CRC外设里比想象中隐蔽。STM32 CRC外设的DR寄存器是32位的写入一个32位字的时候硬件按“先处理高位还是先处理低位”来做。这对应到软件CRC里就是数据是按大端还是小端送入计算。当你用HAL库的InputDataFormat选择Bytes模式时HAL库会把你传入的字节缓冲区按buf[0]为低字节、buf[1]为高字节……的方式打包成一个32位字再写入DR寄存器。如果外部工具计算CRC时是按大端方式逐字节处理的两边结果就对不上。更麻烦的是如果直接操作寄存器把一个uint32_t数据写入DR芯片内部会按数据本身的位序处理。在STM32小端模式下0x12345678在内存里实际是78 56 34 12写入DR后CRC外设处理的可能是字节序已经翻转过的数据。这一层字节序叠加非常容易搞乱。我的经验是项目里明确指定协议层的字节序并且把数据先整理成协议定义的顺序再交给CRC外设。不要想着靠CRC外设自己吸收字节序差异它做不到也没必要让它做。5.4 第四个坑硬件CRC算CRC16/Modbus的迷惑行为很多人在串口项目里用Modbus RTU协议帧尾是CRC16就想着“既然芯片有CRC外设不如让它算CRC16”。理论上F4系列把POLYSIZE配成16位、POL配成0x8005、INIT配成0xFFFF再加输入反射就能实现Modbus的CRC16。但实际用起来非常别扭。Modbus的CRC是按字节逐个计算的且字节内是LSB-first。CRC外设的DR寄存器是32位你写一个字节进去它并不会只处理8位而是会把数据放在32位宽的环境里处理。即使你配置了8位输入格式HAL库也会先把字节打包成32位字再送入这就改变了CRC计算的字节到达顺序。所以在Modbus这种逐字节流式协议里我一般直接劝退硬件CRC老老实实用软件查表法。哪怕F4能把CRC外设配成16位处理串口流的时候仍然很别扭得不偿失。硬件CRC更适合“整块数据一次性计算”的场景。真要逐字节算CRC16软件查表法几百微秒就能算完完全够用。5.5 常见问题速查表现象可能原因解决办法和PC端zlib库对不上STM32默认是CRC-32/MPEG-2zlib是IEEE CRC-32配置反射位或PC端切换算法第二次计算结果不对没在计算前复位CRC单元每次计算前置位RESET位和上位机字节对齐不上字节序打包顺序不一致明确协议字节序统一处理DMA方式计算结果全0DMA未等待完成就读取结果在DMA完成回调里读结果DMA传输触发总线错误缓冲区未对齐32位缓冲区32位对齐Modbus CRC16对不上逐字节流不适合硬件CRC改软件查表法5.6 一个实战调试技巧最后分享一个我常用的调试技巧能帮你快速判断CRC外设配置是否正确。先用一个已知的软件CRC工具比如在线CRC计算器算出几组小数据的期望值然后把同样的数据用STM32硬件CRC算一遍对比两者是否一致。我一般用三组数据做对比单字节0x00单字节0xFF四字节0x01 0x02 0x03 0x04这三组数据覆盖面够广能快速暴露初值、反射、字节序三类问题。如果三组全对基本可以判定配置和字节序处理正确剩余的就是业务逻辑的比对问题了。如果第一组就错了优先检查初值如果第二组错大概率是反射问题如果第三组错但前两组对那就是字节序打包的锅。这段经验帮我省了很多次“对着工具算半天还是各种对不上”的无效调试。你按这个顺序排查通常几分钟就能定位问题。
返回列表