当前位置: 首页 > news >正文

STM32 USART 标准库完全指南——从串口发送到Printf重定向

写在最前:为什么是USART?

如果说GPIO是STM32的"手脚",那USART就是它的"嘴巴和耳朵"——USART让单片机能够"说话"和"倾听"。

USART(Universal Synchronous Asynchronous Receiver Transmitter,通用同步异步收发器)最常用的形态就是串口。它的意义在于:

  1. 调试利器:当你不知道程序跑到了哪里、变量的值是什么时,串口打印是最好的"显微镜"

  2. 通信基础:GPS模块、蓝牙模块、WiFi模块、很多传感器都通过串口通信

  3. 简单可靠:只需要两根线(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常用引脚映射(不同型号可能不同,以手册为准):

USARTTX引脚RX引脚
USART1PA9(默认)/ PB6(重映射)PA10(默认)/ PB7(重映射)
USART2PA2PA3
USART3PB10PB11

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

直接写入想要的波特率数值,如9600115200921600

⚠️注意:波特率不能无限高,它受系统时钟限制。一般115200是最常用的"黄金波特率"——速度够快且稳定。

参数2:USART_WordLength— 数据位长度
参数值说明
USART_WordLength_8b8位数据(最常用)
USART_WordLength_9b9位数据(较少用,用于多机通信等)
参数3:USART_StopBits— 停止位个数
参数值说明
USART_StopBits_11位停止位(最常用)
USART_StopBits_0_50.5位停止位
USART_StopBits_22位停止位
USART_StopBits_1_51.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_CTSRTS和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 → 移位寄存器 → TXTXE发送DRDR数据转入移位寄存器时
接收RX → 移位寄存器 → DR → CPURXNE接收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操作步骤

  1. 点击魔术棒(Options for Target)

  2. 切换到Target标签页

  3. Code Generation区域,勾选Use MicroLIB

  4. 点击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使能/失能USARTENABLE/DISABLE
USART_ITConfig使能/失能指定中断USART_IT_RXNE/TXE/TC等,ENABLE/DISABLE
USART_DMACmd使能/失能DMA传输USART_DMAReq_Tx/RxENABLE/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_RXNEUSART_IT_TXEUSART_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_FLOATINGIPU

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()(一般启动文件已开)


九、总结

今天我们学到了:

  1. USART的本质:串行通信的一种方式,用一根TX线发送,一根RX线接收

  2. 数据帧格式:起始位 + 数据位 + 校验位(可选)+ 停止位

  3. 编程五步走:时钟 → GPIO → USART参数 → 使能 → 收发 ; 数据收发流程

  4. 三种收发方式

    • 阻塞发送(最常用,简单可靠)

    • 阻塞接收(只适合简单测试)

    • 中断接收(实际项目首选)

  5. printf重定向:让串口打印像电脑编程一样方便

http://www.jsqmd.com/news/1403682/

相关文章:

  • Vue3 构建产物过大:先查副作用声明、导入方式和环境变量
  • AI模型自动化构建与部署实战:从MLOps原理到Oumi应用
  • Syncthing for Android 完整上手指南:三步实现不经过网盘的免费文件同步
  • 解决MSSTDFMT.DLL注册表缺失:从原理到实践的完整指南
  • Claude Code:轻量化代码协作工具的核心功能与实践
  • 玄琨GEO技术架构深度解析:从核心引擎到数据流闭环(产品技术篇) - 汇聚至此
  • Android 11 定制设置系统默认语言列表(en-US + zh-CN)与“添加语言“建议项
  • jqGrid经典用法全解析:从数据驱动到企业级表格实践
  • 2026年8月厦门外墙漏水维修防水公司推荐,高层高空渗水修缮避坑指南 - 聪居到家
  • 2026湛江泡沫混凝土楼顶保温电话施工队道路软基处理,泡沫轻质土改良松软土层?-萧昇建筑材料 - 行业甄选汇
  • AI+形式化验证实战:从Lean环境搭建到数学证明自动化
  • 基于DiFy、FastGPT和MaxKB构建企业级AI应用全栈方案
  • C++质数筛算法详解:从埃氏筛到欧拉筛的实战指南
  • 从CUDA Tile视角解析大模型算力核心:GEMM并行优化原理与实践
  • 移动硬盘格式选择指南:NTFS、exFAT、APFS、FAT32跨系统兼容性与数据安全实战
  • Arduino智慧交通沙盘:从零搭建物联网科创项目全攻略
  • 单仁牛商玄琨GEO:GEO生成式引擎优化的核心原理与技术架构解析(原理解析篇) - 汇聚至此
  • VSCode开发Java与SpringBoot:从环境配置到疑难排错实战指南
  • 2026年3款主流短视频总结软件测评3个核心维度帮你选对好用工具
  • 2026年新消息:伊春彩色鹅卵石规格深耕卵石业,老友常相伴-弘源达建材 - 行业甄选汇
  • DS4Android开源项目:安卓设备连接PS4手柄的原理、配置与实战指南
  • Windows 10管理员权限失效与用户名修改难题的深度解析与实战修复
  • 笔记本摄像头故障排查全攻略:从驱动修复到注册表清理
  • 优化.NET开发环境:迁移NuGet全局包文件夹释放C盘空间
  • Agent 故障复盘:按状态机还原工具调用链
  • 从零搭建现代化AI实验室:技术栈、工具链与RAG实战指南
  • 2026年新消息:衢州广场游乐车工厂实地展厅可看样,选款购机心里更敞亮-山东童星游乐设备厂 - 行业甄选汇
  • 安卓文件同步避坑实战:关于 Syncthing for Android,新手最想弄清的5个问题
  • Harness 架构智能体平台 Policy Engine 高风险操作安全控制方案
  • 0458-Bomb-道具和效果