
做功耗管理和内核驱动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、governordevfreq 框架的核心抽象是三个东西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 这种高频设备常见设 50msGPU 这种游戏负载剧烈的设备可能会设到 20ms我见过有人调成 5ms结果系统平均功耗直接涨了 5%频率曲线看着细碎实际效果并没好多少。3.4 常用 sysfs 节点速查表devfreq 框架挂载在 class 设备下接入设备后系统里就会出现/sys/class/devfreq/device-name/目录。写驱动时常用这几个节点来验证节点作用常见操作governor查看/切换当前 governorecho simple_ondemand governoravailable_governors列出所有可用策略cat 查看cur_freq当前实际频率cat 查看target_freq框架认为应该达到的频率cat 查看available_frequencies从 OPP 表导出可用的频率档位cat 查看min_freq / max_freq频率上下限echo 写入polling_interval轮询周期单位 msecho 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. 实操示例把一个外设接入 devfreq4.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_DEFERprobe 函数要正确返还这个错误码让 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 都做成可配置项放到调试内核参数里。很多真正的问题不会在单模块测试时暴露只有整机功耗测试时才看得出是调频策略、负载统计还是硬件反馈环的问题。做好实验工具效率和返修率都会不一样。