STM32F407 标准库 USART 串口通信:中断收发 + printf 重定向
2026/9/14 23:39:12 网站建设 项目流程

一、USART 是什么

USART(通用同步/异步收发器)说白了就是两根线 + 一个约定:

发送方 TX ────────────► RX 接收方 接收方 RX ◄──────────── TX 发送方 (全双工:两边可以同时说)

一帧数据的格式(无校验、1 停止位):

┌──────┬──────────────────────┬────────┐ │起始位│ D0 D1 D2 ... D7 │ 停止位 │ │ 0 │ 8 位数据(低位在前) │ 1 │ └──────┴──────────────────────┴────────┘

双方约定好波特率(每秒多少个 bit),发送方按这个节奏把数据一位一位往线上放,接收方按同样的节奏采样,就能还原出数据。没有时钟线,全靠"对表"——这也是为什么收发双方波特率必须一致,差一点就全是乱码。

两个最重要的标志位(本篇的主角)

标志全称含义我们的用法
TXETransmit data register Empty发送数据寄存器空了,可以写下一个数据发送前死等它置 1
RXNEReceive data register Not Empty收到数据了,赶紧来读开中断,一来数据就进中断服务函数

记住这对标志位,串口收发的代码就都能看懂了。


二、硬件连接

我的开发板(STM32F407)上:

串口引脚连接用途
USART1PA9=TX, PA10=RX板载 CH340C 芯片 → USB Type-B 口 → 电脑主调试串口,115200
USART3PB10=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 就卡死:

  1. Keil 里勾选Target → Use MicroLIB

  2. 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 的初始化,只有两处不同

  1. 时钟总线:RCC_APB1PeriphClockCmd(USART3 在 APB1);
  2. 引脚复用: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

七、总结

串口通信的核心就三件事:

  1. 开时钟——注意 USART1 在 APB2、USART3 在 APB1,总线别搞错;
  2. 配 GPIO 复用——PA9/PA10 给 USART1,PB10/PB11 给 USART3;
  3. 收发靠标志位——发送等 TXE,接收看 RXNE(中断)。

标准库的套路高度统一:开时钟 → 配 GPIO → 配外设 → 配 NVIC → 使能。把 USART1 跑通,USART3 就是改三个地方的事。串口通了,你的嵌入式调试之路才算真正开始。

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

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

立即咨询