1 引言

在嵌入式开发当中,日志是调试排障、程序运行监控的核心工具。但新手常陷入这些困境:用printf裸打日志,调试信息杂乱无章;项目落地时需逐行注释日志,效率极低;日志无等级区分,现场运行时冗余输出占用串口 / 内存资源。

本期教大家快速开发一款极简嵌入式日志模块,完全基于标准C实现,无任何第三方依赖,核心设计满足嵌入式开发刚需:单接口调用、日志等级划分、宏独立控制等级开关,能够无缝嵌入stm32等项目中,帮助大家快速集成到自己的项目中;

核心需求

设计目标

轻量化

仅依赖标准 C 库(stdio.h/stdarg.h),无额外依赖,占用极少 ROM/RAM

接口简单

对外仅暴露LOG()一个接口,用法和printf一致,学习成本为 0

日志等级划分

划分 4 个嵌入式高频等级:DEBUG(调试)、INFO(运行)、WARNING(警告)、ERROR(错误)

等级独立可控

通过宏开关单独控制每个等级的输出,调试 / 发布版本切换仅改宏,无需动业务代码

输出使用通配

默认用printf(PC 端可直接测试),嵌入式端可无缝替换为串口 / Flash 输出

2.1 头文件解读

  • 日志等级枚举:定义 4 个嵌入式高频日志等级,用枚举替代数字,提升代码可读性和类型安全性;

  • 等级开关宏:每个宏对应一个日志等级,1 为开启、0 为关闭,可独立控制各等级输出;

  • 函数声明:log_printf是底层打印函数,上层业务代码不直接调用;

  • LOG()对外唯一接口,通过等级和宏开关判断是否输出日志,do-while(0)避免宏在条件语句中出现语法错误,##__VA_ARGS__支持可变参数,让接口用法与printf一致。

日志模块头文件
#ifndef __LOG_H__
#define __LOG_H__

#include <stdio.h>
#include <stdarg.h>

typedef enum {
    LOG_DEBUG,       
    LOG_INFO,        
    LOG_WARNING,     
    LOG_ERROR      
} LogLevel;

#define ENABLE_LOG_DEBUG    1   
#define ENABLE_LOG_INFO     1   
#define ENABLE_LOG_WARNING  1   
#define ENABLE_LOG_ERROR    1   

void log_printf(LogLevel level, const char *fmt, ...);

#define LOG(level, fmt, ...) \
    do { \
        if (level == LOG_DEBUG && ENABLE_LOG_DEBUG) { \
            log_printf(LOG_DEBUG, fmt, ##__VA_ARGS__); \
        } else if (level == LOG_INFO && ENABLE_LOG_INFO) { \
            log_printf(LOG_INFO, fmt, ##__VA_ARGS__); \
        } else if (level == LOG_WARNING && ENABLE_LOG_WARNING) { \
            log_printf(LOG_WARNING, fmt, ##__VA_ARGS__); \
        } else if (level == LOG_ERROR && ENABLE_LOG_ERROR) { \
            log_printf(LOG_ERROR, fmt, ##__VA_ARGS__); \
        } \
    } while(0)

#endif 

2.2 源文件解读

  • 等级转字符串:将枚举等级转为可视化标识,方便区分日志类型,default分支处理异常等级,避免程序崩溃;

  • 可变参数处理:va_list定义参数列表,va_start初始化列表,vprintf适配可变参数实现格式化输出,va_end释放列表,是 C 语言处理不定参数的标准方式;

  • 日志输出:先打印等级标识,再打印格式化内容,最后换行,让日志每行独立、可读性强。

日志模块源文件
#include "log.h"

void log_printf(LogLevel level, const char *fmt, ...) {
    const char *level_str = NULL;
    switch(level) {
        case LOG_DEBUG:    level_str = "[DEBUG]";    break;
        case LOG_INFO:     level_str = "[INFO]";     break;
        case LOG_WARNING:  level_str = "[WARNING]";  break;
        case LOG_ERROR:    level_str = "[ERROR]";    break;
        default:           level_str = "[UNKNOWN]";  break;
    }

    va_list args;
    va_start(args, fmt);
    printf("%s ", level_str);  
    vprintf(fmt, args);        
    va_end(args);
    printf("\r\n");           
}

2.3 测试

测试程序
#include "log.h"  

// ==================== 测试函数:验证日志模块功能 ====================
void test_log_module(void) {
    // 测试1:默认配置(开启所有等级)
    printf("===== Test 1: All log levels enabled =====\r\n");
    LOG(LOG_DEBUG,    "LiqModGet addr=0x%x, idx=%d", 0x60005, 1);
    LOG(LOG_INFO,     "System initialization completed");
    LOG(LOG_WARNING,  "UART buffer usage 85%, nearly full");
    LOG(LOG_ERROR,    "LiqModSet failed: addr=0x%x return NULL", 0x60002);

    // 测试2:临时关闭DEBUG/INFO(模拟发布版本配置)
    printf("\r\n===== Test 2: Only WARNING/ERROR enabled =====\r\n");
    #undef ENABLE_LOG_DEBUG
    #undef ENABLE_LOG_INFO
    #define ENABLE_LOG_DEBUG    0
    #define ENABLE_LOG_INFO     0

    LOG(LOG_DEBUG,    "This DEBUG log will NOT output");  // 被关闭
    LOG(LOG_INFO,     "This INFO log will NOT output");   // 被关闭
    LOG(LOG_WARNING,  "This WARNING log will output");    // 正常输出
    LOG(LOG_ERROR,    "This ERROR log will output");      // 正常输出
}

// 主函数:仅执行测试逻辑
int main(void) {
    test_log_module();
    return 0;
}

3 实际项目适配

默认的printf仅用于 PC 测试,嵌入式项目中需将输出替换为串口 / Flash,适配后,日志会通过 STM32 的 USART1 串口输出,上层LOG()接口无需任何修改。以下是 STM32 串口适配示例(核心改log.c):

#include "log.h"
#include "uart.h"  // STM32串口驱动头文件

void log_printf(LogLevel level, const char *fmt, ...) {
    const char *level_str = NULL;
    switch(level) {
        case LOG_DEBUG:    level_str = "[DEBUG]";    break;
        case LOG_INFO:     level_str = "[INFO]";     break;
        case LOG_WARNING:  level_str = "[WARNING]";  break;
        case LOG_ERROR:    level_str = "[ERROR]";    break;
        default:           level_str = "[UNKNOWN]";  break;
    }

    // 嵌入式适配:用固定缓冲区拼接日志(避免栈溢出) 重要!!!!!!!!!!!!!!!!!!!!!
    char log_buf[128] = {0};
    uint16_t len = 0;
    va_list args;
    va_start(args, fmt);

    // 1. 拼接等级标识
    len += snprintf(log_buf + len, sizeof(log_buf) - len, "%s ", level_str);
    // 2. 拼接格式化内容
    len += vsnprintf(log_buf + len, sizeof(log_buf) - len, fmt, args);
    // 3. 拼接换行
    len += snprintf(log_buf + len, sizeof(log_buf) - len, "\r\n");
    va_end(args);

    // 4. 替换为STM32串口发送函数(替换为你项目中的函数)
    HAL_UART_Transmit(&huart1, (uint8_t*)log_buf, len, 100);
}

最后,为了便于新手学习,本文删减了日志模块的很多功能,例如时间戳等;同时,读者也可根据自身实际情况进行升级,例如增加日志来源等。

Logo

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

更多推荐