嵌入式Linux时钟驱动开发:CCF框架从入门到调试实战
做嵌入式Linux驱动开发,时钟是个绕不开的老大难。头两年我写外设驱动,遇到UART乱码、eMMC速率异常、网卡丢包这类问题,第一反应是反复核对寄存器配置,折腾半天,最后发现全栽在时钟上。后来把Linux内核的CCF(Common Clock Framework,通用时钟框架)从头啃了一遍,才算把这个坑填上。这篇文章就把我自己实际做嵌入式Linux时钟驱动开发的经验整理出来,从框架原理到设备树描述,从最基础的固定时钟到复合时钟驱动,再到调试排障,一路讲透。
1. 项目背景:为什么非要拆这个CCF框架
1.1 一个让整块板子挂掉的时钟问题
先讲一个真实案例。之前在一块ARM SoC板卡上调SDIO WiFi模组,模块怎么都识别不到,dmesg里反复报
mmc0: error -110
。查寄存器、换电源、看中断,折腾了整整两天,最后用示波器量SDIO_CLK引脚,发现时钟频率是27MHz而不是期望的50MHz。顺着SoC的时钟树一查,才发现WiFi模组的时钟源被另一个驱动在初始化时改了配置,导致SDIO控制器的rate被降了下来。
这种问题在嵌入式Linux开发里非常典型。硬件上,一颗SoC通常有几十甚至上百个时钟源,它们像树一样层层分布:晶振作为根节点,经过PLL倍频、分频器分频、选择器选源、门控开关,最终喂给各个外设。芯片手册的时钟树图画得密密麻麻,但驱动代码里要怎么管理这套东西,早期内核并没有统一答案,各大芯片厂商各自为政,驱动里面到处都是
clk_enable
、
writel
操作寄存器之类的散装代码,复用性极差,配错一次就是一个不稳定板子。
1.2 CCF到底解决了什么问题
CCF就是为解决这个乱局被合入内核主线的。它给"时钟"这个概念做了标准化抽象:不管底层是PLL、MUX选择器、DIV分频器,还是GATE门控,对外都统一成
struct clk
对象,驱动只需要调用
clk_get
、
clk_prepare_enable
、
clk_set_rate
这些标准API,完全不用关心这个时钟是哪个寄存器控制的、频率是怎么算出来的。
这套框架的核心价值在于"树"。它会自动维护时钟之间的父子关系,当你在任意节点改变频率时,父时钟和子时钟会联动重算;当父级被关闭时,框架会判定哪些子节点不该继续工作。换句话说,CCF像是给整个芯片的时钟网络做了一个统一调度中心,驱动只管提需求,具体由框架去完成。
1.3 什么时候需要自己写时钟驱动,什么时候不需要
很多人一提到时钟驱动就头大,但其实不是所有场景都需要你去写provider侧的代码。我的经验是把需求分三类:
-
第一类是固定频率时钟,比如板子上的24MHz晶振、32.768KHz RTC时钟。这类直接在设备树里用
fixed-clock描述即可,一个节点搞定,完全不需要C代码。 - 第二类是通用结构的时钟,比如一个寄存器里同时有mux位、divider位、gate位,且都是最标准的行为。这种可以直接使用CCF提供的helper函数注册,几十行代码就能跑通。
-
第三类是真正需要写自定义驱动的场景:硬件上有特殊的分频算法(非2的幂次)、时钟切换时需要关中断、多路时钟需要同步联动、或者芯片厂家有私有的锁相环配置流程。这时候才需要
struct clk_hw和struct clk_ops上场。
换句话说,CCF框架的一个核心设计目标就是"把简单的时钟做简单,把复杂的时钟做可控"。这篇文章后面给的例子,会从第二类切入,再延伸到第三类。
2. CCF框架的核心设计拆解
2.1 三个关键数据结构:clk、clk_hw、clk_ops
理解CCF一定要先分清三个数据结构。
struct clk
是给消费者用的"句柄",驱动调用
clk_get
拿到它,然后通过它调用各种接口。
struct clk_hw
是给provider用的"硬件对象",它代表一个具体的时钟硬件,内部保存了名字、父时钟、操作集等信息。
struct clk_ops
则是一组函数指针,定义了时钟支持的所有操作。
用大白话打个比方:
struct clk
像是你要打的一个电话号码,
struct clk_hw
是电话那头的设备,
struct clk_ops
就是设备上那些按键的功能说明。
clk_ops
里的关键回调函数需要重点掌握,我整理成一张常用表:
| 回调函数 | 作用 | 注意点 |
|---|---|---|
prepare
/
unprepare
| 可能睡眠的准备工作,如PLL锁定、切换父时钟 | 不能在原子上下文调用 |
enable
/
disable
| 原子操作的开/关,如写gate位 | 在中断上下文安全 |
is_enabled
| 查询当前是否使能 | 实现简单时建议补上,方便调试 |
recalc_rate
| 根据父时钟和寄存器值重新计算当前频率 | 返回0通常代表实现有问题 |
round_rate
| 上报硬件上最接近请求频率的可用频率 | 只计算不打硬件,用于策略决策 |
set_rate
| 实际配置分频/倍频寄存器 | 内部会先触发父级rate重算 |
set_parent
/
get_parent
| 配置/读取时钟源选择 | 多父时钟必实现 |
我早期写自定义时钟驱动时踩过一个坑:只实现了
enable/disable
,没有实现
recalc_rate
,结果
clk_get_rate
返回0,外设拿到0频率直接罢工。所以只要不是纯门控,
recalc_rate
一定要写对。
2.2 时钟树是怎么串起来的:parent与child的关系
CCF真正精彩的不是单个时钟的抽象,而是它对"树"的维护。每个
struct clk_hw
内部都有一个parent指针,指向他的父时钟。时钟注册时通过
parent_names
、
parent_hws
或
parent_data
三种方式之一来建立指向。
从软件角度看,整个系统就是一棵大的时钟树。根节点通常是晶振或内部振荡器,中间经过PLL、MUX、DIV,最终到达外设。当你调用
clk_set_rate(consumer_clk, 96000000)
时,框架会沿着这条链一路向上找到能改变频率的PLL或DIV,计算出一个可行的配置方案,然后从上层往下逐级应用。
实际开发中最常用的写法是在设备树里用
clocks
属性挂在父节点上,驱动解析时通过
clk_get
拿到父时钟句柄。字符串
parent_names
是老内核的习惯,新代码更推荐用
parent_hws
或设备树方式,可以避免命名冲突。这一点在排查"时钟匹配错乱"问题时尤其重要,我见过多个平台因为臆造了parent name,导致明明挂对了PLL却被框架当成另一个时钟。
2.3 prepare/unprepare与enable/disable分离,为什么这么设计
很多新手不理解为什么CCF要把"打开时钟"拆成两个接口。其实是因为内核里不同上下文的约束不一样。
prepare_unprepare
可以睡大觉,适合做耗时操作,比如PLL锁定需要几十微秒、切换父时钟要等稳定点。而
enable/disable
要求原子化,可以在中断处理函数、自旋锁临界区里安全调用。
实际的使用惯例是:驱动一般用
clk_prepare_enable()
一步完成,底层框架先调用prepare再调用enable;关闭时用
clk_disable_unprepare()
,先disable再unprepare。但如果你在中断上下文,就必须只调用
clk_enable
和
clk_disable
,前提是对方驱动已经把prepare做过了。
这里有一个非常实用的经验:查"时钟没输出"的问题时,可以先用
clk_enable
试试看,如果连enable都失败,多半是prepare阶段的父级没准备好,或者时钟树某个节点被标记为不可用。
2.4 时钟频率计算与修改的完整路径
频率计算是CCF里最容易出bug的部分。
clk_set_rate
并不是简单的写一个分频系数,而是一套"向上协商"的过程:框架把请求频率传给最底层的子时钟,子时钟调用
round_rate
向上(父时钟方向)询问"你能不能提供某个频率",父时钟若有可配置PLL/DIV也会参与计算,最终确定一整套配置方案。
recalc_rate
则刚好相反,它根据当前寄存器状态和父时钟频率算出"当前实际频率",系统启动时用它来初始化整棵树的频率视图。这两个函数一个向上、一个向下,配合
set_rate
、
set_parent
,构成了CCF的频率管理闭环。
顺着这个逻辑,你就明白调试为什么难:如果你只改了某个
divider
寄存器,但没有让框架知道,
clk_get_rate
返回的还是旧值,下游外设若依赖该值做波特率/时序参数计算,就会直接出错。所以,每次手动操作硬件寄存器后,一定要调用
clk_hw_recalc_rate
或让框架走一遍
clk_set_rate
流程,保持软件模型和硬件状态一致。
3. 时钟驱动开发的完整实操
3.1 设备树里的时钟描述方式
时钟驱动开发,第一步往往不是写C代码,而是搭设备树。设备树承担了"拓扑描述"的工作,它告诉内核这个芯片上有哪些时钟节点、每个节点挂在哪个父时钟下。
#clock-cells
属性很关键:为0表示该节点只输出一路时钟;为1表示输出多路,需要用索引区分。
一个典型的时钟产生器节点长这样:
clk_audio: clk-audio {
compatible = "myvendor,audio-clock";
reg = <0x1000 0x4>;
clocks = <&osc24m>, <&pll_audio>;
clock-names = "osc", "pll";
#clock-cells = <0>;
mux-shift = <8>;
mux-width = <1>;
div-shift = <0>;
div-width = <6>;
gate-shift = <12>;
};
消费者侧则这样引用:
sound_i2s: i2s@2000 {
compatible = "myvendor,i2s";
reg = <0x2000 0x100>;
clocks = <&clk_audio>;
clock-names = "aud";
assigned-clocks = <&clk_audio>;
assigned-clock-parents = <&pll_audio>;
assigned-clock-rates = <96000000>;
};
assigned-clocks
这一组属性值得单独拿出来说。它在时钟provider注册完成后会立即生效,用来设置初始parent和频率。很多驱动开发者在设备树里写了
clock-frequency
却发现不生效,就是因为没有在对应节点或者消费者节点里用
assigned-clock-rates
做显式声明。
3.2 不需要写代码的固定时钟配置
做板级适配时,最常面对的就是固定频率晶振和倍频分频。这两个用设备树就能搞定,不需要写驱动。
osc24m: osc24m {
compatible = "fixed-clock";
clock-frequency = <24000000>;
#clock-cells = <0>;
};
clk_uart: clk-uart {
compatible = "fixed-factor-clock";
clocks = <&osc24m>;
clock-div = <2>;
clock-mult = <3>;
#clock-cells = <0>;
};
这个例子中,
clk_uart
的父时钟是24MHz,经过
clock-div / clock-mult
后输出36MHz。
fixed-factor-clock
在不需要PLL的普通外设场景里非常方便,比如把24MHz转成48MHz给USB,或转成32.768KHz给RTC。
不过要提醒一句:
fixed-factor-clock
输出的频率只代表"名义上的频率",如果父时钟实际频率变了,它并不会自动跟着变(有些内核版本也不会触发重算)。所以在做动态调频、低功耗切换的系统里,不要过度依赖它,最好明确使用可配置divider。
3.3 从零到一:注册一个门控时钟驱动
最典型的自研时钟驱动,是给某个外设做一个门控时钟。硬件上可能就是一个寄存器里的一个bit:写1打开时钟,写0关闭时钟。这种场景完全可以直接用框架的helper,不需要自己实现
struct clk_ops
里的enable/disable。
#include <linux/clk-provider.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>
static int my_gate_clk_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
void __iomem *base;
u32 gate_bit;
struct clk_hw *hw;
base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base))
return PTR_ERR(base);
if (of_property_read_u32(dev->of_node, "gate-bit", &gate_bit))
gate_bit = 0;
hw = clk_hw_register_gate(dev, dev_name(dev),
of_clk_get_parent_name(dev->of_node, 0),
0, base, gate_bit, 0, NULL);
if (IS_ERR(hw))
return PTR_ERR(hw);
return devm_of_clk_add_hw_provider(dev, of_clk_hw_simple_get, hw);
}
static const struct of_device_id my_gate_clk_match[] = {
{ .compatible = "myvendor,gated-clock" },
{ }
};
MODULE_DEVICE_TABLE(of, my_gate_clk_match);
static struct platform_driver my_gate_clk_driver = {
.probe = my_gate_clk_probe,
.driver = {
.name = "my-gate-clk",
.of_match_table = my_gate_clk_match,
},
};
module_platform_driver(my_gate_clk_driver);
clk_hw_register_gate
封装了
struct clk_ops
的enable/disable/is_enabled,你只需要提供寄存器地址和bit位置。
devm_of_clk_add_hw_provider
注册一个provider给设备树使用,配套的
of_clk_hw_simple_get
是标准getter函数,负责把设备树引用映射到
struct clk_hw
。
这里有个技巧:
clk_hw_register_gate
和
devm_clk_hw_register_gate
在不同内核版本可用性有差异,老内核(4.x早期)可能没有devm版本。如果你目标内核比较老,用
clk_hw_register_gate
后记得在
remove
回调里手动
clk_hw_unregister
。我总是建议直接去看手边内核源码里的
clk-provider.h
,以那个为准,光靠背函数名很容易翻车。
3.4 进阶:带divider/mux的组合时钟实现
门控时钟只是开胃菜,真实SoC里一个外设时钟往往是一个mux(选源)加一个divider(分频)再加一个gate(门控)的组合。CCF的
clk_register_composite
就是专门干这个的,它把三个子时钟打包成一个。
以一个音频外设时钟为例:设备树里配置了两路父时钟,
osc24m
和
pll_audio
,寄存器里bit[8]选源,bit[5:0]分频,bit[12]门控。probe代码可以这样组织:
static int audio_clk_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
void __iomem *base;
const char **parent_names;
struct clk *mux_clk, *div_clk, *gate_clk;
struct clk_hw *audio_hw;
int nparents, i;
base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base))
return PTR_ERR(base);
nparents = of_clk_get_parent_count(dev->of_node);
if (nparents < 1)
return -EINVAL;
parent_names = devm_kcalloc(dev, nparents, sizeof(char *), GFP_KERNEL);
if (!parent_names)
return -ENOMEM;
for (i = 0; i < nparents; i++) {
parent_names[i] = of_clk_get_parent_name(dev->of_node, i);
if (!parent_names[i])
return -EINVAL;
}
mux_clk = clk_register_mux(dev, "audio_mux", parent_names, nparents,
0, base, 8, 1, 0, NULL);
if (IS_ERR(mux_clk))
return PTR_ERR(mux_clk);
div_clk = clk_register_divider(dev, "audio_div", "audio_mux", 0,
base, 0, 6, 0, NULL);
if (IS_ERR(div_clk))
return PTR_ERR(div_clk);
gate_clk = clk_register_gate(dev, "audio_gate", "audio_div", 0,
base, 12, 0, NULL);
if (IS_ERR(gate_clk))
return PTR_ERR(gate_clk);
audio_hw = clk_register_composite(dev, dev_name(dev),
parent_names, nparents,
__clk_get_hw(mux_clk), &clk_mux_ops,
__clk_get_hw(div_clk), &clk_divider_ops,
__clk_get_hw(gate_clk), &clk_gate_ops,
0);
if (IS_ERR(audio_hw))
return PTR_ERR(audio_hw);
return devm_of_clk_add_hw_provider(dev, of_clk_hw_simple_get, audio_hw);
}
这些helper函数是CCF为通用时钟硬件准备的"积木":
clk_register_mux
管选源,
clk_register_divider
管分频,
clk_register_gate
管门控。关键在于
clk_register_composite
的第二个参数组,它把三个子hw对象和自己的底层ops打包成对外一个时钟节点。外设驱动看到的就只是一个干净的
clk
,内部怎么选源分频完全透明。
如果你遇到的是多路父时钟之间需要切换后等稳定再关旧源、分频系数不是简单的2的幂次、或者分频器需要写入校准序列这类狗屁硬件,helper就顶不住了。这时候就得手动实现
struct clk_ops
,把
set_parent
、
round_rate
、
set_rate
等回调自己写清楚,注册方式仍然参考上面的流程,只是把
audio_hw
换成
devm_clk_hw_register(dev, &你的_hw)
。
3.5 消费者侧如何在驱动里正确使用clk API
写完provider的时钟驱动后,普通外设驱动的用法其实很简单。以I2S音频驱动为例,标准流程是:在probe里
devm_clk_get
拿时钟,
clk_prepare_enable
打开,用
clk_get_rate
获取实际频率,最后在remove里关闭。
struct clk *audio_clk;
audio_clk = devm_clk_get(dev, "aud");
if (IS_ERR(audio_clk))
return PTR_ERR(audio_clk);
ret = clk_prepare_enable(audio_clk);
if (ret)
return ret;
rate = clk_get_rate(audio_clk);
dev_info(dev, "audio clock rate: %lu\n", rate);
/* 低功耗时关闭 */
clk_disable_unprepare(audio_clk);
比较常见的坑有两个。第一个是
devm_clk_get
返回
-EPROBE_DEFER
时,驱动一定要返回这个错误码,让内核稍后重试probe。很多新手把这当成外部错误直接返回负数,导致时钟provider注册完成后再也等不到消费者重试。第二个是
clk_prepare_enable
失败后,不要直接继续访问外设寄存器,有些SoC在无时钟状态下访问寄存器会导致总线挂死,系统直接卡死。
还有一个容易被忽略的性能点:不要在频繁调用的热路径里用
clk_get_rate
,它内部会遍历父级链做计算,开销不小。我会在驱动程序初始化时取一次频率缓存起来,只有在收到对应
notifier
事件或
clk_set_rate
操作后才刷新。
4. 调试手段与验证流程
4.1 debugfs三件套:clk_summary、clk_dump、clk_tree
写时钟驱动,不会用debugfs等于盲人摸象。内核编译时要确保打开
CONFIG_DEBUG_FS
和
CONFIG_COMMON_CLK
,然后在板子上的
/sys/kernel/debug/clk
目录下,就能看到整个时钟树的状态。
最常用的是
clk_summary
,它输出每个时钟的enable count、prepare count、rate和parent关系。我曾经靠它一眼定位了UART乱码问题:某个PLL的enable count是0,但底下的divider rate显示却还在,说明是之前的驱动在变频后把PLL关了,UART没有感知。贴一段典型的输出:
enable prepare protect duty
clock count count count rate phase
-------------------------------------------------------------------------------------------
osc24m 6 6 0 24000000 0
├── pll_audio 1 1 0 983040000 0
│ ├── audio_mux 1 1 0 983040000 0
│ │ └── audio_div 1 1 0 98304000 0
│ │ └── audio_gate 1 1 0 98304000 0
绿色框中英文部分其实不用全看懂,重点是rate那一列。如果某个中间节点的rate是0,说明recalc_rate或者上游出现断层,就顺着parent链去查。
clk_dump
会把整个树以JSON格式输出,适合脚本解析。
clk_tree
则是某些内核版本提供的可视化树状输出,读起来更直观。我个人的习惯是先
cat clk_summary
,再根据关键节点去追
clk_dump
。
提示:
clk_summary显示的enable count和prepare count是两个独立计数。enable count不为0说明底层的enable被调用了,prepare count不为0说明耗时的准备流程执行过。两者不匹配也是排查的重点,常见于该关没关或已被关两次的驱动。
4.2 通过tracepoint定位变频问题
如果问题还定位不到,就要上trace了。CCF框架在关键路径上埋了
trace_clk_enable
、
trace_clk_disable
、
trace_clk_set_rate
、
trace_clk_set_parent
等tracepoint。打开
/sys/kernel/debug/tracing
下的相关功能,就能记录每个时钟操作是谁发起的、参数是什么。
我有一棵树的PLL频率会无故漂移,就是靠trace抓到是某个音频驱动的
clk_set_rate
把PLL带偏了。那次经历让我形成习惯:系统只要出现"莫名其妙的外设异常",先把trace打开挂十分钟,再用
trace-cmd report
导出,往往比看代码猜效率高得多。
设计CCF这类框架,本身就是为了让运行时状态可见、可追踪。所以不要一上来就改代码,先看状态,再上trace,最后才动手。这是所有驱动调试的通用顺序。
4.3 一个实际的调试案例:UART波特率为何不对
分享一个当时让我印象很深的排查过程。板子启动后,UART控制台输出乱码,但用示波器看波形又是有的。排查分三步走:
第一步,看设备树。UART节点挂的时钟是
clk_uart
,
assigned-clock-rates
给的是
<1843200>
这个标准波特率基频。这里看着没有毛病。第二步,看
clk_summary
。发现输出路径里多了一个中间层,
clk_uart
下面还有
clk_uart_mux
和
clk_uart_div
,而
clk_uart_mux
实际选择的父PLL是
pll_sys
,频率只有720MHz,分频后得到的是1728000Hz,明显不是目标值。第三步,查
assigned-clock-parents
。设备树里忘记指定UART的父时钟,而寄存器默认上电值是0,默认选了
pll_sys
。补上
assigned-clock-parents = <&pll_uart>
,并把PLL频率调到
22118400
后,乱码问题彻底消失。
这个案例的教训是:CPU里的时钟树“看似默认可用”,实际上默认寄存器和你的期望往往差得很远。新板子bring-up阶段,一定要先梳理芯片手册里的上电默认时钟配置,再决定设备树怎么写,不然就会被各种看不见的默认值坑掉。
5. 常见问题排查与避坑实录
5.1 probe失败、clk_get失败怎么办
驱动开发中,
clk_get
失败是非常高频的错误。常见返回码无非三种:
-ENOENT
表示设备树里找不到对应时钟;
-EINVAL
表示
clock-names
写错了;
-EPROBE_DEFER
表示provider还没准备好。
处理
EPROBE_DEFER
的正确姿势是原样向上return,让内核framework在provider注册完成后重新调用probe。我看到很多团队会在这里加日志、加延迟,甚至直接手动
msleep(100)
再重试,这些都是馊主意,既破坏内核时序又掩盖真实问题。如果drivers/base的deferred_probe机制正常,你根本不需要操心重试节奏。
排查
clk_get
失败时,我一般是先打开设备树编译器的实时检查,确认
clocks
和
clock-names
两两匹配,再去看
clk_summary
里provider的输出名是否和
clock-output-names
对得上。
5.2 rate和parent对不上
另一大类问题是"软件里的rate"和"硬件实际输出的rate"不一致。之前讲过一个很典型的情况:只手动改了寄存器,没让框架重新
recalc_rate
。但还有一种更隐蔽的场景:某个父时钟被
assigned-clocks
设置成了某频率,框架会沿着树自动传播,可中间某个节点的
round_rate
实现得很随意,总是返回一个"近似值"。于是你听到的是96MHz,实际硬件给的是96.5MHz,UART和音频还好,网卡这种对频率敏感的模块就会疯狂丢包。
我的排查习惯是优先在
debugfs
里观察rate,再拿示波器实测。两边的数字差个千分位,问题多半在
round_rate
和
recalc_rate
没有严格按照硬件分频关系实现,只有把这两个函数的数学逻辑和芯片手册对齐,才能根治。
5.3 enable后外设仍不工作
这种情况最让人头大:设备树对,
clk_get
成功,
clk_prepare_enable
没报错,但外设就是一个字节都不吐。除了最基础的电源和中断检查之外,常见原因是"多路时钟漏了"。很多SoC外设不止一个时钟,比如I2S外设可能同时需要总线时钟、音频主时钟、DMA时钟三路。
clk_summary
里一路是1、另外两路是0,你只看最重要的那一路就漏了。
顺便说一句,
CLK_IGNORE_UNUSED
这个flag值得留意。有些SoC厂商为了硬件跳线方便,给关键UART、调试口时钟加了这个flag,让内核在裁剪时钟时跳过它们。调试初期用一用很省心,但产品定型前要记得去掉,不然漏电和睡眠唤醒问题会接踵而来。
5.4 平台差异与时钟框架的演进
最后说一点关于兼容性的事情。不同SoC厂商对CCF的使用方式差异很大。只见过瑞萨、NXP i.MX、Rockchip这些主流平台的人,往往会形成"这个框架就这么点东西"的错觉,真换到新平台还是会被私有扩展卡一下。而高通的
clk_rcg
、ChipIdea的
hw_clk
等,都是基于CCF但不完全等价的实现。我的建议是:拿到一款新SoC的BSP驱动,先分类整理它的
clk_ops
实现,尤其要注意它怎么处理
round_rate
、
set_rate
之间的交互,以及是否有私有flags。
内核版本演进也在改变CCF的使用方式。老内核里常见的
clk_register()
已经被
clk_hw_register()
取代,新版更强调
struct clk_hw
为中心。新版本还逐步收紧了
clk_set_parent
等API的并发保护,多个核心同时调时钟管理的场景越来越多,所以直接照抄网上老例子的风险也在变大。我遇到代码编译不过时,第一件事是翻手边内核源码的
include/linux/clk-provider.h
和
drivers/clk/clk.c
,而不是搜索引擎,这个习惯帮我避免了很多过时API的坑。
写时钟驱动这三年,我真的觉得CCF是那种“框架搭得好,能帮全系统排雷”的设计。只要你愿意先花时间搞清楚
clk_hw
、
clk_ops
和树的关系,后面所有外设驱动的时钟操作都会变得有章可循。我现在的习惯是拿到一块新板子,第一件事就是把完整时钟树dump出来,把每个外设挂在哪个分支上画一遍,确认无悬念再动代码。驱动碰到任何灵异现象,也先回debugfs确认时钟状态,这一招帮我省下了无数个加班的夜晚。希望这篇能让你少走点弯路,自己去板子上跑一跑
clk_summary
,很多之前觉得玄学的问题,真的会瞬间通透。
更多推荐


所有评论(0)