资讯详情

STM32 USART寄存器实战:从初始化配置到中断接收

📅 2026/10/13 1:30:28 | 华诺云谱 👁 阅读
STM32 USART寄存器实战:从初始化配置到中断接收
第五章终于要讲串口了。熟悉我前面几章风格的读者应该知道我写这套STM32寄存器白话手册从来不打算把参考手册复述一遍而是把每个外设剥开揉碎用干工程的人看得懂、用得上、记得住的逻辑去讲。USART通用同步异步收发器就是典型的“官方手册写得极其细致、库函数封装得极其顺手、一遇到坑却无从下手”的模块这一章我会把相关的寄存器、位定义、配置顺序、初始化代码和能想到的坑全部串起来。这一章适合两类人一类是刚刚会点灯、会用库函数发个串口字符串但一换成寄存器操作就懵的初学者另一类是已经在用寄存器开发却被“乱码”“只能发不能收”“进中断死循环”折磨过的朋友。说到底USART是所有通信调试的基础口子——板子上电第一件事就是printf打印信息你如果连这一环都不稳后续调传感器、调通信协议都会非常痛苦。所以我把这一章当作整个手册的枢纽章节来写你可以在这一章找到所有你能用到的发送、接收、中断、标志位清法以及我踩过几次之后总结下来的避坑清单。1. 为什么是串口先给USART画个像1.1 USART到底在干什么USART的U是Universal但大家平时最常用的其实是异步模式也就是UART。它干的事情可以概括为把并行的数据变成一比特一比特的串行电平在TXD引脚上按照约定的速率送出去同时从RXD引脚上把电平采样还原成一个字节。通信双方不需要时钟线靠的是双方约好的同一个波特率再加上每一帧的起始位、数据位、停止位结构来对齐。我经常给学生打一个比方两个人约好在校门口碰头但谁也不带钟怎么保证同时出现只能提前约定“八点整”并且每个人都按这个时间走。UART也是这个逻辑发送方按9600bps一个比特一个比特地往外摆电平接收方也按9600bps去采样。如果没有起始位接收方根本不知道从哪个电平开始算一个字节有了起始位和停止位双方才能一次一次地框出一个数据帧。从寄存器的视角来看你不需要理解波形怎么翻转、采样时点怎么取你只需要关心几个事情外设时钟有没有开、引脚有没有配置对、波特率寄存器写得对不对、状态寄存器里的TXE/RXNE标志我怎么判断、数据该怎么往DR里写、以及中断来了之后我该干什么。把这些点全部串起来USART就彻底拿下了。1.2 我要把哪些寄存器“唤醒”STM32F1系列的USART寄存器不算多核心就这几类RCC里的外设时钟开关USART1挂在APB2上USART2/3挂在APB1上GPIO的复用配置让PA9、PA10这样的引脚把控制权交给外设USART_CR1、CR2、CR3分别是控制1、2、3负责总开关、数据位长度、停止位、发送使能、接收使能、各种中断使能USART_BRR波特率寄存器决定每秒传多少比特USART_SR状态寄存器里面是TXE、RXNE、TC、ORE这些标志位USART_DR数据寄存器写入就是发送读出就是接收。可能你是从库函数转过来了库函数里一个USART_SendData()就把发送盖得严严实实但看完寄存器你会有一种“原来如此”的感觉发送无非是等TXE为1然后把数据丢进DR接收也无非是等RXNE为1然后把DR读出来。就这么简单剩下的全是细节。2. 动手前必须做对的两件事时钟和引脚2.1 时钟这一层就藏着一半BUG我一直强调STM32外设寄存器操作第一件事永远是时钟。时钟不开后面写什么都像对着空气挥拳而且不会报错就是没反应。USART1在APB2总线上所以要置RCC-APB2ENR的USART1EN位USART2和USART3在APB1总线上要操作RCC-APB1ENR。很多人会把APB2当成“高速”总线、APB1当成“低速”总线来记但这里更要紧的是波特率计算时的时钟频率USART1用APB2的时钟常见72MHzUSART2/3用APB1的时钟常见36MHz。同样是配置9600波特率USART1和USART2的BRR值不一样因为分母不同。我见过太多人把同一份USART1的初始化代码改成USART2结果乱码乱得怀疑硬件。根源基本都在时钟源上你拿72MHz的BRR值往36MHz的外设上写波特率差一半能不错吗如果用到USART1的引脚重映射比如映射到PB6/PB7那还要额外开启AFIO时钟并操作AFIO-MAPR寄存器。默认的PA9、PA10不需要开AFIO时钟很多库函数初始化代码里不管三七二十一全都开一遍问题不大但如果你在做低功耗或者眼神好抠寄存器内存映射会介意这个多余开关。2.2 GPIO复用模式怎么选USART的TXD引脚必须配置成“复用推挽输出”RXD引脚配置成“浮空输入”或者“带上拉输入”。在F1系列里GPIO的CRH/CRL寄存器控制每个引脚的模式一个引脚占4个位CNF两位是模式MODE两位是速度。以PA9作为USART1_TX为例CNF要写成10复用推挽输出MODE我习惯给10即2MHz翻转速率。串口波形频率本身不高2MHz速度足够而且能减少噪声。PA10作为USART1_RXCNF要写成01浮空输入MODE两位写成00即可。这一段我单独拎出来写是因为很多人的第一反应是像配置普通LED一样把TX配成“通用推挽输出”结果数据发不出去。CNF那两位的区别决定了引脚是受GPIO数据寄存器控制还是受复用功能外设控制。你把它配成通用输出就等于把外设的信号挡在了门外面。2.3 一个可以直接抄的初始化顺序综合下来寄存器方式初始化USART1的完整顺序是打开GPIOA时钟和USART1时钟配置PA9为复用推挽输出、PA10为浮空输入写波特率寄存器BRR配置CR1使能UE、TE、RE如果有需要再配置中断使能。这一段顺序其实有讲究。参考手册上是建议先置UE再配置TE/RE因为TE的置位会使USART在发送完当前数据后拉高TXD而这样一个顺序能避免线上出现意外的电平毛刺。实际工程中只要不涉及热插拔或者对时序极敏感的外部设备先写BRR后开总开关的区别不大但建议还是养成“先算好波特率、配置好帧结构再打开外设总开关”的习惯。代码我直接给一份可用的void uart1_init(void) { // 1. 时钟 RCC-APB2ENR | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN; // 2. GPIOPA9 复用推挽输出PA10 浮空输入 GPIOA-CRH ~(GPIO_CRH_CNF9_Msk | GPIO_CRH_MODE9_Msk); GPIOA-CRH | (2 GPIO_CRH_CNF9_Pos) | (2 GPIO_CRH_MODE9_Pos); GPIOA-CRH ~(GPIO_CRH_CNF10_Msk | GPIO_CRH_MODE10_Msk); GPIOA-CRH | (4 GPIO_CRH_CNF10_Pos); // CNF01, MODE00 // 3. 波特率72MHz 下 9600 - BRR 0x1D4C USART1-BRR 0x1D4C; // 4. 使能串口、发送、接收 USART1-CR1 | USART_CR1_UE | USART_CR1_TE | USART_CR1_RE; }如果你用的是标准外设库这些寄存器位都有现成宏但我更建议你像我这样盯一眼数值这样出问题时能自己算而不是换个芯片就抓瞎。3. 串口核心寄存器逐个拆解3.1 状态寄存器SR数据通没通的晴雨表USART_SR是排查串口问题时第一个要看的地方。它的每一位代表一个硬件状态常见的几个TXE发送数据寄存器空。这个位为1说明DR已经被腾空了你可以写入下一个字节TC发送完成。不仅DR空了而且移位寄存器也把最后一个比特送出去了RXNE接收数据寄存器非空。说明DR里收到了一个完整字节等你去读ORE溢出错误。接收速度太快你还没读DR下一个字节已经把数据覆盖了IDLE检测到空闲帧FE帧错误停止位没采到大概率是波特率偏差太大PE奇偶校验错误。有一个很关键的细节这些状态位怎么清除。TXE是“写DR”清零RXNE是“读DR”清零TC是“先读SR再写DR”清零ORE也是“先读SR再读DR”清除。很多人中断里进得去出不来就是因为只清了中断标志位没读DRRXNE一直为1中断反复触发。3.2 数据寄存器DR唯一的入口出口USART_DR是串口数据进出的唯一通道写入就是发送读出就是接收。它是9位有效的数据通道如果你配置了M位为1那就是9位数据模式M位为0就是8位数据模式一般我们都是8位。我要提醒一下DR的低位数据在写入和读出时直接对应的是你要发的字节或者收到的字节但读取时一定要先看RXNE状态写入时一定要先看TXE状态。不遵循这个顺序会丢数据或者把还没准备好的数据覆盖掉。3.3 波特率寄存器BRR算错一个字乱码满天飞波特率计算是整个USART章节里我强调最多的点。公式如下USARTDIV fck / (16 * baud)假设OVER8位为0即16倍过采样。算出来的值是一个带小数的数字整数部分写入BRR的高12位小数部分乘以16后写入BRR的低4位。举个例子USART1挂在72MHz的APB2上目标波特率9600USARTDIV 72000000 / (16 * 9600) 468.75整数部分468换算成十六进制是0x1D4小数部分0.75乘以16等于12写成十六进制是0xC。所以BRR 0x1D4C。这个数值正好能被整除所以误差是0。但大多数时候波特率不会这么完美比如11.0592MHz晶振跑出的时钟波特率就算出一个小数尾巴这时就要四舍五入到最接近的DIV_Fraction值然后再计算实际误差是否在可接受范围内。UART接收容错一般在正负百分之二以内所以很多非整除配置也能正常工作。还有一点要特别强调USART2和USART3用的是APB1时钟如果APB1是36MHz那同样的9600波特率BRR应该是另一组数字USARTDIV 36000000 / (16 * 9600) 234.375整数234是0xEA小数0.375乘以16等于6所以BRR 0xEA6。我见过一个朋友把USART1的配置抄到USART2以为9600就是9600结果收到一堆乱码查了半天才发现两边外设时钟不一样。这种坑一旦知道原理就再也不会犯。3.4 控制寄存器CR1/CR2/CR3捏住方向盘USART_CR1是最重要的控制开关里面这些位值得一个个说清楚UE串口总使能。这个位等于总闸不开它其他位全白配M字长选择。0是8位1是9位PCE奇偶校验使能TE发送使能RE接收使能RXNEIE接收缓冲区非空中断使能TCIE发送完成中断使能TXEIE发送数据寄存器空中断使能。CR2里最常用的是STOP位。00表示1个停止位01表示0.5个停止位10表示2个停止位11表示1.5个停止位。绝大多数场合用1个停止位就够了。CR3里面如果你不用DMA、不用硬件流控基本保持默认就行。配置帧结构时需要先关UE还是直接改不同芯片要求不同但在F1系列里一般建议改CR1的关键位之前先把UE清零配置完成后再置位。这样能避免配置到一半串口处于不确定状态。3.5 寄存器配置的完整顺序把这一小节当作结论部分。任何USART寄存器初始化我都按这个顺序走查数据手册确认外设总线开对应的RCC时钟配置引脚复用模式关闭UE配置CR1的M、PCE、TE、RE等初始值配置CR2的停止位计算并写入BRR置位UE然后按需打开中断使能位如果开中断还要操作NVIC设置抢占优先级并使能对应IRQ通道。为什么建议关UE再配置因为UE打开时引脚已经处于串口工作状态这时候你改字长、改停止位、改波特率都有可能让外部设备收到意料之外的垃圾信号。先关总闸改好再开干净利落。4. 轮询发送和中断接收的实操代码4.1 轮询发送先看TXE再写DR发送一个字节的逻辑非常简单等TXE置位然后写DR。代码可以写成void uart1_putc(char ch) { while (!(USART1-SR USART_SR_TXE)); USART1-DR ch 0xFF; } void uart1_puts(char *s) { while (*s) { uart1_putc(*s); } }这段代码里有一个细节TXE置位表示DR已经空了但此时移位寄存器可能还在发送上一个字节。连续发送时你不必等TC因为USART内部有缓冲当DR空的时候下一个字节写进去会自动排队。但如果你要发完这一串之后马上操作串口开关或者要把TXD引脚切换成普通GPIO那就必须等TC否则最后一个字节会丢一半。所以更稳妥的发送字符串函数应该是发完最后一个字符之后再等待一次TCvoid uart1_puts_wait(char *s) { while (*s) { uart1_putc(*s); } while (!(USART1-SR USART_SR_TC)); }4.2 轮询接收RXNE是“来货通知”轮询接收更简单等人送数据来送来了就接uint8_t uart1_getc(void) { while (!(USART1-SR USART_SR_RXNE)); return (uint8_t)(USART1-DR 0xFF); }这段代码在只有单一收发的例程里够用但它有一个明显的缺陷如果一直没有人发数据程序就死等在while循环里什么别的事都干不了。这时候真正工程里会用中断接收或者DMA接收。我还遇到过一个新手问题他把这段函数放在主循环里然后用串口助手每秒发200个字节结果只能收到一两个。原因在于处理器读完DR之后立刻又回到while判断RXNE如果RXNE还没被下一个字节置位就一直空转而外设每一秒钟发来的数据其实远大于程序处理的速度。解决方式不是修改这个函数而是改用中断或FIFO缓存。4.3 中断接收不卡主循环的正确姿势中断接收的思路是使能RXNEIE让数据到达时自动打断主循环把收到的字节存进内存缓冲区主循环有空时再来处理缓冲区。这样就规避了轮询等待带来的阻塞问题。初始化部分在之前的代码基础上增加一行USART1-CR1 | USART_CR1_RXNEIE; NVIC_EnableIRQ(USART1_IRQn);然后写中断服务函数void USART1_IRQHandler(void) { uint8_t ch; if (USART1-SR USART_SR_RXNE) { ch USART1-DR 0xFF; // 这里做你想做的事存缓冲区、设置标志位、原样回显等 } if (USART1-SR USART_SR_ORE) { // 溢出处理先读SR再读DR清除ORE (void)USART1-SR; (void)USART1-DR; } }很多人中断服务函数里只写了if (RXNE)这一句结果偶尔高速接收就卡死。问题就出在ORE溢出标志没有处理。当串口数据来太快程序在中断里还没来得及读走DR下一个字节又到了硬件就会置位ORE。如果不管它某些情况下会导致后续中断异常。我在所有中断接收代码里都会加上这段ORE清除不是为了好看是真的踩过坑。关于缓冲区初期你可以用一个简单数组#define RX_BUF_SIZE 64 volatile uint8_t rx_buf[RX_BUF_SIZE]; volatile uint8_t rx_head 0; volatile uint8_t rx_tail 0; void USART1_IRQHandler(void) { uint8_t ch; if (USART1-SR USART_SR_RXNE) { ch USART1-DR 0xFF; rx_buf[rx_head] ch; rx_head (rx_head 1) % RX_BUF_SIZE; } if (USART1-SR USART_SR_ORE) { (void)USART1-SR; (void)USART1-DR; } }这就是最简单的环形缓冲区模型。主循环里判断rx_head ! rx_tail说明有新数据待处理。这个模型在中小数据量场景下非常稳定也是日后接DMA、接FreeRTOS队列的基础思维。4.4 一个实际测试闭环初始化配置完之后怎么快速验证串口通没通我的做法是这样的先用USB转TTL模块连接确保TXD接到对方的RXDRXD接到对方的TXDGND必须共地代码里先发一个固定字符串比如“uart ok”然后在主循环里做回显测试收到什么就原样发什么用串口助手发送“ABC”如果能收到“ABC”收发链路都通再测一变“自发自收”把TX和RX两根线直接短接然后代码里发0x55如果能收到0x55说明串口本身工作正常。这种分层验证理念很重要先验证外设本身再验证接线再验证对端设备。直接一上来就连设备出了问题会多很多排查维度。5. 常见问题与排查技巧实录5.1 乱码问题的排查顺序乱码是串口调试里最高频的问题没有之一。先把排查思路按顺序排好第一确认波特率。先回看BRR计算用的时钟频率和外设总线。用示波器或者逻辑分析仪测TXD引脚波形数一下一帧的时间是最直接的办法。没有仪器也没关系用一个已知能正常通信的串口工具逐个波特率试试到不乱码的那个可能就是真实波特率。第二确认帧格式。两端的数据位、停止位、校验位必须一致。常见组合是8位数据、无校验、1位停止也就是8N1。如果一端配了9位数据另一端按8位解结果肯定是乱的。第三确认电平。单片机的TXD/RXD是TTL电平接电脑串口一般需要USB转TTL模块。如果你的目标设备是RS232电平就必须经过电平转换芯片直接接会烧或者无法通信。第四检查接线是否交叉。TXD接对方RXDRXD接对方TXD这个交叉错一次收发就会不对称。GND不共地也会导致信号参考点不同偶尔能通但数据不稳定。第五检查晶振频率。部分开发板出厂配的是8MHz晶振但有些板子为了省成本用了别的频率或者倍频配置和你预期不一样。如果BRR是按72MHz算的实际PCLK却是36MHz或者8MHz乱码避无可避。最好的办法是读一下RCC的寄存器配置或者直接看系统时钟初始化代码。5.2 能发不能收或者能收不能发如果发送正常但接收不到先看CR1的RE有没有置位。很多人的初始化代码只使能了TE和UE忘了RE。然后检查RXD引脚的GPIO模式必须是输入模式通用输出会干扰外设输入采样。再看外部接线确认对方的TXD是不是真的接到了你的RXD上。反过来能收不能发优先检查TXD引脚配置是不是复用推挽输出以及CR1的TE有没有置位。曾经有个朋友配置TX引脚时用错了宏把引脚设成了通用推挽查了半天就是不知道为何引脚没有波形。用万用表测TXD引脚空闲电平正常情况下空闲应该是高电平如果一直低多半是配置问题。5.3 中断进进出出停不下来中断接收过程中进入ISR后发现RXNE一直为1而且程序反复进中断大概率是数据读得不够干净。记住RXNE由读DR清零。如果你只是使能了RXNEIE但在中断里没有读DR或者读的不是USART_DR这个地址硬件就认为数据还没取走中断会一直被触发。ORE位也会带来莫名中断。高速接收时出现ORE在有些系列里会产生中断请求如果ISR里没有处理ORE就可能出现“中断风暴”。最简单有效的做法是ISR开头同时判断RXNE和ORE并且清掉ORE。另外不要在主循环里和中断里同时读写同一个缓冲区下标要操作缓冲区一定先关中断或者用临界区保护不然会出现数据错位、丢字节的诡异问题。这个在调试时非常隐蔽我遇到过两次最后都是给缓冲区访问加了保护才稳定下来。5.4 用串口助手调试时你还得注意的事串口助手发送字符串时注意“发送新行”选项。如果你的代码是逐字节回显勾选了发送新行回显里就会多出回车换行这不叫故障是正常的。接收缓冲区显示乱码有时候不是因为波特率错而是PC端串口助手的显示编码和实际数据不匹配。比如发的是UTF-8的中文助手用GBK解码看起来就是乱码。这个现象与单片机无关但排查时要能识别出来别在上面浪费时间。最后分享一个我保持了很久的习惯每次新板子调试串口我不会直接嵌业务代码而是先做一个独立的“串口自检工程”只包含时钟、GPIO、USART初始化、回显和状态灯闪烁。自检过了再在这个基础上叠加业务逻辑。这样一旦后续出错我能立刻把问题定位到新增代码而不是去怀疑串口初始化是不是偷偷变了。这套方法帮我省下的排查时间丝毫不夸张地说比几十遍读手册都值。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑