第2章:日志系统基础——嵌入式日志架构、级别与输出方式

做嵌入式开发,尤其是搞OTA升级和异常复位排查,日志系统就是你的“黑匣子”。

我见过太多同事,设备死机了,手里连个像样的日志都没有,只能靠猜。说实话,那感觉就像在黑暗里找一根针。所以,这一章我们先把地基打牢——搞懂嵌入式日志系统到底该怎么搭。

2.1 嵌入式日志系统架构

一个成熟的日志系统,不是简单往串口扔几个printf就完事了。它得有层次,有分工。

我个人习惯把日志系统分成三层:

  • 采集层:负责从代码里把日志信息抓出来。比如你写的LOG_DEBUG("value = %d", val),这就是采集。
  • 处理层:对日志进行格式化、加时间戳、过滤级别。这层决定了日志好不好读。
  • 输出层:把处理好的日志送到目的地——串口、文件、网络,或者内存缓冲区。

你想想看,如果这三层混在一起,后期想改输出方式,那得改多少代码?

核心原则:采集与输出解耦。代码里只写日志宏,底层输出交给驱动层去管。

下面这张图,是我常用的日志系统架构,你可以参考一下:

嵌入式日志系统三层架构 采集层(日志宏与接口) LOG_DEBUG / LOG_INFO / LOG_WARN / LOG_ERROR / LOG_FATAL 支持可变参数、文件名、行号自动注入 处理层(格式化与过滤) 时间戳注入 / 级别过滤 / 标签分类 / 环形缓冲区 支持异步处理,不阻塞主任务 输出层(多通道驱动) UART输出 | 文件系统写入 | 网络上报 | 内存暂存 支持运行时动态切换输出通道 采集与输出解耦,修改输出方式无需改动业务代码

2.2 日志级别:DEBUG / INFO / WARN / ERROR / FATAL

级别这东西,说白了就是给日志分个三六九等。为什么要分?因为嵌入式设备的存储和带宽都有限,你不能什么都往日志里塞。

我见过一个项目,OTA升级失败后,日志文件里全是DEBUG信息,真正的错误原因被淹没了。嗯,这就是级别没用好。

下面这个表格,是我在项目中常用的级别定义:

级别 数值 含义 典型场景
DEBUG 0 调试信息,开发阶段使用 变量值打印、函数入口/出口
INFO 1 正常流程信息 OTA开始下载、校验通过
WARN 2 警告,不影响当前功能 重试次数超限、校验和警告
ERROR 3 错误,功能受影响 下载超时、文件损坏
FATAL 4 致命错误,系统可能复位 看门狗超时、内存越界

我的习惯:发布版本把日志级别设为INFO及以上。DEBUG信息只在开发阶段打开。这样既能保证问题可追溯,又不会把存储空间撑爆。

代码里怎么用?我一般这样定义宏:

/* 日志级别定义 */
#define LOG_LEVEL_DEBUG   0
#define LOG_LEVEL_INFO    1
#define LOG_LEVEL_WARN    2
#define LOG_LEVEL_ERROR   3
#define LOG_LEVEL_FATAL   4

/* 当前编译级别,发布时改为 LOG_LEVEL_INFO */
#define CURRENT_LOG_LEVEL  LOG_LEVEL_DEBUG

/* 日志宏 */
#define LOG_DEBUG(fmt, ...) \
    do { \
        if (CURRENT_LOG_LEVEL <= LOG_LEVEL_DEBUG) \
            log_output("DEBUG", fmt, ##__VA_ARGS__); \
    } while(0)

#define LOG_INFO(fmt, ...)  \
    do { \
        if (CURRENT_LOG_LEVEL <= LOG_LEVEL_INFO) \
            log_output("INFO",  fmt, ##__VA_ARGS__); \
    } while(0)

/* WARN / ERROR / FATAL 类似,省略 */

注意:我曾经在一个项目里,把FATAL级别的日志放在了断言之前。结果设备复位了,日志还没来得及刷出去。后来我改成:FATAL日志先强制刷缓冲区,再触发复位。这个顺序很重要。

2.3 日志输出方式:UART / 文件 / 网络

输出方式的选择,取决于你的硬件资源和应用场景。我按优先级排个序:

2.3.1 UART输出

最基础,也最常用。开发阶段基本靠它。优点是简单,缺点是速度慢,而且占用一个串口。

我记得有一次,OTA升级过程中串口日志太多,导致主任务被阻塞。后来加了DMA传输,问题才解决。

2.3.2 文件输出

适合有文件系统的设备。日志写到Flash或SD卡里,掉电不丢失。排查异常复位时,这个最有用。

但要注意:频繁写Flash会磨损存储介质。我一般用环形缓冲区,攒够一定量再批量写入。

2.3.3 网络输出

适合联网设备。日志通过UDP或TCP上报到服务器。优点是实时性强,缺点是一旦网络断开,日志就丢了。

我建议:网络日志作为辅助,本地文件日志作为主备份。两条腿走路,才稳当。

下面这个表格,对比了三种方式的优劣:

输出方式 优点 缺点 适用场景
UART 实现简单,实时性好 速度慢,占用硬件资源 开发调试、小数据量
文件 掉电保存,容量大 写入慢,有磨损 异常复位分析、OTA日志
网络 远程查看,实时性强 依赖网络,可能丢包 远程运维、大规模部署

实战建议:OTA升级过程中,同时开启UART和文件输出。UART用于实时监控,文件用于事后分析。如果设备支持网络,再加一路网络上报。三路并行,万无一失。