ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F103 USART2奇偶校验配置详解:避开9位数据位的坑

STM32F103 USART2奇偶校验配置详解:避开9位数据位的坑

简介:这份资源面向STM32嵌入式开发初学者与需要实现可靠串口通信的工程师,聚焦STM32F103C8T6的USART2带奇偶校验配置。内容围绕时钟使能、GPIO复用映射、USART控制寄存器中PCE与PS位设置、中断与DMA可选方案以及串口启动流程展开,帮助读者理解奇偶校验在数据链路层的实际作用,并可直接移植到传感器、调试工具等外设通信场景。压缩包共78个文件,约308KB,以34个h头文件与33个c源文件为主体,涵盖标准外设库、CORE内核文件、SYSTEM与HARDWARE驱动模块、USER用户代码及Keil工程与hex目标文件,结构完整便于对照编译。目前已有2445人学习下载。通过该工程,读者可掌握串口2奇偶校验的寄存器级配置方法,获得可直接烧录验证的完整代码框架,并借助README与工程配置快速复现实验,为后续多串口通信与协议解析打下基础。

1. 串口通信里那个被忽略的校验位:STM32F103 串口2奇偶校验到底怎么配

很多人调 STM32 串口,USART1 收发正常就以为大功告成,等到把传感器或工控设备接到 USART2 上,突然发现收到的数据偶尔错一个字节,或者干脆整帧乱掉。回头翻手册才发现,对方设备用的是 9 位数据加奇偶校验,而自己初始化时只写了 8 位无校验。STM32F103 的 USART2 挂在 APB1 总线上,和 USART1 的时钟来源不同,配置奇偶校验时涉及数据位长度、校验位使能、停止位组合这几个寄存器字段的联动,稍不留神就会掉进“校验位占了数据位”的坑里。这份资源围绕 USART2 的奇偶校验配置展开,从寄存器层面讲清 PCE、PS、M 这几个位的配合关系,再落到标准库和 HAL 库两套代码上,适合正在做多串口通信、需要和 Modbus 或带校验的工业设备对接的嵌入式开发者。

2. 奇偶校验的寄存器逻辑:为什么 9 位数据位是个绕不开的坎

2.1 校验位不是“额外加一位”,而是从数据位里借

STM32F103 的 USART 模块里,数据位长度由 USART_CR1 寄存器的 M 位控制。M=0 时数据位是 8 位,M=1 时是 9 位。奇偶校验使能位 PCE 在 USART_CR1 的 bit10,校验选择位 PS 在 bit9,PS=0 选偶校验,PS=1 选奇校验。

关键点在于:当 PCE=1 时,校验位会占用数据帧里的最高位。也就是说,如果你设了 M=0(8 位数据)又开了 PCE,实际有效数据只有 7 位,第 8 位被校验位吃掉了。很多人在这一步翻车,写进去的数据明明是对的,读出来却少了最高位,就是因为没意识到校验位是从数据位里“借”的。

所以正确做法是:需要 8 位数据加奇偶校验时,M 必须设成 1,也就是 9 位数据位。这样 9 位里低 8 位是有效数据,最高位是校验位。接收端同样按 9 位读,取低 8 位就是真实数据。

常见做法是在初始化结构体里把 WordLength 设成 UART_WORDLENGTH_9B,Parity 设成 UART_PARITY_EVEN 或 UART_PARITY_ODD。标准库那边对应的是 USART_WordLength_9b 和 USART_Parity_Even / USART_Parity_Odd。

2.2 USART2 的时钟使能和引脚复用别搞混

USART2 的时钟来自 APB1,使能函数是 RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE)。USART1 用的是 APB2,这是两个不同的总线,写错了一边收不到数据一边还查不出原因。

引脚方面,USART2 默认映射在 PA2(TX)和 PA3(RX)。如果要用重映射,得先开 AFIO 时钟,再调 GPIO_PinRemapConfig。我一般会在初始化最开始就把 GPIO 和 USART 的时钟都打开,避免后面调试时因为时钟没开导致寄存器写不进去。

下面是一段标准库的 USART2 初始化代码,带偶校验:

void USART2_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 使能 GPIOA 和 USART2 时钟,注意 USART2 在 APB1 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // PA2 复用推挽输出,作为 TX 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); // PA3 浮空输入,作为 RX GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // 串口参数:9600,9 位数据,偶校验,1 位停止位 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_9b 和 USART_Parity_Even 是绑定的,不能拆开用。波特率 9600 只是示例,实际要跟对端设备一致。停止位一般用 1 位,如果对方是 2 位停止位,这里也要改。

2.3 发送和接收时的数据宽度处理

初始化成 9 位之后,发送函数要传 9 位数据。标准库的 USART_SendData 第二个参数是 uint16_t,低 9 位有效。如果你只传 8 位数据,最高位是 0,校验位由硬件自动算,这没问题。但接收时 USART_ReceiveData 返回的也是 9 位,你得手动取低 8 位:

uint8_t ch; if (USART_GetFlagStatus(USART2, USART_FLAG_RXNE) != RESET) { // 取低 8 位,丢弃校验位 ch = (uint8_t)(USART_ReceiveData(USART2) & 0x00FF); }

这里有个细节:如果你不取低 8 位,直接把返回值当 8 位用,编译器会隐式截断,结果可能碰巧对,但代码可读性差,换个人维护容易懵。我习惯显式写 & 0x00FF,一眼就能看出是在丢校验位。

3. HAL 库下的 USART2 奇偶校验配置:CubeMX 生成之后还要改什么

3.1 CubeMX 里选 9 位和校验位,生成的代码长什么样

用 CubeMX 配 USART2 时,Parameter Settings 里 Word Length 选 9 Bits,Parity 选 Even 或 Odd。生成之后 MX_USART2_UART_Init 函数里会看到 huart2.Init.WordLength = UART_WORDLENGTH_9B; 和 huart2.Init.Parity = UART_PARITY_EVEN;。

但 CubeMX 生成的代码有个地方要注意:它默认把 huart2.Init.Parity 和 huart2.Init.WordLength 分开写,如果你手动改代码时只改了 Parity 没改 WordLength,编译能过,运行起来数据就错。我一般会在初始化之后加一句断言或者打印,确认这两个值匹配。

HAL 库的收发函数和标准库不同,发送用 HAL_UART_Transmit,接收用 HAL_UART_Receive,数据指针是 uint8_t*。但底层寄存器还是 9 位的,HAL 库帮你做了截断。发送时你传 8 位缓冲区,HAL 会把每个字节放到低 8 位,校验位硬件算。接收时 HAL 把 9 位数据的低 8 位存进缓冲区,校验位丢掉。

// HAL 库发送,pData 是 uint8_t 缓冲区 HAL_UART_Transmit(&huart2, pData, len, 1000); // HAL 库接收,同样用 uint8_t 缓冲区 HAL_UART_Receive(&huart2, pData, len, 1000);

这里有个坑:HAL_UART_Receive 是阻塞式的,超时参数单位是毫秒。如果你在中断里调用它,超时设太长会卡住整个中断。我一般用 HAL_UART_Receive_IT 或者直接操作寄存器,避免在中断上下文里用阻塞收发。

3.2 中断接收时校验错误标志怎么处理

开了奇偶校验之后,接收端如果校验失败,硬件会置位 USART_SR 的 PE 位(Parity Error)。标准库用 USART_GetFlagStatus(USART2, USART_FLAG_PE) 查,HAL 库在中断回调里通过 huart2.ErrorCode 判断。

很多人不处理 PE 标志,结果校验错的那一帧数据被当成正常数据读走,后面解析全乱。正确做法是在接收中断里先查 PE,如果置位了,读一次 USART_ReceiveData 把脏数据清掉,然后直接返回,不往缓冲区里存。

void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { // 先查校验错误 if (USART_GetFlagStatus(USART2, USART_FLAG_PE) != RESET) { // 读一次清标志,丢弃这帧 (void)USART_ReceiveData(USART2); return; } uint8_t ch = (uint8_t)(USART_ReceiveData(USART2) & 0x00FF); // 存进环形缓冲区 RingBuffer_Put(&rxBuf, ch); } }

这段代码里先查 PE 再读数据,顺序不能反。如果先读数据再查 PE,标志可能已经被清了,查不到。另外 PE 标志的清除方式是“先读 SR 再读 DR”,标准库的 USART_GetFlagStatus 和 USART_ReceiveData 组合起来正好满足这个时序。

3.3 波特率误差对校验的影响

奇偶校验只能检错不能纠错,如果波特率误差太大,采样点偏移,收到的数据位本身就错了,校验位再对也白搭。STM32F103 在 9600 波特率下误差很小,但跑到 115200 以上时,如果 APB1 时钟不是标准值(比如 36MHz 而不是 36.864MHz),误差可能超过 2%,这时候校验失败率会明显上升。

我一般会在初始化之后用示波器或者逻辑分析仪抓一下 TX 波形,量一个位宽,算实际波特率。没有仪器的话,可以发一个已知字节,用另一块板子收,看误码率。如果误码率高,先降波特率试,确认是时钟问题还是校验配置问题。

4. 避坑与排查:USART2 奇偶校验最常见的五个翻车现场

4.1 现象:数据最高位总是 0,低 7 位正常

原因:WordLength 设成了 8 位,Parity 开了。校验位占了第 8 位,有效数据只剩 7 位,最高位被硬件当校验位处理了。

解决:把 WordLength 改成 9 位。标准库用 USART_WordLength_9b,HAL 用 UART_WORDLENGTH_9B。改完重新编译烧录,用逻辑分析仪看波形,数据位应该是 9 个。

4.2 现象:能发不能收,或者能收不能发

原因:GPIO 模式配错。TX 要配成复用推挽输出,RX 要配成浮空输入或上拉输入。有人把 TX 配成普通推挽输出,结果波形出不来;有人把 RX 配成推挽输出,直接把对端 TX 拉死。

解决:检查 GPIO_InitStructure.GPIO_Mode。TX 用 GPIO_Mode_AF_PP,RX 用 GPIO_Mode_IN_FLOATING。如果是 HAL 库,CubeMX 一般会配好,但手动改代码时容易漏。

4.3 现象:校验错误标志 PE 一直置位,读不到正确数据

原因:对端设备的校验方式和你配的不一致。你配偶校验,对方是奇校验,或者对方根本没开校验。还有一种可能是停止位不匹配,对方 2 位停止位,你配 1 位,帧边界对不上。

解决:拿示波器抓一帧完整波形,数一下数据位和校验位。或者用串口调试助手,把校验方式逐个试。确认一致后,PE 标志应该只在真正出错时置位。

4.4 现象:中断里读数据,偶尔丢帧

原因:中断服务函数里处理时间太长,或者没清中断标志。USART2 的中断标志清除方式和 USART1 一样,读 SR 再读 DR 自动清 RXNE。但如果你在中断里调用了 HAL_UART_Receive 这种阻塞函数,会把中断卡住。

解决:中断里只做最少的操作——读数据、存缓冲区、清标志。解析和业务逻辑放到主循环。如果用了 HAL 库,用 HAL_UART_Receive_IT 注册回调,在回调里存数据。

4.5 现象:换到 USART1 正常,USART2 就是不行

原因:时钟总线搞混了。USART1 在 APB2,USART2 在 APB1。有人复制 USART1 的代码改端口号,忘了把 RCC_APB2PeriphClockCmd 改成 RCC_APB1PeriphClockCmd,结果 USART2 根本没时钟,寄存器写不进去。

解决:检查时钟使能函数。USART2 用 RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE)。另外注意 APB1 的最高频率是 36MHz,如果你系统时钟超了,USART2 的波特率会算错。

5. 用示波器验证校验位:一个可以复用的排查习惯

校验位这东西,代码里配没配对,光看寄存器值有时候不放心。我习惯在第一次调通之后,用逻辑分析仪抓一帧完整波形,手动数一下位。具体做法是:让 USART2 发一个已知字节,比如 0x55(二进制 01010101),偶校验时校验位应该是 0(因为 1 的个数是 4,偶数)。抓到的波形应该是:起始位 0,然后 8 位数据 01010101,然后校验位 0,然后停止位 1。

如果校验位不对,比如发 0x55 抓出来校验位是 1,那说明 PS 位配反了,偶校验配成了奇校验。这时候改 USART_Parity_Odd 或者 UART_PARITY_ODD 就行。

下面这个表格是我常用的几个测试字节和对应的偶校验位,抓波形时可以直接对照:

发送字节二进制1 的个数偶校验位奇校验位
0x0000000000001
0x5501010101401
0xAA10101010401
0xFF11111111801
0x0100000001110
0x0300000011201

抓波形的时候,触发条件设成下降沿(起始位),时基调到能看清一个位宽。9600 波特率下,一个位宽约 104 微秒,时基设 50 微秒每格比较合适。115200 波特率下,一个位宽约 8.68 微秒,时基要设到 2 微秒每格。

还有一点:如果你用的是 USB 转串口模块接 USART2,模块本身可能不支持 9 位数据。很多 CH340 和 CP2102 的默认配置是 8 位无校验,你发 9 位数据过去,它可能把校验位当停止位处理,导致接收端看到的数据错位。这种情况我一般直接用另一块 STM32 板子对收,绕过 USB 转串口芯片。

从那以后我每次配 USART2 奇偶校验,都会先抓一帧波形确认校验位,再跑数据。这个习惯帮我省了不少来回改代码的时间。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进