1. 先弄清楚一件事common clk framework 到底解决了什么痛点做嵌入式 Linux 的开发者尤其是跟 BSP、驱动打过交道的朋友应该对“时钟”这两个字又爱又恨。爱是因为它是所有外设工作的基础恨是因为在相当长一段时间里时钟管理在 Linux 内核里根本称不上“框架”更像是一堆平台代码的堆砌。直到 common clock framework 出现也就是我们常说的 CCF这块才算真正有了统一的章法。在没有 CCF 的年代每个 SoC 厂商都有一套自己的时钟管理方式。有的用结构体指针数组有的用树形链表有的干脆就是一组宏定义。驱动要获取一个外设时钟可能需要调用厂商自定义的接口不同平台之间的 API 还不一样。比如在一个平台上你用 clk_get_from_xxx换到另一个平台就得改成 fb_clk_enable_yyy。这带来的问题远不止代码难看这么简单驱动开发者必须非常清楚底层平台的具体实现一旦要适配多个平台代码里就充斥着条件编译可维护性几乎为零。更麻烦的是时钟树没有一个全局视图。系统里某个 PLL 到底被谁占用了、当前输出频率是多少、能不能安全改频率完全靠猜。很多情况下开发者为了省事干脆在驱动初始化时把所有可能用到的时钟都拉起来后面再也不关。这直接导致整机待机功耗居高不下做低功耗的时候才回头发现根本不知道是哪个时钟在漏电。CCF 的核心思路其实很朴素把所有平台时钟抽象成一颗“树”树的节点就是一个个时钟树的边就是父子关系。一个 PLL 是根它下面挂分频器分频器下面挂 mux再往下是 gate最后挂到外设。驱动不需要关心某个时钟在芯片里具体用了什么寄存器、分频表是怎么排的只需要通过标准 API 去获取、使能、调频。框架内部负责维护引用计数、传播频率变更、确保父子时钟之间的协调。这套思路解决了我个人认为最重要的几个问题统一了驱动接口clk_get、clk_prepare_enable、clk_set_rate 这些 API 在所有平台上行为一致把不安全的操作拦在框架层。比如 rate 变更时能不能影响其它使用者框架会通过 refcount 保护提供一个全局可见的时钟树视图通过 debugfs 就能看出每个时钟当前频率、enable_count 是多少把“睡眠操作”和“原子操作”做了明确区分避免驱动在中断上下文里调用可能睡眠的时钟操作。后面几节我会按这套框架的核心机制、实操注册流程、调试排查、进阶玩法逐层拆开尽量把我在实际项目里踩过的坑和积累的经验一并写进来。2. 框架的骨架看懂三大核心结构与引用计数机制2.1 clk_hw、clk_core、clk 三者的关系很多人第一次看 CCF 源码最懵的就是 struct clk_hw、struct clk_core、struct clk 这三个结构体到底什么关系。我简单梳理一下。struct clk_core这是框架内部真正的“核心对象”时钟树上的每个节点本质上就是一个 clk_core。它保存着时钟的名字、ops、父时钟指针、子时钟链表、prepare_count、enable_count、protect_count 这些关键信息。这个结构体定义在 driver/clk/clk.c 里对外部驱动是不可见的。struct clk_hw这是从内核源码目录 driver/clk/clk.c 暴露出来的“硬件封装”定义在 include/linux/clk-provider.h。它的作用是让底层 clk_ops 回调能拿到一个干净利落的操作句柄。我们可以理解为 clk_hw 是 clk_core 的“面向 provider 的门面”一个时钟驱动的核心工作就是填好 clk_hw 相关的数据结构和回调。struct clk这是消费者驱动拿到的句柄。消费者通过 clk_get 拿到 struct clk 指针再调用 clk_enable、clk_set_rate 等等。实际上 struct clk 内部通过 clk-core 指针关联到真正的 clk_core框架在 API 层做了一层封装。我常用一句话概括provider 面向 clk_hwconsumer 面向 struct clk框架内部用 clk_core 串起来。注册一个时钟时驱动只需要准备 struct clk_hw 和 struct clk_init_data然后调用 clk_hw_register 或 clk_register。框架内部会为它创建 clk_core根据 clk_init_data 里的 ops、parent_names 或 parent_hws 建立父子关系再把节点挂到全局时钟树上。注销时调 clk_hw_unregister 即可。2.2 clk_ops 回调矩阵enable、prepare、set_rate 的调用场景如果把 CCF 比作一个协议struct clk_ops 就是协议的具体行为定义。每个时钟节点都需要实现一组回调框架在合适的时机调用它们。我们需要从使用场景去理解这些回调而不只是记名字。prepare 与 unprepare这两个回调允许睡眠。框架在调用它们时会持有 prepare_lock这是一个 mutex所以实现里可以放慢速操作比如等待 PLL lock、访问 I2C/SPI 配置芯片、修改外部 PMIC 里的频率表。对应地unprepare 是 prepare 的逆操作。enable 与 disable这两个回调的约束是必须在原子上下文执行因为 clk_enable 内部用的是 spinlock。回调里一般只做寄存器操作writel、readl、set_bit 这类。一个时钟被人为地拆成 prepare/enable 两半是因为硬件上有些时钟确实需要两步走先准备好锁相环再在某个关键时刻快速打开门控。set_rate设置频率可能睡眠一般在非原子上下文调用。实现里需要根据传入的 parent_rate 计算分频值、倍频值然后写硬件寄存器。round_rate这是很多人忽略但至关重要的回调。它的职责是回答一个问题给定一个目标频率在当前父时钟频率之下我这个节点最终能输出哪个最接近的频率注意它不是真的去修改硬件只是“估算”。框架在调用 set_rate 之前会先调用 round_rate用来确认这个频率可不可行。这个回调的设计意图是让上层能够提前知道调频结果不用等 set_rate 失败才知道不行。recalc_rate当父时钟频率发生变化时框架会沿着时钟树向下传播调用每个子节点的 recalc_rate重新计算该节点在当前父频率下实际输出的频率。这个回调很关键尤其是分频器的分频值不是线程性的时候必须以硬件实际配置为准。set_parent 与 get_parent用于 mux 类型的时钟节点选择当前父时钟是哪一个。init在时钟注册初始化时调用通常做硬件初始状态检测和必要修正。这里提一个容易踩的坑很多人会混淆“频率改变”和“使能”的关系。有些时钟当它没有 enable 时你调 clk_set_rate 是对的因为可以在关闭状态改分频、改 PLL改完再打开避免输出毛刺但有些时钟比如 DDR 控制器或时序要求严格的 audio PLL必须保证在工作状态下调频这就必须在 enable 状态下调用 set_rate还要确保父时钟先给到正确的频率。框架本身不会替你判断“能不能在关着时调频”这需要驱动开发者清楚硬件约束。2.3 prepare_count 和 enable_count 为什么要分开数引用计数是 CCF 里最核心的保护机制。每调用一次 clk_prepareprepare_count 加一每调用一次 clk_unprepareprepare_count 减一。同样的clk_enable 与 clk_disable 分别增减 enable_count。这两个计数之间还有一层约束只有当 prepare_count 大于 0 时enable_count 才允许大于 0。也就是说一个时钟必须先 prepare 才能 enable必须先 disable 才能 unprepare。框架在实际操作硬件时也遵循这个顺序。我们写驱动时通常只调用 clk_prepare_enable它内部就是先 prepare 后 enable。反过来 clk_disable_unprepare先 disable 再 unprepare。为什么要搞两套计数核心原因是锁粒度不同。prepare 用 mutex可以用在进程上下文enable 用 spinlock可以用于原子上下文。如果某个时钟被多个驱动共享其中一个驱动只想做一次性准备而另一个驱动需要在中断里反复开关两套计数就能精确记录“有多少人准备好了”和“有多少人正在用”。我从实际调试中体会到enable_count 更贴近硬件通电状态prepare_count 更贴近资源占用状态。调试时钟漏电问题时只要看 enable_count 是否归零就知道硬件时钟关了没有。还有一个 protect_count最初是为了配合 clk_protect 这类 API 出现的作用是阻止某些时钟被意外关闭或调频在系统 suspend/resume 流程里尤其重要。不过大部分嵌入式驱动使用频度不高先了解即可。3. 实操一把从零实现一个 mini 时钟节点并接入设备树3.1 用 clk_hw 构造一个最简单的 gate 时钟理论学习再多不如直接写一个能跑的时钟节点。下面这个例子是我的一个常见套路用一个寄存器控制一个外设门控时钟只需要 enable/disable 两个回调。#include linux/clk-provider.h #include linux/io.h #include linux/of_address.h #include linux/platform_device.h #include linux/module.h struct demo_gate_clk { struct clk_hw hw; void __iomem *base; u32 gate_bit; }; #define to_demo_gate_clk(_hw) container_of(_hw, struct demo_gate_clk, hw) static int demo_gate_clk_enable(struct clk_hw *hw) { struct demo_gate_clk *clk to_demo_gate_clk(hw); u32 val; val readl(clk-base); val | BIT(clk-gate_bit); writel(val, clk-base); return 0; } static void demo_gate_clk_disable(struct clk_hw *hw) { struct demo_gate_clk *clk to_demo_gate_clk(hw); u32 val; val readl(clk-base); val ~BIT(clk-gate_bit); writel(val, clk-base); } static const struct clk_ops demo_gate_clk_ops { .enable demo_gate_clk_enable, .disable demo_gate_clk_disable, };这里有几个细节想提醒一下。如果硬件手册里说这个 gate 位是“1 enable0 disable”代码就这样写但如果硬件是反向逻辑就需要在 disable 里置位在 enable 里清位。写寄存器之前最好先 readl 回来再改避免覆盖其它控制位。实际项目里经常有人图省事直接 writel 一个写死的值结果其它外设信号被压坏这个坑我见到不止一次。接下来需要填 struct clk_init_data。这里最重要的是 ops 和 parent 信息。static int demo_clk_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct demo_gate_clk *demo; struct clk_init_data init {0}; struct clk_hw *hw; int ret; demo devm_kzalloc(dev, sizeof(*demo), GFP_KERNEL); if (!demo) return -ENOMEM; demo-base devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(demo-base)) return PTR_ERR(demo-base); /* 假设 bit3 是门控位 */ demo-gate_bit 3; init.name demo_gate_clk; init.ops demo_gate_clk_ops; init.flags 0; /* parent 信息下面再细说 */ init.parent_names NULL; init.num_parents 0; hw demo-hw; demo-hw.init init; ret devm_clk_hw_register(dev, hw); if (ret) return ret; return 0; }注意一个极易犯的错clk_init_data 里的 name、ops、parent_names 是注册时读取的但 struct clk_hw 里的 init 指针在 clk_hw_register 之后不要再保留因为它指向栈上的变量函数返回就失效了。框架内部在注册时会拷贝需要的数据我们无需自己再用 init 指针。3.2 优先级更高的做法用现成的 helper 组装时钟其实如果只是一个 gate 时钟完全没必要手写 ops。内核里的 clk-gpio 或者 clk-gate 模块提供了通用 gate 实现。在设备树里可以这样描述clk_demo_gate: demo-gate { compatible fixed-clock; #clock-cells 0; clock-frequency 24000000; };但固定频率时钟不能满足所有场景更常见的是用组合方式一个 mux 选父时钟一个 divider 分频最后 gate 输出。如果不想自己写代码可以一步步串“fixed-factor-clock”“clk-mux”“clk-divider”“clk-gate”节点。这里我建议新手先在设备树里用这些通用 helper把概念弄熟再去看厂商驱动自己实现的 composite clock。下面是一个 multi-node 时钟链的简单示意clk_ref_pll: ref-pll { compatible fixed-clock; #clock-cells 0; clock-frequency 1200000000; }; clk_div2: demo-div2 { compatible fixed-factor-clock; clocks clk_ref_pll; clock-div 2; clock-mult 1; #clock-cells 0; }; demo-uart { clocks clk_div2; clock-names uart; };这就是“框图式”的时钟配置法。每个节点代表一个时钟父子关系通过 clocks 属性表达。框架在解析设备树时会按 compatible 找到对应的注册函数自动构建 clk_core 树。这种方式充分展示了 CCF 的价值你不用写一行 C 代码就把一颗时钟树搭好了调试时通过 debugfs 就能看到每级频率是否符合预期。3.3 时钟消费者驱动里怎么拿、怎么用、怎么避免悬空接下来是消费端。一个普通外设驱动从设备树里拿时钟最稳妥的方式是 devm_clk_getstruct clk *clk; clk devm_clk_get(dev, uart); if (IS_ERR(clk)) return PTR_ERR(clk); ret clk_prepare_enable(clk); if (ret) return ret; rate clk_get_rate(clk); dev_info(dev, clock rate: %lu\n, rate);这里有一个最重要的经验使能时钟一定要用 clk_prepare_enable而不是只调 clk_enable。因为绝大多数平台级时钟的 prepare 回调是真实存在的即使这个时钟的 ops 里只实现了 enable框架也会在 prepare 阶段做统一处理。如果你只调用 clk_enable内核对 prepare_count 的计数不会增加某些驱动在 suspend 时会根据 prepare_count 判断是否该保留时钟结果就是时钟被莫名关掉外设醒来后直接挂掉。还有一点devm_clk_get 和 clk_get 的区别前者是 devres 管理的资源驱动卸载时自动释放不用手动 clk_put后者需要手动对称维护。我在多人协作的项目里经常看到有人混用两种风格导致代码里出现重复 clk_get 和 clk_put 或者漏掉的 clk_put。建议整个驱动统一用 devm 版本省心得多。获取时钟后如何调频常见写法是clk_set_rate(clk, 115200 * 16);但要注意clk_set_rate 的成功返回并不意味着硬件最终频率就是那个精确值。因为时钟链路上有分频器、PLL 的离散挡位最终频率可能是向上或向下取整后的值。所以严谨的做法是设置之后再用 clk_get_rate 去读真实频率如果真实频率和预期频率差异超过误差容忍范围再做处理。尤其是串口波特率高低偏差超过 2% 就可能乱码最终频率必须核对。4. 时钟树调频是怎么一路传导下去的4.1 set_rate 与 clk_ops 的协同流程很多初学者对“调用 clk_set_rate 之后发生什么”没有完整概念。我在这里拆一下流程。首先clk_set_rate 会找到目标时钟的 clk_core然后调用 clk_calc_new_rates也就是计算新的频率。计算过程从目标节点开始沿着父链向上走依次调用各节点 op-round_rate判断在给定 parent_rate 下能否输出请求的 rate。如果某个节点发现自己无法直接得到足够高的父频率它可能会要求父节点也提高频率这样一层层往上最终可能要调整根 PLL。计算完成后框架会用一个“保护锁”检查这个改动是否安全如果某个时钟的 enable_count 不为 0并且它的驱动没有允许频率被修改框架会拒绝本次变更。这就是为什么有时候 clk_set_rate 返回 -EBUSY往往不是硬件问题而是有其他驱动在占用这条链路。确认可行后框架开始执行变更。变更顺序非常讲究先自上而下调整上游频率再自下而上调整下游节点最后再更新每个节点的 recalc_rate。这是为了保证任何时刻整个链路不至于出现一个节点频率还没改、另一个节点已经用了新分频的现象。当然PLL 频率本身连续调整时必须硬件支持“渐进锁定”这里涉及 SoC 具体实现CCF 提供了 PRE_RATE_CHANGE / POST_RATE_CHANGE / ABORT_RATE_CHANGE 通知后续进阶部分会讲。4.2 recalc_rate 为什么不能偷懒我见过不少 driver 作者在实现 recalc_rate 时直接返回自己缓存的一个 frequency 字段而不是重新根据硬件寄存器计算。这在固定频率时钟上没问题但一旦父时钟被动态调频你缓存的值就失真了下游外设比如 USB、以太网会跑在错误频率上表现是通信偶发失败、一会儿好一会儿坏极难定位。正确的 recalc_rate 实现逻辑是读取当前分频寄存器结合clk_hw_get_rate(hw)拿到的父频率计算实际输出频率。如果分频器支持小数分频还要注意硬件取整规则。这里分享一个实用的调试技巧在写驱动阶段给 recalc_rate 加一个 dev_dbg打印 parent_rate 与 div_reg 的实时值比对 clk_summary 里的结果能很快发现分频表配置错误。4.3 round_rate 决定系统调频的“可见范围”在不同外设驱动共同跑在一条时钟链上的场景round_rate 基本决定了整个系统的调频弹性。设计 round_rate 时的核心原则是只返回你的节点真正能支持的频率不要试图返回一个非常接近但不可达的值否则上层驱动会误以为调频成功。举一个实例。一个音频外设的时钟要求 12.288 MHz但分频器只能做整数分频父时钟固定 49.152 MHz那么 round_rate(12288000) 应该返回 12288000 吗不一定要看分频的路径。如果默认整数分频只能出 9830400 或 16384000而存在一个倍频分支能出 12.288M你必须在 round_rate 里把这两个可能值都算出来挑选误差最小的作为候选。如果只是简单返回一个固定的中间值音频驱动就会跑出杂音。另外round_rate 必须保证 set_rate 一定能够实现自己承诺的返回值这是框架正确性的前提。曾在某平台上遇到过 round_rate 返回一个中间频率但 set_rate 因为 PLL 挡位跳不到那个值直接返回 -EINVAL导致用户空间改频率失败且错误信息难以理解。后来把这个实现改成“先设父再算分频”的逻辑才彻底解决。5. 出现问题怎么查debugfs、内核日志与经典踩坑5.1 让 debugfs 成为你的第一诊断工具内核里只要开启 CONFIG_COMMON_CLK并且挂上 debugfs你可以直接看时钟树的全景图。最常用的三个节点/sys/kernel/debug/clk/clk_summary二维表格列出每个时钟的 name、prepare_count、enable_count、protect_count、rate、accuracy、父节点。/sys/kernel/debug/clk/clk_dump把整棵时钟树以层级缩进格式导出更适合看父子拓扑。/sys/kernel/debug/clk/xxx/clk_rate如果对应平台实现了 debugfs hook可以只读某个时钟的当前频率。实际排障时我的第一反应永远是打开 clk_summary看这条外设链路的每个节点是否都有正确的 enable_count 和 rate。如果某层 enable_count 是 0但它的下游驱动明明调用了 clk_prepare_enable那就说明驱动拿到的 clk 句柄和这颗硬件时钟不是同一个节点典型错误是设备树里 clock-names 写错导致映射到别的时钟上。5.2 常见问题速查表现象可能原因排查建议clk_get 返回 -EPROBE_DEFER时钟 provider 尚未注册依赖的设备驱动还没就绪查看系统中 provider 的 probe 是否存在失败检查时钟节点 compatible 是否匹配到的驱动确实注册了 clk驱动使能后外设不工作gate 时钟的 enable 回调未写寄存器或 parent 时钟被关闭用 clk_summary 确认从根到末级的 enable_count 和 rate用示波器量硬件时钟引脚调用 clk_set_rate 返回 -EBUSY某节点已 enable 且不支持运行时调频打开 CONFIG_COMMON_CLK 相关追踪日志找到哪颗时钟锁住了链路系统休眠漏电外设无法唤醒驱动未在 suspend 中调用 clk_disable_unprepare检查 prepare_count/enable_count 在 suspend 前是否归零必要时用 clk_protect 标记关键时钟更改父时钟后下游频率不变化recalc_rate 未实现或实现错误查看 recalc_rate 是否从寄存器读取实际分频值而不是返回缓存调频后串口乱码实际速率与预期偏差过大在驱动中先调用 round_rate 再核对 clk_get_rate确认误差范围clk_summary 中某频率异常大分频寄存器配置被覆盖或 mux 选错了父时钟检查父时钟节点选择的 parent 是否来自正确的 PLL尽量用 ftrace 或 debugfs 动态查看变更5.3 EPROBE_DEFER 的处理原则EPROBE_DEFER 在时钟子系统里太常见了。原因很简单某个消费者驱动 probe 时它所依赖的时钟 provider 还没 probe于是内核返回 -517 试图让消费者稍后重试。但有一个细节很多人不知道CCF 在注册一个 provider 时会扫描所有挂在它名下但未 probe 的消费者给它们重新排队的调度时机。也就是说只要 provider 最终能成功注册消费者迟早会恢复 probe。如果消费者一直卡在 defer 状态我们就要怀疑 provider 是否 probe 失败了比如设备树里缺少必要的寄存器、时钟输入配置错误。排查路径我一般三步走检查内核日志找到重复打印的 EPROBE_DEFER 相关消息用 debugfs 查看该时钟是否出现在 clk_summary 里若不存在说明注册没成功查看 provider 驱动的 probe 回调返回值确认必要的 io resource 是否拿到。5.4 锁相关prepare_lock 与 enable_lock 引发的死锁CCF 内部有两把锁prepare_lockmutex和 enable_lockspinlock。它们的命名很直白但使用上有硬性规定不能在持有 enable_lock 的时候调用任何可能睡眠的函数。一旦在 clk_ops-enable 实现里调用了 msleep、mutex_lock、i2c_transfer 之类恭喜你系统可能会在某个高负载时刻进入死锁状态而且这个 bug 是间歇性的特别难抓。同样clk_ops-prepare 实现里不能调用 clk_prepare_enable 自身否则会因为重复加锁而致死锁。如果你的 prepare 实现需要 enable 某颗辅助时钟应该通过框架 API 来操作框架在 prepare 阶段会对锁进行递归处理不要在自己实现里直接裸调用另一个 clk 的 enable 回调。另外不要在中断上下文调用 clk_set_rate、clk_prepare。这点从接口命名也能看出来prepare 带有可睡眠语义set_rate 内部也要处理一系列操作都可能睡眠。如果真的需要在中断里临时开关某个时钟必须使用 clk_enable / clk_disable而且这两个回调实现必须保证快速、无阻塞。6. 进阶实践notifier、protect 与平台厂商的定制套路6.1 用 clk_notifier 感知频率变化在某些高可靠性场景下设备树把某个时钟共享给音频、DDR、显示三个驱动。当音频驱动调频时DDR 驱动可能也需要同步调整时序参数。如果每次都用轮询去读 clk_get_rate既低效又不及时更糟糕的是可能读到中间态。CCF 提供了 clk_notifier_register 机制允许消费者在时钟发生 rate change 前、后、失败时获得通知。static int demo_clk_notifier_cb(struct notifier_block *nb, unsigned long event, void *data) { struct clk_notifier_data *ndata data; unsigned long old_rate ndata-old_rate; unsigned long new_rate ndata-new_rate; switch (event) { case PRE_RATE_CHANGE: /* 可以在此准备硬件比如停止某些接口 */ break; case POST_RATE_CHANGE: /* 频率已经改变做相关修正 */ break; case ABORT_RATE_CHANGE: /* 变更失败恢复现场 */ break; default: break; } return NOTIFY_DONE; }使用 notifier 的驱动要格外小心回调里不能做阻塞操作因为它是原子上下文典型的做法是设置 flag 或唤醒工作队列在处理完软中断/线程后再去访问寄存器。6.2 clk_protect 与系统低功耗的关系我参与过一个低功耗项目外设驱动都写得很规范enable_count 也准确但 suspend 测试时电流就是降不下来。最后定位到问题出在一颗音频参考时钟上驱动在播放后忘记调用 clk_disable_unprepare导致 prepare_count 永远大于 0系统进入 suspend 时PM framework 误以为有驱动还在使用它就跳过关闭逻辑。有趣的是这类问题用 clk_summary 一眼就能看出来某个时钟在 suspend 前后 enable_count 一直是 1而它的消费者全部 probe 成功了说明有驱动漏关了。这时候有两种修法一是修驱动把漏掉的 disable_unprepare 补上二是如果这颗时钟确实需要常开就用 clk_protect 明确标记让框架知道它必须常开避免有人误关。我更倾向于前者因为 clk_protect 会把频率锁定值也保存下来系统里如果有别的驱动想调这个时钟的频率就会收到失败返回值表现为另一个难以解释的“神秘失败”。6.3 厂商常用的 composite clock 套路SoC 厂商实现 CCF 时几乎都会遇到同一个问题一颗多路输出时钟往往同时包含 mux、divider、gate 三种功能如果注册成三颗独立时钟节点对驱动调用方极其不友好——他们要分别 prepare 三颗才能让最终外设工作。更重要的是某些硬件设计里分频值与门控位放在同一个寄存器里根本没办法拆成三颗独立时钟。于是 Linux 引入了 composite clock 的概念也就是在 clk_hw 层面上把多个 sub-clock 组合成一个逻辑时钟。厂商通常会在自己的 clk-xxx.c 里定义类似 struct xxx_clk_comb { struct clk_hw hw; struct clk_mux mux; struct clk_divider div; struct clk_gate gate; } 的结构体然后注册到一个 clk_ops 里。框架在调用 enable 时自动执行 gate 的 enable调用 set_rate 时自动执行 divider 的 set_rate必要时还会前置 mux 的切换。这里有一个必须注意的问题composite clock 的 set_rate 回调往往比较复杂因为 mux 选父时钟可能需要在 set_parent 和 set_rate 之间协调顺序。比如当前父时钟是 PLL_A但目标频率在 PLL_B 上更合适你可能需要先切时钟源到 PLL_B再设置分频这样才能保证输出不会在切换瞬间产生毛刺。而这种切换顺序严重依赖 SoC 的硬件设计框架只能提供 callback 接口具体策略得开发者在实现里仔细斟酌。7. 我自己的一点经验与建议CCF 是一个非常典型的“框架先于实现”的成功案例。它不限制平台怎么做硬件只要求平台提供一套标准的操作语义。这种标准化换来了巨大的生态收益今天我们可以在一个驱动里调用 clk_prepare_enable而完全不需要关心底下硬件是纯寄存器时钟还是需要 I2C 访问 PMIC 的远时钟。不过框架做得再好也架不住驱动开发者不守规矩。我见过的最典型的反面案例是有人为了“省性能”在驱动运行时缓存了某个时钟的 rate之后再也不去读 clk_get_rate结果父时钟被别处调频后外设直接罢工。记住时钟树是活的父节点频率一旦改变下游所有节点的实际频率都可能跟着变缓存 rate 这种短期优化长期来看都是隐患。如果你是一个刚接触嵌入式的读者我的建议是先别急着去翻内核 clk.c 源码而是花时间把设备树里的时钟绑定关系理清楚我们用的是哪颗根时钟中间经过了几级分频门控由谁控制谁和谁共享了同一条时钟链路。这些信息搞清楚之后再回来看框架的 API基本一眼就能看懂。对于已经有不少驱动经验的朋友建议多花点精力在 clk_ops 的调试和 round_rate 实现上这两个地方是绝大多数时钟 bug 的藏身之处。最后分享一个小技巧排查时钟类似问题时不要只盯代码。把时钟树和实际硬件测量结合起来会快很多。先用 clk_summary 确认软件层面的频率和 enable 状态再用示波器量一下真实的管脚输出两者一比对问题通常就缩小到一个很小的范围。这个习惯帮我在多个项目里省下了不少查数时间。
阅读完成 · 觉得有帮助?