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 日志级别与分类的交叉矩阵
级别和分类不是孤立的。它们组合起来,才能构成一个完整的日志体系。我画了张图,帮你理清这个关系:
3.4 实战中的级别选择原则
级别怎么选?我总结了几条经验,你可以直接拿来用:
- Error 要少而精:只有系统确实无法继续时,才打 Error。我曾经接手过一个项目,代码里到处都是 Error 级别的日志,结果真正出问题时,反而没人关注了——狼来了的故事,你懂的。
- Warning 要可操作:Warning 不能只是“发现异常”,最好能给出建议。比如“SPI 时钟频率从 100MHz 降级到 50MHz,建议检查 PCB 走线长度”。
- Info 要可追溯:每个 Info 日志最好带上时间戳或阶段编号。这样你回看日志时,能一眼看出系统卡在了哪个阶段。
- 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 分压电阻虚焊了。
你看,级别告诉你严重程度,分类告诉你问题在哪,两者结合才能找到根因。