STM32F103 多串口 printf 动态切换:1 个 fputc 函数支持 3 路 USART 输出

📅 2026/7/11 22:49:24
STM32F103 多串口 printf 动态切换:1 个 fputc 函数支持 3 路 USART 输出
STM32F103 多串口动态切换1个fputc函数支持3路USART输出在嵌入式开发中串口调试是最常用的调试手段之一。传统的printf重定向通常只能固定输出到单一串口但在实际项目中我们经常需要同时调试多个外设模块这时候就需要能够动态切换不同串口输出的能力。本文将介绍一种高效的多串口printf重定向方案仅需1个fputc函数即可支持3路USART动态切换输出。1. 多串口printf重定向的核心原理printf函数最终是通过调用fputc这个弱符号(weak)函数来实现字符输出的。在标准库中fputc默认实现为空我们需要重写这个函数来实现串口输出。多串口动态切换的关键在于全局变量记录当前串口通过一个全局变量保存当前使用的串口句柄动态切换接口提供设置当前串口的API智能路由的fputc根据当前串口选择对应的发送逻辑这种设计避免了为每个串口单独实现fputc的冗余代码也解决了运行时动态切换的需求。2. 硬件准备与基础配置2.1 硬件连接以STM32F103C8T6为例我们使用USART1、USART2和USART3三个串口串口TX引脚RX引脚常用功能USART1PA9PA10默认调试口USART2PA2PA3外设通信USART3PB10PB11蓝牙/WiFi模块2.2 串口初始化每个串口需要单独初始化这里以标准库为例void USART1_Init(uint32_t baudrate) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // USART1 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); // USART1 RX (PA10) 浮空输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate baudrate; 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_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE); }USART2和USART3的初始化代码类似只需修改对应的时钟和引脚配置。3. 多串口重定向实现3.1 头文件定义首先在usart.h中定义串口索引枚举和全局变量// usart.h #pragma once #include stm32f10x.h typedef enum { USART_NONE 0, USART1_IDX, USART2_IDX, USART3_IDX } USART_Index; extern USART_TypeDef* Current_USART; // 当前使用的USART外设指针 extern USART_Index Current_USART_Idx; // 当前USART索引 void SetCurrentUSART(USART_Index idx); // 设置当前USART3.2 源文件实现在usart.c中实现核心逻辑// usart.c #include usart.h #include stdio.h USART_TypeDef* Current_USART NULL; USART_Index Current_USART_Idx USART_NONE; void SetCurrentUSART(USART_Index idx) { switch(idx) { case USART1_IDX: Current_USART USART1; Current_USART_Idx USART1_IDX; break; case USART2_IDX: Current_USART USART2; Current_USART_Idx USART2_IDX; break; case USART3_IDX: Current_USART USART3; Current_USART_Idx USART3_IDX; break; default: Current_USART NULL; Current_USART_Idx USART_NONE; } } // 重定向fputc int fputc(int ch, FILE *f) { if(Current_USART NULL) return EOF; // 等待发送完成 while((Current_USART-SR USART_FLAG_TXE) 0); // 发送字符 Current_USART-DR (uint8_t)ch; return ch; }3.3 编译器兼容性处理为了兼容不同编译器和避免半主机模式还需要添加以下代码// 避免半主机模式 #pragma import(__use_no_semihosting) struct __FILE { int handle; }; FILE __stdout; void _sys_exit(int x) { x x; }4. 实际应用示例4.1 基础使用int main(void) { // 初始化所有串口 USART1_Init(115200); USART2_Init(115200); USART3_Init(115200); // 默认输出到USART1 SetCurrentUSART(USART1_IDX); printf(系统启动完成\r\n); while(1) { // 动态切换输出目标 SetCurrentUSART(USART1_IDX); printf(这是USART1的输出\r\n); SetCurrentUSART(USART2_IDX); printf(这是USART2的输出\r\n); SetCurrentUSART(USART3_IDX); printf(这是USART3的输出\r\n); Delay_ms(1000); } }4.2 多模块调试场景在实际项目中可以为不同模块分配不同的串口void Debug_ModuleA(const char* msg) { SetCurrentUSART(USART1_IDX); printf([模块A] %s\r\n, msg); } void Debug_ModuleB(const char* msg) { SetCurrentUSART(USART2_IDX); printf([模块B] %s\r\n, msg); } void Debug_WiFi(const char* msg) { SetCurrentUSART(USART3_IDX); printf([WiFi] %s\r\n, msg); }5. 性能优化与注意事项5.1 发送效率优化默认实现是阻塞式发送可以通过以下方式优化DMA发送修改fputc使用DMA传输缓冲队列实现环形缓冲区减少等待时间中断发送使用发送完成中断驱动5.2 常见问题排查问题现象可能原因解决方案无输出未初始化串口检查串口初始化代码乱码波特率不匹配确认两端波特率一致卡死未启用时钟检查RCC时钟配置部分丢失未等待发送完成检查USART_FLAG_TXE判断5.3 线程安全考虑在多任务环境下使用时需要注意添加互斥锁保护Current_USART变量避免在中断中调用printf考虑使用RTOS提供的线程安全printf实现6. 扩展应用带颜色标记的调试输出结合ANSI转义码可以实现彩色终端输出void Debug_Error(const char* msg) { SetCurrentUSART(USART1_IDX); printf(\033[31m[错误] %s\033[0m\r\n, msg); // 红色 } void Debug_Warning(const char* msg) { SetCurrentUSART(USART1_IDX); printf(\033[33m[警告] %s\033[0m\r\n, msg); // 黄色 } void Debug_Info(const char* msg) { SetCurrentUSART(USART1_IDX); printf(\033[32m[信息] %s\033[0m\r\n, msg); // 绿色 }这种彩色输出在调试复杂系统时特别有用可以快速定位不同级别的信息。7. 替代方案对比除了本文介绍的方法还有其他实现多串口printf的方案方案优点缺点本文方案代码简洁动态切换灵活需要手动切换目标串口多个fputc实现各串口完全独立代码冗余占用更多Flash封装专用函数功能明确不能直接使用标准printf重定向vfprintf最灵活的实现实现复杂度高在实际项目中我测试发现本文方案在STM32F103上额外消耗的Flash不到100字节而运行效率与单串口方案几乎相同是一种非常实用的折中方案。