3. 添加启动日志输出功能

添加启动日志输出功能 学习卡片

任务目标

在Bootloader启动流程中初始化UART外设,通过串口输出带时间戳和日志级别的启动信息,用于调试与状态监控。本案例基于STM32F407平台,实现最小可复用的日志模块,不依赖RTOS。

硬件依赖

  • USART2:PA2(TX)、PA3(RX),波特率115200,8N1
  • 时钟:APB1时钟42MHz(系统时钟168MHz时)

实现代码

// boot_log.h
#ifndef BOOT_LOG_H
#define BOOT_LOG_H

#include <stdint.h>

#define LOG_LEVEL_DEBUG   0
#define LOG_LEVEL_INFO    1
#define LOG_LEVEL_WARN    2
#define LOG_LEVEL_ERROR   3

#define LOG_LEVEL         LOG_LEVEL_DEBUG

#define LOG_DEBUG(fmt, ...) \
    log_output(LOG_LEVEL_DEBUG, "DBG", fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) \
    log_output(LOG_LEVEL_INFO, "INF", fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) \
    log_output(LOG_LEVEL_WARN, "WRN", fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) \
    log_output(LOG_LEVEL_ERROR, "ERR", fmt, ##__VA_ARGS__)

void log_init(void);
void log_output(uint8_t level, const char *tag, const char *fmt, ...);

#endif // BOOT_LOG_H
// boot_log.c
#include "boot_log.h"
#include <stdarg.h>
#include <stdio.h>
#include "stm32f4xx.h"

static uint32_t tick_ms;

static void uart2_init(void) {
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    RCC->APB1ENR |= RCC_APB1ENR_USART2EN;

    GPIOA->AFR[0] |= (7 << 4) | (7 << 8);  // PA2=AF7, PA3=AF7
    GPIOA->MODER |= (2 << 4) | (2 << 6);   // PA2=AF, PA3=AF

    USART2->BRR = 42000000 / 115200;        // 42MHz / 115200
    USART2->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;
}

static void uart2_putc(char c) {
    while (!(USART2->SR & USART_SR_TXE));
    USART2->DR = c;
}

static void uart2_puts(const char *s) {
    while (*s) uart2_putc(*s++);
}

static void put_hex(uint32_t val) {
    char buf[11];
    buf[0] = '0'; buf[1] = 'x';
    for (int i = 0; i < 8; i++) {
        uint8_t nib = (val >> (28 - i * 4)) & 0xF;
        buf[2 + i] = nib < 10 ? '0' + nib : 'A' + nib - 10;
    }
    buf[10] = '\0';
    uart2_puts(buf);
}

static void put_dec(uint32_t val) {
    char buf[12];
    int i = 11;
    buf[i--] = '\0';
    if (val == 0) buf[i--] = '0';
    while (val > 0 && i >= 0) {
        buf[i--] = '0' + (val % 10);
        val /= 10;
    }
    uart2_puts(&buf[i + 1]);
}

void log_init(void) {
    uart2_init();
    tick_ms = 0;
    SysTick->LOAD = 168000 - 1;  // 1ms @168MHz
    SysTick->VAL = 0;
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;
}

void log_output(uint8_t level, const char *tag, const char *fmt, ...) {
    if (level < LOG_LEVEL) return;

    // 时间戳 [ms]
    uart2_putc('[');
    put_dec(tick_ms);
    uart2_puts("ms][");
    uart2_puts(tag);
    uart2_puts("] ");

    char buf[128];
    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, sizeof(buf), fmt, args);
    va_end(args);
    uart2_puts(buf);
    uart2_putc('\r');
    uart2_putc('\n');
}

// 在SysTick_Handler中调用
void SysTick_Handler(void) {
    tick_ms++;
}
// main.c 调用示例
#include "boot_log.h"

int main(void) {
    log_init();
    LOG_INFO("Bootloader started");
    LOG_DEBUG("Reset cause: 0x%08X", RCC->CSR & 0x1F);
    LOG_WARN("Flash size: %d KB", *(uint16_t *)0x1FFF7A22);
    LOG_ERROR("Invalid app magic");

    while (1);
}

编译与运行

  1. 将三个代码块分别保存为boot_log.h、boot_log.c,并加入工程编译
  2. 在stm32f4xx_it.c中确认SysTick_Handler已调用tick_ms++(或将上述Handler实现加入工程)
  3. 使用USB转TTL连接PA2(TX)到RX,GND共地
  4. 编译烧录后打开串口助手,波特率115200

验证方法

复位开发板后,串口助手应收到以下格式输出(数值因芯片实际值而异):

[0ms][INF] Bootloader started
[0ms][DBG] Reset cause: 0x00000000
[0ms][WRN] Flash size: 1024 KB
[0ms][ERR] Invalid app magic

判定通过条件:

  • 四条日志均按级别正确输出,时间戳随SysTick递增(若在日志间插入延时,时间戳应相应增加)
  • 十六进制与十进制数值格式正确,无乱码
  • 改变LOG_LEVEL为LOG_LEVEL_WARN后重新编译,DEBUG和INFO日志不再输出

常见问题

  • 无输出:检查PA2/PA3复用配置是否正确,确认USART2时钟源为APB1(42MHz),若时钟不同需重新计算BRR
  • 乱码:波特率不匹配或BRR计算错误,核对系统时钟频率
  • 时间戳不更新:确认SysTick_Handler被正确调用,且中断优先级未屏蔽