4. 时钟框架概述:Linux Common Clock Framework (CCF) 架构与核心数据结构
时钟管理,说白了就是给芯片里的各个模块提供心跳。你想想看,CPU要跑、DMA要搬数据、外设要收发,哪个离得开时钟?早期Linux里,各家SoC的时钟驱动都是各写各的,那叫一个乱。直到Common Clock Framework(CCF)出现,才算是把这事儿给统一了。
我个人习惯把CCF理解成一个「时钟超市」。你不需要关心时钟是怎么产生的,只需要告诉框架:我要一个多少MHz的时钟,给我挂上就行。框架底层会帮你搞定PLL配置、分频系数、门控开关这些脏活累活。
4.1 CCF要解决什么问题?
在CCF出现之前,我见过最离谱的代码——一个时钟操作函数能写300行,里面全是直接操作寄存器。换一颗SoC,整个驱动就得重写。CCF的核心目标就三个:
- 统一抽象:把时钟源、分频器、门控、多路选择器都抽象成统一的「时钟节点」
- 拓扑管理:自动处理时钟树上下游的依赖关系,比如关一个父时钟前,先看看有没有子时钟在用
- 驱动复用:同样的PLL驱动代码,换颗芯片改个寄存器地址就能用
核心思想:CCF把硬件时钟树映射成软件里的树形结构。每个节点都是一个struct clk_hw,节点之间通过parent指针连接。你操作任何一个节点,框架都会自动向上追溯,确保整个链路状态一致。
4.2 核心数据结构:从硬件到软件的映射
CCF里最重要的三个结构体,我当年啃代码的时候可是反复看了好几遍。咱们一个一个说。
4.2.1 struct clk_hw —— 时钟硬件抽象
这是每个时钟节点的「身份证」。它不直接操作寄存器,而是通过ops回调函数来间接操作。说白了,它就是硬件和框架之间的适配层。
struct clk_hw {
struct clk_core *core; // 指向框架内部核心结构
struct clk_init_data *init; // 初始化数据,包括名字、ops、父时钟列表
};
嗯,这里要注意:clk_hw 是驱动开发者主要打交道的对象。你写一个时钟驱动,本质上就是填充一个 clk_hw,然后注册到框架里。
4.2.2 struct clk_ops —— 操作函数集
这个结构体定义了时钟节点支持的所有操作。我建议你把它想象成一张「能力清单」——你的硬件支持什么操作,就实现对应的回调函数。
| 回调函数 | 作用 | 我在项目中遇到过的问题 |
|---|---|---|
| prepare/unprepare | 可能睡眠的操作(如PLL锁定等待) | 曾经在中断上下文里调了prepare,直接内核panic |
| enable/disable | 原子操作,不能睡眠(如门控开关) | spinlock没处理好,导致死锁 |
| set_rate/recalc_rate | 设置/计算当前频率 | 分频系数算错,导致UART波特率偏差 |
| round_rate | 返回硬件支持的最接近频率 | 没实现这个,上层调用clk_round_rate直接返回0 |
个人经验:实现ops时,一定要区分清楚哪些函数可以睡眠,哪些不能。prepare系列可以调mutex,enable系列只能用spinlock。我刚开始写驱动时,就因为在enable里调了msleep,结果系统直接卡死。
4.2.3 struct clk_init_data —— 初始化数据
这个结构体是注册时钟节点时的「配置单」。你告诉框架:我这个时钟叫什么名字,父时钟是谁,支持哪些操作。
struct clk_init_data {
const char *name; // 时钟名字,dts里clock-names就靠这个匹配
const struct clk_ops *ops; // 操作函数集
const char **parent_names; // 父时钟名字列表
u8 num_parents; // 父时钟数量
};
我记得有一次调试,发现时钟频率死活不对。查了半天,原来是 parent_names 里写错了父时钟名字,框架找不到父节点,直接用了默认频率。从那以后,我每次注册时钟都会先打印一下父时钟的匹配情况。
4.3 时钟框架的层次结构
CCF内部其实分了三层,我画个图帮你理解:
你看这个图就清楚了:
- 最上层是驱动开发者直接调用的API,比如
clk_prepare_enable()。你不需要知道底层怎么操作寄存器。 - 中间层是框架的核心逻辑,处理各种边界情况。比如你调
clk_disable()时,框架会检查引用计数,只有计数归零时才真正调用你的disable回调。 - 最底层才是我们驱动开发者需要关心的。实现
struct clk_ops,告诉框架怎么操作硬件。
4.4 时钟注册流程:从dts到内核
我习惯把时钟驱动的加载流程分成四步,每一步都有坑:
- 解析dts:内核启动时,遍历设备树中的
clocks节点,为每个节点分配struct clk_hw - 初始化数据:从dts中读取
clock-frequency、clock-output-names等属性,填充clk_init_data - 注册到框架:调用
clk_hw_register()或devm_clk_hw_register(),将节点加入全局时钟树 - 提供对外接口:通过
of_clk_add_hw_provider()告诉其他驱动:我这个节点可以提供时钟了
避坑指南:我曾经在注册时钟时忘了调 of_clk_add_hw_provider(),结果其他驱动通过dts引用这个时钟时,返回的永远是 -EPROBE_DEFER。查了两天才发现是provider没注册。
4.5 时钟框架的调试手段
调试时钟问题,我常用的几个方法:
- /sys/kernel/debug/clk/clk_summary:查看整个时钟树的状态,每个节点的频率、使能计数一目了然
- clk_ignore_unused:内核启动参数,防止框架关闭未使用的时钟。调试阶段必加
- trace_clk:打开内核的时钟trace点,可以追踪每次clk_enable/clk_disable的调用栈
说实话,CCF的设计思路非常优雅。它把复杂的时钟管理抽象成树形结构,让驱动开发者只需要关注自己的那一亩三分地。理解了这些核心数据结构,后面再看具体的时钟驱动实现,就会轻松很多。
个人建议:刚开始接触CCF时,别急着看代码。先在开发板上跑起来,用debugfs看看时钟树长什么样。有了直观感受,再回头啃源码,效果会好很多。