
这是一个很有意思的标题把一个操作系统里最不起眼、但又最影响体验的东西给串起来了。我最早接触 Linux 能源管理不是因为好奇而是被一块板子逼的整机功耗怎么都降不下来电池续航不到两小时摸上去还烫手。后来把 CPUFreq 和 Runtime PM 这两条线摸透才意识到所谓“省电”根本不是某个开关的功劳而是内核里一整套从处理器频率到外设电源的协同体系。这篇文章就想把这条链路完整拆开从 CPUFreq 怎么调频到 Runtime PM 怎么管设备再到它们之间怎么互相配合一次讲清楚。适合刚接触内核电源管理的嵌入式工程师也适合正在做系统功耗优化的开发者参考。1. 能源管理的全景框架先看清内核在省谁的“电”1.1 不是“一个开关省电”而是分层分工很多人一提到 Linux 内核的能源管理第一反应是“待机睡眠”。这没错但只是冰山一角。真正要理解这个体系得先接受一个事实内核的省电方式不是集中式的而是分层的每一层管不同的硬件对象用不同的机制。我习惯把它分成四个层次来看处理器频率层也就是 CPUFreq负责动态调节 CPU 核心的运行频率和电压常说的 DVFS动态电压频率调节就是这一层在做。处理器空闲层CPUIdle负责在 CPU 没有任务时进入各种 idle state比如 WFI、WFI 加关 cache、关电源域让 CPU 在“发愣”的时候少耗电。设备电源管理层也就是 Runtime PM负责在某个外设暂时不用时关掉它的时钟、电源轨道、IO 电源用的时候再恢复。这是设备级别的“零碎省电”。系统睡眠层suspend/resume也就是整机挂起到内存suspend-to-RAM或者硬盘suspend-to-disk所有设备统一进入低功耗状态。这四层不是独立工作的。举个我调试过的例子手机息屏放在桌上系统进入 idleCPU 频率被调度器逐渐压到最低屏幕、触摸、传感器这些外设逐个被 Runtime PM 挂起如果整机持续空闲系统才进入更深度的 suspend。也就是说一个完整的省电流程是“频率先降、设备再睡、系统最后躺平”。只有把这四层拼起来整机的功耗才能压下来。1.2 各子系统的职责分工与典型接口刚接手一个平台时我最常做的一件事就是先对照下面的表格把每个子系统的入口和观察点找出来不然后面排查功耗问题时会特别被动。子系统作用对象核心任务典型内核接口sysfs 观察点CPUFreqCPU 核心调频调压匹配负载需求cpufreq_driver、cpufreq_governor/sys/devices/system/cpu/cpu*/cpufreq/CPUIdleCPU 核心空闲时进入低功耗状态cpuidle_driver、cpuidle_governor/sys/devices/system/cpu/cpu*/cpuidle/Runtime PM单个设备设备不用时停时钟/断电dev_pm_ops 中的 runtime_suspend/resume/sys/devices/.../power/Devfreq非 CPU 设备GPU/DDR动态频率调节devfreq_dev_profile/sys/class/devfreq/Genpd电源域管理一组设备的电源开关generic_pm_domain/sys/kernel/debug/pm_genpd/Suspend/Resume整机系统级睡眠platform_suspend_ops/sys/power/state这张表里 Devfreq 和 Genpd 可能有人不熟但它们恰恰是设备级能源管理里绕不开的东西。Devfreq 可以理解成“非 CPU 版 CPUFreq”GPU、DDR 控制器这类设备频率也会随负载动态变化Genpd 则是把一组设备绑定到一个电源域里域里的最后一个设备 Runtime PM 挂起整个域的电源和时钟才会关掉。后面在第四章里会详细讲它们怎么和 Runtime PM 协作。1.3 能源管理的核心目标不是“省到极致”而是“供需匹配”内核做能源管理目标从来不是把功耗降到最低而是让功耗匹配当前负载同时满足性能和时延要求。举个例子后台音乐播放时CPU 频率可以低一些但音频 DMA 中断来了必须立刻响应如果 CPU 进入太深的 idle state中断响应时延就会超标音乐就会卡顿。这就引出了能源管理里一个非常核心的矛盾省电和响应速度是天然对立的。CPUFreq 调频有延迟Runtime PM 挂起设备也有恢复时间所以内核引入了 PM QoS 机制来协调。简单说谁需要更快响应谁就可以注册一个 QoS 约束CPUFreq 和 CPUIdle 在决策时必须遵守这些约束。理解了“供需匹配”这个核心思想再看后面每个子系统的设计逻辑就全都对上了。2. CPUFreq 子系统CPU 频率是怎么被管起来的2.1 三层架构核心逻辑、驱动、策略CPUFreq 在内核里的实现不是一坨代码而是分了三层理解这个分层是看懂调频流程的前提。第一层是 cpufreq core也就是核心框架负责把所有 CPU 的调频需求统一管理维护每个 policy 的状态向上提供 sysfs 接口向下调用具体的驱动。第二层是 cpufreq_driver它封装了具体硬件怎么改频率比如写硬件寄存器、配置 PLL、改供电电压等。常见的驱动有 intel_pstate、amd-pstate、qcom-cpufreq-hw以及嵌入式平台常用的 cpufreq-dtdevice tree 驱动。第三层是 cpufreq_governor也就是调频策略决定“当前负载下应该跑什么频率”。三层的关系可以类比成governor 是决策者core 是调度中心driver 是执行者。决策者说要 1.8GHzcore 把这个指令整理好driver 动手改硬件。实际工程中改动哪一层影响范围完全不同嵌入式平台上换一个平台通常只需要换 cpufreq-dt 里的 OPP 表想改变整机调频行为则要选对 governor 甚至改 governor 逻辑。2.2 Governor 选型从 performance 到 schedutilGovernor 是调频策略的核心选错了要么费电要么性能差。我梳理一下最常见的几个Governor行为特点适用场景performance固定在最高频率基准测试、性能优先场景powersave固定在最低频率只求低功耗、不在乎性能userspace由用户态写入目标频率调试、特殊调频策略ondemand按 CPU 负载中断式调频老平台常用响应有延迟conservative负载变化时逐步调频负载波动大的省电场景schedutil跟随调度器负载即时调频现代内核默认选择我实际用得最多的是 schedutil它在 4.7 之后进入内核后来成为大多数架构的默认 governor。它的核心思路是直接读取调度器的负载跟踪结果PELT 计算出的 util_est而不是通过采样中断来估算 CPU 负载。这样调频决策和任务调度在时间上完全同步负载上来频率立刻跟着涨不用等采样周期。很多人在低版本内核上觉得调频“慢半拍、卡顿”换到 schedutil 之后体感明显改善原因就在这。不过 schedutil 也不是万能药。它依赖调度器的负载数据如果任务频繁迁移、或者有大量不可睡眠的内核线程它也会出现频率估计偏高或偏低的情况。此时我会配合/sys/kernel/debug/sched/下面的调试节点观察负载变化再决定是否调整 schedutil 的 rate limit 或者改用其他 governor。2.3 sysfs 实操怎么看频率、切 governor、限频CPUFreq 在 sysfs 里的信息非常丰富我调频问题最常用的路径是/sys/devices/system/cpu/cpu0/cpufreq/。先在目标 CPU 上查当前状态# 查看当前频率 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq # 查看可用的 governor cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors # 查看当前 governor cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 查看频率表 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies切换 governor 和限制频率范围也很直接# 切到 schedutil echo schedutil /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 限制最大频率为 1.5GHz单位 kHz echo 1500000 /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq # 限制最小频率为 300MHz echo 300000 /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq批量操作所有 CPU 时写个 for 循环或者用 cpupower 工具更省事# 查看所有 CPU 频率信息 cpupower frequency-info # 设置所有 CPU 的 governor cpupower frequency-set -g schedutil # 限制频率范围 cpupower frequency-set -d 300000 -u 1500000这里有个细节要注意scaling_cur_freq在部分平台上实际上是 governor 的计算目标频率而不是硬件实际频率真正的硬件频率要看cpuinfo_cur_freq但这个节点在有些驱动下不提供。所以碰到频率读数和预期不符时别急着怀疑硬件先确认一下你读的是哪个节点。2.4 调频不是免费的频率切换的代价与时机CPUFreq 调频看着简单实际上每次切换频率都有代价。切换过程中 CPU 要花时间改 PLL、重新校准电压这段时间内任务执行会受影响。在 SMP 系统上一个核心调频还会影响其他核心的共享资源比如总线频率、电源域稳定性。频率差得越大切换代价越大。所以内核里的 governor 普遍做了频率变化的“惰性”处理负载只有短暂尖峰时不一定立刻升频而是等负载持续超过阈值才升同样的负载刚降下来时频率也不会马上降到最低要维持一小段时间防止频繁抖动。schedutil 里的 rate_limit 就是这个作用默认是 20ms 内最多调频一次。我踩过的一个坑是为了“更省电”把 rate limit 调得特别小结果系统在高低频之间来回跳功耗反而更高延迟也变大了。调频和换挡一个道理频繁换挡肯定比匀速行驶费油。所以做调频优化时先看自己的场景是突发型负载还是持续型负载再决定要不要动 rate limit。3. Runtime PM设备级的运行时电源管理3.1 核心思想设备不用时就“断电”用之前要“通电”Runtime PM 管的不是 CPU 频率而是单个设备的电源状态。它的核心思想非常朴素一个设备有工作要做时保证它处于打开状态没有工作时就把它的时钟关了、电源断了、IO 隔离了让它进入低功耗状态。有趣的是Runtime PM 不要求驱动自己控制电源而是定义了一组回调函数由内核在适当的时机调用。设备驱动要做的事就是“告诉内核我需要用什么、什么时候用完”剩下什么时候断电、什么时候上电由内核根据设备状态和应用场景来决定。这个思想在嵌入式上特别有用。一块手机 SoC 上挂了摄像头、GPU、ISP、DSP、音视频编解码器它们不可能同时工作。比如你只在后台播放音乐屏幕、摄像头、ISP 这类设备就完全可以被 Runtime PM 挂起有时候整个显示子系统功耗能降下来好几倍。这比只调 CPU 频率带来的收益大得多。3.2 核心数据结构与状态机Runtime PM 的核心状态机定义在struct dev_pm_info里其中runtime_status字段表示设备当前处于哪种状态状态含义RPM_ACTIVE设备正在工作电源/时钟已打开RPM_SUSPENDED设备已挂起电源/时钟已关闭RPM_SUSPENDING正在执行 runtime_suspend 回调期间RPM_RESUMING正在执行 runtime_resume 回调期间设备驱动通过struct dev_pm_ops里的runtime_suspend、runtime_resume、runtime_idle三个回调来参与这个过程。runtime_suspend做“断电”的最后一步比如关时钟、保存寄存器runtime_resume做“上电”的第一步比如恢复寄存器、开时钟runtime_idle则是内核在设备空闲时先询问驱动“你要不要现在挂起”驱动可以选择立即挂起也可以延迟执行。有个非常容易忽视的点runtime_status和设备实际电源状态不是一回事。内核帮你调了状态机但真正硬件是不是断电了取决于回调里执行的代码。我见过不少驱动把runtime_suspend写成空函数sysfs 里显示设备已挂起实际功耗一点没降。检查这种问题时直接量板子上的电流最靠谱。3.3 核心 API 使用套路在驱动里使用 Runtime PM核心就是管理引用计数。每次“我要用设备”就pm_runtime_get()每次“我用完了”就pm_runtime_put()引用计数归零后内核才会考虑挂起设备。看一个典型流程// 在设备驱动 probe 里初始化 Runtime PM pm_runtime_enable(dev); pm_runtime_set_autosuspend_delay(dev, 200); // 200ms 自动挂起 pm_runtime_use_autosuspend(dev); // 在打开设备时获取引用 pm_runtime_get_sync(dev); // ... 设备工作 ... // 在关闭设备时释放引用 pm_runtime_put(dev);这里最常用、也最坑的几个 API 差异很大我列一下实际工程中踩过的区别pm_runtime_get()和pm_runtime_put()是异步的只改引用计数和状态实际挂起/恢复动作在 workqueue 或 idle 路径里异步执行。pm_runtime_get_sync()和pm_runtime_put_sync()是同步的会立刻执行 runtime_resume 或 runtime_suspend 回调适合中断上下文或需要立即完成操作的场景但调用时要注意不能在原子上下文里长期阻塞。pm_runtime_get_noresume()只增加引用计数不触发恢复操作pm_runtime_put_noidle()只减少引用计数不触发挂起操作。这两个是给特殊场景用的比如设备本来就处于活动状态但不想被打断。还有一个容易踩的雷是irq_safe标志。如果调用pm_runtime_irq_safe()设置了这个标志那么 runtime_suspend/resume 回调可能在中断上下文里执行回调内部绝对不允许睡眠。反过来如果没设这个标志调用pm_runtime_get_sync()时要求不能在原子上下文里否则会触发调度器告警。3.4 sysfs 控制与观察Runtime PM 在 sysfs 设备节点下提供了一组 power 接口调试时非常有价值# 查看设备当前状态 cat /sys/devices/.../power/runtime_status # 查看/设置控制方式on 表示一直打开auto 表示允许自动挂起 cat /sys/devices/.../power/control echo auto /sys/devices/.../power/control # 查看引用计数 cat /sys/devices/.../power/runtime_usage # 查看累计挂起/活动时间单位 ms cat /sys/devices/.../power/runtime_suspended_time cat /sys/devices/.../power/runtime_active_time # 设置自动挂起延迟单位 ms前提是已经启用 autosuspend echo 200 /sys/devices/.../power/autosuspend_delay_msruntime_usage这个字段特别值得关注。如果设备一直不能挂起多半是这个值不为 0说明有驱动模块一直持有引用没有释放。我在调试中见过最典型的情况是某个驱动在 open 里调了pm_runtime_get_sync()但 release 里忘了pm_runtime_put()结果这个设备永远挂在 ACTIVE 状态整机功耗高出正常值 300mW 以上。这种问题用 sysfs 一查runtime_usage马上就能定位到设备再配合代码检索就能找到元凶。提示不是所有设备都支持 Runtime PM。如果/sys/devices/.../power/runtime_status显示 unsupported说明这个设备的驱动或总线没有注册dev_pm_ops或者没有调用pm_runtime_enable()先补驱动再谈调试。3.5 自动挂起延迟的权衡Runtime PM 有一个 autosuspend 机制设备引用计数为 0 后不会立刻挂起而是等一个延迟时间如果这段时间内设备又被使用就取消挂起。这个机制是防止“用一下就挂、马上又用”带来的频繁开关开销。这个延迟设置多少非常依赖场景。我的经验是对人类操作相关设备触摸屏、按键延迟可以设 500ms 左右因为用户短时间内再次操作的可能性高。对传感器这类周期性工作的设备延迟最好大于采样周期否则每次采样都要从挂起状态恢复反而增加功耗。对音频、视频这类流式设备可能完全不适合 autosuspend直接用 0 延迟强制快速挂起更合适。之前我在一个智能音箱项目上把音频编解码器的 autosuspend 延迟设成了 50ms结果每次播放声音时编解码器频繁在挂起/恢复之间切换耗电反而比一直开高。后来改成手动控制播放前pm_runtime_get_sync()、播放结束后pm_runtime_put()功耗才真正降下来。4. 从 CPU 到设备的大协同QoS、CPUIdle、热管理怎么联动4.1 PM QoS给调频和挂起加上“硬约束”前两章把 CPUFreq 和 Runtime PM 分开讲了但真实系统里它们是穿在一起的穿针引线的主角就是 PM QoS。PM QoS 的本质是一个带约束条件的资源申请机制。一个设备如果对性能有硬性要求可以注册一个 QoS request比如“我的中断响应必须在 100us 以内”那么内核在决定 CPU 进入多深的 idle state、频率降到多低时都必须把它考虑进去。对 CPUFreq 而言关键是freq_qos它允许设备请求某个频率下限或上限。比如相机 ISP 在处理图像时会请求 CPU 频率不能低于 1.2GHz保证图像处理不掉帧。CPUFreq 在调整频率时会先遍历所有 QoS request取其中最高的下限作为底线不能越线调频。对 Runtime PM 而言关键是resume_latency约束。一个设备挂起后恢复是需要时间的如果某个框架要求它的恢复时延不能超过 10ms而当前设备的实际恢复需要 50ms那么内核宁可不挂起它也不要冒险耽误后续工作。这个机制在 NVMe、USB 这类对时延敏感的设备上非常常见。4.2 CPUFreq 与 CPUIdle两个 governor 的分工配合很多人容易把 CPUFreq 和 CPUIdle 搞混其实它们管的是两件不同的事CPUFreq 管“CPU 跑多快”CPUIdle 管“CPU 没事时多懒”。一个内核里有两个独立 governorcpufreq governor 决定调频策略cpuidle governor 决定进入哪个 idle state。这两个 governor 的联动很有意思。CPU 准备进入 idle 之前cpufreq governor 可能会把频率先调低因为反正没事做不需要高频但 CPU 一旦被唤醒又需要尽快把频率提上去不然任务会被拖慢。在 schedutil 成为默认 governor 之后这种“先降频再睡眠、唤醒后快速升频”的配合变得更顺畅了因为调度器在唤醒路径上会主动触发调频请求。我调试低功耗问题时发现一个常见现象系统进入 idle 后频率降到最低但负载一来频率从最低升到最高的延迟过长导致操作卡顿。这个延迟来自两部分一是 cpufreq 驱动改硬件频率的时间二是 governor 的 rate limit。如果用的是 intel_pstate 这类硬件调频驱动切换速度会快很多如果是嵌入式平台用 cpufreq-dt 配合软件调压延迟就可能到几百微秒甚至毫秒级。碰到这种情况优先排查是驱动慢还是策略慢再对症下药。4.3 Runtime PM 与 Genpd关掉一整片电源域Runtime PM 只关单个设备但现代 SoC 上很多外设是共享电源轨和时钟的。比如摄像头 ISP 和图像编解码器可能共用同一个电源域单独挂起其中一个没有意义因为它们只要有一个在干活整个域的电源就得开着。内核用 Generic Power Domaingenpd来解决这个问题。Genpd 把一组设备放进一个电源域域里每个设备各自通过 Runtime PM 管理自己的开关状态。当域内最后一个设备进入 runtime suspend 时genpd 的回调会被触发这时才真正关掉这个域的电源和时钟。也就是说Runtime PM 是“设备级”的开关genpd 是“域级”的开关层层递进。调试 genpd 时我最常用的入口是/sys/kernel/debug/pm_genpd/里面能看到每个电源域的实时状态、域内设备列表和挂起次数。比如一个电源域显示on但域里所有设备都已经 runtime suspend那就说明有 genpd governor 认为当前还不适合关闭这时就得查约束或者看域里是否有设备仍然活跃。4.4 与 Thermal 联动频率还得看“脸色”能源管理和热管理是孪生兄弟。CPUFreq 降频不只是为了省电很多时候是为了降温。Linux 的 thermal 框架把 CPU 频率看成一个 cooling devicethermal governor 在温度超标时会通过cpufreq_cooling设备来降低最大允许频率。这个联动常见的问题是“频率被钳制而不知道”。系统温度高了thermal 把 CPU 最高频率限制到 1.0GHz但用户态读到的scaling_max_freq可能还是标称的 2.4GHz看不出限制在哪。真的要排查频率异常需要看 thermal 的 cooling device 状态路径一般在/sys/class/thermal/cooling_device*/里面cur_state表示当前节流等级max_state表示总的等级数。如果cur_state不为 0说明热管理正在介入频率上不去是“热的锅”不是调频策略的问题。4.5 场景串联一次完整的省电流程把上面这些机制串起来看一个完整场景会更直观。假设一部手机息屏后放在桌上用户按下电源键屏幕背光关闭显示子系统进入 Runtime PM 挂起显示屏电源域被 genpd 关掉。系统进入 idleCPUIdle 逐步进入低功耗 idle state。后台任务负载很低schedutil 根据调度器负载把 CPU 频率逐步降到最低档。一些传感器设备按周期工作两次采样之间进入 runtime suspend。如果整机继续空闲系统最终会进入 suspend-to-RAM所有设备执行系统挂起回调CPU 进入更深度的电源状态。在这个完整链路里任何一环出问题都会导致整机功耗异常。比如 genpd 没正确关闭显示电源域、某个驱动在 Runtime PM 回调里没关时钟、CPUIdle 的 polling 状态被错误配置导致 CPU 空转这些都会直接反映在整机电流上。5. 实测排查与常见问题5.1 几个值得养成的调试习惯做能源管理调试光看代码是不够的得养成用工具和实时数据说话的习惯。我常用的手段有以下几种用 trace event 观察 Runtime PM 的实际动作cd /sys/kernel/tracing echo 1 events/rpm/rpm_suspend/enable echo 1 events/rpm/rpm_resume/enable echo 1 tracing_on cat trace这个 trace 能直接看到每个设备什么时候、因为什么原因被挂起或恢复对排查“设备为什么一直不挂起”特别高效。我自己曾经抓到一个问题一个 USB 控制器因为子设备还持有引用导致整机无法进入低功耗状态就是靠 trace 先把设备范围缩小再顺着引用计数查到 root cause 的。用 powertop 和 turbostat 快速摸底powertop 在桌面和服务器平台很好用能统计 CPU 频率分布、设备唤醒次数、电源状态占比。turbostat 则更偏处理器可以直接看每个 CPU 核心在 C-state 和 P-state 上的实际停留时间。嵌入式平台上没有这些工具的话就多依赖 sysfs 节点和 debugfs 里的 pm_genpd。5.2 常见问题速查表下面这几个问题都是我在调试过程中反复遇到的整理成速查表供参考问题现象可能原因排查方向设备一直显示 active挂不起来有驱动持有引用未释放查看 power/runtime_usage搜索对应驱动 get/put 配对设备显示 suspended但整机功耗没降runtime_suspend 回调是空实现或未关时钟检查回调代码实测挂起前后电流频率上不去thermal 节流 / PM QoS 频率上限约束查看 cooling_device cur_state、freq_qos 约束frequency 在 sysfs 里不变驱动不支持调频或 governor 是 performance/powersave查 scaling_driver、scaling_available_governors唤醒后明显卡顿设备恢复时延过长或 CPUFreq 升频延迟大查看 rpm resume trace检查 autosuspend 延迟设置genpd 显示 on但设备全挂起genpd governor 认为有子设备未 idle查看 debugfs/pm_genpd 里的域状态与约束系统睡眠后电流居高不下某个设备没进入系统挂起状态用 sysfs power/runtime_status 和 /sys/power/wakeup_count 排查5.3 我踩过的几个实在坑做这行时间久了难免踩坑。分享三个我自己印象最深的希望你们能绕开。第一个坑pm_runtime_get_sync()在设备 probe 里调用后忘记配对的pm_runtime_put()。这是最经典的 Runtime PM 问题。我接手一个项目时某个 GPIO 扩展芯片的 probe 里对每个子设备都做了 get_sync但去掉引用只在 remove 里执行导致系统只要启动这个芯片就永远无法 runtime suspend。整机待机电流多出几百毫安。排查方法很简单看 power/runtime_usage 是否为 0不是就说明有引用泄漏。第二个坑误以为 sysfs 显示 suspended 就真的省电了。前面说过runtime_suspend 回调可以是空实现。有一次我们验证一款音频 codec 的功耗优化效果软件上怎么查都是“已挂起”但整机电流纹丝不动。最后量了硬件引脚发现主时钟还开着。这个 codec 的驱动注册了 runtime PM 但回调里没做任何实际关时钟的动作。从那以后我养成了习惯任何电源优化改动最后都以整机电流实测为准软件状态只作参考。第三个坑autosuspend 延迟设置不当导致功耗不降反升。对于一个周期性唤醒的设备autosuspend 延迟小于设备唤醒周期会导致设备频繁在挂起/恢复之间翻转。每翻转一次都要重新初始化寄存器、恢复时钟耗电可能比一直开着还大。正确做法是延迟要大于设备的唤醒周期或者干脆手动控制引用计数避免自动机制带来不必要的翻转。5.4 最后的经验之谈做了这么多年能源管理我最深刻的体会是这个领域没有银弹没有哪个配置能适合所有场景。CPUFreq 选 schedutil 还是 performanceRuntime PM 开 autosuspend 还是强制手动必须结合设备的实际使用模式来定。怀疑一个设备耗电异常先看 sysfs 状态和整机电流再动手改代码。功耗优化是一个反复测量、修改、再测量的过程工具链用熟了剩下的耐心和细心决定成败。