STM32 USART — 串口通信与 printf 重定向

野火 EBF STM32F103 Mini HAL 库:配置 USART1 为 115200 8-N-1,重定向 printf 到串口,并通过中断接收实现回显。

STM32 USART — 串口通信与 printf 重定向

开发板:野火 EBF STM32F103 Mini
固件库:HAL 库
对应例程14-USART—串口通信 / USART1回显
参考源码User/main.cUser/usart/bsp_debug_usart.cUser/usart/bsp_debug_usart.hUser/stm32f1xx_it.c

串口(UART/USART)是嵌入式开发中最重要的调试通道。没有屏幕的开发板,靠串口把运行日志打印到电脑上的串口助手,是定位问题的第一手段。本文带你配好 USART1,并学会用熟悉的 printf 输出信息。


一、功能简介

本例把 USART1 配置为 115200 波特率、8 位数据、无校验、1 位停止位(8-N-1),并使能接收中断:

  • 程序启动后通过 printf 打印欢迎语;
  • 通过自定义的 Usart_SendString() 发送字符串;
  • 上位机串口助手发送的数据,会在 USART1_IRQHandler 中进入接收中断,实现"回显"。

核心知识点:

  1. USART 外设的 HAL 初始化套路(UART_HandleTypeDef);
  2. TX/RX 引脚必须配置为复用推挽/复用输入
  3. fputc 重定向让 printf 走串口;
  4. 中断接收的基本框架。

二、硬件准备

项目 说明
开发板 野火 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 不阻塞。回显实验就是趁这个时机把收到的字节再发回去。

五、实验现象

  1. 打开串口助手,选择正确串口号、波特率 115200、8-N-1、无流控
  2. 开发板复位后,串口助手收到两行欢迎语:
    1
    2
    
    欢迎使用野火开发板
    自定义函数输出:这是一个串口中断接收回显实验
    
  3. 在串口助手发送区输入任意字符并发送,开发板会把相同字符回发回来(回显)。

六、常见问题

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 工程都保留它;
  • “主循环空闲 + 中断处理"是相比轮询更高效的结构。

八、拓展应用

  1. DMA + 空闲中断接收:在 HAL_UARTEx_RxEventCallback 中利用 IDLE 中断收完整帧,避免逐字节中断开销(见 15-DMA 章节的串口 DMA 用法)。
  2. 自定义日志分级:封装 log_info()/log_err() 宏,带文件名、行号、颜色,做成一个小日志库。
  3. 命令行交互:在串口上实现一个简易 Shell(解析 led onadc read 等命令),用串口控制整个板子。
  4. 485 通信:把 USART 接 485 收发器芯片,通过 DE/RE 控制收发方向,做总线式多机通信(参考 USART—485通信)。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计