Linux电源管理(14)_从设备驱动的角度看电源管理

作者:wowo 发布于:2015-3-2 22:53 分类:电源管理子系统

1. 前言

相信工作稍微久一点的linux驱动工程师都深有体会:

在旧时光里,实现某一个设备的电源管理功能,是非常简单的一件事情。大多数设备都被抽象为platform设备,driver只需要提供suspend/resume/shutdown等回调函数,并注册到kernel即可。kernel会在系统电源状态切换的过程中,调用driver提供的回调函数,切换设备的电源状态。

但是在新时代中,设备电源管理有关的操作,被统一封装在struct dev_pm_ops结构中了。该结构包含20多个回调函数,再加上复杂的电源管理机制(常规的suspend/resume、runtime PM等等),使设备驱动的电源管理工作不再那么单纯,工程师(如蜗蜗自己)的思路也不再特别清晰。

因此本文希望能以单一设备的电源管理为出发点,结合kernel的电源管理机制,介绍怎样在设备驱动中添加电源管理功能,并分析设备电源状态切换和系统电源状态切换的关系。

另外,我们在电源管理系列文章中,介绍了很多的电源管理机制,如generic PM、wakeup event framework、wakelock、autosleep、runtime PM、PM domain、等等,本文也算是对它们的梳理和总结。

2. 功能描述

设备的电源状态切换,和系统电源状态切换基本保持一致(runtime PM除外),切换的场景如下:

1)系统reboot的过程,包括halt、power off、restart等(可参考“Linux电源管理(3)_Generic PM之Reboot过程”),要求设备进入shutdown状态,以避免意外产生。

2)系统suspend/resume的过程(可参考“Linux电源管理(6)_Generic PM之Suspend功能”),要求设备也同步suspend/resume。

3)系统hibernate及恢复的过程,要求设备在suspend/resume的基础上,增加poweroff的动作。

4)runtime PM过程(可参考“Linux电源管理(11)_Runtime PM之功能描述”),要求设备在引用计数为0时suspend甚至power off,并在引用计数大于0时power on以及resume。

旧有的电源管理框架中,通过bus、class、device_driver等结构体中的shutdown、suspend、resume三个回调函数,就可以实现上面处runtime PM之外的所有功能。但是在新框架中,特别是引入struct dev_pm_ops结构之后,其中的suspend/resume就不再推荐使用了。

不过,对有些设备来说,例如platform device,如果电源管理需求不是很复杂,driver工程师仍然可以使用旧的方法实现,kernel会自动帮忙转换为新的方式。但是,如果有更多需求,就不得不面对struct dev_pm_ops了。下面将会详细说明。

3. 数据结构回顾

正式开始之前,我们先回顾一下设备电源管理有关的数据结构。它们大多都在之前的文章中介绍过了,本文放在一起,权当一个总结。

3.1 .shutdown回调函数以及使用方法

由于reboot过程是相对独立和稳定的,且该过程依赖于设备的.shutdown回调函数,这里把它独立出来,单独描述,后面就不再涉及了。

.shutdown回调函数存在于两个数据结构中:struct device_driver和struct bus_type,在系统reboot的过程中被调用,负责关闭设备。设备驱动可以根据需要,实现其中的一个。我们以一个普通的platform设备为例,介绍这个过程。

1)定义一个platform_driver,并实现其.shutdown回调,然后调用platform_driver_register将它注册到kernel中

   1: static void foo_shutdown(struct platform_device *pdev)
   2: {
   3:         ...
   4: } 
   5: static platform_driver foo_pdrv = 
   6: {
   7:         .shutdown = foo_shutdown,
   8:         ...
   9: };

2)platform_driver_register时,会把struct device_driver变量的shutdown函数,替换为platform设备特有的shutdown函数(platform_drv_shutdown),并调用driver_register将device_driver注册到kernel

   1: int __platform_driver_register(struct platform_driver *drv,
   2:                                 struct module *owner)
   3: {
   4:
   5:  
   6:         if (drv->shutdown)
   7:                 drv->driver.shutdown = platform_drv_shutdown; 
   8:  
   9:         return driver_register(&drv->driver);
  10: }

3)系统reboot的过程中,会调用每个设备的shutdown函数。对这里的foo_pdrv而言,会先调用platform_drv_shutdown,它继续调用foo_shutdown。

3.2 legacy的不再使用的.suspend/.resume

旧的suspend/resume操作,主要依赖struct device_driver、struct class、struct bus_type等结构中的suspend和resume回调函数,其使用方式和上面的.shutdown几乎完全一样。以platform设备为例,只需多定义两个函数即可,如下:

   1: static int foo_suspend(struct platform_device *pdev, pm_message_t state)
   2: {
   3:         ...
   4: }
   5:  
   6: static int foo_resume(struct platform_device *pdev)
   7: {
   8:         ...
   9: }
  10:  
  11: static void foo_shutdown(struct platform_device *pdev)
  12: {
  13:         ...
  14: }
  15:  
  16: static platform_driver foo_pdrv = {
  17:         .suspend = foo_suspend,
  18:         .resume = foo_resume,
  19:         .shutdown = foo_shutdown,
  20:         ...
  21: };

在较新的kernel中,已经不再建议使用这些回调函数了,但对platform设备来说,如果场景比较简单,可以照旧使用上面的实现方法,platform.c会自动帮忙转换为struct dev_pm_ops回调,具体请参考后面描述。

3.3 struct dev_pm_ops结构

struct dev_pm_ops是设备电源管理的核心数据结构,用于封装和设备电源管理有关的所有操作

   1: struct dev_pm_ops {
   2:         int (*prepare)(struct device *dev);
   3:         void (*complete)(struct device *dev);
   4:         int (*suspend)(struct device *dev);
   5:         int (*resume)(struct device *dev);
   6:         int (*freeze)(struct device *dev);
   7:         int (*thaw)(struct device *dev);
   8:         int (*poweroff)(struct device *dev);
   9:         int (*restore)(struct device *dev);
  10:         int (*suspend_late)(struct device *dev);
  11:         int (*resume_early)(struct device *dev);
  12:         int (*freeze_late)(struct device *dev);
  13:         int (*thaw_early)(struct device *dev);
  14:         int (*poweroff_late)(struct device *dev);
  15:         int (*restore_early)(struct device *dev);
  16:         int (*suspend_noirq)(struct device *dev);
  17:         int (*resume_noirq)(struct device *dev);
  18:         int (*freeze_noirq)(struct device *dev);
  19:         int (*thaw_noirq)(struct device *dev);
  20:         int (*poweroff_noirq)(struct device *dev);
  21:         int (*restore_noirq)(struct device *dev);
  22:         int (*runtime_suspend)(struct device *dev);
  23:         int (*runtime_resume)(struct device *dev);
  24:         int (*runtime_idle)(struct device *dev);
  25: };

该结构基本上是个大杀器了,该有的东西都有,主要分为几类:

传统suspend的常规路径,prepare/complete、suspend/resume、freeze/thaw、poweroff、restore;

传统suspend的特殊路径,early/late、noirq;

runtime PM,suspend/resume/idle。

各类driver需要做的事情很单纯,实现这些回调函数,并保存在合适的位置,我们接着往下看。

3.4 struct dev_pm_ops的位置

   1: struct device {
   2:         ...
   3:         struct dev_pm_domain    *pm_domain;
   4:         const struct device_type *type;
   5:         struct class            *class;
   6:         struct bus_type *bus;
   7:         struct device_driver *driver; 
   8:         ...
   9: };
  10:  
  11:  
  12:  
  13: struct dev_pm_domain {
  14:         struct dev_pm_ops       ops; 
  15:         ...
  16: };
  17:  
  18: struct device_type {
  19:         ...
  20:         const struct dev_pm_ops *pm;
  21: };
  22:  
  23: struct class {
  24:         ...
  25:         const struct dev_pm_ops *pm;
  26:         ...
  27: };
  28:  
  29: struct bus_type {
  30:         ...
  31:         const struct dev_pm_ops *pm;
  32:         ...
  33: };
  34:  
  35: struct device_driver {
  36:         ...
  37:         const struct dev_pm_ops *pm;
  38:         ...
  39: };

可谓是狡兔多窟,struct dev_pm_ops存在于struct device、struct device_type、struct class、struct bus_type、struct device_driver等所有和设备模型有关的实体中

由之前的文章可知,kernel在电源管理的过程中,会按照如下优先级调用dev_pm_ops中的回调函数,以命令设备实现相应的状态切换

dev->pm_domain->ops、dev->type->pm、dev->class->pm、dev->bus->pm、dev->driver->pm。

因此,设备driver需要做的事情也很单纯,实现这些回调函数,并保存在合适的位置。但这么多位置,到底怎么实现呢?我们接着分析。

4. struct dev_pm_ops的实现

由之前的描述可知,系统在电源状态切换时,会按照一定的优先顺序,调用设备的pm ops。所谓的优先顺序,是指:只要存在优先级高的ops(如dev->pm_domain->ops),则调用该ops,否则继续查找下一个优先级。因此,设备驱动可以根据该设备的实际情况,在指定层次上,实现dev pm ops,以达到电源管理的目的。

dev pm ops可以存在于pm domain、device type、class、bus、device driver任何一个地方,本章以pm domain、bus和device driver三个典型场景为例,介绍设备电源管理的实现思路。

注1:为了方便,我会以struct dev_pm_ops中的.suspend函数为例,其它类似。

4.1 pm domain

当一个设备属于某个pm domain时(具体可参考“Linux PM domain framework(1)_概述和使用流程”),系统suspend的过程中,会直接调用pm_domain->ops.suspend。而由pm_genpd_init可知,pm_domain->ops.suspend由pm_genpd_suspend实现:

genpd->domain.ops.suspend = pm_genpd_suspend;

该接口的实现为:

   1: static int pm_genpd_suspend(struct device *dev)
   2: {
   3:         struct generic_pm_domain *genpd;
   4:  
   5:         dev_dbg(dev, "%s()\n", __func__);
   6:  
   7:         genpd = dev_to_genpd(dev);
   8:         if (IS_ERR(genpd))
   9:                 return -EINVAL;
  10:  
  11:         return genpd->suspend_power_off ? 0 : pm_generic_suspend(dev);
  12: }

最终会调用pm_generic_suspend,由“Linux电源管理(4)_Power Management Interface”的描述可知,该接口最终会调用该设备驱动的suspend接口(如果有的话),即:dev->driver->pm->suspend。

看来是空欢喜一场,本以为pm domain帮忙做了,设备驱动就可以偷一点懒,谁知道绕来绕去,又把球踢给了设备驱动!让我们思考一下其中的原因:

1)suspend时,设备的动作到底是什么,只有设备驱动最清楚,所以,把事情交给driver做,是合理的。

2)那么,为什么要经过pm domain这一层呢?直接调用driver的suspend不就可以了吗?因为需要在suspend前,由pm domain做一些处理,例如判断该设备是否已经掉电(如果掉电了,就不能再suspend了,否则可能有非预期的结果),等等。

4.2 dev->bus->pm

来看另一个例子,如果该设备所在的bus提供了dev_pm_ops呢?开始之前,我们再强调一下这个事实:suspend时,设备的动作到底是什么,只有设备驱动最清楚,所以,把事情交给driver做,是合理的。所以相信大家猜到了,就算bus有suspend回调,最终还是要绕到设备驱动的suspend接口上。

我们以platform bus为例,原因是这个bus很简单,而且我们平时需要面对的大多数设备都是platform设备。

在drivers/base/platform.c中,platform bus是这样定义的:

   1: struct bus_type platform_bus_type = {
   2:         .name           = "platform",
   3:         .dev_groups     = platform_dev_groups,
   4:         .match          = platform_match,
   5:         .uevent         = platform_uevent,
   6:         .pm             = &platform_dev_pm_ops,
   7: };

接着看一下platform_dev_pm_ops:

   1: static const struct dev_pm_ops platform_dev_pm_ops = {
   2:         .runtime_suspend = pm_generic_runtime_suspend,
   3:         .runtime_resume = pm_generic_runtime_resume,
   4:         USE_PLATFORM_PM_SLEEP_OPS
   5: };

哦,有runtime PM相关的两个回调,有一个宏定义:USE_PLATFORM_PM_SLEEP_OPS,该宏定义指定了dev_pm_ops的suspend回调为platform_pm_suspend(其它的类似)。该接口的实现如下:

   1: int platform_pm_suspend(struct device *dev)
   2: {
   3:         struct device_driver *drv = dev->driver;
   4:         int ret = 0;
   5:  
   6:         if (!drv)
   7:                 return 0;
   8:  
   9:         if (drv->pm) {
  10:                 if (drv->pm->suspend)
  11:                         ret = drv->pm->suspend(dev);
  12:         } else {
  13:                 ret = platform_legacy_suspend(dev, PMSG_SUSPEND);
  14:         }
  15:  
  16:         return ret;
  17: }

原来如此,如果该设备的驱动提供了dev_pm_ops指针,调用相应的suspend接口。否则,调用legacy的接口(即pdrv->suspend)。再对比3.1,3.2小节的描述,是不是豁然开朗了?

另外,由于platform bus是一个虚拟的bus,不需要其它的动作。对于一些物理bus,可以在bus的suspend接口中,实现bus有关的suspend操作。这就是设备模型的魅力所在

4.3 dev->driver->pm

无论怎样,如果一个设备需要在suspend时有一些动作,就一定要在设备驱动中实现suspend,那样怎么实现呢?定义一个struct dev_pm_ops变量,并实现设备所需的回调函数,在driver注册之前,保存在driver->pm指针中即可。

那有什么变化?大多数的设备是platform设备,我们也可以用旧的方式(3.1,3.2小节),实现platform driver的suspend/resume。但是,在新时代,不建议这样做了,注意platform_legacy_suspend中的legacy字样哦,遗产、遗留下来的,只是为了兼容。如果我们新写driver,就用新的方式好了。

5. 设备电源状态的切换过程

本来还想梳理一下系统电源切换的过程中,driver是怎么处理的。但经过上面的分析,传统的suspend/resume已经很明确了,无非是按照pm_domain—>device driver或者class—>device driver或者bus—>device driver的顺序,调用相应的回调函数。而runtime PM,还是放到runtime PM的分析文章里比较好。所以本文就结束好了。

 

原创文章,转发请注明出处。蜗窝科技www.wowotech.net

标签: Device driver PM

评论:

winton
2017-09-21 20:43
请教一个问题,我想在drivers/rtc/rtc-ds1374.c的watchdog设备加一个suspend函数,但是本身是一个miscdevice设备,没有合适的地方加dev_pm_ops.
已经有rtc的dev_pm_ops,但是同一个芯片还有watchdog的功能,不知道该加在哪个地方合适。
#ifdef CONFIG_RTC_DRV_DS1374_WDT
    save_client = client;
    ret = misc_register(&ds1374_miscdev);
    if (ret)
        return ret;
    ret = register_reboot_notifier(&ds1374_wdt_notifier);
    if (ret) {
        misc_deregister(&ds1374_miscdev);
        return ret;
    }
    ds1374_wdt_settimeout(131072);
#endif
wowo
2017-09-22 09:40
@winton:misc设备注册的时候,kernel会为它创建一个struct device,之后的事情,你懂的(参考本文的介绍,应该可以找到放的地方吧?):

由之前的文章可知,kernel在电源管理的过程中,会按照如下优先级调用dev_pm_ops中的回调函数,以命令设备实现相应的状态切换:
dev->pm_domain->ops、dev->type->pm、dev->class->pm、dev->bus->pm、dev->driver->pm。

int misc_register(struct miscdevice * misc)                                    
{
        ...
        dev = MKDEV(MISC_MAJOR, misc->minor);                                  
                                                                                
        misc->this_device = device_create(misc_class, misc->parent, dev,        
                                          misc, "%s", misc->name);              
        if (IS_ERR(misc->this_device)) {                                        
                int i = DYNAMIC_MINORS - misc->minor - 1;                      
                if (i < DYNAMIC_MINORS && i >= 0)                              
                        clear_bit(i, misc_minors);                              
                err = PTR_ERR(misc->this_device);                              
                goto out;                                                      
        }
        ...
}
enjoyer
2015-07-17 16:51
后悔没有早点看到你们写的东西~
enjoyer
2015-07-17 16:50
读完感觉思路清晰多了,wowo太厉害了,赞~!
wowo
2015-07-17 19:48
@enjoyer:过奖了,我也是学习的过程,没事可以多交流
tigger
2015-03-05 15:57
首席功耗专家不是盖的啊,读完豁然开朗。
wowo
2015-03-05 21:35
@tigger:哈哈,这不是损我的吧?只是记录一下阅读笔记而已,让Tigger兄见笑了哈~~
linuxer
2015-03-05 22:38
@tigger:我觉得还是叫做华南区首席PM driver专家好了,不加上个华南区好像不习惯啊
schedule
2015-03-04 15:07
wowo & linuxer 是帝都的吗?
linuxer
2015-03-04 16:12
@schedule:非也,虽然我们都是北方人,但是都在珠三角(非深圳)工作,都曾经是华南(深圳除外)首席linux团队(我们自己封的,哈哈)的一员,^_^
tigger
2015-03-04 18:33
@linuxer:听起来好厉害的样子
linuxer
2015-03-04 18:43
@tigger:哈哈,当时都是为了好玩,每当想要把一个任务,比如说I2C驱动交给某个工程师的时候,我都是语重心长的告诉他,xxx,组织上准备把你培养成华南区首席I2C驱动工程师,你要好好干啊,慢慢的,我们team每个工程师没有2~3首席的称号都不好意思和别人打招呼
haichunzhao
2015-03-04 10:25
suspend时,设备的动作到底是什么,只有设备驱动最清楚,所以,把事情交给driver做,是合理的。
这句话道出了本质。曾经有其它模块的工程师问我,在suspend中到底要做什么,我和他们说,为了省电,你可以进入低功耗模式,也可以给设备掉电。现在看来还是应该驱动自己决定到底要怎么做。
wowo
2015-03-04 12:28
@haichunzhao:是的,抛开具体的设备去讲suspend时要做什么,是没有意义的,因为设备是多样的。
schedule
2015-03-04 09:46
wowo & linuxer 二位您是在具体公司工作吗?怎么有这么多时间写这么详细的文档?
wowo
2015-03-04 12:22
@schedule:是在公司,都是晚上抽空写的。
schedule
2015-03-04 14:43
@wowo:牛!期待你们关于网络系统的分析!
linuxer
2015-03-04 16:09
@schedule:当然是在公司上班了,主要是没有爱好,不爱看电视,不看电影,不爱泡吧,不爱交际....唯有从这些技术知识上获取快感,呵呵~~~

发表评论:

Copyright @ 2013-2015 蜗窝科技 All rights reserved. Powered by emlog