1. 项目概述:为什么需要定制化LOG系统

在嵌入式开发中,调试信息输出是排查问题的生命线。标准的printf功能单一,无法满足复杂场景需求。这个Micro LOG驱动通过宏定义魔法,实现了:

  • 带颜色分级:信息/警告/错误一目了然
  • 自动时间戳:精确到毫秒的运行时序分析
  • 代码定位:自动显示文件名、行号、函数名
  • 零运行时开销:所有功能在编译期完成

示例代码:STM32F103RCT6 + STM32CubeMX + HAL库,通过USART1输出(波特率115200)


2. 工程配置

2.1 STM32CubeMX 配置

①:配置时钟,然后设置时钟树,HCLK设置为72MHz,确保HAL_GetTick精度为1ms

②:调试接口:SYS中Debug选项选择Serial Wire,防止烧录后无法连接

③:USART1配置:模式设为Asynchronous,参数默认115200-8-N-1

④:NVIC:如需要中断接收可开启USART1全局中断(本LOG方案仅使用轮询发送)

2.2 Keil5配置

新增文件夹与.c.h文件,将文章末尾的源码复制进去:

Project/
├── Core/
├── Drivers/
├── Micro_LOG/          ← 新增
│   ├── micro_log.c
│   ├── micro_log.h
└── MDK-ARM/

(1)添加文件micro_log.c.h 到工程文件中,然后keil5添加组,加入.c.h文件:

(2) 添加头文件路径:

(3) 必须勾选Use MicroLIB选项(Options for Target -> Target),否则printf会链接标准C库,导致HAL_UART_Transmit无法正常工作,程序卡死。


3. 移植内容,适配工程

3.1 重定向原理

单片机没有终端,需要将printf的输出重定向到串口。C库通过调用底层fputc函数实现每个字符的输出:

int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
  return ch;
}

为何必须勾选MicroLIB?

  • 标准C库printf依赖大量系统调用(文件系统、内存管理等),在裸机环境无法链接
  • MicroLIB是Keil为嵌入式裁剪的精简库,允许用户自定义fputc实现
  • 不勾选会导致链接错误或运行时异常(通常表现为程序启动后立即死机)

3.2 头文件内容修改

只需要修改micro_log.h头文件中的 串口打印 修改为自己的函数接口,以及还有时间tick获取函数,其他无需修改


4. 宏的魔法:编译器内置符号与变参宏

4.1 编译器自动提供的定位信息

这些宏由编译器在预处理阶段自动替换:

  • __FILE__:当前源文件的完整路径字符串(如 "C:\\Project\\Src\\main.c"
  • __LINE__:当前代码行号整数
  • __FUNCTION__:当前函数名字符串
  • __VA_ARGS__:可变参数宏的参数列表

4.2 变参宏的两种格式

基础格式:

#define LOG(fmt, ...) log_printf(fmt, ##__VA_ARGS__)
  • ...代表可变参数,调用时fmt后的所有参数会被打包
  • __VA_ARGS__在宏展开时解包为实际参数
  • ##__VA_ARGS__##作用是:当可变参数为空时,自动删除前面的逗号,避免编译错误

带颜色格式:

#define LOG_C(color, fmt, ...) log_printf(color fmt CLR_RESET, ##__VA_ARGS__)
  • 这里用到了字符串自动拼接特性:color fmt CLR_RESET会被连接成一个格式字符串
  • 例如:LOG_C(CLR_RED, "Error: %d", 500) 展开为 log_printf("\x1b[31m" "Error: %d" "\x1b[0m", 500)
  • C语言中相邻字符串字面量会自动拼接,最终为 log_printf("\x1b[31mError: %d\x1b[0m", 500)

5. 三大核心功能实现原理

5.1 时间戳机制

基于HAL_GetTick()获取系统启动后的毫秒计数(32位循环计数):

// 转换逻辑:总毫秒 → 时分秒毫秒
uint32_t log_time = HAL_GetTick();
uint16_t msec = log_time % 1000;
uint8_t sec = (log_time / 1000) % 60;
uint8_t min = (log_time / 1000 / 60) % 60;
uint8_t hour = log_time / 1000 / 60 / 60;

精度与范围:

  • 精度1ms,最大计时约49天(2^32 ms)
  • 超过24小时不自动回零,持续累加

5.2 颜色输出原理

利用ANSI转义码控制终端显示属性:

  • 格式:\x1b[XXm\033[XXm
  • \x1b是ESC键的ASCII码(十进制27)
  • [31m设置前景色为红色,[0m重置所有属性

单片机串口发送这些字符后,PC端串口助手必须支持ANSI解析才能显示颜色(如MobaXterm、Xshell、VSCode Serial Monitor),教程中使用的软件是微软商店下载的“串口调试助手”。

5.3 代码定位实现

通过__FILE____LINE____FUNCTION__在调用点捕获信息:

LOG_I("初始化完成");
// 实际展开:
log_printf(CLR_GREEN "[%s][%s:%d] 初始化完成\n" CLR_RESET,
           log_get_time(), 
           log_get_filename("C:\\Project\\Src\\main.c"),  // __FILE__的值
            __FUNCTION__, 								  //__FUNCTION__的值
           98);                                           // __LINE__的值

log_get_filename()函数使用strrchr查找最后一个路径分隔符,返回文件名部分,避免长路径占用串口带宽。


6. 断言机制:运行时检查利器

LOG_ASSERT(expr);

#define LOG_ASSERT(expr)                                                                              \
    do                                                                                                \
    {                                                                                                 \
        if (!(expr))                                                                                  \
        {                                                                                             \
            log_printf(CLR_RED "\r\n[ASSERT FAILED] %s\r\nFile: %s:%d\r\nFunction: %s\r\n" CLR_RESET, \
                       #expr, log_get_filename(__FILE__), __LINE__, __FUNCTION__);                    \
            while (1)                                                                                 \
                ;                                                                                     \
         }                                                                                             \
     } while (0)

expr为假时,打印错误信息并死循环:

  • 打印失败表达式字符串(#expr的字符串化操作)
  • 显示文件名、行号、函数名
  • 锁定程序防止继续运行

适用场景:

  • 指针空检查:LOG_ASSERT(p != NULL);
  • 参数合法性:LOG_ASSERT(size <= MAX_SIZE);
  • 状态机完整性:在default分支中触发

7. 完整使用示例

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  LOG_I("系统启动,版本v1.0\r\n");  // 绿色普通信息
  
  while (1)
  {
    LOG("普通打印:%d\r\n", 123); // 无附加信息

    LOG_C(CLR_GREEN, "纯绿色文本\r\n"); // 仅颜色

    LOG_T("带时间戳:传感器值=%d\r\n", 1234); // 时间+数据

    LOG_TC(CLR_BLUE, "蓝色时间戳\r\n"); // 颜色+时间

    LOG_W("电压偏低:%.2fV\r\n", 3.16); // 黄色警告+全信息

    LOG_E("通信超时!\r\n"); // 红色错误+全信息

    HAL_Delay(1000);
  }
}

8. 效果展示与验证

预期输出格式:

[00:00:00.001][main.c,main:98] 系统启动,版本v1.0
普通打印:123
纯绿色文本
[00:00:00.010] 带时间戳:传感器值=1234
[00:00:00.014] 蓝色时间戳
[00:00:00.017][main.c,main:117] 电压偏低:3.14V
[00:00:00.023][main.c,main:119] 通信超时!

9.结语

这个Micro LOG方案在最小资源占用下提供了专业级的调试体验,关键是所有功能通过宏在编译期展开,不增加运行时负担。代码已验证可在STM32F103RCT6上稳定运行,如移植其他芯片,只需要适配自己的printf函数即可。


10.源码

扫描下方二维码加入嵌入式技术交流群,即可获取源码压缩包,群内同步答疑 AT 驱动开发、移植问题及后续版本更新,同时有更多嵌入式问题也欢迎讨论~ +q:181921938

MicroLOG.zip

源码遵循开源TIM协议,如果这个项目对你的物联网开发有帮助,请给它一个 ⭐ !:

10.1 micro_log.c

#include "micro_log.h"
#include "usart.h"

static char log_time_str[16];

int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
  return ch;
}

/**
 * @brief   获取当前时间
 * @param   无
 * @retval  无
 */
char *log_get_time(void)
{
    uint32_t log_time = log_get_tick();
    uint16_t msec = log_time % 1000;
    log_time /= 1000;
    uint8_t sec = log_time % 60;
    log_time /= 60;
    uint8_t min = log_time % 60;
    log_time /= 60;
    uint8_t hour = log_time;

    snprintf(log_time_str, sizeof(log_time_str), "%02d:%02d:%02d.%03d", hour, min, sec, msec);
    return log_time_str;
}

/**
 * @brief   从路径中提取文件名
 * @param   path 文件路径
 * @retval  文件名指针
 */
const char *log_get_filename(const char *path)
{
    const char *p = strrchr(path, '/');
    if (p)
        return p + 1;

    p = strrchr(path, '\\');
    if (p)
        return p + 1;

    return path;
}

10.2 micro_log.h

#ifndef _MICRO_LOG_H
#define _MICRO_LOG_H

#include <stdio.h>
#include <string.h>

#include "main.h"

char *log_get_time(void);       // 获取时间
const char *log_get_filename(const char *path); // 获取文件名

#define log_printf(fmt, ...) printf(fmt, ##__VA_ARGS__) // 核心函数打印接口
#define log_get_tick HAL_GetTick                        // 获取系统滴答计数

/* ============== 颜色定义 ============== */
#define CLR_RED         "\x1b[31m"      // 红色
#define CLR_ORANGE      "\x1b[33;1m"    // 橙色
#define CLR_YELLOW      "\x1b[33m"      // 黄色
#define CLR_GREEN       "\x1b[32m"      // 绿色
#define CLR_BLUE        "\x1b[34m"      // 蓝色
#define CLR_PURPLE      "\x1b[35m"      // 紫色
#define CLR_RESET       "\x1b[0m"       // 重置颜色


/* ============== 核心日志接口 ============== */

// 1. LOG(...) - 无时间戳printf,无颜色
#define LOG(fmt, ...) log_printf(fmt, ##__VA_ARGS__)

// 2. LOG_C(color, ...) - 无时间戳printf,带颜色
#define LOG_C(color, fmt, ...) log_printf(color fmt CLR_RESET, ##__VA_ARGS__)

// 3. LOG_T(...) - 带时间戳printf,无颜色
#define LOG_T(fmt, ...) log_printf("[%s] " fmt, log_get_time(), ##__VA_ARGS__)

// 4. LOG_TC(color, ...) - 带时间戳printf,带颜色
#define LOG_TC(color, fmt, ...) log_printf(color "[%s] " fmt CLR_RESET, log_get_time(), ##__VA_ARGS__)

/* ============== 文件位置+时间戳日志 ============== */

// LOG_I - 文件+时间信息(绿色)
#define LOG_I(fmt, ...) log_printf(CLR_GREEN "[%s][%s,%s:%d] " fmt CLR_RESET, \
                                   log_get_time(), log_get_filename(__FILE__), __FUNCTION__, __LINE__, ##__VA_ARGS__)

// LOG_W - 文件+时间警告(黄色)
#define LOG_W(fmt, ...) log_printf(CLR_YELLOW "[%s][%s,%s:%d] " fmt CLR_RESET, \
                                   log_get_time(), log_get_filename(__FILE__), __FUNCTION__, __LINE__, ##__VA_ARGS__)

// LOG_E - 文件+时间错误(红色)
#define LOG_E(fmt, ...) log_printf(CLR_RED "[%s][%s,%s:%d] " fmt CLR_RESET, \
                                   log_get_time(), log_get_filename(__FILE__), __FUNCTION__, __LINE__, ##__VA_ARGS__)

/* ============== 断言宏 ============== */
#define LOG_ASSERT(expr)                                                                              \
    do                                                                                                \
    {                                                                                                 \
        if (!(expr))                                                                                  \
        {                                                                                             \
            log_printf(CLR_RED "\r\n[ASSERT FAILED] %s\r\nFile: %s:%d\r\nFunction: %s\r\n" CLR_RESET, \
                       #expr, log_get_filename(__FILE__), __LINE__, __FUNCTION__);                    \
            while (1)                                                                                 \
                ;                                                                                     \
        }                                                                                             \
    } while (0)
    
#endif /* _MICRO_LOG_H */

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐