目录

一、引言
二、电源管理
------> 2.1、电源管理的两种模型
三、系统Suspend
------> 3.1、系统睡眠模型Suspend
------> 3.2、开启suspend
------> 3.3、驱动支持Suspend
四、runtime
------> 4.1、runtime_PM 框架
------> 4.2、使用Runtime功能
------> 4.3、驱动支持Suspend
------> 4.4、实例
------> 4.5、异步(ASYNC)和同步(SYNC)

一、引言

这篇文件简单的来介绍一下linux下的电源管理框架-PM

二、电源管理

1、电源管理的两种模型

以往接触的Linux驱动,没遇到使用电池供电的情况,因此几乎没关注电源的管理。
然而实际中,不少使用电池供电的硬件平台,例如手机、POS机等,就需要对电源进行管理,比如在不使用设备的时候,休眠屏幕省电。

Linux电源管理模型有两种:系统睡眠模型suspend和Runtime电源管理模型。

三、系统Suspend

1、系统睡眠模型Suspend
名称含义
On (on)S0 - Working
Standby (standby)S1 - CPU and RAM are powered but not executed
Suspend to RAM (mem)S3 - RAM is powered and the running content is saved to RAM
Suspend to Disk, Hibernation (disk)S4 - All content is saved to Disk and power down

其中
S3 aka STR(suspend to ram),挂起到内存,简称待机。计算机将目前的运行状态等数据存放在内存,关闭硬盘、外设等设备,进入等待状态。此时内存仍然需要电力维持其数据,但整机耗电很少。恢复时计算机从内存读出数据,回到挂起前的状态,恢复速度较快。对DDR的耗电情况进行优化是S3性能的关键,大多数手持设备都是用S3待机。

S4 aka STD(suspend to disk),挂起到硬盘,简称休眠。把运行状态等数据存放在硬盘上某个文件或者某个特定的区域,关闭硬盘、外设等设备,进入关机状态。此时计算机完全关闭,不耗电。恢复时计算机从休眠文件/分区中读出数据,回到休眠前的状态,恢复速度较慢。

系统休眠模型给我的感觉是以整机角度进行省电。
S3类似电脑的睡眠,在教长时间不使用电脑后,电脑黑屏,再次敲击键盘迅速显示桌面,原来的工作内容仍不变。
S4类似电脑的休眠,在长时间不使用电脑后,电脑黑屏,再次敲击键盘无反应,按下电源键,开机,原来的工作内容仍不变。

对于嵌入式设备,更多的是使用S3,将数据暂时放在内存里,以实现快速恢复,就像手机的电源键按下黑屏,再次按下迅速亮屏。

在Linux中,通过cat /sys/power/state可以得知当前设备支持的节能模式,一般情况有如下选项:
standby:前面的S1状态,CPU处于浅睡眠模式,主要针对CPU功耗;
mem:前面的S3状态,Suspend to RAM;
disk:前面的S4状态,Suspend to Disk;

需要设置以上模式,只需echo mem > /sys/power/state即可。下面来用我们的虚拟机试验一下

~$ cat /sys/power/state 
freeze standby mem disk

echo standby > /sys/power/state		//进入待机
echo mem > /sys/power/state 		//进入待机
echo disk > /sys/power/state 		//进入休眠

睡眠可能有两种方式:mem和standby,这两种方式都是suspend to RAM,简称STR,只是standby耗电更多一些,返回到正常工作方式时间更短一些而已。

2、开启suspend

首先将suspend功能加入内核:

Power management options  --->
    [*] Suspend to RAM and standby

设置唤醒源

要进入休眠,必须要有唤醒源,没有唤醒源,休眠也没有意义,唤醒源最常见的就是按键中断,就如同手机进入锁屏状态下,按下电源键唤醒一样

3、驱动支持Suspend

电源管理是在冻结APP之后和重启APP之前对驱动的电源进行控制
需要suspend和resume来实现
a.在platform_driver里的driver里添加pm结构体:

static struct platform_driver lcd_driver =
{
    .driver        = {
        .name           = "lcd_s702",
        .pm             = &lcd_pm,
        .of_match_table = of_match_ptr(lcd_dt_ids),
    },
    .probe         = lcd_probe,
    .remove        = lcd_remove,
};

其中pm支持的操作接口有如下

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_late)(struct device *dev);
	int (*resume_early)(struct device *dev);
	int (*freeze_late)(struct device *dev);
	int (*thaw_early)(struct device *dev);
	int (*poweroff_late)(struct device *dev);
	int (*restore_early)(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);
};

b.设置pm成员函数:

static struct dev_pm_ops lcd_pm = {
	.suspend = s702_lcd_suspend,
	.resume  = s702_lcd_resume,	
};

c.编写成员函数:
这个根据需求来编写了

随便来看一个内核中驱动的例子

static int gmux_suspend(struct device *dev)
{
	struct pnp_dev *pnp = to_pnp_dev(dev);
	struct apple_gmux_data *gmux_data = pnp_get_drvdata(pnp);

	gmux_data->resume_client_id = gmux_active_client(gmux_data);
	gmux_disable_interrupts(gmux_data);
	return 0;
}
static int gmux_resume(struct device *dev)
{
	struct pnp_dev *pnp = to_pnp_dev(dev);
	struct apple_gmux_data *gmux_data = pnp_get_drvdata(pnp);

	gmux_enable_interrupts(gmux_data);
	gmux_switchto(gmux_data->resume_client_id);
	if (gmux_data->power_state == VGA_SWITCHEROO_OFF)
		gmux_set_discrete_state(gmux_data, gmux_data->power_state);
	return 0;
}

static const struct dev_pm_ops gmux_dev_pm_ops = {
	.suspend = gmux_suspend,
	.resume = gmux_resume,
};

static struct pnp_driver gmux_pnp_driver = {
	.name		= "apple-gmux",
	.probe		= gmux_probe,
	.remove		= gmux_remove,
	.id_table	= gmux_device_ids,
	.driver		= {
			.pm = &gmux_dev_pm_ops,
	},
};

可以看到,在suspend和resume中有开关中断等操作

如果驱动中没有填充pm结构体,则该设备在系统休眠或恢复时无法正确保存和恢复其状态。这可能导致设备在恢复后无法正常工作,出现功能失常或完全不可用的情况

四、runtime

1、runtime_PM 框架

前面的suspend系统睡眠模型是将整个系统进行休眠,但如果需要在系统运行时,单独对某个模块进行休眠 ,就需要Runtime电源管理模型,这两个模型互相协作,才能最大的发挥电源管理的效果。

Runtime电源管理模型的原理比较简单,就是计数,
当该设备驱动被使用时就加1,放弃使用时就减1,
计数大于1时,就打开该设备的电源,等于0时就关闭电源。调用我们注册的回调函数

Runtime PM相关的函数:
a. 使能/禁止 Runtime PM:pm_runtime_enable / pm_runtime_disable (修改disable_depth变量),这个变量的初始化值是1,默认是disable的状态
b. 增加计数/减少计数:pm_runtime_get_sync / pm_runtime_put_sync (修改usage_count变量),并判断是否进入suspend/resume
c. 回调函数 暂停/恢复/空闲:runtime_suspend / runtime_resume / runtime_idle

上面4个函数不会直接导致runtime_suspend,runtime_resume,runtime_idle被调用,只是使能和修改计数值,当引用计数减为0,调用suspend,从0变为大于0调用resume。

对于runtime PM,默认状态下设备的状态是suspend,如果硬件上它是运行状态,需要调用pm_runtime_set_active()来修改它的状态,然后调用pm_runtime_enable()来使能runtime PM。一般是在probe()的结尾处使用,以为它可能导致runtime的suspend/resume函数立即调用。一般在驱动remove中调用pm_runtime_disable()

在open()/release()接口中可以调用pm_runtime_get_sync/pm_runtime_put_sync

2、使用Runtime功能

调用方式一:
驱动程序提供接口, APP来调用。
在驱动函数的open()、close()里,增加和减少引用计数。
APP调用驱动的时候就能相应的恢复、暂停设备。

调用方式二:
直接操作应用层文件:
恢复:

echo on >  /sys/devices/.../power/control

暂停:

echo auto >  /sys/devices/.../power/control

3、使驱动支持Runtime

a.在platform_driver里的driver里添加pm结构体:(和前面的一样,这里就无需操作)

static struct platform_driver lcd_driver =
{
    .driver        = {
        .name           = "lcd_s702",
        .pm             = &lcd_pm,
        .of_match_table = of_match_ptr(lcd_dt_ids),
    },
    .probe         = lcd_probe,
    .remove        = lcd_remove,
};

b.设置pm成员函数:

static struct dev_pm_ops lcd_pm =
{
    .suspend = s702_lcd_suspend,
    .resume  = s702_lcd_resume,
    .runtime_suspend = s702_lcd_suspend,
    .runtime_resume  = s702_lcd_resume,
};

c.编写成员函数:也是根据需求来

d.使能Runtime:
对于Runtime PM,默认状态下设备的状态是Suspended,
如果硬件上它是运行状态,需要调用pm_runtime_set_active()来修改它的状态,
然后调用pm_runtime_enable()来使能Runtime PM。

在probe()函数的后面添加:

    pm_runtime_set_active(&pdev->dev);
    pm_runtime_enable(&pdev->dev);

反之,还要在remove()里禁止:

pm_runtime_disable(&pdev->dev);

e.修改计数:
一般在open()和release()里面增加和减少引用计数:

4、实例

1、定义

static const struct dev_pm_ops rtl8169_pm_ops = {
	.suspend		= rtl8169_suspend,
	.resume			= rtl8169_resume,
	.freeze			= rtl8169_suspend,
	.thaw			= rtl8169_resume,
	.poweroff		= rtl8169_suspend,
	.restore		= rtl8169_resume,
	.runtime_suspend	= rtl8169_runtime_suspend,
	.runtime_resume		= rtl8169_runtime_resume,
	.runtime_idle		= rtl8169_runtime_idle,
};

#define RTL8169_PM_OPS	(&rtl8169_pm_ops)
static struct pci_driver rtl8169_pci_driver = {
	.name		= MODULENAME,
	.id_table	= rtl8169_pci_tbl,
	.probe		= rtl_init_one,
	.remove		= rtl_remove_one,
	.shutdown	= rtl_shutdown,
	.driver.pm	= RTL8169_PM_OPS,
};

可以看到,这个驱动注册了runtime机制,下面就来看一下这个计数是如何执行的

rtl_init_one
	

rtl_open
	pm_runtime_get_sync(&pdev->dev);
	pm_runtime_put_noidle(&pdev->dev);		//only put,只减少计数器
rtl8169_close
	pm_runtime_get_sync(&pdev->dev);		//增加计数
	......									//释放内存的去初始化工作
	pm_runtime_put_sync(&pdev->dev);
rtl_shutdown
	pm_runtime_get_sync(d);	
	pm_runtime_put_noidle(d);
rtl_remove_one
	pm_runtime_get_noresume(&pdev->dev);
	pm_runtime_put_noidle(&pdev->dev);
5、异步(ASYNC)和同步(SYNC)

设备驱动代码可在进程和中断两种上下文执行,因此put和get等接口,要么是由用户进程调用,要么是由中断处理函数调用。由于这些接口可能会执行device的.runtime_xxx回调函数,而这些接口的执行时间是不确定的,有些可能还会睡眠等待。这对用户进程或者中断处理函数来说,是不能接受的。

因此,RPM core提供的默认接口(pm_runtime_get/pm_runtime_put等),采用异步调用的方式(由ASYNC flag表示),启动一个work queue,在单独的线程中,调用.runtime_xxx回调函数,这可以保证设备驱动之外的其它模块正常运行。

另外,如果设备驱动清楚地知道自己要做什么,也可以使用同步接口(pm_runtime_get_sync/pm_runtime_put_sync等),它们会直接调用.runtime_xxx回调函数,不过,后果自负!

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐