☰
STM32 UART多串口配置与printf重定向实战指南
2026/10/15 0:42:37 网站建设 项目流程

1. 背景与核心概念

1.1 为什么要重新理解 UART 配置

在日常嵌入式开发里,UART 串口是使用频率最高、也最容易出问题的外设之一。无论是调试日志输出、跟传感器通信、对接 WiFi/蓝牙模块,还是做出厂测试的 AT 指令通道,都离不开串口。很多初学者以为串口就是“初始化一下、发送一个字符串”这么简单,但真正到了实际项目里,会遇到一串很现实的问题:printf 打印不出来、多个串口不知道该往哪个口发、换了一个板子代码就跑不通、中文输出乱码、进入 HardFault……

这些问题大多不是 UART 硬件本身的问题,而是我们在代码组织方式上没有把“串口”抽象好。尤其是当你同时需要“调试口”和“业务串口”时,如果所有 printf 都写死到某一个串口,后续做日志切换、板级适配会非常痛苦。

本文围绕 UART 的宏定义、printf 输出选择、多串口配置三个核心点展开,通过一个 STM32 HAL 库示例,演示如何用宏把串口资源、printf 输出目标、缓冲区大小管理起来,并把常见的坑和排查思路整理出来。

1.2 什么是 UART 串口通信

UART 是 Universal Asynchronous Receiver/Transmitter 的缩写,全双工异步串行通信接口。它只需要两根数据线:TX 发送、RX 接收,收发双方约定好波特率、数据位、停止位、奇偶校验位之后,就可以按字节流传输数据。硬件层还有流控引脚 RTS/CTS,但不是所有场景都需要。

嵌入式开发中经常提到 USART 和 UART,简单区分一下:

名称说明典型应用
UART异步串口,不支持同步时钟普通串口调试、传感器、蓝牙模块
USART通用同步/异步收发器,支持同步模式带时钟同步的通信场景,或兼容 UART 模式

绝大多数情况下,我们把 USART 直接当 UART 用。学习时不用过分纠结这两个名字,只要知道 STM32 的 USART 外设既能同步也能异步,而串口调试更多用异步模式即可。

1.3 宏定义与 printf 重定向的关系

串口功能本身很简单,但代码写得好不好,差异很大。宏定义在这里的作用并不是“显摆技巧”,而是为了做到几个非常实际的目标:

  • 串口资源可配置:换板子后,只需改宏,不用改业务代码。
  • printf 输出目标可选:编译期决定日志走哪个串口,运行时也能动态切换。
  • 缓冲区大小可调整:不同项目对日志长度、接收缓存需求不同,通过宏统一控制。
  • 代码可读性高:写DEBUG_USART比写记忆中的&huart1更清楚。

printf 重定向则是把 C 库函数printf最终要调用的底层字符输出函数,从默认的显示器输出重定向到串口。以 ARMCC/Keil 环境为例,最常见的做法是重写fputc,让每一个字符都通过HAL_UART_Transmit发出去。

#include <stdio.h> int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }

这么写之后,printf("hello\n")的内容就会从串口 1 发出去。但这个写法有一个局限:printf 的输出目标被固定死在huart1上。如果项目里还有第二个串口,或者你想把日志切换到别的串口,只靠上面这段代码就不够了。

1.4 printf 重定向的常见误区

很多新手在重定向 printf 后,运行程序发现没有任何输出,于是开始在初始化代码里找问题。但实际上问题往往出在以下几个被忽略的地方:

  1. 没有启用微库或关闭半主机模式。在 Keil MDK 环境下,默认的 C 库printf依赖半主机模式,串口没有实现对应底层函数时,调用 printf 会进入 HardFault 或直接卡死。
  2. 波特率不对。printf 重定向成功,但串口助手设置的波特率和代码不一致,看起来就像“没输出”。
  3. 时钟和引脚配置错误。HAL_UART_Transmit 卡在超时等待标志位,多半是串口时钟或 GPIO 复用配置丢失。
  4. 调用了fputc,但没有包含头文件或函数签名不对,编译器用了别的弱定义。

这些问题会在后面的实战部分逐一演示如何规避。

2. 环境准备与版本说明

2.1 本文的硬件与软件环境

本文示例以 STM32F103 系列芯片为背景,使用 STM32CubeMX 生成初始化代码,再在 Keil MDK 中编写业务逻辑。之所以选这套环境,是因为 STM32 的 HAL 库代码结构清晰、寄存器操作封装完整,非常有利于讲清楚“配置在哪里”“宏定义如何替换”。

注意:不同芯片、不同 HAL 库版本,生成的初始化函数名可能略有差异,但核心思路完全一致。如果你使用的是 GD32、AT32、NXP 或其他 MCU,只需把外设句柄和初始化接口替换成对应平台的 API 即可。

版本方面不限定死,重点演示配置思路:

项目说明
芯片型号STM32F103C8T6(或兼容型号)
开发环境STM32CubeMX + Keil MDK
外设库STM32 HAL 库
调试工具任意 USB 转串口模块(CH340、CP2102 等)
系统Windows 10/11

USB 转串口模块通常需要安装驱动。如果电脑识别不到串口设备,可以先检查驱动是否正确安装。CH340、CP2102、FT232 这类芯片的驱动均需要到厂商官网下载匹配版本,避免安装到错误版本导致设备管理器里显示异常。

2.2 示例工程的整体结构

为了演示“多串口 + 宏定义 + printf 选择”,本文设计一个双串口场景:

  • USART1:作为调试串口,输出系统启动信息和日志。
  • USART2:作为业务串口,用于接收外部设备发送的命令,并将处理结果回传。
  • 通过宏PRINTF_UART_INDEX选择 printf 默认输出到 USART1 或 USART2。
  • 提供printf_uart_select()运行时切换函数,便于在业务代码中动态改变 printf 输出目标。

最终的项目文件结构规划如下:

Demo_UART/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── uart_config.h │ │ └── usart.h │ └── Src/ │ ├── main.c │ ├── uart_config.c │ ├── usart.c │ └── stm32f1xx_it.c ├── Drivers/ │ └── ... └── Demo_UART.ioc

其中uart_config.h和uart_config.c是我们后加的串口管理文件,用于统一管理串口句柄、宏定义和 printf 重定向。

3. 核心配置与原理拆解

3.1 在 STM32CubeMX 中创建双串口工程

打开 STM32CubeMX,新建工程并选择 STM32F103C8T6,然后在芯片引脚图上配置两个串口:

  1. USART1 模式选择Asynchronous,波特率设置为115200 Bits/s,数据位 8,无校验,停止位 1。
  2. USART2 同样选择Asynchronous,波特率设置为115200 Bits/s,其他参数保持一致。
  3. 系统时钟可以使用默认 HSI 或外部晶振。如果使用外部晶振,需要确认板子上是否有 8MHz 晶振。

在 CubeMX 的Project Manager里配置工程名称、工具链(MDK-ARM)并生成代码。

生成后的usart.c会包含两个初始化函数:MX_USART1_UART_Init()和MX_USART2_UART_Init(),同时会在main.c中自动声明huart1和huart2两个全局句柄。

// 文件路径:Core/Src/usart.c UART_HandleTypeDef huart1; UART_HandleTypeDef huart2; void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&huart1); }

3.2 用宏定义统一管理串口资源

直接使用huart1、huart2虽然能跑,但代码里到处都是硬件句柄,后期维护成本很高。建议在uart_config.h里定义一组“逻辑串口”宏:

// 文件路径:Core/Inc/uart_config.h #ifndef __UART_CONFIG_H #define __UART_CONFIG_H #include "usart.h" #include "stdio.h" #define DEBUG_UART (&huart1) // 调试串口 #define CMD_UART (&huart2) // 业务串口 // printf 默认输出串口选择 #define PRINTF_UART_1 1 #define PRINTF_UART_2 2 #ifndef PRINTF_UART_INDEX #define PRINTF_UART_INDEX PRINTF_UART_1 #endif #endif

这样修改了一个好处:如果换了硬件平台,把huart1和huart2的映射关系调整一下,业务代码里DEBUG_UART、CMD_UART的引用不用逐个修改。

3.3 printf 重定向原理

Keil MDK 环境下,printf最终会调用fputc来输出单个字符。默认实现是发送到调试器或半主机通道。我们需要把fputc重写为“通过串口发一个字节”。

最直接的写法:

int fputc(int ch, FILE *f) { HAL_UART_Transmit(DEBUG_UART, (uint8_t *)&ch, 1, 0xFFFF); return ch; }

这段代码的原理是:

  • ch是printf格式化后产生的一个字符。
  • (uint8_t *)&ch取该字符的地址,作为HAL_UART_Transmit的数据源。
  • 0xFFFF是超时时间,单位毫秒。这里设置一个较大值,避免发送阻塞时立刻返回错误。

在 Keil 中还需要注意两点:

  1. 勾选 Use MicroLIB。在工程选项的 Target 页签中勾选Use MicroLIB,这样可以避免完整的 C 库依赖半主机模式的默认实现。
  2. 如果不使用 MicroLIB,则需要自行实现fputc并屏蔽半主机模式,否则程序运行到 printf 时可能进入 HardFault。

3.4 多串口 printf 输出选择

上面固定写死DEBUG_UART的做法依然不够灵活。更好的方案是把“当前 printf 使用哪个串口”提升为可配置状态。

首先定义一个全局指针,指向当前 printf 输出串口句柄:

static UART_HandleTypeDef *s_print_uart = DEBUG_UART;

再提供两个函数:

void printf_uart_set(UART_HandleTypeDef *huart) { s_print_uart = huart; } UART_HandleTypeDef *printf_uart_get(void) { return s_print_uart; }

然后fputc改为:

int fputc(int ch, FILE *f) { HAL_UART_Transmit(s_print_uart, (uint8_t *)&ch, 1, 0xFFFF); return ch; }

这样在运行时调用printf_uart_set(&huart2);,后面的 printf 内容就会全部发到 USART2。这是一个非常实用的模式,在“日志切换”“进入 AT 指令测试模式”的场景中很常见。

3.5 编译期选择与运行时切换结合

宏定义可以在编译期就确定默认输出串口。在uart_config.h中我们已经定义了PRINTF_UART_INDEX,那么可以在初始化代码里根据该宏设置s_print_uart的初始值:

void uart_config_init(void) { #if PRINTF_UART_INDEX == PRINTF_UART_1 printf_uart_set(DEBUG_UART); #elif PRINTF_UART_INDEX == PRINTF_UART_2 printf_uart_set(CMD_UART); #else printf_uart_set(DEBUG_UART); #endif printf("[UART] printf output initialize done.\r\n"); }

这样有两种切换方式:

  • 编译期固定:修改uart_config.h中PRINTF_UART_INDEX的宏值,重新编译。
  • 运行期动态:调用printf_uart_set(&huart2),随时切换。

两种方式结合,既能满足“发布版本固定日志口”的需求,也能满足“调试时现场切口”的灵活性。

4. 完整实战:双串口 printf 输出与多串口配置

4.1 创建工程并添加串口配置文件

在 STM32CubeMX 中完成 1 个串口初始化后,在工程的Core/Inc和Core/Src目录下分别新建uart_config.h和uart_config.c。

注意:由于uart_config.c会被 Keil 编译,因此要把这个文件手动添加到 Keil 工程的 Source Group 中。很多初学者生成了文件但忘记添加,导致编译器报“未定义符号uart_config_init”,这一点要特别留意。

4.2 编写 uart_config.h

// 文件路径:Core/Inc/uart_config.h #ifndef __UART_CONFIG_H #define __UART_CONFIG_H #include "usart.h" #include <stdio.h> // 逻辑串口映射 #define DEBUG_UART (&huart1) #define CMD_UART (&huart2) // printf 默认输出串口 #define PRINTF_UART_1 1 #define PRINTF_UART_2 2 #ifndef PRINTF_UART_INDEX #define PRINTF_UART_INDEX PRINTF_UART_1 #endif void uart_config_init(void); void printf_uart_set(UART_HandleTypeDef *huart); UART_HandleTypeDef *printf_uart_get(void); #endif

4.3 编写 uart_config.c

// 文件路径:Core/Src/uart_config.c #include "uart_config.h" static UART_HandleTypeDef *s_print_uart = DEBUG_UART; void printf_uart_set(UART_HandleTypeDef *huart) { s_print_uart = huart; } UART_HandleTypeDef *printf_uart_get(void) { return s_print_uart; } void uart_config_init(void) { #if PRINTF_UART_INDEX == PRINTF_UART_1 printf_uart_set(DEBUG_UART); #elif PRINTF_UART_INDEX == PRINTF_UART_2 printf_uart_set(CMD_UART); #else printf_uart_set(DEBUG_UART); #endif printf("[UART] printf output init done.\r\n"); }

这里要注意一个细节:uart_config_init必须在串口初始化之后调用,因为s_print_uart本身只是保存了一个指针,指针指向的huart1、huart2必须已经完成MX_USARTx_UART_Init()初始化。

4.4 重定向 printf

在 Keil MDK 中,重定向fputc即可。如果使用 GCC 工具链(例如 STM32CubeIDE),则通常需要重写_write函数,两者思路相同。

在main.c中添加:

// 文件路径:Core/Src/main.c #include <stdio.h> int fputc(int ch, FILE *f) { HAL_UART_Transmit(printf_uart_get(), (uint8_t *)&ch, 1, 0xFFFF); return ch; }

同时确保 Keil 工程勾选了Use MicroLIB。如果不使用 MicroLIB,又不想勾选,则需要在工程中加入以下代码来禁用半主机模式:

#include <rt_sys.h> void _ttywrch(int ch) { (void)ch; } struct __FILE { int handle; }; FILE __stdout; FILE __stdin; void _sys_exit(int return_code) { (void)return_code; while (1); }

这类代码属于编译环境兼容层,不同编译器差异较大。最省事的方案仍然是勾选 MicroLIB,对于绝大多数串口日志场景完全够用。

4.5 用宏定义管理接收缓冲区

除了 printf 输出,串口接收也需要通过宏管理。以 USART2 接收外部命令为例,在uart_config.h中定义接收缓冲区大小:

#define CMD_RX_BUF_SIZE 128 #define CMD_RX_MAX_LEN (CMD_RX_BUF_SIZE - 1)

再在uart_config.c中定义缓冲区:

static uint8_t s_cmd_rx_buf[CMD_RX_BUF_SIZE]; static uint8_t s_cmd_rx_len = 0; static uint8_t s_cmd_rx_complete = 0;

这里使用“一帧结束判定”和简单的接收回显。实际项目中,可以使用空闲中断、DMA 或者环形缓冲区,但为了把原理讲清楚,先用最简单的HAL_UART_Receive_IT方式。

4.6 启动串口中断接收

在main.c中,初始化完成后调用:

int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); MX_USART2_UART_Init(); uart_config_init(); // 启动串口2中断接收 HAL_UART_Receive_IT(&huart2, s_cmd_rx_buf, 1); printf("System boot OK.\r\n"); while (1) { if (s_cmd_rx_complete) { printf("Recv CMD: %s\r\n", s_cmd_rx_buf); s_cmd_rx_complete = 0; s_cmd_rx_len = 0; memset(s_cmd_rx_buf, 0, CMD_RX_BUF_SIZE); HAL_UART_Receive_IT(&huart2, s_cmd_rx_buf, 1); } } }

注意:这里s_cmd_rx_buf和s_cmd_rx_complete需要跨文件访问时,在uart_config.h中声明为 extern。

4.7 编写串口中断回调

在stm32f1xx_it.c或stm32f1xx_hal_uart.c的 UART 中断回调函数中处理接收:

// 文件路径:Core/Src/stm32f1xx_it.c #include "uart_config.h" extern uint8_t s_cmd_rx_buf[]; extern uint8_t s_cmd_rx_len; extern uint8_t s_cmd_rx_complete; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { if (s_cmd_rx_len < CMD_RX_MAX_LEN) { if (s_cmd_rx_buf[s_cmd_rx_len] == '\r' || s_cmd_rx_buf[s_cmd_rx_len] == '\n') { s_cmd_rx_buf[s_cmd_rx_len] = '\0'; s_cmd_rx_complete = 1; } else { s_cmd_rx_len++; HAL_UART_Receive_IT(&huart2, &s_cmd_rx_buf[s_cmd_rx_len], 1); } } else { s_cmd_rx_buf[s_cmd_rx_len] = '\0'; s_cmd_rx_complete = 1; } } }

这种逐字节接收的方式虽然效率不高,但它简单直观,非常适合理解“接收中断 + 缓冲区管理”的基本流程。实际项目如果数据量大,建议改用 DMA + 空闲中断。

4.8 运行与验证

编译下载后,使用串口调试助手连接 USB 转串口模块:

  1. 将 USB 转串口模块的 TX 接到 STM32 的 PA10(USART1_RX),RX 接到 PA9(USART1_TX),GND 共地。
  2. 打开串口助手,设置波特率 115200,数据位 8,停止位 1,无校验。
  3. 复位板子,应该能看到:
[UART] printf output init done. System boot OK.
  1. 如果把 USB 转串口模块切换到 USART2 对应的引脚,并在代码里调用printf_uart_set(&huart2);,则重新复位后,同样的内容会从 USART2 发出。

这就验证了双串口配置和 printf 输出选择都生效了。

5. 常见问题与排查思路

5.1 printf 完全没有输出

这是最常遇到的问题。按下面的顺序排查:

检查点原因解决方案
是否勾选 MicroLIB半主机模式导致卡死Keil 中勾选 Use MicroLIB
串口初始化是否完成引脚或时钟错误检查 MX_USARTx_UART_Init 是否被调用
波特率是否一致串口助手和代码不一致统一为 115200
接线是否正确TX/RX 接反交换 USB 转串口模块的 TX/RX
引脚复用是否正确HAL 库未配置 GPIO_AF检查 CubeMX 生成代码

排查技巧:先用HAL_UART_Transmit(&huart1, (uint8_t *)"A", 1, 0xFFFF);直接发一个字节,如果这样也没有输出,说明串口底层就有问题,不用先查 printf。

5.2 printf 中文乱码

中文乱码的根本原因是“字符编码不一致”。也就是说,源文件里保存的汉字是什么编码,串口助手按什么编码显示。如果两边的编码方式不一致,就会出现乱码。

常见场景:

  • 源文件是 UTF-8,串口助手用 GBK/GB2312 显示。
  • 源文件是 GB2312,串口助手用 UTF-8 显示。
  • Keil MDK 默认编辑器对中文支持依赖本地编码,不同版本的 MDK 行为还不完全一样。

解决方案:

  1. 尽量用英文日志,避免编码问题。
  2. 确需输出中文时,统一源文件编码和串口助手显示编码。
  3. 也可以把中文转成 Unicode 转义序列,不过可读性差,工程中不常用。
  4. 串口助手大多数支持“UTF-8”和“GBK”显示切换,尝试切换一下显示编码即可。

5.3 多串口 printf 固定到某一个口

很多人的需求是“USART1 打印日志,USART2 也打印日志,互不影响”。直接用同一个printf做不到,因为printf是全局唯一的输出函数。思路有两条:

  1. 实现两个打印函数,例如debug_printf和cmd_printf,内部各自调用HAL_UART_Transmit。这种方式不依赖fputc,也不占用printf。
  2. 用printf_uart_set在切换后输出到指定串口。注意切换后要恢复,否则后续日志会串口。

如果需要真正同时输出到两个串口,也可以定义一个printf_broadcast函数,内部对多个串口逐一执行HAL_UART_Transmit,适合日志需要分发的场景。

5.4 进入 HardFault

调用 printf 后进入 HardFault,通常和半主机模式有关。在 Keil 中,如果未勾选 MicroLIB,且没有关闭半主机模式,printf会尝试通过半主机通道输出,MCU 中并没有对应硬件,从而触发 HardFault。

解决思路:

  • 勾选 MicroLIB。
  • 或者实现_sys_exit、_ttywrch等半主机函数,阻止进入半主机分支。
  • 使用 SEGGER RTT 或 SWO 输出日志时不要使用标准 printf 重定向,应使用库提供的专用输出接口。

5.5 HAL_UART_Transmit 卡死

HAL_UART_Transmit的最后一个参数是超时时间。如果设置为HAL_MAX_DELAY,并且串口硬件状态异常,会出现永久等待。例如 GPIO 复用配置错误、串口时钟未开启,都会导致发送标志位一直不置位。

解决办法:

  1. 减少超时时间,例如改为1000,让异常快速暴露。
  2. 检查HAL_UART_Init的返回值。
  3. 检查 GPIO 是否配置为复用功能,而不是普通输出。

6. 最佳实践与工程建议

6.1 用逻辑串口抽象硬件

不要直接在业务代码里写&huart1、&huart2。用宏或者全局变量定义逻辑串口,例如DEBUG_UART、CMD_UART、GPS_UART,这样硬件调整的改动范围会小很多。

#define DEBUG_UART (&huart1) #define LORA_UART (&huart2)

当你的板子从 F103 换到 F407,串口引脚变化后,只需要修改 CubeMX 配置和这里的一行宏定义,业务逻辑完全不用动。

6.2 区分“日志输出”与“业务通信”

调试日志和业务数据混在同一个串口、同一个 printf 通道里,是后期问题最多的地方。比如业务串口收到一帧数据,你顺手 printf 出去,如果此时另一个线程也在 printf,两条日志会交错,接收端解析可能出问题。

建议:

  • 调试日志固定走专门串口。
  • 业务通信使用独立的串口和回调函数。
  • 日志输出尽量带时间戳、级别、模块名。
  • 业务数据帧不要走 printf 格式化,直接以字节数组发送。

6.3 缓冲区大小用宏管理

无论是发送缓冲区还是接收缓冲区,都不要在代码里散落魔法数字。比如:

#define LOG_BUF_SIZE 256 #define CMD_RX_BUF_SIZE 128

这样即使将来要加大缓冲区,也只需修改宏定义,程序其他部分会自动适配。

接收缓冲区的溢出也要重点关注。当接收数据超过缓冲区时,应该做截断处理,并记录一次溢出事件,而不是让缓冲区继续写入越界地址。

6.4 printf 重定向的跨平台写法

如果项目需要在 Keil 和 GCC 两种工具链之间切换,可以写一个条件编译分支:

#if defined(__ARMCC_VERSION) int fputc(int ch, FILE *f) { HAL_UART_Transmit(s_print_uart, (uint8_t *)&ch, 1, 0xFFFF); return ch; } #elif defined(__GNUC__) int _write(int file, char *ptr, int len) { for (int i = 0; i < len; i++) { HAL_UART_Transmit(s_print_uart, (uint8_t *)&ptr[i], 1, 0xFFFF); } return len; } #endif

注意:GCC 环境还需要在链接或重定向层面避免半主机,STM32CubeIDE 中在项目属性里设置底层函数,或者直接使用上面这段_write,并配合setvbuf(stdout, NULL, _IONBF, 0);关闭标准输出缓冲。这里不展开具体 IDE 菜单,因为不同版本设置入口差异较大。

6.5 谨慎使用全局 printf_uart_set

运行时切换 printf 输出串口这个功能很方便,但也要小心。如果两个模块同时调用printf_uart_set,会出现“把 printf 输出切到了别人的串口”的典型问题。建议:

  • 设置前保存旧值。
  • 使用完毕后恢复。
  • 或者只允许在初始化阶段调用。
void some_module_entry(void) { UART_HandleTypeDef *old = printf_uart_get(); printf_uart_set(&huart2); printf("module start\r\n"); printf_uart_set(old); }

这样局部切换不会影响其他模块的日志输出。

6.6 接收回调中不要做耗时操作

串口中断回调函数是在中断上下文执行的,不要在里面调用 printf、HAL_Delay、大循环等操作。如果需要打印数据,建议在回调里设置标志位,然后在主循环中处理:

volatile uint8_t rx_flag = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { rx_flag = 1; } } while (1) { if (rx_flag) { rx_flag = 0; // 处理接收数据 } }

这能有效避免中断嵌套、日志阻塞带来的问题。

6.7 正确关闭串口接收

在调试过程中,如果串口一直处于中断接收状态,调试器复位后可能还会触发中断。建议在系统进入低功耗前调用HAL_UART_AbortReceive关闭接收,避免意外唤醒。生产环境下也要考虑收到无用数据时如何处理,而不是让缓冲区无限堆积。

7. 总结与学习路线

本文从 UART 的基础概念讲起,结合 STM32 HAL 库演示了宏定义统一管理串口资源、printf 重定向原理、多串口 printf 输出选择以及双串口收发配置的方法。所有代码都遵循“逻辑串口 + 宏定义 + 可切换 printf 输出目标”的思路,实际项目里可以直接套用。

如果你刚接触 UART,建议按照下面的路线继续练习:

  1. 先熟练掌握单个串口的初始化、轮询发送、中断接收。
  2. 尝试用宏定义把串口句柄抽象出来,模拟板级适配。
  3. 研究 DMA + 空闲中断接收不定长数据。单字节中断接收适合学习,但不适合大数据量场景。
  4. 学习环形缓冲区,解决串口数据连续接收与主循环消费之间的矛盾。
  5. 在真实项目里再验证本文的 printf 输出切换方案,体会编译期宏与运行时切换各自的适用场景。

串口是嵌入式调试的“眼睛”,把这套配置和代码组织方式吃透,后面调试 SPI、I2C、CAN 时,你会少走很多弯路。遇到打印不出数据时,先用最底层的HAL_UART_Transmit确认硬件通路,再从 printf 重定向、编码、工程配置三个层面逐一排查,问题基本都能定位。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询