2. 利用printf调试嵌入式程序

任务目标

在嵌入式目标板上通过串口实现printf输出,完成标准C库printf底层字符输出函数的重定向,使调试信息能够从开发板串口发送到PC终端。本案例以STM32F103系列(HAL库)为例,给出最小可复现实现。

环境与依赖

  • 硬件:STM32F103C8T6最小系统板,USB转TTL模块,USART1(PA9/PA10)连接PC
  • 软件:STM32CubeIDE 或 Keil MDK-ARM(本文以STM32CubeIDE为例)
  • 固件库:STM32CubeF1 HAL库(已包含在CubeIDE中)

实现原理

C标准库的printf最终会调用底层字符输出函数。在ARMCC(Keil)中该函数为fputc,在GCC(CubeIDE默认)中为_write。重定向的核心就是重新实现这两个函数之一,将字符通过串口发送。同时需要确保不使用微库(Microlib)或正确配置半主机模式,避免程序卡死在调试通道。

关键完整代码

以下代码在STM32CubeIDE(GCC工具链)中可直接使用。若使用Keil,将_write函数替换为fputc实现即可。

// main.c 中需要包含的头文件
#include <stdio.h>
#include <stdarg.h>

// 在 main() 函数之前添加以下重定向代码

// GCC工具链(STM32CubeIDE)的printf重定向
int _write(int file, char *ptr, int len)
{
    // 逐个字符通过USART1发送
    for (int i = 0; i < len; i++)
    {
        // 等待发送数据寄存器为空
        while (!(USART1->SR & USART_SR_TXE));
        // 发送字符
        USART1->DR = (uint8_t)ptr[i];
    }
    return len;
}

// 若使用Keil MDK (ARMCC),则用以下函数替代上面的_write
// int fputc(int ch, FILE *f)
// {
//     while (!(USART1->SR & USART_SR_TXE));
//     USART1->DR = (uint8_t)ch;
//     return ch;
// }

// USART1初始化(若已在CubeMX中生成则无需重复)
static void USART1_Init(void)
{
    // 使能GPIOA和USART1时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_USART1_CLK_ENABLE();

    // 配置PA9为TX(复用推挽),PA10为RX(浮空输入)
    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = GPIO_PIN_9;
    gpio.Mode = GPIO_MODE_AF_PP;
    gpio.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &gpio);

    gpio.Pin = GPIO_PIN_10;
    gpio.Mode = GPIO_MODE_INPUT;
    gpio.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &gpio);

    // 配置USART1:115200-8-N-1
    USART_HandleTypeDef huart1 = {0};
    huart1.Instance = USART1;
    huart1.Init.BaudRate = 115200;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart1.Init.OverSampling = UART_OVERSAMPLING_16;
    HAL_UART_Init(&huart1);
}

int main(void)
{
    HAL_Init();
    USART1_Init();

    // 测试printf输出
    int count = 0;
    while (1)
    {
        printf("Count: %d, Hex: 0x%X\r\n", count++, count);
        HAL_Delay(1000);  // 延时1秒
    }
}

运行与验证

  1. 将代码编译下载到STM32开发板,PA9连接USB转TTL的RX,PA10连接TX,GND共地
  2. PC端打开串口助手(如PuTTY、SSCOM),选择对应COM口,设置波特率115200、8数据位、无校验、1停止位
  3. 打开串口后复位开发板,观察串口助手是否每秒收到一行递增计数信息

通过判定标准:串口助手持续显示类似Count: 0, Hex: 0x0、Count: 1, Hex: 0x1的递增输出,且无乱码、无卡死,即表示printf重定向成功。

常见问题

  • 程序卡死在printf:检查是否使用了半主机模式。在CubeIDE中需在Project Properties → C/C++ Build → Settings → MCU GCC Linker → Miscellaneous 中添加 --specs=nano.specs --specs=nosys.specs;在Keil中需勾选Target页的“Use MicroLIB”
  • 输出乱码:检查串口助手波特率是否与代码中配置一致(本例115200),以及TX/RX接线是否交叉连接
  • 无输出:确认USART1时钟已使能,且PA9/PA10复用功能配置正确
printf重定向数据流 printf("...") 标准C库函数 _write() / fputc() 重定向底层函数 逐个字符发送 USART1 外设 TX: PA9 波特率 115200 PC 串口终端 显示调试信息 应用层 重定向层(需修改) 硬件层