短文标题:printf重定向:一句fputc,串口打印任意变量

你有没有想过一个问题:电脑上调试程序,用printf直接打印变量值,一目了然。
单片机没有显示器,怎么实现类似的效果?重定向printf到串口。 把printf这个"输出口",从电脑屏幕"拧"到单片机的USART上。重定向fputc,printf底层会调用fputc输出每一个字符。我们只需要重写fputc,把字符通过USART发送出去:
int fputc(int ch, FILE *f)
{
while (!(USART1->SR & USART_SR_TXE)); // 等待上次发送完成
USART1->DR = (uint8_t)ch; // 发送字符
return ch;
}

写好这个函数,就可以直接用printf了:
int temperature = 25;
printf("当前温度: %d 度\r\n", temperature);
串口调试助手就能看到"当前温度: 25 度"。浮点数(%f)支持需额外配置(MicroLIB默认不支持)。MicroLIB:必须勾选的关键,在MDK(Keil)中,只用fputc还不够。必须勾选Use MicroLIB(微库)。否则链接时会报错,或运行时卡死。MicroLIB是ARM为嵌入式设备精简的C库,移除了半主机依赖,体积小。
半主机模式的陷阱,ARM标准库默认支持"半主机模式"(通过调试器与主机通信,如printf输出到IDE的Debug Viewer)。单片机独立运行时没有调试器,半主机模式会让程序卡死。勾选MicroLIB(禁用半主机),或手动实现__FILE结构体的桩函数,让半主机失效。MicroLIB最简单,推荐。

MicroLIB的限制
• 不支持浮点数(%f):浮点转字符串代码体积大,MicroLIB默认不包含。变通:整数部分和小数部分分开打印
int temp_x10 = 261;
printf("温度: %d.%d℃\r\n", temp_x10 / 10, temp_x10 % 10);
• 不支持宽字符
• 部分标准函数功能简化
对嵌入式调试而言,这些限制通常可接受。多串口支持,需要同时使用多个串口(如USART1打印日志,USART2通信),可自定义类printf函数,传入串口句柄:
int printf_usart(UART_HandleTypeDef *huart, const char *format, ...)
{
char buf256;
va_list args;
va_start(args, format);
int len = vsnprintf(buf, sizeof(buf), format, args);
va_end(args);
HAL_UART_Transmit(huart, (uint8_t*)buf, len, 100);
return len;
}
这个故事的启示,重定向fputc是用标准库实现硬件输出的通用方法------printf只负责格式化,底层输出函数指向你的USART。改一个函数,printf就能活过来。调试效率提高10倍。写在最后,嵌入式调试,信息越详细,定位Bug越快。重定向printf到串口,让单片机"开口说话"。调试神器,学会就回不去。

(本文灵感源于于振南《新概念ARM32单片机》教程第4.7节"优化 USART 发送,用 printf 打印输出"。)
觉得有用?点赞、转发,让更多人学会"printf重定向"这个调试利器。
