调试与监控:日志系统设计、性能指标采集、事件追踪、内存泄漏检测
说实话,嵌入式系统里最让人头疼的,不是写代码,而是出了问题之后怎么找到问题。我见过太多同事,代码写得飞快,结果一跑起来就懵了——不知道程序在干什么,不知道内存去哪了,不知道哪个事件丢了。嗯,这就是我们今天要聊的:怎么给你的系统装上「仪表盘」和「黑匣子」。
日志系统设计:别让printf毁了你
很多人一开始做嵌入式,习惯用printf来调试。我个人觉得,这玩意儿在原型阶段还行,一旦进入正式产品,它就是灾难。为什么?因为printf是阻塞的,而且没有分级,没有时间戳,没有模块标记。
我建议你从一开始就设计一个分层日志系统。说白了,就是给每条日志打上「等级标签」和「模块标签」。
/* 日志等级定义 */
typedef enum {
LOG_LEVEL_ERROR = 0, /* 致命错误,必须处理 */
LOG_LEVEL_WARN = 1, /* 警告,不影响运行但需关注 */
LOG_LEVEL_INFO = 2, /* 重要信息,如状态切换 */
LOG_LEVEL_DEBUG = 3, /* 调试信息,开发阶段使用 */
LOG_LEVEL_TRACE = 4 /* 详细追踪,慎用 */
} log_level_t;
/* 日志模块定义 */
typedef enum {
MOD_SYSTEM = 0,
MOD_TASK_A,
MOD_TASK_B,
MOD_QUEUE,
MOD_MEMORY,
MOD_MAX
} log_module_t;
/* 日志输出接口 */
void log_write(log_level_t level, log_module_t mod,
const char *fmt, ...);
日志输出目标也要灵活。我习惯的做法是:开发阶段输出到串口,正式版输出到Flash或SD卡。你想想看,产品交付后用户不可能给你接串口线吧?
性能指标采集:别靠猜
性能问题,最怕「我觉得这里慢」。我见过有人花了两周优化一个根本不慢的函数,真正慢的地方反而没人管。所以,你得用数据说话。
性能指标采集,核心就三样:CPU占用率、任务执行时间、队列深度。
| 指标 | 采集方法 | 注意事项 |
|---|---|---|
| CPU占用率 | 空闲任务计数 / 总时间 | 采样周期要固定,别用延时 |
| 任务执行时间 | GPIO翻转 + 示波器 / 时间戳 | 注意中断对时间戳的影响 |
| 队列深度 | 记录队列中消息数量 | 峰值比平均值更重要 |
/* CPU占用率采集示例 */
static uint32_t idle_count = 0;
static uint32_t total_count = 0;
void idle_task(void *param) {
while(1) {
idle_count++;
/* 空闲任务,啥也不干 */
}
}
void monitor_task(void *param) {
while(1) {
total_count = idle_count;
vTaskDelay(pdMS_TO_TICKS(1000));
uint32_t delta = idle_count - total_count;
float cpu_usage = 100.0f - (delta * 100.0f / total_count);
log_write(LOG_LEVEL_INFO, MOD_SYSTEM,
"CPU usage: %.1f%%", cpu_usage);
}
}
事件追踪:还原现场
事件追踪,说白了就是给系统装一个「黑匣子」。系统崩溃了,你能回放崩溃前发生了什么。我做过一个项目,设备偶尔死机,查了两个月没找到原因。后来加了事件追踪,发现是某个中断里调用了malloc——嗯,中断里不能用malloc,这是铁律。
事件追踪的核心是:记录关键事件的时间戳和上下文。每个事件就是一个结构体,塞进环形缓冲区里。
/* 事件记录结构 */
typedef struct {
uint32_t timestamp; /* 系统滴答计数 */
uint16_t event_id; /* 事件ID */
uint8_t task_id; /* 任务ID */
uint8_t param; /* 附加参数 */
} trace_event_t;
#define TRACE_BUF_SIZE 256
static trace_event_t trace_buf[TRACE_BUF_SIZE];
static uint32_t trace_index = 0;
void trace_record(uint16_t event_id, uint8_t param) {
uint32_t idx = trace_index % TRACE_BUF_SIZE;
trace_buf[idx].timestamp = get_sys_tick();
trace_buf[idx].event_id = event_id;
trace_buf[idx].task_id = get_current_task_id();
trace_buf[idx].param = param;
trace_index++;
}
事件ID最好用枚举定义清楚。我习惯把事件分成几类:任务切换、消息收发、内存操作、错误发生。这样分析的时候一眼就能看出问题在哪。
内存泄漏检测:看不见的杀手
内存泄漏在嵌入式系统里特别隐蔽。你想想看,一个设备跑几天、几个月才出问题,你怎么复现?我遇到过最离谱的一次,设备运行了47天后突然死机,查到最后是一个定时器回调里每次申请了16字节没释放。
检测内存泄漏,我常用的方法有三种:
- 统计法:记录malloc和free的调用次数,差值就是当前分配数。如果这个值一直增长,肯定有泄漏。
- 栈回溯法:每次malloc时记录调用栈,泄漏后能定位到具体代码行。
- 内存池法:固定大小内存块,统计使用率。这个最可靠,但灵活性差一些。
/* 内存泄漏检测 - 统计法 */
static uint32_t malloc_count = 0;
static uint32_t free_count = 0;
void *debug_malloc(size_t size) {
void *ptr = malloc(size);
if(ptr) {
malloc_count++;
log_write(LOG_LEVEL_TRACE, MOD_MEMORY,
"malloc: %p, size=%u, total=%u",
ptr, size, malloc_count - free_count);
}
return ptr;
}
void debug_free(void *ptr) {
free(ptr);
free_count++;
log_write(LOG_LEVEL_TRACE, MOD_MEMORY,
"free: %p, total=%u",
ptr, malloc_count - free_count);
}
/* 定期检查泄漏 */
void mem_leak_check(void) {
uint32_t leaked = malloc_count - free_count;
if(leaked > 0) {
log_write(LOG_LEVEL_WARN, MOD_MEMORY,
"Possible memory leak: %u blocks not freed",
leaked);
}
}
知识体系总览
下面这张图,是我对调试与监控体系的整体理解。你可以把它当作一个检查清单,看看自己的系统还缺什么。
这四个模块,说白了就是一套组合拳。日志系统告诉你「发生了什么」,性能指标告诉你「有多快」,事件追踪告诉你「怎么发生的」,内存泄漏检测告诉你「资源去哪了」。缺了任何一个,调试起来都会很痛苦。
我个人习惯是在项目启动阶段就把这些基础设施搭好。别等到出问题了再补,那时候压力大,容易出错。你想想看,系统都跑飞了,你再去加日志,那不是刻舟求剑吗?
公众号:蓝海资料掘金营,微信deep3321