Linux通用时钟框架CCF:时钟使用者API详解与驱动实战避坑指南
发布时间:2026/9/26 1:38:51来源:尧图网络
1. 从设备树到驱动Linux通用时钟框架到底在管什么搞嵌入式Linux的人迟早会跟时钟打交道。你写一个I2C驱动发现寄存器写不进去查了半天发现是I2C控制器的时钟没使能你调一个音频Codec采样率死活对不上最后发现是MCLK的分频系数配错了。这类问题在RK3568、i.MX、全志、瑞芯微这些平台上反复出现而解决它们的核心工具就是Linux的通用时钟框架Common Clock Framework简称CCF。CCF是Linux内核在2012年前后引入的一套子系统位于drivers/clk/目录下。它的核心目标只有一个把SoC内部错综复杂的时钟树抽象成统一的模型让驱动开发者用一套标准API就能获取时钟、使能时钟、设置频率和选择父时钟而不用关心底层是PLL、MUX、DIVIDER还是GATE。在没有CCF之前每个SoC厂商都自己搞一套时钟管理代码驱动里充斥着#ifdef CONFIG_ARCH_XXX移植成本极高。CCF出现之后时钟消费者consumer和时钟提供者provider彻底解耦设备树Device Tree成为两者之间的桥梁。这篇文章面向的是已经能写基本字符设备驱动、但对时钟框架还停留在“照抄dts里clk配置”阶段的开发者。我会从时钟使用者的角度出发把clk_get、clk_prepare_enable、clk_set_rate、clk_set_parent这些API的用法、坑点和底层逻辑讲透。读完之后你应该能做到拿到一份陌生的设备树能看懂时钟树的拓扑关系写驱动时能正确申请和释放时钟资源调频率时知道为什么clk_set_rate有时候“不生效”。先明确一个概念区分。在CCF的语境里时钟提供者provider是SoC时钟控制器驱动它注册struct clk_hw和struct clk_ops描述PLL、分频器、选择器、门控这些硬件单元。时钟使用者consumer是各种外设驱动比如UART、SPI、I2C、LCD控制器、音频接口它们通过struct clk *句柄来操作时钟。本文聚焦后者也就是“时钟使用者API”。你不需要去写clk_ops但你必须知道每个API调用背后发生了什么否则调试时会非常被动。提示CCF的API分为两套一套是老的clk_enable/clk_disable一套是clk_prepare_enable/clk_disable_unprepare。新代码一律用后者原因后面会详细讲。2. 时钟使用者API的核心接口与选型逻辑2.1 获取时钟句柄clk_get与devm_clk_get的区别驱动里拿时钟句柄最原始的方式是clk_getstruct clk *clk_get(struct device *dev, const char *id);dev是设备指针id是时钟在设备树里的名字对应clock-names属性。比如设备树里写了uart2: serialfe650000 { clocks cru SCLK_UART2, cru PCLK_UART2; clock-names baudclk, apb_pclk; };驱动里就要这样拿struct clk *baudclk clk_get(dev, baudclk); struct clk *apbclk clk_get(dev, apbclk);但实际项目中我几乎不用clk_get而是用devm_clk_getstruct clk *devm_clk_get(struct device *dev, const char *id);区别在于devm_前缀代表“设备资源管理”Device Managed。用devm_clk_get获取的时钟在设备卸载或驱动probe失败时内核会自动调用clk_put释放不需要你手动写错误处理路径。我踩过的坑是早期用clk_getprobe函数里有五个错误分支每个分支都要记得clk_put漏一个就造成引用计数泄漏时钟永远关不掉功耗下不去。换成devm_clk_get之后这类问题直接消失。还有一个变体devm_clk_get_optional它在时钟不存在时返回NULL而不是ERR_PTR(-ENOENT)。这个API适合那些“时钟可选”的场景比如某些低速外设可以走内部RC振荡器也可以走外部晶振。用devm_clk_get的话时钟不存在会直接报错驱动加载失败用devm_clk_get_optional则允许你判断if (clk)来决定后续行为。2.2 使能时钟为什么必须用clk_prepare_enable这是新手最容易犯错的地方。老API是int clk_enable(struct clk *clk); void clk_disable(struct clk *clk);新API是int clk_prepare_enable(struct clk *clk); void clk_disable_unprepare(struct clk *clk);为什么要有prepare这一层因为有些时钟的使能操作可能睡眠。比如一个PLL的锁定需要等待一段时间这期间要调用usleep_range或者时钟控制器挂在I2C/SPI总线上使能时钟需要发起一次总线传输。这些操作不能在原子上下文中断处理函数里执行。CCF把使能拆成两步clk_prepare可以睡眠的部分比如等待PLL锁定、配置寄存器。clk_enable不能睡眠的部分通常是简单的寄存器位操作。clk_prepare_enable就是两者依次调用。对应的clk_disable_unprepare先clk_disable再clk_unprepare。注意在中断上下文里只能调用clk_enable不能调用clk_prepare_enable。但实践中绝大多数驱动都在probe或resume路径里使能时钟这些路径允许睡眠所以直接用clk_prepare_enable是安全的。我实测过一个案例在RK3568平台上某个SPI屏的驱动在中断里调用clk_enable结果内核报“scheduling while atomic”。原因是该时钟的clk_ops-enable回调里调用了regmap_read而regmap底层走了I2CI2C传输会睡眠。后来改成在probe里clk_prepare_enable中断里只操作SPI数据问题解决。2.3 设置频率clk_set_rate的“不生效”之谜clk_set_rate的签名很直观int clk_set_rate(struct clk *clk, unsigned long rate);你传入目标频率单位Hz它返回0表示成功。但很多人发现调用之后用clk_get_rate读回来频率跟设的不一样。这不是bug而是CCF的设计逻辑时钟树有层级子时钟的频率受父时钟约束。举个例子。假设时钟树是这样的PLL (600MHz) - DIVIDER (1~32) - GATE - UART你想让UART跑115200波特率需要时钟频率是14.7456MHz假设16倍过采样。你调用clk_set_rate(uart_clk, 14745600)。CCF会从UART这个时钟节点向上遍历找到最近的可以调频率的节点——也就是那个DIVIDER。DIVIDER的父时钟是600MHz它只能做整数分频。600MHz / 14745600 40.69取整后分频系数是41实际输出600MHz / 41 14.634MHz。所以clk_get_rate返回的是14634146而不是14745600。这就是“不生效”的真相CCF只能在你给定的时钟树约束下找一个最接近目标频率的合法值。如果你需要精确频率要么换一个能产生该频率的父时钟比如专门的音频PLL要么用分数分频器fractional divider。clk_set_rate还有一个变体clk_set_rate_exclusive它会独占该时钟的频率设置权防止其他驱动同时修改。这个API在多驱动共享同一PLL时很有用但用不好会导致其他驱动设置频率失败。我的建议是除非你明确知道自己在做什么否则不要用exclusive版本。2.4 选择父时钟clk_set_parent的使用场景clk_set_parent用于切换时钟的父节点int clk_set_parent(struct clk *clk, struct clk *parent);典型场景是音频子系统。比如一个I2S控制器它的MCLK可以来自PLL_A适合48kHz系列采样率或PLL_B适合44.1kHz系列采样率。播放不同采样率的音频时驱动需要动态切换父时钟以得到精确的MCLK。struct clk *mclk devm_clk_get(dev, mclk); struct clk *pll_a devm_clk_get(dev, pll_a); struct clk *pll_b devm_clk_get(dev, pll_b); if (sample_rate % 48000 0) clk_set_parent(mclk, pll_a); else clk_set_parent(mclk, pll_b);这里有个坑clk_set_parent可能会失败如果目标父时钟当前被其他子时钟占用或者硬件不支持动态切换。失败时返回负值驱动必须检查返回值。我见过一个驱动直接忽略返回值结果播放44.1kHz音频时声音变调查了一天才发现是父时钟没切过去。2.5 其他常用API速查API作用是否可睡眠典型调用位置devm_clk_get获取时钟句柄是probeclk_prepare_enable准备并使能时钟是probe/resumeclk_disable_unprepare关闭并取消准备是remove/suspendclk_set_rate设置频率是probe/运行时clk_get_rate读取当前频率否任意clk_set_parent切换父时钟是运行时clk_get_parent读取当前父时钟否任意clk_is_enabled查询使能状态否调试提示clk_get_rate和clk_is_enabled不会睡眠可以在中断里调用但不要依赖它们做关键决策因为时钟状态可能被其他驱动并发修改。3. 设备树中的时钟绑定与实操解析3.1 clocks与clock-names的对应关系设备树是时钟使用者和提供者之间的契约。一个设备节点通过clocks属性引用时钟提供者的phandle通过clock-names给每个时钟起名字。驱动里devm_clk_get(dev, name)的name必须和clock-names里的字符串完全匹配。以RK3568的UART2为例uart2: serialfe650000 { compatible rockchip,rk3568-uart, snps,dw-apb-uart; reg 0x0 0xfe650000 0x0 0x100; interrupts GIC_SPI 117 IRQ_TYPE_LEVEL_HIGH; clocks cru SCLK_UART2, cru PCLK_UART2; clock-names baudclk, apb_pclk; status disabled; };这里clocks有两个条目clock-names也有两个顺序一一对应。第一个SCLK_UART2是波特率时钟第二个PCLK_UART2是APB总线时钟。驱动里通常这样写struct clk *baudclk, *apbclk; baudclk devm_clk_get(pdev-dev, baudclk); if (IS_ERR(baudclk)) return PTR_ERR(baudclk); apbclk devm_clk_get(pdev-dev, apbclk); if (IS_ERR(apbclk)) return PTR_ERR(apbclk);如果clock-names写错了比如写成baud_clk驱动里用baudclk去拿会返回ERR_PTR(-ENOENT)probe直接失败。这种错误在移植设备树时非常常见尤其是从其他平台抄dts的时候。3.2 assigned-clocks的自动配置机制设备树里还有一组属性assigned-clocks、assigned-clock-rates、assigned-clock-parents。这些属性让内核在设备初始化时自动配置时钟不需要驱动写代码。i2s1_8ch { assigned-clocks cru SCLK_I2S1_RX, cru SCLK_I2S1_TX; assigned-clock-rates 12288000, 12288000; assigned-clock-parents cru PLL_I2S; };这段配置的意思是在I2S1设备probe之前内核自动把SCLK_I2S1_RX和SCLK_I2S1_TX的父时钟设为PLL_I2S频率设为12.288MHz。驱动里就不需要再调用clk_set_parent和clk_set_rate了。这个机制的好处是配置与代码分离。同一份驱动在不同板子上可以用不同的设备树覆盖时钟配置随板子走。但坑在于assigned-clocks的处理时机是在of_clk_set_defaults里发生在设备probe之前。如果你的驱动在probe里又调了一次clk_set_rate会覆盖掉设备树的配置。所以要么全用设备树要么全用驱动代码不要混着来。3.3 时钟树的调试从sysfs看时钟状态调试时钟问题最直接的工具是sysfs。挂载debugfs之后mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/clk/clk_summary输出类似clock enable_cnt prepare_cnt rate accuracy phase -------------------------------------------------------------------------------------- clk_32k 1 1 32768 0 0 sclk_uart2 1 1 24000000 0 0 pclk_uart2 1 1 100000000 0 0enable_cnt是使能计数prepare_cnt是准备计数rate是当前频率。如果某个时钟的enable_cnt是0但你期望它是1说明驱动没使能时钟。如果rate跟你设的不一样说明分频系数被约束了。我习惯在驱动probe前后各dump一次clk_summary对比哪些时钟的计数变了。这个方法能快速定位“哪个时钟没开”或者“哪个时钟被多开了一次”。注意clk_summary的输出格式在不同内核版本略有差异但核心字段一致。如果debugfs没挂载先确认内核配置里开了CONFIG_DEBUG_FS和CONFIG_COMMON_CLK_DEBUG。4. 驱动中的时钟管理实战从probe到remove4.1 probe阶段的时钟获取与使能顺序一个规范的时钟使用者驱动probe阶段通常按以下顺序操作获取所有需要的时钟句柄devm_clk_get。检查返回值任何一个失败就返回错误。设置频率和父时钟如果设备树没配。使能时钟clk_prepare_enable。访问硬件寄存器初始化设备。注册字符设备、输入设备、网络设备等。顺序很重要。必须先使能时钟再访问寄存器。否则访问会触发总线错误SIGBUS或者返回全0xFF。我在全志H3平台上遇到过SPI控制器驱动先读寄存器再使能时钟结果读出来全是0驱动误判硬件不存在直接返回-ENODEV。代码示例static int my_probe(struct platform_device *pdev) { struct my_dev *mdev; int ret; mdev devm_kzalloc(pdev-dev, sizeof(*mdev), GFP_KERNEL); if (!mdev) return -ENOMEM; mdev-clk devm_clk_get(pdev-dev, core); if (IS_ERR(mdev-clk)) return dev_err_probe(pdev-dev, PTR_ERR(mdev-clk), failed to get core clock\n); mdev-pclk devm_clk_get(pdev-dev, apb); if (IS_ERR(mdev-pclk)) return dev_err_probe(pdev-dev, PTR_ERR(mdev-pclk), failed to get apb clock\n); ret clk_set_rate(mdev-clk, 50000000); if (ret) return dev_err_probe(pdev-dev, ret, failed to set core clock rate\n); ret clk_prepare_enable(mdev-clk); if (ret) return dev_err_probe(pdev-dev, ret, failed to enable core clock\n); ret clk_prepare_enable(mdev-pclk); if (ret) { clk_disable_unprepare(mdev-clk); return dev_err_probe(pdev-dev, ret, failed to enable apb clock\n); } /* 现在可以安全访问寄存器了 */ my_hw_init(mdev); return 0; }注意dev_err_probe这个辅助函数它会把错误码转成可读字符串并且对-EPROBE_DEFER做特殊处理不打印错误日志因为这是正常的延迟探测。在新内核里推荐用它替代dev_err。4.2 remove与suspend/resume中的时钟处理remove阶段要关闭时钟。如果用devm_clk_get获取句柄clk_put是自动的但clk_disable_unprepare必须手动调用static int my_remove(struct platform_device *pdev) { struct my_dev *mdev platform_get_drvdata(pdev); clk_disable_unprepare(mdev-pclk); clk_disable_unprepare(mdev-clk); return 0; }suspend/resume里也要处理时钟。如果设备在suspend时不需要保持时钟就关闭resume时重新使能。但要注意有些时钟关闭后寄存器的值会丢失resume时需要重新初始化硬件。static int my_suspend(struct device *dev) { struct my_dev *mdev dev_get_drvdata(dev); clk_disable_unprepare(mdev-clk); return 0; } static int my_resume(struct device *dev) { struct my_dev *mdev dev_get_drvdata(dev); int ret; ret clk_prepare_enable(mdev-clk); if (ret) return ret; my_hw_reinit(mdev); return 0; }提示如果设备支持运行时PMRuntime PM时钟管理会更复杂。基本原则是在runtime_suspend里关时钟在runtime_resume里开时钟并且用pm_runtime_get_sync/pm_runtime_put来管理引用计数。4.3 时钟引用计数与并发保护CCF内部对每个时钟维护enable_count和prepare_count。每次clk_prepare_enable加1每次clk_disable_unprepare减1。只有计数降到0时硬件时钟才真正关闭。这个设计允许多个驱动共享同一个时钟比如两个SPI控制器共用同一个PCLK。但引用计数不是线程安全的。如果两个驱动并发调用clk_prepare_enable可能一个成功一个失败或者计数出错。CCF内部用了自旋锁保护计数但驱动层面仍然要注意不要在多个上下文里同时操作同一个时钟句柄。如果确实需要用互斥锁保护。我遇到过一个案例一个驱动在中断里调用clk_enable在workqueue里调用clk_disable结果计数变成负数内核报“clk: invalid enable count”。后来改成所有时钟操作都在workqueue里串行执行问题消失。5. 常见问题排查与避坑指南5.1 clk_get返回-ENOENT的排查思路devm_clk_get返回-ENOENT说明设备树里没有对应的时钟。排查步骤确认设备节点的clock-names属性存在且字符串拼写正确。确认clocks属性的条目数和clock-names一致。确认时钟提供者节点比如cru的#clock-cells和phandle正确。用of_dump或cat /proc/device-tree/.../clock-names查看实际解析结果。常见错误是clock-names里用了下划线驱动里用了连字符或者大小写不一致。设备树是大小写敏感的。5.2 clk_set_rate返回-EINVAL的原因clk_set_rate返回-EINVAL通常是因为目标频率超出了时钟的可调范围。比如一个固定分频器只能输出1MHz、2MHz、4MHz你设3MHz就会失败。排查方法cat /sys/kernel/debug/clk/clk_summary看该时钟的rate范围。或者用clk_round_rate先查询long rounded clk_round_rate(clk, target_rate); if (rounded 0) return rounded; clk_set_rate(clk, rounded);clk_round_rate返回的是最接近目标频率的合法值不改变硬件状态。先round再set可以避免-EINVAL。5.3 时钟使能后设备仍不工作的检查清单如果时钟使能了但设备还是不工作按以下顺序检查检查项方法可能问题时钟频率clk_get_rate频率不对分频系数错父时钟clk_get_parent父时钟选错频率源不对复位信号检查reset控制器设备处于复位状态电源域检查power domain电源未上电引脚复用检查pinctrl引脚功能没配成外设模式寄存器写入读回寄存器总线时钟没使能我遇到过最隐蔽的一个问题时钟频率、父时钟、复位、电源都正常但设备就是不工作。最后发现是pinctrl配置里时钟输出引脚被配成了GPIO输入模式时钟信号根本没送到外设。所以时钟框架只管“时钟源”不管“时钟线”的物理连接。5.4 时钟相关的内核报错速查报错信息含义解决方法clk: invalid enable count使能计数为负检查enable/disable是否配对scheduling while atomic在原子上下文调用了可睡眠API改用clk_enable或移到workqueuefailed to get clockdevm_clk_get失败检查设备树clock-namesclk_set_rate failed频率设置失败用clk_round_rate先查询clock is not prepared未prepare就enable用clk_prepare_enable提示内核启动参数加clk_ignore_unused可以防止未使用的时钟被关闭适合调试阶段。但生产环境不要加否则功耗下不去。6. 从使用者视角理解时钟框架的扩展能力6.1 clk_notifier频率变化的异步通知有些驱动需要在时钟频率变化时做出响应。比如一个定时器驱动时钟频率变了定时周期就要重新计算。CCF提供了clk_notifier_registerstruct clk_notifier { struct list_head node; struct clk *clk; struct notifier_block nb; }; int clk_notifier_register(struct clk *clk, struct notifier_block *nb);当clk_set_rate成功改变频率后CCF会调用注册的回调函数传入PRE_RATE_CHANGE、POST_RATE_CHANGE或ABORT_RATE_CHANGE事件。驱动可以在POST_RATE_CHANGE里用clk_get_rate读取新频率更新内部状态。这个机制在音频子系统里用得很多。I2S控制器的MCLK频率变了Codec的采样率就要跟着调。但要注意notifier回调是在持有自旋锁的上下文里调用的不能睡眠不能调用clk_set_rate。6.2 clk_bulk批量时钟管理如果一个设备需要很多时钟比如LCD控制器可能需要PCLK、HPCLK、VPLL、DPHY等五六个时钟逐个devm_clk_get和clk_prepare_enable很啰嗦。CCF提供了批量APIstruct clk_bulk_data { const char *id; struct clk *clk; }; int devm_clk_bulk_get(struct device *dev, int num_clks, struct clk_bulk_data *clks); int clk_bulk_prepare_enable(int num_clks, struct clk_bulk_data *clks); void clk_bulk_disable_unprepare(int num_clks, struct clk_bulk_data *clks);用法static const struct clk_bulk_data lcd_clks[] { { .id pclk }, { .id hclk }, { .id vpll }, { .id dphy }, }; struct clk_bulk_data *clks; int num_clks ARRAY_SIZE(lcd_clks); clks devm_kmemdup(pdev-dev, lcd_clks, sizeof(lcd_clks), GFP_KERNEL); ret devm_clk_bulk_get(pdev-dev, num_clks, clks); if (ret) return ret; ret clk_bulk_prepare_enable(num_clks, clks); if (ret) return ret;批量API的好处是错误处理简单任何一个时钟获取失败devm_clk_bulk_get会自动释放已经获取的时钟。使能时如果中间某个失败clk_bulk_prepare_enable会回滚已经使能的时钟。这比手动写循环和错误分支可靠得多。6.3 时钟精度与jitter什么时候需要关心大多数驱动不需要关心时钟精度但音频、视频、射频类驱动必须关心。clk_get_accuracy返回时钟的精度单位ppb十亿分之一long clk_get_accuracy(struct clk *clk);如果返回0表示精度未知或无限精确。对于音频MCLK精度直接影响采样率误差。一个100ppm的时钟在48kHz采样率下每秒误差4.8个采样点几分钟后就能听出音调偏差。如果SoC的音频PLL精度不够可以考虑用外部低jitter晶振作为时钟源。设备树里把assigned-clock-parents指向外部晶振对应的时钟节点即可。6.4 时钟框架与电源管理的协同现代SoC里时钟和电源域是绑定的。一个电源域下电时其内部的时钟也会丢失。CCF通过clk_pm_runtime相关的API与genpdGeneric Power Domain协同。驱动开发者需要知道的是如果设备在运行时PM里关闭了时钟那么访问寄存器前必须重新使能时钟。有些驱动在runtime_resume里只调用了pm_runtime_get_sync忘了clk_prepare_enable结果访问寄存器时总线报错。正确的做法是在runtime_resume里同时处理电源域和时钟static int my_runtime_resume(struct device *dev) { struct my_dev *mdev dev_get_drvdata(dev); int ret; ret clk_prepare_enable(mdev-clk); if (ret) return ret; /* 电源域由pm_runtime自动管理 */ return 0; }7. 个人实操体会与几个容易忽略的细节调了这么多年的时钟我最大的体会是时钟问题很少是CCF本身的bug绝大多数是设备树配置和驱动调用顺序的问题。CCF的API设计已经足够健壮但它的行为高度依赖设备树描述的时钟树拓扑。如果设备树写错了API调用再正确也没用。几个我踩过多次的坑分享出来帮你省时间第一clk_prepare_enable和clk_disable_unprepare必须严格配对。我见过一个驱动在probe里使能了两次remove里只关闭了一次结果时钟引用计数永远不为0系统进入suspend时功耗偏高。用devm_add_action_or_reset可以自动处理这种配对但需要额外写回调函数。第二clk_set_rate之后一定要用clk_get_rate确认实际频率。不要假设设置的值就是生效的值。尤其是在有多个驱动共享PLL的场景下你的设置可能被其他驱动覆盖。第三设备树里的assigned-clock-rates和驱动里的clk_set_rate不要同时用。如果设备树配了驱动里就不要再设如果驱动里设了设备树里就留空。两者同时存在时执行顺序是设备树先、驱动后驱动会覆盖设备树但设备树的配置仍然会消耗一次时钟操作可能触发不必要的PLL重锁。第四调试时钟问题时clk_summary比printk好用。在驱动里加printk需要重新编译内核而clk_summary随时可以看。养成在probe前后dump时钟状态的习惯能快速缩小问题范围。最后分享一个实用技巧如果怀疑某个时钟没使能可以在内核命令行加clk_ignore_unused让所有时钟保持开启。如果加了之后设备正常工作说明确实是时钟被意外关闭了。然后逐步去掉这个参数用clk_summary观察哪个时钟的enable_cnt变成了0就能定位到是哪个驱动提前关闭了时钟。这个方法我在RK3568和i.MX8M上都用过百试百灵。
网站建设高端定制企业官网