STM32奇偶校验实战:从原理到代码,彻底搞懂UART通信的“安全卫士”
你有没有遇到过这样的问题?在工厂现场调试一个基于RS-485的温湿度传感器网络,数据偶尔会“发疯”,显示一些明显不合理的数值。查了协议、对了地址、看了CRC——都没错,但就是莫名其妙出错。
后来发现,是某一位被电磁干扰翻转了。而更糟的是,这种单比特错误居然逃过了CRC校验!因为CRC是对整帧数据做运算,某些特定位置的位翻转恰好不会改变校验值(虽然概率低,但工业环境里真会发生)。
这时候,如果你在物理层启用了奇偶校验,这个错误就能在第一个字节接收时就被硬件捕捉到——就像一道前置防火墙,把脏数据挡在系统之外。
今天我们就来深入聊聊STM32中这个低调却关键的功能:USART的奇偶校验机制。不是简单贴个配置步骤,而是带你从底层逻辑走到实际应用,真正把它变成你手里的“通信守护神”。
为什么需要奇偶校验?它和CRC有什么区别?
先别急着配寄存器,咱们得明白:我们到底想解决什么问题?
UART通信本质上是在“裸奔”。没有像TCP那样的重传机制,也没有I2C那样的ACK应答。一旦数据出错,除非你自己处理,否则就默默吞下去了。
常见的差错检测手段有:
- 奇偶校验(Parity Check):检测单比特错误,每字节独立判断。
- CRC(循环冗余校验):检测多比特错误,用于整帧数据完整性验证。
它们的关系不是“二选一”,而是“前后防线”:
🛡️奇偶校验是哨兵,站在城门口逐个检查每个人有没有戴帽子;
CRC是守将,等人都进来了再清点总数对不对。
如果连哨兵都没有,那可能一群伪装者已经混进城了,守将才发现人数不对——为时已晚。
所以,在高噪声环境下,尤其是长距离RS-485通信中,建议同时启用奇偶校验 + CRC,形成双重防护。
奇偶校验是怎么工作的?硬件自动完成的秘密
STM32的USART模块支持硬件级奇偶校验,这意味着你不需要写任何计算“1”的个数的代码。一切都由外设自动完成。
数据帧结构变了!
默认情况下,UART传输的是8N1帧格式:
- 1 起始位
- 8 数据位
- 无校验
- 1 停止位
当你开启奇偶校验后,必须切换到9位模式:
- 1 起始位
- 8 数据位 + 1 校验位
- 1 停止位
也就是说,每个字节变成了9位。这也是为什么你在配置时一定要注意字长设置。
举个例子:你要发送0x5A(二进制0101_1010),其中有4个‘1’。
| 模式 | “1”的总数要求 | 当前数量 | 校验位 | 总数 |
|---|---|---|---|---|
| 偶校验 | 偶数 | 4 | 0 | 4 ✅ |
| 奇校验 | 奇数 | 4 | 1 | 5 ✅ |
发送时,你只需要把这9位中的高8位填上数据,最低位留给硬件填充校验位即可。
接收端收到后,硬件会重新统计“1”的个数是否符合设定。如果不符,就会置位PE标志(Parity Error Flag),你可以通过轮询或中断来响应。
寄存器怎么配?关键就三个位
别被手册上千行寄存器吓住,真正影响奇偶校验的核心控制位只有三个,在USART_CR1寄存器中:
| 位名 | 位置 | 功能说明 |
|---|---|---|
| M | CR1[12] | 字长选择。PCE=1时,M必须为1(即9位) |
| PCE | CR1[10] | 奇偶使能。置1开启校验功能 |
| PS | CR1[9] | 奇偶选择。0=偶校验,1=奇校验 |
记住一句话口诀:
🔑要校验,先开PCE;要9位,M得置1;奇还是偶,看PS定。
此外还有两个状态相关位你也得知道:
- PE(SR[0]):校验错误标志,接收时出错则置1
- PEIE(CR1[8]):允许PE事件触发中断
手撕寄存器:STM32F103 USART1奇校验配置
下面这段代码适用于STM32F1系列,直接操作寄存器完成USART1初始化并启用奇校验。
#include "stm32f10x.h" void USART1_Parity_Init(void) { // 1. 开启时钟:GPIOA 和 USART1 RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN; // 2. 配置PA9(TX)为复用推挽输出,PA10(RX)为浮空输入 GPIOA->CRH &= ~(0xFF << 4); // 清除CNF9~MODE9, CNF10~MODE10 GPIOA->CRH |= (0xB << 4) // PA9: 复用推挽,最大速度2MHz | (0x4 << 8); // PA10: 浮空输入 // 3. 设置波特率:9600 @ PCLK=72MHz USART1->BRR = 72000000 / 16 / 9600; if ((72000000 % (16 * 9600)) >= (16 * 9600 / 2)) { USART1->BRR += 1; } // 4. 关键!配置奇偶校验与9位模式 USART1->CR1 = 0; // 先清空 USART1->CR1 |= USART_CR1_TE // 使能发送 | USART_CR1_RE // 使能接收 | USART_CR1_M // M=1 → 9位字长 | USART_CR1_PCE // PCE=1 → 使能校验 | USART_CR1_PS; // PS=1 → 奇校验(PS=0为偶校验) // 可选:开启校验错误中断 // USART1->CR1 |= USART_CR1_PEIE; // 5. 最后使能USART USART1->CR1 |= USART_CR1_UE; }发送函数:别忘了是9位!
由于现在是9位数据,DR寄存器也是16位宽。虽然你只关心低9位,但调用时要注意类型匹配。
void USART1_SendByte(uint16_t data) { // data 的低9位有效,高位会被忽略 while (!(USART1->SR & USART_SR_TXE)); // 等待发送缓冲区空 USART1->DR = data & 0x01FF; // 确保只写入9位 }接收函数:第一时间抓错误
接收时必须优先检查PE标志,避免读取无效数据。
uint16_t USART1_ReceiveByte(void) { while (!(USART1->SR & USART_SR_RXNE)); // 等待数据就绪 if (USART1->SR & USART_SR_PE) { // 出现校验错误 USART1->SR &= ~USART_SR_PE; // 清除PE标志(读SR + 写DR可清除) return 0xFFFE; // 返回错误码 } return USART1->DR; // 自动包含接收到的9位数据(低8位是原始数据) }💡小贴士:PE标志的清除方式比较特殊,通常需要“读SR + 读DR”才能清除。有些型号还需要显式写0清除,具体参考参考手册。
更推荐的方式:用HAL库快速搭建
对于大多数项目,特别是使用STM32CubeMX生成工程的情况,强烈建议使用HAL库,既简洁又不易出错。
初始化结构体配置
UART_HandleTypeDef huart1; void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 9600; huart1.Init.WordLength = UART_WORDLENGTH_9B; // 必须设为9位! huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_ODD; // 奇校验 // huart1.Init.Parity = UART_PARITY_EVEN; // 或改为偶校验 huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } }发送与接收带错误检测
uint16_t tx_data = 0x00AA; // 实际发送的数据(低8位有效) uint16_t rx_data = 0; // 发送(自动加校验位) if (HAL_UART_Transmit(&huart1, (uint8_t*)&tx_data, 1, 1000) != HAL_OK) { printf("Transmit failed!\n"); } // 接收 if (HAL_UART_Receive(&huart1, (uint8_t*)&rx_data, 1, 1000) == HAL_OK) { // 手动检查是否有校验错误 if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_PE)) { __HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_PE); printf("⚠️ Parity error detected!\n"); } else { printf("✅ Received: 0x%02X\n", (uint8_t)rx_data); } } else { printf("Receive timeout or error.\n"); }📌重点提醒:HAL库的HAL_UART_Receive不会自动返回PE状态,你需要手动查询并清除标志位。
实战场景:工业RS-485总线中的双重防护
设想这样一个系统:
[STM32主控] ↓ (USART1_TX/RX) [SP3485芯片] ←→ [RS-485总线] ←→ [多个传感器节点]每个节点使用Modbus RTU协议通信,格式如下:
[设备地址][功能码][数据...][CRC16]现在我们在物理层增加一层保护:
✅每一字节都带奇校验(如奇校验)
✅整帧数据仍保留CRC16校验
这样做的好处是什么?
| 场景 | 仅CRC | 加上奇偶校验 |
|---|---|---|
| 单字节单比特翻转 | 可能漏检(特定位置) | 立即捕获 |
| 接收过程中断 | 整帧作废 | 可提前丢弃 |
| 错误定位 | 无法定位 | 可知哪一字节异常 |
| CPU负载 | 接收完才校验 | 硬件实时检测 |
你会发现,奇偶校验让系统的容错能力从“事后补救”变成了“事前拦截”。
常见坑点与调试秘籍
新手最容易栽的几个坑,我都帮你踩过了:
❌ 坑1:忘了设9位字长
huart1.Init.WordLength = UART_WORDLENGTH_8B; // 错!必须是9B结果:校验功能看似开了,实则没生效。
❌ 坑2:两边校验模式不一致
发送方用奇校验,接收方用偶校验?那每一个字节都会报错。
🔧秘籍:用串口助手(如XCOM)手动发送测试数据,观察是否持续报PE。
❌ 坑3:波特率偏差过大
超过±2%的误差会导致采样偏移,即使数据正确也可能误判为校验失败。
🔧秘籍:使用内部高速时钟(HSI)时尤其注意分频精度,优先使用外部晶振。
❌ 坑4:忘记清除PE标志
一次错误后未清除标志,后续所有接收都会被认为是错误。
🔧秘籍:每次处理完PE后务必调用__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_PE);
结语:让通信更可靠,从一个小配置开始
奇偶校验不是一个炫酷的新技术,它早在上世纪就存在。但它之所以经久不衰,正是因为它简单、高效、低成本。
在你的下一个STM32项目中,只要涉及UART通信,不妨问自己一句:
“我是不是也应该加上这一道防线?”
哪怕只是多花一行配置代码,换来的是系统稳定性质的飞跃。
毕竟,在工业现场,少一次宕机,可能就等于省下了几千块的维护成本。
如果你正在做传感器采集、PLC通信、智能仪表或者任何依赖串行链路的项目,欢迎在评论区分享你的抗干扰经验。我们一起打造更健壮的嵌入式系统。