1. 从设备树到驱动代码:时钟使用者API到底在解决什么问题
很多刚接触Linux内核驱动开发的朋友,第一次看到clk_get、clk_prepare_enable这些函数时,脑子里冒出的第一个问题往往是:我直接往寄存器里写值把时钟打开不就行了吗,为什么还要绕一层框架?这个问题问得特别好,因为它恰恰点出了通用时钟框架存在的意义。
在早期的ARM Linux内核里,时钟管理确实是一团乱麻。每个SoC厂商都有自己的时钟控制寄存器布局,有的时钟门控位在寄存器偏移0x10,有的在0x24,有的需要先解锁再写,有的还涉及分频器和锁相环的联动配置。驱动开发者每换一个平台就得重新翻一遍芯片手册,把那些魔数硬编码到驱动里。这种做法带来的后果就是驱动代码完全不可移植,同一个外设驱动在A平台上能跑,换到B平台就得大改。
通用时钟框架(Common Clock Framework,简称CCF)的引入就是为了解决这个碎片化问题。它把时钟的抽象分成了两个世界:一个是时钟提供者(clock provider),也就是SoC时钟控制器驱动,负责描述和管理硬件时钟树;另一个是时钟使用者(clock consumer),也就是各种外设驱动,它们只需要通过一套统一的API来获取和操作时钟,完全不需要关心底层寄存器长什么样。
这篇文章聚焦的是后者——时钟使用者API。我会把clk_get到clk_disable_unprepare这条完整链路拆开讲清楚,包括设备树里怎么描述时钟、驱动代码里怎么获取、使能顺序为什么不能乱、出错路径怎么处理,以及那些文档里不会写但实际调试中一定会遇到的坑。不管你是刚入门的内核驱动开发者,还是已经写过几个驱动但对接时钟时总是心里没底的老手,这篇内容应该都能帮你把这块知识补完整。
2. 设备树里的时钟描述:consumer侧的第一道关卡
2.1 clocks与clock-names的配对逻辑
时钟使用者API的起点其实不在C代码里,而在设备树。一个外设节点要使用时钟,必须在自己的节点里声明clocks属性,这个属性指向时钟提供者的phandle和对应的时钟索引。举个典型的例子:
uart0: serial@ff1a0000 { compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart"; reg = <0x0 0xff1a0000 0x0 0x100>; interrupts = <GIC_SPI 116 IRQ_TYPE_LEVEL_HIGH>; clocks = <&cru SCLK_UART0>, <&cru PCLK_UART0>; clock-names = "baudclk", "apb_pclk"; status = "okay"; };这里clocks属性里有两个时钟引用,分别对应波特率时钟和APB总线时钟。clock-names则给这两个时钟起了名字,顺序必须和clocks里一一对应。驱动代码里调用clk_get(dev, "baudclk")时,框架就是靠这个名字去匹配的。
注意:
clock-names里的名字是驱动代码里写死的,不是随便起的。你必须去查对应驱动源码里clk_get调用时用的字符串是什么,设备树里就得写什么。写错了不会报编译错误,但运行时会返回-ENOENT,而且这种错误往往在系统启动阶段就导致驱动probe失败,排查起来比较费劲。
2.2 为什么有些节点没有clock-names
你可能会发现,有些设备树节点只有clocks没有clock-names。这种情况通常出现在只需要一个时钟的外设上。当驱动调用clk_get(dev, NULL)时,框架会直接返回clocks属性里的第一个时钟。这种写法在简单外设上很常见,比如某些GPIO控制器或者简单的定时器。
但我的建议是,即使只有一个时钟,也尽量把clock-names写上。原因很简单:可读性。后来的人看你的设备树,一眼就能知道这个时钟是干什么用的,而不是去翻驱动源码猜。而且万一以后硬件改版需要加第二个时钟,有名字的写法扩展起来更自然。
2.3 时钟索引与phandle的常见误区
clocks = <&cru SCLK_UART0>这行代码里,&cru是时钟控制器的phandle,SCLK_UART0是时钟控制器驱动里定义的时钟索引宏。这个宏的值是在时钟控制器驱动的头文件里定义的,通常位于include/dt-bindings/clock/目录下。
一个常见的误区是以为SCLK_UART0这个宏的值就是寄存器的偏移地址。实际上它只是一个逻辑索引,时钟控制器驱动内部会把这个索引映射到具体的寄存器操作上。所以你在设备树里看到的数字,比如SCLK_UART0可能被定义为150,这个150跟寄存器地址没有任何直接关系,它只是时钟控制器驱动内部数组的下标。
3. clk_get到clk_put:获取与释放的完整生命周期
3.1 clk_get的两种调用方式与返回值处理
在驱动代码里获取时钟,最常用的就是clk_get。它有两个变体:
struct clk *clk_get(struct device *dev, const char *id); struct clk *devm_clk_get(struct device *dev, const char *id);clk_get是手动管理版本,获取到的时钟需要你自己在驱动卸载或出错时调用clk_put释放。devm_clk_get是设备资源管理版本,框架会在设备detach时自动释放,省去了手动清理的麻烦。
我的经验是,除非你有非常特殊的理由需要手动控制时钟的生命周期,否则一律用devm_clk_get。手动管理clk_get/clk_put的代码,十个里面有八个在出错路径上漏掉了clk_put,导致时钟引用计数泄漏。这种泄漏在系统长时间运行后可能导致时钟无法被正确关闭,功耗下不来。
返回值处理也有讲究。clk_get返回的是struct clk *指针,出错时返回的是ERR_PTR编码的错误码,不是NULL。所以正确的错误判断应该是:
clk = devm_clk_get(dev, "baudclk"); if (IS_ERR(clk)) { ret = PTR_ERR(clk); dev_err(dev, "failed to get baudclk: %d\n", ret); return ret; }用if (!clk)来判断是错误的,因为ERR_PTR(-ENOENT)不是NULL,这样写会漏掉错误。
3.2 clk_prepare与clk_enable的分工
获取到时钟之后,下一步是使能。但这里有个关键点:CCF把使能操作分成了两个阶段——clk_prepare和clk_enable。
为什么要分两步?这跟时钟的硬件特性有关。有些时钟在使能之前需要先做一些准备工作,比如配置锁相环、等待时钟稳定、设置分频比等。这些操作可能需要在可以睡眠的上下文里完成,而clk_enable被设计成可以在原子上下文里调用。所以框架把可能睡眠的操作放在clk_prepare里,把纯粹的寄存器写操作放在clk_enable里。
对于大多数外设驱动来说,你不需要分别调用这两个函数,直接用组合版本:
int clk_prepare_enable(struct clk *clk); void clk_disable_unprepare(struct clk *clk);但理解它们的分工很重要,因为有些场景下你确实需要分开调用。比如在中断处理函数里,你只能调用clk_enable,因为中断上下文不能睡眠。这时候你就需要在驱动probe时先调用clk_prepare,然后在中断里只调clk_enable。
3.3 引用计数机制与重复使能
CCF内部对每个时钟维护了一个引用计数。每次clk_prepare_enable会让计数加一,每次clk_disable_unprepare会让计数减一。只有当计数从0变到1时,硬件时钟才真正被打开;从1变到0时,才真正被关闭。
这个机制意味着重复使能是安全的,但前提是你的使能和关闭必须严格配对。我见过不少驱动在probe里使能了一次,然后在resume里又使能一次,但suspend里只关闭一次,结果时钟计数永远回不到零,系统进入低功耗模式时时钟还在跑。
提示:如果你在调试时发现某个时钟的引用计数不对,可以通过
/sys/kernel/debug/clk/clk_summary查看。这个文件会列出所有时钟的当前状态、引用计数、频率等信息,是排查时钟问题的第一手资料。
4. 时钟频率设置:clk_set_rate的边界与陷阱
4.1 clk_set_rate的调用时机
设置时钟频率用clk_set_rate:
int clk_set_rate(struct clk *clk, unsigned long rate);这个函数返回实际设置成功的频率,可能跟你请求的频率不完全一样。因为时钟树有分频器和锁相环的约束,不是所有频率都能精确产生的。比如你请求100MHz,但父时钟是24MHz,分频系数只能是整数,那实际出来的可能是96MHz或者120MHz。
调用时机上,clk_set_rate通常应该在clk_prepare_enable之前调用。虽然框架允许在使能后改频率,但有些时钟控制器在时钟运行中切换频率可能会导致输出毛刺,影响外设正常工作。所以稳妥的做法是先把频率设好,再使能时钟。
4.2 频率传播与父时钟影响
clk_set_rate的一个关键特性是频率传播。当你设置一个子时钟的频率时,框架会尝试调整父时钟的频率来满足需求。如果父时钟不能改,就尝试调整分频比。如果都不行,就返回最接近的频率。
这个传播机制有时候会带来意想不到的副作用。比如你只想改UART的波特率时钟,结果框架把整个PLL的频率都改了,导致其他依赖这个PLL的外设时钟也跟着变了。虽然框架会尽量通知受影响的时钟,但有些驱动可能没有正确处理频率变化通知,就会出现问题。
避免这个问题的办法是,在设置频率之前先了解时钟树的拓扑结构。通过/sys/kernel/debug/clk/clk_summary可以看到每个时钟的父时钟是谁,以及当前的频率。如果你发现要改的时钟和别的外设共享父时钟,就要考虑是否需要在设备树里给这个时钟单独指定一个PLL。
4.3 clk_round_rate的预检作用
在正式设置频率之前,可以用clk_round_rate来预检:
long clk_round_rate(struct clk *clk, unsigned long rate);它会返回在不实际改变硬件的情况下,最接近请求频率的可实现频率。这个函数在需要精确频率的场景下特别有用,比如音频驱动需要精确的采样率时钟,你可以先用clk_round_rate确认能否达到,再决定是否继续。
5. 设备树与驱动代码的对接实战
5.1 一个完整的consumer驱动片段
下面是一个简化的UART驱动片段,展示了从设备树获取时钟到使能的完整流程:
static int my_uart_probe(struct platform_device *pdev) { struct my_uart *uart; int ret; uart = devm_kzalloc(&pdev->dev, sizeof(*uart), GFP_KERNEL); if (!uart) return -ENOMEM; uart->baudclk = devm_clk_get(&pdev->dev, "baudclk"); if (IS_ERR(uart->baudclk)) { ret = PTR_ERR(uart->baudclk); dev_err(&pdev->dev, "failed to get baudclk: %d\n", ret); return ret; } uart->pclk = devm_clk_get(&pdev->dev, "apb_pclk"); if (IS_ERR(uart->pclk)) { ret = PTR_ERR(uart->pclk); dev_err(&pdev->dev, "failed to get apb_pclk: %d\n", ret); return ret; } ret = clk_set_rate(uart->baudclk, 115200 * 16); if (ret < 0) { dev_err(&pdev->dev, "failed to set baudclk rate: %d\n", ret); return ret; } ret = clk_prepare_enable(uart->pclk); if (ret) { dev_err(&pdev->dev, "failed to enable pclk: %d\n", ret); return ret; } ret = clk_prepare_enable(uart->baudclk); if (ret) { dev_err(&pdev->dev, "failed to enable baudclk: %d\n", ret); clk_disable_unprepare(uart->pclk); return ret; } platform_set_drvdata(pdev, uart); return 0; }这段代码里有几个值得注意的细节。第一,两个时钟的获取顺序和使能顺序是分开的。获取时先baudclk后pclk,使能时先pclk后baudclk。为什么?因为APB总线时钟是寄存器访问的前提,如果pclk没开,你连UART的寄存器都读写不了,更别说配置波特率了。所以使能顺序必须是先总线时钟后功能时钟。
第二,出错处理里的回滚。如果baudclk使能失败,要记得把已经使能的pclk关掉。这种嵌套的错误处理在时钟多的驱动里很容易漏,漏掉的结果就是时钟泄漏。
5.2 时钟获取失败时的排查链路
当你看到驱动probe失败,日志里打印failed to get baudclk: -2(-ENOENT),排查思路应该是这样的:
第一步,确认设备树里clock-names属性的拼写。baudclk和baud_clk是两个不同的字符串,框架不会帮你做模糊匹配。
第二步,确认clocks属性的phandle指向是否正确。如果phandle指向了一个不存在的节点,或者指向的节点没有被时钟控制器驱动probe,clk_get也会失败。
第三步,确认时钟控制器驱动是否已经probe成功。如果时钟控制器本身的驱动还没加载,它提供的时钟当然也获取不到。这种情况通常发生在驱动加载顺序不对的时候,可以通过调整设备树里的节点顺序或者使用EPROBE_DEFER机制来解决。
第四步,检查时钟索引宏是否匹配。SCLK_UART0这个宏的值必须和时钟控制器驱动里定义的索引一致。如果设备树头文件和驱动源码里的定义不同步,就会取到错误的时钟或者直接失败。
5.3 使用debugfs验证时钟状态
/sys/kernel/debug/clk/clk_summary是调试时钟问题的利器。它输出的格式大概是这样的:
clock enable_cnt prepare_cnt rate accuracy phase ---------------------------------------------------------------------------------------- clk_uart0_baud 1 1 24000000 0 0 sclk_uart0 1 1 24000000 0 0 clk_pll_24m 1 1 24000000 0 0enable_cnt和prepare_cnt分别对应clk_enable和clk_prepare的引用计数。如果你发现某个时钟的计数不对,比如驱动已经卸载了但计数还是1,那就说明有地方漏了clk_disable_unprepare。
这个文件还能看到时钟树的层级关系。缩进层级表示父子关系,子时钟缩进在父时钟下面。通过这个层级关系,你可以快速定位频率传播的路径。
6. 那些文档里不会写的踩坑经验
6.1 时钟使能顺序反了导致寄存器访问异常
这是我实际项目中遇到过的一个问题。一个SPI控制器驱动,在probe里先使能了SPI功能时钟,然后才使能APB总线时钟。结果在配置SPI寄存器时,读回来的值全是0。一开始以为是SPI控制器坏了,后来查了半天才发现是总线时钟没开,寄存器写入根本没生效。
这个问题的隐蔽性在于,它不会导致内核崩溃,也不会打印任何错误。你只是发现寄存器读写不正常,但很难联想到是时钟顺序的问题。所以记住一个原则:先使能总线时钟,再使能功能时钟;先关闭功能时钟,再关闭总线时钟。
6.2 clk_disable_unprepare在原子上下文的限制
clk_disable_unprepare这个组合函数内部会调用clk_unprepare,而clk_unprepare是可能睡眠的。所以你不能在中断上下文或者持有自旋锁的情况下调用它。如果你确实需要在原子上下文里关闭时钟,只能用clk_disable,但前提是你之前已经单独调用过clk_prepare。
这个限制在编写中断处理程序时特别容易踩。比如你在中断里关闭一个时钟来省电,用了clk_disable_unprepare,结果内核报scheduling while atomic的警告。正确的做法是在probe里clk_prepare,在中断里只clk_disable,在remove里再clk_unprepare。
6.3 设备树时钟引用变更后的兼容性处理
硬件改版时,时钟控制器的phandle或者时钟索引可能会变。如果你的驱动同时支持新旧两版硬件,设备树里就得做兼容处理。常见的做法是在驱动里尝试获取多个时钟名字,哪个成功用哪个:
clk = devm_clk_get(dev, "baudclk"); if (IS_ERR(clk)) { clk = devm_clk_get(dev, "uart_clk"); if (IS_ERR(clk)) return PTR_ERR(clk); }但这种做法会让代码变得比较啰嗦。更好的方式是通过of_device_id的data字段来区分硬件版本,不同版本走不同的时钟获取路径。
6.4 时钟频率变更对已配置外设的影响
有些外设的配置依赖于时钟频率。比如UART的波特率分频器是根据输入时钟频率计算出来的。如果你在运行时改了UART的时钟频率,但没有重新配置波特率分频器,通信就会出错。
这个问题在动态调频(DVFS)场景下特别常见。系统根据负载调整CPU频率时,可能会影响到共享同一个PLL的外设时钟。所以外设驱动应该注册clk_notifier来监听时钟频率变化,在频率改变时重新配置外设。
static int my_uart_clk_notifier(struct notifier_block *nb, unsigned long event, void *data) { struct clk_notifier_data *cnd = data; if (event == PRE_RATE_CHANGE) { /* 保存当前配置 */ } else if (event == POST_RATE_CHANGE) { /* 根据新频率重新配置波特率 */ } return NOTIFY_OK; }这个notifier的注册和注销也需要在probe和remove里配对处理,否则驱动卸载后notifier还在,会导致use-after-free。
6.5 时钟gating与省电的平衡
在低功耗场景下,时钟gating是最有效的省电手段之一。但过度gating也会带来问题。比如你在一个外设空闲时关闭了它的时钟,但外设的某些状态可能依赖于时钟来维持。下次使能时钟后,外设可能处于一个不确定的状态,需要重新初始化。
我的经验是,对于需要保持状态的设备,比如DMA控制器、显示控制器,不要轻易在运行时关闭时钟。可以在系统进入suspend时统一关闭,resume时重新初始化。对于简单的数据传输外设,比如UART、SPI,可以在传输完成后关闭时钟,下次传输前重新使能并配置。
7. 从consumer API看时钟框架的设计哲学
7.1 抽象层级的取舍
CCF的consumer API设计体现了一个经典的抽象取舍:把复杂性留给provider,把简单性留给consumer。时钟控制器驱动需要处理各种硬件细节——PLL锁定、分频器切换、时钟门控、glitch-free切换等,而外设驱动只需要调用几个简单的API。
这种设计的好处是外设驱动的可移植性大大提高。同一个UART驱动,只要设备树里描述正确,可以在Rockchip、Allwinner、NXP等不同平台上运行,驱动代码一行都不用改。
但代价是provider驱动的复杂性增加了。时钟控制器驱动需要实现struct clk_ops里定义的一系列回调,包括prepare、unprepare、enable、disable、recalc_rate、set_rate、round_rate等。这些回调的实现质量直接影响到整个时钟框架的稳定性。
7.2 引用计数与资源管理的权衡
CCF选择用引用计数来管理时钟的使能状态,而不是简单的开关。这个选择的好处是支持多个consumer共享同一个时钟。比如多个外设可能共用同一个PLL,每个外设都调用clk_prepare_enable,引用计数累加,只有当所有外设都关闭时钟后,PLL才真正关闭。
但引用计数也带来了管理上的复杂性。consumer必须严格保证使能和关闭的配对,否则计数就会泄漏。devm_clk_get的引入在一定程度上缓解了这个问题,因为它把时钟的释放和设备的生命周期绑定在一起,减少了手动管理的负担。
7.3 设备树作为硬件描述的载体
CCF与设备树的结合是Linux内核硬件描述方式的一个典型代表。设备树负责描述"有什么时钟"和"谁用哪个时钟",驱动代码负责描述"怎么用时钟"。这种职责分离让硬件描述和驱动逻辑解耦,同一份驱动可以适配不同的硬件配置。
但这种分离也带来了新的挑战。设备树里的错误不会在编译时被发现,只能在运行时暴露。而且设备树的调试手段相对有限,不像代码可以用printk和断点。所以写好设备树需要对时钟框架和具体硬件都有深入的理解。
8. 几个值得记住的实操要点
关于时钟使用者API,最后再分享几个我在实际项目中总结的要点,都是踩过坑之后才记住的。
第一,devm_clk_get的id参数不要传NULL,除非你确定设备树里只有一个时钟。传NULL时框架取第一个时钟,如果设备树里时钟顺序变了,你的驱动就会取到错误的时钟。显式指定名字更安全。
第二,clk_prepare_enable的返回值一定要检查。虽然大多数情况下它返回0,但在时钟控制器驱动有问题或者硬件异常时,它可能返回错误。忽略返回值可能导致后续的寄存器操作在时钟未使能的情况下进行。
第三,在suspend/resume回调里处理时钟时,注意resume路径上的错误处理。如果resume时时钟使能失败,要确保系统能正常恢复或者至少不会崩溃。有些驱动在resume里直接调用clk_prepare_enable而不检查返回值,结果时钟没开,后续访问寄存器就挂了。
第四,调试时钟问题时,clk_summary和clk_dump是两个最常用的debugfs节点。clk_summary给出概览,clk_dump给出更详细的树形结构。结合dmesg里的时钟相关日志,大部分问题都能定位。
第五,如果你在写一个时钟控制器驱动,记得在clk_ops里实现is_enabled和is_prepared回调。这两个回调虽然可选,但实现了之后debugfs才能正确显示时钟状态,对调试帮助很大。
时钟框架这块内容,刚接触时觉得API不多应该不难,但真正用起来才发现细节很多。每个API的调用时机、错误处理、上下文限制都有讲究。希望这篇内容能帮你把这些细节串起来,在实际开发中少走一些弯路。