加入星计划,您可以享受以下权益:

  • 创作内容快速变现
  • 行业影响力扩散
  • 作品版权保护
  • 300W+ 专业用户
  • 1.5W+ 优质创作者
  • 5000+ 长期合作伙伴
立即加入
  • 正文
    • 什么是power domain?
    • 如何使用power domain?
  • 推荐器件
  • 相关推荐
  • 电子产业图谱
申请入驻 产业图谱

Linux电源管理之 Power Domain

2022/10/13
5217
阅读需 47 分钟
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

作者简介:JuoJuo6,码龄7年,从事芯片行业嵌入式软件开发工作,主要负责kernel网络子系统模块

最近工作上接触到了kernel power domain framework,kernel文档对这块的解释不多,网上的参考资料也很少,废话不多说,梳理一下,也让自己加深一下印象。

什么是power domain?

电源域(power domain)可以理解为多个模块共用一个电源,比如usb、gpio在soc的内部实现上都属于同一个xxx子系统,那么xxx子系统要进入低功耗状态的前提是什么?就是usb和gpio都得进入低功耗;那反过来,如果要使用usb呢?usb要能上电的前提就是xxx子系统得先上电(即xxx子系统的控制寄存器的enable位需要置1)。

那么usb和gpio在软件实现上需要如何进入低功耗呢?相信大家也能猜到,那就是runtime_pm,所以power domain和runtime_pm是存在耦合关系的,本篇文章不会深入分析runtime_pm,想了解的朋友见上一篇。

如何使用power domain?

power domian的使用方法非常的简单,如果不想去深入分析power domain如何工作的,只想使用kernel提供的方法,那么只需在dts中定义一个power domain节点,同时在驱动中注册该power domain即可,是不是很简单?(参考:Documentationdevicetreebindingspower)

//DTS中定义power domain节点,这里以内核文档提供的例程
 parent: power-controller@12340000 { 
    compatible = "foo,power-controller";
    reg = <0x12340000 0x1000>;
    #power-domain-cells = <1>;   //这里表明parent节点是一个power domain,也就是内核文档所形容的provider,parent管理它下面挂接着的其它模块,为它们提供电源
  };

 child: power-controller@12341000 { 
    compatible = "foo,power-controller";
    reg = <0x12341000 0x1000>;
    power-domains = <&parent 0>; //这里表明child节点是parent下面的一个模块,它使用parent提供的电源
    #power-domain-cells = <1>; 
  };

从上面的dts语法中可以看出,一个系统的电源域从dts中可以很容易就看出来,#power-domain-cells声明该节点是一个power domain,power-domains = <&xxx x>表明该节点属于xxx电源域,那么系统中某个电源域下面有多少个模块就从dts中看有多少个节点引用了该电源域即可。

介绍完了dts,那么驱动中如何注册一个电源域呢?

/**
 * pm_genpd_init - Initialize a generic I/O PM domain object.
 * @genpd: PM domain object to initialize.
 * @gov: PM domain governor to associate with the domain (may be NULL).
 * @is_off: Initial value of the domain's power_is_off field.
 *
 * Returns 0 on successful initialization, else a negative error code.
 */
int pm_genpd_init(struct generic_pm_domain *genpd,
    struct dev_power_governor *gov, bool is_off)  //初始化一个 generic_pm_domain 实例

/**
 * of_genpd_add_provider_onecell() - Register a onecell PM domain provider
 * @np: Device node pointer associated with the PM domain provider.
 * @data: Pointer to the data associated with the PM domain provider.
 */
//下面两个接口都可以用来向内核注册一个power domian
int of_genpd_add_provider_onecell(struct device_node *np,
      struct genpd_onecell_data *data)     
int of_genpd_add_provider_simple(struct device_node *np,
      struct generic_pm_domain *genpd)

可以看到,驱动想要注册一个power domain只需要调用两个函数即可,甚至不需要实现任何具体的函数。

代码分析

接下来进入正题,开始分析内核是如何实现power domain的,为何consumer只需引用power domain节点就可以和provider有耦合。

provider分析

先来看一个非常重要的数据结构,这个数据结构是内核为device电源管理所抽象出来的回调函数,我们关心最后三个即可,即runtime_pm方法,设备的suspend和resume最终都调用该结构体下的某个函数。

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);  //runtime suspend函数
 int (*runtime_resume)(struct device *dev);
 int (*runtime_idle)(struct device *dev);     //runtime resume函数
};

前面提到过驱动想要注册一个power domain只需要两个函数即可,那么分析power domain就从这两个函数入手吧。pm_genpd_init 这个函数从注释可以看出它初始化了一个power domain实例,从入口参数结合具体的实现代码,可以很容易得到 struct generic_pm_domain 结构体就对应着一个power domain实例。

struct generic_pm_domain {
 struct dev_pm_domain domain; /* PM domain operations */
 struct list_head gpd_list_node; /* Node in the global PM domains list */
 struct list_head master_links; /* Links with PM domain as a master */
 struct list_head slave_links; /* Links with PM domain as a slave */
 struct list_head dev_list; /* List of devices */
 struct mutex lock;
 struct dev_power_governor *gov;
 struct work_struct power_off_work;
 struct fwnode_handle *provider; /* Identity of the domain provider */
 bool has_provider;
 const char *name;
 atomic_t sd_count; /* Number of subdomains with power "on" */
 enum gpd_status status; /* Current state of the domain */
 unsigned int device_count; /* Number of devices */
 unsigned int suspended_count; /* System suspend device counter */
 unsigned int prepared_count; /* Suspend counter of prepared devices */
 int (*power_off)(struct generic_pm_domain *domain); //驱动只要实现该函数即可
 int (*power_on)(struct generic_pm_domain *domain);  //驱动只要实现该函数即可
 struct gpd_dev_ops dev_ops;
 s64 max_off_time_ns; /* Maximum allowed "suspended" time. */
 bool max_off_time_changed;
 bool cached_power_down_ok;
 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;  /* Bit field of configs for genpd */
 struct genpd_power_state states[GENPD_MAX_NUM_STATES];
 unsigned int state_count; /* number of states */
 unsigned int state_idx; /* state that genpd will go to when off */

};

该结构体成员很多,看着很复杂,但是真正需要驱动去赋值的成员只有两个,那就是int (*power_off)(struct generic_pm_domain *domain); 与int (*power_on)(struct generic_pm_domain *domain);,其它的power domain framework已经帮我们做好了,这里我们也可以猜到一个电源域的打开与关闭最终是通过驱动注册的这两个函数操作的,因为该电源域的操作只有编写驱动的工程师才最了解,比如它的寄存器地址、bit位等等。

当驱动得到一个power domain实例后,便可以调用of_genpd_add_provider_simple函数向内核注册一个power domain了,来进入该函数看看,做了哪些事情,看不太懂,但是从入口参数看,传进去了驱动实现的struct generic_pm_domain和power domain的node节点。后续这两个参数应该会有作用。

consmer分析

cousmer的分析很复杂,可能有朋友会疑惑,cousmer只需要在dts中引用所使用的power domain节点即可,啥事都不用做,为何还复杂?哈哈,那是因为kernel在背后帮我们驱动工程师完成了太多事情,所以我们感知不到。

正如上面所述,consumer只需在dts中声明即可和provider耦合,那要分析如何完成耦合的,就需要consumer被加载的时候开始分析,consumer其实就是一个电源域下的device,device的加载就涉及到platform bus了。

drivers/base/platform.c
    
static int platform_drv_probe(struct device *_dev)
{
 struct platform_driver *drv = to_platform_driver(_dev->driver);
 struct platform_device *dev = to_platform_device(_dev);
 int ret;

 ret = of_clk_set_defaults(_dev->of_node, false);
 if (ret < 0)
  return ret;

 ret = dev_pm_domain_attach(_dev, true);  //将设备与电源域进行耦合
 ...
}

int genpd_dev_pm_attach(struct device *dev)
{
 struct of_phandle_args pd_args;
 struct generic_pm_domain *pd;
 unsigned int i;
 int ret;

 if (!dev->of_node)
  return -ENODEV;

 if (dev->pm_domain)
  return -EEXIST;

 ret = of_parse_phandle_with_args(dev->of_node, "power-domains",
     "#power-domain-cells"0, &pd_args); //该函数的作用就是解析device引用的power domain节点的所包含的参数,最终幅值给pd_args结构
 if (ret < 0) {
  if (ret != -ENOENT)
   return ret;

  pd_args.args_count = 0;
  pd_args.np = of_parse_phandle(dev->of_node,
      "samsung,power-domain"0);
  if (!pd_args.np)
   return -ENOENT;
 }

 mutex_lock(&gpd_list_lock);
 pd = genpd_get_from_provider(&pd_args);  //根据pd_args结构成员获取provider dts的phandle
 of_node_put(pd_args.np);
 if (IS_ERR(pd)) {
  mutex_unlock(&gpd_list_lock);
  dev_dbg(dev, "%s() failed to find PM domain: %ldn",
   __func__, PTR_ERR(pd));
  return -EPROBE_DEFER;
 }

 dev_dbg(dev, "adding to PM domain %sn", pd->name);

 for (i = 1; i < GENPD_RETRY_MAX_MS; i <<= 1) {
  ret = genpd_add_device(pd, dev, NULL);   //给该power domain添加设备信息
  if (ret != -EAGAIN)
   break;

  mdelay(i);
  cond_resched();
 }
 mutex_unlock(&gpd_list_lock);

 if (ret < 0) {
  dev_err(dev, "failed to add to PM domain %s: %d",
   pd->name, ret);
  goto out;
 }

 dev->pm_domain->detach = genpd_dev_pm_detach;  //一些初始化过程
 dev->pm_domain->sync = genpd_dev_pm_sync;     //一些初始化过程

 mutex_lock(&pd->lock);
 ret = genpd_poweron(pd, 0);
 mutex_unlock(&pd->lock);
out:
 return ret ? -EPROBE_DEFER : 0;
}

static int genpd_add_device(struct generic_pm_domain *genpd, struct device *dev,
       struct gpd_timing_data *td)
{
 struct generic_pm_domain_data *gpd_data;
 int ret = 0;

 dev_dbg(dev, "%s()n", __func__);

 if (IS_ERR_OR_NULL(genpd) || IS_ERR_OR_NULL(dev))
  return -EINVAL;

 gpd_data = genpd_alloc_dev_data(dev, genpd, td);
 if (IS_ERR(gpd_data))
  return PTR_ERR(gpd_data);

 mutex_lock(&genpd->lock);

 if (genpd->prepared_count > 0) {
  ret = -EAGAIN;
  goto out;
 }

 ret = genpd->attach_dev ? genpd->attach_dev(genpd, dev) : 0;
 if (ret)
  goto out;

 genpd->device_count++;
 genpd->max_off_time_changed = true;

 list_add_tail(&gpd_data->base.list_node, &genpd->dev_list);

 out:
 mutex_unlock(&genpd->lock);

 if (ret)
  genpd_free_dev_data(dev, gpd_data);
 else
  dev_pm_qos_add_notifier(dev, &gpd_data->nb);

 return ret;
}

static int genpd_add_device(struct generic_pm_domain *genpd, struct device *dev,
       struct gpd_timing_data *td)
{
 struct generic_pm_domain_data *gpd_data;
 int ret = 0;

 dev_dbg(dev, "%s()n", __func__);

 if (IS_ERR_OR_NULL(genpd) || IS_ERR_OR_NULL(dev))
  return -EINVAL;

 gpd_data = genpd_alloc_dev_data(dev, genpd, td);
 if (IS_ERR(gpd_data))
  return PTR_ERR(gpd_data);

 mutex_lock(&genpd->lock);

 if (genpd->prepared_count > 0) {
  ret = -EAGAIN;
  goto out;
 }

 ret = genpd->attach_dev ? genpd->attach_dev(genpd, dev) : 0;
 if (ret)
  goto out;

 genpd->device_count++;
 genpd->max_off_time_changed = true;

 list_add_tail(&gpd_data->base.list_node, &genpd->dev_list);

 out:
 mutex_unlock(&genpd->lock);

 if (ret)
  genpd_free_dev_data(dev, gpd_data);
 else
  dev_pm_qos_add_notifier(dev, &gpd_data->nb);

 return ret;
}

static struct generic_pm_domain_data *genpd_alloc_dev_data(struct device *dev,
     struct generic_pm_domain *genpd,
     struct gpd_timing_data *td)
{
 struct generic_pm_domain_data *gpd_data;
 int ret;

 ret = dev_pm_get_subsys_data(dev);
 if (ret)
  return ERR_PTR(ret);

 gpd_data = kzalloc(sizeof(*gpd_data), GFP_KERNEL);
 if (!gpd_data) {
  ret = -ENOMEM;
  goto err_put;
 }

 if (td)
  gpd_data->td = *td;

 gpd_data->base.dev = dev;
 gpd_data->td.constraint_changed = true;
 gpd_data->td.effective_constraint_ns = -1;
 gpd_data->nb.notifier_call = genpd_dev_pm_qos_notifier;

 spin_lock_irq(&dev->power.lock);

 if (dev->power.subsys_data->domain_data) {
  ret = -EINVAL;
  goto err_free;
 }

 dev->power.subsys_data->domain_data = &gpd_data->base;

 spin_unlock_irq(&dev->power.lock);

 dev_pm_domain_set(dev, &genpd->domain);  //重点来啦,设置了struct device的struct dev_pm_domain成员

 return gpd_data;

 err_free:
 spin_unlock_irq(&dev->power.lock);
 kfree(gpd_data);
 err_put:
 dev_pm_put_subsys_data(dev);
 return ERR_PTR(ret);
}

贴出来了这么多代码,可见consumer的分析确实蛮复杂,接下来做个总结:

驱动在probe的时候会调用dev_pm_domain_attach函数检查设备是否属于power domain设备,如果是,则获取设备的power domain信息,从哪里获取信息?就是从前面power domain驱动调用of_genpd_add_provider_simple函数注册进来的信息。

调用genpd_add_device函数,将设备添加到该power domain中,主要完成一些struct generic_pm_domain成员的赋值操作。

最后最重要的就是调用dev_pm_domain_set该函数,设置了struct device中的struct dev_pm_domain成员,该成员被赋值为struct generic_pm_domain中的struct dev_pm_domain成员。

为什么说上面第三点调用的函数很重要呢?文章开头本人介绍了一个很重要的结构体,设备的低功耗操作几乎都是引用驱动注册到该结构体的函数。下面再介绍一个对power domain很重要的结构体,那就是struct dev_pm_domain

struct dev_pm_domain {
 struct dev_pm_ops ops;  //有没有发现,这个结构体嵌入了设备power manage的结构体
 void (*detach)(struct device *dev, bool power_off);
 int (*activate)(struct device *dev);
 void (*sync)(struct device *dev);
 void (*dismiss)(struct device *dev);
};

该结构体最重要的就是嵌入了struct dev_pm_ops结构体,该结构体是设备低功耗的具体操作函数,我们再来看看pm_genpd_init函数里具体的实现。

int pm_genpd_init(struct generic_pm_domain *genpd,
    struct dev_power_governor *gov, bool is_off)
{
 ...... 
 genpd->domain.ops.runtime_suspend = genpd_runtime_suspend;
 genpd->domain.ops.runtime_resume = genpd_runtime_resume;
 genpd->domain.ops.prepare = pm_genpd_prepare;
 genpd->domain.ops.suspend_noirq = pm_genpd_suspend_noirq;
 genpd->domain.ops.resume_noirq = pm_genpd_resume_noirq;
 genpd->domain.ops.freeze_noirq = pm_genpd_freeze_noirq;
 genpd->domain.ops.thaw_noirq = pm_genpd_thaw_noirq;
 genpd->domain.ops.poweroff_noirq = pm_genpd_suspend_noirq;
 genpd->domain.ops.restore_noirq = pm_genpd_restore_noirq;
 genpd->domain.ops.complete = pm_genpd_complete;
 ......
}

关注重点,power domain framework在初始化一个struct generic_pm_domain实例的时候,dev_pm_domain下的dev_pm_ops被初始化为power domain特有的函数,即设备属于power domain时,设备在发起suspend和resume流程时,都是调用到power domain的genpd_runtime_suspend和genpd_runtime_resume函数。

这也是很好理解的,因为设备一旦属于一个power domain,设备发起suspend和resume必须要让power domain framework感知到,这样它才能知道每个power domain下的设备当前状态,才能在合适的时机去调用provider的power on和power off函数。但是设备具体的suspend和resume肯定还是驱动自己编写,只有驱动对自己的设备最熟悉,其实可以猜到genpd_runtime_suspend函数只是完成一些power domain framework的一些逻辑,最终还是要调用到设备自己的suspend函数,可以看看代码验证一下猜想。

static int __genpd_runtime_suspend(struct device *dev)
{
 int (*cb)(struct device *__dev);

 if (dev->type && dev->type->pm)
  cb = dev->type->pm->runtime_suspend;
 else if (dev->class && dev->class->pm)
  cb = dev->class->pm->runtime_suspend;
 else if (dev->bus && dev->bus->pm)
  cb = dev->bus->pm->runtime_suspend;
 else
  cb = NULL;

 if (!cb && dev->driver && dev->driver->pm)
  cb = dev->driver->pm->runtime_suspend;

 return cb ? cb(dev) : 0;
}

struct device_driver {
 const char  *name;
 struct bus_type  *bus;

 struct module  *owner;
 const char  *mod_name; /* used for built-in modules */

 bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
 enum probe_type probe_type;

 const struct of_device_id *of_match_table;
 const struct acpi_device_id *acpi_match_table;

 int (*probe) (struct device *dev);
 int (*remove) (struct device *dev);
 void (*shutdown) (struct device *dev);
 int (*suspend) (struct device *dev, pm_message_t state);
 int (*resume) (struct device *dev);
 const struct attribute_group **groups;

 const struct dev_pm_ops *pm;

 struct driver_private *p;
};

这里验证了本人的猜想,genpd_runtime_suspend函数最终调用了__genpd_runtime_suspend函数,而该函数的作用就是取具体的设备低功耗操作函数,对于platform设备,dev->bus及dev->bus->pm为真,所以最终cb应该是cb = dev->bus->pm->runtime_suspend,

这个时候有些朋友可能会有疑问,设备驱动似乎并没有去初始化dev->bus->pm->runtime_suspend函数呀。是的,驱动没有做,但是内核帮我们做了。

int __platform_driver_register(struct platform_driver *drv,
    struct module *owner)
{
 drv->driver.owner = owner;
 drv->driver.bus = &platform_bus_type;
 drv->driver.probe = platform_drv_probe;
 drv->driver.remove = platform_drv_remove;
 drv->driver.shutdown = platform_drv_shutdown;

 return driver_register(&drv->driver);
}

struct bus_type platform_bus_type = {
 .name  = "platform",
 .dev_groups = platform_dev_groups,
 .match  = platform_match,
 .uevent  = platform_uevent,
 .pm  = &platform_dev_pm_ops,
};

static const struct dev_pm_ops platform_dev_pm_ops = {
 .runtime_suspend = pm_generic_runtime_suspend,
 .runtime_resume = pm_generic_runtime_resume,
 USE_PLATFORM_PM_SLEEP_OPS
};

int pm_generic_runtime_suspend(struct device *dev)
{
 const struct dev_pm_ops *pm = dev->driver ? dev->driver->pm : NULL;
 int ret;

 ret = pm && pm->runtime_suspend ? pm->runtime_suspend(dev) : 0;

 return ret;
}

platform driver在注册的时候,会传入platform_bus_type结构体,该结构体dev_pm_ops成员被赋值为platform_dev_pm_ops结构体,该结构体实现了platform具体的suspend和resume操作,pm_generic_runtime_suspend 该函数呢,最终调用的是dev->driver->pm中的实现方法,也就是驱动实现的runtime suspend和runtime resume。

写到这里,我似乎又多了一点理解,不管是platform bus还是power domain,他们的dev_pm_ops结构体的实现都没有具体的suspend和resume,它们只决定去哪里或者dev_pm_ops,具体的实现还是device driver。只不过power domain相对于platform bus更上层,power domain可以从dev bus、dev type、dev class那里获取dev_pm_ops,而platform则只能从device driver获取具体的实现函数。

推荐器件

更多器件
器件型号 数量 器件厂商 器件描述 数据手册 ECAD模型 风险等级 参考价格 更多信息
DSC1121CM1-050.0000 1 Microchip Technology Inc OSC MEMS 50.000MHZ CMOS SMD
$1.56 查看
AFBR-5710PZ 1 Foxconn Transceiver, 830nm Min, 860nm Max, 1250Mbps(Tx), LC Connector, Panel Mount, ROHS COMPLIANT
$33.34 查看
CY14B116L-ZS25XIT 1 Cypress Semiconductor Non-Volatile SRAM, 2MX8, 25ns, CMOS, PDSO44, ROHS COMPLIANT, TSOP2-44
暂无数据 查看

相关推荐

电子产业图谱