恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
STM32F103硬件CRC实战:通信校验与固件升级可靠性设计
首页
资讯中心
/
STM32F103硬件CRC实战:通信校验与固件升级可靠性设计
STM32F103硬件CRC实战:通信校验与固件升级可靠性设计
发布时间:2026/9/16 1:56:57
简介本资源是一份面向STM32F103初学者与嵌入式开发者的CRC循环冗余校验实战工程聚焦通信与存储场景下的数据完整性保障问题。项目基于STM32F103标准外设库构建完整实现硬件CRC单元的初始化、多字节数据校验及结果验证全流程涵盖CRC-16/CRC-32常用多项式配置与字节序处理要点适用于CAN、UART等易受干扰的通信协议开发。压缩包共73个文件481KB含33个头文件h提供模块接口定义32个C源文件c实现CRC驱动、USART收发、LED指示及主控逻辑另有启动文件s、Keil工程配置uvprojx/uvoptx、编译输出hex/obj、调试脚本bat及关键现象截图png目录结构按Public/User/Libraries/APP分层组织便于理解STM32标准工程框架。目前已有775人学习下载可直接导入Keil MDK运行快速掌握硬件CRC加速机制与HAL库调用实践。1. STM32F103 的硬件 CRC 单元不是“锦上添花”而是通信链路里最后一道数据可信防线在工业 Modbus RTU 从机固件升级场景中我曾遇到过一个典型问题上位机下发的 64KB 固件包经 RS-485 传输后接收端校验通过但烧录失败——复位后芯片直接跑飞。抓取波形发现线路存在偶发毛刺导致某字节被翻转而当时仅用简单累加和Sum Check校验完全无法识别这种单比特错误。换成 STM32F103 内置的 CRC 单元后同一毛刺条件下CRC 校验值立刻失配升级流程主动中止并重传。这说明硬件 CRC 不是理论玩具而是嵌入式系统中对抗信道噪声、电源扰动、Flash 写入异常等真实故障的确定性手段。它不依赖 CPU 轮询、不占用主频资源、计算结果可复现特别适合对实时性敏感的 CAN、UART、SPI Bootloader 场景。本实验基于标准外设库v3.5.0构建覆盖从寄存器级配置到 HAL 封装调用的完整路径所有代码均可在 Keil MDK-ARM v5.37 下直接编译运行无需额外驱动或中间件。2. 理解 CRC 计算本质为什么 STM32F103 的 CRC 单元必须手动配置初始值与输入反码2.1 CRC 的数学内核多项式除法与硬件映射CRC 并非哈希其核心是模 2 除法XOR 替代减法。以 CRC-32IEEE 802.3为例生成多项式为 $ G(x) x^{32} x^{26} x^{23} x^{22} x^{16} x^{12} x^{11} x^{10} x^8 x^7 x^5 x^4 x^2 x 1 $对应十六进制常量0x04C11DB7。该多项式决定了校验码的检错能力边界能 100% 检出所有单比特、双比特错误以及长度 ≤32 的突发错误。STM32F103 的 CRC 外设正是将此多项式固化为硬件逻辑电路每次写入一个 32 位数据时内部移位寄存器自动执行 32 次 XOR 运算比软件查表法快 10 倍以上。但关键点在于硬件实现必须显式声明初始状态与数据流向否则结果与协议规范不兼容。2.2 STM32F103 CRC 寄存器组的关键配置项解析CRC 外设由三个核心寄存器控制全部位于 APB1 总线上RCC_APB1ENR 第 22 位需置 1寄存器地址偏移关键位域典型值作用说明CRC_CR0x00RESET(bit0)1→0写 1 清空 CRC_DR 并重载 INIT 值写 0 保持当前状态CRC_DR0x04—0x00000000数据输入寄存器每次写入 32 位数据触发一次计算CRC_INIT0x08—0xFFFFFFFF初始余数决定 CRC 输出是否含“补码”特性注意STM32F103 的 CRC 单元默认采用MSB First 输入顺序且不自动反转输入字节。这意味着若协议要求 LSB First如某些 Modbus 变种必须在写入CRC_DR前手动对每个字节执行位反转。这是初学者最常踩的坑——直接送入原始数据结果与 Pythonzlib.crc32()或binascii.crc32()完全不一致。2.3 配置代码从寄存器操作到标准外设库封装以下代码在crc.c中实现严格遵循 STM32F10x_StdPeriph_Driver v3.5.0 规范#include stm32f10x.h #include crc.h void CRC_Configuration(void) { // 1. 使能 CRC 时钟APB1 总线 RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_CRC, ENABLE); // 2. 复位 CRC 单元清空 DR 并加载 INIT 值 CRC_ResetDR(); // 3. 设置初始值为 0xFFFFFFFFCRC-32 IEEE 标准 CRC_SetInitRegister(0xFFFFFFFF); // 4. 配置数据输入为 32 位模式默认即为此模式显式确认 // 注意STM32F103 不支持 8/16 位输入必须按 32 位对齐写入 // 若原始数据为 uint8_t 数组需按小端序组合成 uint32_t } // 计算指定长度数据的 CRC-32 值 uint32_t CRC_Calculate(uint8_t *data, uint32_t length) { uint32_t i 0; uint32_t crc_value 0; uint32_t data_word 0; // 确保数据长度为 4 的倍数不足则补 0实际应用中应按协议填充 uint32_t aligned_len (length 3) ~0x03; // 分块写入 32 位数据 for (i 0; i aligned_len; i 4) { if (i length) { // 小端序组装data[i] 为最低字节 data_word ((uint32_t)data[i]) | ((uint32_t)data[i1] 8) | ((uint32_t)data[i2] 16) | ((uint32_t)data[i3] 24); } else { data_word 0; // 填充字节 } CRC_WriteData(data_word); // 写入 CRC_DR触发计算 } crc_value CRC_ReadCRC(); // 读取最终结果 return crc_value; }2.3.1 关键参数说明CRC_ResetDR()调用此函数等效于向CRC_CR的RESET位置 1 再清 0强制加载CRC_INIT值。不可省略否则上次计算结果残留会影响本次。CRC_WriteData(uint32_t data)底层宏定义为*(__IO uint32_t *) 0x40023004 data直接操作CRC_DR地址。每次写入即启动一轮 32 次 XOR 运算。字节序处理STM32F103 是小端机data[0]存储在最低地址。若协议要求大端输入如 Ethernet II需将data_word改为__REV(data_word)CMSIS 内联函数。2.3.2 与 HAL 库的对比验证若项目已迁移到 HAL 库如 STM32CubeMX 生成等效实现如下#include stm32f103xb_hal.h CRC_HandleTypeDef hcrc; void HAL_CRC_Init_Config(void) { __HAL_RCC_CRC_CLK_ENABLE(); hcrc.Instance CRC; hcrc.Init.DefaultPolynomialUse DEFAULT_POLYNOMIAL_DISABLE; // 禁用默认多项式 hcrc.Init.DefaultInitValueUse DEFAULT_INIT_VALUE_DISABLE; // 禁用默认初值 hcrc.Init.GeneratingPolynomial 0x04C11DB7; // 手动设置 CRC-32 多项式 hcrc.Init.CRCLength CRC_POLYLENGTH_32B; // 32 位长度 hcrc.Init.InitialCRC 0xFFFFFFFF; // 初始值 hcrc.Init.InputDataInversionMode CRC_INPUTDATA_INVERSION_NONE; // 不反转输入 hcrc.Init.OutputDataInversionMode CRC_OUTPUTDATA_INVERSION_NONE; HAL_CRC_Init(hcrc); } uint32_t HAL_CRC_Calculate_Custom(uint8_t *data, uint32_t length) { // HAL_CRC_Accumulate() 内部已处理字节对齐但需确保 data 为 uint32_t 对齐 uint32_t *aligned_data (uint32_t*)data; return HAL_CRC_Accumulate(hcrc, aligned_data, (length 3) / 4); }提示HAL 库的HAL_CRC_Accumulate()默认假设输入为uint32_t*若传入uint8_t*会导致地址未对齐异常。务必使用(uint32_t*)data强制转换并确认data地址本身是 4 字节对齐的Keil 编译器默认满足。3. 实战在 UART 通信中嵌入 CRC 校验帧实现可靠固件分包传输3.1 通信协议设计定义带 CRC 的数据帧结构为适配 STM32F103 的硬件 CRC 能力我们设计一个轻量级固件升级协议帧格式如下字段长度字节说明示例SOH1帧起始符0x01CMD1命令类型0x02固件块LEN2数据长度小端0x40 0x0064 字节PAYLOADN实际数据64 字节二进制CRC324CRC-32 校验码小端0x1A 0x2B 0x3C 0x4DETX1帧结束符0x04该设计确保所有字段均为整数字节避免 CRC 计算时字节对齐问题CRC 仅覆盖CMD LEN PAYLOAD不包含起始/结束符防止误触发接收端收到完整帧后先提取CMDLENPAYLOAD再调用CRC_Calculate()计算与接收到的CRC32字段比对。3.2 发送端实现在main.c中集成 CRC 计算与串口发送#include usart.h #include crc.h #include led.h #define FW_BLOCK_SIZE 64 uint8_t fw_buffer[FW_BLOCK_SIZE]; uint32_t fw_crc; void Send_FW_Block(uint8_t *block_data, uint16_t len) { uint8_t frame[FW_BLOCK_SIZE 10]; // SOH(1)CMD(1)LEN(2)PAYLOAD(N)CRC(4)ETX(1) uint16_t frame_len 0; // 1. 构建帧头 frame[0] 0x01; // SOH frame[1] 0x02; // CMD: firmware block frame[2] len 0xFF; // LEN low byte frame[3] (len 8) 0xFF; // LEN high byte frame_len 4; // 2. 复制 payload memcpy(frame[4], block_data, len); frame_len len; // 3. 计算 CRC-32仅针对 CMDLENPAYLOAD共 4len 字节 uint8_t crc_input[68]; crc_input[0] 0x02; // CMD crc_input[1] frame[2]; // LEN low crc_input[2] frame[3]; // LEN high memcpy(crc_input[3], block_data, len); fw_crc CRC_Calculate(crc_input, 3 len); // 4. 将 CRC32 按小端序填入帧 frame[frame_len] fw_crc 0xFF; frame[frame_len] (fw_crc 8) 0xFF; frame[frame_len] (fw_crc 16) 0xFF; frame[frame_len] (fw_crc 24) 0xFF; // 5. 添加 ETX frame[frame_len] 0x04; // 6. 通过 USART1 发送阻塞式实际应用建议用 DMA USART_SendData(USART1, frame[0]); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); for (uint16_t i 1; i frame_len; i) { USART_SendData(USART1, frame[i]); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); } }3.2.1 关键逻辑说明CRC_Calculate()输入数组crc_input严格按协议定义构造跳过 SOH 和 ETX确保发送端与接收端计算范围完全一致。CRC 结果fw_crc是 32 位整数写入帧时按小端序拆分为 4 字节最低字节在前与 STM32F103 的内存布局一致。串口发送使用轮询方式USART_FLAG_TC因固件升级对实时性要求不高且代码简洁易调试。若需高速传输应改用USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE)配合 DMA。3.3 接收端校验在usart.c的中断服务程序中解析帧并验证// 全局缓冲区需在 usart.h 中 extern 声明 extern uint8_t rx_buffer[128]; extern uint16_t rx_index; extern uint8_t rx_state; // 0idle, 1soh_received, 2collecting, 3etx_received void USART1_IRQHandler(void) { uint8_t res; USART_ClearITPendingBit(USART1, USART_IT_RXNE); res USART_ReceiveData(USART1); switch(rx_state) { case 0: // 等待 SOH if(res 0x01) { rx_index 0; rx_state 1; } break; case 1: // 已收到 SOH等待 CMD if(rx_index 0) { if(res ! 0x02) { // 非固件块命令丢弃 rx_state 0; break; } rx_buffer[rx_index] res; rx_state 2; } break; case 2: // 收集数据直到 ETX if(res 0x04) { // ETX rx_buffer[rx_index] res; rx_state 3; Process_Frame(); // 解析并校验 } else { if(rx_index sizeof(rx_buffer)-1) { rx_buffer[rx_index] res; } } break; } } void Process_Frame(void) { uint16_t payload_len; uint32_t received_crc, calc_crc; uint8_t *payload_ptr; // 1. 提取 LEN 字段第 2-3 字节小端 payload_len rx_buffer[2] | (rx_buffer[3] 8); // 2. 提取接收到的 CRC倒数第 5 到倒数第 2 字节 received_crc rx_buffer[rx_index-5] | (rx_buffer[rx_index-4] 8) | (rx_buffer[rx_index-3] 16) | (rx_buffer[rx_index-2] 24); // 3. 构造校验输入CMD(1) LEN(2) PAYLOAD(payload_len) uint8_t calc_input[68]; calc_input[0] rx_buffer[1]; // CMD calc_input[1] rx_buffer[2]; // LEN low calc_input[2] rx_buffer[3]; // LEN high payload_ptr rx_buffer[4]; memcpy(calc_input[3], payload_ptr, payload_len); calc_crc CRC_Calculate(calc_input, 3 payload_len); // 4. 比较校验结果 if(calc_crc received_crc) { LED_On(LED1); // 绿灯亮校验通过 // 此处调用 Flash 写入函数 Write_To_Flash(payload_ptr, payload_len); } else { LED_On(LED2); // 红灯亮校验失败 // 发送 NAK 帧请求重传 Send_NAK_Frame(); } rx_state 0; // 重置状态机 }3.3.1 排错要点帧长度溢出rx_buffer大小必须 ≥SOH(1)CMD(1)LEN(2)MAX_PAYLOADCRC(4)ETX(1)本例设为 128可支持最大 115 字节 payload。CRC 字段定位错误received_crc提取位置必须精确为rx_index-5到rx_index-2因 ETX 占最后 1 字节CRC 占前 4 字节。状态机鲁棒性实际产品中需增加超时机制如 SysTick 计时防止因线路断开导致rx_state卡死在case 2。4. 进阶技巧用 CRC 单元加速 OTA 升级中的 Flash 页校验与差分更新4.1 Flash 页级 CRC 校验规避擦写异常导致的静默损坏STM32F103 的 Flash 擦除操作存在风险若擦除过程中电压跌落可能造成某页仅部分扇区失效而FLASH_GetStatus()仍返回FLASH_COMPLETE。此时单纯校验固件整体 CRC 无法定位问题页。解决方案是为每个 1KB Flash 页单独计算 CRC并将校验值存储在独立备份区。// 定义 Flash 页信息结构体存于备份区 typedef struct { uint16_t page_num; // 页号0~127 for 128KB device uint32_t page_crc; // 该页数据 CRC-32 值 uint8_t valid_flag; // 0xAA 表示有效 } flash_page_info_t; flash_page_info_t page_info_backup[128]; // 128 页的校验信息 void Generate_Page_CRC(uint16_t page_addr, uint16_t page_size) { uint32_t *flash_ptr (uint32_t*)page_addr; uint32_t crc_val 0; uint16_t word_count page_size / 4; CRC_ResetDR(); // 每页重新初始化 CRC_SetInitRegister(0xFFFFFFFF); for(uint16_t i 0; i word_count; i) { CRC_WriteData(flash_ptr[i]); // 直接读 Flash 地址写入 CRC_DR } crc_val CRC_ReadCRC(); // 将 crc_val 写入 page_info_backup[page_num] uint16_t page_num page_addr / 1024; page_info_backup[page_num].page_num page_num; page_info_backup[page_num].page_crc crc_val; page_info_backup[page_num].valid_flag 0xAA; // 调用 FLASH_ProgramHalfWord() 将 page_info_backup 写入备份区地址 0x0801FC00 }4.1.1 技术优势零拷贝CRC_WriteData()直接接受 Flash 地址读取的uint32_t无需将整页数据复制到 RAM节省 1KB 内存。原子性每页 CRC 独立计算某页损坏不影响其他页校验结果便于 OTA 升级时精准定位坏页并跳过。4.2 差分 CRC 更新减少无线传输带宽消耗在 NB-IoT 等低带宽场景固件升级包过大。可采用差分更新策略只传输新旧固件的差异块并为每个差异块附加 CRC。关键在于快速定位差异位置// 使用滑动窗口比较两固件old_fw, new_fw窗口大小 64 字节 void Generate_Diff_Patch(uint8_t *old_fw, uint8_t *new_fw, uint32_t fw_size) { uint32_t offset 0; uint8_t diff_block[64]; uint32_t block_crc; while(offset fw_size) { // 比较 64 字节 if(memcmp(old_fw[offset], new_fw[offset], 64) ! 0) { // 发现差异提取 new_fw 的 64 字节块 memcpy(diff_block, new_fw[offset], 64); // 计算该块 CRC block_crc CRC_Calculate(diff_block, 64); // 将 offset block_crc diff_block 打包发送 Send_Diff_Packet(offset, block_crc, diff_block, 64); } offset 64; } }提示差分更新需在 PC 端预处理固件本实验中Template.uvprojx工程已包含diff_tool.c可编译为 Windows 命令行工具输入两个.bin文件输出差分包。实际部署时MCU 仅需实现Send_Diff_Packet()的接收与 Flash 写入逻辑大幅降低空中下载流量。本文还有配套的精品资源点击获取