Linux I/O 设备运行时电源管理框架深度解析
第1章 框架概述
1.1 四大支柱
Runtime PM 框架由四个核心组件构成:
/** * @brief Runtime PM 的四大支柱 * * 1. pm_wq 工作队列 - 所有运行时PM工作项的统一队列 * 2. struct dev_pm_info 字段 - 设备电源状态与同步原语 * 3. struct dev_pm_ops 回调 - 子系统/驱动的挂起/恢复/空闲钩子 * 4. 辅助函数集 - drivers/base/power/runtime.c 提供的API * * @note pm_wq 的强制使用保证了运行时PM操作与系统睡眠转换的同步。 * @note 所有辅助函数由PM核心负责互斥,驱动无需自行实现复杂的同步。 */
1.2 设计哲学
与传统电源管理不同,Runtime PM 的设计哲学是机会主义节能——当设备空闲一段时间后就将其挂起,有 I/O 请求时再迅速唤醒。
工作流程: I/O完成 → 标记最后忙碌时间 → 释放引用计数 → 空闲计时器到期 → 子系统回调 → 驱动回调 → 设备进入低功耗状态 → 新I/O到达 → 恢复设备 → 处理I/O
场景调试:如果设备频繁在挂起和恢复之间“抖动”(bouncing),通常是因为 autosuspend 延迟设置过短。可以通过 /sys/devices/.../power/autosuspend_delay_ms 增大该值。
第2章 设备运行时PM回调
2.1 回调优先级
Runtime PM 的回调执行具有严格优先级:PM domain > device type > device class > bus type。如果所有这些子系统级回调都不存在,PM核心才会调用驱动自身的回调。
/** * @brief 回调查找优先级 * * 1. dev->pm_domain 存在 → 使用 PM domain 回调 * 2. dev->type 和 dev->type->pm 存在 → 使用 device type 回调 * 3. dev->class 和 dev->class->pm 存在 → 使用 device class 回调 * 4. dev->bus 和 dev->bus->pm 存在 → 使用 bus type 回调 * 5. 以上都不存在 → 使用 dev->driver->pm 回调 (如果存在) * * @note 高优先级回调完全负责设备挂起/恢复,可选择是否调用驱动回调。 */
2.2 三种回调的行为契约
| 回调 | 语义 | 关键约束 |
|---|---|---|
->runtime_suspend() |
将设备置于低功耗状态 | 成功后设备状态为 'suspended';返回 -EBUSY/-EAGAIN 表示重试;其他错误为致命 |
->runtime_resume() |
将设备恢复到完全可操作状态 | 成功后设备状态为 'active';致命错误会阻止后续 PM 操作 |
->runtime_idle() |
检查设备是否可挂起 | 返回 0 时PM核心自动尝试挂起;非零值阻止挂起 |
/** * @brief 重要约束 * * - 如果设备需要远程唤醒(remote wakeup)正常工作, * 但 device_can_wakeup() 返回 false,则 runtime_suspend() 必须返回 -EBUSY。 * - 如果 device_can_wakeup() 返回 true,且设备在 runtime_suspend() 中 * 进入低功耗状态,必须启用远程唤醒。 * - 所有输入设备在运行时进入低功耗状态时,远程唤醒必须启用。 */
2.3 回调的互斥保证
PM核心保证以下互斥条件,驱动开发者无需自行实现复杂的锁:
-
除空闲回调外的互斥:
->runtime_suspend()和->runtime_resume()不能与自身或彼此并行执行,但它们可以与->runtime_idle()并行。 -
状态门控:
->runtime_idle()和->runtime_suspend()仅在设备状态为 'active' 时调用。 -
引用计数门控:仅当
usage_count == 0且 (child_count == 0或ignore_children被设置) 时才能执行这两个回调。 -
恢复门控:
->runtime_resume()仅在设备状态为 'suspended' 时调用。
设计精髓:空闲回调被设计为可与挂起/恢复并行的“观察者”——它只检查条件而不修改状态,因此不需要严格的互斥。此设计避免了 I/O 热路径上的锁竞争。
第3章 设备运行时PM字段
/**
* @brief struct dev_pm_info 中的关键 Runtime PM 字段
*/
struct dev_pm_info {
struct timer_list suspend_timer; /**< 调度延迟挂起/autosuspend的定时器 */
unsigned long timer_expires; /**< 定时器到期时间 (jiffies) */
struct work_struct work; /**< 排队到 pm_wq 的工作项 */
wait_queue_head_t wait_queue; /**< 同步等待队列 */
spinlock_t lock; /**< 保护 Runtime PM 字段的自旋锁 */
atomic_t usage_count; /**< 设备使用计数 */
atomic_t child_count; /**< 'active' 子设备计数 */
unsigned int ignore_children:1; /**< 忽略子设备状态 */
unsigned int disable_depth; /**< 禁用深度 (初始=1, 0=启用) */
int runtime_error; /**< 致命错误码 (0=正常) */
unsigned int idle_notification:1;/**< 正在执行 idle 回调 */
unsigned int request_pending:1; /**< 有待处理请求 */
enum rpm_request request; /**< 待处理请求类型 */
unsigned int deferred_resume:1; /**< 挂起完成时立即恢复 */
enum rpm_status runtime_status; /**< 当前运行时PM状态 */
unsigned int runtime_auto:1; /**< 用户空间允许运行时PM */
unsigned int no_callbacks:1; /**< 设备不使用运行时PM回调 */
unsigned int irq_safe:1; /**< 回调在关中断+持锁下执行 */
unsigned int use_autosuspend:1; /**< 启用自动延迟挂起 */
unsigned int timer_autosuspends:1;/**< 定时器到期执行 autosuspend */
int autosuspend_delay; /**< autosuspend 延迟 (ms) */
unsigned long last_busy; /**< 最后一次活跃时间 (jiffies) */
};
设计精髓剖析:
-
disable_depth初始值为 1,即默认禁用运行时 PM。驱动必须在probe()中调用pm_runtime_enable()才能激活。这避免了未初始化的设备被意外挂起。 -
usage_count和child_count组成双重门控:仅当设备和其所有子设备都空闲时才允许挂起。ignore_children可绕过子设备检查。 -
deferred_resume是快速 I/O 路径上的优化:当设备正在挂起但新请求到达时,不等待挂起完成,而是标记“挂起完成后立即恢复”,减少延迟。 -
runtime_auto通过/sys/devices/.../power/control暴露给用户空间,允许动态控制运行时 PM 策略。
第4章 辅助函数集
4.1 核心操作函数
/** * @brief 同步恢复设备 * @param dev 目标设备 * @return 0 成功,1 设备本就 active,负值错误码 * * 如果设备已挂起,执行子系统级 resume 回调将其恢复到 active。 * 如果 disable_depth > 0 但之前状态为 active,返回 1。 */ int pm_runtime_resume(struct device *dev); /** * @brief 恢复设备并增加引用计数 * @return 0 成功,或 pm_runtime_resume() 的错误码 * * 成功时设备的引用计数+1,保证设备保持 active 状态。 * 推荐在需要检查返回值的场景下使用此函数而非 pm_runtime_get_sync(), * 因为它不会在错误时泄漏引用计数。 */ int pm_runtime_resume_and_get(struct device *dev); /** * @brief 同步挂起设备 * @return 0 成功,1 已挂起,-EAGAIN/-EBUSY 可重试,-EACCES 禁用中 */ int pm_runtime_suspend(struct device *dev); /** * @brief 自动延迟挂起 * * 与 pm_runtime_suspend() 相同,但会考虑 autosuspend 延迟。 * 如果延迟未过期,会安排定时器,届时再执行挂起。 */ int pm_runtime_autosuspend(struct device *dev);
4.2 引用计数管理
引用计数是 Runtime PM 的核心控制变量:
| 函数 | 效果 | 典型调用位置 |
|---|---|---|
pm_runtime_get() |
usage++,异步请求恢复 |
I/O 请求开始时 |
pm_runtime_get_sync() |
usage++,同步等待恢复完成 |
I/O 需要确定设备就绪时 |
pm_runtime_put() |
usage--,若归零异步请求空闲检查 |
I/O 完成时 |
pm_runtime_put_sync() |
usage--,若归零同步执行空闲检查 |
需要等待挂起完成时 |
pm_runtime_put_autosuspend() |
usage--,若归零请求 autosuspend |
autosuspend 模式下的 I/O 完成 |
pm_runtime_get_noresume() |
仅 usage++,不触发恢复 |
阻止设备挂起但不访问硬件 |
pm_runtime_put_noidle() |
仅 usage--,不触发挂起 |
对称地释放 get_noresume |
/** * @brief 驱动的典型引用计数使用模式 * * I/O 开始时: * pm_runtime_get(&dev); // 异步请求恢复,增加引用计数 * 或 * ret = pm_runtime_get_sync(&dev); // 同步等待恢复完成 * if (ret < 0) goto error; // 恢复失败则放弃 I/O * * I/O 完成时: * pm_runtime_mark_last_busy(&dev); // 更新最后活跃时间 * pm_runtime_put_autosuspend(&dev); // 释放引用,触发 autosuspend * * @note 错误路径:如果 pm_runtime_get_sync() 返回错误, * 调用者必须调用 pm_runtime_put_noidle() 或 pm_runtime_put() * 来释放已增加的引用计数。 */
4.3 中断上下文安全性
以下函数始终可从中断上下文安全调用:pm_request_idle(), pm_request_autosuspend(), pm_schedule_suspend(), pm_request_resume(), pm_runtime_get(), pm_runtime_put(), pm_runtime_put_autosuspend(), pm_runtime_enable(), pm_runtime_mark_last_busy()。
以下函数只有在 pm_runtime_irq_safe() 设置后才能在中断上下文使用:pm_runtime_idle(), pm_runtime_suspend(), pm_runtime_autosuspend(), pm_runtime_resume(), pm_runtime_get_sync(), pm_runtime_put_sync(), pm_runtime_put_sync_suspend(), pm_runtime_put_sync_autosuspend()。
第5章 初始化、探测与移除
5.1 设备生命周期中的 Runtime PM
设备注册 --> 初始状态: status='suspended', disable_depth=1 | v probe(): pm_runtime_set_active(dev); // 标记为 active (如果需要) pm_runtime_enable(dev); // disable_depth--,启用 Runtime PM 或 pm_runtime_resume(dev); // 若初始状态已正确反映硬件,直接唤醒 | v normal operation: pm_runtime_get() / pm_runtime_put() / autosuspend 循环 | v remove(): pm_runtime_disable(dev); // 禁用 Runtime PM pm_runtime_dont_use_autosuspend(dev); // 撤销 probe() 中的 PM 配置
/** * @brief probe 阶段的关键注意事项 * * 1. 初始 status='suspended', disable_depth=1。 * 2. 如果设备实际上是 active 的,必须先 pm_runtime_set_active(), * 再 pm_runtime_enable()。 * 3. 调用 pm_runtime_set_active() 会影响父设备的 child_count, * 可能导致父设备无法挂起。因此应尽快随后调用 pm_runtime_enable()。 * 4. 如果 probe 中设备可能被子系统回调,应使用 pm_runtime_get_sync() * 配合 pm_runtime_put() 确保 probe 期间设备保持唤醒。 */ /** * @brief remove 阶段的关键注意事项 * * 1. remove() 中应撤销 probe() 的 PM 配置。 * 2. 驱动核心在 bus notifier 之前调用 pm_runtime_get_sync(), * 之后调用 pm_runtime_put_sync(),保证通知期间设备处于 active。 * 3. 如果 remove() 中需要挂起设备,可直接调用 pm_runtime_suspend()。 */
第6章 Runtime PM 与系统睡眠的交互
/** * @brief 系统睡眠期间 Runtime PM 的协调机制 * * 系统 suspend 时: * - .prepare() 之前: pm_runtime_get_noresume() 阻止设备挂起 * - .suspend() 之前: pm_runtime_barrier() 等待所有运行时操作完成 * - .suspend_late() 之前: __pm_runtime_disable(false) 临时禁用 * * 系统 resume 时: * - .resume_early() 之后: pm_runtime_enable() 重新启用 * - .complete() 之后: pm_runtime_put() 释放系统睡眠期间持有的引用 */ /** * @brief 系统唤醒后的状态同步 * * 如果设备在系统睡眠前已挂起,系统唤醒后应执行: * pm_runtime_disable(dev); * pm_runtime_set_active(dev); * pm_runtime_enable(dev); * 这套调用序列确保运行时PM状态反映实际的硬件状态。 */ /** * @brief 保持运行时挂起的优化 * * 如果 .prepare() 返回正数,且设备的所有子孙也满足条件, * PM核心将跳过系统 suspend/resume 回调,仅调用 .complete()。 * 此优化避免了不必要的硬件状态切换,前提是设备状态未改变。 * 此优化不适用于休眠(hibernation)。 */
场景调试:系统唤醒后设备异常,通常是因为驱动程序未正确同步状态。检查 ->resume() 中是否调用了上述三段式同步代码。如果设备支持“保持挂起”优化但行为异常,检查 ->prepare() 是否错误地返回了正数。
第7章 Autosuspend 机制
7.1 设计原理
Autosuspend 的核心思想是延迟挂起:设备空闲后不立即挂起,而是等待一段可配置的时间,避免短暂的 I/O 间歇导致设备频繁地挂起和恢复。
/**
* @brief autosuspend 的工作流程
*
* 1. 驱动在 I/O 完成时调用 pm_runtime_mark_last_busy() 更新时间戳
* 2. 驱动调用 pm_runtime_put_autosuspend() 释放引用计数
* 3. 当引用计数归零时,PM核心检查 autosuspend_delay 是否已过期
* 4. 若已过期,立即执行挂起;否则安排定时器届时触发
*
* 用户空间接口:
* /sys/devices/.../power/autosuspend_delay_ms (延迟毫秒数)
* /sys/devices/.../power/control ("auto" 或 "on")
*/
7.2 并发控制伪代码
/**
* @brief autosuspend 下的 I/O 并发控制示例
*
* 核心挑战: foo_runtime_suspend() 可能在 foo_read_or_write()
* 请求到达的同时被调用。驱动必须用私有锁保护关键区。
*/
static int foo_runtime_suspend(struct device *dev) {
struct foo_priv *foo = dev_get_drvdata(dev);
int ret = 0;
spin_lock(&foo->lock);
if (foo->pending_reqs > 0) {
ret = -EBUSY; // 有新请求到达,拒绝挂起
} else {
foo->suspended = true; // 标记为已挂起,执行硬件挂起操作
}
spin_unlock(&foo->lock);
return ret;
}
static int foo_runtime_resume(struct device *dev) {
struct foo_priv *foo = dev_get_drvdata(dev);
spin_lock(&foo->lock);
foo->suspended = false; // 恢复硬件
pm_runtime_mark_last_busy(dev);
if (foo->pending_reqs > 0)
foo_process_requests(foo); // 处理积压的 I/O
spin_unlock(&foo->lock);
return 0;
}
关键点:
-
runtime_suspend()返回-EBUSY时,PM核心会自动重新安排 autosuspend。 -
必须在持有私有锁的情况下检查
pending_reqs,避免新请求在检查后、挂起前到达。 -
pm_runtime_autosuspend_expiration()可在回调中检查延迟是否已过期,返回 0 表示可立即挂起。
第8章 通用子系统回调
/** * @brief 通用回调集 (driver/base/power/generic_ops.c) * * 子系统若未提供自己的回调,PM核心默认使用这些函数。 * 驱动若使用相同的函数作为系统睡眠和运行时PM回调, * 可通过 UNIVERSAL_DEV_PM_OPS 宏简化定义。 */ int pm_generic_runtime_suspend(struct device *dev); int pm_generic_runtime_resume(struct device *dev); int pm_generic_suspend(struct device *dev); int pm_generic_resume(struct device *dev); // ... 等
第9章 无回调设备
某些设备(如 USB 接口)只是其父设备的逻辑子设备,无法单独进行电源管理。其驱动可调用 pm_runtime_no_callbacks() 告知 PM 核心不执行运行时回调。
/** * @brief 无回调设备的行为 * * 设置 power.no_callbacks 后: * - PM核心假设挂起/恢复始终成功 * - PM核心假设空闲设备应挂起 * - 父设备驱动负责在自身电源状态变化时通知子设备 * - 运行时PM sysfs 属性不会被创建 */
设计精髓:此特性允许子系统用标记位替代空回调函数,实现“仅父设备驱动负责”的级联电源管理逻辑。
第10章 Linux PCI 电源管理硬件与平台支持
10.1 两种电源管理方式
PCI 设备可通过两种途径进入低功耗状态:
/** * @brief 原生 PCI PM 与平台固件 PM 的区别 * * 1. 原生 PCI PM (Native PCI PM): * - 基于 PCI 总线电源管理接口规范 (PCI PM Spec) * - 通过写入标准配置寄存器切换设备电源状态 * - 可生成 PME (Power Management Event) 唤醒信号 * * 2. 平台固件 PM (Platform-based PM): * - 通过 ACPI BIOS 提供的控制方法 (_PSx, _DSW, _ON/_OFF 等) * - 固件负责协调电源资源和设备状态 * - 通常与原生 PCI PM 协同使用 * * @note 大多数情况下两种机制需同时使用:平台固件负责传递 PME, * 原生 PCI PM 负责配置设备本身。 */
10.2 PCI 电源状态 (D-States)
PCI PM Spec 定义了四种设备电源状态:
| 状态 | 功耗 | 延迟 | 描述 |
|---|---|---|---|
| D0 | 满功耗 | 最低 | 设备完全运行 |
| D1 | 较低 | 中等 | 可选状态,比 D2 功耗高但延迟低 |
| D2 | 更低 | 较长 | 可选状态,比 D1 功耗低但延迟高 |
| D3hot | 极低 | 最长 | 软件可访问的 D3,配置寄存器仍可访问 |
| D3cold | 零功耗 | 需完全复位 | Vcc 电源移除,不可编程进入 |
/** * @brief 状态转换规则 * * 允许的下行转换: D0→D1, D0→D2, D0→D3, D1→D2, D1→D3, D2→D3 * 允许的上行转换: D1→D0, D2→D0, D3→D0 * * @note D3cold→D0 需恢复供电,相当于冷启动。 * @note PCIe 设备必须支持 PCI PM Spec,传统 PCI 设备可选。 */
10.3 ACPI 设备电源管理
ACPI 对 PCI 电源管理的补充:
/** * @brief ACPI 电源管理机制 * * - _PSx (x=0..3): 将设备置入 Dx 状态的控制方法 * - _DSW / _PSW: 配置设备唤醒能力的控制方法 * - _SxD: 返回设备在系统 Sx 睡眠状态下的最高电源状态 * - _SxW: 返回设备能从系统 Sx 状态唤醒的最低电源状态 * - _PRW: 返回设备唤醒所需的电源资源 * - _ON / _OFF: 控制电源资源开关的方法 * * @note ACPI 的 D-states 与 PCI 原生 D-states 大致对应, * 但不区分 D3hot 和 D3cold。 */
10.4 唤醒信号机制
/** * @brief PME 传递路径 * * 传统 PCI 设备: * 设备 PME → 平台固件 GPE → SCI 中断 → ACPI 驱动 → PCI 子系统 * * PCI Express 设备: * 设备 PME 消息 → 根端口 (Root Port) → 中断 * (原生 PCIe PME 机制) * * @note PCIe 原生 PME 是带内消息,需通过 PCIe 层次传递。 * 根端口收到 PME 后记录 Requester ID 供中断处理程序识别源设备。 * @note ACPI 系统上,内核需请求 BIOS 释放根端口配置寄存器控制权 * 才能使用原生 PCIe PME。如果 BIOS 拒绝,只能用 GPE 方式。 */
场景调试:如果设备无法唤醒系统,首先检查 /proc/acpi/wakeup 确认设备对应的 GPE 已启用。对于 PCIe 设备,还需确认 lspci -vv 中 Root Port 的 PME 状态。
第11章 PCI 子系统与设备电源管理
11.1 PCI 子系统的回调层
PCI 子系统在 PM 核心和 PCI 设备驱动之间提供了中间层回调:
/**
* @brief pci_dev_pm_ops 结构
*
* PCI 总线类型的 dev_pm_ops 对象,包含以下关键回调:
* - .suspend / .resume 系统睡眠/唤醒
* - .freeze / .thaw 休眠冻结/解冻
* - .poweroff / .restore 休眠断电/恢复
* - .runtime_suspend / .runtime_resume / .runtime_idle 运行时PM
*
* @note 这些回调负责 PCI 标准配置寄存器的保存/恢复,
* 以及设备唤醒信号的配置,使驱动无需关心这些细节。
*/
const struct dev_pm_ops pci_dev_pm_ops = {
.runtime_suspend = pci_pm_runtime_suspend,
.runtime_resume = pci_pm_runtime_resume,
.runtime_idle = pci_pm_runtime_idle,
// ... 其他回调
};
11.2 设备初始化
/** * @brief 关键初始化函数 * * pci_pm_init(): * - 检查设备是否支持原生 PCI PM * - 存储 PM 能力结构偏移到 pm_cap 字段 * - 检查设备支持的 D-states * - 检查设备可从哪些状态生成 PME * - 更新 pci_dev 和内嵌 struct device 的电源管理字段 * * platform_pci_wakeup_init(): * - 检查设备是否可通过平台固件唤醒 * - 更新 struct device 的唤醒字段 * - 使用固件方法禁止设备初始唤醒 * * @note 对于无驱动的设备,PM 功能仅限于系统睡眠转换的基本操作。 */
11.3 运行时电源管理
PCI 子系统的运行时 PM 实现是为其他总线类型的范例:
/** * @brief 运行时挂起流程 * * 1. 驱动调用 pm_runtime_suspend() 或 pm_schedule_suspend() * 2. PM 核心调用 pci_pm_runtime_suspend() * 3. 执行驱动的 ->runtime_suspend() 回调 * 4. 保存设备标准配置寄存器 * 5. 配置设备唤醒信号 * 6. 将设备置入目标低功耗状态 * * @note 驱动的 runtime_suspend() 不负责准备唤醒或进入低功耗, * 这些由 PCI 子系统完成。 * @note 选择的低功耗状态是设备可从中唤醒的最深状态。 */ /** * @brief 运行时恢复流程 * * 1. 驱动调用 pm_runtime_resume() 或 pm_request_resume() * 2. PM 核心调用 pci_pm_runtime_resume() * 3. 将设备恢复到 D0 * 4. 禁止该状态下的唤醒 * 5. 恢复标准配置寄存器 * 6. 执行驱动的 ->runtime_resume() 回调 * * @note 恢复可能由两个原因触发: * 1. 驱动主动请求(有新数据需处理) * 2. 远程唤醒(设备自身发出唤醒信号) */
11.4 系统睡眠转换
系统睡眠期间各阶段的回调执行顺序:
/** * @brief 系统挂起阶段 * * prepare: pci_pm_prepare() - pm_runtime_resume() + 驱动 prepare() * suspend: pci_pm_suspend() - 异步执行,驱动 suspend() 回调 * suspend_noirq: pci_pm_suspend_noirq() - 关中断后:保存寄存器、配置唤醒、进入低功耗 * * @note 系统恢复阶段对应: resume_noirq, resume, complete * @note suspend/resume 阶段是异步的,不相互依赖的设备可并行处理 */
11.5 驱动标志位
/** * @brief 影响 PCI 电源管理行为的驱动标志 * * DPM_FLAG_NO_DIRECT_COMPLETE: * - 阻止 PM 核心跳过设备的 suspend/resume 回调 * - 影响所有祖先设备,仅在绝对必要时使用 * * DPM_FLAG_SMART_PREPARE: * - 仅当驱动 prepare() 返回正数时允许 direct-complete * - 支持动态选择是否参与优化 * * DPM_FLAG_SMART_SUSPEND: * - 允许设备在系统挂起期间保持运行时挂起状态 * - pci_pm_suspend() 等函数避免不必要的运行时恢复 * * DPM_FLAG_MAY_SKIP_RESUME: * - 允许跳过 "noirq" 和 "early" 恢复回调 * - 需与电源管理核心协调 * * @note 标志通过 dev_pm_set_driver_flags() 在 probe 时设置 */
第12章 PCI 设备驱动与电源管理
12.1 驱动回调分类
PCI 驱动需要实现的回调可分为三大类,对应不同的电源管理场景:
/** * @brief 回调函数分类 * * 系统睡眠回调: * suspend(), suspend_noirq(), resume_noirq(), resume(), complete() * * 休眠回调: * freeze(), freeze_noirq(), thaw_noirq(), thaw() * poweroff(), poweroff_noirq(), restore_noirq(), restore() * * 运行时 PM 回调: * runtime_suspend(), runtime_resume(), runtime_idle() * * @note 许多回调可以使用相同的处理函数,通过 SIMPLE_DEV_PM_OPS、 * UNIVERSAL_DEV_PM_OPS 等宏简化声明。 */
12.2 运行时 PM 的驱动实现
PCI 驱动实现运行时 PM 的关键步骤:
/** * @brief PCI 驱动运行时 PM 实现要点 * * 1. probe() 中递减 usage_count: * - PCI 核心在 probe 前自动递增 usage_count (local_pci_probe) * - 驱动必须在 probe() 中调用 pm_runtime_put() 或变体函数 * 以允许设备进入运行时挂起 * - 推荐使用 pm_runtime_put_noidle() 或 pm_runtime_put_autosuspend() * * 2. remove() 中递增 usage_count: * - 调用 pm_runtime_get_noresume() 平衡 probe 中的 put * * 3. runtime_suspend() 回调: * - 负责冻结合理设备和准备挂起 * - 不负责 PCI 特定的挂起操作(由 PCI 子系统处理) * * 4. runtime_resume() 回调: * - 负责恢复设备功能 * - 在 PCI 子系统将设备置入 D0 后执行 * * @warning runtime_suspend() 可能在 probe 中递减计数后立即执行。 * 驱动必须准备好处理这种竞争条件。 */
场景调试:如果设备一直不进入 Runtime Suspend,检查 /sys/devices/.../power/runtime_status 确认状态,并查看 usage_count 是否始终非零。最常见的原因是 probe 中没有调用 pm_runtime_put()。
第13章 PCI 电源管理设计原则
-
分层责任:硬件特性由 PCI 子系统层统一处理,驱动只需关心设备特定逻辑。
-
统一接口:通过
struct dev_pm_ops提供一致的 PM 回调,系统睡眠和运行时 PM 共用框架。 -
主动管理:驱动通过引用计数和 autosuspend 主动控制挂起时机,而非被动等待。
-
平台适配:通过 ACPI 和原生 PCIe PME 的双轨机制,适应不同硬件平台的唤醒需求。
第14章 Linux 电源供应类
14.1 设计目标与核心思想
Power supply class 的设计目标是为用户空间提供统一的、可预测的电源属性接口,无论底层硬件是智能电池、充电管理芯片还是简单的 GPIO 检测电路。
/** * @brief Power supply class 的核心设计原则 * * 1. 预定义属性集: 提供一组适用于几乎所有电源供应器的核心属性 * 2. 单位统一: 所有电压、电流、电量、能量、时间、温度分别以 * µV, µA, µAh, µWh, 秒, 十分之一摄氏度为单位 * 3. 可扩展: 驱动可以定义自己的专有属性 * 4. 双重接口: 属性通过 sysfs 和 uevent 两种方式暴露给用户空间 * 5. 集成 LED 框架: 自动为电池充电/满电状态和 AC/USB 在线状态提供 LED 反馈 * * @note 并非所有属性都必须由驱动提供。如果硬件不支持某个属性, * 可以直接跳过,用户空间会自行处理缺失的属性。 * * @see drivers/power/supply/ds2760_battery.c * @see drivers/power/supply/pda_power.c */
14.2 属性体系与命名规范
14.2.1 核心概念:Charge vs Energy vs Capacity
这是整个框架中最容易混淆的概念,内核文档专门进行了解释:
/** * @brief 三种"容量"表示法的严格区分 * * CHARGE_* 属性: * - 以 µAh (微安时) 为单位 * - 表示电池的电荷容量 * * ENERGY_* 属性: * - 以 µWh (微瓦时) 为单位 * - 表示电池的能量容量 * * CAPACITY 属性: * - 以百分比 (0-100) 为单位 * - 表示当前电量占满电量的比例 * * @warning 这三种属性不能混用。如果你提供了 CHARGE_FULL, * 就不要同时提供 ENERGY_FULL 来表示同一个概念。 * 驱动应根据硬件实际支持的量纲选择合适的属性。 */
14.2.2 后缀约定
| 后缀 | 含义 | 典型使用场景 |
|---|---|---|
_NOW |
瞬时/当前值 | VOLTAGE_NOW, CURRENT_NOW |
_AVG |
硬件平均后的值 | VOLTAGE_AVG (采样窗口平均) |
_FULL |
电池满电时的值 | CHARGE_FULL, ENERGY_FULL |
_EMPTY |
电池空电时的值 | CHARGE_EMPTY, ENERGY_EMPTY |
_FULL_DESIGN |
设计上的满电值 | 出厂标称容量 |
_EMPTY_DESIGN |
设计上的空电值 | 出厂标称最低容量 |
_MAX / _MIN |
最大/最小阈值 | 运行时可测量的极限值 |
_MAX_DESIGN / _MIN_DESIGN |
设计的最大/最小阈值 | 设计电压范围 |
14.2.3 关键属性详解
/** * @brief 状态与健康属性 * * STATUS (POWER_SUPPLY_STATUS_*): * - Charging: 正在充电 * - Discharging: 正在放电(为负载供电) * - Full: 电池已满 * - Not charging: 未充电(例如温度保护) * - Unknown: 状态未知 * * HEALTH (POWER_SUPPLY_HEALTH_*): * - Good: 正常 * - Overheat: 过热 * - Dead: 电池失效 * - Overvoltage: 过压 * - Cold: 过冷 * - Unknown: 未知 * * CHARGE_TYPE: * - None: 不在充电 * - Trickle: 涓流预充 * - Fast: 快速充电 * - N/A: 不适用(非电池或已充满) * - Unknown: 未知 */
/** * @brief 电压属性族 * * VOLTAGE_OCV: 开路电压 (Open Circuit Voltage) - 无负载时的电池电压 * VOLTAGE_MAX_DESIGN / VOLTAGE_MIN_DESIGN: 设计的满电/空电电压阈值 * VOLTAGE_MAX / VOLTAGE_MIN: 实际测量的电压阈值 * VOLTAGE_BOOT: 启动时测量的电压 * * @note 电压和电池容量之间没有直接的线性关系。 * 一些简单的电池使用电压粗略估算容量,但这是不准确的。 * 驱动可以使用这些属性向用户空间提供阈值信息。 */
/** * @brief 充电控制属性 * * CONSTANT_CHARGE_CURRENT: 编程设定的恒流充电电流 * CONSTANT_CHARGE_CURRENT_MAX: 电源支持的最大充电电流 * CONSTANT_CHARGE_VOLTAGE: 编程设定的恒压充电电压 * CONSTANT_CHARGE_VOLTAGE_MAX: 电源支持的最大充电电压 * * CHARGE_TERM_CURRENT: 充电终止电流(低于此值认为充电完成) * PRECHARGE_CURRENT: 预充电阶段最大电流 * * INPUT_CURRENT_LIMIT / INPUT_VOLTAGE_LIMIT / INPUT_POWER_LIMIT: * 输入限制,从充电源汲取的电流/电压/功率上限 */
/** * @brief 时间属性 * * TIME_TO_EMPTY: 电池继续为负载供电时,预计的剩余使用时间(秒) * TIME_TO_FULL: 电池充电到满电所需的预计时间(秒) * * @note 这些值是估算值,不保证精度。 */
/** * @brief 温度属性 * * TEMP: 电池温度(十分之一摄氏度) * TEMP_AMBIENT: 环境温度 * TEMP_MIN / TEMP_MAX: 电池可工作的温度范围 * TEMP_ALERT_MIN / TEMP_ALERT_MAX: 温度报警阈值 * TEMP_AMBIENT_ALERT_MIN / TEMP_AMBIENT_ALERT_MAX: 环境温度报警阈值 */
/** * @brief 其他属性 * * CAPACITY: 当前电池电量百分比 (0-100) * CAPACITY_ALERT_MIN / CAPACITY_ALERT_MAX: 容量报警阈值 * CAPACITY_LEVEL: 容量等级 (POWER_SUPPLY_CAPACITY_LEVEL_*): * - Critical, Low, Normal, High, Full * * AUTHENTIC: 电源是否认证 (1=认证, 0=非认证) * * CHARGE_COUNTER: 电荷计数器值 (µAh),可正可负, * 用于相对时间测量,无满/空概念 * * CALIBRATE: 电池或库仑计校准状态 */
14.3 电池与外部电源的交互
/** * @brief 电池通知机制 * * 电池作为用电设备 (supplicant),外部电源 (AC/USB) 作为供电设备。 * 外部电源在 provided_to 成员中列出它所供电的电池名称。 * 当外部电源状态变化时,调用 power_supply_changed() 会通过 * external_power_changed 回调通知所有连接的电池。 * * 典型使用场景: * [AC adapter] --provided_to--> [battery] * 当 AC 插入/拔出时,电池驱动自动感知并更新充电/放电状态。 * * @note 这种设计使得电池驱动无需主动轮询外部电源状态, * 而是依赖于事件的推送机制,提高了效率。 */
14.4 设备树电池特性
/** * @brief 设备树中的电池参数 * * 驱动应调用 power_supply_get_battery_info() 从设备树中获取电池特性。 * struct power_supply_battery_info 中的字段与 enum power_supply_property * 命名保持一致,便于 sysfs 属性和设备树节点之间的对应。 * * 设备树绑定定义在: * Documentation/devicetree/bindings/power/supply/battery.yaml * * 典型实现参考: * drivers/power/supply/bq27xxx_battery.c * * @note 命名一致性确保了从固件描述到用户空间接口的统一性。 */
14.5 常见问题与设计决策
Q: 我应该把电池颜色这样的特有属性加入标准属性吗?
A: 不建议。特有属性放在驱动自己的命名空间中。只有当某个属性被大量驱动使用,或者来自于公开的电池标准时,才应该加入核心属性集。属性集目前是可扩展的,如果需要且合理,可以提交 patch 添加。
Q: 如果硬件只能测量 charge_now/full/empty,但提供不了百分比,我应该自己在驱动里计算 CAPACITY 吗?
A: 不建议。电源供应类被设计用来导出硬件直接可测量的属性。通过数学推导或启发式算法推断出来的属性不是电池驱动的工作。事实上,apm_power 驱动使用了简单的启发式方法来近似估算剩余容量,但完整复杂的电池模型(需要浮点运算、卡尔曼滤波器等)不应该放在内核中。这项工作更适合在用户空间的 batteryd/libbattery 中进行。
/** * @brief 驱动开发者的两个关键原则 * * 1. 只导出硬件直接可测量的属性。 * 如果你无法找到合适的属性,欢迎向社区提交新属性的 patch。 * 当前属性集是基于已存在的驱动所提供的能力不断演进的。 * * 2. 使用标准单位: µV, µA, µAh, µWh, 秒, 十分之一摄氏度。 * 驱动负责将原始硬件值转换为这些标准单位。 * 这确保了不同设备和驱动的结果可以直接比较。 */
14.6 与 LED 框架的集成
/** * @brief LED 框架集成 * * 电源供应类自动与 LED 框架集成,提供以下常见的视觉反馈: * - 电池充电中 / 已充满状态 * - AC/USB 在线状态 * * 用户和/或机器默认值可以完全控制指示的具体细节(包括是否使用), * 这遵循 LED 框架的设计原则。 * * @note 这意味着驱动开发者通常不需要手动管理 LED, * 系统会根据电源供应类的状态自动更新关联的 LED。 */
第15章 PCI 电源管理聚焦
PCI 电源管理聚焦于设备如何进入和退出低功耗状态的硬件与软件协作机制,而电源供应类则聚焦于如何向用户空间报告电源状态的统一接口规范。二者共同构成了 Linux 设备电源管理从内核到用户空间、从 PCI 总线到电池芯片的完整视图,是驱动开发者在电源管理领域不可或缺的双重参考。
第16章 设备能量模型 (Energy Model)
16.1 概述与设计动机
/** * @brief Energy Model 框架的核心目标 * * EM 框架充当驱动(知道设备在各性能等级下的功耗)和内核子系统 * (希望利用这些信息进行能耗感知决策)之间的接口。 * * 功耗信息来源多样: * - 设备树 (devicetree) 中的 "opp-microwatt" 属性 * - 固件 (如 SCMI) * - 用户空间 * - 数学公式 (如 P = C * V^2 * f) * * EM 框架作为抽象层,统一内核中功耗表的格式,避免每个客户端子系统 * 都重新实现一轮功耗信息收集。 */
16.1.1 功耗值的单位
功耗值可以用微瓦 (micro-Watts) 或抽象标度表示。
-
微瓦:需要真实功耗时使用(如热管理 IPA),可以从物理单位推导实际能量消耗。
-
抽象标度:对于无法获取真实功耗的平台,使用相对效率值。此时子系统无法推导出真实的微焦耳能量,可能产生警告或错误。
/** * @warning 当使用抽象标度时,不允许从 EM 推导真实的能量值。 * 子系统应实现自动检测,检查已注册 EM 设备是否标度不一致。 */
16.1.2 体系结构图
+---------------+ +-----------------+ +---------------+ | Thermal (IPA) | | Scheduler (EAS) | | Other | +---------------+ +-----------------+ +---------------+ | | em_cpu_energy() | | | em_cpu_get() | +---------+ | +---------+ | | | v v v +---------------------+ | Energy Model | | Framework | +---------------------+ ^ ^ ^ | | | em_dev_register_perf_domain() +----------+ | +---------+ | | | +---------------+ +---------------+ +--------------+ | cpufreq-dt | | arm_scmi | | Other | +---------------+ +---------------+ +--------------+ ^ ^ ^ | | | +--------------+ +---------------+ +--------------+ | Device Tree | | Firmware | | ? | +--------------+ +---------------+ +--------------+
性能域 (performance domain):一组性能扩展步调一致的 CPU,通常与 CPUFreq 策略一一对应。同一性能域内 CPU 微架构相同,不同性能域可不同。
16.2 核心 API
16.2.1 配置选项
CONFIG_ENERGY_MODEL 必须启用。
16.2.2 注册性能域
EM 支持四种注册方式:
16.2.2.1 高级注册 (Advanced)
驱动提供更精确的功耗模型(可包含静态功耗),通过回调返回每个性能状态的 <frequency, power> 元组。
/** * @brief 注册性能域 (高级方式) * @param dev 设备指针 * @param nr_states 性能状态数量 * @param cb 回调函数结构体,提供 get_power() * @param cpus 仅对 CPU 设备有效:性能域包含的 CPU 掩码;其他设备传 NULL * @param microwatts 功耗值是否为微瓦:true 为微瓦,false 为抽象标度 * @return 0 成功,负值错误 */ int em_dev_register_perf_domain(struct device *dev, unsigned int nr_states, struct em_data_callback *cb, cpumask_t *cpus, bool microwatts);
16.2.2.2 通过 DT 注册
使用 OPP 框架和 DT operating-points-v2 中的 opp-microwatt 属性,直接填入总功耗(静态+动态)。
16.2.2.3 人工注册 (Artificial)
驱动提供 .get_power() 和 .get_cost() 回调,允许使用抽象效率值构建模型。设置 microwatts 为 0,EM_PERF_DOMAIN_ARTIFICIAL 标志被置位。
16.2.2.4 简单注册 (Simple)
通过 cpufreq_register_em_with_opp() 注册,使用数学公式 Power = C * V^2 * f。适用于静态功耗不明显的设备。
16.2.3 访问性能域
-
em_cpu_get(cpu):通过 CPU ID 获取性能域。
-
em_pd_get(dev):通过设备指针获取性能域。
-
em_cpu_energy(…):估计性能域消耗的能量(假设使用 schedutil CPUFreq 调度器)。
/** * @brief 获取 CPU 的能耗模型 * @param cpu CPU ID * @return 指向 em_perf_domain 的指针,失败为 NULL */ struct em_perf_domain *em_cpu_get(int cpu); /** * @brief 估计 CPU 性能域消耗的能量 * @param pd 性能域指针 * @param max_util 最大计算利用率 * @param sum_util 总计算利用率 * @return 预估能量 (μJ) */ unsigned long em_cpu_energy(struct em_perf_domain *pd, unsigned long max_util, unsigned long sum_util);
16.3 示例驱动
以下示例展示一个虚构的 CPUFreq 驱动如何注册 EM:
/**
* @file foo_cpufreq.c
* @brief 虚构 CPUFreq 驱动注册 EM 的示例
*/
/**
* @brief 估算功耗回调函数
* @param dev 设备
* @param mW 输出参数:功耗 (mW)
* @param KHz 输入输出:频率 (KHz),会取整到实际支持的频率
* @return 0 成功,负值错误
*/
static int est_power(struct device *dev, unsigned long *mW,
unsigned long *KHz)
{
long freq, power;
/* 使用 foo 协议取整频率 */
freq = foo_get_freq_ceil(dev, *KHz);
if (freq < 0)
return freq;
/* 估算在该频率下的功耗 */
power = foo_estimate_power(dev, freq);
if (power < 0)
return power;
/* 返回估算值 */
*mW = power;
*KHz = freq;
return 0;
}
/**
* @brief 注册性能域到 EM 框架
* @param policy CPUFreq 策略
*/
static void foo_cpufreq_register_em(struct cpufreq_policy *policy)
{
struct em_data_callback em_cb = EM_DATA_CB(est_power);
struct device *cpu_dev;
int nr_opp;
cpu_dev = get_cpu_device(cpumask_first(policy->cpus));
nr_opp = foo_get_nr_opp(policy);
em_dev_register_perf_domain(cpu_dev, nr_opp, &em_cb, policy->cpus, true);
}
static struct cpufreq_driver foo_cpufreq_driver = {
.register_em = foo_cpufreq_register_em,
};
16.4 设计模式分析
| 模式 | 应用 | 说明 |
|---|---|---|
| 策略模式 | em_data_callback 中的 .get_power() 和 .get_cost() |
驱动可提供不同的功耗估计算法 |
| 外观模式 | em_dev_register_perf_domain() |
将复杂的注册过程封装为简单接口 |
| 代理模式 | EM 框架本身 | 充当驱动和子系统的中间层,隔离硬件细节 |
16.5 场景调试
-
EAS 不生效:检查
CONFIG_ENERGY_MODEL是否开启,cpufreq 驱动是否实现了.register_em,opp-microwatt属性是否填写。 -
调度器报错:可能由于不同性能域使用了不一致的标度(微瓦 vs 抽象值)。检查所有 EM 设备是否使用同一标度。
-
功耗数据不准确:使用抽象标度或简单的
C * V^2 * f模型可能无法反映静态漏电流,影响 IPA 热管理精度。建议在可能的情况下使用实测微瓦值。
16.6 场景总结
能量模型框架通过标准化功耗表,解耦了功耗数据提供者与数据消费者。四种注册方式灵活覆盖了从精确测量到简单公式的各种需求。掌握 EM 的使用,是在现代 ARM 平台上实现高效能调度的必备知识。
第17章 调试休眠与挂起
17.1 测试冬眠
17.1.1 基本验证
冬眠最基本的验证是使用 reboot 模式:
# echo reboot > /sys/power/disk # echo disk > /sys/power/state
系统会创建冬眠镜像、重启、恢复,然后返回到命令提示符。如果成功,冬眠基本正常。
/** * @brief 冬眠测试的重要注意事项 * * 1. 至少重复测试两到三次才能获得可信结果。 * 有些问题只在第二次挂起/恢复时才会出现。 * * 2. "reboot" 和 "shutdown" 模式下,PM 核心会跳过某些平台相关的回调。 * 在 ACPI 系统上,这些回调可能是冬眠成功的关键。 * * 3. 如果 "reboot" 模式失败,应首先尝试推荐的默认 "platform" 模式: * # echo platform > /sys/power/disk * # echo disk > /sys/power/state * * 4. 对于有 BIOS 问题的系统,"shutdown" 模式可能有效: * # echo shutdown > /sys/power/disk * # echo disk > /sys/power/state * (需要手动按电源按钮恢复) */
场景调试:如果一台机器在 reboot 模式下第二次冬眠测试时失败,而第一次成功,通常是因为某个驱动在恢复时没有完全重建状态。应重点检查网络驱动和存储控制器驱动。
17.1.2 分层测试模式
内核编译时启用 CONFIG_PM_DEBUG 后,可以通过 /sys/power/pm_test 使用五种测试模式,按照“侵入性”递增排列:
| 测试模式 | 包含的操作 | 用于隔离的问题 |
|---|---|---|
| freezer | 仅测试冻结进程 | 进程冻结失败 |
| devices | 冻结进程 + 挂起设备 | 设备挂起/恢复失败 |
| platform | + 平台全局控制方法¹ | ACPI/平台固件问题 |
| processors | + 禁用非启动 CPU | SMP CPU 禁用/启用失败 |
| core | + 挂起平台/系统设备 | 最深层的硬件问题 |
¹ 平台全局控制方法仅在 ACPI 系统上可用,且仅在冬眠模式设为 "platform" 时测试。
/** * @brief 分层测试的使用方法 * * 使用方法: * # echo devices > /sys/power/pm_test * # echo platform > /sys/power/disk * # echo disk > /sys/power/state * * 内核会尝试冻结进程、挂起设备、等待若干秒(默认5秒, * 可通过 suspend.pm_test_delay 模块参数配置),然后恢复设备和进程。 * * 写入 "none" 可切换回正常操作模式。 * 读取 /sys/power/pm_test 会显示所有可用测试,当前级别用方括号标记。 */
故障定位规则:如果前一级测试失败,后续所有级别都会失败。应从最低侵入性开始,逐级测试:
freezer → devices → platform → processors → core
17.1.3 分析各层测试失败
/** * @brief 各层失败的原因分析 * * freezer 失败: * - 有进程无法冻结,通过 dmesg 可识别出错的进程 * - 通常是进程冻结子系统的问题 * * devices 失败: * - 有驱动无法挂起或恢复其设备 * - 恢复失败时系统可能挂起或变得不稳定 * - 使用二进制搜索法定位: * 1) 如果测试失败,卸载当前加载的一半驱动,重新测试 * 2) 如果测试成功,重新加载最近卸载的一半驱动 * 3) 重复直到找到问题驱动 * - 如果卸载所有模块后仍然失败,检查编译进内核的驱动 * - 可尝试特殊内核命令行选项: "noapic", "noacpi", "acpi=off" * * platform 失败: * - 平台固件(ACPI)处理有问题 * - "platform" 模式可能无法工作,可尝试 "shutdown" 模式 * * processors 失败: * - 非启动 CPU 的禁用/启用失败(仅 SMP 系统) * - 可通过 /sys/devices/system/cpu/cpu*/online 手动测试 * * core 失败: * - 平台/系统设备挂起失败(在单 CPU 上关中断执行) * - 很可能是硬件相关的严重问题 */
场景调试:如果在 devices 测试中失败,但无法找到具体的驱动,可以启动时使用 init=/bin/bash 进入最小配置,然后手动加载模块进行测试,逐步缩小范围。
17.2 使用 test_resume 选项
# echo test_resume > /sys/power/disk # echo disk > /sys/power/state
此模式会创建冬眠镜像后立即触发恢复,完全绕过平台固件。
/** * @brief test_resume 的用途 * * 1. 区分恢复失败是否由平台固件交互引起。 * 如果此测试工作正常,但实际冬眠恢复失败或不可靠, * 责可能是平台固件。 * * 2. 在支持不同内核恢复的架构上,可用于检查恢复失败 * 是否与恢复内核和镜像内核之间的差异有关。 */
17.3 高级调试
如果以上方法都无法定位问题:
-
串口调试:使用
no_console_suspend参数启动内核,通过串口控制台记录日志 -
FireWire 调试:使用 firescope 工具
-
PM_TRACE:x86 架构可用
Documentation/power/s2ram.rst中的机制
17.5 测试挂起到 RAM
挂起到 RAM 的测试模式定义与冬眠相同。/sys/power/pm_test 同样可用,其中 core 模式测试除实际调用平台固件进入睡眠状态之外的所有操作。
/** * @brief 挂起到 RAM 统计信息 (debugfs) * * # cat /sys/kernel/debug/suspend_stats * success: 20 # 成功次数 * fail: 5 # 失败次数 * failed_freeze: 0 # 冻结失败次数 * failed_prepare: 0 # 准备失败次数 * failed_suspend: 5 # 挂起失败次数 * failed_suspend_noirq: 0 # noirq 阶段失败次数 * failed_resume: 0 # 恢复失败次数 * failed_resume_noirq: 0 # noirq 恢复失败次数 * failures: * last_failed_dev: alarm # 最后失败设备 * adc * last_failed_errno: -16 # 最后失败错误码 * -16 * last_failed_step: suspend # 最后失败阶段 * suspend * * @note 统计信息只保留最后 2 次失败的详细信息。 */
场景调试:如果 suspend_stats 显示 last_failed_errno: -16 (EBUSY),且 last_failed_step: suspend,通常表示某个设备在挂起时返回了 -EBUSY。检查该设备的驱动 suspend() 回调是否正确处理了设备繁忙的状态。
17.6 调试策略总结
| 步骤 | 操作 | 目的 |
|---|---|---|
| 1 | 使用 reboot 模式测试基本冬眠 |
快速验证核心功能 |
| 2 | 如果失败,切换到 platform 模式 |
测试平台固件协作 |
| 3 | 如果仍失败,使用 freezer 测试 |
隔离进程冻结问题 |
| 4 | 逐级增加 pm_test 级别 |
定位失败的具体层级 |
| 5 | 在目标层级上使用二进制搜索法 | 定位具体的故障驱动 |
| 6 | 使用 test_resume |
区分固件与内核问题 |
| 7 | 使用 init=/bin/bash 最小环境 |
消除用户空间干扰 |
17.7 常见案例:USB 设备导致挂起失败
/** * @brief USB 设备挂起的典型问题定位 * * 现象: * # cat /sys/kernel/debug/suspend_stats * last_failed_dev: usb1 * last_failed_step: suspend * * 分析: * USB 集线器或设备在挂起时返回了错误。 * 最常见的原因是设备不支持选择性挂起(selective suspend) * 或 USB 控制器的 runtime PM 配置不正确。 * * 解决方案: * 1. 在挂起前手动卸载 USB 设备驱动: * # modprobe -r uhci_hcd ehci_hcd xhci_hcd * 2. 或者在挂起前手动将设备进入自动挂起: * # echo auto > /sys/bus/usb/devices/1-1/power/control * 3. 检查 USB 控制器是否启用了 runtime PM: * # cat /sys/bus/usb/devices/usb1/power/control * 如果显示 "on",可能需要在驱动中实现 runtime PM 支持。 */
第18章 功率封顶框架
18.1 术语与设计理念
/** * @brief 功率封顶框架的核心概念 * * - 控制类型 (control type): 对应不同的功率封顶方法。 * 例如 intel-rapl 代表 Intel RAPL 技术,idle-injection 代表空闲注入。 * * - 功率区域 (power zone): 系统中可独立控制和监控的部分。 * 每个区域包含功耗监控属性和约束控制属性。 * 功率区域可以层级化组织,父区域包含多个子区域, * 反映系统的功率控制拓扑。 * * - 约束 (constraint): 对某个功率区域施加的功耗限制, * 包括功率上限和生效时间窗口。 * * 层级化设计允许: * - 通过父区域对整个子系统统一施加功率上限 * - 通过子区域对特定部分进行更精细的控制 */
18.2 sysfs 接口树解析
以 Intel RAPL 为例的完整 sysfs 树结构:
/sys/devices/virtual/powercap └──intel-rapl # 控制类型 ├──intel-rapl:0 # 功率区域 (CPU Package 0) │ ├──constraint_0_name # 约束0名称 (如 "long_term") │ ├──constraint_0_power_limit_uw # 约束0功率上限 (μW) │ ├──constraint_0_time_window_us # 约束0时间窗口 (μs) │ ├──constraint_1_name # 约束1名称 (如 "short_term") │ ├──constraint_1_power_limit_uw │ ├──constraint_1_time_window_us │ ├──energy_uj # 能量计数器 (μJ),可写0重置 │ ├──max_energy_range_uj # 能量计数器范围 (μJ) │ ├──max_power_range_uw # 功率值范围 (μW) │ ├──name # 区域名称 (如 "package-0") │ ├──enabled # 启用/禁用控制 │ ├──intel-rapl:0:0 # 子区域 (Core 部分) │ │ ├──constraint_0_name │ │ ├──constraint_0_power_limit_uw │ │ ├──constraint_0_time_window_us │ │ ├──energy_uj │ │ ├──max_energy_range_uj │ │ ├──name │ │ └──enabled │ └──intel-rapl:0:1 # 子区域 (Uncore 部分) │ └──... ├──intel-rapl:1 # 功率区域 (CPU Package 1) │ └──... └──...
/** * @brief 层级结构的含义 * * intel-rapl:0 代表 CPU Package 0 的整体功耗 * ├── intel-rapl:0:0 代表 Package 0 的 Core (计算核心) 部分 * └── intel-rapl:0:1 代表 Package 0 的 Uncore (非核心) 部分 * * 父区域的约束作用于整个 Package, * 子区域的约束仅作用于其各自部分。 * * @note Intel RAPL 不提供瞬时功率值 (power_uw), * 只提供能量计数器 (energy_uj)。 */
18.3 功率区域属性
18.3.1 监控属性
| 属性 | 权限 | 描述 |
|---|---|---|
energy_uj |
rw | 当前能量计数器 (μJ)。写入 "0" 重置。若不可重置则为只读 |
max_energy_range_uj |
ro | 能量计数器的范围 (μJ) |
power_uw |
ro | 当前瞬时功率 (μW) |
max_power_range_uw |
ro | 瞬时功率值范围 (μW) |
name |
ro | 功率区域名称 |
/** * @brief 监控属性的使用示例 * * 读取当前能量消耗: * cat /sys/class/power_cap/intel-rapl/intel-rapl:0/energy_uj * * 读取功率区域名称: * cat /sys/class/power_cap/intel-rapl/intel-rapl:0/name * → package-0 * * 注意: 某些域可能同时拥有功率范围和能量计数器范围, * 但只有一个是强制性的。 */
18.3.2 约束属性
Intel RAPL 支持多种约束类型:长期 (long term)、短期 (short term) 和峰值 (peak) 功率。
| 属性 | 权限 | 描述 |
|---|---|---|
constraint_X_name |
ro | 约束的名称(可选) |
constraint_X_power_limit_uw |
rw | 功率上限 (μW) |
constraint_X_time_window_us |
rw | 时间窗口 (μs) |
constraint_X_max_power_uw |
ro | 最大允许功率 (μW) |
constraint_X_min_power_uw |
ro | 最小允许功率 (μW) |
constraint_X_max_time_window_us |
ro | 最大允许时间窗口 (μs) |
constraint_X_min_time_window_us |
ro | 最小允许时间窗口 (μs) |
/** * @brief 约束属性的使用示例 * * 查看约束0名称: * cat constraint_0_name * → long_term * * 设置长期功率上限为 15W,时间窗口 1 秒: * echo 15000000 > constraint_0_power_limit_uw * echo 1000000 > constraint_0_time_window_us * * 注意: 时间窗口不适用于峰值功率约束。 * power_limit_uw 和 time_window_us 是唯二必须的属性。 */
18.3.3 通用属性
| 属性 | 权限 | 描述 |
|---|---|---|
enabled |
rw | 在功率区域级别启用/禁用控制,或通过控制类型对所有区域统一控制 |
18.4 驱动接口
/** * @brief 功率封顶客户端驱动 API * * 注册控制类型: * powercap_register_control_type() - 注册控制类型对象 * * 注册功率区域: * powercap_register_zone() - 注册功率区域(位于给定控制类型下), * 可以是顶级功率区域或之前注册区域的子区域。 * 在调用此函数前,必须已定义区域中的约束数量和对应的回调。 * * 注销: * powercap_unregister_zone() - 释放功率区域 * powercap_unregister_control_type() - 释放控制类型对象 * * 详细 API 可通过 kernel-doc 生成: * include/linux/powercap.h */
/** * @brief 驱动注册的典型流程 * * 1. 定义约束的 get/set 回调函数 * 2. 定义功率区域的 ops 结构体 * 3. 调用 powercap_register_control_type() 注册控制类型 * 4. 调用 powercap_register_zone() 注册顶层功率区域 * 5. (可选)调用 powercap_register_zone() 注册子区域 * * 示例 (Intel RAPL): * 控制类型: intel-rapl * 顶层区域: intel-rapl:0 (代表 CPU Package 0) * 子区域: intel-rapl:0:0 (Core), intel-rapl:0:1 (Uncore) */
18.5 设计模式分析与应用场景
/** * @brief 设计模式 * * 1. 组合模式 (Composite Pattern): * 功率区域的树状层级结构使得客户端可以统一对待 * 单个区域和区域组合。 * * 2. 策略模式 (Strategy Pattern): * 不同的控制类型 (intel-rapl, idle-injection) 代表不同的 * 功率封顶策略,通过统一接口暴露。 * * 3. 观察者模式 (Observer Pattern): * 当功率限制被修改时,框架通知所有相关层级。 */
场景调试:如果在数据中心需要限制某台服务器的功耗不超过 100W:
# echo 100000000 > /sys/class/power_cap/intel-rapl/intel-rapl:0/constraint_0_power_limit_uw
如果设置后功耗仍未下降,检查 enabled 属性是否为 1,以及 constraint_0_name 是否对应正确的约束类型(长期/短期)。
第20章 DTPM 框架深度解析
20.1 设计动机
/** * @brief DTPM 的三大核心动机 * * 热点管理: SoC 复杂度导致热点数量激增。DTPM 全局协调各设备功耗, * 避免单一设备的降温操作被其他设备的高功耗行为抵消。 * * 性能维持: 在 VR 场景下,若因某 CPU 任务过重而急剧削减整体性能, * 用户会感到眩晕。DTPM 通过全局预算平衡,优先保障关键路径算力。 * * 电池保护: 动态限制充电电流或其他外设功耗,防止瞬间总功率过大, * 导致电池电压骤降或系统不稳定。 */
20.2 树状模型剖析
DTPM 构建的并非物理总线树,而是功耗约束的虚拟描述树。
/** * @brief DTPM 树状模型机制 * * SoC (400mW - 3100mW) # 根节点:全局预算 * | * `-- pkg (400mW - 3100mW) # 中间节点:聚合子节点功耗 * | * |-- pd0 (100mW - 700mW) # 叶子节点:真实可管理设备 (CPU0-3) * | * `-- pd1 (300mW - 2400mW)# 叶子节点:真实可管理设备 (CPU4-5) * * 插入新节点时,功耗特性向上传播: * SoC 的功耗范围 = pkg 功耗范围 + pd2 功耗范围 * = (400mW - 3100mW) + (200mW - 2800mW) = (600mW - 5900mW) * * 每个节点有 2^10 (1024) 基数的权重,反映在同级节点中的功耗占比: * SoC (w=1024) * |-- pkg (w=538) → 538/1024 ≈ 52.5% 的总预算 * `-- pd2 (w=486) → 486/1024 ≈ 47.5% 的总预算 */
20.2.1 预算分布算法
/** * @brief 功耗限制的传播算法 * * 当在根节点设置 power_limit = 3200mW: * pkg 的预算 = 3200 * 538 / 1024 = 1681mW * pd2 的预算 = 3200 * 486 / 1024 = 1519mW * pkg 内部继续分配: * pd0 = 1681 * 231 / 1024 = 378mW * pd1 = 1681 * 794 / 1024 = 1303mW * * 这种算法的优势在于: * 1. 只需在根节点设置一个总预算,子设备自动获得合理分配 * 2. 权重基于实际功耗特征,高功耗设备自动获得更多预算 * 3. 当某个子设备功耗特征变化时,整个树会重新平衡 */
20.3 用户空间 API
DTPM 完全兼容 powercap sysfs 接口:
/** * @brief DTPM 节点属性详解 * * power_uw (ro): 瞬时功耗。 * - 叶子节点:设备实际功耗 * - 中间节点:所有子节点功耗之和 * * max_power_range_uw (ro): 功耗动态范围 = 最大功耗 - 最小功耗 * * name (ro): 节点名称。实现相关,不保证唯一性。 */
约束属性:
/** * @brief DTPM 约束 0 的特殊语义 * * constraint_0_name: "imi" (immediate),即时约束 * constraint_0_power_limit_uw: 目标功率上限 (μW) * constraint_0_time_window_us: 时间窗口 (μs)。 * - 设为 0 表示无限(永久生效) * - 设为特定值表示间歇性限制的占空比周期 * constraint_0_max_power_uw: 最大允许功率上限。 * 写入此值会清除约束。 */
场景调试:若要让 CPU 集群整体功耗不超过 2W:
# 查看当前功耗 cat /sys/class/power_cap/dtpm/dtpm:0/power_uw # 设置即时功耗限制为 2W (2000000 μW) echo 2000000 > /sys/class/power_cap/dtpm/dtpm:0/constraint_0_power_limit_uw
20.4 内核 API
/** * @brief DTPM 节点的生命周期管理 * * 1. dtpm_alloc(): 分配并初始化 dtpm 结构 * 2. dtpm_register(): 将节点插入 DTPM 树 * 3. dtpm_update_power(): 功耗特征变化时,更新树中的权重和范围 * 4. dtpm_unregister(): 从树中移除节点 * * 注册时须实现 powercap ops 回调:获取/设置功耗和限制。 * 若节点是中间节点,可用 dtpm_register_parent() 快速插入。 */
设计模式:
| 模式 | 应用 | 说明 |
|---|---|---|
| 组合模式 | 树状节点结构 | 统一对待叶子和中间节点 |
| 观察者模式 | 功耗特征更新 | 子节点功耗变化时自动更新父节点 |
| 策略模式 | 权重分配算法 | 同级节点按权重分配预算 |
| 适配器模式 | powercap 接口 | 将 DTPM 语义适配为统一 sysfs 接口 |
20.5 DTPM总结
DTPM 为嵌入式 SoC 的全局热管理和功耗预算提供了统一的树状抽象。理解其权重传播机制和约束语义,是实现稳定、高效的移动设备功耗管理的关键。
第21章 Linux Regulator 框架
21.1 概述与术语
/** * @brief 调节器框架核心定义 * * - 调节器 (Regulator): 为其他设备供电的电子器件。多数可以启/禁输出, * 部分支持控制输出电压或电流。 * * - 消费者 (Consumer): 由调节器供电的设备。 * - 静态消费者:不改变电压/电流,仅需启/禁电源。 * 电压由硬件/引导加载器/内核初始化代码设定。 * - 动态消费者:需根据工作点改变电压或电流限制。 * * - 电源域 (Power Domain): 由一个调节器或开关输出供电的电路。 * 可包含开关和多个消费者,形成“供给”关系。 * * - 约束 (Constraints): 用于保护硬件的性能与安全限制。 * 存在于三个层级:调节器级 (硬件数据手册)、电源域级 (板级初始化代码)、 * 消费者级 (驱动动态设置)。 */
设计目标:提供标准的电压和电流控制接口,以动态调节功耗、延长电池寿命,并防止因错误配置导致的硬件损坏。
21.2 设计原则与安全模型
/** * @brief API 设计的核心安全原则 * * 1. 除非明确知道在当前系统上安全,否则 API 绝不应改变硬件状态。 * 2. 大部分设备无需运行时的电压/电流配置,仅需开关电源。 * 3. 共享调节器场景普遍,消费者 API 必须自动处理共享而不增加额外代码。 * * @note 这些原则确保了驱动开发的简洁性和系统的健壮性。 */
21.3 消费者驱动接口
/** * @brief 消费者获取与释放调节器 * * regulator = regulator_get(dev, "Vcc"); // 通过 supply ID 获取 * regulator_put(regulator); // 释放 * * @note 通常在 probe()/remove() 中调用。 * 一个消费者可拥有多个调节器 (如模拟和数字电源)。 */
输出启/禁控制:
int regulator_enable(regulator); // 启用电源,共享时引用计数 int regulator_disable(regulator); // 禁用,仅当引用计数归零时真正关闭 int regulator_force_disable(regulator); // 紧急强制关闭,忽略所有消费者 int regulator_is_enabled(regulator); // 查询启用状态 (返回 >0 表示已启用)
电压控制 (动态消费者):
/** * @brief 设置与获取输出电压 * * regulator_set_voltage(reg, min_uV, max_uV); * - 可在启用或禁用时调用。启用时立即生效,禁用时记录配置待启用时生效。 * * regulator_get_voltage(reg); * - 返回配置的输出电压,无论启用与否。 * - 不可用于判断实际输出状态,需结合 is_enabled 使用。 */
电流限制控制 (动态消费者):
int regulator_set_current_limit(reg, min_uA, max_uA); // 设置电流限制 int regulator_get_current_limit(reg); // 获取配置的电流限制
工作模式控制:
/** * @brief 间接模式控制 (推荐大多数消费者使用) * * regulator_set_load(reg, load_uA); * 消费者根据数据手册声明其最大负载电流。 * 核心重新计算总负载,选择合适的调节器工作模式以优化效率。 * 消费者无需了解调节器细节或是否共享。 * * @brief 直接模式控制 (仅适用于了解调节器的专用消费者) * * regulator_set_mode(reg, mode); * regulator_get_mode(reg); */
调节器事件通知:
regulator_register_notifier(reg, &nb); // 注册通知,如过温、欠压等 regulator_unregister_notifier(reg, &nb);
直接寄存器访问 (用于特殊固件/时钟等):
struct regmap *regulator_get_regmap(reg); // 获取底层 regmap int regulator_get_hardware_vsel_register(reg, &vsel_reg, &vsel_mask); int regulator_list_hardware_vsel(reg, selector); // 硬件电压选择器转换
21.4 调节器驱动接口
/** * @brief 调节器注册与注销 * * struct regulator_dev *regulator_register(regulator_desc, config); * 向核心注册调节器的能力和回调操作。 * * void regulator_unregister(rdev); */
事件发送:
/** * @brief 驱动可调用此函数向消费者发送事件 (过温、欠压等) */ int regulator_notifier_call_chain(rdev, event, data);
回调实现:驱动需填充 struct regulator_desc 和 struct regulator_ops,提供 enable/disable、set_voltage、get_voltage 等操作函数。
21.5 机器驱动接口
/** * @brief 板级初始化代码配置调节器映射与约束 * * 1. 定义 regulator_consumer_supply 映射:将消费者 dev_name 和 supply 名字 * 关联到具体的调节器。 * 2. 定义 regulator_init_data,包含约束 (min_uV, max_uV, valid_modes_mask 等) * 以及消费者映射列表。 * 3. 若调节器有父子供电关系,设置 supply_regulator 字段,建立树状依赖。 * 4. 通过 platform_device_register() 注册调节器设备。 * * @note 设备树中也可通过 regulator 节点完成此配置,但内核仍保留此传统接口。 */
21.6 场景调试与分析
案例1:CPU 调频时电压不变
-
检查 CPUfreq 驱动是否调用了
regulator_set_voltage。 -
查看
/sys/kernel/debug/regulator/下的电压限制和实际输出。 -
确认调节器约束
valid_ops_mask包含REGULATOR_CHANGE_VOLTAGE。
案例2:共享调节器异常关闭
-
某消费者调用
regulator_disable,引用计数减1,但真正的关闭发生在计数归零时。 -
使用
regulator_is_enabled和 debugfs 中的use_count确认引用计数。 -
若误用
regulator_force_disable会导致其他消费者断电。
案例3:过温/欠压保护
-
驱动实现
get_error_flags或get_status回调。 -
消费者通过
regulator_register_notifier接收REGULATOR_EVENT_OVER_TEMP等事件,采取降频或关断措施。
21.7 设计模式分析
| 模式 | 应用位置 | 说明 |
|---|---|---|
| 外观模式 | regulator_get/set_voltage 等API |
简化底层硬件操作,提供统一接口 |
| 观察者模式 | 调节器事件通知 | 消费者注册回调,响应电压/温度异常 |
| 策略模式 | 间接 vs 直接模式控制 | 不同消费者选择不同负载调整策略 |
| 组合模式 | 调节器树与电源域 | 层级化描述供电关系 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)