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,又有点大材小用。

嗯,这一章的内容就到这里。记住一句话:日志框架是工具,怎么用好它,取决于你对系统的理解。


专注资料整理