16. 电源管理移植:Linux电源管理框架(PM/QoS)、Android WakeLock机制、Suspend/Resume调试、功耗测量与优化

电源管理,说白了就是让设备该睡的时候睡,该醒的时候醒,别在不该干活的时候瞎忙活。我在做i.MX平台移植时,遇到过最头疼的问题不是驱动跑不起来,而是设备睡下去就醒不过来,或者醒过来发现功耗比睡着前还高。嗯,这章我们就来把这些坑一个个填上。

16.1 Linux电源管理框架:PM Core与QoS

Linux的电源管理框架,我习惯把它分成三层来看:

  • PM Core:内核最底层的电源管理基础设施,负责suspend/resume流程的调度
  • PM QoS:一个约束机制,告诉系统“我现在需要多快的响应,别给我睡太死”
  • Driver PM:每个驱动自己实现的suspend/resume回调

先看PM Core。它的核心数据结构是struct dev_pm_ops,每个设备驱动都得实现它:

static const struct dev_pm_ops imx_uart_pm_ops = {
    .suspend = imx_uart_suspend,
    .resume  = imx_uart_resume,
    .freeze  = imx_uart_freeze,
    .thaw    = imx_uart_thaw,
    .poweroff = imx_uart_poweroff,
    .restore  = imx_uart_restore,
};

这里有个细节——freeze/thawsuspend/resume有什么区别?我刚开始也搞混过。简单说:suspend/resume是标准休眠流程,freeze/thaw用于hibernate(休眠到磁盘)。在Android Automotive上,我们主要用suspend/resume

核心要点:PM Core通过dpm_list维护所有设备的顺序,suspend时从后往前遍历,resume时从前往后遍历。这个顺序决定了设备间的依赖关系。

再来说PM QoS。这玩意儿很有意思——它允许驱动或用户空间程序告诉内核:“我现在需要CPU频率不低于xxx MHz”或者“内存延迟不能超过xxx us”。

PM QoS有四种约束类型:

约束类型 描述 典型场景
CPU_DMA_LATENCY CPU从idle状态唤醒的最大延迟 音频播放时要求低延迟
CPU_FREQ_MIN CPU最低频率约束 视频解码时保证性能
CPU_FREQ_MAX CPU最高频率约束 散热限制时降频
NETWORK_LATENCY 网络设备唤醒延迟 车载以太网实时通信

在i.MX8上,我曾经遇到一个问题:车载音频播放时总有卡顿。查了半天,发现是PM QoS没设置好,CPU在播放音频时进入了太深的idle状态,唤醒延迟超过了音频buffer的阈值。解决办法就是在音频驱动里加一行:

pm_qos_add_request(&audio_qos_req, PM_QOS_CPU_DMA_LATENCY, 100);

这行代码告诉内核:“音频播放期间,CPU唤醒延迟不能超过100us”。问题就解决了。

实战技巧:调试PM QoS时,可以查看/sys/devices/system/cpu/cpu0/cpuidle/下的状态,看看CPU到底进入了哪级idle。如果发现不该进深睡眠时进了,八成是QoS约束没设对。

16.2 Android WakeLock机制

WakeLock是Android在Linux PM之上封装的一套机制。说白了,就是让App能告诉系统:“我现在有事,别睡!”

WakeLock分两种类型:

  • Partial WakeLock:阻止系统进入suspend,但CPU可以降频。App后台下载时常用。
  • Full WakeLock:保持屏幕和CPU全速运行。游戏、导航时用。

在Android Automotive上,WakeLock的使用要格外小心。车载系统不像手机,用户不会频繁操作,但后台服务(如导航、语音助手)需要持续运行。滥用WakeLock会导致电池快速耗尽。

我见过一个案例:某第三方导航App在后台持有一个Full WakeLock不释放,导致车机在停车后一晚上耗掉了30%的电。后来我们在系统层加了一个WakeLock超时机制——任何App持有的WakeLock超过30分钟,系统自动回收。

WakeLock在驱动层的实现,其实是通过/sys/power/wake_lock/sys/power/wake_unlock这两个节点操作的。Android的PowerManagerService就是通过写这两个节点来控制休眠的。

// 内核中WakeLock的实现
struct wakeup_source *ws;
ws = wakeup_source_register(NULL, "my_wakelock");
__pm_stay_awake(ws);   // 持有WakeLock
__pm_relax(ws);        // 释放WakeLock

注意:在i.MX平台上,WakeLock的注册名称不要超过45个字符,否则会被截断。我踩过这个坑——调试时死活找不到某个WakeLock,后来发现是名字太长被截了。

16.3 Suspend/Resume调试

调试suspend/resume,我有一套固定的流程:

  1. 检查设备树:确保所有设备的power-domains属性正确
  2. 打开PM调试日志echo 15 > /sys/module/suspend/parameters/pm_test_delay
  3. 测试suspendecho mem > /sys/power/state
  4. 查看唤醒源cat /sys/kernel/debug/wakeup_sources

我最常用的调试手段是pm_test。它可以把suspend流程拆成几个阶段,逐个测试:

# 只测试freeze阶段
echo freezer > /sys/power/pm_test
echo mem > /sys/power/state

# 只测试设备suspend阶段
echo devices > /sys/power/pm_test
echo mem > /sys/power/state

# 关闭pm_test
echo none > /sys/power/pm_test

为什么要这么做?因为如果整个suspend流程失败了,你根本不知道是哪个阶段出的问题。分段测试可以快速定位。

我曾经遇到一个i.MX8QM平台的bug:suspend后系统立即唤醒。用wakeup_sources一看,发现是GPIO1_3这个唤醒源一直处于active状态。查硬件发现,这个GPIO在suspend时被外部上拉了,导致系统认为有唤醒事件。解决方案是在设备树中把这个GPIO的唤醒功能关掉:

&gpio1 {
    wakeup-source;
    gpio1_3: gpio1_3 {
        // 添加这一行
        wakeup-disable;
    };
};

调试口诀:suspend失败查设备,resume失败查唤醒源。设备suspend失败看dmesg,resume失败看wakeup_sources。

16.4 功耗测量与优化

功耗测量,我分三步走:

  • 硬件测量:用万用表或功率计测整板电流
  • 软件估算:通过/sys/class/power_supply/读取电池信息
  • 细分测量:逐个关闭外设,找出功耗大户

在i.MX平台上,我常用regulator框架来查看各个电源域的功耗:

# 查看所有regulator的状态
cat /sys/kernel/debug/regulator/regulator_summary

# 输出示例
 regulator                      use open bypass voltage   current   min     max
----------------------------------------------------------------------------
 vdd_soc                        0   0   0      800mV     0mA      800mV   900mV
 vdd_arm                        0   0   0      900mV     0mA      900mV   1000mV
 vdd_dram                       0   0   0      1200mV    0mA      1200mV  1200mV
 vdd_usb                        0   0   0      5000mV    0mA      5000mV  5000mV

优化功耗,我总结了几条经验:

  1. 关闭未使用的时钟:很多驱动在probe时打开了时钟,但suspend时没关。用clk_summary检查。
  2. 降低CPU频率:在idle时用cpufreq降到最低频率。
  3. 关闭未使用的电源域:通过regulator_disable()关掉不用的regulator。
  4. 优化唤醒源:只保留必要的唤醒源,其他的在suspend时禁用。

我的习惯:每次做完功耗优化,都会跑一个24小时的待机测试。记录每小时的功耗曲线,看看有没有异常波动。有一次发现凌晨3点功耗突然飙升,查日志发现是某个App在后台做数据同步。后来我们把这个同步任务改到了充电时执行。

16.5 本章知识体系

下面这张图总结了电源管理移植的核心脉络:

电源管理移植知识体系 Linux PM Core dev_pm_ops / dpm_list suspend/resume流程调度 PM QoS CPU_DMA_LATENCY / CPU_FREQ 约束机制 / 唤醒延迟控制 Android WakeLock Partial / Full WakeLock wakeup_source / pm_stay_awake 调试手段 pm_test分段测试 | wakeup_sources唤醒源分析 | dmesg日志 | clk_summary时钟检查 regulator_summary电源域查看 | /sys/power/state手动触发 功耗优化目标 关闭未用时钟 | 降低CPU频率 | 关闭未用电源域 | 优化唤醒源 | 限制WakeLock时长 待机功耗 < 5mA | 唤醒延迟 < 100ms | 无异常唤醒 i.MX Android Automotive 电源管理移植核心流程

这张图从左到右展示了电源管理的三个核心模块,中间是调试手段,下面是优化目标。你在做移植时,可以按这个顺序来:先理解PM Core和QoS,再搞定WakeLock,然后用调试手段定位问题,最后做功耗优化。

最后提醒一句:电源管理移植最忌讳的就是“差不多就行”。我见过太多项目,因为suspend/resume流程没调好,导致车机在用户停车后偷偷耗电,第二天电瓶亏电打不着火。每个唤醒源、每个regulator、每个时钟,都得仔细检查。

公众号:蓝海资料掘金营,微信deep3321