简介:面向STM32嵌入式开发者的一份实战工程,基于Cortex-M3内核的F103系列单片机,完整演示串口2带奇偶校验通信的配置方法。资源共78个文件,以头文件和C源码为主,包含串口、按键、LED、延时等模块驱动以及标准外设库,并附有Keil工程与说明文档,压缩包仅308KB,便于快速下载对照。目前已有2442人学习,工程从时钟使能、GPIO复用写到USART控制寄存器奇偶校验位设置,同时给出基于标准外设库的底层操作,有助于理解串口流程与校验原理。无论入门串口应用还是参考校验细节,这份工程都能提供可直接使用的代码框架与排错思路。 做嵌入式这些年,串口通信是我用过最多,也最容易被细节绊倒的功能。最近在做一块基于STM32F103的设备板,因为串口1被调试打印占用,串口2(USART2)就负责跟外部工业设备交互。对方协议明确要求“偶校验”,刚开始我以为就是把标准库里的校验位参数从 No 改成 Even,结果一上电就发现:发出去的字节对方能收到,但内容全不对,而且接收端数据错乱,调试助手那边显示出来一堆乱码。折腾了一下午,最后发现是数据位和校验位配置的关系没搞清楚。
这篇博文就以STM32F103 + 标准外设库 V3.5 为背景,把串口2配置成带奇偶校验的完整过程讲清楚。内容包括:奇偶校验的原理、USART2的时钟和引脚、寄存器级帧结构分析、完整可复用的初始化代码、收发中断处理,以及我在实际调试中遇到的典型问题和排查方法。适合正在用F103做串口通信、准备对接上位机或工业设备、甚至想移植Modbus RTU协议栈的同学参考。
1. 项目背景与设计选型思路
1.1 为什么选择串口2来承担校验通信
很多实际项目的资源分配是:串口1接调试串口,串口2接业务通信。STM32F103的USART1挂载在APB2总线上,主频72MHz;而USART2和USART3挂在APB1总线上,主频36MHz。虽然在标准库中波特率计算会自动处理这个差异,不需要手动配置BRR,但我们要记住这个前提:USART2的时钟源是PCLK1(36MHz),不是72MHz。这在用寄存器方式配置波特率时特别容易踩坑,库函数封装好了反而省心。
USART2的默认引脚是PA2(TX)和PA3(RX),重映射后可以放到PD5和PD6。大多数情况下我们不需要重映射,PA2/PA3足够用。选择USART2还有一个原因是它在中断处理上比较独立,不会跟串口1的调试打印互相干扰,开发和调试时逻辑清晰。如果你的板子上PA2/PA3被其他外设占用,再考虑重映射,或者换用USART3的PB10/PB11。
1.2 奇偶校验的应用场景与取舍
奇偶校验是串口通信中最基础、开销最小的一种检错手段。它的核心思想是:发送端根据本帧数据中“1”的个数,自动填一个校验位;接收端重新计算“1”的个数,如果不匹配,就认为这一帧出错。偶校验要求一帧中“1”的总数为偶数,奇校验则要求为奇数。
它适合的场景是:通信距离较短、电磁干扰不严重但仍有偶发噪声的RS232链路,以及一些老式工业设备、传感器、上位机PLC的固定协议。比如Modbus RTU的标准串口参数之一就是“8E1”,含义是8位数据、偶校验、1位停止位。所以做Modbus设备时,奇偶校验几乎是绕不开的。
但奇偶校验也有明显的局限性:它只能检测奇数个比特错误,如果一帧中有偶数个比特发生翻转,校验依然能通过;而且校验位本身也可能出错。因此它只负责“报错”,不负责“纠错”。如果需要更高可靠性,通常在协议层再叠一层CRC校验,比如Modbus RTU的CRC16。物理层的奇偶校验负责单帧检错,协议层的CRC负责整条报文完整性校验,两者并不冲突。
2. 奇偶校验原理与STM32帧格式细节
2.1 一帧数据里到底有哪些位
很多人刚接触STM32的校验配置时,会忽略一个关键点:奇偶校验位不是额外加在停止位后面的,而是硬件自动嵌在数据位和停止位之间。一帧典型的结构是:
起始位(1位) + 数据位(7或8位) + 校验位(1位) + 停止位(1或2位)
也就是说,开了奇偶校验后,真正传输的有效数据位数会发生变化。STM32F103的USART外设通过CR1寄存器里的M位(数据字长)和PCE位(奇偶校验使能)共同决定帧结构:
- M = 0,PCE = 0:1起始位 + 8数据位 + 停止位
- M = 0,PCE = 1:1起始位 + 7数据位 + 1校验位 + 停止位
- M = 1,PCE = 0:1起始位 + 9数据位 + 停止位
- M = 1,PCE = 1:1起始位 + 8数据位 + 1校验位 + 停止位
看到这里你应该明白了:要传输标准的8位有效数据并带奇偶校验,必须让USART工作在9位字长模式下,其中1位是校验位,剩下8位才是真实数据。如果只设置了8位字长且开启校验,那么有效数据会被压到7位,收发双方只要多传几个字节,数据就会对不上。
2.2 标准库参数与寄存器的对应关系
在标准库中,这个配置体现在两个参数上:
USART_InitStructure.USART_WordLength = USART_WordLength_9b; USART_InitStructure.USART_Parity = USART_Parity_Even;USART_WordLength_9b对应M位为1,USART_Parity_Even对应PCE为1且PS为0,USART_Parity_Odd对应PCE为1且PS为1。不少人只改了Parity,没有改WordLength,结果就是开成7位数据+校验位,通信双方只要约定的有效数据是8位就必然出错。这是我第一次移植Modbus RTU偶校验方案时踩得最深的坑。
还有一个迷惑点:很多串口调试助手里显示的“数据位”选择,比如8、7、6这些数字,是指有效数据位,并不包含校验位。所以如果协议约定的是“8E1”,调试助手里面通常也选8位数据位、偶校验、1位停止位;但在STM32寄存器层面,USART的M位必须是9位字长模式。这两套表述的逻辑并不冲突,但“8位”这个词在协议侧和芯片侧的含义不一样。
3. USART2带奇偶校验的完整实现
3.1 时钟、引脚与初始化配置
先用标准库写一个通用初始化函数。这个函数不仅支持开关奇偶校验,还支持偶校验和奇校验切换,方便调试时直接换参数。
void USART2_Init(uint32_t baud, uint16_t parity) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 1. 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 2. 配置PA2为TX复用推挽,PA3为RX浮空输入 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); // 3. USART初始化 USART_InitStructure.USART_BaudRate = baud; USART_InitStructure.USART_WordLength = (parity == USART_Parity_No) ? USART_WordLength_8b : USART_WordLength_9b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = parity; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART2, &USART_InitStructure); // 4. 使能接收中断和校验错误中断 USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); USART_ITConfig(USART2, USART_IT_PE, ENABLE); // 5. 配置NVIC 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); // 6. 使能USART USART_Cmd(USART2, ENABLE); }调用方式:
// 使用9600波特率、偶校验 USART2_Init(9600, USART_Parity_Even); // 使用115200波特率、无校验(适用于前期裸调) USART2_Init(115200, USART_Parity_No);PA3配置成浮空输入是基于大多数TTL电平串口直接互联的情况。如果板子上PA3有外部上下拉电阻,或者对方设备是开漏输出,可以考虑使用上拉输入模式。这个要看电路,不能一概而论。
3.2 发送函数与接收中断处理
发送数据时,STM32硬件会在写入DR寄存器后自动计算校验位,并按照配置好的帧格式发送。我们不需要手动做任何异或计算。
void USART2_SendByte(uint8_t data) { // 等待发送数据寄存器清空 while ((USART2->SR & USART_FLAG_TXE) == 0); // 写入数据,硬件会自动附加校验位 USART2->DR = data; } void USART2_SendString(char *str) { while (*str) { USART2_SendByte((uint8_t)*str++); } }接收中断里,我习惯用寄存器直接读DR,而不是标准库的USART_ReceiveData。原因是校验错误出现时,读SR再读DR是清除PE标志最直接的办法;同时,顺手把DR的低8位取出来,保证在9位字长模式下不会误把校验位当数据。
void USART2_IRQHandler(void) { // 先处理校验错误,再处理正常接收 if (USART_GetITStatus(USART2, USART_IT_PE) != RESET) { uint16_t tmp = USART2->SR; tmp = USART2->DR; // 可以在这里统计校验错误次数,便于调试 // err_cnt++; } if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { uint8_t data = (uint8_t)(USART2->DR & 0xFF); // 将接收到的数据放入自定义缓冲区,或者直接处理 UART2_RxBuffer[UART2_RxCnt++] = data; if (UART2_RxCnt >= UART2_RxBufferSize) { UART2_RxCnt = 0; } } }这里有个中断优先级的细节:最好把校验错误判断放在RXNE判断之前。因为当一个字节同时出现校验错误和数据,如果先去读DR,可能会提前清掉一些标志,导致后续对PE的处理逻辑错乱。实测下来,先读SR再读DR这种标准操作,配合先判PE后判RXNE,工作最稳定。
3.3 与串口调试助手的联调要点
调试带校验的串口,很多人第一反应是打开串口助手,设置好波特率,选“偶校验”,然后发数据。但有一个细节容易被忽略:串口助手里“数据位”一栏,要选择8位数据位。注意,这不是STM32寄存器里的8位字长模式,而是说8个有效数据位。板上按“8E1”协议交互,助手这边就选8数据位、偶校验、1停止位。
我推荐一个稳妥的联调步骤:
- 先用无校验模式确认收发通路正常,排除接线和波特率问题。
- 改成偶校验后,先用串口助手给板子发送AA、55、01、F0这一组特征明显的字节,观察接收中断拿到的数据是否原样一致。
- 用逻辑分析仪或示波器抓PA2的波形,解析UART协议,看帧结构里是否有校验位,校验位与理论值是否一致。
如果发现接收端偶发丢包或数据偶尔错乱,不要急着改代码,先看波特率误差是否超标,再看校验位是否正确。尤其在使用USB转TTL芯片(比如CH340、FT232)时,确认为PC串口端和单片机端的校验配置一致,否则会出现明明代码没问题、调试助手端却接收不到有效数据的现象。
4. 实际调试中的问题排查与经验
4.1 常见异常现象与分析
在配置USART2带奇偶校验的过程中,我遇到过好几类典型问题,下面按现象分类说明。
第一类:数据错乱但都能收到。表现为一步步发0x01、0x02、0x03,那边显示0x00、0x01、0x01,甚至根本看不出规律。这种绝大多数是数据位模式配置错误,即USART_WordLength用了8b,导致有效数据只有7位,高位被吃掉或位移。解决方法是改成USART_WordLength_9b,保持8位有效数据。
第二类:能收到但全部是0xFF或者FF 00的规律跳变。多半是奇偶校验设置不一致,比如单片机开偶校验,PC端开无校验,或者反之。也有的是因为PA3引脚没有正确配置为浮空输入,造成RX电平被拉偏。建议先用万用表量一下TX/RX引脚电平,确认接线没问题。
第三类:偶发校验错误。表现为接收中断里的PE标志偶尔被置位,或者业务层偶尔丢帧。这种情况优先排查波特率误差。USART2时钟源来自PCLK1(36MHz),如果系统初始化时时钟树没配好,或者外部晶振频率不准,波特率误差会放大。在115200波特率下可能还不明显,在921600这种高波特率下就会频繁报错。建议先用PCLK1 = 36MHz这个标准配置,用示波器测实际波形占空比。
第四类:同一个数据发两次,一次成功一次失败。这种问题往往出现在中断服务函数没有正确清理错误标志。在标准库中,PE标志必须通过“读SR寄存器+读DR寄存器”这个组合操作来清除。如果漏了这步,错误标志会一直挂在那里,导致后续每次中断都先进入PE分支,把有效数据覆盖掉或丢掉。
4.2 问题排查速查表
为了方便现场调试,我把常见问题整理成一张速查表。
| 现象 | 可能原因 | 排查与处理 |
|---|---|---|
| 数据全乱,无规律 | WordLength设成8b且开校验 | 改用USART_WordLength_9b |
| 接收数据固定少一位或位移 | 校验位被当作数据处理 | 读取DR时用& 0xFF截取低位 |
| 偶发PE错误 | 波特率误差偏大 | 用示波器测波形,校准时钟树 |
| 频繁PE错误 | PC端或设备端校验配置不一致 | 统一校验模式,建议PC端选“偶校验” |
| 串口助手显示乱码 | 调试助手数据位、停止位配置不对 | 配置为8数据位、1停止位、与板端一致的校验位 |
| 发送正常,接收超时 | PA3没配置成输入模式 | 检查GPIO_Mode,改为GPIO_Mode_IN_FLOATING |
| 帧结构里没有校验位 | 校验未真正使能或误用8位字长 | 检查PCE位和M位,确认配置顺序 |
这张表是我每次遇到串口异常时必看的第一份资料,很多时候发现问题并不在代码,而在于配置层面某个不起眼的参数。
4.3 从基础串口到Modbus RTU的实际场景
文章开头提到我最初是在做串口通信对接时用到奇偶校验的。后来在移植freemodbus v1.6到STM32F103时,Modbus RTU的串口帧格式也就是我们前面讲的标准配置:8数据位、偶校验、1停止位。这一步在STM32侧对应的库函数配置,就是USART_InitStructure里把USART_WordLength设为9b、USART_Parity设为Even、USART_StopBits设为1。
当时我做波特率自适应时,接收侧需要响应对手的波特率变化,于是把USART2初始化函数提炼成了一个可随时改参的接口。实测下来,用9600和19200两种波特率跑一整天,偶校验误码率为0。但这里有个隐性开销:一旦开启了校验错误中断,CPU会被错误标志频繁打扰。在干扰严重的RS485总线上,如果总线上存在多设备竞争,错误中断次数会明显上升。我最终的方案是:使能校验错误中断做统计,但不在中断里做复杂处理,只记录错误计数,交给主循环判断是否需要复位通信状态机。
从这个案例可以看出来,奇偶校验对STM32来说是一个硬件上很成熟的功能,但它不能解决所有通信问题。串口通信要稳定,最终要靠物理层电平、波特率精度、协议层校验和及时的错误处理共同保证。奇偶校验只是第一道防线,大部分业务协议都会在数据链路层再叠加CRC校验,这也是Modbus RTU至今仍被广泛应用的原因之一。
5. 写在最后的几条实操心得
代码调通之后,再分享几个我试过之后觉得特别实用的小经验。
第一,调试校验位之前,先做“自收发测试”。把PA2和PA3短接,然后在代码里周期性发送预设字节序列。如果能在接收FIFO里原样收到,说明单板收发通路和校验逻辑都没问题。这个测试步骤能在排除外部线缆、对端设备的情况下,迅速把问题定位在板内还是板外。
第二,如果想观察校验位是否正确,可以用普通逻辑分析仪抓串口波形,然后用软件解析UART协议。软件会直接把帧格式解析成“起始位、数据位、校验位、停止位”,你甚至能手动算一遍校验位,验证硬件是否真的按照奇偶规则填充。我之前在偶校验模式下发送0x01,抓到的波形里校验位是1,因为0x01本身只有一个1,要凑成偶数个1,校验位就得是1。这个例子很直观。
第三,中断服务函数要精简。特别是校验错误处理,不要在此处做浮点计算、打印、延时这些操作。ST的库函数在错误中断里必须要读SR和DR才能清标志,这已经是一次总线访问了,如果再加很多业务逻辑,中断延迟会变大,高波特率下容易产生新的溢出错误。
第四,如果是做工业设备通信,建议保留一个“无校验”功能位。产品现场调试时,对端设备可能不支持校验,或者默认设置就是无校验。我在做固件时通常会留一个配置参数,允许用户在无校验、偶校验、奇校验之间切换,这样现场调试时能少跑很多冤枉路。
第五,也是最重要的一条经验:奇偶校验永远是协议层的一部分,不要只改STM32这一端的配置,必须确认通信双方对“有效数据位数”的理解完全一致。否则哪怕两端都叫“偶校验”,只要一个认为应该带9位字长,另一个认为是8位字长,出来的数据就会错位。
整个项目折腾下来,我对串口通信和奇偶校验的理解深入了不少。以前觉得这就是一个库函数参数,现在知道了它背后是帧结构设计、时钟精度、中断标志管理和协议联调的综合工程。希望这篇基于USART2奇偶校验的总结,能帮你少走一些我踩过的弯路。
本文还有配套的精品资源,点击获取