☰
STM32F103串口2奇偶校验配置与避坑指南
2026/10/17 17:01:04 网站建设 项目流程

简介:这份资源面向STM32嵌入式开发初学者与需要调试串口通信的工程师,聚焦STM32F103C8T6上USART2带奇偶校验的完整实现。内容涵盖时钟使能、GPIO复用配置、USART控制寄存器中PCE与PS位的奇偶校验设置、中断与DMA可选方案,以及基于标准外设库的初始化流程,可帮助读者理解串口通信底层寄存器操作与工程组织方式。压缩包共78个文件,以34个h头文件与33个c源文件为主,另含Keil工程文件、启动汇编、hex目标文件及编译批处理脚本,整体约308KB,目录按HARDWARE、SYSTEM、CORE、STM32F10x_FWLib、USER等模块划分,结构清晰便于对照学习。目前已有2445人学习下载,适合作为串口校验配置的参考范例与工程模板。

1. 串口通信里的“隐形守门员”:为什么这个 STM32F103 串口2奇偶校验项目值得拆一遍

如果你调过 STM32 的串口,大概率经历过这种场景:PC 端发来一串数据,MCU 收到的字节偶尔错一位,程序照单全收,后面解析全乱套。多数教程只讲波特率、数据位、停止位,唯独把奇偶校验一笔带过。可一旦你对接的是电表、工控板、老式传感器这类设备,对方协议里白纸黑字写着“偶校验、8 数据位、1 停止位”,你不开校验,通信就是玄学——时好时坏,抓包也看不出所以然。这份 STM32F103 串口2带奇偶校验的工程,解决的正是这个被很多人跳过的环节:它把 USART2 配置成带奇偶校验的收发通道,让你能直接对着协议要求把校验位打开,而不是靠软件层自己算。适合正在做多串口项目、需要和外部设备硬碰硬对接的嵌入式开发者,尤其是那些已经跑通串口1、准备把第二个串口也用起来的同学。

2. 工程结构与串口2初始化:从时钟到 GPIO 的完整配置链路

2.1 目录里都有什么,先看骨架再动手

拿到一份 STM32F103 的串口工程,别急着编译。先扫一眼目录,判断它是标准库还是 HAL 库,这决定了你后面改代码的方式。这个项目走的是标准外设库路线,结构大致如下:

Project/ ├── CMSIS/ # 内核支持文件,启动汇编在这里 ├── STM32F10x_StdPeriph_Driver/ │ ├── inc/ # 标准库头文件 │ └── src/ # 标准库源文件,usart.c 就在这 ├── User/ │ ├── main.c # 主流程,初始化 + 收发测试 │ ├── stm32f10x_it.c # 中断服务函数,串口中断入口 │ └── usart2.c / usart2.h # 串口2的配置与收发封装 └── Project.uvprojx # Keil 工程文件

关键点在于usart2.c和stm32f10x_it.c这两个文件。前者负责把 USART2 的寄存器配好,后者决定收到数据后怎么处理。很多人移植失败,不是配置写错,而是中断函数名对不上——标准库里 USART2 的中断入口固定是USART2_IRQHandler,你写成别的名字,编译不报错,但中断永远不进,程序就卡在死等里。

2.2 时钟、GPIO 与 USART2 的初始化顺序

STM32F103 的 USART2 挂在 APB1 总线上,这点和 USART1 挂在 APB2 上不同,初学者最容易在这里翻车。初始化必须按“先开时钟、再配引脚、最后配串口”的顺序来,顺序错了寄存器写不进去。

void USART2_Init(u32 bound) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 1. 开时钟:USART2 在 APB1,GPIOA 在 APB2 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // 2. 配置 TX(PA2) 为复用推挽输出 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); // 3. 配置 RX(PA3) 为浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // 4. 串口参数:重点在 Parity 这一行 USART_InitStructure.USART_BaudRate = bound; USART_InitStructure.USART_WordLength = USART_WordLength_8b; 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); // 5. 开接收中断并启动串口 USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); USART_Cmd(USART2, ENABLE); }

逐段说明:第 1 步的时钟使能是整段代码的地基,APB1 和 APB2 分属两条总线,漏掉任何一个,后面所有配置都是往空气里写。第 2、3 步的引脚模式不能互换,TX 必须是复用推挽,RX 必须是浮空或上拉输入,配反了表现为“能发不能收”或“能收不能发”。第 4 步是这份工程的核心,USART_Parity_Even把校验位打开,此时数据帧变成“1 起始位 + 8 数据位 + 1 校验位 + 1 停止位”,共 11 位。这里有个隐藏坑:开了校验后,USART_WordLength_8b指的是数据位仍是 8 位,校验位是额外附加的,不要误以为要改成 9 位。第 5 步的USART_IT_RXNE是接收非空中断,每收到一个完整帧就触发一次。

2.3 波特率与校验位的关系,别只改一个

波特率由bound参数传入,常见值 9600、115200。开了奇偶校验后,每帧多了一位,实际有效数据吞吐会略降,但波特率本身不用改——波特率描述的是位传输速率,不是帧速率。真正要同步改的是 PC 端串口助手:数据位选 8、校验选 Even、停止位选 1,三处必须和 MCU 完全一致。我见过太多人只改了 MCU 的校验位,PC 端还停在 None,结果收到的全是乱码,然后回头怀疑代码。校验位不匹配时,STM32 会在状态寄存器里置 PE 标志,如果你没读它,错误就悄悄溜过去了。

3. 收发逻辑与中断处理:校验位打开后数据怎么进出

3.1 发送一个字节:等标志位再写数据

发送逻辑比接收简单,但也不能直接往 DR 寄存器里怼。必须等 TXE(发送数据寄存器空)标志置位,才能写下一个字节,否则会覆盖上一个还没发完的数据。

void USART2_SendByte(u8 data) { // 等待发送数据寄存器空 while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) == RESET); // 写入数据寄存器,硬件自动附加校验位 USART_SendData(USART2, data); } void USART2_SendString(u8 *buf, u16 len) { u16 i; for (i = 0; i < len; i++) { USART2_SendByte(buf[i]); } // 等最后一个字节真正发完 while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); }

USART_FLAG_TXE表示数据寄存器已空,可以接受新数据;USART_FLAG_TC表示整个帧(含校验位和停止位)已经移位完成。发字符串时循环里等 TXE,循环外等 TC,这个搭配能保证最后一个字节不会因为函数返回而被截断。校验位是硬件自动计算的,你写进 DR 的始终是 8 位有效数据,不需要手动算奇偶。

3.2 接收中断:读 DR 之前先看错误标志

接收端是这份工程最需要小心的地方。开了校验后,如果传输过程中校验失败,硬件会置位 PE(Parity Error)。标准库的USART_GetITStatus只判断 RXNE,不会自动帮你清 PE,所以正确做法是先读 SR 再读 DR,用这个动作序列把错误标志一起清掉。

void USART2_IRQHandler(void) { u8 recv; if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { // 先读状态寄存器,再读数据寄存器,可清除 PE/FE/NE 等错误标志 recv = USART_ReceiveData(USART2); // 这里可以把 recv 存入环形缓冲区,交给主循环处理 RingBuffer_Put(&uart2_rx_buf, recv); } }

USART_ReceiveData内部就是读 DR 寄存器,而读 DR 之前硬件要求先读 SR,标准库函数已经帮你做了这个动作,所以直接调用即可。但如果你用的是寄存器操作,务必手动(void)USART2->SR; (void)USART2->DR;这个序列。中断里只做入队,不做解析,这是嵌入式收发的通用习惯——中断要短,解析放主循环,否则高波特率下容易丢字节。

3.3 主循环里的数据解析与回环测试

主循环负责从环形缓冲区取数据、判断帧头帧尾、执行命令。一个最小可用的回环测试如下:

int main(void) { u8 data; USART2_Init(9600); // 9600, 8, Even, 1 RingBuffer_Init(&uart2_rx_buf); while (1) { if (RingBuffer_Get(&uart2_rx_buf, &data)) { // 收到什么原样发回,用于验证校验链路 USART2_SendByte(data); } } }

把这段烧进去,PC 端串口助手设成 9600、8、Even、1,发一个字符就应该收到同一个字符。如果收到的是乱码,先查 PC 端校验设置;如果一个都收不到,查中断函数名和 NVIC 配置。回环通了,说明校验链路完整,再往上叠协议解析就稳了。

4. 避坑与排查:串口2开校验后最容易翻车的五个点

4.1 现象:PC 端能发,MCU 收不到任何数据

原因通常是中断没进。先确认stm32f10x_it.c里写的是USART2_IRQHandler,不是USART1_IRQHandler;再确认 NVIC 里使能了USART2_IRQn通道,并且优先级分组配置正确。标准库里 NVIC 配置容易被漏掉,因为编译不会报错,只是中断静默失效。解决方式是在初始化末尾补上NVIC_Init,通道号选USART2_IRQn。

4.2 现象:收到的数据整体偏移或全是 0xFF

这多半是波特率不匹配或校验位不一致。检查 PC 端和 MCU 端的波特率是否都是 9600,校验位是否都是 Even。特别注意:有些串口助手把“校验位”藏在高级设置里,默认是 None,你不手动改,它就一直按无校验发,MCU 按偶校验收,每个字节的校验位都对不上,硬件直接丢弃或置错。解决方式是两端逐项核对:波特率、数据位、校验、停止位,四项全对齐。

4.3 现象:偶发丢字节,发得快就乱

原因是中断里做了耗时操作,或者环形缓冲区太小。中断服务函数里只应该读 DR 和入队,任何printf、延时、复杂判断都会拖慢响应。9600 波特率下每字节约 1ms,看似宽裕,但如果你在中断里调用了阻塞函数,下一个字节到来时 RXNE 还没被读走,就会触发溢出错误 ORE。解决方式是把解析逻辑全部挪到主循环,中断只负责搬运,缓冲区至少开到 64 字节。

4.4 现象:开了校验后,某些字节总是收错

先怀疑硬件连线。TX/RX 没有交叉、地线没共、线太长没屏蔽,都会让校验位在传输中翻转。校验位的作用是检错,不是纠错,它发现错误后硬件置 PE 标志,但数据本身已经错了。如果你的应用对可靠性要求高,应该在软件层再加一层 CRC 或帧校验,不能只依赖串口的奇偶校验。解决方式是缩短杜邦线、共地、降低波特率测试,确认是电气问题还是配置问题。

4.5 现象:换到串口1就正常,串口2死活不通

这是最典型的“总线归属”坑。USART1 在 APB2,USART2/3 在 APB1,时钟使能函数不同。很多人复制串口1的代码改串口2,只改了USART1到USART2,却忘了把RCC_APB2PeriphClockCmd改成RCC_APB1PeriphClockCmd。时钟没开,USART2 的寄存器全是复位值,配置写进去也不生效。解决方式是逐行核对时钟使能那一句,APB1 和 APB2 不能混。

5. 二次开发改造:把校验串口接进你自己的协议栈

5.1 从回环测试到协议解析的改造路径

回环跑通只是起点。真实项目里,你收到的不再是单个字符,而是带帧头、长度、命令字、校验和的完整数据包。改造思路是保留USART2_Init和中断入队逻辑不动,把主循环里的“原样发回”替换成状态机解析。常见做法是定义一个帧结构体,用状态机逐字节推进:等帧头、收长度、收数据、验校验和、执行命令。这样串口2的校验能力就成了你协议栈的物理层保障,上层不用再关心位错误。

typedef struct { u8 head; // 帧头,比如 0xAA u8 cmd; // 命令字 u8 len; // 数据长度 u8 data[32]; // 载荷 u8 checksum; // 累加和或 CRC } Uart2Frame; // 状态机解析示例(在主循环中调用) void Uart2_ParseByte(u8 byte) { static u8 state = 0, idx = 0; static Uart2Frame frame; switch (state) { case 0: // 等帧头 if (byte == 0xAA) { frame.head = byte; state = 1; } break; case 1: // 收命令字 frame.cmd = byte; state = 2; break; case 2: // 收长度 frame.len = byte; idx = 0; state = (byte > 0) ? 3 : 4; break; case 3: // 收载荷 frame.data[idx++] = byte; if (idx >= frame.len) state = 4; break; case 4: // 收校验和并校验 frame.checksum = byte; if (Uart2_CheckSum(&frame)) { Uart2_Execute(&frame); } state = 0; break; default: state = 0; break; } }

这段状态机的关键在于:它把“物理层校验”和“协议层校验”分开了。串口的奇偶校验负责发现位级错误,状态机里的 checksum 负责发现帧级错误,两层叠加,可靠性比单开一层高得多。改造时注意data数组大小要按你实际协议的最大载荷来定,别照抄 32,否则长帧会溢出。

5.2 参数怎么改:波特率、校验模式与缓冲区

如果你对接的设备要求的是奇校验而不是偶校验,只改一行:USART_InitStructure.USART_Parity = USART_Parity_Odd;。如果要改成无校验,设为USART_Parity_No,同时 PC 端也要同步改。波特率改USART2_Init(115200)即可,但高波特率下建议把中断优先级调高、缓冲区加大,否则容易丢包。环形缓冲区的大小我一般按“最大帧长 × 4”来定,给突发流量留余量。

参数常见取值改动位置注意事项
波特率9600 / 115200USART2_Init入参两端必须一致
校验模式Even / Odd / NoUSART_Parity改完同步 PC 端
数据位8bUSART_WordLength开校验后仍填 8b
停止位1USART_StopBits老设备可能要求 2
缓冲区64 / 128 字节RingBuffer定义按最大帧长放大

5.3 验证方法:用示波器和串口助手交叉确认

改完代码别只信串口助手。我习惯先用示波器抓 TX 引脚波形,数一下每帧是不是 11 位(1 起始 + 8 数据 + 1 校验 + 1 停止),校验位的高低电平是否符合偶校验规则。波形对了,再开串口助手看数据。如果波形对但助手收错,问题在 PC 端设置;如果波形就不对,问题在 MCU 配置。这个交叉验证能帮你快速定位是硬件层还是软件层的问题,比反复烧录快得多。

从那以后我每次开新串口,都强制走一遍“时钟归属核对 → 引脚模式核对 → 两端参数核对 → 回环测试 → 波形确认”的流程,少一步都可能在后面对接设备时翻车。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询