STM32 进阶封神之路(十二):串口实战全攻略 ------ 发送 / 接收 / 中断 /printf 重定向(库函数 + 寄存器)
上一篇我们吃透了串口通信的底层原理,这一篇就进入核心实战环节!基于 STM32F103 的 USART 外设,从串口初始化配置、单字节 / 字符串发送、查询式接收,到中断非阻塞接收、printf 重定向,全程拆解每一步操作,让你真正实现 "STM32 与电脑串口双向通信",彻底掌握串口的全场景应用!
本文基于实战资料,所有代码均以 "USART1+115200bps+8N1" 为标准配置,库函数与寄存器双版本覆盖,新手可直接照搬,进阶者可深挖底层逻辑!
一、STM32 串口核心资源与硬件连接
1. STM32F103 串口资源分布
STM32F103 系列搭载 3 个 USART(通用同步异步收发器)和 2 个 UART(仅异步),核心资源如下:
表格
| 串口型号 | 挂载总线 | TX 引脚 | RX 引脚 | 核心特性 | 典型应用 |
|---|---|---|---|---|---|
| USART1 | APB2(72MHz) | PA9(复用推挽) | PA10(浮空输入) | 支持同步 / 异步、中断、DMA | 与电脑通信、高速数据传输 |
| USART2 | APB1(36MHz) | PA2 / PD5 | PA3 / PD6 | 支持中断、DMA | 与蓝牙 / WiFi 模块通信 |
| USART3 | APB1(36MHz) | PB10 / PC10 | PB11 / PC11 | 支持中断、DMA | 与 485 模块通信 |
实战选型:优先选择 USART1(APB2 总线,时钟频率高,传输速度快),本文以 USART1 为例。
2. 硬件连接(STM32 与电脑通信)
- 核心链路:STM32 USART1 → USB-TTL 模块(CH340G) → 电脑 USB 口;
- 具体接线:
- STM32 PA9(USART1_TX) → USB-TTL 模块 RX;
- STM32 PA10(USART1_RX) → USB-TTL 模块 TX;
- STM32 GND → USB-TTL 模块 GND;
- USB-TTL 模块 USB 口 → 电脑 USB 口;
- 关键注意:TX 与 RX 必须交叉连接(STM32 的 TX 接模块的 RX,反之亦然),共地是通信稳定的前提。
二、串口初始化配置(核心步骤,必掌握)
串口使用前必须完成 "时钟使能→GPIO 配置→串口参数配置→中断配置(可选)",以下是库函数与寄存器双版本实现。
1. 库函数版初始化(USART1,115200bps 8N1)
c
运行
#include "stm32f10x.h"
// USART1初始化:115200bps,8位数据位,无校验,1位停止位
void USART1_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
// 1. 使能USART1和GPIOA时钟(USART1挂载APB2,GPIOA挂载APB2)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 2. 配置GPIO引脚(PA9=TX,PA10=RX)
// 配置PA9为复用推挽输出(USART1_TX)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出(关键!)
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置PA10为浮空输入(USART1_RX)
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入(推荐)
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置USART1参数(8N1)
USART_InitStruct.USART_BaudRate = 115200; // 波特率115200
USART_InitStruct.USART_WordLength = USART_WordLength_8b; // 8位数据位
USART_InitStruct.USART_StopBits = USART_StopBits_1; // 1位停止位
USART_InitStruct.USART_Parity = USART_Parity_No; // 无校验
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; // 无硬件流控
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; // 收发模式
USART_Init(USART1, &USART_InitStruct);
// 4. 使能USART1
USART_Cmd(USART1, ENABLE);
}
2. 寄存器版初始化(底层逻辑拆解)
c
运行
#include "stm32f10x.h"
void USART1_Init(void) {
// 1. 使能USART1和GPIOA时钟(APB2ENR寄存器)
RCC->APB2ENR |= (1<<14) | (1<<2); // bit14=USART1,bit2=GPIOA
// 2. 配置GPIOA9(TX)为复用推挽输出
GPIOA->CRH &= ~(0x0F<<4); // 清除PA9配置(CRH对应PA8~PA15,PA9对应bit5~bit4)
GPIOA->CRH |= (0x0B<<4); // MODE9=11(50MHz),CNF9=10(复用推挽)
// 3. 配置GPIOA10(RX)为浮空输入
GPIOA->CRH &= ~(0x0F<<8); // 清除PA10配置(PA10对应bit9~bit8)
GPIOA->CRH |= (0x04<<8); // MODE10=00(输入),CNF10=01(浮空输入)
// 4. 配置USART1波特率(115200bps,APB2时钟72MHz)
// 波特率计算公式:USARTDIV = 时钟频率 / (16×波特率) = 72000000/(16×115200)≈39.0625
USART1->BRR = 0x271; // 39.0625 → 整数部分39=0x27,小数部分0.0625×16=1 → 0x271
// 5. 配置USART1参数(8N1)
USART1->CR1 &= ~(1<<12); // M=0(8位数据位)
USART1->CR1 &= ~(1<<10); // PCE=0(无校验)
USART1->CR2 &= ~(3<<12); // STOP=00(1位停止位)
// 6. 使能USART1收发功能
USART1->CR1 |= (1<<2) | (1<<3); // RE=1(接收使能),TE=1(发送使能)
// 7. 使能USART1
USART1->CR1 |= (1<<13); // UE=1(USART使能)
}
3. 初始化核心要点(避坑关键)
- GPIO 模式:TX 必须配置为
GPIO_Mode_AF_PP(复用推挽输出),不能用普通输出模式(否则无串口信号输出); - 波特率计算:USART1 挂载 APB2(72MHz),波特率 = 72MHz/(16×USARTDIV),115200bps 对应的 USARTDIV=39.0625,
BRR=0x271; - 时钟使能:USART1 属于 APB2 总线外设,需调用
RCC_APB2PeriphClockCmd,而非 APB1; - 引脚复用:PA9/PA10 默认复用为 USART1_TX/RX,无需额外配置复用寄存器(库函数自动处理)。
三、串口发送实战:单字节 + 字符串 + printf 重定向
串口发送是最常用的功能(如调试打印、数据上传),核心是 "等待发送数据寄存器为空→写入数据",以下是三种发送场景的实现。
1. 单字节发送(库函数 + 寄存器)
(1)库函数版:USART_SendData
c
运行
// 发送单个字节
void USART1_SendByte(uint8_t data) {
// 等待发送数据寄存器(TDR)为空
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
// 写入数据到发送寄存器
USART_SendData(USART1, data);
// 等待发送完成(可选,确保数据发送完毕)
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}
(2)寄存器版:直接操作TDR寄存器
c
运行
void USART1_SendByte(uint8_t data) {
// 等待TXE位(bit7)置1(发送数据寄存器为空)
while(!(USART1->SR & (1<<7)));
// 写入数据到TDR寄存器(DR寄存器)
USART1->DR = data;
// 等待TC位(bit6)置1(发送完成)
while(!(USART1->SR & (1<<6)));
}
2. 字符串发送(基于单字节封装)
c
运行
// 发送字符串(以'\0'为结束标志)
void USART1_SendString(uint8_t *str) {
while(*str != '\0') {
USART1_SendByte(*str);
str++;
}
// 可选:发送换行符(\r\n),电脑串口助手自动换行
USART1_SendByte('\r');
USART1_SendByte('\n');
}
3. printf 重定向(调试打印神器)
通过重定向fputc函数,可直接使用printf打印调试信息(如变量值、程序状态),无需手动调用发送函数。
(1)重定向实现(需包含stdio.h)
c
运行
#include <stdio.h>
// 重定向fputc函数,printf输出到USART1
int fputc(int ch, FILE *f) {
// 调用单字节发送函数
USART1_SendByte((uint8_t)ch);
return ch;
}
(2)工程配置(关键!避免编译报错)
- 点击 KEIL 工具栏 "Options for Target"→"Target" 选项卡;
- 勾选 "Use MicroLIB"(启用微型标准库,支持 printf 重定向);
- 点击 "OK",编译时自动链接标准库函数。
(3)使用示例
c
运行
int main(void) {
uint16_t temp = 255;
float voltage = 3.3f;
USART1_Init(); // 初始化USART1
while(1) {
// 发送字符串
USART1_SendString("Hello STM32 USART!");
// printf打印变量
printf("temp = %d, voltage = %.2fV\r\n", temp, voltage);
// 延时1秒
delay_ms(1000);
}
}
(4)运行效果
电脑串口助手(如 SecureCRT、SSCOM)配置 115200bps 8N1,会周期性接收:
plaintext
Hello STM32 USART!
temp = 255, voltage = 3.30V
四、串口接收实战:查询式 + 中断式(非阻塞)
串口接收分为 "查询阻塞式" 和 "中断非阻塞式",查询式适用于简单场景,中断式不占用 CPU 资源,是实战首选。
1. 查询阻塞式接收(简单场景,少用)
核心逻辑:循环查询接收数据寄存器(RDR)是否有数据,有则读取,无则等待(阻塞主程序)。
库函数版实现
c
运行
// 查询接收单个字节(阻塞,超时返回0)
uint8_t USART1_ReceiveByte_Block(uint32_t timeout) {
uint32_t tick = 0;
// 等待接收数据寄存器(RDR)非空
while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET) {
tick++;
if(tick > timeout) {
return 0; // 超时返回0
}
}
// 读取接收数据
return (uint8_t)USART_ReceiveData(USART1);
}
// 主函数测试
int main(void) {
uint8_t rx_data;
USART1_Init();
USART1_SendString("USART1 Ready!");
while(1) {
// 等待接收数据(超时1000000次循环)
rx_data = USART1_ReceiveByte_Block(1000000);
if(rx_data != 0) {
// 回显接收的数据
USART1_SendString("Receive: ");
USART1_SendByte(rx_data);
USART1_SendByte('\r');
USART1_SendByte('\n');
}
}
}
优缺点
- 优点:代码简单,无需配置中断;
- 缺点:阻塞主程序,期间无法执行其他任务(如 LED 闪烁),仅适用于低要求场景。
2. 中断非阻塞式接收(实战首选)
核心逻辑:接收数据时触发中断,在中断服务函数中读取数据,主程序无阻塞,可并行执行其他任务。
(1)库函数版完整实现
c
运行
#include "stm32f10x.h"
#include <stdio.h>
#define RX_BUFFER_SIZE 128 // 接收缓冲区大小
uint8_t rx_buffer[RX_BUFFER_SIZE]; // 接收缓冲区
uint16_t rx_index = 0; // 缓冲区索引
// USART1初始化(含中断配置)
void USART1_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct;
USART_InitTypeDef USART_InitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 2. 配置GPIO
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置USART1参数
USART_InitStruct.USART_BaudRate = 115200;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(USART1, &USART_InitStruct);
// 4. 配置USART1接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); // 使能接收数据中断
// 5. 配置NVIC(中断优先级)
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级1
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; // 响应优先级0
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
// 6. 使能USART1
USART_Cmd(USART1, ENABLE);
}
// USART1中断服务函数
void USART1_IRQHandler(void) {
uint8_t rx_data;
// 检查接收数据中断标志位
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) {
// 读取接收数据
rx_data = (uint8_t)USART_ReceiveData(USART1);
// 数据存入缓冲区(循环缓冲区,防止溢出)
if(rx_index < RX_BUFFER_SIZE) {
rx_buffer[rx_index++] = rx_data;
// 回显数据(可选)
USART1_SendByte(rx_data);
} else {
rx_index = 0; // 缓冲区溢出,重置索引
}
// 清除中断标志位(库函数自动清除,寄存器版需手动清除)
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
// 读取接收缓冲区数据(非阻塞)
uint16_t USART1_ReadBuffer(uint8_t *buf, uint16_t len) {
uint16_t count = 0;
while(rx_index > 0 && count < len) {
buf[count++] = rx_buffer[--rx_index];
}
return count;
}
// 单字节发送函数(printf重定向用)
void USART1_SendByte(uint8_t data) {
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, data);
while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}
// printf重定向
int fputc(int ch, FILE *f) {
USART1_SendByte((uint8_t)ch);
return ch;
}
// 主函数测试(非阻塞接收+LED闪烁)
int main(void) {
uint8_t buf[RX_BUFFER_SIZE];
uint16_t len;
USART1_Init();
printf("USART1 Interrupt Receive Ready!\r\n");
// 配置PB0为推挽输出(LED)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
while(1) {
// 非阻塞读取接收数据
len = USART1_ReadBuffer(buf, RX_BUFFER_SIZE);
if(len > 0) {
// 打印接收的数据
printf("Receive %d bytes: ", len);
for(uint16_t i=0; i<len; i++) {
printf("%02X ", buf[i]);
}
printf("\r\n");
}
// 并行执行LED闪烁(无阻塞)
GPIO_SetBits(GPIOB, GPIO_Pin_0);
delay_ms(500);
GPIO_ResetBits(GPIOB, GPIO_Pin_0);
delay_ms(500);
}
}
(2)寄存器版中断接收核心代码
c
运行
// USART1中断服务函数(寄存器版)
void USART1_IRQHandler(void) {
uint8_t rx_data;
// 检查RXNE中断标志位(bit5)
if(USART1->SR & (1<<5)) {
// 读取接收数据
rx_data = USART1->DR;
// 存入缓冲区
if(rx_index < RX_BUFFER_SIZE) {
rx_buffer[rx_index++] = rx_data;
} else {
rx_index = 0;
}
// 寄存器版无需手动清除RXNE标志位(读取DR后自动清除)
}
}
核心优势
- 非阻塞:主程序可并行执行其他任务(如 LED 闪烁、传感器采集),不被接收操作阻塞;
- 高效:数据接收由中断触发,仅在有数据时占用 CPU 资源;
- 可扩展:通过循环缓冲区支持多字节连续接收,适配大数据量传输。
五、串口通信常见问题与避坑指南
1. 串口无输出 / 接收不到数据
高频原因与解决方案
- 原因 1:TX/RX 接反→数据传输方向错误;解决:交换 STM32 TX 与 USB-TTL 模块 RX 的接线;
- 原因 2:GPIO 模式配置错误(TX 未设为复用推挽);解决:TX 引脚必须配置为
GPIO_Mode_AF_PP,而非GPIO_Mode_Out_PP; - 原因 3:时钟未使能(USART1 或 GPIOA 时钟未开启);解决:确认
RCC_APB2PeriphClockCmd已使能对应时钟; - 原因 4:波特率配置错误(如 USART1 按 APB1 时钟计算);解决:USART1 挂载 APB2(72MHz),波特率计算需用 72MHz 时钟;
- 原因 5:未共地→信号干扰导致通信失败;解决:确保 STM32 与 USB-TTL 模块共地。
2. 通信乱码
高频原因与解决方案
- 原因 1:波特率 / 数据位 / 校验位 / 停止位不匹配;解决:确保 STM32 与电脑串口助手配置一致(115200bps 8N1);
- 原因 2:时钟频率误差过大(如外部晶振不是 8MHz);解决:核对
system_stm32f10x.c中的系统时钟配置,确保 USART1 时钟为 72MHz; - 原因 3:信号干扰(导线过长、靠近电源模块);解决:使用短于 10cm 的优质杜邦线,远离电源电路和高频信号;
- 原因 4:printf 重定向未启用 MicroLIB;解决:勾选 KEIL 的 "Use MicroLIB" 选项。
3. 中断接收不响应
高频原因与解决方案
- 原因 1:未使能 USART 接收中断;解决:调用
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); - 原因 2:NVIC 未配置或优先级错误;解决:正确配置 NVIC 中断通道和优先级,确保中断使能;
- 原因 3:中断服务函数名称错误;解决:中断服务函数名称必须为
USART1_IRQHandler(启动文件弱定义名称); - 原因 4:未清除中断标志位;解决:寄存器版需读取 DR 寄存器清除 RXNE 标志位,库函数版调用
USART_ClearITPendingBit。
六、总结:串口实战核心要点与进阶方向
1. 核心要点回顾
- 串口初始化四步走:时钟使能→GPIO 配置(TX 复用推挽,RX 浮空输入)→串口参数配置→中断配置(可选);
- 发送核心:等待 TXE 位为空→写入数据,printf 重定向需启用 MicroLIB;
- 接收核心:查询式适用于简单场景,中断式非阻塞是实战首选,需配合缓冲区存储数据;
- 避坑关键:TX/RX 交叉连接、共地、GPIO 复用模式、波特率计算正确。
2. 进阶学习方向
- DMA 接收:使用 DMA 实现串口数据高速接收,彻底解放 CPU;
- 多串口通信:同时使用 USART1(与电脑)和 USART2(与蓝牙模块),实现数据转发;
- 串口协议:自定义串口通信协议(如帧头 + 数据 + 校验位),实现可靠数据传输;
- 无线通信:通过串口对接蓝牙、WiFi、4G 模块,实现远程数据传输。
串口是 STM32 开发的 "万能接口",掌握发送、接收、中断、printf 重定向后,你可以实现调试打印、设备通信、数据上传等几乎所有嵌入式通信场景。下一篇我们将学习 I2C 通信,实现 STM32 与 OLED 屏幕、EEPROM 等外设的交互!