ESC
输入关键词搜索文章标题和内容

电源管理框架:从系统休眠到运行时PM的完整实战

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

电源管理框架:从系统休眠到运行时PM的完整实战

嵌入式设备跑Linux,功耗控制是绕不开的课题。手机待机一晚上掉电5%还是30%,笔记本合盖后能不能真省电,工业网关空闲时CPU温度能不能降下来——全靠电源管理框架。Linux把电源管理拆成两套机制:系统休眠管整机状态切换,运行时PM管单个设备的按需开关。两套机制独立运作,但驱动回调有交叉。本文基于Linux 6.8内核,从架构讲起,过一遍Suspend/Resume流程和Runtime PM引用计数模型,再覆盖电源域、调试手段和常见坑点。

一、电源管理架构

Linux电源管理分四层,从用户空间到硬件逐级传递:

flowchart TB USR["用户空间<br/>pm-utils / systemd-logind"] -->|"写/sys/power/state"| KPM["内核PM核心<br/>kernel/power/"] KPM -->|"调用 dev_pm_ops 回调"| DRV["驱动PM回调<br/>suspend / resume / runtime_*"] DRV -->|"操作寄存器/时钟/电源"| HW["硬件电源控制<br/>CLK / Regulator / PSCI"] style USR fill:#E3F2FD,stroke:#1565C0 style KPM fill:#FFF8E1,stroke:#EF6C00 style DRV fill:#E8F5E9,stroke:#2E7D32 style HW fill:#F3E5F5,stroke:#7B1FA2

用户空间通过sysfs节点触发系统休眠,内核PM核心协调各子系统的冻结和恢复,驱动层实现具体的硬件操作,最底层是时钟框架、regulator框架和PSCI固件接口。
两大核心机制:
| 机制 | 粒度 | 触发方式 | 典型场景 |
|:-----|:-----|:-------|:---------|
| 系统休眠(Suspend/Resume) | 整机 | 用户空间主动触发 | 手机息屏、笔记本合盖 |
| 运行时PM(Runtime PM) | 单设备 | 内核自动(引用计数驱动) | 传感器空闲关时钟、GPU无任务降频 |

两套机制正交:系统休眠时Runtime PM的状态会被保存,唤醒后恢复。驱动可以只实现其中一套,也可以两套都实现。

二、系统休眠(Suspend/Resume)

休眠状态

ACPI定义了S0~S5六个状态,嵌入式平台不一定全部实现:

状态 名称 CPU 内存 外设 功能 唤醒延迟
S0 运行 运行 保持 工作 最低 无延迟
S1 待机(Standby) 停止时钟 保持 部分关闭 较低 极快(~10ms)
S2 浅睡 断电 保持 关闭 较低 (~50ms)
S3 挂起到内存(Suspend to RAM) 断电 保持(自刷新) 关闭 低 (~100ms)
S4 挂起到磁盘(Hibernation) 断电 内容写入磁盘 关闭 极低 (~1s)
S5 软关机 断电 断电 断电 近零 需重启

嵌入式平台最常用S3(echo mem > /sys/power/state),S1和S2很多SoC不实现。S4在嵌入式上少见,桌面和笔记本用得多。S5就是关机,不算真正的休眠。

休眠流程

flowchart TB A(["用户触发休眠<br/>echo mem > /sys/power/state"]) --> B["冻结用户空间进程<br/>freeze_processes()"] B --> C["同步文件系统<br/>sync()"] C --> D["冻结内核线程<br/>freeze_kernel_threads()"] D --> E["通知PM notifier"] E --> F["逐设备调用suspend 回调"] F --> G["逐设备调用suspend_late 回调"] G --> H["关闭非启动CPU<br/>disable_nonboot_cpus()"] H --> I["关闭中断<br/>local_irq_disable()"] I --> J["进入低功耗状态<br/>SoC特定实现"] style A fill:#E3F2FD,stroke:#1565C0 style B fill:#FFF8E1,stroke:#EF6C00 style F fill:#E8F5E9,stroke:#2E7D32 style J fill:#F3E5F5,stroke:#7B1FA2

关键步骤说明:
1. 冻结进程:用户进程先冻,内核线程后冻结。冻结方式是设置PF_FROZEN标志,进程调度时检查该标志,发现被冻就主动睡眠
2. 同步文件系统:脏页写回磁盘,防止休眠后掉电丢数据
3. 逐设备suspend:按设备树从叶子到根的顺序调用,先挂起子设备再挂起父设备。这样保证父设备(比如I2C控制器)在子设备(I2C从设备)之后才关
4. 关闭中断:最后一步,关中断后系统不再响应外部事件,只等唤醒源

唤醒流程

flowchart TB A(["唤醒中断触发<br/>RTC / GPIO / 电源键"]) --> B["恢复中断<br/>local_irq_enable()"] B --> C["启动非启动CPU<br/>enable_nonboot_cpus()"] C --> D["逐设备调用resume_early 回调"] D --> E["逐设备调用resume 回调"] E --> F["解冻内核线程"] F --> G["解冻用户空间进程"] G --> H(["系统恢复正常运行"]) style A fill:#FFEBEE,stroke:#C62828 style D fill:#E8F5E9,stroke:#2E7D32 style H fill:#E3F2FD,stroke:#1565C0

唤醒是休眠的逆过程,但有个区别得注意:唤醒中断必须提前注册。休眠前内核会检查哪些中断是唤醒源(enable_irq_wake()),只有这些中断能唤醒系统。

驱动回调

驱动通过dev_pm_ops结构体注册回调:

// include/linux/pm.h
struct dev_pm_ops {
    int (*suspend)(struct device *dev);
    int (*resume)(struct device *dev);
    int (*suspend_late)(struct device *dev);
    int (*resume_early)(struct device *dev);
    int (*freeze)(struct device *dev);        // hibernation。    int (*thaw)(struct device *dev);           // hibernation。    int (*poweroff)(struct device *dev);       // hibernation。    int (*restore)(struct device *dev);        // hibernation。    // Runtime PM回调
    int (*runtime_suspend)(struct device *dev);
    int (*runtime_resume)(struct device *dev);
    int (*runtime_idle)(struct device *dev);
};

回调调用顺序和适用场景:
| 回调 | 调用时机 | 典型操作 |
|:-----|:---------|:---------|
| suspend | 休眠时,中断还开着 | 保存寄存器、关闭时钟、通知子设备 |
| suspend_late | 休眠时,中断已关 | 关闭最后遗留的硬件资源 |
| resume_early | 唤醒时,中断还没开 | 恢复最基础的硬件状态 |
| resume | 唤醒时,中断已开 | 恢复寄存器、重新初始化硬件 |

大多数驱动只实现suspend和resume就够了。suspend_late/resume_early是给需要在中断关闭。开启前做操作的驱动用的,比如中断控制器自己。

完整代码示例:Platform驱动的suspend/resume

// my_sensor_drv.c - 带PM回调的Platform驱动
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pm.h>
#include <linux/clk.h>
#include <linux/regulator/consumer.h>
#include <linux/io.h>

struct my_sensor_data {
    void __iomem *base;
    struct clk *clk;
    struct regulator *vdd;
    u32 saved_ctrl;    // 休眠前保存控制寄存器
    u32 saved_cfg;     // 休眠前保存配置寄存器
};

static int my_sensor_suspend(struct device *dev)
{
    struct my_sensor_data *data = dev_get_drvdata(dev);

    // 保存寄存器,唤醒后恢。    data->saved_ctrl = readl(data->base + CTRL_REG);
    data->saved_cfg  = readl(data->base + CFG_REG);

    // 关闭时钟
    clk_disable_unprepare(data->clk);

    // 关闭电源(如果设备有独立供电。    if (data->vdd)
        regulator_disable(data->vdd);

    dev_info(dev, "suspended\n");
    return 0;
}

static int my_sensor_resume(struct device *dev)
{
    struct my_sensor_data *data = dev_get_drvdata(dev);
    int ret;

    // 恢复电源
    if (data->vdd) {
        ret = regulator_enable(data->vdd);
        if (ret) {
            dev_err(dev, "failed to enable vdd: %d\n", ret);
            return ret;
        }
    }

    // 恢复时钟
    ret = clk_prepare_enable(data->clk);
    if (ret) {
        dev_err(dev, "failed to enable clk: %d\n", ret);
        return ret;
    }

    // 恢复寄存。    writel(data->saved_ctrl, data->base + CTRL_REG);
    writel(data->saved_cfg,  data->base + CFG_REG);

    dev_info(dev, "resumed\n");
    return 0;
}

// 用SET_SYSTEM_SLEEP_PM_OPS宏简化定。static const struct dev_pm_ops my_sensor_pm_ops = {
    SET_SYSTEM_SLEEP_PM_OPS(my_sensor_suspend, my_sensor_resume)
};

static int my_sensor_probe(struct platform_device *pdev)
{
    struct my_sensor_data *data;
    struct resource *res;

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

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    data->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(data->base))
        return PTR_ERR(data->base);

    data->clk = devm_clk_get(&pdev->dev, NULL);
    if (IS_ERR(data->clk))
        return PTR_ERR(data->clk);

    data->vdd = devm_regulator_get_optional(&pdev->dev, "vdd");
    if (IS_ERR(data->vdd)) {
        if (PTR_ERR(data->vdd) != -ENODEV)
            return PTR_ERR(data->vdd);
        data->vdd = NULL;  // 没有独立供电,不影响
    }

    platform_set_drvdata(pdev, data);

    return clk_prepare_enable(data->clk);
}

static void my_sensor_remove(struct platform_device *pdev)
{
    struct my_sensor_data *data = platform_get_drvdata(pdev);

    clk_disable_unprepare(data->clk);
}

static const struct of_device_id my_sensor_of_match[] = {
    { .compatible = "myvendor,my-sensor" },
    { }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);

static struct platform_driver my_sensor_driver = {
    .probe  = my_sensor_probe,
    .remove = my_sensor_remove,
    .driver = {
        .name           = "my-sensor",
        .of_match_table = my_sensor_of_match,
        .pm             = &my_sensor_pm_ops,  // 挂载PM操作
    },
};
module_platform_driver(my_sensor_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Sensor driver with suspend/resume support");

SET_SYSTEM_SLEEP_PM_OPS宏展开后给suspend和resume赋值,freeze/thaw/poweroff/restore设为NULL。如果驱动要支持hibernation,光靠这个宏不够,得手动填freeze和thaw。

三、运行时电源管理(Runtime PM)

系统休眠。一刀。——整台机器一起睡一起醒。但很多时候只需要让某个空闲设备省电,其他设备照常工作。Runtime PM就是干这个的:设备不用时自动挂起,要用时自动恢复,驱动只需要维护引用计数。

引用计数模型

Runtime PM的核心是一个引用计数器dev->power.usage_count。

flowchart TB GET["pm_runtime_get_sync()<br/>usage_count++"] -->|"count > 0"| ACT["RPM_ACTIVE<br/>设备工作"] PUT["pm_runtime_put_sync()<br/>usage_count--"] -->|"count == 0"| IDLE["runtime_idle()"] IDLE -->|"自动调用"| SUSP["runtime_suspend()<br/>设备挂起"] ACT -.->|"不再使用"| PUT SUSP -.->|"需要使用 | GET style GET fill:#E8F5E9,stroke:#2E7D32 style PUT fill:#FFF8E1,stroke:#EF6C00 style ACT fill:#E3F2FD,stroke:#1565C0 style SUSP fill:#FFEBEE,stroke:#C62828
API 作用 说明
pm_runtime_get_sync(dev) usage_count++,唤醒设备 同步等待resume完成,返。成功
pm_runtime_put_sync(dev) usage_count--,可能挂起 count归零时触发runtime_idle→runtime_suspend
pm_runtime_get_noresume(dev) usage_count++,不唤醒 只加计数,不触发resume
pm_runtime_put_noidle(dev) usage_count--,不挂起 只减计数,不触发suspend
pm_runtime_set_active(dev) 标记设备为active probe时调用,告诉PM核心设备已就绪
pm_runtime_set_suspended(dev) 标记设备为suspended 设备初始状态为挂起时使用

autosuspend:延迟挂。

设备刚idle又立刻被访问是常见场景——传感器读完一个数据,过几毫秒又来一个请求。如果每次put都立刻suspend,下次get又要resume,频繁开关反而更费电。autosuspend就是加个延迟:put后等一段时间,如果期间没有get请求,才真正suspend。

// 设置autosuspend延迟。00ms
pm_runtime_set_autosuspend_delay(dev, 200);

// 启用autosuspend
pm_runtime_use_autosuspend(dev);

// 用autosuspend版本的put
pm_runtime_put_sync_autosuspend(dev);  // 200ms内没有get才真正suspend

autosuspend的延迟值需要根据实际场景调。太短没效果,太长又浪费电。一般传感器100~500ms,GPU这类重型设备可以。~2秒。

完整代码示例:I2C传感器的Runtime PM

// my_i2c_sensor.c - 带Runtime PM的I2C传感器驱动#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/pm_runtime.h>
#include <linux/delay.h>

struct my_i2c_sensor {
    struct i2c_client *client;
    struct mutex lock;
};

// 读传感器数据前先唤醒设备
static int my_i2c_sensor_read(struct my_i2c_sensor *sensor, u8 reg, u8 *val)
{
    struct device *dev = &sensor->client->dev;
    int ret;

    // 唤醒设备,usage_count++
    ret = pm_runtime_get_sync(dev);
    if (ret < 0) {
        pm_runtime_put_noidle(dev);  // get失败也要减计数        return ret;
    }

    mutex_lock(&sensor->lock);
    ret = i2c_smbus_read_byte_data(sensor->client, reg);
    mutex_unlock(&sensor->lock);

    if (ret >= 0)
        *val = (u8)ret;

    // 用完放回,usage_count--。00ms后自动suspend
    pm_runtime_put_sync_autosuspend(dev);

    return ret < 0 ? ret : 0;
}

static int my_i2c_sensor_runtime_suspend(struct device *dev)
{
    struct i2c_client *client = to_i2c_client(dev);

    // 进入低功耗模式,传感器停止采。    i2c_smbus_write_byte_data(client, 0x10, 0x00);

    dev_dbg(dev, "runtime suspended\n");
    return 0;
}

static int my_i2c_sensor_runtime_resume(struct device *dev)
{
    struct i2c_client *client = to_i2c_client(dev);

    // 退出低功耗模式,恢复采样
    i2c_smbus_write_byte_data(client, 0x10, 0x01);
    // 等待传感器稳。    msleep(10);

    dev_dbg(dev, "runtime resumed\n");
    return 0;
}

static int my_i2c_sensor_runtime_idle(struct device *dev)
{
    // idle回调可选,返回0表示让PM核心继续走suspend流程
    // 返回调表示阻止suspend
    return 0;
}

static const struct dev_pm_ops my_i2c_sensor_pm_ops = {
    SET_RUNTIME_PM_OPS(my_i2c_sensor_runtime_suspend,
                       my_i2c_sensor_runtime_resume,
                       my_i2c_sensor_runtime_idle)
    // 系统休眠时也要处。    SET_SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend,
                            pm_runtime_force_resume)
};

static int my_i2c_sensor_probe(struct i2c_client *client)
{
    struct device *dev = &client->dev;
    struct my_i2c_sensor *sensor;
    int ret;

    if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA))
        return -EIO;

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

    sensor->client = client;
    mutex_init(&sensor->lock);
    i2c_set_clientdata(client, sensor);

    // 初始化Runtime PM
    pm_runtime_set_active(dev);
    pm_runtime_set_autosuspend_delay(dev, 200);  // 200ms延迟挂起
    pm_runtime_use_autosuspend(dev);
    pm_runtime_enable(dev);

    // probe期间设备是active的,probe结束后可以put让它idle
    pm_runtime_put_sync_autosuspend(dev);

    return 0;
}

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

    // remove时确保设备active,禁用Runtime PM
    pm_runtime_get_sync(dev);
    pm_runtime_disable(dev);
    pm_runtime_set_suspended(dev);
}

static const struct of_device_id my_i2c_sensor_of_match[] = {
    { .compatible = "myvendor,i2c-sensor" },
    { }
};
MODULE_DEVICE_TABLE(of, my_i2c_sensor_of_match);

static struct i2c_driver my_i2c_sensor_driver = {
    .probe = my_i2c_sensor_probe,
    .remove = my_i2c_sensor_remove,
    .driver = {
        .name           = "my-i2c-sensor",
        .of_match_table = my_i2c_sensor_of_match,
        .pm             = &my_i2c_sensor_pm_ops,
    },
};
module_i2c_driver(my_i2c_sensor_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("I2C sensor driver with Runtime PM");

几个要点。
1. pm_runtime_force_suspend/pm_runtime_force_resume。.8内核提供的辅助函数,系统休眠时自动调用Runtime PM的suspend/resume回调,避免写两套逻辑
2. pm_runtime_get_sync失败时必须调用pm_runtime_put_noidle,否则usage_count会泄。3. probe末尾调用pm_runtime_put_sync_autosuspend让设备进入idle状态,否则设备一直active,Runtime PM形同虚设
4. remove时先pm_runtime_get_sync确保设备active,再pm_runtime_disable禁用Runtime PM

四、电源域(Power Domain)

genpd框架

SoC上多个设备经常共享同一路电源。比如视频编解码器和显示控制器挂在同一个电源域上,任何一个设备在工作,这个电源域就不能关。Linux用generic power domain(genpd)框架管理这种共享关系。

flowchart TB PD["电源。A<br/>VDD_VIDEO"] --> D1["设备1: VPU"] PD --> D2["设备2: Display"] PD --> D3["设备3: ISP"] PD2["电源。B<br/>VDD_GPU"] --> D4["设备4: GPU"] subgraph genpd核心 PD PD2 end PD -->|"所有设备suspended"| OFF["pd->power_off()<br/>关闭VDD_VIDEO"] PD -->|"任一设备active"| ON["pd->power_on()<br/>打开VDD_VIDEO"] style PD fill:#FFF8E1,stroke:#EF6C00 style PD2 fill:#FFF8E1,stroke:#EF6C00 style OFF fill:#FFEBEE,stroke:#C62828 style ON fill:#E8F5E9,stroke:#2E7D32

genpd内部维护每个电源域的引用计数。域下任何一个设备active,整个域就on;所有设备都suspended,域才off。驱动不用手动操作电源域,Runtime PM和系统休眠框架会自动和genpd交互。

来自 linuxros.cn · linuxROS

电源域驱动核心结。

// include/linux/pm_domain.h
struct generic_pm_domain {
    const char *name;                    // 电源域名。    int (*power_off)(struct generic_pm_domain *pd);
    int (*power_on)(struct generic_pm_domain *pd);
    unsigned int device_count;           // 域下设备。    unsigned int suspended_count;        // 已挂起设备数
    // ...锁、链表等内部字段
};

驱动需要实现power_on和power_off,里面写具体的硬件操作——通常是写SoC的PMU寄存器来开关某路电源。

设备树绑。

// 设备树中声明电源。power-domains {
    pd_video: power-domain@0 {
        compatible = "myvendor,power-domain";
        reg = <0 0x10>;          // PMU寄存器偏。        #power-domain-cells = <0>;
    };

    pd_gpu: power-domain@1 {
        compatible = "myvendor,power-domain";
        reg = <1 0x20>;
        #power-domain-cells = <0>;
    };
};

// 设备引用电源。&vpu {
    power-domains = <&pd_video>;
};

&display {
    power-domains = <&pd_video>;  // 和VPU共享同一个域
};

&gpu {
    power-domains = <&pd_gpu>;
};

设备树里power-domains属性指向电源域节点,内核解析后自动把设备挂到对应域下。驱动不用写额外代码,Runtime PM框架在suspend/resume时会自动通知genpd更新引用计数。

何时需要写电源域驱动

不是所有平台都需要自己写电源域驱动。判断标准:

场景 是否需要写 原因
SoC厂商已提供genpd驱动(如RK、NXP BSP 不需要 直接在设备树里引用即
自研SoC或FPGA平台 需要 没人帮你写,得自己实现power_on/power_off
多设备共享电源且需要精细控制 需 标准regulator框架不够
设备有独立供电,不共享 不需 用regulator框架就行

Rockchip、NXP这些厂商的BSP里一般都有现成的genpd驱动,设备树配好power-domains属性就能用。自研平台才需要从头写。

五、系统休眠操。

手动触发休眠

# 挂起到内存S3)——最常用
echo mem > /sys/power/state

# 挂起到磁盘(S4)
echo disk > /sys/power/state

# 待机(S1)——部分平台支。echo freeze > /sys/power/state

# 查看当前支持的状态cat /sys/power/state
# 输出示例: freeze mem disk

freeze是内存.9引入的轻量级休眠,只冻结进程和设备,不关CPU和内存,唤醒最快但省电最少。适合桌面系统"合盖即走"的场景:

pm-test模式

内核提供PM测试模式,可以在不真正休眠的情况下验证驱动的suspend/resume回调是否正确。

# 查看可用测试级别
cat /sys/power/pm_test

# 输出: [none] core processors platform devices freezer

# 设置测试级别
echo devices > /sys/power/pm_test

# 触发休眠——只会走suspend/resume流程,不会真正断。echo mem > /sys/power/state
测试级别 测试内容 适用场景
none 真正休眠 最终验
core 核心PM子系 验证时钟、中断控制器
processors CPU热插 验证多核冻结/恢复
platform 平台相关回调 验证SoC特定PM操作
devices 设备suspend/resume 验证驱动回调
freezer 进程冻结 验证进程冻结/解冻

开发阶段用devices级别测试驱动回调,不用等真机休眠那么久。

RTC唤醒

调试休眠流程时,RTC唤醒比按电源键方便——可以设定自动唤醒时间:

# 清除之前的闹。echo 0 > /sys/class/rtc/rtc0/wakealarm

# 设置5秒后唤醒
echo +5 > /sys/class/rtc/rtc0/wakealarm

# 触发休眠。秒后自动唤醒
echo mem > /sys/power/state

RTC唤醒的前提是RTC驱动支持唤醒功能,且设备树里RTC节点配置了wakeup-source属性。

六、电源管理调试

/sys/power/目录

# 查看当前PM状态cat /sys/power/state

# 查看上次休眠成功/失败
cat /sys/power/suspend_stats/success
cat /sys/power/suspend_stats/fail

# 查看哪个设备导致休眠失败
cat /sys/power/suspend_stats/last_failed_dev

# 查看休眠耗时
cat /sys/power/suspend_stats/last_hardware

# Runtime PM状态cat /sys/devices/platform/soc/xxx/power/runtime_status
# 输出: active / suspended / unsupported

pm_trace:追踪休眠卡住位。

休眠卡死是最头疼的问题——系统冻住了,没任何输出,不知道卡在哪个驱动。pm_trace就是干这事的。

# 开启pm_trace
echo 1 > /sys/power/pm_trace

# 触发休眠
echo mem > /sys/power/state

# 如果卡死,重启后查看
# pm_trace把设备哈希写入RTC寄存器,重启后能读出。dmesg | grep "hash matches"

输出类似。

PM: hash matches drivers/clk/clk.c:245

这就告诉你卡在时钟框架的某个位置,排查范围一下就小了。

CONFIG_PM_DEBUG

内核配置选项,开启后提供更多PM调试信息。

CONFIG_PM_DEBUG=y           # PM调试总开销CONFIG_PM_SLEEP_DEBUG=y     # 休眠调试信息
CONFIG_PM_TRACE=y           # pm_trace支持
CONFIG_PM_TRACE_RTC=y       # pm_trace写RTC寄存```

开启`CONFIG_PM_SLEEP_DEBUG`后,`/sys/power/`下会多出`suspend_stats`的详细信息和每个设备的PM耗时统计数
### ftrace跟踪PM事件

```bash
# 使能PM相关tracepoint
echo 1 > /sys/kernel/debug/tracing/events/power/cpu_frequency/enable
echo 1 > /sys/kernel/debug/tracing/events/power/cpu_idle/enable
echo 1 > /sys/kernel/debug/tracing/events/power/pm_qos/enable

# 使能设备suspend/resume trace
echo 1 > /sys/kernel/debug/tracing/events/device/dev_suspend/enable
echo 1 > /sys/kernel/debug/tracing/events/device/dev_resume/enable

# 抓取trace
echo > /sys/kernel/debug/tracing/trace
echo mem > /sys/power/state
cat /sys/kernel/debug/tracing/trace

ftrace能记录每个设备的suspend/resume耗时,快速定位哪个驱动拖慢了休眠。

七、常见问题

Q1:休眠后无法唤醒。

第一步查唤醒源。不是所有中断都能唤醒系统,只有通过enable_irq_wake()注册的中断才行。检查设备树里有没有配wakeup-source。

&gpio_keys {
    wakeup-source;   // 声明该设备可以唤醒系。};

第二步查驱动的resume回调。resume回调里如果操作了还没恢复的时钟或电源,会直接panic。resume顺序是先子后父,子设备的resume如果依赖父设备,得确保父设备先恢复。
第三步查regulator。有些电源域在休眠时被关了,唤醒后驱动得重新使能regulator。

Q2:Runtime PM导致设备不可用?

这是最常见的坑——设备已经suspended,驱动直接去读写寄存器,拿到00xFF。规则就一条:访问硬件前必须pm_runtime_get_sync。

// 错误:直接读寄存器,设备可能已suspended
val = readl(data->base + DATA_REG);

// 正确:先唤醒再读
pm_runtime_get_sync(dev);
val = readl(data->base + DATA_REG);
pm_runtime_put_sync_autosuspend(dev);

中断处理函数里也要注意。设备suspended时来了中断,需要在中断里先resume设备再处理数据:

static irqreturn_t my_isr(int irq, void *dev_id)
{
    struct device *dev = dev_id;

    // 中断上下文不能用pm_runtime_get_sync(会睡眠。    // 用pm_runtime_get_if_in_use检查设备状态    if (!pm_runtime_get_if_in_use(dev))
        return IRQ_NONE;  // 设备已suspended,忽略中。
    // 处理中断...
    pm_runtime_put_sync_autosuspend(dev);
    return IRQ_HANDLED;
}

Q3:休眠耗时过长。

用pm_trace或ftrace定位卡在哪个驱动。常见原因:

  1. *驱动的suspend回调里调了睡眠函数:msleep、usleep_range在suspend后期(中断已关)会死等。改用mdelay/udelay,或者把耗时操作移到suspend回调(中断还开着时)
  2. 设备resume时重新初始化太慢:某些传感器上电后需要几百毫秒稳定,resume回调里等这么久会拖慢整个唤醒流程。可以只恢复寄存器,把传感器稳定放在工作队列里异步处。3. 文件系统同步太慢:休眠前sync写回大量脏页。可以在休眠前手动sync一次,减少休眠时的写回调

八、总结

速查看

类别 项目 关键字
系统休眠 触发 echo mem/disk > /sys/power/state
驱动回调 suspend/resume/suspend_late/resume_early
宏定 SET_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn)
测试 echo devices > /sys/power/pm_test
Runtime PM 引用计数 pm_runtime_get_sync / pm_runtime_put_sync
autosuspend pm_runtime_set_autosuspend_delay + pm_runtime_use_autosuspend
宏定 SET_RUNTIME_PM_OPS(suspend_fn, resume_fn, idle_fn)
系统休眠复用 pm_runtime_force_suspend / pm_runtime_force_resume
*电源 框架 genpd,power_on/power_off
设备 power-domains = <&pd_xxx>
调试 状态查看 /sys/power/state、/sys/power/suspend_stats/
卡死追踪 pm_trace:echo 1 > /sys/power/pm_trace
耗时分析 ftrace:events/device/dev_suspend
RTC唤醒 echo +5 > /sys/class/rtc/rtc0/wakealarm

本文首发于linuxros.cn,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页