深度解析Linux内核runtime PM:核心机制与驱动落地实践
做内核功耗这摊子事的人迟早都要跟 runtime PM 打交道。不管你是调显示驱动、网络驱动还是搞 MIPI 摄像头只要芯片有低功耗需求就一定绕不开这个机制。这篇文章是系列第七篇专门把 runtime PM 这条线彻底捋一遍从核心数据结构到 API 使用方式再到驱动里怎么落地、出问题了怎么查一次讲透。1. runtime PM 在设计上解决了什么问题在讲 API 和代码之前先得把 runtime PM 出现的原因想清楚。很多做驱动的人一开始都会困惑系统不是有 suspend/resume 吗为什么还要搞一套 runtime PM这两套机制完全是两码事。系统级 suspend/resume 是全局操作一旦触发整个 SoC 都要进低功耗状态所有设备按依赖关系排队挂起这个过程通常很慢动辄几百毫秒甚至几秒。而 runtime PM 解决的是单个设备在系统正常运行时的空闲功耗问题。比如一部手机亮屏待机时系统整体在运行但是触摸屏控制器其实已经没事干了Sensor Hub 也只需要在数据变化时醒一下。这些设备如果不切到低功耗状态芯片就会一直白白耗电。两个机制叠加起来看系统 suspend 像一个厂房整体断电而 runtime PM 就是每个工位上不干活时随手关掉工位上的小灯。这个区别非常重要因为两者在代码路径、锁机制、回调函数上有本质差异。runtime PM 最核心的思路是围绕设备的使用计数和状态机来运作。当一个设备不再被使用时系统可以自动把它挂起当需要操作它时再把它唤醒。整个过程不需要用户态介入驱动开发者在 probe 阶段把回调注册好、把自动挂起的参数配置好内核会在适当时机完成后续工作。这套机制最早在嵌入式平台上大规模落地现在已经是 Linux 内核里标准化的功耗管理基础设施。从 kernel/drivers/base/power/runtime.c 的核心实现到 include/linux/pm_runtime.h 暴露给驱动的 API再到 Device Tree 里的电源域和状态节点配置一整条链路都围绕着 runtime PM 展开。对驱动开发者来说理解 runtime PM 至少有三个实际好处第一能够正确管理设备功耗延长电池续航或降低整机发热第二避免低功耗状态导致外设工作异常比如 I2C 挂死、中断丢失第三能用标准机制替代自己手写的状态机代码更简洁也更容易让后续维护的人看懂。2. 核心机制拆解状态机、引用计数与回调函数2.1 设备状态机ACTIVE、SUSPENDED 与 SUSPENDINGruntime PM 的内部状态是通过 enum rpm_status 来描述的关键有三个状态RPM_ACTIVE、RPM_SUSPENDED以及两个中间状态 RPM_SUSPENDING 和 RPM_RESUMING。RPM_ACTIVE 表示设备当前已经处于工作状态时钟、电源、IO 全都正常读写寄存器没有问题。RPM_SUSPENDED 则表示设备已被挂起此时从软件层面看对设备的访问需要先触发 resume 流程否则操作很可能挂死或读回全 F。两个中间态是异步操作时才会出现的。异步请求 resume 或 suspend 时内部会先把状态置为 RESUMING 或 SUSPENDING然后通过工作队列异步执行整个切换流程。同步操作虽然也会短暂经过这些状态但因为调用者在原地等到切换完成感知上就只有 active/suspended 两种。需要特别留意的是runtime PM 的状态只是一个软件状态记录不代表硬件一定处于什么状态。比如你可以在 runtime_resume 回调里不实际开时钟、不拉电源只是把状态标记为 active。这么做虽然能用但功耗优化效果为零。真正的好坏取决于回调函数里做了什么所以驱动开发者不能只想着调用 PM 框架的 API更得把回调里的硬件操作写到位。2.2 引用计数usage_count 与 disable_depth每个设备的 struct device 结构体里有一个 dev_pm_info 类型的 power 字段它内部维护了 usage_count、disable_depth、runtime_status 等关键信息。usage_count 是门卫设备只要有一个引用者就坚决不能挂起。有几个 API 直接操作这个计数pm_runtime_get 把 usage_count 加一pm_runtime_put 把 usage_count 减一。当计数从 1 变到 0 时内核会触发挂起流程当计数从 0 变到 1 时设备如果处于挂起状态就会触发恢复流程。使用计数的设计逻辑非常像 C 的 shared_ptr 或者 Python 的引用计数。多个驱动模块可以同时引用同一个设备比如一个 MIPI DSI 显示屏驱动被 framebuffer 和 DSI controller 同时引用只要还有人在用设备就不会被挂起。只有所有引用者都释放了设备才有资格进入低功耗状态。disable_depth 则是另一个维度的控制。它表示 runtime PM 功能是否被使能。初始状态是 1设备创建后 runtime PM 默认是禁用的。驱动必须在 probe 过程调用 pm_runtime_enable将 disable_depth 减到 0运行时电源管理才算真正打开。反过来remove 时调用 pm_runtime_disable把功能关闭。如果忘了调 pm_runtime_enable后面无论怎么 get/put设备都不会挂起或恢复所有调用都会静默失败。这个问题我在实际代码里见过不止一次而且很难发现因为内核日志上没有任何报错。2.3 回调函数runtime_suspend、runtime_resume 和 runtime_idle设备驱动的 dev_pm_ops 结构体里有三个与运行时电源管理相关的回调static const struct dev_pm_ops xxx_pm_ops { .runtime_suspend xxx_runtime_suspend, .runtime_resume xxx_runtime_resume, .runtime_idle xxx_runtime_idle, };runtime_suspend 在 usage_count 降到 0 时调用负责关时钟、关电源域、停 DMA、把寄存器保存下来等。runtime_resume 则在 usage_count 从 0 变 1 时调用负责重新开时钟、恢复电源、写回寄存器、重建 DMA 通道。runtime_idle 的语义更柔和它通知驱动设备已经空闲但还没真正挂起驱动可以选择现在就开始做准备也可以忽略这个通知。runtime_idle 有一个容易误用的点。如果驱动不提供 runtime_idle 回调内核会马上执行自动挂起如果提供了但回调返回 0内核会认为驱动已经处理了空闲事件不再往下走到 runtime_suspend。很多驱动为了让设备立即进入挂起状态会故意让 runtime_idle 返回 -EBUSY 或直接不注册。想利用 autosuspend 延迟挂起就会在回调里启动一个延迟定时器。这些用法没有绝对的对错只要跟产品的功耗策略匹配就好。2.4 父子设备依赖与唤醒机制runtime PM 框架还处理了设备的层级依赖问题。一个设备挂起前先检查它的 child 设备是否都已经挂起如果有子设备还在活跃状态父设备就不允许挂起。这是在 struct dev_pm_info 内部的 child_count 以及 ignore_children 标志配合下实现的。反过来父设备恢复时会先递归恢复所有子设备。这个顺序很讲究因为很多硬件依赖是父设备提供的时钟、电源或 IO 域。父设备没准备好子设备就算调用了 resume 回调和硬件沟通也会失败。这跟系统 suspend 里的 dpm_list 顺序是一致的只不过 runtime PM 是每个设备独立触发更加细粒度。对于唤醒设备runtime PM 也有对应的 wakeup 机制。设备可以设置 wakeup source在挂起状态下产生唤醒信号把系统或某个电源域拉起来。enable_irq_wake 这类操作在 runtime_resume 回调里经常出现尤其是对触控、传感器这类需要超低功耗待机的外设来说是很常规的做法。3. 核心 API 梳理从 get 到 put 的完整闭环3.1 同步接口与异步接口pm_runtime_get_sync 和 pm_runtime_put_sync 是同步接口调用后会阻塞直到设备完全进入目标状态。pm_runtime_get_sync 在执行中会等 runtime_resume 返回pm_runtime_put_sync 会等 runtime_suspend 返回。这两个接口在正常的进程上下文用最保险。我建议驱动的文件操作接口、IOCTL、以及可以睡眠的线程上下文中优先使用同步接口。它们虽然慢一点但语义清晰出问题时好排查。异步接口 pm_runtime_get 和 pm_runtime_put 只调整引用计数并下发请求实际的恢复或挂起操作放到内核工作队列 pm_wq 里异步执行。异步接口可以在中断上下文调用比如网卡收包中断里通过 pm_runtime_get 唤醒设备等唤醒完成后再读 FIFO这个流程如果用同步接口就会导致睡眠在原子上下文中直接崩溃。kernel 文档里有一张经典表格列出了每个接口对应在何种上下文安全使用我直接抄在这里供参考接口中断上下文原子上下文进程上下文pm_runtime_get_sync禁止禁止允许pm_runtime_put_sync禁止禁止允许pm_runtime_get允许允许允许pm_runtime_put允许允许允许实际写代码时我经常看到有人不分场景一律用同步接口。短时间看没出问题但在某些高速中断里就会偶发 panic。测试通常很难压出来等到量产现场才爆雷。所以在驱动里定义一套自己的封装习惯非常重要凡是能在进程上下文处理的统一用 get_sync/put_sync可能进中断路径的地方替换成异步接口加一个完成量或者 refcount 来保证时序。3.2 引用配对与 error code 的陷阱pm_runtime_get_sync 的返回值需要特别小心。这个函数如果在设备恢复过程中出错除了返回错误码还会把 usage_count 减回去。也就是说它实际上是 get 操作如果失败就会变回没拿过的状态。所以正常写法必须判断返回值不然你觉得自己持有了设备引用实际上并没有后续操作设备可能白忙活甚至踩坏状态。相反pm_runtime_put_sync 不管内部是否出错都会把计数减一。如果 put 之后设备立刻被再次 get两次调用之间也要保证时序正确。很多 bug 出现在两个线程同时操作一个设备时一边 get 一边 putusage_count 就像仰卧起坐一样一直在 1 和 2 之间跳。要避免这个问题必须给引用操作加合适的锁或者用 atomic 语义。还有一个容易疏忽的地方是自动挂起的延迟机制。如果启用了 autosuspend调用 pm_runtime_put 后设备不会立刻挂起而是等 autosuspend_delay 超时后才挂起。有一种典型用法用户关闭一个节点驱动里 put 一下然后立刻手动调用 pm_runtime_suspend 来强制挂起。这会导致 autosuspend 的延迟毫无意义甚至可能跟后面的异步挂起请求冲突。要么就不开 autosuspend要么就不要手动强制挂起二选一。3.3 autosuspend 的配置与场景选择autosuspend 的核心价值是避免设备频繁地在 active 和 suspended 间乒乓切换。每次切换都有成本尤其是 MIPI 或 PCIe 这类需要重新协商链路的设备几十毫秒的恢复时间期间如果数据又来系统会陷入歇一下又忙起来的恶性循环。pm_runtime_set_autosuspend_delay 用来设置超时时间pm_runtime_use_autosuspend 用来使能自动挂起模式。我通常习惯把网卡、蓝牙这类流量有突发性的设备设置 100ms 到 500ms 的延迟把触摸屏这类交互设备设置 1 到 2 秒。具体数值一般通过 modprobe 参数或者设备树属性暴露方便后期调试时改。与 autosuspend 相关但常被忽略的一个接口是 pm_runtime_idle。在非 autosuspend 模式下设备空闲后内核会先调用 runtime_idle 回调如果回调返回 0 才会继续 runtime_suspend如果驱动希望延迟挂起可以在 runtime_idle 里返回 -EBUSY让内核放弃本次挂起尝试然后驱动自己安排定时器超时后再强制挂起。这种做法在 WiFi 驱动里很普遍因为网卡一旦挂起再恢复重新关联 AP 的时间成本非常高。3.4 force 系列接口什么时候需要绕过引用计数pm_runtime_suspend、pm_runtime_resume 和 pm_runtime_forbid 这些接口不修改 usage_count而是直接强制设备进入某个状态。pm_runtime_suspend 会忽略 usage_count直接调用 runtime_suspend 回调把设备挂起。pm_runtime_resume 亦然。这类强制接口适合设备进入低功耗功能模式时使用比如传感器要进入 one-shot 采样模式内核先把设备强制挂起然后通过一个特殊寄存器唤醒做单次采集采集完成后再次强制挂起。这种场景并不改变设备的被使用语义而是在功耗上做精细控制。但强制接口用起来要非常谨慎。如果代码中同时存在 get/put 引用和 force 操作很容易出现状态错乱。我一个同事就因为在一个驱动里混用这两种方式结果出现偶发的 resume 请求在 suspend 过程中被打断设备状态变得既不是 active 也不是 suspended后续操作全部超时。直到加了完整的状态机日志才定位到问题。3.5 runtime PM 与系统 suspend/resume 的联动runtime PM 不是孤立运行的它与系统级 PM 之间有明确的交互逻辑。系统 suspend 流程中__device_suspend 会先看设备是不是因为 runtime PM 已经挂起了如果是就直接跳过 suspend 回调如果没有挂起则正常执行 suspend。同样在系统 resume 时如果设备之前是 runtime suspendedresume 回调会触发一次 runtime_resume然后系统再执行完整的 resume 回调。这套设计的关键点是 runtime PM 的挂起和系统 suspend 的挂起不能互相覆盖。如果驱动在 runtime_suspend 里关了中断、停了 DMA但在系统 suspend 回调里没有做对应处理恢复时就会出现设备不可用的隐患。所以大多数复杂驱动在 system suspend 前会先调用 pm_runtime_resume 或 pm_runtime_get_sync 强制设备回到 active保证系统 suspend 时设备处于一个已知状态。4. 实操在平台驱动中完整落地 runtime PM4.1 基本框架probe、remove、open 和 release 中的配对用一个虚拟的平台设备举例。假设这个设备是一个 SPI 外设接口内部有独立的 SRAM不工作时可以关掉 PLL 和 GPIO 使能脚。驱动 probe 中的初始化操作通常是这样的static int foo_probe(struct platform_device *pdev) { struct foo_dev *foo; struct device *dev pdev-dev; int ret; foo devm_kzalloc(dev, sizeof(*foo), GFP_KERNEL); if (!foo) return -ENOMEM; platform_set_drvdata(pdev, foo); mutex_init(foo-lock); init_completion(foo-pm_completion); // 注册各种子设备、初始化硬件寄存器等 foo-regmap devm_regmap_init_spi(...); if (IS_ERR(foo-regmap)) return PTR_ERR(foo-regmap); // 关键允许设备运行时电源管理 pm_runtime_set_active(dev); pm_runtime_enable(dev); pm_runtime_set_autosuspend_delay(dev, 100); pm_runtime_use_autosuspend(dev); return 0; }pm_runtime_set_active 这一步容易被忽略但意义重大。它把设备的初始状态标记为 active并绕过一次 runtime_resume 调用。如果不调用设备初始状态是 suspended但硬件时钟和寄存器并没有真正初始化后续对设备的访问会因为设备处于超低功耗状态而失败。remove 时也要倒过来操作static int foo_remove(struct platform_device *pdev) { struct foo_dev *foo platform_get_drvdata(pdev); struct device *dev pdev-dev; pm_runtime_disable(dev); pm_runtime_put_sync(dev); // 卸载中断、释放资源等 free_irq(foo-irq, foo); return 0; }这里有个细节pm_runtime_disable 会把 disable_depth 重新加一让 runtime PM 停止工作。但在 disable 之前应该把引用计数清干净所以前面放了 pm_runtime_put_sync。顺序反了的话disable 之后 put_sync 虽然不会报错但整个引用计数看起来是残的容易对后来排查问题的人产生误导。4.2 文件操作接口里的 get 与 put设备对应的 open 和 release 操作是引用计数最常见的切入点。当一个应用程序打开设备节点时设备必须活跃关闭时可以让它重新进入可挂起状态。static int foo_open(struct inode *inode, struct file *filp) { struct foo_dev *foo container_of(inode-i_cdev, struct foo_dev, cdev); struct device *dev foo-dev; int ret; ret pm_runtime_get_sync(dev); if (ret 0) { dev_err(dev, failed to resume: %d\n, ret); pm_runtime_put_sync(dev); return ret; } filp-private_data foo; return 0; } static int foo_release(struct inode *inode, struct file *filp) { struct foo_dev *foo filp-private_data; struct device *dev foo-dev; pm_runtime_put_sync(dev); return 0; }在 open 里我最常用的写法是 get_sync 然后判断返回值失败就 put 回去。这看起来有点绕但实际是标准做法。因为 pm_runtime_get_sync 在内核源码中带有一个隐含规则如果它返回错误就不能假设设备处于 active 状态内部已经自动执行了 put此时再手动 put 一次就会把计数减成负数。等于是额外留了一个对称性保护。release 里的 put_sync 则触发 runtime PM 自动挂起流程。如果开启了 autosuspend这里不会立刻挂起而是进入延迟计时这非常适合交互类设备。4.3 中断路径中的异步恢复处理有些设备在挂起后仍然能产生中断比如一个低功耗按键检测模块。此时在中断上下文不能直接调用 pm_runtime_get_sync。业界普遍的处理方式是用 pm_runtime_get 加一个完成量在 resume 完成后通过完成量通知中断线程。static irqreturn_t foo_irq_handler(int irq, void *data) { struct foo_dev *foo data; struct device *dev foo-dev; int ret; ret pm_runtime_get(dev); if (ret 0) return IRQ_HANDLED; wait_for_completion(foo-resume_done); // 此时设备一定处于 active可以读取 FIFO、处理事件 foo_read_status(foo); pm_runtime_put(dev); return IRQ_HANDLED; }注意这里的 wait_for_completion 必须在进程上下文可睡眠的前提下才能使用。如果你现在已经在 hardirq 上下文就不能 sleep那么这套方案就不适用了。正确做法是只用 pm_runtime_get 开启异步恢复然后把实际的数据读取挪到 workqueue 或 threaded irq 中。这是中断编程和 PM 框架交织时最容易犯错的地方要格外小心。4.4 runtime PM 回调的落位驱动的 runtime_suspend 主要做两件事保存硬件状态、关闭硬件电源来源。static int foo_runtime_suspend(struct device *dev) { struct foo_dev *foo dev_get_drvdata(dev); // 保存关键寄存器以便 resume 时恢复 foo-saved_reg readl(foo-base FOO_CFG_REG); // 停 DMA、关中断、关时钟 dmaengine_terminate_all(foo-dma_chan); clk_disable_unprepare(foo-clk); // 关电源域或使能脚 regulator_disable(foo-vddio); return 0; }runtime_resume 则是反过来的操作static int foo_runtime_resume(struct device *dev) { struct foo_dev *foo dev_get_drvdata(dev); // 恢复电源域 regulator_enable(foo-vddio); // 重新开时钟、开 DMA clk_prepare_enable(foo-clk); writel(foo-saved_reg, foo-base FOO_CFG_REG); return 0; }这段代码看起来简单但有两个容易弄反的地方。第一时钟和电源域的操作顺序一般是先电源后时钟因为很多时钟模块本身依赖电源域稳定供电。第二如果设备使用 DMA必须在 DMA 通道重建之后再触发上一次未完成的传输否则数据会丢。这些细节在常规文档里不会写但都是实际调试中频繁踩坑的点。4.5 与 regmap/pm_domain 的衔接大多数现代驱动都用 regmap 操作寄存器而 regmap 本身是支持 runtime PM 的。驱动器通过 regmap_init_i2c 或 regmap_init_spi 初始化 regmap 时可以把设备的 power ops 接到 regmap 的 read/write 回调上。实际使用中我常常把 runtime PM 的 get/put 包在 regmap 的 read/write 前后static int foo_regmap_read(void *context, unsigned int reg, unsigned int *val) { struct foo_dev *foo context; struct device *dev foo-dev; int ret; ret pm_runtime_get_sync(dev); if (ret 0) return ret; ret regmap_raw_read(foo-regmap, reg, val, 4); pm_runtime_put_sync(dev); return ret; }这套封装可以确保任何通过 regmap 访问寄存器的路径都不会在设备挂起时直接去访问硬件有效避免了系统挂死和总线错误。总线级 PM 域genpd通常也会在设备挂起时把整条总线电源关掉所以这一步非常关键。5. 常见问题与排查技巧实录5.1 runtime PM 导致驱动 probe 阶段崩溃很多驱动一开始在 probe 阶段添加 runtime PM 代码后发现系统在启动时直接崩了。一个常见原因是 pm_runtime_set_active 和 pm_runtime_enable 的调用顺序反了。必须先 set_active 再 enable因为 enable 之后框架就认为这个设备已经可以被挂起如果它的初始状态是 suspended但又没有对应的恢复路径第一次 get 就会异常。另一个原因是 probe 函数在拿到任何资源之前就启用了 runtime PM然后某个资源申请失败走 error path 时又恰恰触发了挂起流程此时设备的内部结构还没有初始化完全一执行 runtime_suspend 就崩溃。所以建议在 probe 函数的资源申请全部成功之后再考虑开启 runtime PM。5.2 usage_count 一直为 1 导致设备永不挂起设备出不来低功耗状态读 /sys/devices/.../power/runtime_status 时老显示 active用 cat /sys/devices/.../power/usage_count 看到计数是 1怎么都不归零。这种情况通常是驱动代码在某个路径里做了 get 但对应的 put 被丢掉了。常见的位置是错误处理分支。比如一个 write 操作函数前半段 get_sync 成功中间某个判断失败提前 return直接跳过最后的 put_sync。要根除这类问题可以在开发阶段给常用驱动路径加上 WARN_ON(atomic_read(dev-power.usage_count) 0) 这类断言或者直接在 release 函数里检查计数并打印日志逐步缩小排查范围。5.3 resuming/suspending 状态卡死如果 runtime_status 一直停留在 suspending 或 resuming说明异步任务卡住了。最常见的原因是 runtime_suspend 回调里等待了一个永远等不到的事件比如等一个 DMA 完成中断但 DMA 在挂起流程中已经被终止了。排查思路是看内核日志有没有对应驱动打印的阻塞等待信息或者用 crash 工具抓当前任务栈。有一种比较隐蔽的情况是回调函数里调用了某个会睡眠的接口但当前处于 atomic 上下文比如在 timer 回调中触发了挂起流程。这样连调度器都起不来状态机就彻底卡住了。处理方法是检查所有 pm_runtime_suspend/pm_runtime_resume 的调用点确保它们都来自允许睡眠的上下文。5.4 autosuspend 延迟参数太短造成的性能抖动曾经把一块 MIPI DSI 触摸屏的 autosuspend_delay 设置成 10ms结果屏幕操作时明显卡顿。原因其实很简单每次触摸抬起后 10ms 设备就挂起下一次触摸到来时又需要重新 resumeMIPI 链路重新同步的时间比人手的触控间隔还长。对交互类设备建议把 autosuspend_delay 调到 500ms 到 2000ms 左右对网卡这类有週期性流量但又不想频繁开关的设备500ms 通常合适对传感器这类数据频率固定的设备100ms 到 300ms 一般够了。具体的值要结合硬件恢复延迟和业务流量模式综合判断开发时最好做成 module 参数方便现场调优。5.5 调试手段sysfs、tracepoint 和 ftraceruntime PM 的调试信息非常丰富第一个必看的地方是每个设备目录下的 power 节点cat /sys/devices/.../power/runtime_status cat /sys/devices/.../power/runtime_suspended_time cat /sys/devices/.../power/runtime_active_time cat /sys/devices/.../power/usage_count cat /sys/devices/.../power/controlcontrol 文件可以切换 auto/on 来控制设备是否允许 runtime PM 决定挂起。在开发阶段现场怀疑设备因为挂起而出问题时直接把 control 改成 on 可以强制设备一直 active快速排查问题是否和 PM 相关。这个手段非常高效我经常用它做二分定位。如果要在代码层面观察 PM 切换的完整过程可以打开 ftrace 的 power 事件echo power:pm_runtime_begin /sys/kernel/debug/tracing/set_event echo power:pm_runtime_end /sys/kernel/debug/tracing/set_event cat /sys/kernel/debug/tracing/trace这样能看到每次设备的挂起/恢复请求是谁发起的耗时多久成功率如何。配合 function_graph 跟踪某个驱动的 pm_runtime_* 函数调用栈基本能定位绝大多数 runtime PM 异常。6. 驱动开发者的几个实践心得做了几年驱动被 runtime PM 坑过很多次也帮别人救过不少现场。总结几条经验。第一probe 之后立即 pm_runtime_enable但在所有资源就绪前不要让设备进入挂起状态。一个稳妥的办法是 probe 最前面先 pm_runtime_set_active资源全部初始化完再 enable这样即使中途失败设备也始终停留在 active 状态不会触发空回调。第二所有访问硬件的路径不管是在字符设备接口、中断线程、定时器回调还是 regmap 封装里都必须统一通过 runtime PM 的引用计数来保护。千万不要只在某几个操作里加保护因为硬件挂起之后任何未保护的寄存器访问都会造成不可预期的结果这种 bug 最难复现也最难以排查。第三给 runtime_suspend/runtime_resume 回调函数加充足的日志尤其在新芯片 bringup 阶段。内核的 PM 日志经常被裁剪可以自己在回调里用 dev_dbg 或 trace_printk 记录关键节点。量产时再把日志关掉不影响性能。第四多设备协同场景下充分考虑父子设备的依赖关系。如果某个外设挂在 I2C 总线上I2C controller 本身也有 runtime PM 管理那么每次总线空闲都可能把 controller 挂起子设备的操作就会变慢。这时候要么在子设备 get 的同时也 get 父设备要么把父设备的 ignore_children 置位避免父设备提前挂起影响子设备。最后遇到奇怪的硬件异常时先把 runtime PM 关掉并强制设备 active跑一遍业务流程。如果问题消失了大概率是 PM 挂起/恢复的时序或者状态保存出了问题。这套思路在我手上定位过至少四五个疑难杂症比单纯看代码逻辑要快得多。