第一章:Zephyr日志系统概述

各位同学好,我是老张。今天咱们开始聊Zephyr的日志系统。说实话,日志这东西,很多工程师一开始都不太重视。我当年刚接触嵌入式时也觉得,不就是打印点信息嘛,有啥好学的?直到有一次产品上线后,一个偶发bug让我抓了整整两周...

嗯,从那以后,我对日志系统的态度就彻底变了。今天这堂课,我就把Zephyr日志系统的底裤给你扒干净。

1.1 日志系统架构

Zephyr的日志系统,说白了就是一个分层架构。它不像Linux那样搞个syslog那么复杂,但麻雀虽小五脏俱全。

整个架构分三层:

  • 前端API层:你写代码时调用的那些LOG_INF、LOG_ERR宏
  • 核心处理层:负责过滤、格式化、分发日志消息
  • 后端输出层:把日志真正写到串口、文件、网络等地方

我刚开始看源码时,觉得这分层有点多余。后来自己写了个小型RTOS才明白——不分层,后期想加个日志输出方式,那叫一个痛苦。

核心要点:日志消息从前端产生,经过核心层过滤,最终由后端输出。整个过程是异步的,不会阻塞你的业务代码。

1.2 日志级别

Zephyr定义了6个日志级别,从低到高分别是:

级别 宏定义 说明
0 LOG_LEVEL_NONE 关闭日志
1 LOG_LEVEL_ERR 错误信息
2 LOG_LEVEL_WRN 警告信息
3 LOG_LEVEL_INF 普通信息
4 LOG_LEVEL_DBG 调试信息

你可能会问:为什么是6个级别,不是5个或7个?其实早期版本只有4个,后来社区讨论加上了NONE和DBG。我个人习惯是:产品代码只开ERR和WRN,调试阶段才开INF和DBG。

我的经验:千万别在产品里开DBG级别。我曾经见过一个同事,产品上线后忘了关DBG日志,结果串口被日志刷爆,导致关键控制指令丢失...嗯,那晚我们一起加了个通宵的班。

1.3 日志后端

后端,就是日志最终要去的地方。Zephyr支持多种后端,你可以同时启用多个:

  • 串口后端:最常用,通过UART输出
  • 文件后端:写到文件系统里
  • 网络后端:通过UDP/TCP发到远程服务器
  • Shell后端:配合Zephyr的Shell子系统
  • 自定义后端:自己实现回调函数

我记得有个项目,设备部署在野外,没法接串口。我就写了个自定义后端,把日志通过LoRa无线模块发回来。虽然速率慢,但关键错误信息都能抓到。

注意:后端越多,系统开销越大。尤其是网络后端,如果日志量太大,可能把协议栈都拖垮。我曾经就踩过这个坑——日志把网络缓冲区占满了,正常数据包反而发不出去。

1.4 日志模块初始化

好了,理论说完了,咱们看看代码怎么写。日志模块的初始化其实很简单:

#include <zephyr/logging/log.h>

// 注册日志模块
LOG_MODULE_REGISTER(my_module, LOG_LEVEL_INF);

void my_function(void)
{
    LOG_ERR("这是一个错误信息");
    LOG_WRN("这是一个警告信息");
    LOG_INF("这是一个普通信息");
    LOG_DBG("这是一个调试信息");  // 不会输出,因为级别是INF
}

这里有个细节要注意:LOG_MODULE_REGISTER这个宏,必须在源文件的全局作用域调用。我见过有人把它放在函数里,结果编译报错,找了半天才发现。

如果你想在多个文件里用同一个模块名,可以这样:

// file1.c
LOG_MODULE_REGISTER(my_module, LOG_LEVEL_INF);

// file2.c
LOG_MODULE_DECLARE(my_module, LOG_LEVEL_INF);

嗯,这里要特别提醒:LOG_MODULE_DECLARE只是声明,不注册。如果你忘了在某个文件里注册,链接时会报错。

小技巧:调试阶段,我习惯把日志级别设成DBG。等代码稳定了,再统一改成INF或WRN。用Kconfig配置的话,可以在prj.conf里加一行:CONFIG_LOG_DEFAULT_LEVEL=3,这样所有模块默认就是INF级别。

1.5 总结

今天的内容就这些。日志系统看似简单,但用好它,能帮你省下大量调试时间。我见过太多工程师,遇到bug就加print,改完又删,来回折腾。其实只要把日志系统规划好,很多问题看一眼日志就能定位。

下节课,咱们聊聊日志的过滤机制和性能优化。到时候我会分享一个真实案例——如何用日志系统抓到一个只在凌晨3点出现的bug。