## 为什么需要 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 为例: ```c // 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: ```c #include // 重定向 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. 完整示例代码 以下是一个完整的示例,包含初始化、重定向和浮点输出: ```c #include "stm32f10x.h" #include // 初始化 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 时间。 ```c // 简单环形缓冲区示例(伪代码) #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` 并启用浮点支持,开发者可以轻松输出格式化的调试信息。本文提供了完整的配置步骤和代码示例,并指出了常见陷阱。掌握这一技术,将显著提升开发效率。