STM32 USART 标准库完全指南——从串口发送到Printf重定向
写在最前:为什么是USART?
如果说GPIO是STM32的"手脚",那USART就是它的"嘴巴和耳朵"——USART让单片机能够"说话"和"倾听"。
USART(Universal Synchronous Asynchronous Receiver Transmitter,通用同步异步收发器)最常用的形态就是串口。它的意义在于:
调试利器:当你不知道程序跑到了哪里、变量的值是什么时,串口打印是最好的"显微镜"
通信基础:GPS模块、蓝牙模块、WiFi模块、很多传感器都通过串口通信
简单可靠:只需要两根线(TX和RX)就能实现双向通信
今天我们就把USART从里到外学透。
一、USART是什么?——从"对话"说起
1.1 最朴素的理解
想象两个人打电话:
一个人说话(发送),一个人听(接收)
他们事先约定好语速(波特率)、语言格式(数据帧格式)
这样才能保证对方听得懂
USART串口通信就是这样:两个设备通过一根TX线(发送)和一根RX线(接收)互相"对话"。发送方按约定的节奏把数据一位一位地发出去,接收方按相同的节奏一位一位地收进来。
1.2 关键术语一网打尽
在开始编程之前,先搞清楚这几个"串口黑话":
| 术语 | 通俗解释 | 常见值 |
|---|---|---|
| 波特率(Baud Rate) | 每秒传输的符号个数。在串口通信中,1个符号=1个bit,所以波特率=比特率。可以理解为"说话速度" | 9600、115200、921600 |
| 起始位(Start Bit) | 数据传输开始的"信号"。拉低数据线表示"我要开始发数据了" | 1位(固定) |
| 数据位(Data Bits) | 真正要传输的数据的位数。就像每个字由几个字母组成 | 8位(最常用)或9位 |
| 停止位(Stop Bits) | 数据传输结束的"信号"。拉高数据线表示"这一字节发完了" | 1位(最常用)或2位 |
| 校验位(Parity Bit) | 检错机制。通过额外加一位让数据中"1"的个数为奇数(奇校验)或偶数(偶校验) | 无校验(最常用)、奇校验、偶校验 |
一个完整数据帧的样子(以8位数据、无校验、1位停止位为例):
空闲状态:高电平 起始位:低电平(1位) 数据位:从低位到高位依次发送(8位) 停止位:高电平(1位) 空闲状态:回到高电平
💡记忆:串口传输的节奏就像摩斯电码——有规律的"高低变化"代表"0和1",收发双方用相同的"速度"(波特率)来理解这些变化。
二、USART的内部结构——从框图看本质
2.1 简化版框图
┌─────────────────────────────────────┐ │ USART │ │ ┌─────────┐ ┌──────────────┐ │ TX引脚 ◄─────────┼──┤ 发送 │ │ 波特率 │ │ │ │ 移位寄存器│◄──┤ 发生器 │ │ RX引脚 ◄─────────┼──┤ 接收 │ │ (波特率) │ │ │ │ 移位寄存器│ └──────────────┘ │ │ └─────────┘ │ │ ▲ ▲ │ │ │ │ │ │ ┌────┴────┴────┐ │ │ │ 数据寄存器 │ │ │ │ (DR寄存器) │ │ │ └─────────────┘ │ │ ▲ │ │ │ │ │ ┌────┴────┐ │ │ │ 控制逻辑 │ │ │ │ (CR1/CR2)│ │ │ └─────────┘ │ └─────────────────────────────────────┘ │ ▼ 与CPU总线连接
2.2 三大核心部件
① 波特率发生器:由内部时钟分频得到想要的波特率。F1系列用APB2(USART1)或APB1(USART2/3)时钟作为输入。
② 发送/接收移位寄存器:并行(CPU读写)转串行(引脚输出)的"翻译官"。CPU把一字节数据写入数据寄存器,移位寄存器再把它一位一位地"推"出去。
③ 数据寄存器(DR):CPU和USART之间"交接数据"的地方。你往USART_DR写数据,USART自动发送;你读USART_DR,就拿到接收到的数据。
三、标准库编程——五步走
3.1 编程五步骤
使用USART的标准流程:
text
1. 使能USART时钟和GPIO时钟 2. 配置GPIO为复用功能(TX推挽输出,RX浮空输入或上拉输入) 3. 配置USART参数(波特率、数据位、停止位、校验位等) 4. 使能USART 5. 在程序中发送或接收数据
3.2 第一步:使能时钟
c
#include "stm32f10x_rcc.h" // USART1挂在APB2总线上,GPIOA也挂在APB2 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // 注:USART2和USART3挂在APB1总线上,用RCC_APB1PeriphClockCmd // RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
⚠️重要:不同USART挂载的总线不同!
USART1:APB2(最高72MHz)
USART2、USART3:APB1(最高36MHz)
3.3 第二步:配置GPIO为复用功能
USART的TX和RX引脚需要配置为复用推挽输出(TX)和复用功能输入(RX):
c
#include "stm32f10x_gpio.h" GPIO_InitTypeDef GPIO_InitStructure; // 配置TX引脚(PA9):复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置RX引脚(PA10):浮空输入或上拉输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入(推荐) // 或 GPIO_Mode_IPU(上拉输入,噪声较大的环境) GPIO_Init(GPIOA, &GPIO_InitStructure);
USART1常用引脚映射(不同型号可能不同,以手册为准):
| USART | TX引脚 | RX引脚 |
|---|---|---|
| USART1 | PA9(默认)/ PB6(重映射) | PA10(默认)/ PB7(重映射) |
| USART2 | PA2 | PA3 |
| USART3 | PB10 | PB11 |
3.4 第三步:配置USART参数
c
#include "stm32f10x_usart.h" 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_Tx | USART_Mode_Rx; // 同时使能发送和接收 USART_Init(USART1, &USART_InitStructure);
各参数的可选值详解:
参数1:USART_BaudRate
直接写入想要的波特率数值,如9600、115200、921600。
⚠️注意:波特率不能无限高,它受系统时钟限制。一般115200是最常用的"黄金波特率"——速度够快且稳定。
参数2:USART_WordLength— 数据位长度
| 参数值 | 说明 |
|---|---|
USART_WordLength_8b | 8位数据(最常用) |
USART_WordLength_9b | 9位数据(较少用,用于多机通信等) |
参数3:USART_StopBits— 停止位个数
| 参数值 | 说明 |
|---|---|
USART_StopBits_1 | 1位停止位(最常用) |
USART_StopBits_0_5 | 0.5位停止位 |
USART_StopBits_2 | 2位停止位 |
USART_StopBits_1_5 | 1.5位停止位 |
参数4:USART_Parity— 校验位
| 参数值 | 说明 |
|---|---|
USART_Parity_No | 无校验(最常用) |
USART_Parity_Even | 偶校验 |
USART_Parity_Odd | 奇校验 |
参数5:USART_HardwareFlowControl— 硬件流控
| 参数值 | 说明 |
|---|---|
USART_HardwareFlowControl_None | 无流控(最常用) |
USART_HardwareFlowControl_RTS | 使能RTS流控 |
USART_HardwareFlowControl_CTS | 使能CTS流控 |
USART_HardwareFlowControl_RTS_CTS | RTS和CTS都使能 |
💡 什么是硬件流控?就是通过额外的引脚(RTS/CTS)通知对方"我准备好了,你发吧"或"我还没处理完,稍等"。防止数据丢失。
参数6:USART_Mode— 工作模式
| 参数值 | 说明 |
|---|---|
USART_Mode_Tx | 仅使能发送 |
USART_Mode_Rx | 仅使能接收 |
USART_Mode_Tx | USART_Mode_Rx | 收发都使能(最常用) |
3.5 第四步:使能USART
c
USART_Cmd(USART1, ENABLE);
3.6 第五步:发送和接收
发送单个字节(阻塞方式)
c
void USART_SendByte(USART_TypeDef* USARTx, uint8_t data) { // 等待发送数据寄存器为空(即上一个数据已发送完成) while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET); // 发送数据 USART_SendData(USARTx, data); }发送字符串
c
void USART_SendString(USART_TypeDef* USARTx, char* str) { while (*str) { USART_SendByte(USARTx, *str++); } }接收单个字节(阻塞方式)
c
uint8_t USART_ReceiveByte(USART_TypeDef* USARTx) { // 等待接收数据寄存器非空(即收到数据) while (USART_GetFlagStatus(USARTx, USART_FLAG_RXNE) == RESET); // 读取数据 return USART_ReceiveData(USARTx); }发送数据流程(CPU → TX引脚)
一句话概括:CPU把数据放到"发货窗口",硬件自动把它从TX引脚"搬"出去。
text
CPU写DR → 数据到发送DR → 自动转移到发送移位寄存器 → 按波特率从TX一位位发出
关键点:
TXE标志(发送DR空):发送前必须等它为1,否则新数据会覆盖旧数据数据从DR到移位寄存器是硬件自动完成的,不需要CPU参与
每一位的发送速度由波特率发生器控制
代码对应:
c
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); // 等窗口空 USART_SendData(USART1, data); // 放数据到窗口
接收数据流程(RX引脚 → CPU)
一句话概括:硬件从RX引脚一位位收,拼成一字节放到"收货窗口",CPU来取。
text
RX收到起始位 → 按波特率采样数据位 → 拼成一字节到接收DR → RXNE置位 → CPU读DR取走
关键点:
RXNE标志(接收DR非空):收到数据后置1,表示"窗口有货"CPU读DR时,硬件自动清除RXNE标志
如果上一字节没被读走,新数据又来了 →溢出错误(ORE)→ 数据丢失
代码对应:
c
while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET); // 等有数据 data = USART_ReceiveData(USART1); // 从窗口取货
速记对照表
| 方向 | 流程 | 关键标志 | 标志含义 | 何时置位 |
|---|---|---|---|---|
| 发送 | CPU → DR → 移位寄存器 → TX | TXE | 发送DR空了 | DR数据转入移位寄存器时 |
| 接收 | RX → 移位寄存器 → DR → CPU | RXNE | 接收DR非空 | 收到一字节存入DR时 |
核心理解:DR是CPU和USART硬件之间的"交接台"。发送时CPU把货放上交接台,硬件自动搬走;接收时硬件把货放上交接台,CPU来取。
四、实战:串口发送Hello World
4.1 硬件连接
以STM32F103C8T6为例,USART1的默认引脚是:
TX(PA9)→ 连接到USB转TTL模块的RX
RX(PA10)→ 连接到USB转TTL模块的TX
GND→ 连接到USB转TTL模块的GND
⚠️注意交叉连接:STM32的TX接模块的RX,STM32的RX接模块的TX(就像两个人面对面,你的右手握对方的左手)。
4.2 完整代码
c
#include "stm32f10x.h" // 硬件定义 #define USARTx USART1 #define USARTx_CLK RCC_APB2Periph_USART1 #define USARTx_GPIO_PORT GPIOA #define USARTx_GPIO_CLK RCC_APB2Periph_GPIOA #define USARTx_TX_PIN GPIO_Pin_9 #define USARTx_RX_PIN GPIO_Pin_10 // 发送单个字节 void USART_SendByte(USART_TypeDef* USARTx, uint8_t data) { while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET); USART_SendData(USARTx, data); } // 发送字符串 void USART_SendString(USART_TypeDef* USARTx, char* str) { while (*str) { USART_SendByte(USARTx, *str++); } } // 简单延时 void delay(uint32_t count) { while (count--); } int main(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; // 1. 使能时钟 RCC_APB2PeriphClockCmd(USARTx_GPIO_CLK | USARTx_CLK, ENABLE); // 2. 配置GPIO:TX复用推挽,RX浮空输入 GPIO_InitStructure.GPIO_Pin = USARTx_TX_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(USARTx_GPIO_PORT, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = USARTx_RX_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(USARTx_GPIO_PORT, &GPIO_InitStructure); // 3. 配置USART参数 USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_Init(USARTx, &USART_InitStructure); // 4. 使能USART USART_Cmd(USARTx, ENABLE); // 5. 主循环:定时发送 while (1) { USART_SendString(USARTx, "Hello STM32!\r\n"); delay(1000000); } }4.3 代码解读
关键点1:等待TXE标志
c
while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET);
TXE(Transmit Data Register Empty)标志表示"发送数据寄存器空了,可以往里面写新数据了"。如果不等它,直接连续发送会导致数据覆盖。
关键点2:\r\n换行
在串口助手中,\r(回车)和\n(换行)一起用才能正确换行。只用\n在某些终端会显示为只换行不回车。
五、进阶:重定向printf到串口
如果每次都用USART_SendString来打印,写起来太麻烦。我们可以把标准库的printf函数重定向到串口,这样就能像写电脑程序一样方便地打印了。
5.1 实现原理
printf最终会调用底层的一个函数来输出单个字符。我们只需实现这个函数——在标准库中叫fputc。
5.2 完整实现
c
// 需要包含标准库头文件 #include <stdio.h> // 重定向fputc:printf最终会调用这个函数 int fputc(int ch, FILE *f) { // 将字符ch发送到串口 while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }5.3 工程配置(关键!)
使用printf需要勾选"Use MicroLIB"(微库),否则会报错:
Keil MDK操作步骤:
点击魔术棒(Options for Target)
切换到Target标签页
在Code Generation区域,勾选Use MicroLIB
点击OK保存
💡MicroLIB是ARM专门为嵌入式设备裁剪的C标准库,体积小,适合单片机使用。
5.4 使用示例
配置完成后,就可以直接用了:
c
int main(void) { // ... USART初始化代码同上 ... int count = 0; while (1) { printf("count = %d, 温度 = %.2f°C\r\n", count++, 26.5); delay(1000000); } }串口助手会输出:
text
count = 0, 温度 = 26.50°C count = 1, 温度 = 26.50°C count = 2, 温度 = 26.50°C ...
六、进阶:中断方式接收(不阻塞CPU)
阻塞方式接收(while循环等标志位)有一个致命问题:CPU被卡住,什么事都干不了。在实际项目中,我们更常用中断方式接收。
6.1 中断接收的编程步骤
text
1. 初始化USART(同上) 2. 使能USART接收中断 3. 配置NVIC(嵌套向量中断控制器) 4. 编写中断服务函数
6.2 完整代码实现
c
#include "stm32f10x.h" uint8_t received_data = 0; volatile uint8_t new_data_flag = 0; // volatile防止被编译器优化 int main(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; // 1~3步:使能时钟、配置GPIO、配置USART参数(同前面,略) // ... // 使能USART接收中断 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 配置NVIC NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); // 使能USART USART_Cmd(USART1, ENABLE); while (1) { if (new_data_flag) { new_data_flag = 0; // 处理接收到的数据:回显(发回去) printf("收到: %c\r\n", received_data); } // 在这里CPU可以放心干别的事 } } // USART1中断服务函数(中断向量表里固定名称) void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { received_data = USART_ReceiveData(USART1); new_data_flag = 1; // 清除中断标志(读取DR寄存器会自动清除RXNE标志) } }6.3 中断方式的核心优势
| 方式 | CPU占用 | 适用场景 |
|---|---|---|
| 阻塞接收(轮询) | 100%等待 | 简单测试程序 |
| 中断接收 | 仅当有数据时才处理 | 实际项目开发 |
七、标准库USART函数完整速查
7.1 初始化与配置
| 函数 | 功能 | 参数说明 |
|---|---|---|
USART_Init | 初始化USART | 传入USART_InitTypeDef结构体指针 |
USART_Cmd | 使能/失能USART | ENABLE/DISABLE |
USART_ITConfig | 使能/失能指定中断 | USART_IT_RXNE/TXE/TC等,ENABLE/DISABLE |
USART_DMACmd | 使能/失能DMA传输 | USART_DMAReq_Tx/Rx,ENABLE/DISABLE |
7.2 数据收发
| 函数 | 功能 | 说明 |
|---|---|---|
USART_SendData | 发送一字节 | 写入DR寄存器 |
USART_ReceiveData | 接收一字节 | 读取DR寄存器 |
7.3 标志位与状态
| 函数 | 功能 | 常用标志 |
|---|---|---|
USART_GetFlagStatus | 获取标志位状态 | USART_FLAG_TXE(发送寄存器空)、USART_FLAG_RXNE(接收寄存器非空)、USART_FLAG_TC(发送完成) |
USART_ClearFlag | 清除标志位 | 同上 |
USART_GetITStatus | 获取中断状态 | USART_IT_RXNE、USART_IT_TXE、USART_IT_TC |
USART_ClearITPendingBit | 清除中断挂起位 | 同上 |
7.4 常用标志位详解
| 标志位宏 | 含义 | 用途 |
|---|---|---|
USART_FLAG_TXE | 发送数据寄存器空 | 发送数据前等待此标志为SET,表示可以写新数据 |
USART_FLAG_TC | 发送完成 | 表示最后一个数据已从移位寄存器发出,可用于一帧数据发送完的确认 |
USART_FLAG_RXNE | 接收数据寄存器非空 | 收到数据后此标志置位,表示可以读取DR |
USART_FLAG_IDLE | 空闲线路检测 | 检测到线路空闲时置位,可用于判断一帧数据结束 |
USART_FLAG_ORE | 溢出错误 | 接收缓冲区满又有新数据时产生,需清除 |
USART_FLAG_NE | 噪声错误 | 信号受干扰时产生 |
USART_FLAG_FE | 帧错误 | 停止位不正确时产生 |
USART_FLAG_PE | 校验错误 | 校验位不对时产生 |
八、常见问题与避坑指南
Q1:串口助手没收到数据,或收到乱码?
| 可能原因 | 解决方法 |
|---|---|
| 波特率不一致 | 检查代码和串口助手设置的波特率是否一致 |
| TX/RX接反了 | 检查连接:STM32的TX接模块的RX,RX接模块的TX |
| 共地问题 | STM32的GND和USB转TTL模块的GND必须连在一起 |
| 时钟未开启 | 检查是否调用了RCC_APB2PeriphClockCmd使能USART时钟 |
| GPIO模式错误 | TX用AF_PP,RX用IN_FLOATING或IPU |
Q2:使用printf报错?
| 错误现象 | 解决方法 |
|---|---|
undefined symbol __stdout | 勾选"Use MicroLIB" |
undefined symbol fputc | 在代码中实现int fputc(int ch, FILE *f)函数 |
编译报错FILE未定义 | 包含头文件#include <stdio.h> |
Q3:接收到的数据是乱码?
检查波特率设置
检查数据位、停止位、校验位是否与串口助手一致
如果使用了
printf,检查fputc中的USART是否与初始化的是同一个
Q4:中断方式收不到数据?
检查
USART_ITConfig是否已调用检查NVIC是否已配置且使能
检查中断服务函数名称是否正确(F103系列USART1的中断函数名是
USART1_IRQHandler)检查总中断是否开启:
__enable_irq()(一般启动文件已开)
九、总结
今天我们学到了:
USART的本质:串行通信的一种方式,用一根TX线发送,一根RX线接收
数据帧格式:起始位 + 数据位 + 校验位(可选)+ 停止位
编程五步走:时钟 → GPIO → USART参数 → 使能 → 收发 ; 数据收发流程
三种收发方式:
阻塞发送(最常用,简单可靠)
阻塞接收(只适合简单测试)
中断接收(实际项目首选)
printf重定向:让串口打印像电脑编程一样方便
