1. 为什么需要重定向printf和scanf在STM32开发中调试信息的输出和用户交互输入是必不可少的环节。默认情况下printf和scanf函数会输出到标准输出(stdout)和从标准输入(stdin)读取但在嵌入式系统中我们通常需要通过串口来收发这些信息。我刚开始接触STM32时最困惑的就是为什么直接调用printf函数看不到任何输出。后来才发现需要将标准输出重定向到串口。这个过程就像是给printf函数重新指定了一个工作地点——从电脑屏幕变成了串口终端。重定向的主要好处有三个调试方便可以直接在串口终端看到程序运行状态交互性强可以通过串口输入指令控制程序行为资源占用少相比其他调试方式串口通信占用的系统资源更少2. 基础重定向方法库函数重定向2.1 printf重定向实现最基础的重定向方法是重写fputc函数。这个函数是标准库中用于字符输出的底层函数printf最终就是通过调用它来输出字符的。int fputc(int ch, FILE *f) { /* 发送一个字节数据到USART1 */ USART_SendData(USART1, (uint8_t) ch); /* 等待发送完毕 */ while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return (ch); }这段代码做了三件事调用USART_SendData发送一个字节等待发送完成标志位返回发送的字符我在实际项目中遇到过一个问题如果不加等待发送完成的循环连续快速调用printf会导致数据丢失。这是因为串口发送需要时间而MCU的执行速度远快于串口波特率。2.2 scanf重定向实现类似地我们可以重定向scanf函数通过重写fgetc函数int fgetc(FILE *f) { /* 等待串口1输入数据 */ while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) RESET); return (int)USART_ReceiveData(USART1); }这里有个重要注意事项使用scanf时不要开启USART中断。我踩过这个坑开了中断后scanf就收不到数据了即使中断处理函数是空的。这是因为中断会改变USART的状态机运行方式。3. 进阶方案自定义格式化输出3.1 为什么要自定义格式化标准库的printf功能强大但体积庞大会显著增加代码量。在资源受限的STM32上我们可以实现一个精简版的格式化输出函数。我做过对比测试使用标准库printf会使代码量增加约20KB而自定义实现通常只需要2-3KB这对Flash只有64KB的STM32F103来说差别很大。3.2 实现自定义printf下面是一个支持%d和%s格式的自定义实现void USART_printf(USART_TypeDef *USARTx, char *Data, ...) { const char *s; int d; char buf[16]; va_list ap; va_start(ap, Data); while (*Data ! 0) { if (*Data 0x5c) { // 处理转义字符 switch (*Data) { case r: // 回车 USART_SendData(USARTx, 0x0d); Data; break; case n: // 换行 USART_SendData(USARTx, 0x0a); Data; break; default: Data; break; } } else if (*Data %) { switch (*Data) { case s: // 字符串 s va_arg(ap, const char *); for (; *s; s) { USART_SendData(USARTx, *s); while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) RESET); } Data; break; case d: // 十进制数 d va_arg(ap, int); itoa(d, buf, 10); for (s buf; *s; s) { USART_SendData(USARTx, *s); while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) RESET); } Data; break; default: Data; break; } } else { USART_SendData(USARTx, *Data); while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) RESET); } } va_end(ap); }这个实现包含了几个关键点使用可变参数va_list处理不定参数实现了基本的转义字符处理(\r\n)支持%d和%s两种最常用的格式包含了一个简单的itoa函数用于整数转字符串4. 半主机模式与重定向优化4.1 什么是半主机模式半主机模式是ARM架构的一种调试机制允许目标设备通过调试接口使用主机的输入输出设备。但在实际产品中我们通常不希望使用这种模式。禁用半主机模式的典型做法是#pragma import(__use_no_semihosting_swi) struct __FILE { int handle; }; FILE __stdout; FILE __stdin; int fputc(int ch, FILE *f) { return SendChar(ch); } int fgetc(FILE *f) { return GetKey(); } void _ttywrch(int ch) { SendChar(ch); } int _ferror(FILE *f) { return EOF; }4.2 优化后的发送接收函数int SendChar(int ch) { while(!(USART1-SR USART_FLAG_TXE)); USART1-DR (ch 0x1FF); return ch; } int GetKey(void) { while(!(USART1-SR USART_FLAG_RXNE)); return ((int)(USART1-DR 0x1FF)); }这种直接寄存器操作的方式比库函数效率更高在我的测试中相同条件下能提升约15%的吞吐量。特别是在高波特率(如115200)时差异更加明显。5. 实际应用中的经验分享5.1 性能优化技巧缓冲区的使用对于高频输出可以添加环形缓冲区避免等待发送完成DMA传输大量数据输出时使用DMA可以大幅降低CPU占用率格式化优化避免在循环中频繁使用printf可以先格式化到缓冲区再输出我曾经优化过一个需要频繁输出传感器数据的项目通过引入缓冲区和DMACPU占用率从35%降到了8%。5.2 常见问题排查无输出检查USART时钟是否使能确认波特率设置正确验证TX引脚配置乱码检查两端波特率是否一致确认停止位、校验位设置检查时钟源精度scanf不响应确保没有开启USART中断检查终端软件是否发送了回车符验证RX引脚配置5.3 多串口环境下的重定向当项目需要使用多个串口时可以通过以下方式灵活重定向// 全局变量指定当前使用的串口 USART_TypeDef *g_debugUART USART1; int fputc(int ch, FILE *f) { USART_SendData(g_debugUART, (uint8_t)ch); while(USART_GetFlagStatus(g_debugUART, USART_FLAG_TXE) RESET); return ch; } // 运行时可以切换输出串口 void SetDebugUART(USART_TypeDef *uart) { g_debugUART uart; }这种设计在需要同时与多个设备通信的项目中特别有用比如我做过的一个同时与GPS模块和无线模块通信的项目。