☰
Linux电源域通用框架深度解析:从设备树到驱动实战
2026/10/31 1:07:16 网站建设 项目流程

1. 电源域通用框架到底在解决什么问题

Linux 内核的功耗管理子系统经过多年演进,已经形成了一套相当成熟的层次化架构。如果你之前接触过时钟框架(Common Clock Framework)或者稳压器框架(Regulator Framework),你会发现电源域(Power Domain)通用框架的设计思路和它们如出一辙——都是把硬件上分散的、各平台各异的电源控制逻辑,抽象成一套统一的、可复用的软件模型。

先把这个框架要解决的核心问题说清楚。在一颗典型的 SoC 里面,电源并不是铁板一块。CPU 核心有自己的电源域,GPU 有独立的电源域,显示控制器、视频编解码器、ISP、DSP 等等,每个功能模块或者一组功能模块,通常都挂在一个可以独立开关的电源域下面。这些电源域之间还可能存在父子关系——比如一个顶层的电源域下面挂着几个子域,关掉父域的时候子域自然也没电了,但反过来你可以在父域保持供电的情况下单独关掉某个子域。

在没有通用框架之前,每个 SoC 厂商的 BSP 里面都有一套自己的电源管理代码,驱动开发者要控制某个设备的电源,得直接调用平台相关的接口,代码完全不可移植。电源域通用框架的出现,就是要把这种混乱局面收拾干净:用设备树描述硬件上的电源域拓扑结构,用统一的 API 让消费者设备请求电源开关,用一套注册接口让平台代码提供具体的电源控制操作。

这个框架在源码树里的位置是drivers/base/power/domain.c,头文件是include/linux/pm_domain.h。它属于设备驱动模型(Device Driver Model)的一部分,和 Runtime PM、System Sleep 这些机制紧密配合。理解这个框架,对于做嵌入式 Linux 驱动开发、特别是做 SoC 平台支持的人来说,是绕不过去的一关。

注意:电源域框架和时钟框架、稳压器框架虽然设计思路相似,但它们的耦合关系非常紧密。一个设备在 Runtime Suspend 的时候,通常需要按顺序关闭时钟、关闭稳压器、最后关闭电源域。这个顺序搞错了,轻则功耗降不下来,重则硬件行为异常。

适合阅读这篇内容的人:有一定 Linux 驱动开发基础,了解设备模型和设备树的基本概念,正在或者即将参与 SoC 电源管理相关工作的工程师。如果你刚开始学 Linux 内核,建议先把 platform driver 和设备树的基础打牢再来看这个。

2. 框架整体设计与核心数据结构拆解

2.1 三个核心角色:provider、consumer 和 domain

电源域框架里面有三个关键角色,理解它们之间的关系是理解整个框架的基础。

Provider是电源域的控制者,通常由 SoC 的电源管理单元(PMU)或者电源管理 IC(PMIC)驱动来实现。Provider 需要向框架注册一个或多个电源域,并提供实际的电源开关操作回调。在设备树里面,provider 通常是一个带有#power-domain-cells属性的节点。

Consumer是电源域的使用者,也就是那些需要控制自己电源状态的设备。一个 consumer 设备通过设备树中的power-domains属性来声明自己属于哪个电源域。在驱动代码里面,consumer 通过dev_pm_domain_attach()或者devm_pm_domain_attach()来绑定到自己的电源域。

Domain是框架内部对电源域的抽象表示,用struct generic_pm_domain来描述。每个 domain 有自己的名字、电源开关回调、以及一个设备列表——记录着所有依附于这个域的设备。

这三者之间的关系可以这样理解:provider 负责“造”电源域,consumer 负责“用”电源域,而 domain 结构体就是它们之间的桥梁。框架在中间做协调,确保电源域的开关时机正确、顺序合理。

2.2 struct generic_pm_domain 关键字段解析

struct generic_pm_domain是整个框架最核心的数据结构,定义在include/linux/pm_domain.h里面。这个结构体字段不少,我挑几个最关键的来说。

struct generic_pm_domain { struct dev_pm_domain domain; /* PM domain 操作的钩子 */ struct list_head gpd_list_node; /* 全局 gpd_list 链表节点 */ struct list_head parent_links; /* 指向父域的链接 */ struct list_head child_links; /* 指向子域的链接 */ struct list_head dev_list; /* 依附于此域的设备列表 */ struct dev_power_governor *gov; /* 电源开关的调速器 */ struct work_struct power_off_work; const char *name; /* 域名,调试用 */ unsigned int in_suspend : 1; /* 是否在系统 suspend 中 */ atomic_t sd_count; /* 子域计数 */ bool synced_poweroff; /* 是否需要同步下电 */ int (*power_off)(struct generic_pm_domain *domain); int (*power_on)(struct generic_pm_domain *domain); int (*set_performance_state)(...); ... };

power_off和power_on这两个回调是 provider 必须实现的,框架在需要开关电源域的时候就会调用它们。dev_list记录了所有绑定到这个域的设备,框架在决定是否可以关掉某个域的时候,会遍历这个列表检查每个设备的 Runtime PM 状态。parent_links和child_links维护了电源域的树形拓扑结构,这是处理嵌套电源域的关键。

gov这个字段指向一个 governor(调速器),它决定了框架在什么时候真正去执行电源关闭操作。默认的 governor 是simple_qos_governor,它会考虑设备的延迟要求(PM QoS latency)来决定是否允许关闭电源域。这个设计很巧妙——有些设备虽然当前空闲,但它的延迟容忍度很低,关掉电源域再打开的开销可能超过它能接受的范围,governor 就是用来做这个权衡的。

2.3 设备树中的电源域描述方式

设备树是电源域拓扑结构的描述入口。一个典型的 provider 节点长这样:

pmu: power-management@ff000000 { compatible = "vendor,soc-pmu"; reg = <0xff000000 0x1000>; #power-domain-cells = <1>; }; power: power-controller@ff001000 { compatible = "vendor,soc-power-controller"; reg = <0xff001000 0x1000>; #power-domain-cells = <1>; #address-cells = <1>; #size-cells = <0>; pd_gpu: power-domain@0 { reg = <0>; #power-domain-cells = <0>; }; pd_vpu: power-domain@1 { reg = <1>; #power-domain-cells = <0>; }; };

Consumer 设备通过power-domains属性来引用:

gpu: gpu@ff100000 { compatible = "vendor,soc-gpu"; reg = <0xff100000 0x10000>; power-domains = <&power 0>; /* 或者引用子节点 */ /* power-domains = <&pd_gpu>; */ };

#power-domain-cells的值决定了引用电源域时需要几个参数。如果是 0,说明直接引用节点就行;如果是 1,说明还需要一个索引参数来指定具体是哪个域。这个设计和时钟框架的#clock-cells、中断控制器的#interrupt-cells是一个套路。

实操心得:在调试电源域问题时,第一件事就是确认设备树里的power-domains属性是否正确。我遇到过好几次因为 cells 数量写错导致 domain 绑定失败的情况,内核日志里会有failed to attach to power domain之类的报错,但不会告诉你具体是 cells 的问题,得自己对着 provider 节点的#power-domain-cells一个一个数。

3. 核心机制深度解析与实操要点

3.1 电源域的注册流程

Provider 驱动注册电源域的过程,核心是调用pm_genpd_init()来初始化一个generic_pm_domain结构体,然后通过of_genpd_add_provider_onecell()或者of_genpd_add_provider_simple()把它注册到框架里面。

先看pm_genpd_init()的典型用法:

static int my_power_driver_probe(struct platform_device *pdev) { struct my_power *pm = dev_get_drvdata(&pdev->dev); int ret; /* 初始化电源域结构体 */ pm->gpd.name = "my_power_domain"; pm->gpd.power_off = my_power_off; pm->gpd.power_on = my_power_on; pm->gpd.flags = GENPD_FLAG_ALWAYS_ON; /* 根据实际情况设置 */ /* 初始化,第三个参数是 governor,传 NULL 用默认的 */ ret = pm_genpd_init(&pm->gpd, NULL, false); if (ret) { dev_err(&pdev->dev, "failed to init genpd\n"); return ret; } /* 注册为 provider */ ret = of_genpd_add_provider_simple(pdev->dev.of_node, &pm->gpd); if (ret) { dev_err(&pdev->dev, "failed to add provider\n"); return ret; } return 0; }

pm_genpd_init()的第三个参数是is_off,表示这个域初始状态是否是关闭的。如果你的硬件在上电后电源域默认是打开的,就传false;如果默认是关闭的,传true,框架会在第一个 consumer 绑定的时候自动打开它。

of_genpd_add_provider_simple()适用于一个 provider 节点只对应一个电源域的情况。如果一个 provider 节点下面有多个子域(像前面设备树示例那样),就需要用of_genpd_add_provider_onecell():

static struct generic_pm_domain *my_domains[] = { &pm->pd_gpu, &pm->pd_vpu, }; struct genpd_onecell_data my_onecell_data = { .domains = my_domains, .num_domains = ARRAY_SIZE(my_domains), }; ret = of_genpd_add_provider_onecell(pdev->dev.of_node, &my_onecell_data);

onecell这个名字来源于设备树里的#power-domain-cells = <1>,表示引用时需要指定一个索引。框架会根据这个索引从domains数组里找到对应的电源域。

3.2 Consumer 设备的绑定与 Runtime PM 集成

Consumer 设备绑定电源域的过程,通常发生在驱动 probe 的早期。最常用的接口是devm_pm_domain_attach():

static int my_device_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; int ret; /* 绑定电源域 */ ret = devm_pm_domain_attach(dev, true); if (ret) return ret; /* 后续初始化 */ pm_runtime_enable(dev); ... return 0; }

第二个参数true表示如果设备树里没有指定电源域,就尝试用默认的。绑定成功之后,设备的dev->pm_domain指针就会指向对应的generic_pm_domain的domain字段。

绑定完成之后,设备的 Runtime PM 操作就会自动和电源域关联起来。当设备调用pm_runtime_put_sync()进入 Runtime Suspend 时,框架会检查这个电源域下面所有设备的状态,如果都空闲了,就会触发电源域的关闭流程。反过来,当设备调用pm_runtime_get_sync()时,框架会先确保电源域是打开的,然后再唤醒设备。

这个联动机制是电源域框架最核心的价值之一。驱动开发者不需要在代码里面显式地调用“打开电源域”或者“关闭电源域”,只需要管好自己的 Runtime PM 状态,框架会自动处理电源域的开关。

注意:devm_pm_domain_attach()的第二个参数如果传false,那么设备树里没有power-domains属性时绑定会失败。有些驱动为了兼容没有电源域的旧硬件,会传false然后检查返回值,失败就跳过电源域相关的初始化。这个做法在维护老驱动的时候很常见。

3.3 电源域的开关时机与 governor 决策逻辑

电源域什么时候真正被关掉,这个决策过程比想象中要复杂。框架不是简单地“所有设备都 idle 了就关”,中间还有 governor 的参与。

默认的simple_qos_governor会检查两个条件:第一,域下面所有设备的 Runtime PM 状态都是 suspended;第二,没有设备通过 PM QoS 提出延迟约束,或者约束值允许关闭。如果设备提出了一个很低的延迟要求(比如 100 微秒),而关闭再打开电源域需要 1 毫秒,governor 就会拒绝关闭请求。

这个逻辑在drivers/base/power/domain_governor.c里面实现。核心函数是default_power_down_ok(),它会遍历域下面的设备,检查每个设备的qos约束:

static bool default_power_down_ok(struct dev_pm_domain *pd) { struct generic_pm_domain *genpd = to_gpd(pd); struct gpd_link *link; s64 min_off_time_ns; s64 off_on_time_ns; /* 计算关闭再打开的总开销 */ off_on_time_ns = genpd->power_on_latency_ns + genpd->power_off_latency_ns; /* 检查子域 */ list_for_each_entry(link, &genpd->parent_links, child_node) { ... } /* 检查设备 */ list_for_each_entry(pdd, &genpd->dev_list, list_node) { ... if (pdd->qos_req) min_off_time_ns = min(min_off_time_ns, ...); } return off_on_time_ns <= min_off_time_ns; }

power_on_latency_ns和power_off_latency_ns这两个值需要 provider 在初始化的时候设置,它们反映了硬件电源开关的实际开销。如果这两个值设置得不准确,governor 的决策就会出问题——设得太小会导致频繁开关电源域反而增加功耗,设得太大又会导致该关的时候关不掉。

实操心得:power_on_latency_ns这个值最好通过实际测量来标定。我的做法是在 provider 的power_on回调里面打时间戳,然后统计从调用power_on到设备真正可以访问寄存器之间的时间差。多测几次取最大值,再留一点余量。这个值不需要特别精确,但数量级不能错。

4. 完整实操流程:从设备树到驱动验证

4.1 设备树配置的完整示例

假设我们要为一个虚拟的 SoC 配置电源域,这个 SoC 有一个顶层的电源控制器,下面挂了 GPU 和 VPU 两个子域。设备树可以这样写:

/* 顶层电源控制器 */ power_controller: power-controller@f0000000 { compatible = "myvendor,power-controller"; reg = <0xf0000000 0x1000>; #power-domain-cells = <1>; #address-cells = <1>; #size-cells = <0>; /* GPU 电源域 */ pd_gpu: power-domain@0 { reg = <0>; #power-domain-cells = <0>; /* 可以指定父域 */ /* power-domains = <&pd_top>; */ }; /* VPU 电源域 */ pd_vpu: power-domain@1 { reg = <1>; #power-domain-cells = <0>; }; }; /* GPU 设备 */ gpu: gpu@f1000000 { compatible = "myvendor,gpu"; reg = <0xf1000000 0x10000>; interrupts = <0 10 4>; clocks = <&clk_gpu>; power-domains = <&power_controller 0>; status = "okay"; }; /* VPU 设备 */ vpu: vpu@f2000000 { compatible = "myvendor,vpu"; reg = <0xf2000000 0x10000>; interrupts = <0 11 4>; clocks = <&clk_vpu>; power-domains = <&power_controller 1>; status = "okay"; };

这里power_controller节点的#power-domain-cells = <1>表示引用时需要指定一个索引。GPU 设备用<&power_controller 0>引用索引 0 对应的电源域,VPU 用<&power_controller 1>引用索引 1。

子节点pd_gpu和pd_vpu的#power-domain-cells = <0>表示它们也可以被直接引用,不需要额外参数。这两种引用方式框架都支持,选哪种看个人习惯和具体需求。

4.2 Provider 驱动的关键实现

Provider 驱动的核心是实现power_on和power_off回调,然后注册电源域。下面是一个简化的实现框架:

struct my_power { struct device *dev; void __iomem *base; struct generic_pm_domain pd_gpu; struct generic_pm_domain pd_vpu; struct genpd_onecell_data onecell_data; }; static int my_power_on(struct generic_pm_domain *domain) { struct my_power *pm = container_of(domain, struct my_power, pd_gpu); u32 val; /* 根据 domain 的名字或者地址判断是哪个域 */ if (domain == &pm->pd_gpu) { val = readl(pm->base + GPU_PWR_REG); val |= GPU_PWR_BIT; writel(val, pm->base + GPU_PWR_REG); } else if (domain == &pm->pd_vpu) { val = readl(pm->base + VPU_PWR_REG); val |= VPU_PWR_BIT; writel(val, pm->base + VPU_PWR_REG); } /* 等待电源稳定 */ udelay(10); return 0; } static int my_power_off(struct generic_pm_domain *domain) { struct my_power *pm = container_of(domain, struct my_power, pd_gpu); u32 val; if (domain == &pm->pd_gpu) { val = readl(pm->base + GPU_PWR_REG); val &= ~GPU_PWR_BIT; writel(val, pm->base + GPU_PWR_REG); } else if (domain == &pm->pd_vpu) { val = readl(pm->base + VPU_PWR_REG); val &= ~VPU_PWR_BIT; writel(val, pm->base + VPU_PWR_REG); } return 0; } static int my_power_probe(struct platform_device *pdev) { struct my_power *pm; int ret; pm = devm_kzalloc(&pdev->dev, sizeof(*pm), GFP_KERNEL); if (!pm) return -ENOMEM; pm->dev = &pdev->dev; pm->base = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(pm->base)) return PTR_ERR(pm->base); /* 初始化 GPU 电源域 */ pm->pd_gpu.name = "gpu_power"; pm->pd_gpu.power_on = my_power_on; pm->pd_gpu.power_off = my_power_off; pm->pd_gpu.power_on_latency_ns = 10000; /* 10us */ pm->pd_gpu.power_off_latency_ns = 5000; /* 5us */ ret = pm_genpd_init(&pm->pd_gpu, NULL, true); if (ret) return ret; /* 初始化 VPU 电源域 */ pm->pd_vpu.name = "vpu_power"; pm->pd_vpu.power_on = my_power_on; pm->pd_vpu.power_off = my_power_off; pm->pd_vpu.power_on_latency_ns = 10000; pm->pd_vpu.power_off_latency_ns = 5000; ret = pm_genpd_init(&pm->pd_vpu, NULL, true); if (ret) return ret; /* 注册 onecell provider */ pm->onecell_data.domains = devm_kcalloc(&pdev->dev, 2, sizeof(*pm->onecell_data.domains), GFP_KERNEL); if (!pm->onecell_data.domains) return -ENOMEM; pm->onecell_data.domains[0] = &pm->pd_gpu; pm->onecell_data.domains[1] = &pm->pd_vpu; pm->onecell_data.num_domains = 2; ret = of_genpd_add_provider_onecell(pdev->dev.of_node, &pm->onecell_data); if (ret) { dev_err(&pdev->dev, "failed to add provider\n"); return ret; } platform_set_drvdata(pdev, pm); return 0; }

这个实现里面有几个关键点。pm_genpd_init()的第三个参数传true,表示电源域初始状态是关闭的,框架会在第一个 consumer 绑定的时候自动调用power_on。power_on_latency_ns和power_off_latency_ns根据实际硬件填写,这里假设是 10 微秒和 5 微秒。

4.3 验证电源域是否正常工作

驱动加载之后,怎么确认电源域框架工作正常?最直接的方法是看 sysfs 和 debugfs。

sysfs 里面,每个电源域会在/sys/kernel/debug/pm_genpd/下面有一个目录(需要 debugfs 挂载)。目录里面有几个文件:

# 查看所有电源域 ls /sys/kernel/debug/pm_genpd/ # 查看某个域的当前状态 cat /sys/kernel/debug/pm_genpd/gpu_power/current_state # 查看域下面的设备 cat /sys/kernel/debug/pm_genpd/gpu_power/devices # 查看子域 cat /sys/kernel/debug/pm_genpd/gpu_power/sub_domains

current_state显示on或者off,反映电源域的当前状态。devices列出所有绑定到这个域的设备。sub_domains列出子域。

另一个验证方法是看内核日志。在 provider 的power_on和power_off回调里面加pr_debug或者dev_dbg,然后打开动态调试:

# 打开电源域相关的动态调试 echo 'file domain.c +p' > /sys/kernel/debug/dynamic_debug/control echo 'file domain_governor.c +p' > /sys/kernel/debug/dynamic_debug/control # 然后触发设备的 Runtime PM echo auto > /sys/bus/platform/devices/f1000000.gpu/power/control

如果一切正常,你会在 dmesg 里面看到电源域开关的日志,以及 governor 的决策过程。

实操心得:调试电源域问题的时候,/sys/kernel/debug/pm_genpd/下面的信息非常有用,但很多人不知道这个目录的存在。另外,如果current_state一直是on从来不变,先检查是不是有设备没有正确进入 Runtime Suspend,用cat /sys/bus/platform/devices/*/power/runtime_status可以批量查看所有设备的状态。

5. 常见问题与排查技巧实录

5.1 电源域绑定失败问题排查

绑定失败是最常见的问题,表现是驱动 probe 的时候devm_pm_domain_attach()返回错误,或者内核日志里面出现failed to attach device to power domain之类的报错。

排查思路按这个顺序来:第一,确认设备树里面power-domains属性写对了没有,包括 provider 节点的引用是否正确、cells 数量是否匹配。第二,确认 provider 驱动是否已经加载,如果 provider 和 consumer 在同一个内核模块里面,要确保 provider 先注册。第三,确认 provider 节点的#power-domain-cells和 consumer 引用时提供的参数数量是否一致。

一个很隐蔽的问题是 provider 节点的compatible字符串和驱动里面的of_match_table不匹配,导致 provider 驱动根本没 probe,自然也就没有注册电源域。这种情况内核日志里面通常不会有明显的报错,只是 consumer 绑定的时候找不到 provider。

5.2 电源域无法关闭的典型原因

电源域一直处于on状态,关不掉,这个问题也很常见。原因通常有这几类:

第一类,域下面有设备没有进入 Runtime Suspend。用cat /sys/kernel/debug/pm_genpd/<domain>/devices列出所有设备,然后逐个检查它们的runtime_status。只要有一个设备是active,整个域就关不掉。

第二类,governor 拒绝了关闭请求。这种情况通常是因为设备的 PM QoS 延迟约束太严格。可以临时把 governor 换成always_on来验证是不是 governor 的问题:

# 查看当前 governor cat /sys/kernel/debug/pm_genpd/gpu_power/governor # 临时切换 governor(需要内核支持) echo always_on > /sys/kernel/debug/pm_genpd/gpu_power/governor

如果换成always_on之后域能关掉了,说明是 governor 的决策逻辑在起作用,需要检查设备的 QoS 约束是否合理。

第三类,域被标记为GENPD_FLAG_ALWAYS_ON。这个标志表示电源域永远不能关闭,框架会忽略所有的关闭请求。检查 provider 初始化的时候有没有误设这个标志。

第四类,子域还在活跃状态。如果父域下面有子域,子域没有关闭的话父域也关不掉。用cat /sys/kernel/debug/pm_genpd/<domain>/sub_domains查看子域状态。

5.3 电源域开关顺序与依赖关系处理

当多个电源域之间存在依赖关系时,开关顺序就变得很重要。比如 GPU 电源域依赖一个顶层的电源域,顶层域必须先打开 GPU 域才能打开,关闭的时候顺序反过来。

框架通过parent_links和child_links来维护这种依赖关系。在设备树里面,通过power-domains属性在子域节点里面引用父域:

pd_top: power-domain@0 { reg = <0>; #power-domain-cells = <0>; }; pd_gpu: power-domain@1 { reg = <1>; #power-domain-cells = <0>; power-domains = <&pd_top>; };

这样框架就知道pd_gpu是pd_top的子域。打开pd_gpu的时候,框架会先确保pd_top是打开的;关闭pd_top的时候,框架会先关闭pd_gpu。

注意:子域和父域的power_on/power_off回调是在不同的 provider 驱动里面实现的,框架只负责协调调用顺序,不负责具体的硬件操作。如果父域和子域在同一个 provider 里面,可以在power_on回调里面自己处理顺序,但更推荐用框架的父子关系来描述,这样逻辑更清晰。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
绑定失败,返回 -ENODEV设备树 power-domains 属性缺失或错误检查 dts 中 power-domains 属性修正设备树,确保引用正确的 provider
绑定失败,返回 -EPROBE_DEFERprovider 驱动尚未加载查看 dmesg 中 provider 驱动的 probe 日志确保 provider 驱动先加载,或使用 -EPROBE_DEFER 机制
电源域一直 on,无法关闭域下有设备未 suspend查看 /sys/kernel/debug/pm_genpd/*/devices检查设备 Runtime PM 状态,修复未 suspend 的设备
电源域一直 on,无法关闭governor 拒绝关闭查看 governor 类型和 QoS 约束调整 QoS 约束或更换 governor
电源域一直 on,无法关闭设置了 GENPD_FLAG_ALWAYS_ON检查 provider 初始化代码移除不必要的 ALWAYS_ON 标志
电源域开关导致系统崩溃开关顺序错误检查父子域关系配置在设备树中正确配置 power-domains 父子关系
功耗没有明显降低电源域开关延迟设置不当检查 power_on/off_latency_ns实测标定延迟值,调整 governor 参数

6. 进阶话题与性能调优建议

6.1 性能状态(Performance State)管理

除了简单的开关,电源域框架还支持性能状态管理。有些电源域可以在打开状态下运行在不同的性能档位,比如电压和频率的组合。这个功能通过set_performance_state回调来实现。

Consumer 设备可以通过dev_pm_genpd_set_performance_state()来请求特定的性能状态。框架会聚合域下面所有设备的请求,取最大值作为域的实际性能状态。这个机制在多设备共享一个电源域的场景下很有用——比如 GPU 和显示控制器共享一个域,GPU 需要高性能的时候,整个域都会运行在高性能档位。

/* Consumer 请求性能状态 */ ret = dev_pm_genpd_set_performance_state(dev, state); if (ret) dev_err(dev, "failed to set performance state\n");

Provider 需要实现set_performance_state回调,根据传入的状态值配置硬件。状态值的具体含义由 provider 定义,框架不做解释。

6.2 电源域与系统休眠的配合

系统进入休眠(System Suspend)的时候,电源域框架会参与整个流程。在dpm_suspend()阶段,框架会调用每个电源域的power_off回调,按照父子关系的顺序从叶子节点往上关。在dpm_resume()阶段,顺序反过来,从根节点往下开。

这个过程中,in_suspend标志会被设置,governor 的决策逻辑也会有所不同——系统休眠的时候,即使有 QoS 约束,电源域通常也会被强制关闭,因为整个系统都要断电了。

如果某个电源域在系统休眠时需要保持供电(比如用于唤醒的域),可以在 provider 里面设置GENPD_FLAG_ACTIVE_WAKEUP标志,这样框架在系统休眠时会保持这个域打开。

6.3 调试电源域问题的实用技巧

除了前面提到的 debugfs 和动态调试,还有几个实用的调试技巧。

第一个是使用pm_genpd_summary。在 debugfs 里面有一个pm_genpd_summary文件,它会打印所有电源域的摘要信息,包括域名、状态、子域数量、设备数量、以及 governor 的决策统计。这个文件在快速了解系统电源域整体状态的时候非常有用。

cat /sys/kernel/debug/pm_genpd/pm_genpd_summary

输出大概长这样:

domain status slaves /device runtime status ---------------------------------------------------------------------- gpu_power on 0 /devices/platform/f1000000.gpu active vpu_power off 0 /devices/platform/f2000000.vpu suspended

第二个技巧是在 governor 的决策函数里面加 tracepoint。内核里面已经有一些预定义的 tracepoint 用于电源域调试,比如power_domain_target。打开这些 tracepoint 可以看到 governor 的决策过程:

echo 1 > /sys/kernel/debug/tracing/events/power_domain/enable cat /sys/kernel/debug/tracing/trace_pipe

第三个技巧是使用ftrace跟踪pm_genpd相关的函数调用。可以设置 function graph tracer 来跟踪pm_genpd_poweroff和pm_genpd_poweron的调用栈,看看是谁触发了电源域的开关。

实操心得:电源域的问题往往不是孤立的,它和时钟、稳压器、Runtime PM 都有关系。我排查这类问题的时候,习惯先把/sys/kernel/debug/pm_genpd/pm_genpd_summary、/sys/kernel/debug/clk/clk_summary、/sys/kernel/debug/regulator/regulator_summary这三个文件都看一遍,对系统整体的电源状态有个全局认识,然后再定位具体问题。这个习惯帮我省了很多时间。

6.4 电源域框架的扩展与定制

框架本身提供了一些扩展点,允许平台代码定制行为。比如可以自定义 governor 来实现特定的电源管理策略。自定义 governor 需要实现struct dev_power_governor结构体:

struct dev_power_governor my_governor = { .power_down_ok = my_power_down_ok, .suspend_ok = my_suspend_ok, };

power_down_ok返回true表示允许关闭电源域,返回false表示拒绝。suspend_ok用于系统休眠场景。自定义 governor 可以在pm_genpd_init()的时候传入,替换默认的simple_qos_governor。

另一个扩展点是GENPD_FLAG_*标志。框架定义了一系列标志来控制电源域的行为,常用的有:

  • GENPD_FLAG_ALWAYS_ON:域永远不关闭
  • GENPD_FLAG_ACTIVE_WAKEUP:域在系统休眠时保持打开
  • GENPD_FLAG_IRQ_SAFE:域的开关操作可以在中断上下文中执行
  • GENPD_FLAG_FAST_IRQ:快速中断安全的开关操作

这些标志在 provider 初始化的时候设置,影响框架对电源域的管理策略。选择哪些标志取决于硬件的特性和系统的需求,没有固定的答案,需要根据实际情况权衡。

我在实际项目里面遇到过一个场景:某个电源域的开关操作需要访问 I2C 总线,而 I2C 传输可能会睡眠,所以这个域不能设置GENPD_FLAG_IRQ_SAFE。但另一个域的开关只是写一个寄存器,就可以设置这个标志,让框架在中断上下文里面也能安全地开关它。这种差异化的配置需要根据每个域的具体实现来决定。

电源域通用框架从引入到现在,已经成为了 Linux 内核电源管理不可或缺的一部分。它的设计思想——用设备树描述拓扑、用统一 API 屏蔽硬件差异、用 governor 做策略决策——在时钟、稳压器、引脚控制等子系统里面都能看到影子。把这套框架吃透,不仅对做电源管理有帮助,对理解 Linux 设备驱动模型的整体设计哲学也很有价值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询