各位EEPW的同学,大家好。
今天咱们不聊算法,也不聊架构,聊点“接地气”的——调试。
我相信在座各位都有过这样的经历:程序在仿真器里跑得好好的,一断电重启就翻车;或者客户那边反馈设备偶尔死机,你连上电脑却死活复现不了。这时候,你手里的J-Link和ST-Link仿佛瞬间变成了“摆设”,除了能看看寄存器,什么忙都帮不上。
为什么?因为仿真器依赖“暂停”,而很多Bug恰恰是“动态”的。你一旦暂停,时序就变了,中断就乱了,Bug就跟捉迷藏一样躲起来了。
这时候,能救你的不是更贵的仿真器,而是一个设计良好的日志系统。
---
第一层:别把“printf”当万能药
很多同学一上来就用半主机模式(Semihosting)重定向`printf`到串口。这玩意儿在裸机学习阶段确实香,几句话就能看到数据。但我要泼一盆冷水:在产品级代码里,`printf`是头号性能杀手。
为什么呢?因为`printf`内部有大量的格式解析和字符串处理,动辄耗时几十微秒甚至毫秒级。如果你的串口中断里来一句`printf`,恭喜你,系统响应延迟直接原地爆炸。
那怎么办?我的原则是:用“轻量级日志”替代“万能打印”。
我自己常用的方案是封装一个简单的`log_printf`,但里面不直接调用标准库的`printf`,而是用`vsnprintf`先格式化到本地缓冲区,然后通过DMA+环形队列的方式把数据“悄悄”发出去。核心逻辑是:主循环只管往队列里丢数据,串口发送完全由DMA在后台搬运,CPU几乎零感知。
这样一来,哪怕你每秒打几百条日志,CPU占用率也基本可以忽略不计。
---
第二层:日志分级——别让信息淹没你
调试最怕什么?最怕打开串口助手,满屏滚动的全是“Enter Main”、“Tick+1”这种废话,真正关键的错误信息早就被冲没了。
我建议同学们建立一套日志分级机制,简单分四档就够用:
|级别|关键字|适用场景|
|ERROR|[E]|系统致命错误,比如外设初始化失败、内存分配失败|
|WARN|[W]|异常但可恢复的情况,比如通信重试、超时|
|INFO|[I]|关键流程节点,比如设备上线、参数加载完成|
|DEBUG|[D]|详细调试信息,变量值、状态跳转,仅在开发阶段开启|
关键是:通过一个宏开关,在Release版本里把DEBUG级别的日志全部编译掉,不占ROM也不占RAM。这样既保留了调试手段,又不会拖累最终产品的性能。
---
第三层:日志的“归宿”——不止是串口
很多同学觉得日志就是往串口打印,这其实限制了思路。我来分享几个实战中我觉得很实用的“变通”方案:
1.掉电保存型日志:如果你用的是STM32F4/F7系列,内部Flash通常有多余扇区。当系统触发HardFault或看门狗复位之前,把当前的函数调用栈、关键寄存器值、最后一条心跳消息,一股脑写入后备Flash。下次上电时先读取这段“遗书”,通过串口打印出来。这招排查偶发性死机,屡试不爽。
2.RTT(Real-Time Transfer)替代串口:如果你手头有J-Link,强烈建议试试SEGGER的RTT。它比串口快几个数量级,而且不需要额外的TX/RX引脚。调试时开着RTT Viewer,变量变化实时可见,比干瞪眼看串口数据高效太多了。
3.逻辑分析仪“旁路”监听:对于实在没法打日志的极端情况(比如中断频率太高,打日志反而引发新Bug),我会用GPIO翻转法——在关键函数入口拉高一个闲置IO,出口拉低,然后用逻辑分析仪抓波形。看脉冲宽度和间隔,基本就能推断出函数执行时间和调用频率,这招模拟电路出身的工程师特别爱用。
---
最后说几句掏心窝的话
同学们,调试能力其实是一个嵌入式工程师“段位”的重要分水岭。刚入门时,大家都喜欢“哪里出问题就盯哪里”;但真正成熟之后,你会发现优秀的日志设计,本身就是一种防御性编程。
它不解决问题,但它能让你在问题发生的第一时间,拿到最完整的现场证据。有了证据,查Bug就从“玄学推理”变成了“刑侦破案”,差别有多大,谁用谁知道。
我要赚赏金
