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的关键点:
PM驱动的Bug往往不会立即表现为系统崩溃,而是以"设备的偶发性挂起失败"或"恢复后数据丢失"的形式出现。排查这类问题时,dmesg中的PM相关日志和设备树中的Power Domain/Clock绑定是最重要的线索。

