12、日志与调试:日志级别设计、日志格式规范、条件编译调试代码、日志性能影响

日志这东西,说简单也简单,就是printf嘛。但说复杂,它真能决定一个嵌入式项目是死是活。

我见过太多项目,上线后出问题,连个日志都没有,全靠工程师拿示波器戳。还有的项目,日志倒是打了一堆,但全是「error 1」、「error 2」这种,看了等于没看。

今天咱们就把日志这件事聊透。从级别设计,到格式规范,再到条件编译和性能影响,一步到位。

12.1 日志级别设计:别让日志变成噪音

日志级别,说白了就是给信息分个三六九等。为什么需要这个?你想想看,一个跑着的系统,每秒可能产生上千条日志。如果全打出来,你根本找不到重点。

我个人习惯用这五个级别,够用且不啰嗦:

级别 名称 含义 典型场景
0 FATAL 致命错误 系统即将崩溃,必须立即处理
1 ERROR 错误 功能异常,但系统还能跑
2 WARN 警告 潜在风险,需要关注
3 INFO 信息 关键流程、状态变化
4 DEBUG 调试 开发阶段用的详细输出
我的习惯: 正式发布版本只保留 FATAL、ERROR、WARN 三个级别。INFO 和 DEBUG 只在开发阶段打开。这样既保证了问题可追溯,又不会拖慢系统。

我曾经在一个电机控制项目里,把所有日志都打开了。结果电机一转,串口被日志塞满,控制指令都发不出去了。嗯,从那以后,我学会了「日志分级」这个基本功。

12.2 日志格式规范:让日志会说话

光有级别还不够,格式也得统一。我见过最离谱的日志,就一行字:「温度过高」。谁打的?什么时候打的?哪个模块?一概不知。

我建议的日志格式长这样:

[时间戳] [级别] [模块名] [文件名:行号] 消息内容

举个例子:

[2025-03-15 14:23:45.123] [ERROR] [BMS] [bms_temp.c:128] 电池温度异常:85.3°C,超过阈值80.0°C

这条日志一看就明白:

  • 什么时候发生的(精确到毫秒)
  • 什么级别(ERROR)
  • 哪个模块(BMS,电池管理系统)
  • 哪个文件哪一行(方便定位代码)
  • 具体什么问题(温度超标,还有具体数值)

这才是能用的日志。不是给人看的,是给「查问题的人」看的。

注意: 时间戳一定要用系统时间,不要用相对时间(比如「开机后1234ms」)。否则系统跑几天后,你根本不知道那个时间点对应什么场景。

12.3 条件编译调试代码:把调试代码和正式代码分开

嵌入式开发里,调试代码和正式代码混在一起是大忌。你总不能在发布版本里还留着printf吧?

我的做法是用条件编译,干净利落:

#define LOG_LEVEL 3  // 0=FATAL, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG

#define LOG_FATAL(fmt, ...) \
    do { if (LOG_LEVEL >= 0) printf("[FATAL] " fmt "\r\n", ##__VA_ARGS__); } while(0)

#define LOG_ERROR(fmt, ...) \
    do { if (LOG_LEVEL >= 1) printf("[ERROR] " fmt "\r\n", ##__VA_ARGS__); } while(0)

#define LOG_WARN(fmt, ...) \
    do { if (LOG_LEVEL >= 2) printf("[WARN]  " fmt "\r\n", ##__VA_ARGS__); } while(0)

#define LOG_INFO(fmt, ...) \
    do { if (LOG_LEVEL >= 3) printf("[INFO]  " fmt "\r\n", ##__VA_ARGS__); } while(0)

#define LOG_DEBUG(fmt, ...) \
    do { if (LOG_LEVEL >= 4) printf("[DEBUG] " fmt "\r\n", ##__VA_ARGS__); } while(0)

使用时就像这样:

void adc_read(void) {
    uint32_t value = HAL_ADC_GetValue(&hadc1);
    LOG_DEBUG("ADC原始值: %lu", value);
    
    if (value > 4095) {
        LOG_ERROR("ADC值越界: %lu", value);
    }
    
    LOG_INFO("ADC转换完成,值: %lu", value);
}

发布版本时,把 LOG_LEVEL 改成 2 或 1,DEBUG 和 INFO 级别的日志就自动消失了。编译器会优化掉这些代码,不会留下任何痕迹。

避坑指南: 我曾经在宏定义里忘了加 do-while(0) 包裹,结果在 if-else 语句里出了大问题。记住,多语句宏一定要用 do-while(0) 包起来,这是 C 语言的老规矩。

12.4 日志性能影响:别让日志拖垮系统

日志不是免费的。每打一条日志,CPU 就要执行一次格式化、一次字符串拷贝、一次外设写入。在实时性要求高的系统里,这可能是致命的。

我遇到过最夸张的情况:一个中断服务函数里打了 DEBUG 日志,结果中断执行时间从 5us 变成了 200us。整个系统的实时性全毁了。

所以,关于日志性能,我有几条铁律:

  • 中断里不要打日志 —— 除非是 FATAL 级别,而且你准备让系统停下来
  • 高频循环里慎用 INFO 级别 —— 1kHz 的控制循环,每圈打一条日志,串口根本扛不住
  • 用异步日志 —— 把日志丢到队列里,让后台任务慢慢写,别阻塞主流程
  • 字符串格式化很贵 —— 能用整数就别用浮点,能用 %d 就别用 %s

下面这张图,是我自己总结的日志性能影响评估:

日志性能影响评估 安全区 警戒区 危险区 FATAL / ERROR 主循环外 低频事件 异步输出 ✅ 几乎无影响 WARN / INFO 主循环内 100Hz以上 同步输出 ⚠️ 可能影响时序 DEBUG 中断服务函数 1kHz以上 同步输出 ❌ 系统崩溃风险 频率越高、级别越低、输出越同步 → 性能影响越大

核心原则: 日志是调试工具,不是业务逻辑。别让日志反过来控制你的系统行为。如果关掉日志系统就跑不动了,说明你的代码本身就有问题。

最后说一句,日志系统设计好了,调试效率能翻倍。设计不好,那就是给自己挖坑。我这些年踩过的坑,总结下来就一句话:日志要「够用就好」,别贪多


公众号:蓝海资料掘金营,微信deep3321