简介:面向STM32F103C8T6开发者的一份串口2(USART2)奇偶校验通信工程实例,适合正在学习STM32标准外设库或HAL库的嵌入式爱好者。工程完整演示了从RCC时钟使能、GPIO复用映射到USART控制寄存器配置的全过程,涵盖波特率、数据位、停止位以及PCE与PS位设置的奇偶校验选择,并提及中断与DMA用于非阻塞收发的扩展方式,同时对比标准外设库与HAL库的初始化思路。压缩包共78个文件,以C源文件与H头文件为主,辅以Keil工程文件、汇编启动文件、Hex烧录文件、调试配置文件及README说明,可结合代码对照理解底层寄存器操作与库函数封装逻辑;整体大小仅308KB,结构清晰、便于查阅。目前已有2442人学习下载,适合需要快速验证串口通信配置或参考工程模板的开发者。 最近在调一台工控设备的通信,对方的协议里明确写着“8E1”,结果我们的板子一直收乱码。排查了半天,最后发现问题是串口2的奇偶校验根本没配置到位。这种问题在STM32F103开发里太典型了——不是不会配,而是很多人对“奇偶校验到底改变了什么”理解得不够透,导致配置漏掉关键步骤,通信一挂就抓瞎。
这篇文章我就围绕STM32F103的串口2(USART2)展开,把奇偶校验的硬件机制、标准库配置方法、收发配合的细节以及调试排错经验一次讲清楚。适合刚接触单片机通信的初学者,也适合那些用Modbus RTU、自定义协议做设备联调但总被校验位坑到的工程师参考。
1. 奇偶校验到底在解决什么问题:先搞懂原理再动手
1.1 串口帧里的各位是怎么排的
串口通信(UART)的每一帧数据,从示波器上看就是一段固定的电平序列。一个完整的帧通常由这几部分组成:
- 起始位:1位低电平,告诉接收端“我要开始发数据了”
- 数据位:常见的是8位,低位在前
- 校验位:可选,1位
- 停止位:1位或2位高电平,标志一帧结束
我们常说的“8N1”,指的就是8个数据位、无校验位(None)、1个停止位。而“8E1”则是8个数据位、偶校验(Even)、1个停止位。“8O1”对应奇校验(Odd)。
1.2 奇偶校验的工作逻辑
奇偶校验的原理非常简单:统计一帧数据中“1”的个数,然后根据约定给校验位赋值。
- 偶校验:数据位中“1”的个数加上校验位后,总数必须为偶数
- 奇校验:数据位中“1”的个数加上校验位后,总数必须为奇数
举个例子:如果数据是0x55,也就是二进制01010101,里面有4个“1”。
- 偶校验时,校验位填0,让总数保持偶数
- 奇校验时,校验位填1,让总数变成5个,成为奇数
接收端拿到数据后,自己也算一遍“1”的个数,再和校验位对比。对不上,就说明这一帧在传输过程中被干扰了。
1.3 奇偶校验能干什么,不能干什么
奇偶校验是一个开销极低、硬件自动完成的差错检测手段。它不需要额外的通信协议支持,也不消耗额外的数据帧长度,只要发送端和接收端约定一致即可。
但它的检测能力非常有限——只能检测出奇数个位翻转。如果一帧数据里恰好有两个或多个偶数个位被干扰,奇偶校验就“看不出来”。而且它只能报错,不能纠错,更不能判断是数据位错了还是校验位错了。
所以,在真实的项目中,奇偶校验通常是和更高层的校验手段(比如Modbus RTU的CRC16)配合使用。底层校验负责尽早发现字节级错误,上层校验负责保证整帧数据的完整性。
2. STM32F103串口2的奇偶校验硬件机制:数据位和校验位是怎么纠缠的
2.1 串口2在芯片里的位置与引脚
STM32F103的串口2(USART2)挂载在APB1总线上,这是很多人在配置时钟时容易搞错的地方。APB1最高频率是36MHz,而串口1(USART1)挂载在APB2上,最高72MHz。使用标准库时,串口2的时钟使能函数和串口1也不一样:
- USART1:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE) - USART2:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE)
如果不小心把串口2的时钟用成了APB2使能,那串口2就完全没有反应,调试半天才发现是时钟没挂上。
串口2默认复用引脚是PA2(TX)和PA3(RX)。也可以通过重映射功能把这两个引脚换到PD5和PD6,但重映射需要额外开启AFIO时钟,并且在配置完GPIO之后还要调用GPIO_PinRemapConfig(GPIO_Remap_USART2, ENABLE)。多数项目直接用PA2/PA3就足够了,重映射用得比较少。
2.2 M位、PCE位和PS位的组合关系
这是整个奇偶校验配置里的重点,也是很多人写代码出错的高发区。STM32F103的USART控制寄存器USART_CR1里,有三个位和帧格式相关:
M:字长选择。M=0时,字长为8位;M=1时,字长为9位PCE:奇偶校验使能。PCE=0时,无校验;PCE=1时,有校验PS:校验选择。PS=0时,偶校验;PS=1时,奇校验
关键来了——当PCE=1时,实际传输的帧长度会变成数据位加校验位。也就是说:
- 如果M位配置为0(8位字长),开启校验后,实际传输的是8位数据加1位校验位,总共9位。这是最常见的使用方式,对应“8E1”或“8O1”
- 如果M位配置为1(9位字长),开启校验后,实际传输的是9位数据加1位校验位,总共10位,这种情况下数据位就应该是9位
所以,标准库配置中,只要你想做8位数据加校验位,就必须把数据结构体里的WordLength设置为9位数据模式。这一个操作,就是为了给帧里的校验位腾出空间。所以写成WordLength = USART_WordLength_9b并不是说你在发9位原始数据,而是告诉硬件“发送或接收的完整帧要占9位,其中8位是用户数据,1位由硬件根据奇偶校验规则自动生成或解析”。
很多初学者不理解这一点,把WordLength设置成8位,然后又开了校验位。硬件会把这8位当成9位帧来处理,最后发送的数据格式和接收端的解析完全对不上,通信必然混乱。
2.3 硬件自动完成的工作
开启奇偶校验后,发送端用户不需要自己计算校验位,硬件会在发送时根据数据位中的“1”的个数自动把校验位填好。接收端硬件也会自动检查校验位和数据位是否匹配,如果发现不匹配,硬件会置起PE标志(Parity Error)。
PE标志位于USART_SR状态寄存器的第0位。这个标志一旦置起,就说明接收端检测到了帧错误。此时如果没有做任何处理,下一次接收可能还会被这个错误状态干扰。尤其是在中断接收场景下,必须在读取数据之后手动清除PE标志位,否则会出现中断风暴或者接收逻辑错乱。
2.4 一个很多人不知道的细节:USART_DR的第9位
STM32F103的USART数据寄存器USART_DR是16位的,但有效数据最多只有9位。当开启了校验位并选择9位字长时,USART_DR的最高有效位存储的就是校验位对应的数据。
表面上看,这个校验位是给硬件用的,软件一般不需要关心。但在某些调试场景下,你可以通过直接读取USART_DR的第9位来观察硬件解析出来的校验值,这在排查校验逻辑是否匹配时很有用。
发送端也有类似机制。USART_SendData函数接收的是uint16_t类型的数据,虽然在大部分例程里只使用低8位,但在开启校验的场景下,如果你手动把第9位也写进去,这个值会覆盖硬件自动生成的校验位。普通的串口应用不建议这么干,但在某些自定义协议里,这种特性反而可以拿来快速组装特殊帧。
3. 标准库V3.5配置串口2奇偶校验的完整代码
3.1 GPIO和时钟的初始化
使用标准外设库(StdPeriph_Lib V3.5)时,串口2的初始化代码可以拆成三步:GPIO配置、USART配置、NVIC中断配置。先看GPIO和时钟部分:
#include "stm32f10x.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "stm32f10x_usart.h" #include "misc.h" void USART2_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); }这里有个细节:TX引脚必须配置为复用推挽输出(GPIO_Mode_AF_PP),而RX引脚配置为浮空输入(GPIO_Mode_IN_FLOATING)或上拉输入都可以,浮空输入是最保守的选择。
3.2 USART结构体里最关键的三行配置
USART协议的参数都封装在USART_InitTypeDef结构体里。对于串口2奇偶校验场景,核心配置如下:
void USART2_Init(void) { USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = 9600; USART_InitStructure.USART_WordLength = USART_WordLength_9b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_Even; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_RX | USART_Mode_TX; USART_Init(USART2, &USART_InitStructure); USART_Cmd(USART2, ENABLE); }在这段代码里,三条配置需要特别注意:
USART_WordLength = USART_WordLength_9b:前面解释过,开启校验后必须用9位字长。这个不是预留数据位,而是为校验位留位置USART_Parity = USART_Parity_Even:偶校验模式。如果要用奇校验,改成USART_Parity_OddUSART_BaudRate = 9600:波特率。这个值和通信双方设备必须完全一致,哪怕差百分之二,长时间通信后也会出现偶发错帧
很多工程项目中,Modbus RTU默认使用偶校验,所以上面的代码适用于大部分不改协议的场合。
3.3 发送和接收函数怎么写
配置好串口后,发送函数的实现相对简单,奇偶校验完全由硬件完成,用户代码层面和普通串口一样,只是在库函数参数类型上要注意一下:
void USART2_SendByte(uint16_t data) { USART_SendData(USART2, data); while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET); }这里USART_SendData的第二个参数类型是uint16_t,和普通8位数据的写法看起来一样。不需要用户在软件里计算奇偶校验位,硬件会自动填充。
接收部分最简单的方案是轮询:
uint16_t USART2_ReceiveByte(void) { while (USART_GetFlagStatus(USART2, USART_FLAG_RXNE) == RESET); return (uint16_t)USART_ReceiveData(USART2); }但工程上更推荐中断接收,避免主循环被串口阻塞。
3.4 接收中断和PE错误处理的完整套路
开启接收中断时,必须一起处理PE(奇偶校验错误)标志。否则一旦通信线路出现干扰或者双方校验配置不一致,PE标志会一直置起,导致后续数据完全无法接收。
void USART2_NVIC_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); USART_ITConfig(USART2, USART_IT_PE, ENABLE); }中断服务函数里,标准写法是先读SR,再读DR,顺序不能反:
void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_PE) != RESET) { USART_ClearITPendingBit(USART2, USART_IT_PE); } if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { uint16_t data = USART_ReceiveData(USART2); // 这里放入自己的数据处理逻辑 } }如果只开了RXNE中断没有处理PE错误,PE标志占着不释放,整个中断系统的响应都会变得异常。
4. 两端设备配合中的坑:从串口助手到对端设备
4.1 串口调试助手的校验位设置
调试串口2奇偶校验时,最常用的工具就是串口调试助手。在使用串口助手连接STM32F103开发板时,需要保证软件底部的通信参数和单片机端完全一致。比如单片机设置了8位数据、偶校验、1位停止位,那么串口助手里就要选择9600、8、E、1。
很多串口助手在界面上的下拉框是分开的:数据位一个下拉框,校验位一个下拉框,停止位一个下拉框。把校验位选成“Even”或“奇校验”即可。如果选错了,串口助手发出来的帧格式就和单片机期望的不一致,表现为单片机要么收不到数据,要么收到的数据是错的。
| 参数 | 设置值 |
|---|---|
| 波特率 | 与单片机一致,如9600 |
| 数据位 | 8 |
| 校验位 | Even(偶校验)或 Odd(奇校验) |
| 停止位 | 1 |
4.2 对端设备没有校验位时的表现
在真实项目中,最容易踩的坑是:自己这边的单片机代码实现了奇偶校验,但对端设备还是无校验设置。这时候会有什么现象?
假如对端设备用8N1格式发数据,单片机用8E1格式接收。由于帧长度定义不同、校验位处理方式不同,单片机会把对端发来的停止位或下一个帧的起始位当作校验位来解析,轻则偶发错一个字节,重则每帧都报错。
反过来也一样:单片机开了偶校验发送数据,对端配置成无校验,对端会收到9位长度的帧,但按8位数据来解析,导致每个字节的最后一位丢失,数据整体错乱。
4.3 示波器下的波形验证方法
如果你有示波器,最直观的验证方法是直接把TX引脚的电平波形抓出来看。发送一帧0x55的数据时,开启偶校验后,如果数据是01010101,里面4个“1”,那波形上应该能看到数据位后有第9位,且这一位是低电平(0)。
通过这个波形,你可以一眼判断出硬件是否真的把校验位加上了,也能判断停止位的位置是否正常。这个方法在排查“代码好像配置了,但实际没生效”的问题时非常管用。
4.4 自测回环方法
如果手头没有第二块板子,做一个最简单的自测:把STM32F103串口2的TX和RX引脚短接,然后在主循环里定时发送一包数据,看接收端能否原样收到。如果收到数据和自己发送的一致,说明串口2的硬件收发链路没问题;如果收到的数据和发送的不一致,且开启了校验位,那就要检查奇偶校验模式是否匹配了。
不过要注意,自环测试只能验证本机收发链路,不能验证两端设备之间通信参数的匹配关系,因为单片机自己和自己通信,两端参数肯定是一致的。
5. 奇偶校验失败排查链路:一个典型的调试验证过程
5.1 先关校验,验证基础链路
我做串口排查时有个习惯,第一步永远是先把校验位关闭,用最基础的8N1格式跑一遍。这一步的目的不是让步,而是为了切割问题域。
如果8N1模式下数据收发完全正常,说明波特率、GPIO复用、时钟配置这些都是好的,问题大概率出在校验配置上。如果8N1模式本身都通不过,那就别急着碰校验了,先解决基础链路。
5.2 开校验后收不到数据,问题出在哪
开校验之后,最典型的现象就是完全收不到数据,或者收上来一大片乱码。按照经验,优先级最高的排查项是WordLength配置。因为很多人开校验后,WordLength还是默认设置的8位字长,导致硬件对帧长度的解析和实际发送方的帧结构不匹配。
第二个排查项是PE中断。开着PE中断但没做标志清除,会导致接收被PE中断卡死,看起来就像“完全收不到数据”。处理办法是在中断服务函数里加上PE标志清除逻辑,并且在主程序里把PE对应的错误状态打印出来,看看是不是一直在周期性报错。
5.3 偶发数据错误,但又不经常复现
这种情况最折磨人。现象是通信整体能工作,但隔一段时间就错一帧。我通常按下面这个顺序排查:
- 先确认波特率是否准确。可以用示波器测量TX引脚实际波形的高电平宽度,和理论值对比。如果单片机主频晶振偏了,波特率会偏离,长时间传输后偶发错帧概率很高
- 再看线路质量和接地。校验位检测的是串行链路上的干扰,如果线太长、没有接地回路,偶发错误很难根治
- 最后确认两端校验模式是否真正一致。特别注意大小写、伪配置等细节,有些代码里写了USART_Parity_Even,但结构体没有被真正调用或者被后边的代码覆盖了
5.4 一个实际项目的调参经验
我之前做过一个通过RS232和工业触摸屏通信的项目,触摸屏协议配置界面里写的是“偶校验”,我还特意确认过,代码里也配置成了USART_Parity_Even。可现场就是通信异常。
后来用串口助手单独接触摸屏,模拟单片机发送数据,触发响应,再用逻辑分析仪抓触摸屏回发的数据,一帧一帧地分析。最后发现触摸屏虽然界面显示偶校验,但实际发出的数据帧里根本没有校验位,也就是说它界面配置和实际行为不一致。把单片机这边的校验位关掉,改成无校验后,通信马上正常。
这个经历给我的启发是:不要完全相信对端设备的配置显示,一定要从实际波形或者实际收发的帧结构里验证。仪器不会说谎。
写在最后的几个实操建议
如果需要我在文章里直接给出一个最通用的模板,那我会建议:先把8N1跑通,再把校验位打开,用串口助手和示波器双重验证,最后再挂对端设备联调。这个顺序能帮你把“配置问题”“硬件问题”“对端兼容问题“这几个层面逐个隔离。
在实际项目中,我个人做串口链路时会坚持几个习惯:一是所有通信参数集中定义成宏,不要散落在代码里;二是初始化完成后回读USART_InitTypeDef里的配置确认生效;三是在量产前用校验位+CRC24配合做一次长期老化通信测试。串口通信看起来简单,但真正到了工业现场,帧错误、乱码、偶发断线,都是靠这些底层细节兜住的。奇偶校验只是其中一环,把这一环吃透,后面再接Modbus或者其他协议栈,就会顺手很多。
本文还有配套的精品资源,点击获取