3. 日志输出机制:UART日志、RAM日志、Flash日志的存储与读取
做OTA升级,最怕什么?最怕设备变砖后,你连它死在哪一步都不知道。
日志,就是嵌入式系统的“黑匣子”。我个人习惯把日志分成三类:UART实时日志、RAM循环日志、Flash持久日志。这三者各有各的脾气,用好了能救命,用不好就是一堆废数据。
3.1 UART日志:最直接的“现场直播”
UART日志,说白了就是通过串口往外吐数据。开发阶段最常用,插上串口线就能看。
核心特点:实时、直观、不占内部存储。但依赖外部连接,产线和用户手里基本用不了。
我在项目中遇到过这样的情况:量产测试时,工人插错串口线,日志全乱码。后来我强制在bootloader里加了个波特率自适应逻辑,才解决。
UART日志的典型实现
// 简易UART日志输出函数
void uart_log(const char *tag, const char *fmt, ...) {
char buf[256];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
// 加时间戳和标签
printf("[%lu][%s] %s\r\n", get_tick_ms(), tag, buf);
}
嗯,这里要注意:UART输出是阻塞的。如果日志量太大,会拖慢启动速度。我建议在关键路径上只输出等级≥WARN的日志,DEBUG级别的留到调试时再开。
3.2 RAM日志:掉电就丢的“速记本”
RAM日志,就是往内存里写环形缓冲区。速度快,不磨损Flash,但一掉电就全没了。
你想想看,设备死机后如果能自动重启,RAM日志里的死因数据还在。这就是它的价值——记录“临终遗言”。
环形缓冲区的设计要点
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 缓冲区大小 | 4KB ~ 16KB | 取决于RAM余量,太小会丢关键日志 |
| 日志条目最大长度 | 128字节 | 超过则截断,防止单条撑爆缓冲区 |
| 写指针保护 | 原子操作 | 中断中写日志时,必须保证指针不冲突 |
// 环形缓冲区写入示例
typedef struct {
uint8_t buffer[RAM_LOG_SIZE];
uint32_t write_idx;
uint32_t read_idx;
} ram_log_t;
void ram_log_write(ram_log_t *log, const char *data, uint32_t len) {
// 原子操作保护写指针
uint32_t idx = __atomic_fetch_add(&log->write_idx, len, __ATOMIC_SEQ_CST);
idx %= RAM_LOG_SIZE;
// 写入数据(处理回绕)
if (idx + len <= RAM_LOG_SIZE) {
memcpy(&log->buffer[idx], data, len);
} else {
uint32_t first_part = RAM_LOG_SIZE - idx;
memcpy(&log->buffer[idx], data, first_part);
memcpy(&log->buffer[0], data + first_part, len - first_part);
}
}
我曾经踩过的坑:中断服务函数里写RAM日志,没关中断保护,结果写指针被主循环和中断同时修改,日志全乱。后来加了关中断保护,或者用LDREX/STREX指令做无锁写入,才稳定。
3.3 Flash日志:断电不丢的“黑匣子”
Flash日志,才是真正意义上的持久化日志。设备死透了,下次上电还能读出来分析。
但Flash有写入寿命(一般10万次擦除),而且写入慢(ms级)。所以不能每条日志都写Flash,得讲究策略。
Flash日志的存储策略
- 按页存储:每次写满一页(通常256字节或4KB)才擦写一次,减少磨损
- 日志分级:只将ERROR、FATAL级别的日志写入Flash,INFO和DEBUG只写RAM
- 循环覆盖:保留最近N页日志,旧日志被新日志覆盖
读取Flash日志的流程
// Flash日志读取示例(伪代码)
void read_flash_log(uint32_t page_start, uint32_t page_count) {
for (uint32_t i = 0; i < page_count; i++) {
uint32_t page_addr = FLASH_LOG_BASE + (page_start + i) * FLASH_PAGE_SIZE;
// 检查页头标记是否有效
flash_page_header_t *header = (flash_page_header_t *)page_addr;
if (header->magic != FLASH_LOG_MAGIC) {
continue; // 无效页,跳过
}
// 读取日志条目
uint8_t *data = (uint8_t *)(page_addr + sizeof(flash_page_header_t));
uint32_t data_len = header->data_len;
// 通过UART或文件系统输出
output_log_data(data, data_len);
}
}
我的个人习惯:在Flash日志的页头里加一个16位CRC校验。曾经遇到过Flash读取时数据跳变,CRC帮我揪出了硬件信号完整性问题。别省这一步。
3.4 三类日志的协同工作
实际项目中,这三类日志不是孤立的。我一般这样搭配:
- 正常启动:UART输出所有日志,RAM缓存最近512条,Flash只记录ERROR
- 异常重启:启动时先检查RAM日志(如果没掉电),再检查Flash日志,合并输出
- 死机后上电:只能读Flash日志,RAM已丢失
下面这张图,是我自己项目里用的日志协同流程,你可以参考:
3.5 避坑指南与经验总结
做了这么多年OTA,日志这块我踩过的坑不少。挑几个典型的说说:
我曾经犯过的错:Flash日志没做磨损均衡,同一个页反复擦写,3万次后就坏了。后来改成随机起始页+循环写入,寿命提升了10倍。
- 日志不要带换行符进Flash:每条日志末尾加\0就行,读取时再统一加换行,节省空间
- 时间戳用相对时间:记录从启动开始的毫秒数,比绝对时间省4字节,而且分析时更直观
- UART日志加流控:如果上位机收不过来,开硬件流控(RTS/CTS),否则丢日志
- RAM日志区放在BSS段末尾:这样如果栈溢出,最先破坏的是日志区,而不是关键数据
我的一个小技巧:在Flash日志的最后一页,固定存一份“日志索引表”,记录每页的起始时间和日志等级。读取时先看索引,跳过无效页,速度能快不少。
嗯,日志机制就聊到这儿。记住一句话:日志不是写给机器看的,是写给人看的。设计时多想想“如果我是分析人员,我需要什么信息”,你的日志系统就不会差。