4. 日志级别与格式:ERROR/WARNING/INFO/DEBUG、时间戳、模块标签与函数调用链

做 BootROM 调试,说白了就是跟日志打交道。你对着串口终端看那一行行滚动的字符,能不能快速定位问题,很大程度上取决于你懂不懂这些日志的「规矩」。这一章我就带你把这些规矩捋清楚。

4.1 日志级别:从 DEBUG 到 ERROR

BootROM 的日志通常分四个级别。我见过不少新手上来就开 DEBUG 级别,结果被海量信息淹没了。其实每个级别都有它的用途。

级别 含义 典型场景
ERROR 致命错误,启动流程无法继续 CRC 校验失败、内存初始化失败、找不到启动设备
WARNING 非致命异常,但可能影响后续行为 时钟频率降级、备用启动路径、配置项缺失
INFO 关键流程节点,正常启动的里程碑 进入某阶段、加载某镜像、跳转到下一级
DEBUG 详细内部状态,用于开发调试 寄存器值、指针地址、循环计数、临时变量

我的习惯:量产固件只保留 ERROR 和 WARNING,INFO 可选。DEBUG 日志必须用编译开关关掉,否则既占空间又拖慢启动速度。曾经有个项目,DEBUG 日志没关,启动慢了 200 毫秒,客户直接拒收。

你想想看,如果一条日志写着 [ERROR] DDR training failed at rank 0,那基本就是内存硬件或时序有问题。但如果只是 [WARNING] VTT voltage slightly low (0.49V),系统可能还能跑,只是稳定性存疑。

4.2 时间戳格式:谁先谁后

时间戳是日志分析的第一把尺子。BootROM 里常见的时间戳有两种:

  • 绝对时间戳:从上电开始计时的微秒/毫秒数。例如 [+00123.456] 表示上电后 123.456 毫秒。
  • 相对时间戳:从上一条日志到本条日志的间隔。例如 [+0.032] 表示距离上条日志过了 32 微秒。

我个人更推荐绝对时间戳。为什么?因为你可以一眼看出整个启动流程花了多久。比如:

[+00000.001] BootROM v2.1 initialized
[+00012.345] PLL locked at 800 MHz
[+00015.678] DDR training completed
[+00018.902] Boot device detected: eMMC
[+00021.456] Image loaded to 0x8000_0000
[+00022.100] Jumping to next stage...

看到没?从初始化到跳转,总共 22.1 毫秒。如果某个环节突然多了几十毫秒,那肯定有问题。

注意:有些芯片的时间戳精度只有毫秒级,对于微秒级的异常根本看不出来。我曾经遇到过一个问题:DDR 校准偶尔超时,但毫秒级时间戳显示一切正常。后来换成微秒级时间戳,才发现每次超时都多了 300 微秒。精度不够,等于白记。

4.3 模块标签:谁发的日志

BootROM 代码通常按模块划分,每个模块有自己的标签。日志里看到标签,你就知道该去翻哪段代码了。

常见的模块标签举例:

  • [PLL] — 锁相环配置
  • [DDR] — 内存初始化
  • [STOR] — 存储设备检测
  • [IMG] — 镜像加载与校验
  • [SEC] — 安全启动相关
  • [PMU] — 电源管理单元

举个例子,一条完整的日志可能是这样:

[+00015.678] [DDR] [INFO] Rank 0: 2GB, 1600MT/s, 16-bit bus

这条日志告诉你:时间点是 15.678 毫秒,模块是 DDR,级别是 INFO,内容是 rank 0 的配置信息。一目了然。

4.4 函数调用链:谁调了谁

这是调试时最实用的功能。当 BootROM 崩溃或断言失败时,如果能打印出函数调用链,你就能直接定位到出错的代码路径。

调用链的格式通常有两种:

  • 扁平式:main -> boot_device_init -> mmc_probe -> mmc_send_cmd
  • 缩进式:每层调用缩进一次,看起来像树形结构

我见过一个实际案例:

[ERROR] Assertion failed: p->buf != NULL
  Call stack:
    [0] 0x1A0034: ddr_training_main
    [1] 0x1A0078: ddr_write_leveling
    [2] 0x1A00BC: ddr_dqs_calibration
    [3] 0x1A0100: wait_for_dll_lock

看到这个,我直接定位到 wait_for_dll_lock 函数里某个指针没判空。如果没有调用链,你可能得一行行读汇编才能找到问题点。

核心要点:调用链的深度一般控制在 8-10 层以内。太深了,日志缓冲区装不下;太浅了,信息不够。我一般建议在断言、异常、致命错误这三个地方打印完整调用链,其他地方只打印模块标签就够了。

4.5 日志格式的完整示例

把上面几个要素拼起来,一条完整的 BootROM 日志应该是这样的:

[+00018.902] [STOR] [INFO] eMMC detected: CID=12345678
[+00019.100] [STOR] [DEBUG] CMD8 response: 0x1AA
[+00019.120] [STOR] [DEBUG] CMD13 status: 0x700
[+00019.200] [IMG] [INFO] Loading image from offset 0x2000
[+00020.000] [IMG] [WARNING] Image signature not present, skipping verification
[+00021.456] [IMG] [INFO] Image loaded to 0x8000_0000, size=0x3A000
[+00022.100] [BOOT] [INFO] Jumping to 0x8000_0000

你看,时间戳、模块标签、日志级别、具体内容,一个都不少。拿到这样的日志,你就能像侦探一样,一步步还原 BootROM 的执行过程。

4.6 知识体系总览

下面这张图把日志级别、时间戳、模块标签、调用链的关系画清楚了。你可以把它当作本章的思维导图。

BootROM 日志格式知识体系 日志格式四要素 日志级别 ERROR / WARNING INFO / DEBUG 控制输出粒度 时间戳 绝对 / 相对 微秒 / 毫秒 定位时序瓶颈 模块标签 [PLL] [DDR] [STOR] [IMG] [SEC] [PMU] 快速定位代码模块 函数调用链 扁平 / 缩进 异常时打印 定位崩溃路径

4.7 避坑指南

最后分享几个我踩过的坑:

  • 别混用时间戳单位。同一个日志里,要么全用微秒,要么全用毫秒。混着用会让你算时间时一头雾水。
  • 模块标签别太长。我见过有人用 [DDR_TRAINING_CONTROLLER] 这种标签,一行日志一半都是标签。建议控制在 4-6 个字符。
  • 调用链别循环。如果 A 调 B,B 又调 A,日志缓冲区会瞬间爆炸。加个深度限制,比如最多 20 层。
  • 日志缓冲区要够大。曾经有个项目,DEBUG 日志太多,把缓冲区撑满了,最早的日志被覆盖,结果查问题查了半天才发现关键信息丢了。