30、系统电源管理睡眠状态与设备驱动电源管理详解

系统电源管理睡眠状态与设备驱动电源管理详解

1. 系统电源管理睡眠状态概述

系统电源管理旨在将整个系统置于低功耗状态,在这种状态下,系统消耗少量但最低限度的电量,同时保持相对较低的用户响应延迟。具体的电量消耗和响应延迟取决于系统所处的睡眠状态深度,这也被称为静态电源管理,因为它在系统长时间不活动时被激活。

系统可进入的状态取决于底层平台,不同架构甚至同一架构的不同代或系列都会有所不同。不过,大多数平台常见的有四种睡眠状态:
| 睡眠状态 | 别称 | ACPI 状态 | 说明 |
| — | — | — | — |
| 挂起到空闲(Suspend to idle) | 冻结(freeze) | S0 | 最基本、轻量级的状态,纯软件驱动,尽可能让 CPU 处于最深的空闲状态 |
| 开机待机(Power-on standby) | 待机(standby) | S1 | 除冻结用户空间和将所有 I/O 设备置于低功耗状态外,还会关闭所有非引导 CPU |
| 挂起到内存(Suspend-to-ram) | 挂起(suspend, mem) | S3 | 将系统中的所有内容置于低功耗状态,关闭所有 CPU 并将内存置于自刷新状态,以保存系统和设备状态 |
| 挂起到磁盘(Suspend to disk) | 休眠(hibernation) | S4 | 尽可能关闭系统的大部分组件,包括内存,将内存内容写入持久存储介质,恢复时从休眠映像启动 |

1.1 挂起到空闲(冻结)

此状态下,用户空间被冻结,所有 I/O 设备进入低功耗状态,使处理器能更多时间处于空闲状态。可使用以下命令使系统进入空闲状态:

$ echo freeze > /sys/power/state

由于这是纯软件状态,只要设置了 CONFIG_SUSPEND 内核配置选项,该状态总是受支持。它可用于不支持开机待机或挂起到内存的平台,还可与挂起到内存结合使用以减少恢复延迟。

1.2 开机待机(待机)

除了冻结用户空间和将 I/O 设备置于低功耗状态外,该状态还会关闭所有非引导 CPU。使用以下命令将系统置于待机状态(前提是平台支持):

$ echo standby > /sys/power/state

与挂起到空闲相比,此状态能节省更多能量,但恢复延迟通常会稍长一些。

1.3 挂起到内存(挂起)

该状态将系统中的所有内容置于低功耗状态,关闭所有 CPU 并将内存置于自刷新状态,以保存系统和设备状态。使用以下命令将系统置于挂起到内存状态:

# echo mem > /sys/power/state

实际操作由 /sys/power/mem_sleep 文件控制,该文件包含系统在写入 mem /sys/power/state 后可进入的模式列表,可能的模式包括:
- s2idle:等同于挂起到空闲,始终可用。
- shallow:等同于开机待机,其可用性取决于平台对待机模式的支持。
- deep:真正的挂起到内存状态,其可用性取决于平台。

查询示例:

$ cat /sys/power/mem_sleep
[s2idle] deep

选中的模式用方括号 [] 括起来。如果某个模式不受平台支持,对应的字符串将不会出现在 /sys/power/mem_sleep 中。将该文件中的其他字符串写入该文件会使后续使用的挂起模式更改为该字符串所代表的模式。

系统启动时,默认的挂起模式(即不向 /sys/power/mem_sleep 写入任何内容时使用的模式)是 deep (如果支持挂起到内存)或 s2idle ,但可以通过内核命令行中的 mem_sleep_default 参数覆盖。

可以使用系统上的 RTC(实时时钟)来测试挂起到内存功能,前提是 RTC 支持唤醒警报功能。操作步骤如下:
1. 识别系统上可用的 RTC:

$ ls /sys/class/rtc/
  1. 对于支持警报功能的 RTC,在其目录中会有一个 wakealarm 文件,可用于配置警报并将系统挂起到内存:
# 查看是否有警报设置
$ cat /sys/class/rtc/rtc0/wakealarm
# 设置 20 秒后的唤醒警报
# echo +20 > /sys/class/rtc/rtc0/wakealarm
# 现在将系统挂起到内存
# echo mem > /sys/power/state

在唤醒之前,控制台将不再有活动。

1.4 挂起到磁盘(休眠)

此状态能最大程度节省电量,因为它会尽可能关闭系统的大部分组件,包括内存。内存内容(快照)会写入持久存储介质,通常是磁盘,然后关闭内存和整个系统。恢复时,将快照读回内存并从休眠映像启动系统。使用以下命令使系统进入休眠状态:

$ echo disk > /sys/power/state

内存状态写入磁盘后,可执行的操作由 /sys/power/disk 文件及其内容控制。该文件包含一个字符串列表,每个字符串代表系统状态保存到持久存储介质后可执行的操作,可能的操作包括:
- platform:自定义和特定于平台的操作,可能需要固件(BIOS)干预。
- shutdown:关闭系统。
- reboot:重启系统(主要用于诊断)。
- suspend:将系统置于通过前面所述的 mem_sleep 文件选择的挂起睡眠状态。如果系统成功从该状态唤醒,则丢弃休眠映像并继续运行;否则,使用映像恢复系统的先前状态。
- test_resume:用于系统恢复诊断,加载映像就像系统刚从休眠中唤醒一样,并进行完整的系统恢复。

查询示例:

$ cat /sys/power/disk
[platform] shutdown reboot suspend test_resume

选中的操作使用方括号 [] 括起来。将列出的字符串之一写入该文件会选择其代表的选项。休眠是一个复杂的操作,需要设置 CONFIG_HIBERNATION 内核配置选项才能启用该功能,且该选项只有在给定 CPU 架构的支持包括系统恢复的底层代码时才能设置(参考 ARCH_HIBERNATION_POSSIBLE 内核配置选项)。

为了使挂起到磁盘正常工作,根据休眠映像的存储位置,磁盘上可能需要一个专用分区,也称为交换分区,用于将内存内容写入空闲的交换空间。为了检查休眠是否按预期工作,通常会尝试在重启模式下进行休眠:

$ echo reboot > /sys/power/disk
# echo disk > /sys/power/state

第一个命令通知电源管理核心在创建休眠映像后应执行的操作(这里是重启)。重启后,系统将从休眠映像恢复,你应该会回到开始转换的命令提示符处。多次进行此测试可以增强测试的可靠性。

2. 为设备驱动添加电源管理功能

设备驱动本身可以实现一种独特的电源管理功能,即运行时电源管理。并非所有设备都支持运行时电源管理,但支持的设备必须导出一些回调函数,以便根据用户或系统的策略决策控制其电源状态。

2.1 设备电源管理基础

设备电源管理包括描述设备所处的状态以及控制这些状态的机制。内核提供了 struct dev_pm_ops 结构体,每个对电源管理感兴趣的设备驱动、类或总线都必须填充该结构体,以便内核与系统中的每个设备进行通信,而不管设备所在的总线或所属的类。

struct device 结构体如下:

struct device {
    [...]
    struct device *parent;
    struct bus_type *bus;
    struct device_driver *driver;
    struct dev_pm_info power;
    struct dev_pm_domain *pm_domain;
}

在这个结构体中,设备可以是子设备( .parent 字段指向另一个设备)或父设备(另一个设备的 .parent 字段指向它),可以位于给定的总线下,属于某个类或间接属于某个子系统。此外,设备可以是某个电源域的一部分。 .power 字段的类型为 struct dev_pm_info ,主要保存与电源管理相关的状态,如当前电源状态、是否可以唤醒、是否已准备好以及是否已挂起。

2.2 实现设备电源管理操作

为了让设备在子系统级别或设备驱动级别参与电源管理,其驱动需要实现一组设备电源管理操作,通过定义和填充 include/linux/pm.h 中定义的 struct dev_pm_ops 类型的对象来实现:

struct dev_pm_ops {
    int (*prepare)(struct device *dev);
    void (*complete)(struct device *dev);
    int (*suspend)(struct device *dev);
    int (*resume)(struct device *dev);
    int (*freeze)(struct device *dev);
    int (*thaw)(struct device *dev);
    int (*poweroff)(struct device *dev);
    int (*restore)(struct device *dev);
    [...]
    int (*suspend_noirq)(struct device *dev);
    int (*resume_noirq)(struct device *dev);
    int (*freeze_noirq)(struct device *dev);
    int (*thaw_noirq)(struct device *dev);
    int (*poweroff_noirq)(struct device *dev);
    int (*restore_noirq)(struct device *dev);
    int (*runtime_suspend)(struct device *dev);
    int (*runtime_resume)(struct device *dev);
    int (*runtime_idle)(struct device *dev);
};

为了便于阅读,上述数据结构中移除了 *_early() *_late() 回调函数,建议查看完整定义。由于其中有大量回调函数,将在后续需要使用它们的部分进行详细描述。

设备电源状态有时被称为 D 状态,灵感来自 PCI 设备和 ACPI 规范。这些状态范围从 D0 到 D3(包括 D0 和 D3),尽管并非所有设备类型都以这种方式定义电源状态,但这种表示方法可以映射到所有已知的设备类型。

2.3 实现运行时电源管理功能

运行时电源管理是一种针对单个设备的电源管理功能,允许在系统运行时控制特定设备的状态,而不受全局系统的影响。驱动程序若要实现运行时电源管理,只需提供 struct dev_pm_ops 中部分回调函数的子集:

struct dev_pm_ops {
    [...]
    int (*runtime_suspend)(struct device *dev);
    int (*runtime_resume)(struct device *dev);
    int (*runtime_idle)(struct device *dev);
};

内核还提供了 SET_RUNTIME_PM_OPS() 宏,用于填充结构体中的三个回调函数,该宏定义如下:

#define SET_RUNTIME_PM_OPS(suspend_fn, resume_fn, idle_fn) \
        .runtime_suspend = suspend_fn, \
        .runtime_resume = resume_fn, \
        .runtime_idle = idle_fn,

这些回调函数是运行时电源管理中涉及的唯一函数,它们的作用如下:
- .runtime_suspend() :必要时记录设备的当前状态,并将设备置于静止状态。当设备未被使用时,电源管理模块会调用此方法。简单来说,该方法必须使设备处于无法与 CPU 和 RAM 通信的状态。
- .runtime_resume() :当设备需要处于完全功能状态时调用,例如系统需要访问该设备时。此方法必须恢复电源并重新加载任何所需的设备状态。
- .runtime_idle() :当根据设备使用计数器(实际上是计数器达到 0)以及活动子设备的数量判断设备不再被使用时调用。不过,此回调函数执行的操作是特定于驱动程序的。在大多数情况下,如果满足某些条件,驱动程序会在设备上调用 .runtime_suspend() ,或者调用 pm_schedule_suspend() (给定一个延迟以设置定时器,以便在未来提交挂起请求),或者调用 pm_runtime_autosuspend() (根据已经使用 pm_runtime_set_autosuspend_delay() 设置的延迟在未来安排挂起请求)。如果 .runtime_idle 回调函数不存在或返回 0,电源管理核心将立即调用 .runtime_suspend() 回调函数。为了使电源管理核心不执行任何操作, .runtime_idle() 必须返回非零值,驱动程序通常返回 -EBUSY 或 1。

以下是一个示例,展示了如何将回调函数填充到 struct dev_pm_ops 中:

static const struct dev_pm_ops bh1780_dev_pm_ops = {
    SET_SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend, pm_runtime_force_resume)
    SET_RUNTIME_PM_OPS(bh1780_runtime_suspend, bh1780_runtime_resume, NULL)
};
[...]
static struct i2c_driver bh1780_driver = {
    .probe = bh1780_probe,
    .remove = bh1780_remove,
    .id_table = bh1780_id,
    .driver = {
        .name = “bh1780”,
        .pm = &bh1780_dev_pm_ops,
        .of_match_table = of_match_ptr(of_bh1780_match),
    },
};
module_i2c_driver(bh1780_driver);

这是一个来自 IIO 环境光传感器驱动 drivers/iio/light/bh1780.c 的摘录,展示了如何使用方便的宏填充 struct dev_pm_ops SET_SYSTEM_SLEEP_PM_OPS 用于填充与系统睡眠相关的宏, pm_runtime_force_suspend pm_runtime_force_resume 是电源管理核心提供的特殊辅助函数,分别用于强制设备挂起和恢复。

2.4 驱动程序中的运行时电源管理

实际上,电源管理核心使用两个计数器来跟踪每个设备的活动情况。第一个计数器是 power.usage_count ,用于统计对设备的活动引用,这些引用可以是外部引用,如打开的文件句柄,也可以是其他设备对该设备的使用,或者是在操作期间用于保持设备活动的内部引用。另一个计数器是 power.child_count ,用于统计活动子设备的数量。

这些计数器从电源管理的角度定义了给定设备的活动/空闲条件,设备的活动/空闲条件是电源管理核心确定设备是否可访问的唯一可靠方法。空闲条件是设备使用计数器递减到 0,而活动条件(也称为恢复条件)是设备使用计数器递增时发生。

当出现空闲条件时,电源管理核心会发送/执行空闲通知(即将设备的 power.idle_notification 字段设置为 true ,调用总线类型/类/设备的 .runtime_idle() 回调函数,然后将 .idle_notification 字段再次设置为 false ),以检查设备是否可以挂起。如果 .runtime_idle() 回调函数不存在或返回 0,电源管理核心将立即调用 .runtime_suspend() 回调函数来挂起设备,之后设备的 power.runtime_status 字段将设置为 RPM_SUSPENDED ,表示设备已挂起。当出现恢复条件(设备使用计数器递增)时,电源管理核心将在某些条件下对该设备进行恢复,恢复操作可以是同步的也可以是异步的。可以查看 drivers/base/power/runtime.c 中的 rpm_resume() 函数及其描述。

最初,所有设备的运行时电源管理都是禁用的,这意味着在为设备调用 pm_runtime_enable() 之前,调用大多数与电源管理相关的辅助函数都会失败,该函数用于启用设备的运行时电源管理。尽管所有设备的初始运行时电源管理状态为挂起,但这不一定反映设备的实际物理状态。因此,如果设备最初是活动的(即能够处理 I/O),则必须使用 pm_runtime_set_active() 将其运行时电源管理状态更改为活动状态(将 power.runtime_status 设置为 RPM_ACTIVE ),并在可能的情况下,在为设备调用 pm_runtime_enable() 之前使用 pm_runtime_get_noresume() 增加其使用计数器。设备完全初始化后,可以调用 pm_runtime_put()

调用 pm_runtime_get_noresume() 的原因是,如果调用了 pm_runtime_put() ,设备使用计数器将回到 0,这对应于空闲条件,然后会执行空闲通知。此时,可以检查必要条件是否满足并挂起设备。但是,如果设备的初始状态是禁用的,则无需这样做。

还有 pm_runtime_get() pm_runtime_get_sync() pm_runtime_put_noidle() pm_runtime_put_sync() 等辅助函数。 pm_runtime_get_sync() pm_runtime_get() pm_runtime_get_noresume() 的区别在于,前者在设备使用计数器递增且满足活动/恢复条件时将同步(立即)执行设备的恢复操作,而第二个辅助函数将异步执行(提交请求),第三个辅助函数在递减设备使用计数器后将立即返回(甚至不检查恢复条件)。同样的机制也适用于 pm_runtime_put_sync() pm_runtime_put() pm_runtime_put_noidle()

给定设备的活动子设备数量会影响该设备的使用计数器。通常,访问子设备需要父设备,因此在子设备活动时关闭父设备是适得其反的。然而,有时在确定设备是否空闲时可能需要忽略其活动子设备。一个很好的例子是 I2C 总线,即使总线上的设备(子设备)处于活动状态,总线也可以报告为空闲。对于这种情况,可以调用 pm_suspend_ignore_children() 来允许设备即使有活动子设备也报告为空闲。

2.5 运行时电源管理的同步和异步操作

前面提到电源管理核心可以执行同步或异步的电源管理操作。同步操作比较直接(方法调用是序列化的),但在异步调用电源管理相关操作时,需要注意以下步骤:

graph TD;
    A[电源管理核心设置设备的 power.request 字段] --> B[电源管理核心设置设备的 power.request_pending 字段为 true];
    B --> C[电源管理核心将设备的 RPM 相关工作排队到全局电源管理工作队列];
    C --> D[工作有机会运行时,工作函数检查请求是否仍挂起];
    D --> E{根据 power.request 字段调用相应的请求处理程序};

在异步模式下,不会立即调用操作的处理程序,而是提交该操作的请求。具体步骤如下:
1. 电源管理核心将设备的 power.request 字段(类型为 enum rpm_request )设置为要提交的请求类型(例如, RPM_REQ_IDLE 表示空闲通知请求, RPM_REQ_SUSPEND 表示挂起请求, RPM_REQ_AUTOSUSPEND 表示自动挂起请求),该请求类型对应要执行的操作。
2. 电源管理核心将设备的 power.request_pending 字段设置为 true
3. 电源管理核心将设备的 RPM 相关工作( power.work ,其工作函数为 pm_runtime_work() ,可在 pm_runtime_init() 中查看其初始化)排队到全局电源管理工作队列中,以便稍后执行。
4. 当该工作有机会运行时,工作函数(即 pm_runtime_work() )将首先检查设备上是否仍有挂起的请求( if (dev->power.request_pending) ),并根据设备的 power.request 字段执行 switch...case 语句,以调用底层的请求处理程序。

需要注意的是,工作队列管理自己的线程,可以运行调度的工作。由于在异步模式下,处理程序是在工作队列中调度的,因此异步的电源管理相关辅助函数在原子上下文中调用是完全安全的。例如,在 IRQ 处理程序中调用这些函数相当于延迟处理电源管理请求。

2.6 自动挂起

自动挂起是一种机制,用于那些不希望设备在运行时一空闲就立即挂起的驱动程序,而是希望设备先保持不活动状态一段时间。在运行时电源管理的上下文中,“自动挂起”并不意味着设备自动挂起自身,而是基于一个定时器,定时器到期时会排队一个挂起请求。这个定时器实际上是设备的 power.suspend_timer 字段(可在 pm_runtime_init() 中查看其设置)。

调用 pm_runtime_put_autosuspend() 将启动定时器,而 pm_runtime_set_autosuspend_delay() 将设置超时时间(也可以通过 sysfs 中的 /sys/devices/.../power/autosuspend_delay_ms 属性设置),该超时时间由设备的 power.autosuspend_delay 字段表示。 pm_schedule_suspend() 辅助函数也可以使用这个定时器,通过传入一个延迟参数(在这种情况下,该参数将优先于 power.autosuspend_delay 字段中设置的值),之后将提交一个挂起请求。

可以将这个定时器看作是在计数器达到 0 与设备被视为空闲之间添加延迟的工具,这对于开启或关闭成本较高的设备非常有用。

为了使用自动挂起功能,子系统或驱动程序必须调用 pm_runtime_use_autosuspend() (最好在注册设备之前),该辅助函数将设备的 power.use_autosuspend 字段设置为 true 。在启用自动挂起功能的设备上进行操作后,应该调用 pm_runtime_mark_last_busy() ,该函数将设备的 power.last_busy 字段设置为当前时间(以 jiffies 为单位),因为该字段用于计算自动挂起的不活动时间(例如, new_expire_time = last_busy + msecs_to_jiffies(autosuspend_delay) )。

2.7 实际驱动程序中的应用

bh1780 Linux 驱动为例,它是一个数字 16 位 I2C 环境光传感器驱动。以下是 probe 方法的摘录:

static int bh1780_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    [...]
    /* Power up the device */ [...]
    pm_runtime_get_noresume(&client->dev);
    pm_runtime_set_active(&client->dev);
    pm_runtime_enable(&client->dev);
    ret = bh1780_read(bh1780, BH1780_REG_PARTID);
    dev_info(&client->dev, “Ambient Light Sensor, Rev : %lu\n”, (ret & BH1780_REVMASK));
    /*
     * As the device takes 250 ms to even come up with a fresh
     * measurement after power-on, do not shut it down unnecessarily.
     * Set autosuspend to five seconds.
     */
    pm_runtime_set_autosuspend_delay(&client->dev, 5000);
    pm_runtime_use_autosuspend(&client->dev);
    pm_runtime_put(&client->dev);
    [...]
    ret = iio_device_register(indio_dev);
    if (ret)
        goto out_disable_pm;
    return 0;
out_disable_pm:
    pm_runtime_put_noidle(&client->dev);
    pm_runtime_disable(&client->dev);
    return ret;
}

在这个片段中,为了便于阅读,只保留了与电源管理相关的调用。首先, pm_runtime_get_noresume() 会增加设备使用计数器,但不会执行设备的空闲通知( _noidle 后缀)。可以使用该接口关闭运行时挂起功能,或者即使设备处于挂起状态也使使用计数器为正,以避免因运行时挂起而导致的无法正常唤醒问题。接下来的 pm_runtime_set_active() 辅助函数将设备标记为活动状态( power.runtime_status = RPM_ACTIVE ),并清除设备的 power.runtime_error 字段。此外,设备父设备的未挂起(活动)子设备计数器会相应修改(实际上是递增)。在设备上调用 pm_runtime_set_active() 会阻止该设备的父设备在运行时挂起(假设父设备的运行时电源管理已启用),除非父设备的 power.ignore_children 标志被设置。因此,一旦为设备调用了 pm_runtime_set_active() ,应尽快为其调用 pm_runtime_enable()

pm_runtime_enable() 是启用设备运行时电源管理的必需辅助函数,它会在设备的 power.disable_depth 值大于 0 时将其递减。每次调用运行时电源管理辅助函数时都会检查设备的 power.disable_depth 值,该值必须为 0 才能使辅助函数继续执行。其初始值为 1,调用 pm_runtime_enable() 时会将其递减。在错误处理路径中,调用 pm_runtime_put_noidle() 以平衡电源管理运行时计数器,并调用 pm_runtime_disable() 完全禁用设备的运行时电源管理。

bh1780 驱动还涉及 IIO 框架,它在 sysfs 中暴露了与物理转换通道对应的条目。读取与通道对应的 sysfs 文件将报告该通道转换的数字值。对于 bh1780 ,其驱动中的通道读取入口点是 bh1780_read_raw() ,以下是该方法的摘录:

static int bh1780_read_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int *val, int *val2, long mask)
{
    struct bh1780_data *bh1780 = iio_priv(indio_dev);
    int value;
    switch (mask) {
    case IIO_CHAN_INFO_RAW:
        switch (chan->type) {
        case IIO_LIGHT:
            pm_runtime_get_sync(&bh1780->client->dev);
            value = bh1780_read_word(bh1780, BH1780_REG_DLOW);
            if (value < 0)
                return value;
            pm_runtime_mark_last_busy(&bh1780->client->dev);
            pm_runtime_put_autosuspend(&bh1780->client->dev);
            *val = value;
            return IIO_VAL_INT;
        default:
            return -EINVAL;
    case IIO_CHAN_INFO_INT_TIME:
        *val = 0;
        *val2 = BH1780_INTERVAL * 1000;
        return IIO_VAL_INT_PLUS_MICRO;
    default:
        return -EINVAL;
    }
}

在通道读取时,会调用上述函数。设备驱动需要指示设备采样通道,执行转换,然后读取转换结果并报告给读取者。由于设备可能处于挂起状态,驱动需要立即访问设备,因此调用 pm_runtime_get_sync() 来增加设备使用计数器并同步恢复设备。设备恢复后,驱动可以与设备通信并读取转换值。由于驱动支持自动挂起,调用 pm_runtime_mark_last_busy() 来标记设备最后一次活动的时间,这将更新自动挂起定时器的超时值。最后,驱动调用 pm_runtime_put_autosuspend() ,该函数将在自动挂起定时器到期后执行设备的运行时挂起操作,除非在定时器到期前再次调用 pm_runtime_mark_last_busy() 或再次进入读取函数(例如在 sysfs 中读取通道)。

总结来说,在访问硬件之前,驱动可以使用 pm_runtime_get_sync() 恢复设备;完成硬件操作后,驱动可以使用 pm_runtime_put_sync() pm_runtime_put() pm_runtime_put_autosuspend() (假设启用了自动挂起,此时必须先调用 pm_runtime_mark_last_busy() 来更新自动挂起定时器的超时值)通知设备处于空闲状态。

最后,来看模块卸载时调用的方法:

static int bh1780_remove(struct i2c_client *client)
{
    int ret;
    struct iio_dev *indio_dev = i2c_get_clientdata(client);
    struct bh1780_data *bh1780 = iio_priv(indio_dev);
    iio_device_unregister(indio_dev);
    pm_runtime_get_sync(&client->dev);
    pm_runtime_put_noidle(&client->dev);
    pm_runtime_disable(&client->dev);
    ret = bh1780_write(bh1780, BH1780_REG_CONTROL, BH1780_POFF);
    if (ret < 0) {
        dev_err(&client->dev, “failed to power off\n”);
        return ret;
    }
    return 0;
}

首先调用的 pm_runtime_get_sync() 表明设备即将被使用,即驱动需要访问硬件。该辅助函数会立即恢复设备(实际上是增加设备使用计数器并同步恢复设备)。然后调用 pm_runtime_put_noidle() 来递减设备使用计数器,但不执行空闲通知。接着调用 pm_runtime_disable() 禁用设备的运行时电源管理,这会增加设备的 power.disable_depth 值,如果之前该值为 0,则会取消所有挂起的运行时电源管理请求,并等待所有正在进行的操作完成,这样从电源管理核心的角度来看,设备就不再存在了(因为 power.disable_depth 不再符合电源管理核心的预期,意味着在该设备上调用任何进一步的运行时电源管理辅助函数都会失败)。最后,通过 i2c 命令关闭设备电源,使其硬件状态反映其运行时电源管理状态。

2.8 运行时电源管理回调和执行的一般规则

  • .runtime_idle() .runtime_suspend() 只能对活动设备(状态为活动的设备)执行。
  • .runtime_idle() .runtime_suspend() 只能对使用计数器为 0 且活动子设备计数器为 0 或 power.ignore_children 标志被设置的设备执行。
  • .runtime_resume() 只能对挂起设备(状态为挂起的设备)执行。

此外,电源管理核心提供的辅助函数遵循以下规则:
- 如果 .runtime_suspend() 即将执行或有挂起的执行请求,同一设备的 .runtime_idle() 将不会执行。
- 执行或安排执行 .runtime_suspend() 的请求将取消同一设备上任何挂起的执行 .runtime_idle() 的请求。
- 如果 .runtime_resume() 即将执行或有挂起的执行请求,同一设备的其他回调函数将不会执行。
- 执行 .runtime_resume() 的请求将取消同一设备上任何挂起或安排的执行其他回调函数的请求,但已安排的自动挂起除外。

这些规则可以很好地指示任何调用这些回调函数失败的原因,也可以看出恢复操作或恢复请求优先于任何其他回调函数或请求。

3. 运行时电源管理的总结与最佳实践

3.1 运行时电源管理关键要点回顾

  • 计数器机制 :电源管理核心通过 power.usage_count power.child_count 两个计数器来跟踪设备活动。 power.usage_count 统计设备的活动引用, power.child_count 统计活动子设备数量,它们共同决定设备的活动/空闲状态。
  • 回调函数 :运行时电源管理主要涉及 .runtime_suspend() .runtime_resume() .runtime_idle() 三个回调函数。 .runtime_suspend() 使设备进入静止状态, .runtime_resume() 恢复设备功能, .runtime_idle() 根据条件决定是否挂起设备。
  • 辅助函数 pm_runtime_enable() 启用设备的运行时电源管理, pm_runtime_get_sync() 同步恢复设备, pm_runtime_put_autosuspend() 在自动挂起定时器到期后执行设备挂起等。

3.2 最佳实践建议

  • 初始化阶段 :在设备初始化时,若设备初始为活动状态,先使用 pm_runtime_get_noresume() 增加使用计数器,再用 pm_runtime_set_active() 标记设备为活动,最后调用 pm_runtime_enable() 启用运行时电源管理。例如 bh1780 驱动的 probe 方法:
static int bh1780_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    [...]
    pm_runtime_get_noresume(&client->dev);
    pm_runtime_set_active(&client->dev);
    pm_runtime_enable(&client->dev);
    [...]
}
  • 访问硬件阶段 :在访问硬件前,使用 pm_runtime_get_sync() 恢复设备;完成硬件操作后,若支持自动挂起,先调用 pm_runtime_mark_last_busy() 标记最后活动时间,再使用 pm_runtime_put_autosuspend() 通知设备空闲。如 bh1780 驱动的 bh1780_read_raw() 方法:
static int bh1780_read_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int *val, int *val2, long mask)
{
    [...]
    pm_runtime_get_sync(&bh1780->client->dev);
    [...]
    pm_runtime_mark_last_busy(&bh1780->client->dev);
    pm_runtime_put_autosuspend(&bh1780->client->dev);
    [...]
}
  • 模块卸载阶段 :先使用 pm_runtime_get_sync() 恢复设备,再用 pm_runtime_put_noidle() 递减使用计数器,最后调用 pm_runtime_disable() 禁用运行时电源管理。如 bh1780 驱动的 bh1780_remove() 方法:
static int bh1780_remove(struct i2c_client *client)
{
    [...]
    pm_runtime_get_sync(&client->dev);
    pm_runtime_put_noidle(&client->dev);
    pm_runtime_disable(&client->dev);
    [...]
}

3.3 常见问题及解决方法

问题 原因 解决方法
设备无法正常唤醒 运行时挂起导致设备状态异常 使用 pm_runtime_get_noresume() 避免设备在不需要时挂起,确保使用计数器为正
自动挂起未按预期工作 自动挂起定时器设置错误或未正确调用相关函数 检查 pm_runtime_set_autosuspend_delay() 设置的超时时间,确保在操作设备后调用 pm_runtime_mark_last_busy()
电源管理辅助函数调用失败 power.disable_depth 值不为 0 确保在调用辅助函数前调用 pm_runtime_enable() 使 power.disable_depth 为 0

4. 系统电源管理睡眠状态与运行时电源管理的关联

4.1 整体架构关系

系统电源管理睡眠状态(如挂起到空闲、开机待机、挂起到内存、挂起到磁盘)是针对整个系统的电源管理策略,而运行时电源管理是针对单个设备的电源管理功能。两者相互配合,共同实现系统的节能目标。

4.2 运行时电源管理在系统睡眠状态中的作用

  • 进入睡眠状态 :当系统进入睡眠状态时,设备驱动的运行时电源管理回调函数会被调用,使设备进入相应的低功耗状态。例如,在系统进入挂起到内存状态时,设备的 .runtime_suspend() 函数会被调用,将设备置于静止状态。
  • 从睡眠状态恢复 :当系统从睡眠状态恢复时,设备的 .runtime_resume() 函数会被调用,恢复设备的功能。

4.3 示例说明

bh1780 驱动为例,在系统进入睡眠状态前,设备可能会通过运行时电源管理进入挂起状态;当系统恢复时,设备会通过运行时电源管理恢复活动状态,继续正常工作。

graph LR;
    A[系统进入睡眠状态] --> B[设备运行时电源管理调用 .runtime_suspend()];
    B --> C[设备进入低功耗状态];
    D[系统从睡眠状态恢复] --> E[设备运行时电源管理调用 .runtime_resume()];
    E --> F[设备恢复正常工作];

5. 未来发展趋势与展望

5.1 节能需求推动技术发展

随着对能源效率的要求越来越高,系统电源管理和运行时电源管理技术将不断发展。未来可能会出现更智能的电源管理策略,能够根据系统的实时负载和设备的使用情况动态调整电源状态,实现更精细的节能。

5.2 与新兴技术的融合

  • 人工智能 :利用人工智能算法分析设备的使用模式和系统负载,预测设备的空闲时间,提前进行电源管理操作,提高节能效果。
  • 物联网 :在物联网环境中,大量设备需要进行电源管理。未来的电源管理技术将更好地支持物联网设备的分布式电源管理,确保设备在长时间运行的同时降低功耗。

5.3 标准化与兼容性提升

随着技术的发展,电源管理的标准化和兼容性将变得更加重要。未来可能会出现更统一的电源管理接口和规范,使不同厂商的设备能够更好地协同工作,实现更高效的电源管理。

总之,系统电源管理睡眠状态和设备驱动的运行时电源管理是实现系统节能的重要手段。通过深入理解和合理应用这些技术,可以提高系统的能源效率,延长设备的使用寿命。同时,关注未来的发展趋势,积极探索新的技术和应用场景,将有助于推动电源管理技术的不断进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值