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)的角度;内部实现。具体如下。
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_onecell接口,将所有的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中定义,如下:
struct generic_pm_domain {
struct device dev; // 表示此通用电源管理域对应的设备
struct dev_pm_domain domain; // 电源管理域操作
struct list_head gpd_list_node; // 全局电源管理域链表节点
struct list_head parent_links; // 作为父域的关联链表
struct list_head child_links; // 作为子域的关联链表
struct list_head dev_list; // 设备列表
struct dev_power_governor *gov; // 设备电源管理调节器
struct genpd_governor_data *gd; // 由 genpd 调节器使用的数据
struct work_struct power_off_work; // 电源关闭工作
struct fwnode_handle *provider; // 域提供者的标识
bool has_provider; // 是否有域提供者
const char *name; // 域的名称
atomic_t sd_count; // 子域的电源状态为“开”时的数量
enum gpd_status status; // 域的当前状态
unsigned int device_count; // 设备数量
unsigned int suspended_count; // 在系统挂起时的设备计数
unsigned int prepared_count; // 在准备状态的设备计数
unsigned int performance_state; // 聚合的最大性能状态
cpumask_var_t cpus; // 附加 CPU 的掩码
int (*power_off)(struct generic_pm_domain *domain); // 电源关闭回调函数
int (*power_on)(struct generic_pm_domain *domain); // 电源开启回调函数
struct raw_notifier_head power_notifiers; // 电源开启/关闭通知
struct opp_table *opp_table; // genpd 的 OPP 表
unsigned int (*opp_to_performance_state)(struct generic_pm_domain *genpd, struct dev_pm_opp *opp); // 将 OPP 转换为性能状态的函数
int (*set_performance_state)(struct generic_pm_domain *genpd, unsigned int state); // 设置性能状态的函数
struct gpd_dev_ops dev_ops; // 设备操作函数
int (*attach_dev)(struct generic_pm_domain *domain, struct device *dev); // 附加设备的回调函数
void (*detach_dev)(struct generic_pm_domain *domain, struct device *dev); // 分离设备的回调函数
unsigned int flags; // genpd 的配置位域
struct genpd_power_state *states; // genpd 的电源状态
void (*free_states)(struct genpd_power_state *states, unsigned int state_count); // 释放电源状态的函数
unsigned int state_count; // 状态数量
unsigned int state_idx; // 关闭时 genpd 将转到的状态
u64 on_time; // 处于开启状态的时间
u64 accounting_time; // 账户时间
const struct genpd_lock_ops *lock_ops; // 锁定操作函数
union {
struct mutex mlock; // 互斥锁
struct {
spinlock_t slock; // 自旋锁
unsigned long lock_flags;
};
};
};
这个结构很复杂,包括很多参数,不过:对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的工作流程
provider需要为每个domain定义一个struct generic_pm_domain变量,并初始化必要的字段和回调函数,然后调用pm_genpd_init接口,初始化其余的字段,如下:
/**
* pm_genpd_init - 初始化通用 I/O 电源管理域对象。
* @genpd: 要初始化的电源管理域对象。
* @gov: 要与域关联的电源管理调节器(可以为 NULL)。
* @is_off: 域的 power_is_off 字段的初始值。
*
* 成功初始化返回 0,否则返回负错误代码。
*/
int pm_genpd_init(struct generic_pm_domain *genpd,
struct dev_power_governor *gov, bool is_off)
{
int ret;
if (IS_ERR_OR_NULL(genpd))
return -EINVAL;
// 初始化链表头
INIT_LIST_HEAD(&genpd->parent_links);
INIT_LIST_HEAD(&genpd->child_links);
INIT_LIST_HEAD(&genpd->dev_list);
RAW_INIT_NOTIFIER_HEAD(&genpd->power_notifiers);
genpd_lock_init(genpd); // 初始化锁
genpd->gov = gov; // 关联电源管理调节器
INIT_WORK(&genpd->power_off_work, genpd_power_off_work_fn); // 初始化工作队列
atomic_set(&genpd->sd_count, 0); // 初始化子域计数器
genpd->status = is_off ? GENPD_STATE_OFF : GENPD_STATE_ON; // 初始化状态
genpd->device_count = 0;
genpd->provider = NULL;
genpd->has_provider = false;
genpd->accounting_time = ktime_get_mono_fast_ns();
genpd->domain.ops.runtime_suspend = genpd_runtime_suspend;
genpd->domain.ops.runtime_resume = genpd_runtime_resume;
genpd->domain.ops.prepare = genpd_prepare;
genpd->domain.ops.suspend_noirq = genpd_suspend_noirq;
genpd->domain.ops.resume_noirq = genpd_resume_noirq;
genpd->domain.ops.freeze_noirq = genpd_freeze_noirq;
genpd->domain.ops.thaw_noirq = genpd_thaw_noirq;
genpd->domain.ops.poweroff_noirq = genpd_poweroff_noirq;
genpd->domain.ops.restore_noirq = genpd_restore_noirq;
genpd->domain.ops.complete = genpd_complete;
genpd->domain.start = genpd_dev_pm_start;
// 如果具有 GENPD_FLAG_PM_CLK 标志,则关联设备操作函数
if (genpd->flags & GENPD_FLAG_PM_CLK) {
genpd->dev_ops.stop = pm_clk_suspend;
genpd->dev_ops.start = pm_clk_resume;
}
// 对于 always-on 调节器,设置相应的标志
if (gov == &pm_domain_always_on_gov)
genpd->flags |= GENPD_FLAG_RPM_ALWAYS_ON;
// 对于 always-on 域,必须在初始化时将其上电
if ((genpd_is_always_on(genpd) || genpd_is_rpm_always_on(genpd)) &&
!genpd_status_on(genpd)) {
pr_err("always-on PM domain %s is not on\n", genpd->name);
return -EINVAL;
}
// 对于具有多个状态但没有调节器的情况,输出调试信息
if (!gov && genpd->state_count > 1)
pr_debug("%s: no governor for states\n", genpd->name);
// 分配域的数据
ret = genpd_alloc_data(genpd);
if (ret)
return ret;
// 初始化设备
device_initialize(&genpd->dev);
dev_set_name(&genpd->dev, "%s", genpd->name);
// 将域添加到全局域链表中
mutex_lock(&gpd_list_lock);
list_add(&genpd->gpd_list_node, &gpd_list);
mutex_unlock(&gpd_list_lock);
genpd_debug_add(genpd);
return 0;
}
EXPORT_SYMBOL_GPL(pm_genpd_init);
该接口可接受三个参数: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_onecell接口,将它们添加到kernel中。从该接口的命名上,我们可以猜到,它和DTS有关(of是Open Firmware的缩写),定义如下:
/**
* of_genpd_add_provider_onecell() - 注册一个单元 PM 域提供者
* @np: 与 PM 域提供者关联的设备节点指针。
* @data: 与 PM 域提供者关联的数据指针。
*/
int of_genpd_add_provider_onecell(struct device_node *np,
struct genpd_onecell_data *data)
{
struct generic_pm_domain *genpd;
unsigned int i;
int ret = -EINVAL;
// 检查输入参数是否有效
if (!np || !data)
return -EINVAL;
// 如果没有设置数据的翻译函数,则使用默认的 genpd_xlate_onecell
if (!data->xlate)
data->xlate = genpd_xlate_onecell;
// 遍历所有的 PM 域对象
for (i = 0; i < data->num_domains; i++) {
genpd = data->domains[i];
// 如果当前 PM 域对象为空,跳过继续下一个
if (!genpd)
continue;
// 如果当前 PM 域对象不在系统中,跳过继续下一个
if (!genpd_present(genpd))
goto error;
// 将设备节点与当前 PM 域对象关联
genpd->dev.of_node = np;
// 解析 genpd OPP 表
if (genpd->set_performance_state) {
ret = dev_pm_opp_of_add_table_indexed(&genpd->dev, i);
if (ret) {
// 添加 OPP 表失败,输出错误信息并回滚设置
dev_err_probe(&genpd->dev, ret,
"Failed to add OPP table for index %d\n", i);
goto error;
}
// 保存 OPP 表以便在设置性能状态时使用
genpd->opp_table = dev_pm_opp_get_opp_table(&genpd->dev);
WARN_ON(IS_ERR(genpd->opp_table));
}
// 将当前设备节点设置为 PM 域提供者,并标记已设置提供者
genpd->provider = &np->fwnode;
genpd->has_provider = true;
}
// 将 PM 域对象添加到提供者列表中,同时关联翻译回调函数
ret = genpd_add_provider(np, data->xlate, data);
if (ret < 0)
goto error;
return 0;
error:
// 发生错误时回滚之前的设置
while (i--) {
genpd = data->domains[i];
if (!genpd)
continue;
// 解除设备节点与 PM 域对象的关联,并清除提供者标记
genpd->provider = NULL;
genpd->has_provider = false;
// 如果有设置性能状态函数,移除 OPP 表并释放表资源
if (genpd->set_performance_state) {
dev_pm_opp_put_opp_table(genpd->opp_table);
dev_pm_opp_of_remove_table(&genpd->dev);
}
}
return ret;
}
EXPORT_SYMBOL_GPL(of_genpd_add_provider_onecell);
函数 of_genpd_add_provider_onecell 用于注册一个单元型 PM 域提供者。它将设备节点与一组 PM 域对象关联,同时为每个 PM 域对象进行初始化,包括解析 genpd OPP 表、保存 OPP 表、设置提供者标记等。最后,它将这些 PM 域对象添加到提供者列表中,并关联一个翻译回调函数,以便在电源管理框架中进行使用。当操作失败时,函数会回滚之前的设置,确保系统状态不会被影响。总体而言,这个函数的作用是为电源管理框架提供了一个方便的接口,使得可以通过设备树来注册和配置通用的电源管理域提供者,以实现灵活和可配置的电源管理。
static int genpd_add_provider(struct device_node *np, genpd_xlate_t xlate,
void *data)
{
struct of_genpd_provider *cp;
cp = kzalloc(sizeof(*cp), GFP_KERNEL);
if (!cp)
return -ENOMEM;
cp->node = of_node_get(np);
cp->data = data;
cp->xlate = xlate;
fwnode_dev_initialized(&np->fwnode, true);
mutex_lock(&of_genpd_mutex);
list_add(&cp->link, &of_genpd_providers);
mutex_unlock(&of_genpd_mutex);
pr_debug("Added domain provider from %pOF\n", np);
return 0;
}
也许您会奇怪,该接口的两个参数没有一个和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。如下:
/**
* platform_probe() - 平台设备探测函数
* @_dev: 指向待探测的设备结构体
*
* 该函数用于执行平台设备的探测操作,包括设置时钟、电源域等,然后调用设备驱动的探测函数。
*
* 返回:0 表示探测成功,负数表示错误码
*/
static int platform_probe(struct device *_dev)
{
struct platform_driver *drv = to_platform_driver(_dev->driver);
struct platform_device *dev = to_platform_device(_dev);
int ret;
/*
* 通过 platform_driver_probe() 注册的驱动无法再次绑定,因为探测函数通常位于 __init 代码中,
* 所以一旦执行后就会被清除。对于这些驱动,.probe 被设置为 platform_probe_fail,
* 在 __platform_driver_probe() 中使用。因此,对于这些驱动,不会执行后续的时钟和电源域准备操作,以匹配传统行为。
*/
if (unlikely(drv->probe == platform_probe_fail))
return -ENXIO;
// 设置默认的时钟属性
ret = of_clk_set_defaults(_dev->of_node, false);
if (ret < 0)
return ret;
// 配置电源域并进行附加
ret = dev_pm_domain_attach(_dev, true);
if (ret)
goto out;
// 如果设备驱动存在探测函数,则调用探测函数
if (drv->probe) {
ret = drv->probe(dev);
if (ret)
dev_pm_domain_detach(_dev, true); // 若探测失败,进行电源域分离
}
out:
// 如果设置了阻止延迟探测标志并且返回值是 -EPROBE_DEFER,则给出警告信息并返回错误码 -ENXIO
if (drv->prevent_deferred_probe && ret == -EPROBE_DEFER) {
dev_warn(_dev, "probe deferral not supported\n");
ret = -ENXIO;
}
return ret;
}
函数 platform_probe 用于在平台设备上执行探测操作。它首先检查设备驱动的探测函数是否可用,然后设置默认时钟属性,并配置电源域并进行附加。接着,如果设备驱动存在探测函数,它会调用探测函数。最后,如果设备驱动设置了阻止延迟探测标志并且探测函数返回了 -EPROBE_DEFER,函数会给出警告并返回错误码 -ENXIO。总体而言,这个函数是平台设备探测的核心函数,用于协调时钟、电源域设置和设备驱动的探测。
在执行driver的probe之前,会先调用dev_pm_domain_attach接口,将该设备attach到指定的power domain上(如果有的话)。该接口位于drivers/base/power/common.c中,实现如下:
/**
* dev_pm_domain_attach() - 将设备附加到其电源域
* @dev: 要附加的设备。
* @power_on: 表示是否应该在附加时激活设备电源的标志。
*
* 注意:一个设备只能附加到一个电源域。通过遍历可用的备选电源域,我们尝试找到适合该设备的有效电源域。
* 当附加成功后,应该由相应的附加函数分配在 struct dev_pm_domain 中的 ->detach() 回调。
*
* 通常应该在子系统级别的代码中的探测阶段调用此函数。特别适用于持有需要通过电源管理进行管理的设备的子系统。
*
* 调用者必须确保通过适当的同步机制来保证该函数与电源管理回调的正确同步。
*
* 返回值:在成功附加到电源域时返回 0,或者在发现设备不需要电源域时返回 0,否则返回负数的错误码。
*/
int dev_pm_domain_attach(struct device *dev, bool power_on)
{
int ret;
// 检查设备是否已经附加到电源域,如果是则直接返回成功
if (dev->pm_domain)
return 0;
// 尝试使用 ACPI 机制进行设备的附加,如果成功则返回 0
ret = acpi_dev_pm_attach(dev, power_on);
if (!ret)
ret = genpd_dev_pm_attach(dev);
// 如果附加过程中出现错误,则返回错误码
return ret < 0 ? ret : 0;
}
EXPORT_SYMBOL_GPL(dev_pm_domain_attach);
先不考虑ACPI设备,会直接调用genpd_dev_pm_attach接口(呵呵,回到pm domain framework了),该接口位于drivers/base/power/domain.c,如下
/**
* genpd_dev_pm_attach - 使用设备树将设备附加到其电源域
* @dev: 要附加的设备。
*
* 解析设备的设备树节点,以查找电源域的规范。如果找到电源域规范,则将设备附加到检索到的电源域操作中。
*
* 返回值:在成功附加到电源域时返回 1,当设备不需要电源域或存在多个电源域时返回 0,否则返回负数的错误码。
* 注意,如果设备存在电源域,但找不到或无法打开电源域,则返回 -EPROBE_DEFER,以确保不对设备进行探测,
* 并在稍后重新尝试。
*/
int genpd_dev_pm_attach(struct device *dev)
{
// 如果设备没有设备树节点信息,则不需要进行附加操作
if (!dev->of_node)
return 0;
/*
* 对于拥有多个电源域的设备,必须单独进行附加,因为每个设备只能附加到一个电源域。
*/
if (of_count_phandle_with_args(dev->of_node, "power-domains", "#power-domain-cells") != 1)
return 0;
// 调用 __genpd_dev_pm_attach() 函数进行实际的附加操作
return __genpd_dev_pm_attach(dev, dev, 0, true);
}
EXPORT_SYMBOL_GPL(genpd_dev_pm_attach);
/**
* __genpd_dev_pm_attach - 实际将设备附加到其电源域
* @dev: 要附加的设备。
* @base_dev: 基础设备,通常与 dev 相同。
* @index: 电源域索引。
* @power_on: 指示是否应该启动设备电源。
*
* 根据设备的设备树信息,将设备附加到其适当的电源域。如果成功附加,还会设置设备的性能状态和电源状态。
*
* 返回值:成功附加到电源域时返回 1,设备不需要电源域或附加失败时返回 0,附加失败且需要重试时返回 -EPROBE_DEFER。
*/
static int __genpd_dev_pm_attach(struct device *dev, struct device *base_dev,
unsigned int index, bool power_on)
{
struct of_phandle_args pd_args; // 用于存储电源域信息的结构
struct generic_pm_domain *pd; // 电源域对象的指针
int pstate; // 性能状态
int ret;
// 解析设备的设备树信息,获取电源域的引用
ret = of_parse_phandle_with_args(dev->of_node, "power-domains", "#power-domain-cells", index, &pd_args);
if (ret < 0)
return ret;
// 加锁以获取电源域对象
mutex_lock(&gpd_list_lock);
pd = genpd_get_from_provider(&pd_args); // 获取电源域对象
of_node_put(pd_args.np);
if (IS_ERR(pd)) { // 获取失败,处理延迟探测状态
mutex_unlock(&gpd_list_lock);
dev_dbg(dev, "%s() failed to find PM domain: %ld\n", __func__, PTR_ERR(pd));
return driver_deferred_probe_check_state(base_dev);
}
// 将设备添加到电源域中
ret = genpd_add_device(pd, dev, base_dev);
mutex_unlock(&gpd_list_lock);
if (ret < 0)
return dev_err_probe(dev, ret, "failed to add to PM domain %s\n", pd->name);
// 设置设备的电源管理回调函数和同步函数
dev->pm_domain->detach = genpd_dev_pm_detach;
dev->pm_domain->sync = genpd_dev_pm_sync;
// 如果需要启动设备电源,执行电源开启操作
if (power_on) {
genpd_lock(pd);
ret = genpd_power_on(pd, 0);
genpd_unlock(pd);
}
if (ret) { // 电源开启失败
genpd_remove_device(pd, dev);
return -EPROBE_DEFER;
}
// 设置默认性能状态
pstate = of_get_required_opp_performance_state(dev->of_node, index);
if (pstate < 0 && pstate != -ENODEV && pstate != -EOPNOTSUPP) {
ret = pstate;
goto err;
} else if (pstate > 0) {
ret = dev_pm_genpd_set_performance_state(dev, pstate);
if (ret)
goto err;
dev_gpd_data(dev)->default_pstate = pstate;
}
return 1;
err:
// 设置性能状态失败,移除设备,并返回错误码
dev_err(dev, "failed to set required performance state for power-domain %s: %d\n",
pd->name, ret);
genpd_remove_device(pd, dev);
return ret;
}
还蛮复杂,我们先不管细节,看一个大概的过程:
a)of_parse_phandle_with_args负责从device_node指针中,解析指定名称的字段,其中”power-domains”是consumer DTS中的关键字,最终会解出一个struct of_phandle_args类型的变量(回忆一下上面的genpd_xlate_t函数指针,第一个参数就是该类型指针)。
b)解析完成后,调用genpd_get_from_provider接口,获取power domain指针。
c)最后调用genpd_add_device接口,将该设备添加到该power domain相应的链表中
genpd_get_from_provider负责最终的domain解析,实现如下:
/**
* genpd_get_from_provider() - 从提供者中查找 PM 域
* @genpdspec: 用于查找的 OF phandle args
*
* 在由 @genpdspec 指定的节点下查找一个 PM 域提供者,如果找到,则使用提供者的 xlate 函数将 phandle args 映射到一个 PM 域。
*
* 返回值:成功时返回指向 struct generic_pm_domain 的有效指针,失败时返回 ERR_PTR()。
*/
static struct generic_pm_domain *genpd_get_from_provider(
struct of_phandle_args *genpdspec)
{
struct generic_pm_domain *genpd = ERR_PTR(-ENOENT); // 默认为未找到
struct of_genpd_provider *provider; // 指向提供者的指针
if (!genpdspec)
return ERR_PTR(-EINVAL); // 无效参数,返回错误指针
mutex_lock(&of_genpd_mutex); // 加锁,保护数据结构
/* 遍历提供者列表,查找匹配的提供者 */
list_for_each_entry(provider, &of_genpd_providers, link) {
if (provider->node == genpdspec->np) // 找到匹配的提供者
genpd = provider->xlate(genpdspec, provider->data); // 使用提供者的 xlate 函数映射
if (!IS_ERR(genpd))
break;
}
mutex_unlock(&of_genpd_mutex); // 解锁
return genpd; // 返回找到的 PM 域指针或错误指针
}
找到一个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流程,会在下一篇文章继续分析,本文就先到这里了。