日志系统基础:ATF日志等级与输出机制
做启动调试,说白了就是跟日志打交道。ATF的日志系统设计得挺精巧,但很多人一开始没搞明白,上来就开VERBOSE,结果刷屏刷到怀疑人生。我刚开始接触ATF时也犯过这毛病,后来才慢慢摸清门道。
一、ATF日志等级
ATF定义了5个日志等级,从低到高分别是:
| 等级 | 宏定义 | 数值 | 典型场景 |
|---|---|---|---|
| VERBOSE | LOG_LEVEL_VERBOSE |
40 | 函数入口/出口、寄存器值、循环细节 |
| INFO | LOG_LEVEL_INFO |
30 | 模块初始化完成、版本号、配置参数 |
| NOTICE | LOG_LEVEL_NOTICE |
20 | 正常但值得关注的事件,如电源状态切换 |
| WARNING | LOG_LEVEL_WARNING |
10 | 非致命异常,如降级运行、配置回退 |
| ERROR | LOG_LEVEL_ERROR |
0 | 致命错误,系统无法继续 |
数值越小,优先级越高。ERROR是0,VERBOSE是40。编译时通过LOG_LEVEL宏控制输出阈值——只有等级数值大于等于当前阈值的日志才会被编译进去。
关键点:日志等级是编译期决定的,不是运行时动态切换的。你编译时设了INFO,那VERBOSE的代码根本就不会被编译进去。
二、日志输出机制
ATF的日志输出,走的是console框架。我画了张图,帮你理清整体流程:
三、日志宏的使用
ATF提供了一组宏,让你在代码里打日志。常用的有:
/* VERBOSE级别,调试用 */
VERBOSE("进入函数 %s\n", __func__);
/* INFO级别,正常信息 */
INFO("BL31初始化完成,版本 %d.%d\n", major, minor);
/* NOTICE级别,值得关注 */
NOTICE("CPU频率降级到 %d MHz\n", freq);
/* WARNING级别,非致命异常 */
WARNING("电源域%d未响应,尝试复位\n", domain_id);
/* ERROR级别,致命错误 */
ERROR("内存ECC校验失败,地址 0x%lx\n", fault_addr);
这些宏最终都会调用console_putc或console_puts,把格式化后的字符串送到UART上。
我的习惯:调试阶段我会把LOG_LEVEL设成40(VERBOSE),但正式发布前一定改回20(NOTICE)或30(INFO)。曾经有一次我忘了改,结果客户现场日志刷得飞起,UART缓冲区直接撑爆,系统卡死。嗯,从那以后我每次发布前都会检查这个宏。
四、console驱动注册
ATF的console框架支持多种后端:UART、SPI、I2C、甚至虚拟串口。每个平台在启动早期必须注册console驱动,否则日志根本出不来。
注册流程大致如下:
- 硬件初始化:配置UART引脚、波特率、时钟
- 调用console_register:传入驱动结构体指针
- 设置console_scope:指定哪些安全状态可以输出日志
- 验证:打印一条测试信息,确认链路通
/* 典型的console注册代码 */
static console_t console;
void bl31_early_platform_setup(void)
{
/* 初始化UART硬件 */
uart_init(PLAT_UART_BASE, PLAT_UART_CLOCK, PLAT_UART_BAUDRATE);
/* 注册console */
int ret = console_register(&console, PLAT_UART_BASE,
PLAT_UART_CLOCK, PLAT_UART_BAUDRATE);
if (ret < 0) {
/* 注册失败,这里连ERROR都打不了,只能干瞪眼 */
return;
}
/* 设置日志作用域:仅在安全世界输出 */
console_set_scope(&console, CONSOLE_FLAG_SECURE);
/* 验证输出 */
INFO("BL31 console初始化成功\n");
}
我曾经踩过的坑:console注册必须在任何日志打印之前完成。有一次我调换了初始化顺序,结果INFO宏调用了,但console还没注册,日志全丢了。排查了半天才发现是顺序问题。记住:先注册,再打印。
五、日志等级的实际选择
不同场景下,日志等级的选择是有讲究的。我总结了一套经验:
| 场景 | 推荐等级 | 理由 |
|---|---|---|
| 日常开发调试 | VERBOSE (40) | 能看到所有细节,方便定位 |
| 功能验证 | INFO (30) | 过滤掉过于琐碎的调试信息 |
| 集成测试 | NOTICE (20) | 只关注关键事件和异常 |
| 生产发布 | WARNING (10) | 只输出警告和错误,减少性能开销 |
| 紧急故障排查 | ERROR (0) | 只保留致命错误,避免刷屏 |
你想想看,生产环境开VERBOSE会怎样?每秒几千条日志,UART根本来不及发,CPU全耗在格式化字符串上了。系统性能直接掉一截。
六、日志格式解析
ATF的日志输出有固定格式,看懂它很重要。一条典型的日志长这样:
INFO: BL31: Initializing runtime services
NOTICE: BL31: Preparing for EL3 exit to normal world
ERROR: BL31: Panic at file runtime_svc.c line 42
格式拆解:
- 等级标签:左对齐,固定宽度(INFO后面跟4个空格,NOTICE跟2个)
- 模块前缀:通常是
BL31:、BL32:、BL33:等 - 消息正文:描述性文字,可能包含函数名、行号、变量值
注意:Panic日志会附带文件名和行号,这是定位崩溃的黄金线索。我每次看到Panic,第一件事就是去查对应文件的那一行代码。
七、常见问题与避坑
最后分享几个我实际项目中遇到过的问题:
- 日志乱码:通常是波特率不匹配,或者UART时钟配置错了。先拿示波器量一下TX脚波形。
- 日志丢失:console注册太晚,或者缓冲区太小。我建议在bl31_early_platform_setup里就完成注册。
- 日志刷屏:LOG_LEVEL设太高了。生产环境用WARNING就够了,别开VERBOSE。
- 日志不输出:检查console_scope,是不是把安全世界的日志过滤掉了?
嗯,日志系统基础就讲这么多。搞懂了等级和输出机制,后面分析启动日志时你就能快速判断:哪些日志该看,哪些可以忽略,哪些是致命信号。