3、日志级别与分类:Error、Warning、Info、Debug 的定义与使用场景

日志级别这东西,说白了就是给 BootROM 的“话”分个轻重缓急。你想想看,系统刚上电那会儿,成千上万个模块同时跑,要是每个模块都噼里啪啦往外吐信息,你根本分不清哪个是致命故障,哪个只是例行打卡。

我个人习惯把日志级别比作交通信号灯:Error 是红灯,必须停;Warning 是黄灯,得留个心眼;Info 是绿灯,正常通行;Debug 嘛,就像路边的施工告示,平时看不见,只有你凑近了才需要。

3.1 四个级别的定义与使用场景

级别 数值(常用) 含义 典型使用场景
Error 3 系统无法继续正常执行,必须干预 内存训练失败、关键寄存器读回异常、CRC 校验不通过
Warning 4 出现非预期状况,但系统仍可降级运行 SPI 时钟频率降级、某条内存通道未插满、温度接近阈值
Info 6 记录关键流程节点,用于追踪启动进度 进入某阶段、检测到某设备、版本号打印
Debug 7 开发调试用,包含寄存器值、状态机跳转等细节 每步初始化前后寄存器对比、循环计数、临时变量值

核心原则:生产环境只开 Error + Warning + Info,Debug 级别只在开发板或实验室环境开启。我见过有人把 Debug 日志带到量产固件里,结果 UART 波特率 115200 都扛不住那数据量,启动时间直接翻倍。

3.2 日志分类:按模块切分

光有级别还不够。你想想,一个 BootROM 日志可能有几千行,如果所有级别的日志混在一起,你找内存相关的信息还得 Ctrl+F 搜 "DDR",多累啊。所以,分类是日志管理的第二把刀。

我一般把 BootROM 日志分成三大类:

  • 硬件初始化(HW Init):CPU 微架构初始化、PLL 锁相环配置、时钟树建立、电源域上电时序。这类日志通常最早出现,也最容易被忽略——但一旦出问题,系统连第一条 Info 都看不到。
  • 内存检测(Memory):DDR 控制器初始化、PHY 训练、内存拓扑探测、ECC 初始化、内存测试(如走步、March C)。这是 BootROM 最核心也最复杂的部分,日志量通常占 60% 以上。
  • 外设枚举(Peripheral Enumeration):SPI Flash 识别、eMMC 初始化、USB 设备枚举、PCIe 链路训练。这类日志决定了你能不能从外部介质加载下一级固件。

我的习惯:在日志前缀里加上分类标签,比如 [HW]、[MEM]、[PERI]。这样 grep 的时候一行命令就能过滤出你想要的信息。比如 grep "\[MEM\]" bootrom.log,只看内存相关的日志。

3.3 日志级别与分类的交叉矩阵

级别和分类不是孤立的。它们组合起来,才能构成一个完整的日志体系。我画了张图,帮你理清这个关系:

日志级别 × 分类 交叉矩阵 Error Warning Info Debug 硬件初始化 内存检测 外设枚举 PLL 失锁 电源域异常 时钟精度偏差 电压略低 PLL 锁定完成 时钟树建立 每步寄存器值 状态机跳转 训练失败 ECC 错误 某通道未插满 时序余量偏低 DDR 初始化完成 容量检测结果 每步训练参数 眼图扫描数据 设备无响应 枚举超时 设备降速运行 描述符异常 设备识别完成 固件加载开始 枚举循环细节 协议握手数据

3.4 实战中的级别选择原则

级别怎么选?我总结了几条经验,你可以直接拿来用:

  1. Error 要少而精:只有系统确实无法继续时,才打 Error。我曾经接手过一个项目,代码里到处都是 Error 级别的日志,结果真正出问题时,反而没人关注了——狼来了的故事,你懂的。
  2. Warning 要可操作:Warning 不能只是“发现异常”,最好能给出建议。比如“SPI 时钟频率从 100MHz 降级到 50MHz,建议检查 PCB 走线长度”。
  3. Info 要可追溯:每个 Info 日志最好带上时间戳或阶段编号。这样你回看日志时,能一眼看出系统卡在了哪个阶段。
  4. Debug 要可开关:Debug 日志一定要有编译开关或运行时开关。我习惯用 #ifdef DEBUG_MEM 来控制内存调试信息的输出,而不是全局一个 DEBUG 宏。

避坑指南:我曾经在一个项目中,Debug 日志里直接打印了敏感信息——比如内存中残留的密钥。结果客户在做安全审计时发现了,直接打了回来。从那以后,我要求所有 Debug 日志必须经过安全审查,并且生产固件强制关闭 Debug 输出。

3.5 日志分类的代码示例

说了这么多,不如看一段实际代码。这是我常用的日志宏定义方式:

/* 日志级别 */
#define LOG_LEVEL_ERROR  3
#define LOG_LEVEL_WARN   4
#define LOG_LEVEL_INFO   6
#define LOG_LEVEL_DEBUG  7

/* 日志分类 */
#define LOG_CAT_HW       0x01  /* 硬件初始化 */
#define LOG_CAT_MEM      0x02  /* 内存检测 */
#define LOG_CAT_PERI     0x04  /* 外设枚举 */

/* 日志宏 */
#define LOG_PRINT(level, cat, fmt, ...) \
    do { \
        if ((level) <= g_log_level && (cat) & g_log_cat_mask) { \
            printf("[%s][%s] " fmt "\r\n", \
                   log_level_str(level), \
                   log_cat_str(cat), \
                   ##__VA_ARGS__); \
        } \
    } while(0)

/* 使用示例 */
LOG_PRINT(LOG_LEVEL_INFO, LOG_CAT_MEM,
          "DDR training started, channel %d", ch);
LOG_PRINT(LOG_LEVEL_ERROR, LOG_CAT_HW,
          "PLL lock failed, status = 0x%08x", status);

嗯,这里要注意:g_log_level 和 g_log_cat_mask 可以在运行时通过 UART 命令修改。这样你在调试时,可以动态开关某个分类的日志,不用重新编译固件。

3.6 日志级别与分类的联动分析

最后说一个实战技巧:当 Error 和 Warning 同时出现在同一个分类时,优先看 Error,但别忽略 Warning。

举个例子,有一次我在调试一块新板子,日志里同时出现了:

  • [MEM][Error] DDR rank 0 training failed, Vref drift out of range
  • [MEM][Warning] DDR rank 1 Vref margin only 3%, below threshold 5%

表面上看,Error 是 rank 0 的问题。但 Warning 提示 rank 1 的余量也不足。这说明问题可能出在共同的电源域或参考电压上,而不是某个 rank 本身。我顺着这个思路查下去,发现是 VREF 分压电阻虚焊了。

你看,级别告诉你严重程度,分类告诉你问题在哪,两者结合才能找到根因。