4. 软件日志框架:Linux printk、U-Boot log、RT-Thread ulog 对比
做多核异构调试,日志就是我们的「眼睛」。
但说实话,不同核上跑的软件栈完全不同——A核跑Linux,M核跑RT-Thread,有时候还要兼顾U-Boot的启动日志。每个框架都有自己的脾气,你得摸透了才能让它们好好配合。
这一章,我就把三个最常用的日志框架拎出来,掰开揉碎讲清楚。
4.1 Linux printk:老大哥的底蕴
printk,Linux内核里最古老的日志函数。我刚开始做驱动开发时,第一件事就是学会用它。
它的核心机制很简单:往内核日志缓冲区(kernel log buffer)里写数据。这个缓冲区是环形结构,满了就覆盖最早的记录。
关键特性:
- 日志级别:KERN_EMERG 到 KERN_DEBUG,共8级
- 输出目标:控制台、dmesg、/proc/kmsg
- 原子安全:可以在中断上下文调用
举个例子,你写驱动时经常会这样用:
// 普通调试信息
printk(KERN_DEBUG "i2c transfer complete, addr=0x%x\n", addr);
// 错误上报
printk(KERN_ERR "dma timeout, status=0x%x\n", status);
// 致命错误
printk(KERN_CRIT "watchdog reset triggered!\n");
我个人习惯把调试级别的printk用宏包一下,方便开关:
#define DRV_DEBUG(fmt, ...) \
printk(KERN_DEBUG "[mydrv] " fmt, ##__VA_ARGS__)
嗯,这里要注意——printk在中断里用没问题,但别在里面做复杂计算。我在项目中遇到过有人直接在printk里调了spin_lock,结果死锁了,查了一整天。
避坑指南:
我曾经在SMP系统上遇到过printk乱序的问题。多核同时打印时,日志顺序可能和实际执行顺序不一致。解决方案是加时间戳,或者用trace_printk替代。
4.2 U-Boot log:轻量级的启动日志
U-Boot的日志系统,说白了就是printk的简化版。它没有复杂的缓冲区管理,也没有多级过滤,主打一个「够用就行」。
U-Boot的日志核心是log()宏,定义在include/log.h里:
// 日志级别
#define LOGL_EMERG 0
#define LOGL_CRIT 1
#define LOGL_ERR 2
#define LOGL_WARN 3
#define LOGL_INFO 4
#define LOGL_DEBUG 5
// 使用示例
log(LOGL_INFO, "DDR init at %d MHz\n", freq);
log(LOGL_ERR, "SD card detect failed\n");
U-Boot的日志输出目标很直接——串口。没有dmesg,没有文件系统,全靠串口线。你想想看,在板子刚上电那会儿,串口就是唯一的救命稻草。
调试技巧:
我建议在U-Boot里把日志级别调到最高(LOGL_DEBUG),然后通过环境变量控制是否输出。比如:
setenv log_level 5
saveenv
这样可以在不重新编译的情况下切换调试模式。
U-Boot的日志还有一个特点——它支持设备树控制。你可以通过设备树节点来开关某个驱动的日志,这在调试多核启动时特别有用。
4.3 RT-Thread ulog:小而美的日志框架
RT-Thread的ulog,是我个人最喜欢的嵌入式日志框架。它设计得很精巧,资源占用极低,但功能一点不少。
ulog的核心设计理念是「模块化」和「可配置」。它把日志分为三个维度:
- 级别:DEBUG、INFO、WARN、ERROR、ASSERT
- 标签:每个模块有自己的标签,比如"i2c"、"uart"
- 过滤器:可以按级别和标签组合过滤
看个例子:
// 定义模块标签
#define LOG_TAG "i2c"
#define LOG_LVL LOG_LVL_DBG
#include <ulog.h>
void i2c_transfer(void) {
LOG_D("transfer start, addr=0x%x", addr);
if (ret != RT_EOK) {
LOG_E("transfer failed, err=%d", ret);
}
LOG_D("transfer done");
}
ulog最让我惊艳的是它的后端机制。你可以同时把日志输出到串口、文件系统、网络,甚至自定义后端。我在项目中就写过把日志通过SPI转发到另一个核的后端,调试异构通信时简直不要太爽。
ulog后端配置示例:
// 使能串口后端
ulog_backend_register(&ulog_backend_console);
// 使能文件系统后端
ulog_backend_register(&ulog_backend_file);
// 自定义后端(比如转发到另一个核)
static void my_backend_output(struct ulog_backend *backend,
rt_uint32_t level,
const char *tag,
rt_bool_t is_raw,
const char *log,
rt_size_t len) {
// 通过共享内存发送到另一个核
shmem_send(log, len);
}
4.4 三框架对比:一张表说清楚
我把三个框架的核心差异整理成了表格,方便你快速查阅:
| 特性 | Linux printk | U-Boot log | RT-Thread ulog |
|---|---|---|---|
| 资源占用 | 高(依赖内核) | 极低 | 低(可裁剪) |
| 日志级别 | 8级 | 6级 | 5级 + ASSERT |
| 输出目标 | 控制台、dmesg、文件 | 串口 | 串口、文件、网络、自定义 |
| 中断安全 | 是 | 是 | 是(需配置) |
| 多核支持 | 原生支持 | 无 | 需自行实现 |
| 过滤能力 | 级别 + 模块 | 级别 | 级别 + 标签 + 关键字 |
| 时间戳 | 支持 | 需手动添加 | 支持(微秒级) |
4.5 多核异构场景下的选择策略
好了,三个框架都讲完了。那在实际的多核异构项目里,该怎么选?
我个人的经验是:
- A核(Linux):用printk,配合trace工具做性能分析
- M核(RT-Thread):用ulog,开启文件系统后端,把日志存到共享存储区
- 启动阶段(U-Boot):用U-Boot log,只输出关键信息,避免刷屏
为什么会这样?因为每个框架的设计哲学不同。printk适合「全量记录」,ulog适合「灵活过滤」,U-Boot log适合「快速定位」。
我曾经在一个项目里把三个框架串起来用——U-Boot启动时记录硬件初始化状态,Linux起来后读取U-Boot的日志缓冲区,RT-Thread的ulog通过共享内存把实时日志推给Linux。这样,一个统一的日志视图就出来了。
小技巧:
在多核异构系统里,我建议给每个核的日志加上固定的前缀。比如:
[A] [printk] CPU freq set to 1.2GHz
[M] [ulog] sensor data ready
[U] [uboot] DDR training complete
这样一眼就能看出日志来自哪个核,排查问题快得多。
4.6 本章小结
三个日志框架,三种设计思路。printk追求通用性,U-Boot log追求轻量,ulog追求灵活。没有绝对的好坏,只有合不合适的场景。
你想想看,如果你的M核只有64KB RAM,硬上printk肯定不现实。反过来,Linux内核里用ulog,又有点大材小用。
嗯,这一章的内容就到这里。记住一句话:日志框架是工具,怎么用好它,取决于你对系统的理解。