一、日志系统概述:为什么需要日志?
说实话,我刚开始写嵌入式程序那会儿,压根没把日志当回事。
那时候觉得,代码能跑就行,出问题了拿个LED灯闪一闪,或者串口打印几个数字,够用了。直到有一次,一个设备在现场跑了三天才出故障,而我手里只有一堆「0x01、0x02、0xFF」这样的原始数据……你想想看,这怎么定位问题?
从那以后,我彻底明白了——日志系统不是可有可无的装饰品,而是嵌入式系统的「黑匣子」。
1.1 日志的核心价值
为什么我们需要日志?我总结了几点:
- 问题定位:程序崩溃了,是野指针?还是栈溢出?日志能告诉你最后一刻发生了什么。
- 运行状态监控:CPU温度多少?内存还剩多少?任务调度是否正常?日志就是系统的体检报告。
- 性能分析:哪个函数耗时最长?中断响应是否及时?带时间戳的日志能帮你做 profiling。
- 调试与验证:新功能上线前,日志输出能验证你的逻辑是否走对了分支。
我个人习惯:在产品开发阶段,日志级别开得低一些,多输出一些调试信息;到了量产阶段,只保留 ERROR 和 FATAL 级别的日志,避免影响性能。
1.2 日志级别定义
日志级别,说白了就是给每条日志贴个「严重程度」标签。我见过不少新手把所有信息都打成 ERROR,结果真正出问题时,满屏都是红色,反而找不到重点。
下面是我在项目中常用的五级日志体系:
| 级别 | 名称 | 含义 | 使用场景 |
|---|---|---|---|
| 0 | DEBUG | 调试信息 | 开发阶段,输出变量值、函数入口/出口等细节 |
| 1 | INFO | 普通信息 | 系统启动、配置加载、任务创建等正常流程 |
| 2 | WARN | 警告 | 非致命异常,比如传感器读数超范围、重试操作 |
| 3 | ERROR | 错误 | 功能失效,但系统还能继续运行,比如文件打开失败 |
| 4 | FATAL | 致命错误 | 系统无法继续运行,必须复位或停机 |
我的建议:在代码里用宏定义这些级别,比如 #define LOG_LEVEL_DEBUG 0,方便后期通过编译开关统一控制。
1.3 日志系统设计原则
设计一个日志系统,不是简单地把 printf 包装一下就行。我踩过不少坑,总结出几条原则:
原则一:低侵入性
日志代码不应该影响主业务流程。我曾经在一个项目中,日志打印用了阻塞式串口发送,结果在高优先级中断里调用了日志函数,直接把系统卡死了……嗯,血的教训。
正确的做法是:日志写入缓冲区,由后台任务或 DMA 负责输出。
原则二:分级可控
运行时能动态调整日志级别。比如通过串口命令 log_level 2 把级别设为 WARN,这样 DEBUG 和 INFO 信息就不再输出了。
避坑指南:我曾经在某个产品中把日志级别写死在代码里,结果现场出了问题,想调高日志级别看细节,却没法远程升级……从那以后,我所有日志系统都支持运行时动态配置。
原则三:格式化与可读性
日志不是给机器看的,是给人看的。我要求每条日志至少包含:时间戳、模块名、日志级别、具体信息。
// 一个简单的日志宏示例
#define LOG_PRINT(level, fmt, ...) do { \
if (level >= g_log_level) { \
printf("[%s][%s] " fmt "\r\n", \
get_timestamp(), \
level_to_string(level), \
##__VA_ARGS__); \
} \
} while(0)
原则四:性能开销可控
嵌入式系统资源有限。日志不能占用太多 CPU 时间,也不能吃掉大量内存。我一般会设置日志缓冲区大小,比如 2KB,满了就覆盖最旧的数据。
1.4 知识体系总览
下面这张图,是我对日志系统核心知识的一个梳理。你可以把它当作本章的「地图」:
1.5 一个真实的教训
最后分享一个我自己的经历。
几年前做一款工业控制器,日志系统设计得很简陋——就是直接 printf。结果有一次,客户反馈设备偶尔死机。我远程连上去,发现日志缓冲区早就被冲掉了,根本看不到死机前的信息。
后来我重新设计了日志系统:环形缓冲区 + 掉电保存。每次死机后重启,先读取上次保存的日志,问题一下子就定位到了——是一个中断服务函数里调用了 malloc,导致堆碎片化严重。
你想想看,如果没有日志,这种偶发问题可能排查几个月都找不到原因。
所以我的结论是:日志系统不是锦上添花,而是嵌入式开发的刚需。花一天时间设计好日志系统,能为你省下一周甚至一个月的调试时间。