STM32 USART — 串口通信与 printf 重定向
开发板:野火 EBF STM32F103 Mini
固件库:HAL 库
对应例程:14-USART—串口通信 / USART1回显
参考源码:User/main.c、User/usart/bsp_debug_usart.c、User/usart/bsp_debug_usart.h、User/stm32f1xx_it.c
串口(UART/USART)是嵌入式开发中最重要的调试通道。没有屏幕的开发板,靠串口把运行日志打印到电脑上的串口助手,是定位问题的第一手段。本文带你配好 USART1,并学会用熟悉的 printf 输出信息。
一、功能简介
本例把 USART1 配置为 115200 波特率、8 位数据、无校验、1 位停止位(8-N-1),并使能接收中断:
- 程序启动后通过
printf 打印欢迎语;
- 通过自定义的
Usart_SendString() 发送字符串;
- 上位机串口助手发送的数据,会在
USART1_IRQHandler 中进入接收中断,实现"回显"。
核心知识点:
- USART 外设的 HAL 初始化套路(
UART_HandleTypeDef);
- TX/RX 引脚必须配置为复用推挽/复用输入;
- 用
fputc 重定向让 printf 走串口;
- 中断接收的基本框架。
二、硬件准备
| 项目 |
说明 |
| 开发板 |
野火 EBF STM32F103 Mini |
| 串口外设 |
USART1 |
| 引脚 |
TX → PA9,RX → PA10(复用功能 AFIO) |
| 连接方式 |
板载/外接 USB-TTL 转串口的 TX、RX 交叉连接 |
| 上位机 |
串口调试助手(如 SSCOM、Putty),波特率 115200 |
三、完整代码(带中文注释)
1. main.c(主程序)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#include "main.h"
#include "stm32f1xx.h"
#include "./usart/bsp_debug_usart.h"
#include "./led/bsp_led.h"
int main(void)
{
HAL_Init(); // 初始化 HAL 库(含 SysTick、中断分组)
/* 配置系统时钟为 72 MHz */
SystemClock_Config();
/* 初始化 USART1:115200 8-N-1,中断接收 */
DEBUG_USART_Config();
/* 调用 printf(已重定向到串口) */
printf("欢迎使用野火开发板\n");
/* 自定义函数方式发送字符串 */
Usart_SendString( (uint8_t *)"自定义函数输出:这是一个串口中断接收回显实验\n" );
while(1)
{
/* 主循环空闲,真正的接收工作在中断里完成 */
}
}
|
2. bsp_debug_usart.h(引脚与波特率定义)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
#ifndef __DEBUG_USART_H
#define __DEBUG_USART_H
#include "stm32f1xx.h"
#include <stdio.h>
/* 串口波特率 */
#define DEBUG_USART_BAUDRATE 115200
/* 引脚定义:USART1,TX=PA9,RX=PA10 */
#define DEBUG_USART USART1
#define DEBUG_USART_CLK_ENABLE() __HAL_RCC_USART1_CLK_ENABLE();
#define DEBUG_USART_RX_GPIO_PORT GPIOA
#define DEBUG_USART_RX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
#define DEBUG_USART_RX_PIN GPIO_PIN_10
#define DEBUG_USART_TX_GPIO_PORT GPIOA
#define DEBUG_USART_TX_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
#define DEBUG_USART_TX_PIN GPIO_PIN_9
#define DEBUG_USART_IRQ USART1_IRQn
void Usart_SendString(uint8_t *str);
void DEBUG_USART_Config(void);
int fputc(int ch, FILE *f);
int fgetc(FILE *f);
extern UART_HandleTypeDef UartHandle;
#endif /* __USART1_H */
|
3. bsp_debug_usart.c(配置、MSP、printf 重定向)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
#include "./usart/bsp_debug_usart.h"
UART_HandleTypeDef UartHandle;
/**
* @brief DEBUG_USART 工作模式配置:115200 8-N-1
*/
void DEBUG_USART_Config(void)
{
UartHandle.Instance = DEBUG_USART; // 选用 USART1
UartHandle.Init.BaudRate = DEBUG_USART_BAUDRATE; // 波特率 115200
UartHandle.Init.WordLength = UART_WORDLENGTH_8B; // 8 位数据
UartHandle.Init.StopBits = UART_STOPBITS_1; // 1 位停止位
UartHandle.Init.Parity = UART_PARITY_NONE; // 无校验
UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE; // 无硬件流控
UartHandle.Init.Mode = UART_MODE_TX_RX; // 收发全双工
HAL_UART_Init(&UartHandle); // 调用 HAL,它会进一步调用 HAL_UART_MspInit
/* 使能串口接收中断(RXNE:接收寄存器非空) */
__HAL_UART_ENABLE_IT(&UartHandle, UART_IT_RXNE);
}
/**
* @brief UART MSP 初始化:配置时钟、GPIO 复用、NVIC 中断
* @note 这是 HAL 的"弱函数"钩子,HAL_UART_Init 会自动调用它
*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef GPIO_InitStruct;
DEBUG_USART_CLK_ENABLE(); // 开 USART1 时钟
DEBUG_USART_RX_GPIO_CLK_ENABLE(); // 开 GPIOA 时钟
DEBUG_USART_TX_GPIO_CLK_ENABLE();
/* PA9 配置为复用推挽输出(TX) */
GPIO_InitStruct.Pin = DEBUG_USART_TX_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStruct);
/* PA10 配置为复用输入(RX) */
GPIO_InitStruct.Pin = DEBUG_USART_RX_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_AF_INPUT; // 复用输入
HAL_GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStruct);
/* 配置 NVIC:抢占优先级 0,子优先级 1 */
HAL_NVIC_SetPriority(DEBUG_USART_IRQ, 0, 1);
HAL_NVIC_EnableIRQ(DEBUG_USART_IRQ); // 使能 USART1 中断通道
}
/* 发送字符串:逐字节调用 HAL_UART_Transmit */
void Usart_SendString(uint8_t *str)
{
unsigned int k = 0;
do {
HAL_UART_Transmit(&UartHandle, (uint8_t *)(str + k), 1, 1000);
k++;
} while(*(str + k) != '\0');
}
/* 重定向 C 库 printf 到串口:链接器会把 printf 的底层输出指向这里 */
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&UartHandle, (uint8_t *)&ch, 1, 1000);
return (ch);
}
/* 重定向 C 库 scanf/getchar 到串口 */
int fgetc(FILE *f)
{
int ch;
HAL_UART_Receive(&UartHandle, (uint8_t *)&ch, 1, 1000);
return (ch);
}
|
4. stm32f1xx_it.c(中断服务函数,节选)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include "./usart/bsp_debug_usart.h"
/**
* @brief USART1 中断服务函数
* @note 芯片发生 USART1 中断时,硬件自动跳到这里
*/
void USART1_IRQHandler(void)
{
/* 交给 HAL 的统一中断处理入口,它会判断中断源并调用相应回调 */
HAL_UART_IRQHandler(&UartHandle);
/* 下面的回显逻辑通常写在回调 HAL_UART_RxCpltCallback 中:
在接收完成回调里再次开启接收,即可实现持续回显 */
}
|
四、代码分段讲解
1. TX/RX 为什么要"复用"?
USART 是片上外设,并不是 GPIO 直接产生的信号。要让 PA9/PA10 输出 USART 波形,必须把引脚配置为复用功能(AF),由内部外设"接管"这两个脚。TX 用复用推挽,RX 用复用输入。
2. HAL_UART_Init 会触发 HAL_UART_MspInit
这是 HAL 库的核心设计:“通用配置"和"平台相关配置"分离。
HAL_UART_Init() 只配置 USART 本身的参数(波特率等);
- 它会回调
HAL_UART_MspInit(),由你在里面开时钟、配 GPIO、配 NVIC。
好处是同一套 HAL_UART_Init 代码可以跨芯片复用,差异都收敛在 MSP 函数里。
3. printf 重定向原理
C 语言的 printf 最终会调用底层 _write/fputc 来输出一个字符。我们重写 fputc,让它把字符通过 HAL_UART_Transmit 发到串口,就能直接 printf("调试信息=%d\n", val)。注意工程需勾选 “Use MicroLIB”(Keil)或开启 -u _printf_float(GCC)以支持浮点打印。
4. 中断接收 vs 轮询接收
HAL_UART_Receive() 是阻塞轮询,CPU 干等;
- 开启
UART_IT_RXNE 中断后,每收到一字节硬件自动进 USART1_IRQHandler,在 HAL_UART_RxCpltCallback 回调里处理,CPU 不阻塞。回显实验就是趁这个时机把收到的字节再发回去。
五、实验现象
- 打开串口助手,选择正确串口号、波特率 115200、8-N-1、无流控;
- 开发板复位后,串口助手收到两行欢迎语:
1
2
|
欢迎使用野火开发板
自定义函数输出:这是一个串口中断接收回显实验
|
- 在串口助手发送区输入任意字符并发送,开发板会把相同字符回发回来(回显)。
六、常见问题
Q1:串口助手收不到任何数据?
- 确认波特率、数据位、停止位、校验位与代码一致(本例 115200/8/1/None);
- 确认 TX/RX 交叉连接(开发板 TX 接转接板 RX,反之亦然);
- 确认系统时钟真的是 72MHz——波特率由外设时钟分频得到,时钟错了波特率就偏了。
Q2:能编译但 printf 没输出?
- Keil 需勾选 MicroLIB;GCC 需实现
_write 或确认 fputc 被链接;
- 确认
__HAL_RCC_USART1_CLK_ENABLE() 与 GPIO 时钟都已开启。
Q3:中文乱码?
- 串口助手编码选 GBK/UTF-8 与发送端一致;字符串里的中文以何种编码存储就选哪种。
Q4:接收会丢字节?
- 中断里处理太慢或没及时重新开启接收会导致溢出。正式项目建议用 DMA + 空闲中断(IDLE) 收一帧数据。
七、小结
- USART 初始化 = 配句柄参数 + MSP(时钟/GPIO/NVIC)+ 使能中断;
printf 重定向是调试利器,几乎所有 STM32 工程都保留它;
- “主循环空闲 + 中断处理"是相比轮询更高效的结构。
八、拓展应用
- DMA + 空闲中断接收:在
HAL_UARTEx_RxEventCallback 中利用 IDLE 中断收完整帧,避免逐字节中断开销(见 15-DMA 章节的串口 DMA 用法)。
- 自定义日志分级:封装
log_info()/log_err() 宏,带文件名、行号、颜色,做成一个小日志库。
- 命令行交互:在串口上实现一个简易 Shell(解析
led on、adc read 等命令),用串口控制整个板子。
- 485 通信:把 USART 接 485 收发器芯片,通过
DE/RE 控制收发方向,做总线式多机通信(参考 USART—485通信)。