做嵌入式Linux驱动这行,最容易被低估的就是时钟部分。很多人拿到一块新板子,先在设备树里把引脚、中断、寄存器基址配好,probe一跑,devm_ioremap_resource返回成功,就以为大功告成,结果readl回来的永远是0xffffffff或者0x0。折腾半天才发现,外设的总线时钟根本没打开——寄存器压根没上电。这类问题在嵌入式Linux项目里出现的频率高得离谱,而解决它的核心抓手,就是CCF通用时钟框架(Common Clock Framework)。这篇文章不讲空理论,我想把CCF从"为什么存在"到"怎么写一个能进的时钟控制器驱动"再到"外设拿不到时钟怎么一步步定位"整条链路掰开说清楚。适合已经能写字符设备驱动、跑过几次设备树修改、准备往时钟子系统下沉的嵌入式Linux时钟驱动开发者;如果你只是做应用层、连clk_get都没见过,那前半部分可以先当背景了解,后面的排错章节留着备用。关键词先亮出来:嵌入式Linux、CCF、通用时钟框架、时钟驱动开发,这四个词基本就是全篇的主线。
1. 时钟树在芯片里长什么样,CCF 又是怎么把它抽象成代码的
1.1 一颗 SoC 的时钟树,本质上是一张有向图
在任何一颗现代SoC里,时钟不是一个孤立的方波发生器,而是一张层层分叉的树。最上游通常是晶振(XTAL),常见24MHz或26MHz;晶振进PLL倍频,PLL再分出几路;每一路经过mux选择源、经过divider分频、经过gate开关,最后送到具体外设——UART、I2C、SDIO、GPU、DDR控制器各吃一路。这颗树里的每一个节点都可能被改动:调PLL倍频、切mux父节点、改分频比、开关gate。
问题就在于,这棵树的每个节点都被修改时,上游的变化会传导到下游所有子节点。你只想把I2C的速率从100kHz调到400kHz,结果发现顺手动了一下共享的PLL分频,把DDR的时钟也带偏了,系统直接挂死。所以时钟管理的核心难点从来不是"怎么写寄存器",而是"怎么在改动一个节点时,知道影响范围、知道当前引用计数、知道谁在用它"。
早期ARM平台的做法是各自为政。三星Exynos一套、TI OMAP一套、Freescale i.MX一套,每家的clkAPI都不一样,改一个外设驱动换平台就得重写时钟部分。CCF做的事情,就是把这些私有实现收拢成一套内核公共的抽象层,让驱动只面向统一的struct clk编程,平台差异藏在clk_ops回调里。这是理解CCF所有设计的一把钥匙——它不是为了性能,而是为了解耦和可维护。
1.2 clk、clk_hw、clk_core、clk_provider 这四个名词必须先分清
初学者看CCF源码最容易晕的就是这四个概念混着出现。我用一个类比讲清楚:时钟树的每个节点是一个"硬件实体",它有两张脸。
struct clk_hw是生产者视角的脸,代表硬件本身。时钟控制器驱动的作者拿它来注册节点、实现clk_ops回调。struct clk是消费者视角的脸,外设驱动拿到的就是这个指针,调用clk_prepare_enable、clk_set_rate。两边的接口正交,互不干扰。
struct clk_core是内核内部真正维护的核心对象,持有父节点指针、当前频率、引用计数、prepare计数这些运行状态。它藏在内核源码里,驱动作者基本不直接碰。
clk_provider则是"这一批时钟节点的集合"的概念,设备树里看到#clock-cells,对应的就是某个provider。provider通过of_clk_add_hw_provider发布出去,别的节点用clocks属性引用它。
分清楚这四者的关系,后面写驱动时你就知道:设备树里填的是provider的引用信息,驱动里注册的是clk_hw,而外设驱动拿到的struct clk只是句柄。三者通过CCF的查找机制串起来,任何一个环节名字对不上,都会导致时钟拿不到。
1.3 为什么 CCF 要用 have/consumer 分离的模型
有人会问:为什么不直接让外设驱动拿到硬件指针,非要多一层struct clk句柄?核心原因有两个。
第一是引用计数。一路时钟可能被多个外设共享,比如I2C和UART共用根时钟。如果每个驱动直接开关硬件,一定会互相踩。CCF在clk_core里维护enable_count和prepare_count,只有当计数归零时才真正关掉gate,这就是"共享时钟"能安全工作的基础。
第二是父节点传播。当你调clk_set_rate时,CCF会沿着clk_core的父子链往上走,尝试在合适层级完成调速。它不会傻乎乎地改根PLL,而是从当前节点往上找最近能改频率的节点。外设驱动只需要说"我要400kHz",剩下的层级选择由CCF和clk_ops里的determine_rate共同完成。这种"需求驱动、逐级上溯"的机制,正是CCF相对私有实现的最大进步。
理解了这一层,你在调试时的心态会完全不同——时钟不对,先从"树的结构"和"引用计数"入手,而不是急着翻寄存器手册。
2. 设备树里描述时钟:provider 和 consumer 两端必须严丝合缝地配对
2.1 #clock-cells 填几,取决于这个 provider 有几个参数
#clock-cells是设备树里最容易配错、而且配错了症状最隐晦的字段之一。它的含义是"引用这个provider的某个时钟时,需要几个额外的单元格来定位"。常见取值有三种。
| 取值 | 含义 | 典型场景 |
|---|---|---|
<0> | provider只有一个时钟输出 | 单路晶振、单路固定门控时钟 |
<1> | 需要一个索引号选择具体哪一路 | 最常见的多功能时钟控制器 |
<2> | 需要索引加子索引 | 部分带多级分组的复杂控制器 |
配错的后果很直接。假设provider是<1>,那么consumer就该写clocks = <&clks CLK_ID_I2C0>;如果你把provider写成<0>,consumer又按两个单元格写,解析时就会把后面的数据当成别的属性读进去,轻则报-EINVAL,重则解析出错误的时钟索引,指向一个完全无关的gate。这类错误最坑的地方在于编译期完全不报,只有运行时通过日志和clk_summary才能看出来。
我的建议是:provider驱动的第一件事,就是把时钟索引表定义成头文件或者宏枚举,并且这个头文件同时被provider驱动和consumer的设备树一起引用。设备树里不写裸数字,写CLK_ID_I2C0这类宏,配错单元格数会立刻暴露成编译或解析错误。
2.2 clocks 与 clock-names 的配对规则
外设节点通常这样写:
i2c0: i2c@12020000 { compatible = "vendor,i2c-v1"; reg = <0x12020000 0x1000>; clocks = <&clks CLK_ID_I2C0>, <&clks CLK_ID_I2C0_PCLK>; clock-names = "i2c", "pclk"; status = "okay"; };这里的配对规则是数组下标一一对应。驱动里用devm_clk_get(dev, "i2c")拿到的就是clocks数组第0项,devm_clk_get(dev, "pclk")拿到第1项。如果驱动里写的名字和clock-names任何一个对不上,devm_clk_get就会返回-ENOENT。
我踩过的坑是:厂商BSP的驱动里用"i2c_clk"做名字,而设备树里写的是"i2c",中间被某个人改过一处没改另一处,结果驱动加载时报failed to get i2c_clk。排查起来不难,但如果你手里同时有几十个外设、每个名字都不统一,那就很耗时间。
一个很实用的做法:在驱动里可以用clk_bulk_get或者devm_clk_get_optional来放宽约束。clk_bulk_get按clock-names批量拿,不关心顺序;devm_clk_get_optional在拿不到时钟时返回NULL而不是错误,适合那些时钟可有可无的软IP外设。选哪个取决于外设是否强依赖时钟——硬件上时钟是必须的,就用devm_clk_get,让错误尽早暴露。
2.3 assigned-clocks 系列的生效时机,比你想的更早
assigned-clocks、assigned-clock-rates、assigned-clock-parents这组属性,是设备树里一个非常"反直觉"的设计:它们在consumer驱动probe之前就已经被执行了。
具体机制是:内核在of_clk_set_defaults阶段(也就是设备注册到platform bus、调用probe之前),会解析这些属性并把设置应用到时钟树上。这就是为什么很多SD卡驱动不需要在代码里显式调clk_set_rate,速率却已经对了——根源在设备树。
&mmc0 { assigned-clocks = <&clks CLK_ID_SDIO0>; assigned-clock-rates = <200000000>; assigned-clock-parents = <&clks CLK_ID_PLL_APLL>; };一个真实的教训:曾经调一颗SDIO WiFi芯片,反复改驱动里的clk_set_rate都没用,抓clk_summary发现速率仍是默认值。最后发现是设备树里有个assigned-clock-rates写死了另一个值,它在probe之前就把频率锁定了,驱动里的设置又被后续流程覆盖回去。只要设备树里出现了assigned系列属性,你就得先去看它,再去改驱动,否则永远是"改代码不生效"。
另一个细节:assigned-clocks里的时钟引用,必须能通过provider的#clock-cells正确解析,且provider必须已经注册完成。如果provider的probe晚于consumer,这些设置就会失败并打印failed to set default rate之类的日志。这个时序问题在多级时钟控制器里非常常见。
3. 从零写一个时钟控制器驱动:注册流程与代码骨架
3.1 先照着寄存器手册把时钟树画出来,再动笔
拿到一颗新SoC的时钟控制器CRU/CMU模块,我建议第一件事不是写代码,而是在纸上把时钟树画一遍。需要的输入是寄存器手册里的PLL配置表、mux选择表、divider分频表、gate使能位。
画树的时候回答这几个问题:根晶振频率是多少;有哪几个PLL,各自倍频公式是什么;从PLL出来到目标外设中间经过几级mux和divider;gate位在哪个寄存器的哪一位;有没有必须常开的时钟。
这些信息直接决定了后面clk_ops的复杂度和provider的#clock-cells设计。以我的经验,画树花的这半小时,能省掉后面至少两天的调试时间。很多人上来就抄开源BSP的驱动,结果发现BSP里有些PLL根本没实现、有些gate位对应的外设和自己板子不一样,改起来反而更费劲。
3.2 clk_hw 加 clk_ops 的最小骨架
固定频率时钟是最简单的切入点,但真正能体现CCF设计思想的是门控时钟加可调速时钟的组合。下面给一个可调速率时钟+门控的骨架,涵盖了最常见的回调:
#include <linux/clk-provider.h> #include <linux/platform_device.h> #include <linux/of_address.h> struct myclk { struct clk_hw hw; void __iomem *base; u32 reg_offset; spinlock_t lock; u32 gate_bit; u32 div_shift; u32 div_width; }; #define to_myclk(_hw) container_of(_hw, struct myclk, hw) static unsigned long myclk_recalc_rate(struct clk_hw *hw, unsigned long parent_rate) { struct myclk *clk = to_myclk(hw); u32 val, div; val = readl(clk->base + clk->reg_offset); div = (val >> clk->div_shift) & GENMASK(clk->div_width - 1, 0); /* 硬件分频是 0 表示 1 分频 */ return parent_rate / (div + 1); } static long myclk_round_rate(struct clk_hw *hw, unsigned long rate, unsigned long *parent_rate) { unsigned long prate = *parent_rate; u32 div = DIV_ROUND_CLOSEST(prate, rate); if (div == 0) div = 1; return prate / div; } static int myclk_set_rate(struct clk_hw *hw, unsigned long rate, unsigned long parent_rate) { struct myclk *clk = to_myclk(hw); unsigned long flags; u32 div, val; div = DIV_ROUND_CLOSEST(parent_rate, rate); if (div == 0) div = 1; spin_lock_irqsave(&clk->lock, flags); val = readl(clk->base + clk->reg_offset); val &= ~GENMASK(clk->div_width - 1, 0) << clk->div_shift; val |= (div - 1) << clk->div_shift; writel(val, clk->base + clk->reg_offset); spin_unlock_irqrestore(&clk->lock, flags); return 0; }这段代码里有几个细节值得展开。recalc_rate读的是硬件真实值,所以每次调用都要读寄存器;round_rate负责"诚实回答我能给到什么频率",它必须返回硬件能实现的值,而不是用户期望的值;set_rate才真正写寄存器。
round_rate和set_rate分家的原因在于:CCF在设置频率前会先问一遍round_rate,确认目标频率是否可以达成、达成后实际是多少,再决定要不要真的写。驱动如果round_rate返回一个硬件根本给不出的值,后面set_rate就会写出错误的divider,导致外设工作异常但日志上看起来一切正常——这也是时钟调试里非常隐蔽的一类问题。
3.3 用现成的 helper 少写几百行代码
CCF提供了一堆开箱即用的时钟类型,能直接用的千万不要自己写。clk_register_fixed_rate注册恒定时钟;clk_hw_register_mux注册mux;clk_hw_register_divider注册分频器;clk_hw_register_gate注册门控;clk_hw_register_composite把mux、divider、gate组合成一个"复合时钟"。
以gate为例,一个纯门控时钟只需要:
hw = devm_clk_hw_register_gate(dev, "i2c0_clk", "i2c0_mux", CLK_SET_RATE_PARENT | CLK_IGNORE_UNUSED, base + REG_GATE, BIT(5), CLK_GATE_SET_TO_DISABLE, &lock);一行就能省掉prepare、enable、disable、is_enabled四个回调的编写。而clk_hw_register_composite配合devm_clk_hw_register,能把mux+divider+gate串成一条链,代码量比你手写四个clk_ops回调少一半以上。
需要注意的一点:这些helper注册出来的时钟,父节点的名字必须是字符串,且这个名字必须能通过clk_get链找到provider。如果父节点名字打错,时钟就成了orphan,挂在clk_summary的orphan列表里,永远拿不到正确的父频率。这个名字的匹配是纯字符串比较,没有任何编译期检查。
3.4 of_clk_add_hw_provider 的传参方式和常见误用
provider要发布出来,让设备树能引用,需要调用:
ret = devm_of_clk_add_hw_provider(dev, of_clk_hw_onecell_get, data);这里的data是struct clk_hw_onecell_data,它包含一个clk_hw指针数组和num个时钟节点。关键点在于:数组下标就是#clock-cells里消费者填的索引值。你在设备树里写<&clks CLK_ID_I2C0>,如果CLK_ID_I2C0是3,那么>clk_prepare_enable(clk); /* 大多数场景直接这样一行即可 */ ... clk_disable_unprepare(clk);
但要注意在中断上下文中不能调clk_prepare_enable,只能调clk_enable,因为prepare会走可能存在睡眠的路径。我见过有驱动在中断里调clk_prepare_enable,跑起来是"偶发卡死",非常难定位。
CLK_SET_RATE_PARENT标志也值得单独提一句:它表示"允许这个时钟的频率设置向父节点传播"。如果你的外设时钟是某个分频后的子节点,而分频范围又不够覆盖目标频率,就必须带上这个标志,否则set_rate只能做到局部最接近,永远达不到目标值。这个标志加不加,直接影响clk_round_rate返回的结果。
5. 实测排错链路:外设拿不到时钟、时钟打不开、频率不对
5.1 第一站永远是 clk_summary
调试时钟问题,我第一条命令永远是:
mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/clk/clk_summary输出的信息量极大:每个时钟节点的名字、enable计数、prepare计数、保护标志、当前速率、父节点名字、精度。一个健康的时钟树应该呈现明显的层级关系。
排查时按这个顺序看。第一看顶层是否有输出,如果根晶振都没出现在列表里,说明CONFIG_COMMON_CLK或provider本身没注册成功。第二看你的目标时钟在哪一层,如果它孤零零挂在orphan区,说明父节点名字对不上。第三看enable_count和prepare_count,如果是0,说明外设根本没有成功使能时钟。
clk_summary还有个隐藏用途:它能告诉你时钟的"实际速率"。很多人只看驱动里设置的速率,不看这个值。曾经遇到一个I2C波特率不对的问题,驱动里设置的是400kHz,抓clk_summary发现实际是100MHz——分频比写错了,导致I2C波形完全跑飞,但驱动日志没有任何报错。这种问题只能靠clk_summary和示波器交叉验证。
另外可以关注/sys/kernel/debug/clk/clk_dump和/sys/kernel/debug/clk/clk_orphan_summary。前者给出更底层的引用关系,后者直接列出所有orphan节点——排查"父节点名字对不上"时,orphan列表是最直接的证据。
5.2 orphan clock 与 parent 名字对不上的完整排查
orphan是时钟调试里出现频率最高的一类问题。症状是:provider注册成功,设备树也写对了,但clk_get拿到的时钟频率是错的,或者clk_set_rate怎么调都不生效。
排查步骤我总结成这样一条链路:
第一步,确认目标时钟是否出现在clk_orphan_summary里。如果出现了,问题锁定在父节点名字。
第二步,把orphan时钟声明的父名字和provider里实际注册的名字逐个字符对比。常见的坑是大小写不一致、下划线中划线混用、名字里带了不该有的后缀。字符串匹配没有任何容错。
第三步,确认父节点在注册顺序上是否早于子节点。provider注册的顺序很关键:如果子节点注册时父节点还没注册,CCF会把它放进orphan列表,等父节点出现后才重新挂接。如果你在子节点的clk_ops里或者init_data里写死了父节点指针而不是名字,那顺序就更重要。
第四步,看内核日志有没有clk: failed to reparent或者orphan相关打印。这些日志通常在启动早期就打了,如果没开CONFIG_DEBUG_FS或者串口日志被裁剪过,可能会漏掉。
我实际遇到的一个案例:某颗SoC的PLL名字定义在驱动里是"apll",但另一个时钟的parent_names里写的是"APLL"。整整找了两个小时,最后靠clk_orphan_summary和逐字符对比才定位。只要涉及时钟名,一定要统一大小写规范,最好从宏定义源头控制。
5.3 频率算出来对但外设不工作,问题可能不在时钟
有时候clk_summary显示的速率和目标完全一致,但外设就是不工作,比如I2C超时、SDIO初始化失败、SPI数据错乱。这种情况要分几种可能。
分频相位问题。有些外设对时钟相位敏感,比如SD卡在高速模式下需要采样点和数据对齐。单纯看频率对,不代表相位合适。这类问题通常需要在驱动里调采样相位寄存器,或者在时钟控制器里配phase相关的clk_ops(get_phase/set_phase)。
时钟分频后的占空比。某些硬件分频实现只能整数分频且占空比不固定,比如3分频时高通部分可能只有1/3周期。外设如果对占空比有要求,就需要额外的duty-cycle修复。CCF里对应的回调是get_duty_cycle/set_duty_cycle。
门控还没真正打开。时钟的enable只是设置了一个标记,真正写gate寄存器可能在CCF的clk_disable_unused阶段被优化掉。如果时钟在启动早期被enable过、之后无人使用,clk_disable_unused会把它关掉。解决方式一般是给时钟加CLK_IGNORE_UNUSED标志。
这几类问题的排查顺序是:先确认enable_count大于0且gate位确实置位,再看频率,最后看相位和占空比。按这个顺序走,避免一开始就往最难的地方钻。
5.4 寄存器读不出来:时钟没开的连锁反应
这是我在新平台上遇到的第一个"经典坑"。现象是probe里readl读回来的值永远是0xffffffff或者0x0,ioremap成功了但寄存器像死了一样。
根因很朴素:外设的总线时钟没使能,寄存器接口根本不上电。很多SoC的寄存器访问需要总线时钟(bus clock)的存在,而这个时钟默认是关闭的。驱动里如果只ioremap而不打开时钟,读寄存器就会返回总线的默认值。
修复方法同样是围绕CCF:在probe的早期就devm_clk_get拿到总线时钟,然后clk_prepare_enable,再做寄存器的读写。注意顺序:先拿时钟再碰寄存器,不要反过来。
这里也有个技巧:如果驱动需要多个时钟(比如"bus"、"func"、"core"),用clk_bulk_get_all一次性拿全,配合clk_bulk_prepare_enable批量使能,出错时用clk_bulk_disable_unprepare清理,能省掉大量手写的错误处理代码。
6. 系统裁剪和长期维护阶段,几个容易踩而不自知的细节
6.1 CLK_IS_CRITICAL 与 CLK_IGNORE_UNUSED 的使用边界
这两个标志长得很像,作用完全不同,但都容易被滥用。
CLK_IS_CRITICAL表示"这个时钟永远不能被关闭",一旦注册,CCF就会阻止对它调用clk_disable_unused。典型场景是时钟控制着自己的寄存器访问,或者某些外设(如DDR、总线)的时钟一旦关闭系统直接挂死。用它的代价是省电能力受损,所以只应该给真正不能关的时钟加。
CLK_IGNORE_UNUSED则是"忽略未使用检查",让clk_disable_unused跳过它。区别在于,前者是全局保护,后者只是躲过一次清理。通常应该优先用CLK_IGNORE_UNUSED,只在确认关不掉会死机的场景下才用CLK_IS_CRITICAL。
我在一个低功耗项目里见过反例:有人给一堆外设时钟都加了CLK_IS_CRITICAL,导致设备无法进低功耗模式,功耗比正常高出一大截。排查时必须回到clk_summary看每个节点的标志位,逐一定位。
6.2 引用计数的坑:谁 enable 谁负责 disable
CCF的引用计数机制保证了共享时钟的安全,但前提是每个enable必须配对一个disable。出错的常见场景有两种。
一是probe中途失败后清理不全。devm_clk_get配合devm_clk_prepare_enable这类托管接口能自动在设备销毁时清理,是比较稳的写法;但如果手动写clk_prepare_enable,中途return -EIO时忘了配对,计数就永远不归零,时钟永远关不掉。
二是多个驱动共享同一路时钟时,各自认为对方会关。典型后果是这个时钟从来不会被真正关闭,功耗一直高。定位方法同样是看clk_summary里的enable_count——如果某个外设已经卸载了,它的时钟计数还是1,那就说明有泄漏。
一个实用习惯:在每个驱动的remove或probe失败路径里,都执行clk_disable_unprepare,即使你确信时钟是常开的,让CCF自己管理计数,不要人工判断"这个时钟该不该关"。
6.3 系统裁剪时关掉调试选项之后
量产固件为了节省内核体积,经常会关掉CONFIG_DEBUG_FS和CONFIG_COMMON_CLK_DEBUG。这带来的后果是:调试阶段能用的clk_summary全部消失,出问题时你再也看不到时钟树了。
我的建议是分两套配置维护:调试版本保留DEBUG_FS,量产版本关掉,但要把关键的时钟错误日志保留下来,比如clk_provider注册失败的返回值和设备树解析错误。这需要在驱动的错误处理路径里加足够的pr_err或dev_err,而不是把错误默默吞掉。
另外,裁剪系统时很容易漏掉CONFIG_COMMON_CLK本身。有些精简的配置里,时钟框架被关掉后,devm_clk_get会变成空实现,驱动里所有时钟操作都变成静默成功,硬件层面的问题要等到很远的地方才暴露出来。如果你的驱动用到了CCF的任何API,务必确认目标配置里CONFIG_COMMON_CLK=y。
最后一个体会跟所有的排错经历有关:时钟问题几乎没有"看代码就能看出来"的,一定要动手抓clk_summary、量波形、对比寄存器。我在实际调试里养成的习惯是,每次给新板子写驱动,第一件事就是把时钟树打印出来贴在工位上,每加一个外设就在上面标出它用哪一路。这张纸往往比任何文档都管用,因为它把"哪路时钟在动、谁在用它"这种抽象关系变成了可以随时对照的具体结构。等这张纸上的树画完、clk_summary的层级也对上,剩下的寄存器读写和业务逻辑才不会无谓地背锅。