欢迎光临
我们一直在努力

Linux电源管理框架深度解析:Runtime_PM与System_Suspend的驱动端适配方法

Linux电源管理框架深度解析:Runtime_PM与System_Suspend的驱动端适配方法

一、Linux电源管理的两面性——为什么Runtime PM和System Suspend要分别处理

Linux设备驱动开发者经常会遇到一个困惑:为什么电源管理要区分Runtime PM和System Suspend两种模式?同一个设备驱动为什么需要实现两套PM回调?这个问题的答案在于两种休眠模式的本质差异。

Runtime PM(运行时电源管理)发生在设备空闲时。比如USB键盘30秒无按键操作,内核自动将其挂起以省电。这是设备级别的独立行为,不影响系统整体状态,也不冻结用户进程。它的触发条件通常是设备驱动的计时器或引用计数降到零。

二、Linux PM 框架的分层架构——从设备模型到驱动的回调链

Linux 电源管理框架建立在设备模型之上,核心抽象是struct dev_pm_ops。该框架的执行流程从用户空间触发开始,经过电源管理核心调度,经由总线层处理,最终到达设备驱动层并控制硬件。用户空间通过 systemctl suspend 或接口发起请求,PM Core 通过 dpm_suspend_start 等函数遍历设备并管理挂起/恢复顺序,总线层处理特定总线的电源状态,驱动层实现具体的回调逻辑,最终作用于硬件寄存器与电源域。

这个架构分为四层:

PM Core 层是电源管理框架的核心调度器。System Suspend 时,dpm_suspend_start()遍历所有设备并发起挂起流程。挂起顺序遵循依赖关系——子设备先挂起,父设备在子设备完成后挂起。恢复时顺序相反:父设备先恢复,子设备后恢复。

总线层处理总线特定的 PM 操作。PCI 总线有 PCI Power Management 规范,USB 总线有 USB Selective Suspend 机制,I2C 和 SPI 总线各自处理自身的 PM。总线层的 PM 回调在驱动回调之前执行,用于硬件总线的电源状态切换。

设备驱动层是驱动作者关心的部分。struct dev_pm_ops包含两套回调:

  • System PM:.suspend/.resume/.freeze/.thaw/.poweroff/.restore
  • Runtime PM:.runtime_suspend/.runtime_resume/.runtime_idle硬件层涉及具体的寄存器操作:保存/恢复设备状态、关闭/开启时钟、切换电源域。

三、设备驱动PM的完整实现——以I2C传感器驱动为例

以下是包含完整System PM和Runtime PM支持的I2C传感器驱动实现:

/*
* bmp280_pm.c — 带完整电源管理的BMP280气压传感器驱动
*
* 展示 Runtime PM 与 System Suspend 在 I2C 设备驱动中的协作模式
* 内核版本要求: >= 4.0 (Runtime PM for I2C)
*/

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/pm.h>
#include <linux/pm_runtime.h>
#include <linux/delay.h>
#include <linux/regmap.h>

/* BMP280 寄存器定义 */
#define BMP280_REG_CTRL_MEAS 0xF4
#define BMP280_REG_CONFIG 0xF5
#define BMP280_REG_STATUS 0xF3
#define BMP280_REG_RESET 0xE0

#define BMP280_MODE_SLEEP 0x00
#define BMP280_MODE_NORMAL 0x03
#define BMP280_MODE_FORCED 0x01

#define BMP280_RESET_VALUE 0xB6

struct bmp280_data {
struct i2c_client *client;
struct regmap *regmap;
struct mutex lock;

/* 设备状态缓存: 用于 system suspend/resume 恢复 */
u8 ctrl_meas_cache;
u8 config_cache;
u32 t_fine; /* 温度校准参数 */

/* 电源管理引用计数 */
bool suspended;
};

/* ===== 第一部分: 设备核心操作 ===== */

static int bmp280_soft_reset(struct bmp280_data *data)
{
int ret;

ret = regmap_write(data->regmap, BMP280_REG_RESET, BMP280_RESET_VALUE);
if (ret)
return ret;

/* 复位后等待2ms */
usleep_range(2000, 2500);
return 0;
}

static int bmp280_set_mode(struct bmp280_data *data, u8 mode)
{
int ret;

ret = regmap_update_bits(data->regmap, BMP280_REG_CTRL_MEAS,
0x03, mode);
if (ret)
return ret;

/* 切换模式后等待测量稳定 */
if (mode != BMP280_MODE_SLEEP)
usleep_range(2000, 2500);

return 0;
}

static int bmp280_read_raw(struct bmp280_data *data, int *temperature,
int *pressure)
{
/* 简化: 读取温度和压力ADC值并做补偿计算 */

int ret;
u8 raw_data[6];

/* 触发单次测量 (Forced Mode) */
ret = bmp280_set_mode(data, BMP280_MODE_FORCED);
if (ret)
return ret;

/* 等待测量完成 (最长10ms) */
msleep(10);

/* 读取原始数据 */
ret = regmap_bulk_read(data->regmap, 0xF7, raw_data, 6);
if (ret)
return ret;

/* 简化的温度计算 */
int adc_T = (raw_data[0] << 12) | (raw_data[1] << 4) | (raw_data[2] >> 4);
int var1, var2;
var1 = ((((adc_T >> 3) – ((int)0x9000 << 1))) *
((int)0x8C00)) >> 11;
var2 = (((((adc_T >> 4) – ((int)0x9000)) *
((adc_T >> 4) – ((int)0x9000))) >> 12) *
((int)0x8000)) >> 14;
data->t_fine = var1 + var2;
*temperature = (data->t_fine * 5 + 128) >> 8;

if (pressure) {
int adc_P = (raw_data[3] << 12) | (raw_data[4] << 4) |
(raw_data[5] >> 4);
*pressure = 0; /* 简化处理 */
}

return 0;
}

/* ===== 第二部分: Runtime PM 回调 ===== */

static int bmp280_runtime_suspend(struct device *dev)
{
/*
* Runtime Suspend: 设备空闲时挂起
* 关闭测量模式,进入休眠状态
*/

struct bmp280_data *data = dev_get_drvdata(dev);
int ret;

dev_dbg(dev, "Runtime PM: entering suspend\\n");

mutex_lock(&data->lock);

/* 读取并缓存当前寄存器状态 */
ret = regmap_read(data->regmap, BMP280_REG_CTRL_MEAS,
(unsigned int *)&data->ctrl_meas_cache);
if (ret) {
mutex_unlock(&data->lock);
return ret;
}

ret = regmap_read(data->regmap, BMP280_REG_CONFIG,
(unsigned int *)&data->config_cache);
if (ret) {
mutex_unlock(&data->lock);
return ret;
}

/* 切换到 Sleep 模式 */
ret = bmp280_set_mode(data, BMP280_MODE_SLEEP);

data->suspended = true;

mutex_unlock(&data->lock);
return ret;
}

static int bmp280_runtime_resume(struct device *dev)
{
/*
* Runtime Resume: 有新的读写请求,唤醒设备
* 从缓存恢复配置
*/

struct bmp280_data *data = dev_get_drvdata(dev);
int ret;

dev_dbg(dev, "Runtime PM: resuming from suspend\\n");

mutex_lock(&data->lock);

/* 恢复配置寄存器 */
ret = regmap_write(data->regmap, BMP280_REG_CONFIG,
data->config_cache);
if (ret)
goto out;

/* 恢复测量控制寄存器 */
ret = regmap_write(data->regmap, BMP280_REG_CTRL_MEAS,
data->ctrl_meas_cache);
if (ret)
goto out;

data->suspended = false;

out:
mutex_unlock(&data->lock);
return ret;
}

static int bmp280_runtime_idle(struct device *dev)
{
/*
* Runtime Idle: 设备空闲检测回调
* 返回 0 触发 runtime_suspend
* 返回 -EBUSY 取消挂起
*/
struct bmp280_data *data = dev_get_drvdata(dev);

/* 检查是否有未完成的测量 */
unsigned int status;
int ret = regmap_read(data->regmap, BMP280_REG_STATUS, &status);
if (ret == 0 && (status & 0x08)) {
/* 测量进行中,不挂起 */
return -EBUSY;
}

return 0; /* 允许挂起 */
}

/* ===== 第三部分: System PM 回调 ===== */

static int bmp280_suspend(struct device *dev)
{
/*
* System Suspend: 系统即将休眠
* 必须在 Runtime PM 状态之上做系统休眠处理
*/

struct bmp280_data *data = dev_get_drvdata(dev);
int ret;

dev_info(dev, "System PM: entering suspend\\n");

/*
* 关键:先判断 Runtime PM 状态
* 如果设备已经在 runtime_suspend 状态,
* 无需重复挂起,只需确保系统休眠时设备仍处于低功耗
*/
if (pm_runtime_suspended(dev)) {
dev_dbg(dev, "System PM: device already runtime-suspended\\n");
return 0;
}

/* 设备当前活跃,需要手动挂起 */
mutex_lock(&data->lock);

/* 保存完整状态 */
ret = regmap_read(data->regmap, BMP280_REG_CTRL_MEAS,
(unsigned int *)&data->ctrl_meas_cache);
if (ret)
goto err;

ret = regmap_read(data->regmap, BMP280_REG_CONFIG,
(unsigned int *)&data->config_cache);
if (ret)
goto err;

/* 进入 Sleep 模式 */
ret = bmp280_set_mode(data, BMP280_MODE_SLEEP);
if (ret)
goto err;

data->suspended = true;

err:
mutex_unlock(&data->lock);
return ret;
}

static int bmp280_resume(struct device *dev)
{
/*
* System Resume: 系统从休眠中恢复
* 必须先处理 Runtime PM 层级,再恢复设备状态
*/

struct bmp280_data *data = dev_get_drvdata(dev);
int ret;

dev_info(dev, "System PM: resuming from suspend\\n");

/*
* 如果设备在系统休眠前已经 runtime_suspended,
* 不需要恢复设备状态——让 Runtime PM 在设备再次
* 被使用时自行唤醒
*/
if (pm_runtime_suspended(dev)) {
dev_dbg(dev, "System PM: device was runtime-suspended, skip restore\\n");
return 0;
}

mutex_lock(&data->lock);

/* 软件复位,确保芯片处于已知状态 */
ret = bmp280_soft_reset(data);
if (ret) {
dev_err(dev, "System PM: soft reset failed: %d\\n", ret);
goto err;
}

/* 恢复配置寄存器 */
ret = regmap_write(data->regmap, BMP280_REG_CONFIG,
data->config_cache);
if (ret)
goto err;

/* 恢复测量控制寄存器 */
ret = regmap_write(data->regmap, BMP280_REG_CTRL_MEAS,
data->ctrl_meas_cache);
if (ret)
goto err;

data->suspended = false;

err:
mutex_unlock(&data->lock);
return ret;
}

/* ===== 第四部分: PM Ops 结构与 sysfs 接口 ===== */

static const struct dev_pm_ops bmp280_pm_ops = {
/*
* SET_SYSTEM_SLEEP_PM_OPS: 将 System PM 和 Runtime PM
* 统一在同一个 dev_pm_ops 结构中。
*
* 此宏会自动处理 pm_runtime_force_suspend/resume ——
* 当 Runtime PM 已挂起设备时,System Suspend 不再重复挂起。
*/
SET_SYSTEM_SLEEP_PM_OPS(bmp280_suspend, bmp280_resume)

/*
* SET_RUNTIME_PM_OPS: 注册 Runtime PM 回调
*/
SET_RUNTIME_PM_OPS(bmp280_runtime_suspend, bmp280_runtime_resume,
bmp280_runtime_idle)
};

static int bmp280_i2c_probe(struct i2c_client *client)
{
struct bmp280_data *data;
struct device *dev = &client->dev;
int ret;

data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;

data->client = client;
mutex_init(&data->lock);

/* 初始化 Runtime PM */
pm_runtime_set_active(dev);
pm_runtime_enable(dev);

/*
* 设置自动挂起延迟: 设备最后一次使用后,
* 等待 200ms 触发 runtime_suspend
*/
pm_runtime_set_autosuspend_delay(dev, 200);
pm_runtime_use_autosuspend(dev);

/* 获取 Runtime PM 引用,初始状态保持活跃 */
pm_runtime_get_sync(dev);

/* 初始化硬件 */
ret = bmp280_soft_reset(data);
if (ret)
goto err_pm;

dev_set_drvdata(dev, data);

/* 释放 Runtime PM 引用,允许自动挂起 */
pm_runtime_mark_last_busy(dev);
pm_runtime_put_autosuspend(dev);

dev_info(dev, "BMP280 driver probed with PM support\\n");
return 0;

err_pm:
pm_runtime_put_noidle(dev);
pm_runtime_disable(dev);
return ret;
}

static void bmp280_i2c_remove(struct i2c_client *client)
{
struct device *dev = &client->dev;

/* Runtime PM clean up */
pm_runtime_get_sync(dev); /* 唤醒设备 */
pm_runtime_put_noidle(dev);
pm_runtime_disable(dev);
}

/* 在 sysfs 中提供电源属性 */
static DEVICE_ATTR_RO(pm_status);

static ssize_t pm_status_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct bmp280_data *data = dev_get_drvdata(dev);

return sprintf(buf,
"Mode: %s\\n"
"Runtime Status: %s\\n"
"System Suspended: %s\\n",
data->ctrl_meas_cache == BMP280_MODE_NORMAL ? "NORMAL" :
data->ctrl_meas_cache == BMP280_MODE_FORCED ? "FORCED" : "SLEEP",
pm_runtime_suspended(dev) ? "suspended" : "active",
data->suspended ? "yes" : "no");
}

/* I2C 驱动注册 */
static const struct i2c_device_id bmp280_id[] = {
{ "bmp280", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, bmp280_id);

static struct i2c_driver bmp280_driver = {
.driver = {
.name = "bmp280",
.pm = &bmp280_pm_ops,
},
.probe = bmp280_i2c_probe,
.remove = bmp280_i2c_remove,
.id_table = bmp280_id,
};
module_i2c_driver(bmp280_driver);

MODULE_AUTHOR("PM Framework Demo");
MODULE_DESCRIPTION("BMP280 with Runtime PM and System PM support");
MODULE_LICENSE("GPL v2");

核心设计决策说明:

  • pm_runtime_suspended(dev) 检查:在System Suspend回调中首先检查Runtime PM是否已挂起设备。如果已挂起,跳过重复的寄存器保存操作——这是避免重复操作的性能优化,也是防止PM交互Bug的关键检查。

  • pm_runtime_mark_last_busy 和 pm_runtime_put_autosuspend:这两者配合使用实现自动延迟挂起。mark_last_busy更新设备活跃时间戳,put_autosuspend在引用计数归零后延迟200ms再触发runtime_suspend。这样可以避免频繁的挂起/恢复抖动。

  • 恢复时的软复位(Soft Reset):System Resume时先做软复位确保芯片处于已知状态,再恢复缓存配置。这是因为系统休眠期间芯片可能断电,上电后的寄存器状态不可预测。

  • 四、Runtime PM与System Suspend交互的常见陷阱与解决方案

    四种最常见的PM交互Bug及其解决方案:

    陷阱1:System Suspend时Runtime PM正在挂起/恢复

    系统休眠请求可能在任何Runtime PM操作的中途到来。如果在runtime_suspend()执行到一半时收到System Suspend通知,设备的I2C总线可能正在被关闭。

    解决方案:使用 pm_runtime_get_noresume()/pm_runtime_put_noidle() API对。在System Suspend开始时调用pm_runtime_barrier()等待所有未完成的Runtime PM操作完成:

    static int bmp280_suspend_safe(struct device *dev)
    {
    /* 等待所有未完成的 Runtime PM 操作 */
    pm_runtime_barrier(dev);

    /* 现在安全地执行 System Suspend 操作 */

    }

    陷阱2:System Suspend后设备被自动Resume

    System Suspend完成后,内核可能触发设备的runtime_resume(),导致系统休眠期间设备被异常唤醒。

    解决方案:在System Suspend中调用 pm_runtime_disable() 禁用Runtime PM,Resume完成后再重新启用。但需注意:pm_runtime_disable()会等待所有进行中的操作完成,可能导致死锁。实践中使用 SET_SYSTEM_SLEEP_PM_OPS 宏让内核PM Core层自动处理。

    陷阱3:配置丢失与时钟/Power域状态不一致

    System Suspend后,PMIC可能关掉了设备的电源域或时钟。Resume时如果只恢复寄存器而忘记重新使能时钟,设备无法正常操作。

    解决方案:在Resume回调中先调用clk_prepare_enable(),再写寄存器。且时钟操作必须在寄存器操作之前——这是PM领域的基础法则。

    陷阱4:调试困难——挂起失败不报错

    设备挂起失败时,dpm_suspend不会直接返回错误——它记录警告并继续处理下一个设备。导致系统"似乎"成功休眠但某些设备异常。

    解决方案:启用PM Debug:

    # 内核启动参数
    echo 1 > /sys/power/pm_print_times # 打印各设备挂起/恢复耗时
    echo 1 > /sys/power/pm_debug_messages # 启用PM调试信息

    # 或使用内核启动参数
    # no_console_suspend ignore_loglevel pm_debug_messages

    五、总结

    Linux电源管理框架的核心是双层PM模型:Runtime PM处理设备级别的空闲挂起,System Suspend处理系统级别的全局休眠。两者的协同依靠pm_runtime_force_suspend/resume和SET_SYSTEM_SLEEP_PM_OPS宏自动处理大部分交互。

    设备驱动适配PM的关键点:

  • 状态缓存是必需的:Runtime PM和System PM都需要缓存设备配置,作为Resume时的恢复基准
  • System PM回调需感知Runtime PM状态:pm_runtime_suspended()判断避免重复操作
  • autosuspend延迟需合理设置:太短导致频繁挂起抖动,太长失去省电效果;传感器类设备200-500ms,网络设备500-1000ms是合理范围
  • 调试先行于优化:先确保PM功能正确,再优化挂起/恢复延迟;使用pm_print_times和no_console_suspend辅助调试
  • 时钟与电源域的顺序律:Resume时先恢复电源域→时钟→寄存器;Suspend时顺序相反
  • PM驱动的Bug往往不会立即表现为系统崩溃,而是以"设备的偶发性挂起失败"或"恢复后数据丢失"的形式出现。排查这类问题时,dmesg中的PM相关日志和设备树中的Power Domain/Clock绑定是最重要的线索。

    赞(0)
    未经允许不得转载:171主机测评 » Linux电源管理框架深度解析:Runtime_PM与System_Suspend的驱动端适配方法
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址