第一章: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。