嵌入式Linux时钟驱动开发:从CCF核心结构体到设备树实战
2026/9/22 6:20:20 网站建设 项目流程

1. 为什么需要CCF:从点灯配置寄存器到框架化管理

我早年调试一块嵌入式板卡的时候,I2C总线上的触摸屏总是随机失效,用示波器抓SCL引脚发现时钟频率漂得离谱——后来查了半天,发现是某个驱动在初始化时直接ioremap了时钟控制器的寄存器,随手改了一位分频值,把I2C的根时钟给带偏了。这种"各驱动各自为政、直接操作时钟寄存器"的老式做法,在核数少、外设少的单片机上还能凑合,一旦进入嵌入式Linux这种多进程、多驱动并发的环境,立刻就会变成灾难。

这正是CCF(Common Clock Framework,通用时钟框架)存在的根本原因。它不是一个单纯帮你"配置时钟"的API集合,而是把整个SoC的时钟资源抽象成了一棵有层级关系的树:每个时钟节点知道自己从哪里来(parent)、能输出什么频率(rate)、当前有没有被使用(prepare/enable引用计数),并且通过统一的接口对外提供服务。驱动开发者不再需要关心某个PLL的锁定寄存器在哪、分频器的系数怎么算,只需要向CCF"申请"一个时钟,然后调用标准的clk_get_rateclk_set_rateclk_prepare_enable等API即可。

本文就围绕CCF框架下时钟驱动的完整开发流程展开,从核心数据结构到设备树绑定,从驱动代码编写到调试实测,把整个链路掰开揉碎。适合正在做嵌入式Linux驱动开发、尤其是第一次接触CCF的工程师参考。如果你以前只用过旧的clk_register接口,那这篇文章也能帮你梳理清楚新旧接口的对应关系。

1.1 没有CCF时,驱动开发者在做什么

在老的内核版本或者一些轻量级RTOS里,驱动要使用一个时钟,流程通常是这样的:

  1. 在SoC手册里查到目标外设(比如UART)的时钟源是PLL2的某个分频输出。
  2. 手动计算PLL2的倍频系数、分频系数,算出目标频率对应的寄存器值。
  3. 直接ioremap时钟控制器的基地址,往寄存器里写值。
  4. 如果驱动A和驱动B同时用到了同一个PLL,要么靠约定俗成的顺序初始化,要么就得自己维护一个"谁在用"的计数。

这种做法有几个致命问题:第一,寄存器操作散落在各个驱动里,代码冗余还容易写错;第二,无法感知时钟的父子依赖关系,改了一个PLL的频率,下游所有分频输出的外设全部遭殃;第三,没有引用计数,驱动卸载时时钟状态无法自动恢复。CCF把这些问题全部收归框架管理,驱动开发者只跟struct clk指针打交道,底层的寄存器操作由时钟驱动实现者负责。

1.2 CCF的三大角色:Provider、Consumer、Framework

理解CCF之前,先记住三个角色:

  • Provider(时钟提供者):通常是某个时钟控制器驱动,负责注册一个或多个时钟节点到CCF,实现各种底层操作回调,比如计算频率、设置频率、开关门控等。
  • Consumer(时钟使用者):任何想要使用时钟的外设驱动,通过设备树或板级代码获取struct clk指针,调用通用API。
  • Framework(框架核心):内核的drivers/clk/clk.c,维护时钟树、引用计数、rate传播通知、debugfs统计等。

这三者之间,Provider和Consumer完全通过Framework间接交互,互不知道对方的存在。这种解耦恰恰是CCF能在一个复杂SoC上稳定运行的根基。

2. 动手写驱动前,必须吃透的三个核心结构体

2.1 struct clk_hw:框架视角下的"硬件句柄"

很多刚接触CCF的人会被struct clkstruct clk_hw搞晕。简单来说:

  • struct clk是Consumer手里的"句柄",由框架分配,驱动代码里根本不需要自己创建。
  • struct clk_hw是Provider驱动的核心,它是对一个具体时钟硬件的抽象,里面包含框架需要的一切信息。
struct clk_hw { struct clk_core *core; struct clk_init_data *init; struct clk_ops *ops; };

其中clk_core是框架内部真正管理时钟树的节点,包含parent指针、引用计数、rate等信息,驱动开发中一般不直接碰它。clk_init_data是注册时传给框架的初始化数据,clk_ops则是这个时钟硬件支持的操作函数集合。

写一个时钟驱动,本质就是:填充clk_hw,实现clk_ops,然后把clk_hw注册进CCF的Provider列表里。至于这个clk_hw背后是PLL、MUX、divider还是gate,框架并不关心,它只认你实现的clk_ops

2.2 struct clk_ops:一个时钟的"行为契约"

clk_ops里的每个回调都对应一个标准API,比如Consumer调clk_prepare_enable()时,框架会依次调用注册的prepareenable回调。核心回调如下:

回调对应API语义注意事项
prepareclk_prepare可能睡眠的准备工作,比如PLL锁定进程上下文,可以mutex
unprepareclk_unprepareprepare的反操作进程上下文
enableclk_enable原子操作,比如门控写位中断/自旋锁上下文,不能睡眠
disableclk_disableenable的反操作原子上下文
recalc_rateclk_get_rate根据当前寄存器状态计算实际频率必须真实反映硬件,不能拍脑袋
round_rateclk_round_rate查询目标频率最接近的可实现值返回硬件能实际输出的频率
set_rateclk_set_rate配置寄存器让输出频率等于目标值通常先调round_rate
set_parentclk_set_parent切换时钟源针对MUX型时钟
get_parentclk_get_parent查询当前父时钟返回父clock的index
is_enabledclk_is_enabled查询时钟当前是否开启有时不必实现,读硬件状态更可靠

这里面最有技术含量的是round_rate+set_rate+recalc_rate这组配套操作,它们共同保证了一个时钟的频率配置链路是自洽的。round_rate是"承诺",set_rate是"执行",recalc_rate是"反馈",三者必须一致,否则Consumer读取的频率会和实际不匹配。

2.3 struct clk_init_data:注册时的一次性"简历"

struct clk_init_data { const char *name; const struct clk_ops *ops; const char * const *parent_names; u8 num_parents; unsigned long flags; };

这里最关键的是parent_namesflags

parent_names是父母时钟的名字列表,框架会根据名字在全局时钟树里查找并建立父子关系。对于单时钟源的divider/gate,只需要一个名字;对于MUX型多路选择器,需要把所有可能的父时钟名都列出来。

flags里的常用标志有:

  • CLK_SET_RATE_PARENT:当前时钟设置频率时,如果做不到,允许向上游传播频率请求,让父时钟调整频率。典型场景是I2C的根时钟来自PLL分频,你只想设置I2C的频率,但实际要调整的是PLL。
  • CLK_IGNORE_UNUSED:内核在启动后期会关闭所有没有被使用的时钟,如果不希望某个时钟被自动关闭(比如看门狗时钟),就加上这个标志。
  • CLK_GET_RATE_NOCACHE:每次读频率都重新调用recalc_rate,不从缓存读,适合频率会被硬件自动改变的时钟。

3. 时钟驱动开发全流程:从设备树节点到Rate调整实现

下面我以一块虚拟SoC上的外设时钟为例,走一遍完整的开发流程。这个例子极具普遍性:SoC内部有一个PLL作为主时钟源,经过一个DIV分频器后,输出给UART外设使用。我们要为这个DIV分频器实现一个CCF时钟驱动。

3.1 设备树侧:如何声明一个时钟输出

首先在设备树里声明时钟控制器节点:

clock-controller@10001000 { compatible = "virtual,div-clock"; reg = <0x10001000 0x1000>; clocks = <&pll0>; #clock-cells = <0>; };

注意#clock-cells = <0>表示这个控制器只有一个输出时钟。如果#clock-cells = <1>,则意味着输出多个时钟,Consumer引用时需要传递一个索引号,例如clocks = <&foo 2>

然后在UART节点里引用这个时钟:

uart0: serial@10000000 { compatible = "virtual,uart"; reg = <0x10000000 0x1000>; clocks = <&div_clock>; clock-names = "uart_clk"; };

设备树里的clocks属性是Consumer和Provider绑定的桥梁,驱动代码通过devm_clk_get()加时钟名来获取对应的struct clk指针。

3.2 驱动侧:probe函数里的注册顺序

一个最简单的divider时钟驱动框架如下:

#include <linux/clk-provider.h> #include <linux/platform_device.h> #include <linux/module.h> #include <linux/of.h> #include <linux/clk.h> #define DIV_REG_OFFSET 0x00 struct div_clock_priv { void __iomem *base; struct clk_hw hw; }; static unsigned long div_clock_recalc_rate(struct clk_hw *hw, unsigned long parent_rate) { struct div_clock_priv *priv = container_of(hw, struct div_clock_priv, hw); u32 val = readl(priv->base + DIV_REG_OFFSET); int div = (val & 0xFF) + 1; return parent_rate / div; } static long div_clock_round_rate(struct clk_hw *hw, unsigned long rate, unsigned long *parent_rate) { int div = DIV_ROUND_CLOSEST(*parent_rate, rate); return *parent_rate / div; } static int div_clock_set_rate(struct clk_hw *hw, unsigned long rate, unsigned long parent_rate) { struct div_clock_priv *priv = container_of(hw, struct div_clock_priv, hw); int div = DIV_ROUND_CLOSEST(parent_rate, rate); u32 val; if (div < 1 || div > 256) return -EINVAL; val = readl(priv->base + DIV_REG_OFFSET); val &= ~0xFF; val |= (div - 1); writel(val, priv->base + DIV_REG_OFFSET); return 0; } static const struct clk_ops div_clock_ops = { .recalc_rate = div_clock_recalc_rate, .round_rate = div_clock_round_rate, .set_rate = div_clock_set_rate, }; static int div_clock_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct div_clock_priv *priv; struct clk_init_data init = {0}; struct clk_parent_data parent_data = {0}; int ret; priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->base = devm_platform_ioremap_resource(pdev, 0); if (IS_ERR(priv->base)) return PTR_ERR(priv->base); init.name = dev_name(dev); init.ops = &div_clock_ops; parent_data.fw_name = "parent"; init.parent_data = &parent_data; init.num_parents = 1; priv->hw.init = &init; ret = devm_clk_hw_register(dev, &priv->hw); if (ret) return ret; ret = devm_of_clk_add_hw_provider(dev, of_clk_hw_simple_get, &priv->hw); if (ret) return ret; return 0; } static const struct of_device_id div_clock_match[] = { { .compatible = "virtual,div-clock" }, { } }; MODULE_DEVICE_TABLE(of, div_clock_match); static struct platform_driver div_clock_driver = { .probe = div_clock_probe, .driver = { .name = "div-clock", .of_match_table = div_clock_match, }, }; module_platform_driver(div_clock_driver);

这里需要注意几个细节:

  • 父时钟的获取用了clk_parent_data搭配fw_name,这是新版本内核推荐的方式,本质上是在设备树里用clocks = <&pll0>声明父时钟,驱动里通过fw_name去匹配。老代码里直接在init.parent_names里写死父时钟名字,那种方式不灵活,一旦时钟树改名就会挂。
  • devm_clk_hw_register()devm_of_clk_add_hw_provider()都带devm前缀,驱动卸载时框架会自动清理,不需要手动处理。
  • 如果这个时钟在系统早期就要使用,比如早期的串口打印,那就不能用platform_driver的方式,必须改用CLK_OF_DECLARE,在setup_arch阶段就完成注册。判断依据很简单:属于基础时钟树上的节点,尽量提前;普通外设时钟,platform_driver足够。

3.3 Consumer视角:外设驱动如何调用

时钟驱动注册完之后,外设驱动这边就是标准的CCF API使用流程:

struct clk *clk; int ret; clk = devm_clk_get(dev, "uart_clk"); if (IS_ERR(clk)) { dev_err(dev, "failed to get uart clock: %ld\n", PTR_ERR(clk)); return PTR_ERR(clk); } ret = clk_prepare_enable(clk); if (ret) return ret; unsigned long rate = clk_get_rate(clk); dev_info(dev, "UART clock rate: %lu Hz\n", rate); /* 如果需要调整频率 */ ret = clk_set_rate(clk, 48000000); if (ret) dev_err(dev, "failed to set clock rate: %d\n", ret);

clk_prepare_enable()clk_prepare()clk_enable()的合并快捷调用,最常见。这里有个小坑:clk_prepare可以睡眠,clk_enable不能,所以如果你在原子上下文(比如中断处理函数)里必须分成两步,提前在进程上下文prepare,然后在原子上下文只调clk_enable——很多驱动在这里踩过BUG睡眠的雷。

4. 时钟不听话怎么办:故障排查链路与实测心得

代码写完只是第一步,真正折磨人的是调试阶段。我把自己反复踩过的坑和排查思路整理一下。

4.1 从clk_summary定位异常节点

CCF框架自带debugfs节点,这是排查时钟问题最重要的入口:

# 挂载debugfs(如果尚未挂载) mount -t debugfs none /sys/kernel/debug # 查看整棵时钟树的状态 cat /sys/kernel/debug/clk/clk_summary

输出大致长这样:

clock enable_cnt prepare_cnt rate accuracy phase -------------------------------------------------------------------------------------------- pll0 1 1 120000000 0 0 div_clock 1 1 48000000 0 0 uart0_clk 1 1 48000000 0 0

这个表的信息量极大:enable_cnt和prepare_cnt都能看到每个时钟被引用的次数,rate列能看到实际计算出的频率。如果某个时钟的rate是0,说明recalc_rate没实现或者父时钟rate本身就为0;如果enable_cnt不符合预期,说明有驱动忘了enable或者unprepare。

4.2 set_rate不生效的典型原因与调用链走读

clk_set_rate()是使用频率最高的API之一,它的调用链大致如下:

  1. Consumer调用clk_set_rate(clk, target)
  2. 框架获取时钟树全局锁,调用clk_calc_new_rates(),沿着时钟树从下往上找能够实现目标频率的节点。
  3. 框架调用clk_core_round_rate_nolock(),也就是你的round_rate回调,判断当前节点能否实现目标频率。
  4. 如果当前节点做不到且设置了CLK_SET_RATE_PARENT,框架会继续修改父时钟的频率请求,然后重新计算。
  5. 最终调用clk_change_rate(),依次调用set_rate回调,然后调用recalc_rate更新实际的rate缓存。

所以如果你的set_rate调用了但实际输出不变,复盘时可以按这个顺序排查:

  • round_rate是不是返回了错误的值?框架会用round_rate的结果作为最终频率,如果你这里返回的值和set_rate里算出来的分频比不一致,就会导致设置完频率不对。
  • 有没有加CLK_SET_RATE_PARENT?如果当前分频器能实现目标的整数比值,但硬件分频范围太窄,这时需要父时钟配合调整,不加这个标志,set_rate直接返回一个误差范围内的近似值,甚至直接失败。
  • 寄存器写入位是否正确?我遇到过把chip select偏移当成分频值的,写倒是写了,但写到了相邻的位域上。这时候读一下寄存器对照数据手册就能发现。

4.3 自测方法:trace event配合示波器双验证

软件层面看树、看寄存器是一方面,最终验证还是得回到物理世界。我的习惯是双管齐下:

内核里打开时钟事件的trace:

trace-cmd record -e clk:clk_enable -e clk:clk_disable -e clk:clk_set_rate

然后跑应用触发时钟变更,再用trace-cmd report查看,能精确看到驱动在什么时刻请求了哪个时钟、频率变成了多少,排查逻辑问题非常好使。

硬件层面,如果调的是UART、I2C这类外设时钟,直接测对应引脚的波形,数一下频率对不对。这里有一个经验:很多SoC的UART波特率和时钟频率之间不是直接相等的,中间还有分频逻辑,你测到的引脚波形频率可能是时钟频率的1/16,别一看到数字对不上就以为驱动有问题。

4.4 一个必须记住的原子上下文教训

最后讲一个我调查功耗问题时遇到的案例。内核启动过程中会调用clk_disable_unused()关闭所有没有使用者且没有CLK_IGNORE_UNUSED标志的时钟。如果某个时钟驱动没实现is_enabled回调,框架会按默认逻辑处理,假设时钟是开启的,进而调用disable。但如果这个时钟硬件其实已经被某个早期启动代码关了,第二次关闭时寄存器写入的就是一个非法状态。这类问题不会稳定复现,非常隐蔽,排查手段就是加打印,在disable回调里读寄存器确认实际状态,并且尽量实现is_enabled,让框架准确感知硬件状态。

5. 为什么强调不要绕过CCF:代价与长期收益

项目开发中经常会遇到这样的灵魂拷问:CCF这么复杂,我直接在驱动里操作寄存器还更快,为什么非要绕一圈?我给的建议是:除非你开发的是一个永远不更新、永远不换内核、外设永远不会动态开关频率的板子,否则请老老实实走框架。

原因有几点:

第一,CCF帮你管好了并发。两个驱动同时调用clk_enable引用同一个时钟,框架内部有锁和引用计数,不会出现一个驱动关掉另一个驱动正需要的时钟这种事故。自己操作寄存器根本做不到。

第二,挂起/恢复的电源管理只有框架能做到。系统休眠时,框架会根据PM回调自动关停所有使用者为0的时钟;恢复时按顺序重新打开。这个过程如果靠各个驱动各自去弄,必然会有遗漏,而且难以调试。

第三,调试信息是免费的。上面说的clk_summary、trace event,全部由框架提供,绕过框架就意味着一出事就得自己打dev_dbg逐行查寄存器,效率完全不同。

这套框架在i.MX、Rockchip、Allwinner等常见SoC上早已全面落地,新板子的BSP基本都是基于CCF来实现时钟树。学会CCF不只意味着你能写时钟驱动,更重要的是能看懂厂商SDK里复杂的时钟树实现——很多外设驱动的怪异行为,顺着时钟树往上一查,答案往往就在某个divider的配置逻辑里。

最后分享一个我个人的学习心得:不要一上来就去读drivers/clk/clk.c这个核心文件里那些复杂的rate传播算法,那是框架维护者才需要深入关心的东西。做驱动开发,先从clk_ops的实现入手,对照着数据手册,把自己手里这颗芯片的PLL、divider、gate分别实现出来,跑通一个外设,再回头读框架代码,会有完全不同的体会。CCF是一套边界清晰的设计,你只要把自己这一层的职责做好,上面的框架和下面的硬件,都由它们自己去衔接。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询