4、MTK 休眠唤醒流程(Suspend/Resume)

好,咱们今天来聊聊MTK平台上的休眠唤醒流程。说实话,这块内容在项目初期往往容易被忽略,但到了量产阶段,它往往是功耗问题的重灾区。我见过不少团队,功能跑得飞起,一测待机电流就傻眼——嗯,多半是Suspend/Resume流程没理清楚。

4.1 系统Suspend入口流程

系统进入休眠,不是一拍脑袋就睡的。它有一套严格的流程,像极了我们下班前要关电脑、锁抽屉、打卡一样。在MTK平台上,Suspend的入口大致分这么几步:

  1. 用户态触发:比如按Power键、屏幕超时、或者echo mem > /sys/power/state。这一步会调用到内核的pm_suspend()函数。
  2. 冻结进程:内核会先freeze所有用户态进程和内核线程。为什么要这么做?你想想看,如果进程还在跑,突然把CPU停了,数据就乱了。
  3. 设备Suspend回调:按照设备的注册顺序,逐个调用驱动的suspend回调函数。MTK平台这里有个坑——我曾经遇到过某个外设驱动suspend回调里做了msleep(),直接拖慢了整个休眠流程。
  4. 关闭CPU非启动核:多核处理器会把其他核关掉,只留一个核做最后的收尾工作。
  5. 进入WFI(Wait For Interrupt):最后这个核执行WFI指令,等待唤醒中断。

关键点:MTK的Suspend流程中,有个叫mtk_suspend_ops的结构体,里面定义了平台相关的suspend准备和结束回调。我建议你拿到代码后,先找到这个结构体,看看它注册了什么。

来看一段典型的Suspend入口日志:

[   12.345] PM: suspend entry 2024-01-15 10:30:00 UTC
[   12.346] Freezing user space processes ... (elapsed 0.001 seconds) done.
[   12.350] Freezing remaining freezable tasks ... (elapsed 0.001 seconds) done.
[   12.355] mtk-suspend: mtk_suspend_enter, mode=1
[   12.360] mtk-uart: suspend called
[   12.361] mtk-i2c: suspend called
[   12.362] mtk-display: suspend called
[   12.370] PM: suspend of devices complete after 15.012 msecs
[   12.375] CPU1: shutdown
[   12.376] CPU2: shutdown
[   12.377] CPU3: shutdown
[   12.380] Entering WFI state

看到没?从PM: suspend entryEntering WFI state,整个过程大概几十毫秒。如果超过100ms,你就得查查是哪个设备拖了后腿。

4.2 系统Resume恢复流程

唤醒流程跟Suspend正好相反。系统被中断唤醒后,会从WFI状态跳出来,然后一步步恢复运行状态。我个人习惯把Resume流程分成三个阶段:

  • 中断唤醒阶段:某个外设(比如GPIO按键、RTC闹钟、USB插入)触发中断,CPU从WFI中醒来。这里要注意,不是所有中断都能唤醒系统——只有注册了IRQF_NO_SUSPEND标志的中断才行。
  • 设备Resume回调:内核按顺序调用各个设备的resume回调。顺序跟suspend相反,先恢复后注册的设备。
  • 解冻进程:恢复用户态进程的运行,系统回到正常工作状态。

小技巧:调试Resume问题时,我建议你在每个关键设备的resume回调里加个printk,打印时间戳。这样一眼就能看出哪个设备恢复得慢。我曾经用这个方法抓到一个USB控制器,它的resume里做了300ms的硬件复位等待——这显然不合理。

Resume成功的日志长这样:

[   13.200] PM: resume entry
[   13.201] CPU1: booting
[   13.202] CPU2: booting
[   13.203] CPU3: booting
[   13.210] mtk-display: resume called
[   13.215] mtk-i2c: resume called
[   13.216] mtk-uart: resume called
[   13.220] mtk-suspend: mtk_suspend_exit, mode=1
[   13.225] Restarting tasks ... done.
[   13.230] PM: resume of devices complete after 30.012 msecs

注意看,Resume时间一般比Suspend长一些,因为要重新初始化硬件。如果超过50ms,用户就能感知到唤醒延迟了。

4.3 关键日志分析:/proc/wakeup_sources

这个文件,说白了就是系统的「谁把我叫醒」记录本。每次系统从Suspend状态被唤醒,内核都会记一笔——是哪个中断源干的。我调试功耗问题时,第一件事就是看这个文件。

来看一个实际例子:

# cat /proc/wakeup_sources
name                    active_count   event_count   wakeup_count   expire_count
mtk-rtc                0              12            12             0
mtk-eint               0              3             3              0
mtk-uart               0              1             0              1

表格里的字段含义:

字段含义
name唤醒源名称,对应注册的wakeup_source名字
active_count当前活跃次数,一般正常为0
event_count总事件次数,包括未唤醒系统的事件
wakeup_count实际唤醒系统的次数
expire_count超时次数,如果这个值很大,说明有驱动没及时释放wakeup_source

注意:如果某个wakeup_source的active_count一直不为0,说明有驱动持锁不放。系统就永远无法进入深度休眠。我遇到过最离谱的一次,是一个触摸屏驱动在suspend后还持有wakeup锁,导致待机电流多了5mA。

怎么排查?我一般这么做:

  1. 先看wakeup_count,哪个源唤醒次数最多,它就是主要嫌疑人。
  2. 再看expire_count,如果某个源这个值很大,说明它的超时处理有问题。
  3. 最后看active_count,不为0的话,直接去代码里搜这个wakeup_source的__pm_stay_awake()__pm_relax()调用是否成对出现。

举个例子,假设你发现mtk-eint的wakeup_count是3,说明系统被外部中断唤醒了3次。这时候你就得查查是哪个GPIO触发的。MTK平台下,可以看/proc/mtk_eint或者/sys/kernel/debug/gpio来定位具体引脚。

实战建议:在项目开发阶段,我习惯写一个脚本,每隔5秒采集一次/proc/wakeup_sources的数据,持续跑一晚上。第二天分析数据,基本能把所有异常唤醒源揪出来。这个习惯帮我解决了好几个棘手的待机功耗问题。

嗯,关于Suspend/Resume流程和wakeup_sources的分析,今天就先聊到这儿。下一节我们会深入讲讲MTK的CPUIdle和DPIDLE模式,那才是真正省电的重头戏。