拒绝学生化编程——日志管理
在嵌入式开发当中,日志是调试排障、程序运行监控的核心工具。但新手常陷入这些困境:用printf裸打日志,调试信息杂乱无章;项目落地时需逐行注释日志,效率极低;日志无等级区分,现场运行时冗余输出占用串口 / 内存资源。本期教大家快速开发一款极简嵌入式日志模块,完全基于标准C实现,无任何第三方依赖,核心设计满足嵌入式开发刚需:单接口调用、日志等级划分、宏独立控制等级开关,能够无缝嵌入stm32等项目中
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);
}
最后,为了便于新手学习,本文删减了日志模块的很多功能,例如时间戳等;同时,读者也可根据自身实际情况进行升级,例如增加日志来源等。
更多推荐
所有评论(0)