Linux PM domain framework(1)_概述和使用流程

1. 前言

在複雜的片上系統(SOC)中,設計者一般會將系統的供電分爲多個獨立的block,這稱作電源域(Power Domain),這樣做有很多好處,例如:

1)將不同功能模塊的供電分開,減小相互之間的干擾(如模擬和數字分開)。

2)不同功能所需的電壓大小不同:小電壓能量損耗低,但對信號質量的要求較高;大電壓能量損耗高,對信號質量的要求較低。因此可以根據實際情況,使用不同的電壓供電,例如CPU core只需1.2v左右即可,而大部分的I/O則需要3.3v左右。

3)系統運行的大部分時間,並不需要所有模塊都處於power on狀態,因此可以通過關閉不工作模塊的供電,將它們的耗電降爲最低。

4)等等

雖然電源域的好處多多,卻不是越多越好,因爲劃分電源域是需要成本的(需要在PMU中使用模擬電路完成,包括金錢成本和空間成本)。因此,大多數系統會根據功能,設置有限的幾個電源域,例如:CPU core(1、2、3…);GPU;NAND;DDR;USB;Display;Codec;等等。

這種設計引出一個問題:存在多個模塊共用一個電源域的情況。因而要求在對模塊power on/off的時候,考慮power共用的情況:只要一個模塊工作,就要power on;直到所有模塊停止工作,才能power off。

Kernel的PM domain framework(位於drivers/base/power/domain.c中),提供了管理和使用系統power domain的統一方法,在解決上面提到的問題的同時,結合kernel的suspendruntime pmclock framework等機制,以非常巧妙、靈活的方式,管理系統供電,以達到高效、節能的目的。

同樣,作爲一個framework,我們可以從三個角度分析:使用者(consumer)的角度;提供者(provider)的角度;內部實現。具體如下。

注:本文的linux kernel版本爲3.18-rc4。一般情況下,對於那些相對穩定的framework,蝸蝸不會說明文章所使用的kernel版本,但本文是個例外,因爲PM domain很多方便、易用的patch,只能在最新版本(當前爲3.18-rc4)kernel上才能看到。

2. 怎麼使用(從consumer的角度看)

藉助device tree,pm domain的使用很簡單(非常類似clock的使用,詳見“clock framework的分析文章”),步驟如下:

1)檢查pm domain provider提供的DTS的node名(下面的紅色字體):

powergate: power-domain@e012e000 {

        compatible = "xxx,xxx-pm-domains";

        reg = <0 0xe012e000 0 0x1000>;

        #power-domain-cells = <1>;

};

各個字段和clock framework類似,也可參考“Documentation/devicetree/bindings/power/power_domain.txt”,這裏不再詳細描述。

2)大部分情況下,power-domain-cells爲1,因此需要得到所需使用的power domain的編號(可能會在include/dt-bindings/*中定義),如POWER_DOMAIN_USB。

3)在模塊自己的DTS中,添加對power domian的引用描述,如下面紅色字體:

power-domain-example@e028e000 {

        compatible = "xxx,xxx-dummy";

        reg = <0 0xe0280000 0 0x1000>;

        power-domains = <&powergate POWER_DOMAIN_USB>;

};

其中:“power-domains”爲pm domain framework的關鍵字,由framework core負責解析(由名稱可知,可以指定多個power domain);“&powergate”爲provider提供的DTS node名;“POWER_DOMAIN_USB”具體的domain標識,也是由provider提供。

4)藉助runtime pm,在需要使用模塊時,增加引用計數(可調用pm_runtime_get),不需要使用時,減少引用計數(可調用pm_runtime_put)。剩下的事情,就交給PM core了。

注2:PM core會在設備的引用計數爲0時,自動調用PM domain的接口,嘗試power off設備。同理,會在引用計數從0到1時,嘗試power on設備。整個過程不需要設備的driver關心任何細節。同時,runtime pm也會處理idle、suspend、resume等相關的電源狀態切換,如果driver只想使用PM domain功能,可以在probe中get一次,在remove中put一次,其效果和常規的power on/power off類似。

3. 怎麼編寫PM domain驅動(從provider的角度看)

從接口層面看,編寫PM domain driver相當簡單,只需要三個步驟:

1)將所有的domain,以struct generic_pm_domain(PM domain framework提供的)形式抽象出來,並填充數據結構中需要由provider提供的內容。

2)調用pm_genpd_init,初始化struct generic_pm_domain變量中其餘的內容。

3)調用__of_genpd_add_provider接口,將所有的domain(由struct generic_pm_domain變量抽象)添加到kernel中,同時提供一個根據DTS node獲得對應的domain指針的回調函數(類似clock framework)。

很顯然,這三個步驟對我們編寫pm domain driver沒有任何幫助,因爲其複雜度都被掩蓋在struct generic_pm_domain結構體中了。讓我們分析完內部邏輯後,再回來。

4. 基本流程分析

4.1 一些數據結構

1)我們先回到“Linux設備模型(5)_device和device driver”中,那篇文章我們留下了很多“未解之謎”,其中之一就是pm_domain指針,如下:

   1:  
   2: struct device {
   3:     ...
   4:     struct dev_pm_domain    *pm_domain;
   5:     ...
   6: };

struct dev_pm_domain結構很簡單,只有一個struct dev_pm_ops類型的變量,該結構在“Linux電源管理(4)_Power Management Interface”中已有詳細描述,是設備電源管理相關的操作函數集,包括idle、suspend/resume、runtime pm等有關的回調函數。

那這個結構和PM domain有什麼關係呢?不着急,先看看struct generic_pm_domain結構。

2)struct generic_pm_domain

struct generic_pm_domain結構用於抽象PM domain,在include/linux/pm_domain.h中定義,如下:

   1: /* include/linux/pm_domain.h */
   2: struct generic_pm_domain {
   3:         struct dev_pm_domain domain;    /* PM domain operations */
   4:         struct list_head gpd_list_node; /* Node in the global PM domains list */
   5:         struct list_head master_links;  /* Links with PM domain as a master */
   6:         struct list_head slave_links;   /* Links with PM domain as a slave */
   7:         struct list_head dev_list;      /* List of devices */
   8:         struct mutex lock;
   9:         struct dev_power_governor *gov;
  10:         struct work_struct power_off_work;
  11:         const char *name;
  12:         unsigned int in_progress;       /* Number of devices being suspended now */
  13:         atomic_t sd_count;      /* Number of subdomains with power "on" */
  14:         enum gpd_status status; /* Current state of the domain */
  15:         wait_queue_head_t status_wait_queue;
  16:         struct task_struct *poweroff_task;      /* Powering off task */
  17:         unsigned int resume_count;      /* Number of devices being resumed */
  18:         unsigned int device_count;      /* Number of devices */
  19:         unsigned int suspended_count;   /* System suspend device counter */
  20:         unsigned int prepared_count;    /* Suspend counter of prepared devices */
  21:         bool suspend_power_off; /* Power status before system suspend */
  22:         int (*power_off)(struct generic_pm_domain *domain);
  23:         s64 power_off_latency_ns;
  24:         int (*power_on)(struct generic_pm_domain *domain);
  25:         s64 power_on_latency_ns;
  26:         struct gpd_dev_ops dev_ops;
  27:         s64 max_off_time_ns;    /* Maximum allowed "suspended" time. */
  28:         bool max_off_time_changed;
  29:         bool cached_power_down_ok;
  30:         struct gpd_cpuidle_data *cpuidle_data;
  31:         void (*attach_dev)(struct device *dev);
  32:         void (*detach_dev)(struct device *dev);
  33: };

這個結構很複雜,包括很多參數,不過:對consumer來說,不需要關心該結構;對provider而言,只需要關心有限的參數;大部分參數,framework內部使用。

對provider來說,需要爲每個power domain定義一個struct generic_pm_domain變量,並至少提供如下字段:

name,該power domain的名稱;

power_off/power_on,該power domain的on/off接口;

其它的字段,這裏做一個大概的介紹(後續代碼邏輯分析時會更爲細緻的說明):

domain,struct dev_pm_domain類型的變量,再回憶一下struct device中的pm_domain指針,這二者一定有些關係,後面再詳細描述

gpd_list_node,用於將該domain添加到一個全局的domain鏈表(gpd_list)中;

master_links/slave_links,power domain可以有從屬關係,例如一個power domain,通過一些器件,分出另外幾個power domain,那麼這個domain稱作master domain,分出來的domain稱作slave domain(也成subdomain)。這兩個list用於組成master link和slave link,後面再詳細描述;

dev_list,該domain下所有device的列表;

gov,struct dev_power_governor指針,後面再解釋;

power_off_work,用於執行power off的wrok queue;

in_progress,該domain下正在suspend的device個數;

sd_count,記錄處於power on狀態的subdomain的個數;

status/status_wait_queue,power domain的狀態,以及用於等待狀態切換的wait queue;

power_off_task,

resume_count/device_count/suspended_count/prepared_count,

suspend_power_off,一個struct task_struct指針,記錄正在執行power off操作的任務;

power_on_latency_ns/power_off_latency_ns,執行power on和power off操作時需要等待的時間,一般由provider提供;

dev_ops,struct gpd_dev_ops類型的變量,提供一些回調函數,後面再詳細解釋;

max_off_time_ns/max_off_timer_changed,和PM domain governor有關的變量,後面再詳細解釋;

cached_power_down_ok,同上;

cpuidle_data,可以把cpuidle和PM domain連接起來,這個指針用於保存CPU idle相關的數據; 

attach_dev/detach_dev,當device和pm domain關聯/去關聯時,調用這兩個回調函數。如果provider需要做一些處理,可以提供。

我相信讀者一定被這個數據結構搞暈了,不要着急,太複雜的話,我們先放一下,去看一些簡單的。下面一節我們先從整體流程上,看一下power domain framework怎麼工作的(最簡單的case),然後再回到這些細節上。

4.2 PM domain的工作流程

結合第二章、第三章的說明,PM domain的工作流程包括:

1)Provider在DTS中定義power domain有關的device tree node,並在provider的初始化接口(可以是一個platform driver的probe,也可以是其它形式)中,定義、初始化並註冊所有的power domain。

2)PM domain的初始化和註冊

provider需要爲每個domain定義一個struct generic_pm_domain變量,並初始化必要的字段和回調函數,然後調用pm_genpd_init接口,初始化其餘的字段,如下:

   1: /**
   2:  * pm_genpd_init - Initialize a generic I/O PM domain object.
   3:  * @genpd: PM domain object to initialize.
   4:  * @gov: PM domain governor to associate with the domain (may be NULL).
   5:  * @is_off: Initial value of the domain's power_is_off field.
   6:  */
   7: void pm_genpd_init(struct generic_pm_domain *genpd,
   8:            struct dev_power_governor *gov, bool is_off)
   9: {
  10:     if (IS_ERR_OR_NULL(genpd))
  11:         return;
  12:  
  13:     INIT_LIST_HEAD(&genpd->master_links);
  14:     INIT_LIST_HEAD(&genpd->slave_links);
  15:     INIT_LIST_HEAD(&genpd->dev_list);
  16:     mutex_init(&genpd->lock);
  17:     genpd->gov = gov;
  18:     INIT_WORK(&genpd->power_off_work, genpd_power_off_work_fn);
  19:     genpd->in_progress = 0;
  20:     atomic_set(&genpd->sd_count, 0);
  21:     genpd->status = is_off ? GPD_STATE_POWER_OFF : GPD_STATE_ACTIVE;
  22:     init_waitqueue_head(&genpd->status_wait_queue);
  23:     genpd->poweroff_task = NULL;
  24:     genpd->resume_count = 0;
  25:     genpd->device_count = 0;
  26:     genpd->max_off_time_ns = -1;
  27:     genpd->max_off_time_changed = true;
  28:     genpd->domain.ops.runtime_suspend = pm_genpd_runtime_suspend;
  29:     genpd->domain.ops.runtime_resume = pm_genpd_runtime_resume;
  30:     genpd->domain.ops.prepare = pm_genpd_prepare;
  31:     genpd->domain.ops.suspend = pm_genpd_suspend;
  32:     genpd->domain.ops.suspend_late = pm_genpd_suspend_late;
  33:     genpd->domain.ops.suspend_noirq = pm_genpd_suspend_noirq;
  34:     genpd->domain.ops.resume_noirq = pm_genpd_resume_noirq;
  35:     genpd->domain.ops.resume_early = pm_genpd_resume_early;
  36:     genpd->domain.ops.resume = pm_genpd_resume;
  37:     genpd->domain.ops.freeze = pm_genpd_freeze;
  38:     genpd->domain.ops.freeze_late = pm_genpd_freeze_late;
  39:     genpd->domain.ops.freeze_noirq = pm_genpd_freeze_noirq;
  40:     genpd->domain.ops.thaw_noirq = pm_genpd_thaw_noirq;
  41:     genpd->domain.ops.thaw_early = pm_genpd_thaw_early;
  42:     genpd->domain.ops.thaw = pm_genpd_thaw;
  43:     genpd->domain.ops.poweroff = pm_genpd_suspend;
  44:     genpd->domain.ops.poweroff_late = pm_genpd_suspend_late;
  45:     genpd->domain.ops.poweroff_noirq = pm_genpd_suspend_noirq;
  46:     genpd->domain.ops.restore_noirq = pm_genpd_restore_noirq;
  47:     genpd->domain.ops.restore_early = pm_genpd_resume_early;
  48:     genpd->domain.ops.restore = pm_genpd_resume;
  49:     genpd->domain.ops.complete = pm_genpd_complete;
  50:     genpd->dev_ops.save_state = pm_genpd_default_save_state;
  51:     genpd->dev_ops.restore_state = pm_genpd_default_restore_state;
  52:     mutex_lock(&gpd_list_lock);
  53:     list_add(&genpd->gpd_list_node, &gpd_list);
  54:     mutex_unlock(&gpd_list_lock);
  55: }

該接口可接受三個參數:genpd爲需要初始化的power domain指針;gov爲governor,可以留空,我們先不考慮它;is_off,指明該power domain在註冊時的狀態,是on還是off,以便framework正確設置該domain的status字段。

它的邏輯比較簡單,值得注意的是genpd->domain.ops中各個回調函數的初始化,這些以”pm_genqd_”爲前綴的函數,都是pm domain framework提供的幫助函數,用於power domain級別的電源管理,包括power on/off、suspend/resume、runtime pm等。

回憶一下“Linux電源管理(4)_Power Management Interface”中有關struct dev_pm_ops的描述,bus_type、device_driver、class、device_type、device等結構,都可以包括dev pm ops,而PM core進行相關的電源狀態切換時,只會調用其中的一個。選擇哪個,是有優先順序的,其中優先級最高的,就是device結構中pm_domain指針的。

那麼,我們再思考一下,device中可是一個指針哦,具體的變量哪來的?你一定猜到了,來自struct generic_pm_domain變量,就是這個函數初始化的內容。後面再詳細介紹。

3)完成所有domain的初始化後,調用__of_genpd_add_provider接口,將它們添加到kernel中。從該接口的命名上,我們可以猜到,它和DTS有關(of是Open Firmware的縮寫),定義如下:

   1: /**
   2:  * __of_genpd_add_provider() - Register a PM domain provider for a node
   3:  * @np: Device node pointer associated with the PM domain provider.
   4:  * @xlate: Callback for decoding PM domain from phandle arguments.
   5:  * @data: Context pointer for @xlate callback.
   6:  */
   7: int __of_genpd_add_provider(struct device_node *np, genpd_xlate_t xlate,
   8:                         void *data)
   9: {
  10:         struct of_genpd_provider *cp;
  11:  
  12:         cp = kzalloc(sizeof(*cp), GFP_KERNEL);
  13:         if (!cp)
  14:                 return -ENOMEM;
  15:  
  16:         cp->node = of_node_get(np);
  17:         cp->data = data;
  18:         cp->xlate = xlate;
  19:  
  20:         mutex_lock(&of_genpd_mutex);
  21:         list_add(&cp->link, &of_genpd_providers);
  22:         mutex_unlock(&of_genpd_mutex);
  23:         pr_debug("Added domain provider from %s\n", np->full_name);
  24:  
  25:         return 0;
  26: }

也許您會奇怪,該接口的三個參數沒有一個和struct generic_pm_domain所代表的power domain有關啊!不着急,我們慢慢分析。

參數1,np,是一個device node指針,哪來的?想一下第2章pm domain provider提供的DTS,就是它生成的指針;

參數2,xlate,一個用於解析power domain的回調函數,定義如下:

typedef struct generic_pm_domain *(*genpd_xlate_t)(struct of_phandle_args *args, void *data);

該回調函數的第二個參數,就是__of_genpd_add_provider接口的參數3。它會返回一個power domain指針;

參數3,data,一個包含了所有power domain信息的指針,具體的形式,由provider自行定義,反正最終會傳給同樣由provider提供的回調函數中,provider根據實際情況,獲得對應的power domain指針,並返回給調用者。

注3:這是device tree的慣用伎倆,consumer在DTS中對所使用的資源(這裏爲power domain,如power-domains = <&powergate POWER_DOMAIN_USB>;)的聲明,最終會由對應的framework(這裏爲pm domain framework)解析,並調用provider提供的回調函數,最終返回給consumer該資源的句柄(這裏爲一個struct generic_pm_domain指針)。所有的framework都是這樣做的,包括前面所講的clock framework,這裏的pm domain framework,等等。具體的解析過程,後面會詳細描述。

4)pm domain framework對consumer DTS中的power domain的解析

先把第2章的例子搬過來:

power-domain-example@e028e000 {

        …

        power-domains = <&powergate POWER_DOMAIN_USB>;

};

怎麼解析呢?讓我們從設備模型的platform_drv_probe接口開始。

Linux設備模型相關的文章可知,platform設備的枚舉從platform_drv_probe開始。而所有在DTS中描述的設備,最終會生成一個platform設備,這個設備的driver的執行,也會從platform_drv_probe開始,該接口進而會調用platform driver的probe。如下:

   1: static int platform_drv_probe(struct device *_dev)
   2: {
   3:         struct platform_driver *drv = to_platform_driver(_dev->driver);
   4:         struct platform_device *dev = to_platform_device(_dev);
   5:         int ret;
   6:  
   7:         ret = of_clk_set_defaults(_dev->of_node, false);
   8:         if (ret < 0)
   9:                 return ret;
  10:  
  11:         ret = dev_pm_domain_attach(_dev, true);
  12:         if (ret != -EPROBE_DEFER) {
  13:                 ret = drv->probe(dev);
  14:                 if (ret)
  15:                         dev_pm_domain_detach(_dev, true);
  16:         }
  17:  
  18:         if (drv->prevent_deferred_probe && ret == -EPROBE_DEFER) {
  19:                 dev_warn(_dev, "probe deferral not supported\n");
  20:                 ret = -ENXIO;
  21:         }
  22:  
  23:         return ret;
  24: }

在執行driver的probe之前,會先調用dev_pm_domain_attach接口,將該設備attach到指定的power domain上(如果有的話)。該接口位於drivers/base/power/common.c中,實現如下:

   1: int dev_pm_domain_attach(struct device *dev, bool power_on)
   2: {
   3:         int ret;
   4:  
   5:         ret = acpi_dev_pm_attach(dev, power_on);
   6:         if (ret)
   7:                 ret = genpd_dev_pm_attach(dev);
   8:  
   9:         return ret;
  10: }
  11: EXPORT_SYMBOL_GPL(dev_pm_domain_attach);

先不考慮ACPI設備,會直接調用genpd_dev_pm_attach接口(呵呵,回到pm domain framework了),該接口位於drivers/base/power/domain.c,如下:

   1: /**
   2:  * genpd_dev_pm_attach - Attach a device to its PM domain using DT.
   3:  * @dev: Device to attach.
   4:  *
   5:  * Parse device's OF node to find a PM domain specifier. If such is found,
   6:  * attaches the device to retrieved pm_domain ops.
   7:  *
   8:  * Both generic and legacy Samsung-specific DT bindings are supported to keep
   9:  * backwards compatibility with existing DTBs.
  10:  *
  11:  * Returns 0 on successfully attached PM domain or negative error code.
  12:  */
  13: int genpd_dev_pm_attach(struct device *dev)
  14: {
  15:     struct of_phandle_args pd_args;
  16:     struct generic_pm_domain *pd;
  17:     int ret;
  18:  
  19:     if (!dev->of_node)
  20:         return -ENODEV;
  21:  
  22:     if (dev->pm_domain)
  23:         return -EEXIST;
  24:  
  25:     ret = of_parse_phandle_with_args(dev->of_node, "power-domains",
  26:                     "#power-domain-cells", 0, &pd_args);
  27:     if (ret < 0) {
  28:         if (ret != -ENOENT)
  29:             return ret;
  30:  
  31:         /*
  32:          * Try legacy Samsung-specific bindings
  33:          * (for backwards compatibility of DT ABI)
  34:          */
  35:         pd_args.args_count = 0;
  36:         pd_args.np = of_parse_phandle(dev->of_node,
  37:                         "samsung,power-domain", 0);
  38:         if (!pd_args.np)
  39:             return -ENOENT;
  40:     }
  41:  
  42:     pd = of_genpd_get_from_provider(&pd_args);
  43:     if (IS_ERR(pd)) {
  44:         dev_dbg(dev, "%s() failed to find PM domain: %ld\n",
  45:             __func__, PTR_ERR(pd));
  46:         of_node_put(dev->of_node);
  47:         return PTR_ERR(pd);
  48:     }
  49:  
  50:     dev_dbg(dev, "adding to PM domain %s\n", pd->name);
  51:  
  52:     while (1) {
  53:         ret = pm_genpd_add_device(pd, dev);
  54:         if (ret != -EAGAIN)
  55:             break;
  56:         cond_resched();
  57:     }
  58:  
  59:     if (ret < 0) {
  60:         dev_err(dev, "failed to add to PM domain %s: %d",
  61:             pd->name, ret);
  62:         of_node_put(dev->of_node);
  63:         return ret;
  64:     }
  65:  
  66:     dev->pm_domain->detach = genpd_dev_pm_detach;
  67:  
  68:     return 0;
  69: }
  70: EXPORT_SYMBOL_GPL(genpd_dev_pm_attach);

還蠻複雜,我們先不管細節,看一個大概的過程:

a)of_parse_phandle_with_args負責從device_node指針中,解析指定名稱的字段,其中”power-domains”是consumer DTS中的關鍵字,最終會解出一個struct of_phandle_args類型的變量(回憶一下上面的genpd_xlate_t函數指針,第一個參數就是該類型指針)。

b)解析完成後,調用of_genpd_get_from_provider接口,獲取power domain指針。

c)最後調用pm_genpd_add_device接口,將該設備添加到該power domain相應的鏈表中。

of_genpd_get_from_provider負責最終的domain解析,實現如下:

   1: static struct generic_pm_domain *of_genpd_get_from_provider(
   2:                                         struct of_phandle_args *genpdspec)
   3: {
   4:         struct generic_pm_domain *genpd = ERR_PTR(-ENOENT);
   5:         struct of_genpd_provider *provider;
   6:  
   7:         mutex_lock(&of_genpd_mutex);
   8:  
   9:         /* Check if we have such a provider in our array */
  10:         list_for_each_entry(provider, &of_genpd_providers, link) {
  11:                 if (provider->node == genpdspec->np)
  12:                         genpd = provider->xlate(genpdspec, provider->data);
  13:                 if (!IS_ERR(genpd))
  14:                         break;
  15:         }
  16:  
  17:         mutex_unlock(&of_genpd_mutex);
  18:  
  19:         return genpd;
  20: }

找到一個provider,以data指針爲參數,調用xlate回調,剩下的事情,就交給provider自己了。

5)power domain的使用

device獲得自己的power domain後,可以利用pm_runtime_get_xxx/pm_runtime_put_xxx接口,增加或減少引用計數,runtime pm core會在合適的時機,調用pm domain提供的power on/off提供的接口,power on或者power off設備。

當然,如果不想使用runtime pm接口,pm domain也提供了其它直接調用的形式,不過不建議使用。

具體的on/off流程,會在下一篇文章繼續分析,本文就先到這裏了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章