资讯动态

STM32F103 USART2奇偶校验:9位字长与PE/DMA避坑

发布时间:2026/10/26 11:22:39 来源:尧图企业网站定制
简介面向嵌入式初学者与单片机开发者的STM32F103串口通信实践工程聚焦C8T6上USART2带奇偶校验的配置与收发。工程围绕APB1时钟使能、PA2/PA3引脚复用、波特率与数据位设置以及USART_CR1中PCE、PS位的奇偶校验选择展开可用于传感器、显示模块或调试串口的对接练习。压缩包共78个文件、约308KB以34个h头文件和33个c源文件为主体另含Keil工程、启动汇编、hex烧录文件与批处理脚本目录按HARDWARE、SYSTEM、CORE、STM32F10x_FWLib、USER、OBJ划分驱动与库文件层次清楚。已有2446人学习读者可据此对照标准外设库的串口初始化流程查看UART2驱动、LED与按键模块的配合理解校验位对收发的影响并在此基础上改造成中断或DMA方式积累寄存器级调试与工程编译经验。1. 那一 bit 校验位改的不是协议是 USART 的数据宽度在 STM32F103 上给串口2加奇偶校验很多人的第一反应是打开USART_CR1里的 PCE 位就完事。烧进去才发现数据错了发出去0xA5对端收到0x25或者接收端读回来的值永远比预期多出0x100。这不是玄学而是这颗单片机 USART 的一条硬规则——校验位不是“额外附加”在数据后面的它抢占的正是数据帧的最高位。所以串口2带奇偶校验的本质问题是你到底还剩几个数据位可用。把这 1 bit 的来龙去脉搞清楚PCE、M、PS 三个位的关系、PE 标志的清除时序、9 位数据怎么塞进 16 位的 DR就全部能串起来。2. 帧结构M 位和 PCE 位为什么会互相吃掉一位数据2.1 从起始位到停止位一帧到底有几个 bit异步串口没有时钟线收发双方靠“起始位下降沿”对齐然后按约定的波特率逐位采样。一帧的固定骨架是空闲时线路保持高电平起始位拉低 1 个位时间接着是数据位低位先发然后是可选校验位最后是停止位拉高。8 位数据、无校验、1 位停止位就是常说的 8N1一共 10 个位时间加上偶校验就是 8E1一共 11 个位时间。这里有个容易被忽略的细节波特率误差的容忍度和帧长成反比。10 位的帧累积到停止位时的相位偏移只要不超过半个位时间就能采对余量大约 ±5%11 位的帧余量缩到 ±4.5% 左右。所以在 36MHz 的 APB1 上跑 115200 带校验误差必须认真算不能想当然地认为“以前 8N1 能通加个校验位也照样通”。2.2 M0 配 PCE1 的真实帧你只剩 7 个数据位参考手册里那句关键描述是校验位占用数据帧的最高位。也就是说PCE 一旦置 1硬件会把移位寄存器里本该发出去的最高位数据“顶掉”换成算出来的校验位。于是 M 位字长和 PCE 位就产生了耦合M0 声明的是“8 位帧”这 8 位里被校验位占掉 1 位真正能承载数据的只剩 7 位。M字长PCEPS帧内数据位DR 有效位典型形态00—8DR[7:0]8N1调试口最常见010/17DR[6:0]很少有人故意这么配10—9DR[8:0]9 位多机协议、地址位110/18DR[7:0]8E1 / 8O1带校验的正确形态表格最后一行才是“8 位数据 偶校验”的正确配置字长必须设成 9 位把校验位算进帧长度里这样 8 个数据位才完整。标准库写作USART_WordLength_9bUSART_Parity_EvenHAL 写作UART_WORDLENGTH_9BUART_PARITY_EVEN。第一次踩这个坑的人八成都是配了USART_WordLength_8bUSART_Parity_Even然后对着数据抓狂。2.3 一次可复现的对照实验发 0xA5收到 0x25把上面那个错误配置烧进去发一个0xA5能非常直观地看到后果。0xA5的二进制是1010 0101。因为 M0、PCE1硬件只把低 7 位010 0101当数据最高位1被丢弃。偶校验要求“数据位 校验位里 1 的个数为偶数”低 7 位里有 4 个 1已经是偶数所以校验位补0。对端如果按 8N1 收看到的 8 个 bit 就是0 010 0101也就是0x25。这个实验最扎心的地方在于如果对端同样配了 8E1它收到0x25自己算一遍偶校验0x25里 4 个 1校验通过不报错。数据错了链路却显示一切正常——这才是“黑匣子”级别的问题。排查时不要盯着波特率和接线先把 M 位和 PCE 位一起看。2.4 改成 9 位字长之后DR 里多出来的那一位是什么配置改对之后USART_DR变成一个 9 位的寄存器。发送时你写进 DR 的低 8 位是数据第 9 位会被硬件替换成校验位写什么都会被覆盖接收时读出来的 9 个 bit 里最高位是链路上传过来的校验位本身。所以标准库的USART_ReceiveData()返回uint16_t但你必须再 0xFF才是有效数据否则缓冲区里会混进0x100这样的“幽灵值”后面做协议解析时表现为“偶尔多出一个高位”。提示判断自己有没有配错最省事的办法是在初始化后回读USART2-CR1看 bit12M和 bit10PCE是不是同时为 1。3. 发送路径硬件怎么在移位寄存器里“顺手”生成校验位3.1 TDR、移位寄存器与 TXE/TC一个字节的一生写USART_DR并不等于立刻上线。这个写操作把数据装进发送数据寄存器 TDRTDR 空且移位寄存器也空时硬件把数据搬进移位寄存器然后自动补上起始位、逐位输出数据、插入校验位、最后补停止位。整个过程 CPU 完全不参与这也是为什么校验位的生成“不要钱”。TXE 标志表示 TDR 已空可以写下一个字节TC 标志表示移位寄存器也空了最后一个停止位已经完整送出线路。做半双工方向切换比如 485 收发器使能脚时必须等 TC 而不是 TXE否则最后一个字节的停止位还没发完就切了方向对端会收到一个被截断的帧——现象是“大部分包正常偶尔最后一字节 CRC 错”特别难查。校验位本身怎么算偶校验就是所有数据位做异或异或结果为 0 表示 1 的个数是偶数校验位补 0为 1 则补 1 凑成偶数。奇校验取反。8 个数据位只需要 7 级异或硬件用一个极小的组合逻辑就能完成延迟可以忽略。/* 软件侧算一遍偶校验用来核对硬件的行为是否符合预期 */ static uint8_t even_parity8(uint8_t v) { v ^ v 4; /* 高 4 位折叠到低 4 位 */ v ^ v 2; /* 再折叠 */ v ^ v 1; /* 最后 1 位就是全部 8 位的异或 */ return (uint8_t)(v 0x01); /* 返回 1 表示数据位里 1 的个数为奇数校验位要补 1 */ }这段代码在自测时很有用把要发的字节和实际抓到的校验位比一遍能立刻区分“是我配错了 M 位”还是“对端协议理解错了”。3.2 标准库那几行配置究竟写进了哪些寄存器位void Uart2_Init(uint32_t baud) { GPIO_InitTypeDef gpio; USART_InitTypeDef uart; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); /* 串口2挂在 APB1不是 APB2 */ 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_IN_FLOATING; /* 接收脚必须浮空输入别配成推挽 */ GPIO_Init(GPIOA, gpio); uart.USART_BaudRate baud; uart.USART_WordLength USART_WordLength_9b; /* CR1 bit12 19 位帧 */ uart.USART_Parity USART_Parity_Even; /* CR1 bit10 PCE1bit9 PS0 */ uart.USART_StopBits USART_StopBits_1; uart.USART_Mode USART_Mode_Rx | USART_Mode_Tx; uart.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_Init(USART2, uart); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); USART_ITConfig(USART2, USART_IT_PE, ENABLE); /* 单独开 PE 中断才知哪些帧坏了 */ USART_Cmd(USART2, ENABLE); }USART_Init内部做的事比看上去多它先把 CR1 的 Mbit12、PCEbit10、PSbit9按参数置位再按 CR2 配停止位然后把波特率算成定点数写进 BRR。注意USART_Parity_Even对应 PS0USART_Parity_Odd对应 PS1——偶校验是“数据位与校验位中 1 的总数为偶数”这是最通用的约定但也有规约规定的是“校验位等于数据位异或”两者结果一致只是描述角度不同。时钟这一行值得单独强调USART2 和 USART3 挂 APB1最高 36MHzUSART1 挂 APB2最高 72MHz。同一条代码里如果照抄了 USART1 的RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ...)串口2根本不会工作但编译链接一路绿灯。3.3 BRR 里的定点小数36MHz 下 115200 的真实误差BRR 的结构是 12 位整数部分 4 位小数部分本质是一个 ×16 的定点数。波特率公式为baud fCK / (16 × USARTDIV)。理论值算出来是无限小数时就得四舍五入而标准库为了在整数运算里保住精度用了“先乘 25 再除 4”这种等价变形把 USARTDIV ×100 放大成整数再拆位/* stm32f10x_usart.c 中的定点运算等价于 USARTDIV × 100 */ integerdivider ((25 * apbclock) / (4 * baud)); tmpreg (integerdivider / 100) 4; /* 整数部分 */ fractionaldivider integerdivider - (100 * (tmpreg 4)); /* 余下的百分位 */ tmpreg | (((fractionaldivider * 16) 50) / 100) 0x0F; /* 50 是四舍五入 */在 36MHz 的 APB1 下算出来的几个典型值目标波特率USARTDIV 理论值BRR 实际写入实际波特率误差9600234.3750xEA69600019200117.18750x7531920005760039.06250x27157600011520019.531250x1381153850.16%115200 是唯一有误差的常用值因为 36MHz 除以 16 再除以 115200 得不到 1/16 的整数倍。0.16% 在 11 位的帧上累积不到 0.02 个位时间完全无害但如果有人为了“跑快点”把 APB1 分频改成 24MHz 却忘了重算 BRR误差会直接飙到几个百分点表现是“短帧偶尔能通长帧必错”。3.4 发送 9 位数据USART_SendData的掩码与 HAL 的分支标准库的实现是USART2-DR (Data 0x01FF)只保留低 9 位。所以在 9 位字长 偶校验的配置下你传0xA5低 8 位进移位寄存器第 9 位被硬件换成校验位实际出线的是 8 个数据位加 1 个校验位完全正确。HAL 这边则有一个容易漏看的分支只有当WordLength UART_WORDLENGTH_9B且Parity UART_PARITY_NONE时HAL_UART_Transmit才把pData当成uint16_t*逐半字发送一旦开了校验它就走 8 位路径按uint8_t*逐字节递增。也就是说在 9B 偶校验下你传一个uint16_t数组进去HAL 会把高字节当成下一个独立字节发出去帧数凭空翻倍。这个坑非常隐蔽因为编译不报错逻辑分析仪上也能看到“有数据”。注意带校验时数据宽度就是 8 位接口参数该用uint8_t不要把 9 位字长误解成“每次要发 9 位数据”。4. 接收路径PE 标志、采样窗口和那句“先读 SR 再读 DR”4.1 三取二表决错误是在哪一刻被发现的STM32F103 的 USART 采用固定的 16 倍过采样没有可切换的 8 倍模式。接收端在检测到起始位下降沿后把采样点定位到位周期的中央并且不是只采一次而是在第 8、9、10 个采样时钟上各采一次做三取二多数表决——这就是所谓的噪声容限。表决结果既决定这一位的电平也参与 NE噪声标志的判定。校验是在整个数据字段移完之后算的接收移位寄存器里凑齐 8 个数据位硬件立刻用同样的异或逻辑算一遍期望的校验位再和链路上收到的第 9 位比较不一致就把 SR 的 PE 位置 1。这一位翻转发生在最后一个数据位之后、停止位之前所以 PE 一定比 RXNE 早或同时到达。顺手区分一下三个容易混的标志PE 是校验对不上FE 是停止位没采到高电平帧格式错NE 是采样表决时三次结果不一致线路噪声大。三者互不替代。4.2 PE / RXNE / ORE 挤在同一个 ISR 里的判定顺序这一段是整个带校验接收的核心。手册规定的清标志序列是“先读 SR再读 DR”而且 PE、ORE、FE、NE 全都靠这同一个序列清除。这意味着你只有一次机会SR 一读状态快照就到手了DR 一读所有标志同时被清掉。顺序反过来标志就丢了。volatile uint32_t g_parity_err, g_ore_err, g_frame_err; void USART2_IRQHandler(void) { uint16_t sr USART2-SR; /* 第一步锁存状态快照此时还没清任何标志 */ uint16_t dr USART2-DR; /* 第二步读 DR序列完成PE/RXNE/ORE/FE/NE 一起清 */ if (sr USART_FLAG_PE) /* 校验错优先级最高这帧数据不可信 */ { g_parity_err; return; /* 直接丢不要把 dr 送进缓冲区 */ } if (sr USART_FLAG_ORE) /* 溢出上一条还没被取走这条已被覆盖 */ { g_ore_err; return; } if (sr USART_FLAG_FE) /* 帧错误停止位不对通常是波特率或接线问题 */ { g_frame_err; return; } if (sr USART_FLAG_RXNE) /* 到这里才是干净数据 */ { rx_push((uint8_t)(dr 0x00FF)); /* 9 位字长最高位是校验位必须掩掉 */ } }三个参数值得说明sr是一次性快照不能反复去读USART2-SR来判断不同标志因为读 DR 之后标志就没了dr 0x00FF的掩码是 9 位字长下的必需品return一定要在读完 DR 之后读 DR 这个动作本身就是清标志的一部分任何“提前 return 不读 DR”的写法都会让中断反复触发表现为系统卡死在中断里出不来。4.3 只开 RXNEIE 不开 PEIE 会发生什么这是血泪经验里最贵的一条。很多人想着“我只要收到数据就行”只开了 RXNEIE。但校验错的时候硬件依然把那帧数据搬进了 DRRXNE 照样置位中断照样进——只不过 PE 位也同时被置 1而你的代码根本没看它。于是坏帧被当成好帧喂给协议解析层出现“偶发的一帧数据莫名其妙全错但设备不重启就一直错下去”的现象。反过来只开 PEIE 不开 RXNEIE 也不行校验正确时 PE 不会置位你的中断永远不进数据全丢。正确做法是两个都开并且在 ISR 里按上面的顺序判定。4.4 DMA 接收下的校验错误为什么不会自动丢包用 DMA 收数据时麻烦在于 DMA 的请求源是 RXNEPE 完全不参与 DMA 的搬运决策。也就是说一帧校验失败的字符照样会被 DMA 写进你的环形缓冲区一个字节都不会少。你的代码在 DMA 传输完成中断里看到的是“长度正确的一帧”只有重新按帧结构算一遍校验才能发现错误。常见的两个解法一是保留 PE 中断在 ISR 里记一个“污染标记 当前写入位置”DMA 完成后按位置区间把可疑数据剔除二是不依赖硬件校验在应用层做整帧校验累加和或 CRC硬件奇偶校验只作为“快速告警”使用。前者省 CPU后者更可靠实际项目里往往两个都留着。提示DMA 接收 硬件校验的组合最怕的就是“看起来通了”。建议在自测阶段用信号发生器或另一块板子故意注入一位翻转确认你的代码真的能发现它。5. 避坑排查串口2带奇偶校验最容易翻车的五个地方5.1 配置阶段就该发现的两个坑坑一数据被悄悄截成 7 位。现象是发出0xA5收到0x25或者 0x80 以上的值全部对不上但 0x00~0x7F 一切正常。原因是USART_WordLength_8b配了USART_Parity_Even校验位吃掉了帧的最高位实际只剩 7 个数据位。解决办法是把字长改成USART_WordLength_9b让校验位占用第 9 位改完记得同步检查发送侧会不会误传uint16_t数组。坑二时钟挂在错误的总线或者 GPIO 模式配错。现象是完全没有波形或者 TX 有波形但 RX 一点反应都没有。原因通常是照抄了 USART1 的初始化把RCC_APB1Periph_USART2写成了 APB2 的外设或者把 RX 脚PA3配成了GPIO_Mode_AF_PP推挽输出。解决方法是核对时钟总线USART2/3 在 APB1RX 一律用GPIO_Mode_IN_FLOATING并确认没有误开重映射开了重映射引脚会跑到 PD5/PD6 上去。5.2 跑起来才暴露的三个坑坑三校验错被静默吞掉。现象是长时间跑机后偶发一帧数据全乱但错误计数器一直是 0。原因是只开了 RXNEIE 没开 PEIEISR 里也没检查 PE。解决办法是同时打开两个中断源并在 ISR 里把 PE 放在最高优先级判定同时加一个g_parity_err计数器输出到调试口跑一晚看它涨不涨这是最廉价的健康度指标。坑四ISR 里先读 DR 再读 SR标志丢失。现象是校验错误计数器几乎不涨但数据明显是坏的或者中断进去出不来主循环被饿死。原因是清标志序列写反了DR 一读PE 和 RXNE 同时清零后面再读 SR 自然是干净的另一种情况是某个分支提前return没有读 DRRXNE 一直置位导致中断风暴。解决办法是严格按“先 SR 后 DR”写且保证无论走哪条分支DR 都被读到。坑五DMA 接收以为硬件会帮忙丢坏帧。现象是应用层偶尔解析出离谱的数值重启后恢复硬件校验计数却对不上。原因是 PE 不参与 DMA 搬运坏字符进了缓冲区。解决办法是在 DMA 完成中断里按帧长和帧头重新校验一遍或者用 PE 中断打“污染标记”再做区间剔除。注意这五个坑里有四个都不会导致编译报错也不会导致“完全不通”只会让数据“偶尔不对”。带校验的串口调试第一件事应该是把错误计数器接出来而不是盯着数据看。6. 设计取舍这 1 bit 到底值不值什么时候该上 CRC奇偶校验的能力边界非常清晰它能 100% 发现一帧内的奇数个位翻转对偶数个位翻转完全无能为力。两个相邻位同时被干扰翻转校验和不变链路安静如鸡。所以它从来不是“可靠性方案”而是“廉价告警”。但它有一个别人比不了的优势——开销是每 11 个位时间多 1 个位约 9%而且是逐字符生效的一旦某个字符校验失败你立刻知道具体是哪个字节坏了可以只丢这一字节甚至在协议允许时请求重传该字节。手段开销能发现漏检情况奇偶校验1 bit / 11 bit ≈ 9%任意奇数个位翻转任意偶数个位翻转累加和1~2 字节 / 帧大部分随机错误可相互抵消的组合CRC162 字节 / 帧单/双/奇数位错误、≤16 位突发错误概率极低选择的分界线大致是这样如果链路上主要威胁是“短促的电气干扰导致单 bit 翻转”并且对端是老旧设备、协议帧长不固定、没法加尾部校验字段本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑