Linux内核学习--电源管理(驱动)
目录
一、引言
二、电源管理
------> 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回调函数,不过,后果自负!
更多推荐
所有评论(0)