一、USART 是什么
USART(通用同步/异步收发器)说白了就是两根线 + 一个约定:
发送方 TX ────────────► RX 接收方 接收方 RX ◄──────────── TX 发送方 (全双工:两边可以同时说)一帧数据的格式(无校验、1 停止位):
┌──────┬──────────────────────┬────────┐ │起始位│ D0 D1 D2 ... D7 │ 停止位 │ │ 0 │ 8 位数据(低位在前) │ 1 │ └──────┴──────────────────────┴────────┘双方约定好波特率(每秒多少个 bit),发送方按这个节奏把数据一位一位往线上放,接收方按同样的节奏采样,就能还原出数据。没有时钟线,全靠"对表"——这也是为什么收发双方波特率必须一致,差一点就全是乱码。
两个最重要的标志位(本篇的主角)
| 标志 | 全称 | 含义 | 我们的用法 |
|---|---|---|---|
| TXE | Transmit data register Empty | 发送数据寄存器空了,可以写下一个数据 | 发送前死等它置 1 |
| RXNE | Receive data register Not Empty | 收到数据了,赶紧来读 | 开中断,一来数据就进中断服务函数 |
记住这对标志位,串口收发的代码就都能看懂了。
二、硬件连接
我的开发板(STM32F407)上:
| 串口 | 引脚 | 连接 | 用途 |
|---|---|---|---|
| USART1 | PA9=TX, PA10=RX | 板载 CH340C 芯片 → USB Type-B 口 → 电脑 | 主调试串口,115200 |
| USART3 | PB10=TX, PB11=RX | 引出排针 | 接外部模块,9600 |
电脑端用串口助手(XCOM / 串口调试助手)打开 CH340 对应的 COM 口即可。
注意:不是所有板子都有板载 CH340。如果你的板子没有,需要外接 USB 转 TTL 模块,共地。
三、代码实现
3.1 整体流程
① 开时钟:GPIOA + USART1(USART1 挂在 APB2,84MHz) ② GPIO 复用配置:PA9/PA10 设为 AF 模式 + 复用映射到 USART1 ③ USART 参数:波特率/字长/停止位/校验/流控/模式 ④ NVIC:使能 USART1 中断 ⑤ USART_ITConfig 开 RXNE 中断(收到数据触发) ⑥ USART_Cmd 启动串口3.2 USART1 初始化
voidUART_Init_Config(void){GPIO_InitTypeDef GPIO_InitStructure;//1. 使能 GPIOA 时钟 + USART1 外设时钟// 注意:USART1 挂在 APB2 总线上!RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9|GPIO_Pin_10;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF;//复用功能模式GPIO_InitStructure.GPIO_Speed=GPIO_High_Speed;GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;GPIO_Init(GPIOA,&GPIO_InitStructure);//引脚复用:把 PA9/PA10 交给 USART1 这个外设管GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);//2. 配置串口参数USART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate=115200;//波特率USART_InitStructure.USART_WordLength=USART_WordLength_8b;//8 位数据USART_InitStructure.USART_StopBits=USART_StopBits_1;//1 位停止位USART_InitStructure.USART_Parity=USART_Parity_No;//无奇偶校验USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;//无硬件流控USART_InitStructure.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;//收+发USART_Init(USART1,&USART_InitStructure);//3. NVIC 配置NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel=USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x01;NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x01;NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_Init(&NVIC_InitStructure);//开中断——接收数据寄存器非空(RXNE)触发USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//开启串口1USART_Cmd(USART1,ENABLE);}还是熟悉的套路:开时钟 → 配 GPIO → 配外设 → 配 NVIC → 使能。标准库的外设初始化基本都是一个模板,写多了自然手感就熟了。
3.3 发送:等 TXE
//发送一个字符voidUart_SendByte(USART_TypeDef*USARTx,uint16_tch){USART_SendData(USARTx,ch);//等待发送数据寄存器为空(TXE 置 1 才能写下一个)while(USART_GetFlagStatus(USARTx,USART_FLAG_TXE)==RESET);}//发送字符串voidUart_SendString(USART_TypeDef*USARTx,char*str){uint16_tk=0;while(*(str+k)!='\0'){Uart_SendByte(USARTx,*(str+k));k++;}}发送是阻塞式的:把数据扔进DR寄存器后,死等TXE置位再返回。简单可靠,调试阶段够用了。
3.4 printf 重定向(最爽的一步)
在 uart.c 里重写 fputc,让printf的输出全部走 USART1:
#include"stdio.h"//重定向C库函数,使printf函数能够将数据输出串口1打印intfputc(intch,FILE*stream){USART_SendData(USART1,ch);while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);returnch;}从此你的代码里可以直接写:
printf("Freq = %.2f Hz, Duty = %.1f %%\r\n",Freq,Duty);串口助手里实时看到数据,调试体验直接上一个台阶。
⚠️ 两个前置条件,缺一个 printf 就卡死:
Keil 里勾选Target → Use MicroLIB;
fputc 所在的 .c 文件
#include "stdio.h"。另外记得加
\r\n换行,很多串口助手只认这个。
3.5 中断接收:RXNE 触发
uint16_treceive;voidUSART1_IRQHandler(void){//RXNE 为 1,表示有数据来了——赶紧去拿if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET){receive=USART_ReceiveData(USART1);//收到什么就原样发回去(回显)USART_SendData(USART1,receive);}USART_ClearITPendingBit(USART1,USART_IT_RXNE);}流程:串口助手发一个字节 → RXNE 置 1 → 触发USART1_IRQHandler→ 读DR把数据取走(读取动作本身也会清 RXNE)→ 想干嘛干嘛,这里做的是回显。
注意:中断服务函数里不要做耗时操作(比如 printf、延时)。中断里只做"取数据 + 存缓冲区",真正的业务处理放到主循环里做,否则会拖垮整个系统。
四、USART3 第二路串口:数据转发到串口 1
4.1 为什么单独讲 USART3?
USART1 挂在APB2(84MHz),而 USART3 挂在APB1(42MHz)。两路串口波特率故意设成不一样(115200 vs 9600),就是为了把"不同总线上的串口"这个坑讲清楚——初始化时开时钟的总线别搞错,否则外设根本起不来。
4.2 USART3 初始化(9600bps)
voidUART3_Init_Config(void){GPIO_InitTypeDef GPIO_InitStructure;//1. 使能 GPIOB 时钟 + USART3 外设时钟// 注意:USART3 挂在 APB1 总线上!不是 APB2!RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3,ENABLE);GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10|GPIO_Pin_11;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF;//复用功能模式GPIO_InitStructure.GPIO_Speed=GPIO_High_Speed;GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;GPIO_Init(GPIOB,&GPIO_InitStructure);//引脚复用:把 PB10/PB11 交给 USART3 这个外设管GPIO_PinAFConfig(GPIOB,GPIO_PinSource10,GPIO_AF_USART3);GPIO_PinAFConfig(GPIOB,GPIO_PinSource11,GPIO_AF_USART3);//2. 配置串口参数——波特率 9600USART_InitTypeDef USART_InitStructure;USART_InitStructure.USART_BaudRate=9600;//波特率USART_InitStructure.USART_WordLength=USART_WordLength_8b;//8 位数据USART_InitStructure.USART_StopBits=USART_StopBits_1;//1 位停止位USART_InitStructure.USART_Parity=USART_Parity_No;//无奇偶校验USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;//无硬件流控USART_InitStructure.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;//收+发USART_Init(USART3,&USART_InitStructure);//3. NVIC 配置NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel=USART3_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0x02;NVIC_InitStructure.NVIC_IRQChannelSubPriority=0x00;NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_Init(&NVIC_InitStructure);//开中断——RXNE 触发USART_ITConfig(USART3,USART_IT_RXNE,ENABLE);//开启串口3USART_Cmd(USART3,ENABLE);}对比 USART1 的初始化,只有两处不同:
- 时钟总线:
RCC_APB1PeriphClockCmd(USART3 在 APB1); - 引脚复用:
GPIO_AF_USART3+ PB10/PB11。
其余结构完全一样——这就是标准库的"模板化"魅力。
4.3 USART3 中断:收到数据转发到串口 1
voidUSART3_IRQHandler(void){uint16_tdata;//USART3 收到数据if(USART_GetFlagStatus(USART3,USART_FLAG_RXNE)==SET){data=USART_ReceiveData(USART3);//转发到串口 1 打印(走 printf 重定向)printf("USART3 RX: 0x%02X (%c)\r\n",data,data);}USART_ClearITPendingBit(USART3,USART_IT_RXNE);}说明:这里在中断里直接 printf 是演示用的写法,方便一眼看到效果。实际项目中请遵循 3.5 节的原则——中断里只存数据、置标志,把 printf 挪到主循环里执行。本文示例波特率低(9600)数据量小看不出问题,一旦数据密集,printf 的耗时会导致后面的中断排队甚至丢字节。
这样外部模块(GPS、蓝牙等)发来的数据,就能在电脑串口助手上实时看到了——串口 3 当"耳朵",串口 1 当"嘴巴"。
五、主函数:把两路串口跑起来
#include"stm32f4xx.h"#include"uart.h"intmain(void){//初始化两路串口UART_Init_Config();//USART1: 115200UART3_Init_Config();//USART3: 9600printf("STM32F407 USART Demo Start!\r\n");printf("USART1: 115200bps, USART3: 9600bps\r\n");while(1){//主循环里可以干别的活//中断里已经处理了回显和转发}}六、常见坑与排查清单
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 全是乱码 | 波特率不一致 / 时钟配置错 | 核对收发双方波特率;检查 USART1 是否误用 APB1 时钟 |
| printf 卡死 | 没勾 MicroLIB / 没 include stdio.h / 没加 \r\n | 勾选 Use MicroLIB;补 include;补换行 |
| 串口完全没反应 | 引脚复用没配 / 时钟没开 | 检查 GPIO_PinAFConfig 和 RCC 时钟使能 |
| 中断不触发 | NVIC 没使能 / 没开 RXNE 中断 | 检查 NVIC_Init 和 USART_ITConfig |
| 收到数据但回显乱码 | 发送和接收波特率不一致 | 确认两路串口各自波特率,别混用 |
| USART3 起不来 | 时钟总线开错了(用了 APB2) | USART3 必须用RCC_APB1PeriphClockCmd |
七、总结
串口通信的核心就三件事:
- 开时钟——注意 USART1 在 APB2、USART3 在 APB1,总线别搞错;
- 配 GPIO 复用——PA9/PA10 给 USART1,PB10/PB11 给 USART3;
- 收发靠标志位——发送等 TXE,接收看 RXNE(中断)。
标准库的套路高度统一:开时钟 → 配 GPIO → 配外设 → 配 NVIC → 使能。把 USART1 跑通,USART3 就是改三个地方的事。串口通了,你的嵌入式调试之路才算真正开始。