为什么需要 printf 重定向?
在单片机开发中,调试信息输出至关重要。虽然可以使用 UART 直接发送字符串,但 printf 提供了格式化输出的强大能力,如整数、浮点、十六进制等,能极大简化日志记录。然而,标准 C 库的 printf 默认输出到 stdout,而单片机没有操作系统,需要手动将 stdout 重定向到 UART 外设。
原理剖析:printf 的底层机制
printf 是标准 C 库函数,它内部调用 fputc(或 putc)来输出单个字符。在桌面系统中,fputc 由操作系统实现;在嵌入式环境中,我们需要自己实现 fputc,将字符发送到 UART 的发送寄存器。这就是重定向的核心。
对于浮点支持,标准 C 库(如 newlib)默认不启用浮点格式化,以节省代码空间。需要手动链接浮点库(如 -u _printf_float)或使用微库(MicroLib)来启用。
配置步骤:以 STM32 为例
1. 初始化 UART
首先,确保 UART 外设已正确初始化,波特率、数据位等设置无误。以下以 STM32F103 的 USART1 为例:
// USART1 初始化,波特率 115200
void USART1_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
// 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);
// RX (PA10) 浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// USART 配置
USART_InitStructure.USART_BaudRate = 115200;
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);
}
2. 实现 fputc 重定向
在 C 文件中添加以下代码,将 printf 输出重定向到 USART1:
#include <stdio.h>
// 重定向 printf 到 USART1
int fputc(int ch, FILE *f) {
// 等待发送缓冲区空
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, (uint8_t)ch);
return ch;
}
注意:如果使用 Keil MDK,需要勾选 "Use MicroLIB" 以支持重定向。若使用 GCC,可能需要额外处理。
3. 启用浮点支持
-
Keil MDK:在 Options for Target -> Target 标签页,勾选 "Use MicroLIB",然后编译时添加
--printf_float选项(或在代码中声明#pragma import(__use_no_semihosting)并实现_sys_exit)。更简单的方法是使用 MicroLIB,它默认支持浮点。 -
GCC (arm-none-eabi):在链接时添加
-u _printf_float和-u _scanf_float选项,例如:arm-none-eabi-gcc -u _printf_float -o output.elf ...。
4. 完整示例代码
以下是一个完整的示例,包含初始化、重定向和浮点输出:
#include "stm32f10x.h"
#include <stdio.h>
// 初始化 USART1
void USART1_Init(void) {
// ... 如上所示 ...
}
// 重定向 printf
int fputc(int ch, FILE *f) {
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, (uint8_t)ch);
return ch;
}
int main(void) {
float temperature = 25.36;
int count = 42;
USART1_Init();
printf("System started!\n");
printf("Count: %d, Temperature: %.2f°C\n", count, temperature);
while (1) {
// 主循环
}
}
注意事项与常见问题
-
缓冲区问题:printf 默认使用行缓冲,但在嵌入式环境中,可能没有实现缓冲。如果输出不完整,可以尝试在
fputc中直接发送,或使用setvbuf设置无缓冲。 - 浮点支持代价:启用浮点 printf 会增加代码大小(约 10-20KB),在资源受限的 MCU 上需谨慎。如果不需要浮点,建议使用整数格式化或自定义转换。
-
重定向冲突:如果同时使用
printf和sprintf,确保重定向只影响 stdout,不影响其他流。 - 中断安全:如果在中断中调用 printf,需确保 UART 发送不被打断,否则可能丢失数据。建议使用环形缓冲区。
- 硬件流控:如果使用 RTS/CTS,需在初始化时配置,否则可能通信异常。
进阶技巧:使用环形缓冲区
对于高频日志输出,直接阻塞式发送会浪费 CPU。可以改用中断或 DMA + 环形缓冲区,实现非阻塞 printf。例如,使用一个 FIFO,fputc 将字符放入缓冲区,UART 中断负责发送。这样 printf 几乎不占用 CPU 时间。
// 简单环形缓冲区示例(伪代码)
#define BUF_SIZE 256
volatile uint8_t buf[BUF_SIZE];
volatile uint16_t head = 0, tail = 0;
int fputc(int ch, FILE *f) {
uint16_t next = (head + 1) % BUF_SIZE;
while (next == tail); // 等待空间
buf[head] = ch;
head = next;
// 触发 UART 中断发送
USART_ITConfig(USART1, USART_IT_TXE, ENABLE);
return ch;
}
总结
printf 重定向是嵌入式调试的基石。通过实现 fputc 并启用浮点支持,开发者可以轻松输出格式化的调试信息。本文提供了完整的配置步骤和代码示例,并指出了常见陷阱。掌握这一技术,将显著提升开发效率。