做低功耗产品的工程师大概都见过这种现场:设备休眠之后就"睡死"了,怎么都唤不醒,串口一点输出都没有,只能靠硬件看门狗重启救回来。日志是空的,问题复现又依赖真实的休眠唤醒流程,一查就是好几天。
这篇文章复盘一个智能锁项目上真实发生的休眠卡死案例。排查思路是通用的,不管你用的是哪家的 BLE 芯片,只要架构是"主控 MCU + 蓝牙模块、主控拉模块的复位脚",都可能踩进同一个坑。
现象
智能锁进入休眠后异常,不能正常唤醒,虚拟串口没有任何打印,最后只有硬件看门狗重启这一条路。设备上没有其他外设接口,能用的调试手段非常有限。
四步软件排查:把范围一步步收窄
面对"睡死"问题,第一步不是乱试,而是先确认问题归属的边界。整个排查分四步走。
第一步:排除时钟源。休眠唤醒依赖时钟稳定,先怀疑外部晶振。切换到内部晶振测试,故障依然存在,晶振排除。
第二步:确认死在休眠前还是休眠后。这是本次排查最关键的一步。用虚拟串口在休眠路径上打印 RTC 值:进入休眠前打印b:xxxx,唤醒后打印a:xxxx。伪代码示意:
void enter_sleep(void) { print("b:%d\n", rtc_read()); // 休眠前:b:12345 system_sleep(); // 进入低功耗休眠 } void on_wakeup(void) { print("a:%d\n", rtc_read()); // 唤醒后:a:12346 // ...后续初始化 }加了这两行打印之后,输出里只有 b 没有 a。设备根本没有从休眠中醒来,问题发生在休眠过程中,不在唤醒后的初始化代码。两行 print,一次就把问题切成了两半。
第三步:排查 Flash 启动时序。低功耗休眠模式下 Flash 是掉电的,唤醒后要重新上电、初始化。如果启动延时设得太短,Flash 还没就绪 MCU 就去访问,同样会卡死。把延时调大,问题依旧,打印 log 一字不变。卡死点不在这一层。
第四步:用 GPIO 确认唤醒状态。增加一个 GPIO 电平检测,确认设备确实没有正常唤醒。
四步做完,结论已经清楚:设备进了休眠,没有正常唤醒,软件常规手段用尽。
转入硬件:盯上那根复位线
软件查不下去,就换视角看架构。这个系统里,主控 MCU 通过硬件串口和 BLE 芯片通信,主控还连着 BLE 芯片的复位管脚,可以拉低这根线强制复位蓝牙模块。
排查时有个现象很扎眼:主控不进休眠,BLE 芯片从不异常;主控一进休眠,BLE 芯片大概率卡死。异常和主控的休眠行为强相关,嫌疑自然落到主控对复位线的控制上。
抓复位管脚的波形,异常现形了。
正常情况下,主控要重启 BLE 芯片时,会把复位线拉低 2 到 30 毫秒,这是一次干净完整的复位。但抓到的波形显示,在那次正常复位之后 400 多毫秒,复位线上又出现了一个低电平毛刺,时间很短,下拉电压也不足。
这个毛刺就是凶手。从串口日志看,BLE 芯片确实因它触发了重启,但因为时间太短、电压不足,复位是不完整的:芯片内部每个模块对复位电压的要求不一样,有的模块达到复位条件被复位了,有的没有。典型的情况是 MCU 没被复位,但 Flash 被复位了,Flash 无法正常使用,芯片自然启动不起来。
芯片复位是有时序要求的,比如低电平必须保持足够时间(一般要求 10 毫秒量级),让所有模块都完成复位。一个不满足时序要求的下拉,比不复位更危险。
毛刺从哪来
继续追,这个异常下拉是主控唤醒导致的。
主控的 GPIO 在休眠状态下默认进入低功耗模式,输出关闭。唤醒之后、软件重新初始化之前,GPIO 会短暂处于默认状态,而默认状态是低电平。唤醒后程序才把 GPIO 拉高。这是很多芯片的普遍行为:上电未初始化阶段,引脚电平由默认态决定,不同芯片的默认值还可能不一样。
也就是说,主控每次唤醒,都会先在复位线上漏出一个"默认低电平"的窄脉冲,然后才被软件拉高。这个上电未初始化阶段的低电平,就是抓到的毛刺。
修复:让复位线在休眠全程保持高电平
很多主控芯片的 GPIO 都有"休眠保持"功能:休眠前设置某个 GPIO 在休眠期间保持指定电平,休眠全程这个电平不会丢。
修复步骤:
- 进入休眠前,设置该 GPIO 休眠保持高电平;
- 系统休眠,GPIO 持续输出高电平;
- 系统唤醒,将 GPIO 设为正常的高电平输出,同时取消休眠保持功能;
- GPIO 恢复正常的软件控制;
- 再次休眠时重复上述步骤。
调整之后,复位线在休眠、唤醒全程都是高电平,毛刺消失,BLE 芯片不再复现卡死。
伪代码示意(接口名是示意,各家的 SDK 叫法不同,思路是一样的):
void enter_sleep(void) { // 休眠前:让复位脚在整个休眠期间保持高电平 gpio_hold_en(RESET_PIN); // 启用该引脚的休眠保持功能 gpio_hold_set_level(RESET_PIN, 1); // 保持电平:高 system_sleep(); // 进入低功耗休眠 } void on_wakeup(void) { // 唤醒后:先恢复成正常的软件控制,再取消保持功能 gpio_set_direction(RESET_PIN, OUTPUT); gpio_set_level(RESET_PIN, 1); // 先把高电平输出立起来 gpio_hold_dis(RESET_PIN); // 后取消保持,中间不留低电平窗口 }顺序不能反:如果先取消保持、再设置输出,取消和设置之间复位脚又会漂回默认态,毛刺照旧。核心就一句,高电平在"保持"和"正常输出"之间切换时不能有间隙。
复盘:这类问题的通用排查路径
这个案例值得记下的不是结论,而是路径。遇到"带休眠的设备卡死",可以按这个顺序收窄:
- 先分软件硬件:切内部晶振,排除时钟问题;
- 打印 RTC 分段:休眠前打 b、唤醒后打 a,一次打印就能把问题切成"死在休眠里"还是"死在唤醒后";
- 查低功耗模式下的外设状态:Flash 等外设是否掉电,唤醒后是否需要重新初始化和延时;
- GPIO 打桩:软件手段用尽时,用电平检测确认真实状态;
- 软件走不通就上示波器:主控控制从芯片复位/电源的场景,重点抓复位线、电源线的毛刺。
还有一个教训值得单独说:任何不满足时序要求的复位信号,都可能让芯片处于"一半模块复位、一半没复位"的不确定状态。主控拉从芯片复位线的场景,休眠期间必须保证这根线的电平是明确的,要么保持高,要么完成一次满足时序的复位。这次的问题就出在两者之间。
嵌入式 智能锁 低功耗 BLE GPIO 复位 休眠卡死