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的suspend、runtime pm、clock 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流程,會在下一篇文章繼續分析,本文就先到這裏了。