4. Zephyr日志系统

日志系统,说白了就是嵌入式系统的“黑匣子”。你想想看,代码跑飞了、外设没响应、内存泄漏了——没有日志,你只能靠猜。我做了这么多年嵌入式,最怕的就是那种“看起来一切正常,但就是不对”的bug。这时候,日志就是你的第一道防线。

Zephyr的日志模块设计得相当灵活。它不是简单地把printf包装一下,而是有一套完整的架构。咱们今天就把这个架构拆开看看,再聊聊怎么配置、怎么定制后端。

4.1 日志模块架构:三层分离设计

Zephyr的日志系统,核心思路是“采集”和“输出”分离。我刚开始看源码的时候,也觉得绕。但搞明白之后,你会发现这设计确实巧妙。

核心架构:日志模块分为三层——前端API层、核心处理层、后端输出层。前端只管“我要打日志”,核心层负责过滤和格式化,后端负责“把日志送到哪里去”。

咱们直接看这张图,一目了然:

Zephyr 日志模块三层架构 前端 API 层 LOG_INF() / LOG_DBG() / LOG_WRN() / LOG_ERR() 用户代码直接调用的宏接口,携带模块ID和级别 核心处理层 日志过滤器(按级别/模块过滤) 格式化器(时间戳、颜色、前缀) 消息队列 / 缓冲区管理 后端输出层 UART后端 / RTT后端 / 文件系统后端 / 网络后端 自定义后端(用户实现 log_backend 接口) 特点:前端与后端完全解耦,核心层负责调度和过滤

我个人习惯把这种架构叫做“漏斗模型”。上层只管往里倒,下层决定怎么流出去。好处很明显——你换后端,上层代码一行都不用改。

4.2 日志级别配置:从编译期到运行期

日志级别这事儿,看着简单,但坑不少。Zephyr支持两级过滤:编译期过滤和运行期过滤。

4.2.1 编译期级别

编译期级别是在 prj.conf 里配的。这个级别决定了“哪些日志能被编译进固件”。低于这个级别的日志,连代码都不会生成。

# 设置全局日志级别
CONFIG_LOG_DEFAULT_LEVEL=3

# 单独设置某个模块的级别
CONFIG_LOG_MODULE_LEVEL_MY_MODULE=4

级别数字的含义:

级别宏说明
0LOG_LEVEL_NONE关闭所有日志
1LOG_LEVEL_ERR仅错误
2LOG_LEVEL_WRN错误 + 警告
3LOG_LEVEL_INF错误 + 警告 + 信息
4LOG_LEVEL_DBG全部(含调试)

我的经验:开发阶段我习惯把全局级别设成4,所有调试信息都打开。发布前再降到2或3。但要注意——如果某个模块的日志特别多,单独给它降级,别影响其他模块。

4.2.2 运行期动态控制

运行期控制才是真正好用的地方。你可以在系统运行时,通过shell命令动态调整日志级别,不用重新编译。

# 查看当前所有模块的日志级别
uart:~$ log status

# 动态设置某个模块的级别
uart:~$ log enable 1 my_module
uart:~$ log disable 1 my_module

这里有个关键点:运行期控制只能“降低”级别,不能“升高”超过编译期设定的上限。比如编译期设了3,运行期最多调到3,调不到4。

我曾经踩过的坑:有一次在产线上排查问题,想打开某个模块的调试日志,结果发现怎么调都没输出。查了半天,原来是编译期级别设成了2,而我想看的是DBG级别的信息。嗯,从那以后我养成了习惯——在调试固件里统一把编译期级别设成4,运行期再按需关闭。

4.3 日志后端定制:写一个自己的输出通道

Zephyr内置了好几种后端:UART、RTT、文件系统、网络等。但有时候,这些都不够用。比如你想把日志通过自定义的SPI接口发出去,或者存到特定的外部存储器里。这时候就需要定制后端了。

4.3.1 后端接口结构

定制后端,本质上就是实现 log_backend 结构体里的几个回调函数。核心接口就三个:

#include <logging/log_backend.h>

const struct log_backend_api my_backend_api = {
    .put = my_backend_put,
    .put_sync_string = my_backend_put_sync,
    .put_sync_raw = my_backend_put_raw,
    .panic = my_backend_panic,
};

LOG_BACKEND_DEFINE(my_backend, my_backend_api, true);

我来解释一下这几个回调:

  • put:异步输出,日志先入队列,后端慢慢处理。适合高性能场景。
  • put_sync_string:同步输出字符串,直接打印。适合简单场景。
  • put_sync_raw:同步输出原始二进制数据。适合协议调试。
  • panic:系统崩溃时调用,这时候要确保日志能强制输出。

4.3.2 一个简单的UART后端示例

咱们写一个最简单的UART后端,直接把日志通过串口打印出去。虽然Zephyr自带UART后端,但这个例子能让你看清整个流程。

#include <zephyr.h>
#include <logging/log_backend.h>
#include <uart.h>

static const struct device *uart_dev;

static void uart_backend_put(const struct log_backend *backend,
                             struct log_msg *msg)
{
    char buf[256];
    int len = log_msg_get_snprint(&msg, buf, sizeof(buf));
    if (len > 0) {
        uart_fifo_fill(uart_dev, (const uint8_t *)buf, len);
    }
}

static void uart_backend_panic(const struct log_backend *backend)
{
    /* 紧急模式:轮询发送,不依赖中断 */
    uart_irq_rx_disable(uart_dev);
    uart_irq_tx_disable(uart_dev);
}

const struct log_backend_api uart_backend_api = {
    .put = uart_backend_put,
    .panic = uart_backend_panic,
};

LOG_BACKEND_DEFINE(uart_backend, uart_backend_api, true);

关键点:LOG_BACKEND_DEFINE 宏的第三个参数是 auto_start。设为 true 表示系统启动时自动注册这个后端。设为 false 则需要手动调用 log_backend_enable() 来激活。

4.3.3 后端注册与激活流程

后端写好了,怎么让它跑起来?有两种方式:

  1. 自动注册:LOG_BACKEND_DEFINE 时 auto_start = true,系统初始化时自动调用 log_backend_enable()。
  2. 手动注册:在应用代码里调用 log_backend_enable(),可以控制激活时机。
/* 手动注册示例 */
void main(void)
{
    /* 初始化硬件 */
    uart_dev = device_get_binding("UART_0");
    
    /* 激活自定义后端 */
    log_backend_enable(&uart_backend, NULL, LOG_LEVEL_DBG);
    
    /* 现在可以打日志了 */
    LOG_INF("自定义后端已激活");
}

我建议:如果你的后端依赖某个外设初始化完成才能工作,那就用手动注册。否则用自动注册更省事。我在一个项目里同时用了三个后端——UART用于实时查看,RTT用于调试器交互,文件系统用于记录历史日志。每个后端的激活时机都不一样,手动注册给了你完全的控制权。

4.4 日志性能与资源考量

日志不是免费的。每一条日志都有开销。我见过有人把日志打得太猛,结果系统响应时间直接翻倍。

后端类型延迟内存占用适用场景
UART(同步)高(等待发送)低调试阶段
UART(异步)低(队列缓冲)中(队列内存)生产环境
RTT极低低调试器连接时
文件系统高(写Flash)高(缓冲区)离线分析
网络中中远程监控

注意:在中断上下文里打日志要特别小心。默认情况下,Zephyr的日志系统在中断中会切换到同步模式,避免死锁。但如果你在后端的 put 函数里调用了可能引起阻塞的操作,那就等着系统挂掉吧。我曾经在一个定时器中断里打了条日志,结果日志后端去拿锁,直接死锁——嗯,血的教训。

4.5 小结

Zephyr的日志系统,说白了就是“前端喊话,后端干活”。你只需要记住三点:

  • 架构上:三层分离,前端只管打,后端只管出,核心层做过滤和调度。
  • 配置上:编译期定上限,运行期动态调。别把编译期级别设太低,否则运行期想调都调不出来。
  • 定制上:实现 log_backend_api 的三个回调,用 LOG_BACKEND_DEFINE 注册。注意同步/异步的选择,别在中断里搞阻塞操作。

日志这东西,平时觉得无所谓,出问题的时候才知道它的好。把日志系统玩透了,调试效率能翻一倍。