做功耗管理和内核驱动,devfreq 这层框架迟早要碰。它跟 cpufreq 很像,但更偏门、更容易让人绕晕。尤其在 DDR、GPU、NPU 这些非 CPU 设备的动态频率调节上,devfreq 几乎是绕不开的通用方案。我最早是在一个带 AI 加速器的平台上做内存带宽管控,被供应商的“魔改总线和自定义 governor”折磨了一整轮,后来才意识到,根子上就得先把 devfreq framework 吃透。这篇文章把 devfreq 的来龙去脉、核心对象、注册流程和调试经验整理出来,给正要入门或者已经被它坑过的内核工程师一个可以照着抄的路线。
1. devfreq 解决的是哪一类功耗痛点
1.1 cpufreq 与 devfreq 的本质差异
很多刚接触 devfreq 的同学第一反应是:这不就是 cpufreq 换了个名吗?还真不是一回事。cpufreq 调节的是 CPU 核心的电压频率,它面对的硬件是 CPU 域里的 PLL、时钟树和电源域。devfreq 调节的则是 CPU 之外的设备频率,典型对象包括 DDR 控制器、总线互联、GPU、NPU、ISP、多媒体编解码器。
这两者背后的模型差异非常大。CPU 的工作负载基本靠系统调度器体现,cpufreq 的 governor 可以根据运行队列长度、PELT 负载等判断要不要升频;而一个 DDR 控制器的“繁忙程度”没有那么直接的指标可以看,你得靠硬件计数器统计端口访问量、总线占用时间、buffer 中 pending transaction 的数目等。devfreq 设计的目的就是把“设备自身负载统计”和“频率调节策略”这两件事解耦。上层 governor 只关心一个抽象的 busy_time/total_time,具体怎么统计是设备驱动自己实现 get_dev_status 回调的事。
所以理解 devfreq 的关键,不是把它当成一个“通用频率调节模块”,而是当成一套“可插拔的 DVFS 策略容器”。它把不同设备的负载采集细节挡在框架外面,让 governor 用同一套策略逻辑去适配完全不同的硬件。
1.2 典型应用场景与边界
devfreq 最常见的使用场景有三个。第一个是 DDR/内存总线调频,手机 SoC 里 memory throughput 直接决定了后台应用切换时的流畅度和整机功耗,这一档调频做得烂,会出现“亮屏不动也发热”的诡异现象。第二个是 GPU 调频,游戏场景下 GPU 负载变化非常快,轮询周期设大了画面卡顿,设小了无谓功耗上升。第三个是总线/NPU 这类有固定工作负载窗口的加速器,它们的频率切换往往要配合 QoS 请求,简单轮询反而容易出错。
边界场景也要想清楚。像 Display 这类有严格时序要求的 IP,就不太适合用 devfreq 的轮询模型去做频率调节,因为一帧的刷新窗口非常短,等到统计出负载再切换频率,已经影响帧率了。这类场景更适合在硬件层做 QoS 反馈或者提前预算。另外,对外设频率变化有强实时性要求的场合,比如 PCIe 链路协商,也不该通过 devfreq 这种软件轮询去碰。
我有次在一个 video encode 的 IP 上直接套 devfreq 的 simple_ondemand,结果每秒频率都在跳,编码器输出时间戳都乱了。后来改成 fixed performance governor 并把 target 带宽设置为固定值,问题才消停。所以先搞清楚设备能不能接受动态频率,比怎么调 governor 更重要。
1.3 没有框架时的旧做法对比
在没有 devfreq 的时代,调一个外设频率通常是在设备驱动里自己写一套:
- 起一个内核延时工作队列,每隔几十毫秒采样某个寄存器。
- 自己实现几种调频策略,比如“超过某个阈值就提频”“低于某个阈值就降频”。
- 自己管理 OPP 表里的电压频率对应关系。
- 自己在 sysfs 里造各种 debug 节点,方便调试。
这套做法的毛病很典型:每个驱动都造一遍轮子,阈值写死在驱动里,策略没法在外面切换;其次,每次调频打出的 log 风格都不一样,排查问题全靠 grep dmesg。Devfreq 框架把这些公共逻辑抽出来,让设备驱动只做“设置频率”和“提供负载统计”这两件事,其余轮询、策略选择、频率上下限、统计周期、sysfs 可视化全部由框架统一处理。
实际接入过旧代码的同事应该深有体会,那种自己管理的调频代码经常伴随着“频率高下不去”“负载统计不准”这种问题,而且因为和业务驱动耦合太深,调起来非常难受。devfreq 相当于给 DVFS 打了一个标准件,虽然你不一定喜欢它的轮询模型,但它至少给你一个可以讨论和复现的容器。
2. devfreq framework 核心架构剖析
2.1 三个关键对象:device、profile、governor
devfreq 框架的核心抽象是三个东西:struct devfreq、struct devfreq_dev_profile、struct devfreq_governor。
- struct devfreq 是框架对外暴露的设备实例,也是 sysfs 里注册节点的载体。
- struct devfreq_dev_profile 是设备驱动向框架提交的“硬件描述文件”,里面定义了轮询周期、调频回调、状态采集回调和当前频率读取回调。
- struct devfreq_governor 是调频策略的载体,负责根据 profile 提供的负载状态,计算目标频率后交给 devfreq 去执行。
设备驱动在 probe 阶段做三件事:填充 devfreq_dev_profile,设置好 OPP 表,最后调用 devfreq_add_device() 把设备交给框架。governor 则由框架通过传入的 governor_name 去查找,找到的话,设备实例上电后就开始周期调度。
static const struct devfreq_dev_profile my_dev_profile = { .polling_ms = 50, .target = my_dev_target_set, .get_dev_status = my_dev_get_status, .get_cur_freq = my_dev_get_cur_freq, };关键点是 profile 一旦注册就不建议随便改,尤其是回调函数不能指向临时变量。我见过有驱动把 profile 放在栈上或者 module_init 的局部变量里,结果 devfreq 线程一跑就崩,这类问题内存损坏工具很难查,很容易被误判成“供应商驱动 bug”。
2.2 框架内部数据流向
devfreq 的数据流向可以简化成三个环节:
- 周期定时器唤醒 monitor 函数,触发一次负载统计。
- monitor 调用 profile 的 get_dev_status 回调,拿到当前 busy_time、total_time 和 current_frequency。
- 当前正在使用的 governor 根据 status 计算目标频率,然后走 devfreq_set_target() 去执行。
执行阶段会做一次 OPP 表查找,把目标频率对齐到硬件支持的最近档位。这里有两种查找方向:向上取整和向下取整。通常升频时用 OPP 表里的下一档或 ceil,降频时用 floor。框架里通过 dev_pm_opp_find_freq_floor() 和 dev_pm_opp_find_freq_ceil() 这两个 API 来匹配。
opp = dev_pm_opp_find_freq_floor(dev, &target_freq); if (!IS_ERR(opp)) { dev_pm_opp_get_freq(opp); dev_pm_opp_put(opp); }调频之前框架会检查这次目标频率是否落在 min_freq 和 max_freq 的区间里。区间通过 sysfs 暴露给用户态,也可以在驱动里通过 devfreq_set_min_freq() 系列接口设置。最终 target 回调执行成功后,框架会把本次频率更新记录到 trans_stat 节点,方便后续查看调节轨迹。
2.3 源码位置与驱动注册入口
devfreq 框架主体在 drivers/devfreq/ 目录下。devfreq.c 是核心,governor.c 是 governor 注册与查找,governor_simpleondemand.c 和 governor_performance.c 分别对应不同的策略实现。如果平台用了 complex memory bus,往往还会在 drivers/devfreq/ 下再放对应平台的 devfreq-event 驱动。
注册入口我记得很清楚:
struct devfreq *devfreq_add_device(struct device *dev, struct devfreq_dev_profile *profile, const char *governor_name, void *data);这个函数会在 sysfs 中创建 /sys/class/devfreq/xxx 节点,启动 monitor 内核定时器,并把设备标记为由 framework 管理。对应地,注销用 devfreq_remove_device()。
需要注意 devfreq_add_device() 传入的 dev 不是 profile 里设置频率时的那个直接设备,它更偏向于框架的管理节点。在常见实现里,dev 指向外设平台设备对应的 struct device,比如一个 platform_device 的 &pdev->dev。而 profile 的 target 回调里,你要通过 dev_get_drvdata(dev) 拿到驱动私有结构,再从私有结构里取 clk 和寄存器基地址。
还有一点,如果模块依赖的 governor 还没加载,devfreq_add_device() 会失败返回 -EPROBE_DEFER。这个行为对驱动 probe 影响非常大,实际调试时经常碰到“设备驱动明明写好了,却一直不 probe”,结果就是 governor 模块没有被系统加载。后面我会专门讲这个坑。
3. 关键细节解析与 sysfs 接口
3.1 governor 的注册与加载顺序
governor 在内核里通过 devfreq_governor_init() 注册,这是一个宏,本质是在 module_init 阶段调用 devfreq_governor_register()。
static struct devfreq_governor my_governor = { .name = "my_governor", .get_target_freq = my_governor_get_target_freq, .event_handler = my_governor_event_handler, }; devfreq_governor_init(my_governor);每个 governor 至少要实现 get_target_freq(),框架的 monitor 会在每次唤醒后调用这个方法,拿到一个新的目标频率。governor 内部用不用 OPP 表、用什么样的负载阈值模型,完全自己控制。
governor 加载顺序容易出问题。比如你给设备驱动 module_init 里加了 devfreq_add_device(),但 governor 模块是用 modprobe 手动加载的,两个模块谁先谁后不可控,设备驱动 probe 就会遇到 -EPROBE_DEFER。解决方式有几个:
- 把 governor 编入内核,而不是编成模块。
- 在设备驱动的 module_init 里先调用 try_then_request_governor(),主动请求 governor 模块。
- 在系统 init 脚本里严格排序加载。
我最后的做法是:devfreq governor 如果能进 defconfig 就直接编进内核。这类策略代码本身很小,没必要为了省这点空间给自己埋雷。
3.2 OPP 表与 target 回调
OPP 表是 devfreq 调频的地基。它描述设备有哪些可用频率,以及每个频率对应的电压。内核里很多时候把 OPP 表放在设备树里,格式大概像这样:
opp-table { compatible = "operating-points-v2"; opp-200000000 { opp-hz = /bits/ 64 <200000000>; opp-microvolt = <900000>; }; opp-400000000 { opp-hz = /bits/ 64 <400000000>; opp-microvolt = <950000>; }; };设备驱动在 probe 阶段通过 dev_pm_opp_of_add_table() 解析这张表,之后就可以用 dev_pm_opp_find_freq_floor() 等接口在表里做频率匹配。
target 回调里面做的事大体是:先解析 dev_pm_opp_find_freq_floor() 得到的标准频率,再调用 clk_set_rate() 把时钟树切到对应频率。需要注意的是,某些平台的时钟树上还有 voltage domain 调整,需要额外调用 regulator_set_voltage(),这个动作往往会带来较大延时,执行时要考虑是否会阻塞 monitor 线程。
我实际遇到过的坑是:OPP 表里写了两个相同频率、不同电压的节点,看起来像是一个“名义频率”一个“offset 频率”。dev_pm_opp_find_freq_floor() 在这种场景下行为不好预判,直接导致 clk_set_rate() 设置完成了,但电压没有跟着走。最好的做法是确保 OPP 表频率节点的唯一性。
3.3 devfreq-event 与实时负载采集
负载统计是 devfreq 能否正常工作的前置条件。很多 SoC 上设备自身的计数器很容易拿到,比如 bus monitor 提供的带宽占用量、PLL 反馈频率等。但有些平台的 DDR 控制器只提供带宽占用率,不提供传统意义上的 busy_time/total_time,这时候就要用 devfreq-event 设备。
devfreq_event_dev 是框架提供的另一类抽象,专门用于包装“硬件事件计数器”这类资源。它通过 devfreq_event_get_edev_by_phandle() 在设备树里获取对端 event 设备,然后调用 get_event 类接口读取硬件计数值,再把原始值换算成加载率。
实际写代码时,我通常会在 get_dev_status() 里同时读两个 event 计数器:一个统计活动周期,一个统计总周期,然后算 busy_time 和 total_time。有的平台 event 设备的计数器会自动 wrap,换算时注意位数溢出,避免出现“偶尔统计出 120% 负载”的灵异现象。
示例:
static int my_dev_get_status(struct device *dev, struct devfreq_dev_status *stat) { struct my_dev *mdev = dev_get_drvdata(dev); unsigned long busy, total; devfreq_event_get_event(mdev->load_edev, &busy, &total); stat->busy_time = busy; stat->total_time = total; stat->current_frequency = clk_get_rate(mdev->clk); return 0; }如果 get_dev_status 频繁被调用,event 设备的读取本身也会带来功耗和总线带宽开销。所以 polling_ms 要结合硬件事务频率来定,不是越短越好。DDR 这种高频设备常见设 50ms,GPU 这种游戏负载剧烈的设备可能会设到 20ms,我见过有人调成 5ms,结果系统平均功耗直接涨了 5%,频率曲线看着细碎,实际效果并没好多少。
3.4 常用 sysfs 节点速查表
devfreq 框架挂载在 class 设备下,接入设备后,系统里就会出现/sys/class/devfreq/<device-name>/目录。写驱动时常用这几个节点来验证:
| 节点 | 作用 | 常见操作 |
|---|---|---|
| governor | 查看/切换当前 governor | echo simple_ondemand > governor |
| available_governors | 列出所有可用策略 | cat 查看 |
| cur_freq | 当前实际频率 | cat 查看 |
| target_freq | 框架认为应该达到的频率 | cat 查看 |
| available_frequencies | 从 OPP 表导出可用的频率档位 | cat 查看 |
| min_freq / max_freq | 频率上下限 | echo 写入 |
| polling_interval | 轮询周期,单位 ms | echo 20 写入 |
| trans_stat | 频率切换统计 | cat 查看,echo 0 清零 |
我排查频率“卡死”时,第一步就是同时看 cur_freq 和 target_freq。如果 target_freq 一直在变而 cur_freq 不变,说明 target 回调或 clk_set_rate 环节有问题;如果两个都不变,再看 governor 是不是 performance,这个 governor 会把频率直接锁到最高档,不随着负载走。
还有一个细节:available_frequencies 在有些平台上显示的是“启动时就固定的一串 OPP”,即使 OPP 表后来被动过,这个节点内容也不会刷新。所以别只依赖 sysfs 里的列表去判断硬件最大能力,必要时到设备树里实际确认一下。
4. 实操示例:把一个外设接入 devfreq
4.1 定义 profile 和私有数据结构
假设我现在要为某个媒体 IP 写 devfreq 驱动。第一步先把私有数据准备齐,至少要有 clk 指针、event 设备指针、以及一个串行化的 mutex,避免调频过程中和中断处理函数冲突。
struct my_dev { struct device *dev; struct clk *clk; struct devfreq *devfreq; struct devfreq_event_dev *load_edev; struct mutex lock; unsigned long busy_time_us; unsigned long total_time_us; };结构体里我习惯把 busy_time 和 total_time 取出来缓存,而不是每次都现读。不是担心 event 设备读不出来,而是有些平台的 event 寄存器读取时序比较脆,调用频率太高会触发总线 STALL,反而影响真实性能。
合池设计:现在这些数据结构,到底要不要带 lock,取决于你调频回调是否会跟某个硬实时的中断路径抢频率。如果不会,其实一个 seqcount_t 都够用;但如果没把握,宁可先用 mutex,稳定性优先,锁开销后面再优化。
4.2 实现 target 和 get_dev_status 回调
target 回调是整个调频的执行口,里面要把频率档次对齐到硬件支持的值。
static int my_dev_target_set(struct device *dev, unsigned long *freq, u32 flags) { struct my_dev *mdev = dev_get_drvdata(dev); struct dev_pm_opp *opp; unsigned long target; int ret; target = *freq; opp = dev_pm_opp_find_freq_floor(dev, &target); if (IS_ERR(opp)) return PTR_ERR(opp); dev_pm_opp_put(opp); ret = clk_set_rate(mdev->clk, target); if (ret) return ret; *freq = target; return 0; }get_dev_status 则要把统计到的忙闲数据填进 framework 要求的结构体。这里有个取舍:直接从硬件寄存器读,还是先用某种方式算出比率?我建议在驱动里做一个小的平滑处理,至少把上一轮数值过滤一下,不然某些瞬时波动会把 governor 压到频繁升频降频。
static int my_dev_get_status(struct device *dev, struct devfreq_dev_status *stat) { struct my_dev *mdev = dev_get_drvdata(dev); unsigned long busy = 0, total = 0; devfreq_event_get_event(mdev->load_edev, &busy, &total); mdev->busy_time_us = (mdev->busy_time_us + busy) >> 1; mdev->total_time_us = (mdev->total_time_us + total) >> 1; stat->busy_time = mdev->busy_time_us; stat->total_time = mdev->total_time_us; stat->current_frequency = clk_get_rate(mdev->clk); return 0; }这个平滑式子当然不是万金油,如果业务场景对突发负载很敏感,就不适合取平均值,得改用带上升沿加速的滤波算法。实际调频效果好不好,往往就体现在这些细节上。
4.3 注册 devfreq 设备并挂载 governor
接下来把这些回调组进 devfreq_dev_profile,调用 devfreq_add_device() 完成注册。
static const struct devfreq_dev_profile my_dev_profile = { .polling_ms = 50, .target = my_dev_target_set, .get_dev_status = my_dev_get_status, .get_cur_freq = my_dev_get_cur_freq, }; static int my_dev_probe(struct platform_device *pdev) { struct my_dev *mdev; mdev = devm_kzalloc(&pdev->dev, sizeof(*mdev), GFP_KERNEL); platform_set_drvdata(pdev, mdev); mdev->clk = devm_clk_get(&pdev->dev, "media_clk"); mutex_init(&mdev->lock); mdev->devfreq = devfreq_add_device(&pdev->dev, &my_dev_profile, "simple_ondemand", NULL); if (IS_ERR(mdev->devfreq)) return PTR_ERR(mdev->devfreq); return 0; }注册完成后,cat /sys/class/devfreq/ 下应该能看到设备,接下来就能在用户态测试频率切换效果了。这里要注意:devfreq_add_device() 内部会请求 governor,如果返回 -EPROBE_DEFER,probe 函数要正确返还这个错误码,让 deferred probe 机制后续重新触发。
4.4 用 sysfs 实测效果
我一般会先做一个“人工负载”测试,比如让某个多媒体线程持续跑起来,然后观察 target_freq 和 trans_stat 的变化:
watch -n 0.5 cat /sys/class/devfreq/1000000.media/cur_freq cat /sys/class/devfreq/1000000.media/trans_stat echo 0 > /sys/class/devfreq/1000000.media/trans_stat线程跑起来后,频率应该从低档逐步升到中高档。等线程停掉,负载降下来,频率再在下一个轮询周期回落。如果频率只升不降,说明 governor 没调用 get_dev_status,或者统计值有问题;如果只在两档间反复横跳,那就该检查 busy_time 的平滑系数和 polling_ms 是否设得太小。
5. 调试经验与常见问题排查
5.1 频率不变化,先查这几项
频率不动的排查顺序很重要,直接乱翻代码容易陷入泥潭。我会按照下面的顺序来:
- 看
/sys/class/devfreq/<dev>/governor,确认当前 governor 是不是 performance。performance 会把频率固定在高档,单纯看 cur_freq 不变是正常行为。 - 看 cur_freq 和 target_freq,如果 target_freq 一直变而 cur_freq 不变,问题在 target 回调或 clk_set_rate。
- 看 trans_stat,这个节点记录了每一档的切换统计。如果切换次数始终为零,说明 governor 没有触发调频动作。
- 检查 get_dev_status 有没有被调用。可以用 tracepoint,或者直接在驱动里加一个 dev_info 打印,注意要把它放在 rate_limited 的路径里。
一个细节:target 回调里我喜欢加一行dev_dbg(),调频时打印旧值和新值。但如果这条打印太多,被 printk 的限流机制遮掉,反而找不到关键信息。更好的做法是在 struct devfreq 挂到 sysfs 之后,用 ftrace 的 function_graph 跟踪 devfreq_set_target() 和 clk_set_rate() 的调用链。
5.2 governor 无法使用的常见原因
available_governors里没有任何 governor 的情况并不少见。通常原因就是 governor 模块没有加载,或者编译时没有选上对应的 Kconfig。内核里要打开 CONFIG_DEVFREQ_GOV_SIMPLE_ONDEMAND 等,否则即使 devfreq 框架存在也无策略可用。
另一种情况是 governor 绪域启动异常,比如 governor 的 event_handler 在 DEVFREQ_GOV_START 事件中没有正确初始化内部状态。表现为 sysfs 写 governor 时直接报错,或者设备 probe 成功但 monitor 线程不启动。
我实践中最保险的定位方式是把所有 devfreq governor 都编进内核,并打开 CONFIG_DEVFREQ_EVENT。若平台不支持 event 设备,至少框架的缺省路径还能跑通。
5.3 devfreq 与 cpufreq、cpuidle 的联动
实际项目里,这几个电源管理模块是联动的,不能孤立看待。比如某个平台的总线频率和 DDR 频率是耦合的,CPU 频率提高后总线负载也会变大,busy_time 会跟着涨,然后 devfreq 会继续拉高 DDR 频率。如果你看到 CPU 和 DDR 频率联动得非常“激进”,往往不是 devfreq 本身问题,而是负载统计样本中含了 CPU 引起的瞬时访问。
面对这类联动问题,我的习惯是先在 devfreq 侧加 max_freq 限制,把峰值频率锁住,再做 cpufreq 和 devfreq 的交叉开关实验。比如先把 devfreq 的 max_freq 固定在 400MHz,再跑负载,看功耗变化;固定到 800MHz 再跑,对比性能拐点。这样就把“哪一档是收益拐点”摸出来后,再根据场景去设备树里设 default level,而不是靠 governor 无限动态调整。
同样的,cpuidle 也会影响 devfreq。当 CPU 进入 deep idle,总线时钟会被拉低,devfreq 的 get_dev_status 可能采到一段很长的 idle 区间,导致负载统计被拉平。所以很多平台会把 devfreq 的 monitor 在系统 suspend/resume 时暂停,避免 suspend 期间采集到奇怪的活跃数据。如果 devfreq 驱动没有处理 DEVFREQ_GOV_SUSPEND / DEVFREQ_GOV_RESUME 这两个事件,就要特别小心睡眠唤醒后的频率状态。
5.4 trans_stat 和 tracepoint 定位问题
trans_stat 是我排查频率切换问题时用得非常多的一个节点。它的格式类似一个矩阵,每一行表示一条横跨的频率转换路径,后面是该路径的切换次数。如果发现某个频率档几乎没被跳到,而 program 又明显觉得卡,那可能就是 OPP 表缺少某个中间档位,导致从低档直接升到高档,功耗不降反而高。
cat /sys/class/devfreq/1000000.media/trans_stat From : To : 200000000 400000000 800000000 200000000: 0 2 0 400000000: 1 0 1 800000000: 0 1 0从这个输出能看出,设备没有经过 200MHz 到 800MHz 的切档,说明 governor 的调频步进设计里,升档逻辑跳太快。这个现象常见于使用 max/avg 负载估算的 governor,中间档在阈值设计里变成了死区。
如果 trans_stat 也看不出异常,就用 ftrace 跟踪 devfreq_set_target() 的调用参数。在内核配置中打开 FUNCTION_TRACER,然后:
echo devfreq_set_target > /sys/kernel/debug/tracing/set_ftrace_filter echo 1 > /sys/kernel/debug/tracing/events/power/power_frequency/enable echo 1 > /sys/kernel/debug/tracing/tracing_on这样能看到调频请求的目标频率变化轨迹,快速判断是 governor 决策异常,还是 target 回调里 clk_set_rate 执行失败。
5.5 还有一个关于性能损耗的额外提醒
devfreq monitor 线程如果设计不仔细,会影响实时性。最典型的案例是某平台上 devfreq 的轮询周期和硬件实时时钟抢占同一个硬件单元,每次轮询都触发一次 interrupt storm。CPU 使用率会莫名上涨,功耗反而更高。
如果遇到这种情况,不要把 polling_ms 无限调大,而是要查一下 event 设备获取负载结果时,是不是每次都要 read 好几个寄存器,且这些寄存器访问又要在总线上等待 response。如果是,那就得考虑把统计交给硬件自动累积,软件只读一个最终结果,而不是在轮询路径里反复读取原始计数。也可以把 poll 任务放到普通 workqueue 而不是 high-priority workqueue,避免和实时任务争抢。
最后再补充一个我在实践中特别在意的小技巧
devfreq 的调频输出跟 CPU 频率一样,也存在“乱跳”和“稳不住”的问题。单纯改 polling_ms 并不总能解决,有一个体感很好的补法:在 target 回调里增加一个最小切换间隔,比如至少 100ms 才能再切一次。这样对 DDR 这类切换成本高的设备很有用,代价是响应速度变慢,但实际功耗通常更优。我一般会在私有结构体里记录上次切换的 jiffies,达不到间隔就先把目标频率记录下来,等下一轮再执行。这样合并频繁请求,能显著降低总线时钟切换的开销。
另外,如果你在一个复杂的多设备平台上做整机功耗,建议把 devfreq 当前 governor、polling_ms、max_freq 都做成可配置项放到调试内核参数里。很多真正的问题不会在单模块测试时暴露,只有整机功耗测试时才看得出是调频策略、负载统计还是硬件反馈环的问题。做好实验工具,效率和返修率都会不一样。