简介面向STM32F103C8T6开发者的串口通信实践资料聚焦USART2带奇偶校验的配置与验证适合已掌握C语言和GPIO基础、希望打通时钟、引脚复用、USART寄存器及中断/DMA通信链路的嵌入式学习者。压缩包共78个文件以C源文件和H头文件为主另有Keil工程、启动汇编、HEX目标文件及少量说明与配置文件整体约308KB工程结构涵盖硬件驱动、系统延时、内核启动、标准外设库和用户代码。已有2446人学习下载说明该案例在串口校验调试场景中具有一定参考价值。读者可据此对照偶校验与奇校验的使能位设置理解波特率、数据位、停止位和校验位如何协同并借助现成工程快速编译验证同时能从完整目录组织中学习外设库工程的拆分方式、串口收发接口封装和调试排错思路。1. 奇偶校验不是“多一个标志位”开了 8E1字长就必须改 9 位在 STM32 单片机上调制串口奇偶校验大概是第一个让人怀疑人生的配置项明明把校验使能位置起来了对端就是个普通串口助手结果收到的数据却一半对一半错。问题往往不在校验本身而在一条反直觉的约束——STM32F103 的 USART 里校验位是算在字长里的。想要 8 位数据加 1 位校验字长就必须写成 9 位写 8 位等于只发了 7 位数据。这颗芯片的串口 2 还挂在 APB1 上最高 36 MHz波特率分频的小数位只有 4 位误差、三取二采样、标志清零时序每一环都能把你按在地上摩擦。把这条链路从帧结构一直挖到寄存器校验错就不再是玄学。2. 帧结构拆解8E1 一帧 11 位校验位到底插在哪一位异步串口没有时钟线收发双方全靠起始位的下降沿对齐再按约定的波特率逐位采样。这个“约定”里包含三件事一帧有多少个位、每个位代表什么、什么时候采样。奇偶校验改的是第二件事而它会连带改掉第一件事——这就是所有坑的源头。2.1 起始位、数据位、校验位、停止位的时间轴一帧 8E18 数据位、偶校验、1 停止位在线上是这样排的空闲时线路保持高电平一个低电平的起始位拉开序幕接着是 8 个数据位然后 1 个校验位最后 1 个高电平的停止位一共 11 个位时间。数据位是低位先行先发 bit0 再发 bit7这一点和 SPI 的习惯相反用逻辑分析仪抓波形时经常看反。以 115200 波特率为例每一位占 8.68 微秒一帧 11 位大约 95.5 微秒折算下来每秒最多 10416 帧。到了 9600 波特率一位就是 104 微秒一帧超过 1.1 毫秒——这个量级决定了你在中断里做多少事情是安全的也决定了后面要讲的 DMA 到底该不该上。接收侧不是“在位中间采一次”这么粗糙。过采样 16 模式下硬件用 16 倍波特率的时钟对一个位采样 16 次取第 8、9、10 次的多数结果作为这一位的值这样即使发送方和接收方的波特率有小幅偏差落点也能稳在位的中间区域。校验位同样走这套采样逻辑所以校验错的成因通常不是“采错一位”而是真的有一位翻转了或者字长配错了。2.2 CR1 的 M 位与 PCE/PS为什么字长把校验位算在内打开参考手册的 USART_CR1 寄存器和这件事相关的是四个位位名称取值含义bit 13UE0 关闭 USART1 使能bit 12M0 表示 8 位字长1 表示 9 位字长bit 10PCE0 无校验1 使能校验bit 9PS0 偶校验1 奇校验PCE1 时有效关键在于 M 位描述的是“数据寄存器里有效位的个数”而不是“业务数据位的个数”。当 PCE1 时最高位被硬件征用为校验位剩下 8 位才是数据。所以正确的组合永远是 M1 且 PCE1如果你保持 M0 又打开 PCE硬件会把 bit7 当作校验位用掉实际只有 7 位数据参与传输。发送方向还有个容易搞混的点硬件会根据低 8 位自动算出校验值并填进第 9 位你往 DR 里写进去的 bit8 会被覆盖掉。常见的实现思路是软件只关心(uint8_t)那部分第 9 位留空即可不必自己算校验和——自己算反而可能和对端的期望对不上。注意M1 时帧长变成 11 位如果对端还按 8N18 数据位、无校验、1 停止位解析它会把你发的校验位当成停止位把停止位当成空闲收到的每个字节都“看起来差一点点”但又不是全错。2.3 接收侧 PE 标志的置位条件与清零时序接收方向硬件把 8 位数据 1 位校验收齐后做比对不一致就在状态寄存器 SR 的 bit0 置 PE同时把数据放进 DR 并置 RXNE。注意这两个标志是同时出现的数据是好的至少是被收到了只是校验没过所以软件有得选——丢弃或者打上标记继续用。清 PE 的方式是固定动作先读 SR再读 DR。这一对读操作会顺带清掉 RXNE、ORE、FE、NE、IDLE。顺序不能反也不能只读一个/* 正确一次读 SR 拿到全部状态再读 DR 把标志清干净 */ uint32_t sr USART2-SR; uint16_t dr USART2-DR; if (sr USART_SR_PE) { /* 校验错按业务决定丢弃还是标记 */ }如果只读 DR 不先读 SR标志清不掉中断会一直重入CPU 卡在中断里出不来。如果读了 SR 却不读 DRPE 和 RXNE 都还挂着下一帧进来时 RXNE 会变成 ORE问题从“校验错”升级成“溢出丢数据”排起来更费劲。还有一点PEIE 这个中断使能位是独立于 RXNEIE 的。PEIE0 时 PE 照样会置位只是不触发中断。很多工程在调试阶段只开了 RXNEIE结果现场干扰导致的校验错全部被静默吞掉直到协议层发现数据对不上才回头查这时候已经很难定位了。3. 波特率与时钟USART2 挂在 APB1 上36 MHz 怎么把 BRR 算准USART1 挂 APB2最高 72 MHzUSART2 到 UART5 挂 APB1最高 36 MHz。这个差异在配波特率时必须意识到同样写 115200两条总线的分频值差了整整一倍拿 USART1 的初始化代码直接改成 USART2 用最常见的结果就是波特率差一倍能收到但全是乱码。3.1 USARTDIV 的定点编码尾数 12 位、小数 4 位波特率发生器的核心是一个分频系数 USARTDIV关系式是USARTDIV fCK / (16 × baud) // 过采样 16OVER8 0 USARTDIV fCK / (8 × baud) // 过采样 8 OVER8 1这个 USARTDIV 不是浮点数而是被编码进 BRR 寄存器高 12 位bit15:4是整数部分低 4 位bit3:0是小数部分小数精度只有 1/16 0.0625。也就是说 BRR 的值等于USARTDIV × 16反算时把 BRR 除以 16 就还原了。36 MHz 下算 115200USARTDIV 36000000 / (16 × 115200) 19.53125 整数部分 19 → 0x13小数 0.53125 × 16 8.5 → 取 8 BRR (19 4) | 8 0x138 实际分频 312 / 16 19.5 实际波特率 36000000 / (16 × 19.5) 115384.6 误差 0.16%0.16% 完全在容忍范围内一般认为单帧累计误差控制在 2% 以内比较安全。9600 更巧36000000 / (16 × 9600) 234.375小数 0.375 × 16 6正好整除误差为 0。3.2 过采样 16 与 8 的取舍误差容忍度差在哪过采样 8 是为高波特率准备的分频系数变小小数位只剩 3 位1/8 精度但同样的时钟能跑出翻倍的波特率。代价是每位只采 3 次样抗噪能力和时钟偏差容忍度都下降。36 MHz 下跑 115200过采样 16 的分频比是 19.5过采样 8 是 9.75后者的小数精度只有 1/8实际落点是 9.75 刚好能表示属于运气好换个波特率就未必了。实操上的判断标准很简单APB1 只有 36 MHz除非要上到 1 Mbps 以上否则一律用过采样 16。高波特率加上长线缆、加上光电隔离误码率会肉眼可见地上升这时候省下来的那点采样余量完全不值得。3.3 一段整数运算算出 BRR 并验证误差初始化代码里不建议用浮点算 BRR。不同编译器、不同优化等级下 float 的舍入行为可能有细微差异而且 Cortex-M3 没有硬件 FPU浮点运算会引入软浮点库。把式子放大 100 倍做整数运算就能拿到同样的精度/* pclk1: USART2 所在 APB1 的时钟单位 Hzbaud: 目标波特率 * 返回写入 USART2-BRR 的值过采样 16小数 4 位 */ uint16_t brr_calc(uint32_t pclk1, uint32_t baud) { /* USARTDIV × 100 pclk1 × 100 / (16 × baud) pclk1 × 25 / (4 × baud) * 36 MHz 时 pclk1 × 25 9e8uint32_t 装得下不会溢出 */ uint32_t div100 pclk1 * 25U / (4U * baud); uint32_t mant div100 / 100U; /* 整数部分 → BRR[15:4] */ uint32_t frac ((div100 % 100U) * 16U 50U) / 100U; /* 小数 → BRR[3:0]50 是四舍五入 */ if (frac 16U) { mant 1U; frac 0U; } /* 小数进位别让它溢出到 0x10 */ return (uint16_t)((mant 4) | (frac 0x0FU)); }div100是 USARTDIV 放大 100 倍后的整数mant取整数部分frac把余数映射到 1/16 的刻度上。50是做四舍五入少了它会系统性向下取整长距离通信时误差会一点点累积。最后的frac 16判断是防溢出比如余数是 99(99×1650)/100 16不处理就会让低 4 位变成 0 而整数部分少加 1波特率直接错一档。验证方法很直接把 BRR 代回去算实际波特率和目标的偏差控制在 1% 以内就算过。36 MHz 下常见波特率的实测偏差大致是9600 为 0%19200 约 0.16%115200 约 0.16%921600 约 0.16%。如果要跑 921600 且时钟只有 36 MHz建议把过采样切到 8 再算一遍误差会小一些。4. 寄存器级落地从 GPIO 复用推挽到 DR 读写的完整链路把前面三章的结论拼起来一个能用的 8E1 串口 2 初始化其实只有几十行。用标准外设库写一遍再用寄存器写一遍对照着看更容易理解每一位在干什么。4.1 两个总线的时钟GPIOA 在 APB2USART2 在 APB1这是最容易漏的一步也是最难查的一步。GPIOA 的外设时钟挂在 APB2 上USART2 挂在 APB1 上两个都要开顺序无所谓但少一个都不行。少开 GPIOATX 引脚没有输出能力示波器上是死线少开 USART2所有 USART 寄存器读回全 0你写的每一位都像掉进了黑洞。引脚配置上PA2 是 TX必须配成复用推挽输出GPIO_Mode_AF_PP配成普通推挽虽然也能看到波形但那是软件在翻转电平和 USART 外设无关PA3 是 RX配成浮空输入或上拉输入都行长线缆、可能悬空的场合建议上拉否则浮空引脚会随机收到噪声PE 标志频繁置位看着像“对端一直在发错数据”。4.2 CR1 位拼装与初始化顺序void usart2_init_8e1(uint32_t baud) { GPIO_InitTypeDef gpio; USART_InitTypeDef usart; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* GPIOA → APB2 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); /* USART2 → APB1 */ gpio.GPIO_Pin GPIO_Pin_2; /* PA2 USART2_TX */ gpio.GPIO_Mode GPIO_Mode_AF_PP; /* 复用推挽必须 */ gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); gpio.GPIO_Pin GPIO_Pin_3; /* PA3 USART2_RX */ gpio.GPIO_Mode GPIO_Mode_IPU; /* 上拉输入防悬空噪声 */ GPIO_Init(GPIOA, gpio); usart.USART_BaudRate baud; usart.USART_WordLength USART_WordLength_9b; /* 8 数据 1 校验 */ usart.USART_StopBits USART_StopBits_1; usart.USART_Parity USART_Parity_Even; /* 偶校验 */ usart.USART_Mode USART_Mode_Rx | USART_Mode_Tx; usart.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_Init(USART2, usart); /* 内部会读 RCC-CFGR 算 PCLK1 */ USART_Cmd(USART2, ENABLE); }USART_WordLength_9b这一行就是第 2 章结论的落地。库函数内部会根据RCC_GetClocksFreq()读到的 PCLK1 反算 BRR 并写进去所以即使你之后改了系统时钟或 APB1 分频只要重新调一次USART_Init波特率就是对的——这比自己写死 BRR 常量安全得多。如果不想依赖库直接拼 CR1 也一样只是每一项的含义都在你手上static void usart2_init_reg(uint32_t pclk1, uint32_t baud) { USART2-BRR brr_calc(pclk1, baud); /* 见 3.3 节 */ USART2-CR1 (1U 13) /* UE 1使能 USART */ | (1U 12) /* M 19 位字长含校验位 */ | (1U 10) /* PCE 1校验使能 */ | (0U 9) /* PS 0偶校验 */ | (1U 8) /* PEIE 1校验错也进中断 */ | (1U 5) /* RXNEIE 1收到数据进中断 */ | (1U 3) /* TE 1发送使能 */ | (1U 2); /* RE 1接收使能 */ }PS位选奇偶要和协议约定一致。Modbus RTU 常见的 8E1 是偶校验某些电力规约用 8O1 奇校验配反了就是 100% 校验错而且错得非常“整齐”——每个字节都错这种规律性反而好查。4.3 发送与接收的边界写 DR 只写低 8 位读 DR 要收 9 位发送侧简单等 TXE 置位往 DR 写一个字节硬件会把第 9 位填成校验值。连续发送时要等 TC 而不是 TXE原因在第 5 章展开。接收侧要小心因为 DR 是 16 位宽的有效寄存器第 9 位是校验位取数据必须屏蔽void USART2_IRQHandler(void) { uint32_t sr USART2-SR; /* 先读 SR把状态一次性拿到 */ uint16_t dr USART2-DR; /* 再读 DR清 PE/RXNE/ORE/FE/NE */ if (sr USART_SR_PE) { parity_err_cnt; /* 校验错数据已取走按业务丢弃 */ return; } if (sr USART_SR_RXNE) { ring_push(rx_ring, (uint8_t)(dr 0x00FFU)); /* 只留低 8 位 */ } if (sr USART_SR_ORE) { /* 溢出上面那次读 SR 读 DR 已经清掉了这里只统计 */ overrun_cnt; } }注意sr和dr的读取顺序先 SR 后 DR 是清零序列的要求反过来就清不掉。dr声明成 16 位并按位与 0xFF是因为第 9 位确实是校验位直接转成uint8_t虽然结果一样但代码意图不清晰后面接手的人容易误以为这里没有校验位。5. 排查现场奇偶校验下最容易翻车的五个地方5.1 字长仍设 8 位0x00–0x7F 正常其余全乱现象发英文、数字、换行都没问题一发中文或十六进制数据就满屏乱码对端串口助手如果显示校验错会看到错误率接近 100%但只在 data ≥ 0x80 时出现。原因USART_WordLength_8b加USART_Parity_Even的组合下硬件把 bit7 当校验位用掉实际只有 7 位数据参与传输。所有小于 0x80 的字节最高位本来就是 0截掉最高位再加校验恰好和预期一致所以“看起来是好的”一旦最高位是 1这个 1 被当成校验位覆盖数据就变了。解决把字长改成 9 位这是 8E1 唯一的正确写法。如果是从别的工程复制来的代码重点检查这一行它经常被“顺手”改回 8 位因为 8 位看起来更符合“8 位数据”的直觉。5.2 只开了 GPIOA 时钟寄存器读回 0示波器没波形现象程序不报错、不 HardFault就是发不出去也收不到用调试器看USART2-CR1写进去的 0x2C6C 读回来还是 0TX 引脚一直是高电平一个跳变都没有。原因RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE)漏了。APB1 外设时钟默认是关的关着的时候寄存器写入被静默丢弃读回永远是 0不会触发总线错误。这个特性让“时钟没开”变成最隐蔽的故障之一。解决把两个总线的时钟使能写在初始化的最前面并且养成习惯——凡是外设行为异常第一件事就是用调试器读它的 CR1 或类似的控制寄存器看是不是 0。是 0 就先查时钟树别往下猜。5.3 用 TXE 判断发送完成去切 485 方向校验位被截断现象RS485 半双工总线上单字节收发正常连续发多字节时最后一两个字节对端收不到或者收到但校验错减小波特率后现象减轻但不消失。原因TXE 置位表示“数据寄存器空了可以写下一个字节”但移位寄存器里可能还有一位甚至一帧没发完。485 收发器靠 DE 引脚切换方向你在 TXE 时就拉低 DE正在移出的那一帧被硬生生截断——最后被砍掉的往往就是校验位和停止位。解决切方向前必须等 TC 置位它表示“移位寄存器也空了最后一个位的停止位已经完整送出”。写法上把while (USART_GetFlagStatus(USART2, USART_FLAG_TC) RESET);放在最后一字节写完之后。加了校验位以后帧变长了这个坑的暴露概率比 8N1 时更高因为多出来的那一位正好在最后。5.4 中断里读 DR 与读 SR 的顺序颠倒标志清不掉反复进中断现象程序一进中断就出不来主循环完全饿死或者中断能退出但parity_err_cnt疯涨实际线路上一帧都没发。原因PE、RXNE、ORE 这些标志的清零依赖“读 SR 后读 DR”这个固定序列。如果先读 DR 再读 SR或者两次都读 DR清零条件不成立标志一直挂着中断线一直有效CPU 就在中断入口和函数体之间反复横跳。解决把sr SR; dr DR;作为中断服务函数的第一、第二条语句之后所有判断都基于已经取到的sr变量。不要在中途再插入别的寄存器读取打乱顺序也不要在判断分支里才去读 DR——某些分支不读 DR标志就漏清了。5.5 DMA 用字节宽度搬 9 位数据第 9 位丢失现象中断方式收数据一切正常切到 DMA 后偶发数据错误尤其在高速率、连续大包时错误表现为某些字节的高位翻转或者校验错计数飙升。原因M1 时数据寄存器有效位是 9 位DMA 的外设数据宽度必须配成半字16 位。配成字节时DMA 每次只从 DR 搬走低 8 位第 9 位虽然被读走标志能清但校验结果没有参与后续判断发送方向同理字节宽度的写操作在某些时序下不能正确落到 9 位寄存器上。解决DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord;内存侧可以是DMA_MemoryDataSize_Byte让 DMA 做宽度转换但外设侧必须是半字。改完之后如果数据对了但顺序错再去查内存地址自增和缓冲区长度那是另一个问题。6. 取舍硬件奇偶校验能兜住什么兜不住什么奇偶校验的能力边界要说清楚它能检出奇数个位的翻转检不出偶数个位的翻转也完全不具备纠错能力——PE 只是个标志硬件不会替你重传。所以它适合的场景是“错误能发现、重传代价低”的现场总线比如一问一答的采集系统不适合“一帧几十字节、错了整包重来”的大数据量传输那种场合上一层的 CRC 才是主力奇偶校验只是顺手加的一道筛子。真正的设计取舍在错误恢复策略上。我的习惯是PE 计数在连续 N 次比如 8 次内超过阈值就把 UE 清零再置 1让 USART 彻底复位一次而不是只清标志继续跑。原因是噪声环境下 PE 很少单独出现往往伴随 FE帧错误和 NE噪声标志清干净了但内部的采样状态可能已经偏了硬复位一行代码就能把状态机拉回已知态比逐帧排查省事得多。验证方法也简单把对端故意配成 8N1 或 8O1看你的 PE 计数是不是稳定增长、复位后能不能自动恢复——能说明这条链路是活的不能说明你的错误路径根本没被触发过现场出事时一样抓瞎。还有一层取舍是中断还是 DMA。带校验的帧每字节固定 11 位中断开销是固定的115200 下一秒钟一万多次中断Cortex-M3 扛得住但会挤压实时任务DMA 能把这个开销压到几乎为零代价是 DMA 传输出错时你拿不到逐字节的上下文只能靠 TC 中断和错误标志做粗粒度恢复。我的选择是单帧短、要求逐字节时间戳的走中断连续大包、对 CPU 占用敏感的走 DMA但 DMA 那条路上一定把 PE 中断单独使能别让校验错在 DMA 模式下彻底静默。从那以后我每次配带校验的串口都会先确认三件事字长是不是 9 位、BRR 是不是按所在总线算的、中断里 SR 和 DR 是不是按顺序读的。这三条过一遍剩下的基本就只是接线问题了。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?