1. 从一次驱动调试说起:为什么通用时钟框架值得花时间啃
很多做嵌入式Linux驱动的朋友,第一次接触clk相关代码,大概率是在改某个外设驱动的时候。比如调一个I2S音频接口,发现采样率死活对不上,最后定位到是某个时钟分频系数没配对;又或者调一个SPI屏,刷图总是花屏,查了半天发现是SPI时钟频率超出了屏幕手册的上限。这类问题的共同点是:问题不在你写的业务逻辑里,而在时钟树上。
Linux的通用时钟框架(Common Clock Framework,简称CCF)就是干这件事的:它把SoC内部错综复杂的时钟树抽象成一套统一的模型,让驱动开发者不用去直接操作寄存器,而是通过一组标准API来申请、配置、开关时钟。这套框架从2012年前后进入主线内核,到现在已经是所有主流SoC(瑞芯微RK3568、全志、NXP i.MX、TI等)的标准配置。你打开任何一个现代SoC的设备树,几乎都能看到clocks、clock-names、assigned-clocks这些属性。
但问题在于,CCF的文档分散在内核源码的Documentation/driver-api/clk.rst、include/linux/clk.h以及各个provider驱动的实现里,初学者很容易陷入"知道有这些API,但不知道什么时候该用哪个"的困境。更麻烦的是,clk_prepare_enable和clk_enable到底有什么区别?clk_get和devm_clk_get该选哪个?clk_set_rate调用之后为什么实际频率没变?这些问题在官方文档里往往只有一句话,真正的答案藏在实现细节和使用场景里。
这篇内容就是围绕时钟使用者(clock consumer)API展开的。所谓"使用者",就是那些需要用到时钟的外设驱动,比如UART、I2C、SPI、LCD控制器、音频编解码器等等。我会把常用的API按使用场景分类讲清楚,配上实际调试中踩过的坑,以及设备树里怎么描述时钟关系。目标读者是已经能写基本字符设备驱动、但对时钟框架还比较模糊的嵌入式工程师。看完之后,你应该能独立完成一个外设驱动的时钟配置,并且在时钟出问题时知道从哪里下手排查。
2. 时钟使用者API的全景地图:先搞清楚你手里有哪些牌
2.1 获取时钟:clk_get、devm_clk_get与of_clk_get的区别
在驱动里操作时钟的第一步,是拿到一个struct clk *句柄。这个句柄代表时钟树上某个具体的时钟节点。获取句柄的API有好几个,用哪个取决于你的驱动模型和上下文。
最传统的是clk_get:
struct clk *clk_get(struct device *dev, const char *id);它通过设备指针和时钟名称(con_id)来查找时钟。在设备树时代,这个id对应的是设备树节点里clock-names属性中的字符串。比如:
uart2: serial@ff1a0000 { clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>; clock-names = "baudclk", "apb_pclk"; };驱动里就可以用clk_get(&pdev->dev, "baudclk")来获取第一个时钟。如果id传NULL,则会获取clocks属性里的第一个时钟。
但clk_get有个麻烦:它不会自动释放。你必须在驱动卸载或者出错路径里手动调用clk_put,否则就会泄漏。对于现代驱动,更推荐用devm_clk_get:
struct clk *devm_clk_get(struct device *dev, const char *id);devm_前缀意味着设备资源管理(Device Resource Management),内核会在设备销毁时自动帮你clk_put。这大大简化了错误处理路径。我个人的经验是:只要你的驱动是平台驱动(platform driver)或者基于设备模型的驱动,一律用devm_clk_get。只有在极少数非设备模型场景(比如早期初始化代码)才用clk_get。
还有一个of_clk_get,它直接通过设备树节点和索引获取:
struct clk *of_clk_get(struct device_node *np, int index);这个API在provider驱动或者一些特殊场景下用得多,普通consumer驱动很少直接用。它的缺点是绕过了clock-names的语义,直接用索引,代码可读性差。除非你在写时钟控制器驱动本身,否则不建议用。
注意:
devm_clk_get在失败时返回的是ERR_PTR而不是NULL。所以判断必须用IS_ERR(),不能用if (!clk)。这个坑我见过太多次了,新手很容易写成if (clk == NULL),结果错误指针被当成有效句柄传给后续API,直接oops。
2.2 准备与使能:clk_prepare_enable和clk_enable的层次关系
拿到时钟句柄之后,下一步是让它开始工作。这里有两个层次的API:clk_prepare/clk_unprepare和clk_enable/clk_disable。很多初学者会困惑:为什么要有两套?直接一个clk_enable不就完了?
这个设计跟时钟控制器的硬件特性有关。有些时钟的使能操作可能涉及到可能睡眠的操作,比如通过I2C去配置一个外部时钟芯片,或者等待PLL锁定。这类操作不能在原子上下文(比如中断处理程序)里执行。而另一些时钟的开关只是写一个寄存器位,可以快速完成,能在原子上下文里执行。
所以CCF把时钟控制分成了两个阶段:
clk_prepare:执行可能睡眠的操作,必须在进程上下文调用,不能在中端里调用。clk_enable:执行不能睡眠的快速操作,可以在原子上下文调用。
对应的,关闭时先clk_disable再clk_unprepare。顺序不能反。
对于绝大多数consumer驱动,你不需要分别调用这两个,直接用组合API:
int clk_prepare_enable(struct clk *clk); void clk_disable_unprepare(struct clk *clk);这个组合在进程上下文里完成准备和使能,是最常用的形式。只有在中断处理程序里需要快速开关时钟时,才会单独用clk_enable/clk_disable,而且前提是这个时钟已经被clk_prepare过了。
这里有个重要的引用计数机制:clk_prepare和clk_enable都是可重入的,每次调用会增加引用计数,每次unprepare/disable会减少。只有计数降到0时,硬件才真正关闭。这意味着你可以在多个地方安全地调用clk_prepare_enable,不用担心重复使能的问题。但反过来,每次clk_prepare_enable必须对应一次clk_disable_unprepare,否则计数永远不归零,时钟就关不掉了。
2.3 频率配置:clk_set_rate的生效条件与传播机制
设置时钟频率用clk_set_rate:
int clk_set_rate(struct clk *clk, unsigned long rate);这个API看起来简单,但实际行为比想象中复杂。首先,你请求的频率不一定能被精确满足。时钟树上的分频器、倍频器有硬件限制,最终设置的频率是最接近且不超过请求值的那个可用频率。你可以用clk_round_rate先查询:
long clk_round_rate(struct clk *clk, unsigned long rate);它返回实际能设置的频率。我通常会在clk_set_rate之前先调用clk_round_rate,把结果打印出来,确认硬件是否支持我想要的频率。这在调音频采样率(比如44.1kHz和48kHz系列)时特别有用,因为音频时钟对精度要求高,差一点就会导致音调不对。
其次,clk_set_rate的生效还取决于时钟是否已经使能。有些时钟控制器在时钟关闭时设置频率会失败,或者设置后不会立即生效。稳妥的做法是:先clk_prepare_enable,再clk_set_rate。虽然后设置频率在大多数平台上也能工作,但先使能再设置是更安全的顺序。
还有一个关键点:clk_set_rate会沿着时钟树向上传播。比如你设置一个叶子时钟的频率,框架会尝试调整它的父时钟来满足要求。如果父时钟不能改,就会尝试调整分频器。这个传播过程由provider驱动的determine_rate或round_rate回调实现。如果最终无法满足,clk_set_rate可能返回成功但实际频率没变,或者返回错误。所以设置之后一定要用clk_get_rate读回来确认:
unsigned long actual = clk_get_rate(clk); pr_info("requested %lu, got %lu\n", rate, actual);这个习惯能帮你省下大量调试时间。
2.4 设备树中的时钟描述:clocks、clock-names与assigned-clocks
设备树是CCF的配置入口。一个典型的consumer节点长这样:
i2s1: i2s@ff320000 { compatible = "rockchip,rk3568-i2s", "rockchip,rk3066-i2s"; reg = <0x0 0xff320000 0x0 0x1000>; clocks = <&cru MCLK_I2S1_8CH>, <&cru HCLK_I2S1_8CH>; clock-names = "i2s_clk", "i2s_hclk"; assigned-clocks = <&cru CLK_I2S1_8CH_TX_SRC>; assigned-clock-rates = <1188000000>; assigned-clock-parents = <&cru PLL_GPLL>; };这里有几个关键属性:
clocks:列出该设备用到的所有时钟句柄,顺序很重要。clock-names:给每个时钟起名字,驱动里用这个名字来devm_clk_get。assigned-clocks:指定需要在设备初始化时自动配置的时钟。assigned-clock-rates:配合assigned-clocks,指定目标频率。assigned-clock-parents:指定时钟的父时钟。
assigned-*系列属性的好处是,内核会在设备probe之前自动帮你完成这些配置,驱动代码里就不用再写一遍。这在时钟树初始化顺序敏感的场景下特别有用。比如I2S的MCLK必须从GPLL分频得到,如果驱动自己去设,可能因为父时钟还没准备好而失败。用assigned-clock-parents让框架在合适的时机处理,更可靠。
但要注意:assigned-clock-rates设置的是时钟的初始频率,如果驱动后续用clk_set_rate改了,以驱动为准。另外,assigned-clocks里的时钟不一定要出现在clocks属性里,它可以是中间节点。这个灵活性有时候会让人困惑,我的建议是:只把驱动真正需要操作的时钟放进clocks,中间节点的配置用assigned-*处理。
3. 从设备树到寄存器:一个UART驱动时钟配置的完整链路
3.1 硬件视角:UART时钟树的典型结构
要理解API的行为,得先知道硬件上时钟是怎么走的。以瑞芯微RK3568的UART2为例,它的时钟树大致是这样的:
GPLL (1188MHz) └── CLK_UART2_SRC (mux: GPLL / CPLL / 24M) └── CLK_UART2_DIV (分频器) └── SCLK_UART2 (波特率时钟) └── UART2控制器 PCLK_UART2 (APB总线时钟,来自GPLL分频) └── UART2寄存器接口UART需要两个时钟:一个是波特率时钟SCLK_UART2,决定通信速率;一个是APB总线时钟PCLK_UART2,用于寄存器访问。这两个时钟在设备树里分别对应baudclk和apb_pclk。
波特率时钟的计算公式是:
baud_rate = SCLK_UART2 / (16 * divisor)其中divisor是UART内部的分频系数。所以如果你要115200的波特率,SCLK_UART2最好是115200 * 16 = 1843200Hz的整数倍。实际中通常设SCLK_UART2为24MHz或48MHz,然后通过内部divisor分频。
3.2 驱动代码:从probe到数据传输的时钟操作序列
一个典型的UART驱动probe函数里,时钟相关的代码大概是这样:
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)) { dev_err(&pdev->dev, "failed to get baudclk\n"); return PTR_ERR(uart->baudclk); } uart->pclk = devm_clk_get(&pdev->dev, "apb_pclk"); if (IS_ERR(uart->pclk)) { dev_err(&pdev->dev, "failed to get apb_pclk\n"); return PTR_ERR(uart->pclk); } ret = clk_prepare_enable(uart->pclk); if (ret) { dev_err(&pdev->dev, "failed to enable pclk\n"); return ret; } ret = clk_prepare_enable(uart->baudclk); if (ret) { dev_err(&pdev->dev, "failed to enable baudclk\n"); clk_disable_unprepare(uart->pclk); return ret; } /* 设置波特率时钟频率 */ ret = clk_set_rate(uart->baudclk, 24000000); if (ret) { dev_err(&pdev->dev, "failed to set baudclk rate\n"); goto err_disable; } dev_info(&pdev->dev, "baudclk actual rate: %lu\n", clk_get_rate(uart->baudclk)); /* 后续初始化硬件寄存器... */ return 0; err_disable: clk_disable_unprepare(uart->baudclk); clk_disable_unprepare(uart->pclk); return ret; }这段代码有几个值得注意的地方:
第一,先使能pclk再使能baudclk。因为寄存器访问依赖pclk,如果先使能baudclk,在设置波特率时钟的过程中可能需要访问寄存器,而pclk还没开,就会出问题。这个顺序在大多数SoC上都是必须的。
第二,错误处理路径要逆序关闭。如果baudclk使能失败,要关掉已经使能的pclk。如果clk_set_rate失败,要关掉两个时钟。这种逆序清理是驱动开发的基本功,但时钟这块特别容易漏,因为时钟句柄是devm_管理的,很多人以为不用管,但devm_clk_get只管理句柄释放,不管理使能状态。使能了就必须手动关闭。
第三,设置频率后读回确认。clk_set_rate返回0不代表频率一定设成了你想要的。有些时钟控制器会静默地选择最接近的频率。打印clk_get_rate的结果是最简单的验证手段。
3.3 运行时PM:时钟在suspend/resume中的处理
UART驱动通常支持运行时电源管理(Runtime PM)。在suspend时关闭时钟,resume时重新使能。这部分代码如果写错,会导致系统休眠后串口无输出,或者更严重的,时钟引用计数不平衡导致系统无法进入低功耗状态。
典型的实现:
static int my_uart_suspend(struct device *dev) { struct my_uart *uart = dev_get_drvdata(dev); clk_disable_unprepare(uart->baudclk); clk_disable_unprepare(uart->pclk); return 0; } static int my_uart_resume(struct device *dev) { struct my_uart *uart = dev_get_drvdata(dev); int ret; ret = clk_prepare_enable(uart->pclk); if (ret) return ret; ret = clk_prepare_enable(uart->baudclk); if (ret) { clk_disable_unprepare(uart->pclk); return ret; } return 0; }这里的关键是suspend和resume的时钟操作必须严格配对。如果suspend里disable了两次,resume里只enable一次,引用计数就会变成负数,内核会报warning。反过来,如果suspend里少disable一次,时钟就永远关不掉,功耗下不去。
我调试过一个案例:某驱动在suspend里调用了clk_disable_unprepare,但在resume里只调用了clk_prepare_enable,看起来配对。但问题是,这个驱动在probe里已经clk_prepare_enable过一次,suspend时disable了一次,resume时又enable了一次,计数是平衡的。但如果在suspend和resume之间,有其他代码路径也操作了同一个时钟,计数就会乱。所以最好的做法是每个驱动只管理自己的时钟引用,不要假设其他驱动不会碰同一个时钟。CCF的引用计数是全局的,多个驱动共享一个时钟时,任何一个驱动的操作都会影响整体状态。
4. 那些文档不会告诉你的时钟调试经验
4.1 clk_summary:一眼看穿时钟树的状态
内核提供了一个debugfs接口/sys/kernel/debug/clk/clk_summary,这是调试时钟问题最有力的工具。它列出了系统中所有时钟的当前状态:
clock enable_cnt prepare_cnt rate accuracy phase -------------------------------------------------------------------------------------- clk_24m 5 5 24000000 0 0 clk_32k 1 1 32768 0 0 gpll 3 3 1188000000 0 0 clk_uart2_src 1 1 24000000 0 0 clk_uart2_div 1 1 24000000 0 0 sclk_uart2 1 1 24000000 0 0几个关键列:
enable_cnt:clk_enable的引用计数。如果某个时钟你明明disable了,这里还是非零,说明有其他地方还在用。prepare_cnt:clk_prepare的引用计数。rate:当前实际频率。
我排查时钟问题的第一步永远是cat /sys/kernel/debug/clk/clk_summary。比如有一次调SPI屏,发现SPI时钟频率是50MHz,但屏幕手册要求最大30MHz。在clk_summary里找到SPI时钟节点,确认它的父时钟和分频器,然后算出正确的分频值,用clk_set_rate设置。整个过程不到五分钟。
注意:
clk_summary需要内核开启CONFIG_DEBUG_FS和CONFIG_COMMON_CLK_DEBUG(或者CONFIG_COMMON_CLK自带的debugfs支持)。在量产固件里通常会关掉,但调试阶段一定要开。
4.2 时钟设置失败的五种常见原因
clk_set_rate返回错误或者频率不对,通常逃不出这几种情况:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 返回-EINVAL | 请求的频率超出范围 | 用clk_round_rate查可用范围 |
| 返回0但频率没变 | 时钟被其他驱动占用,或父时钟不可调 | 查clk_summary的enable_cnt和父时钟 |
| 频率是请求值的一半 | 分频器计算方式理解错误 | 查provider驱动的round_rate实现 |
| 设置后系统卡死 | 在原子上下文调用了可能睡眠的API | 确认调用路径不在中断里 |
| 频率抖动 | 父时钟被其他设备动态调整 | 用clk_set_parent固定父时钟 |
其中"频率是请求值的一半"这个坑特别隐蔽。有些SoC的时钟分频器是"分频值+1"的语义,比如你写2表示3分频。如果provider驱动没有正确处理,就会出现频率偏差。这种情况只能去看provider的代码,或者用示波器量实际输出。
4.3 共享时钟的引用计数陷阱
多个设备共享同一个时钟时,引用计数是最容易出问题的地方。假设I2S和SPDIF共享一个MCLK,I2S驱动在probe里clk_prepare_enable,SPDIF驱动也在probe里clk_prepare_enable。此时计数是2。如果I2S驱动在suspend里clk_disable_unprepare,计数变成1,时钟仍然开着,SPDIF还能工作。这没问题。
但如果I2S驱动在remove时忘记clk_disable_unprepare,计数就永远停在2,时钟永远关不掉。更糟的是,如果I2S驱动在suspend里disable了两次(比如错误处理路径重复调用),计数变成0,时钟被关掉,SPDIF就挂了。
我的经验是:每个驱动只在自己的probe/remove和suspend/resume里成对操作时钟,不要在中断或其他异步路径里操作共享时钟。如果确实需要在运行时动态开关,用clk_prepare_enable/clk_disable_unprepare的引用计数来保证安全,但一定要确保每次enable都有对应的disable。
4.4 用clk_get_rate验证时钟的实际状态
clk_get_rate返回的是CCF缓存的实际频率,不是硬件寄存器的原始值。这个区别很重要:如果provider驱动没有正确实现recalc_rate回调,clk_get_rate可能返回一个过时的值。所以在设置频率之后,除了clk_get_rate,最好再用示波器或者频率计确认一下实际输出。特别是在调音频、视频这类对时钟精度敏感的模块时,软件读数和硬件实测可能会有偏差。
我调过一个I2S案例:clk_get_rate返回24576000,看起来是对的(48kHz * 512),但音频还是有杂音。后来用示波器量MCLK引脚,发现实际频率是24.576MHz没错,但占空比不是50%,导致codec采样时序偏移。这个问题在软件层面完全看不出来,只能靠硬件测量。所以时钟调试要软硬结合,不要只信软件读数。
5. 从consumer到provider:理解时钟框架的另一半
5.1 为什么consumer也需要懂provider
你可能觉得,我只是写个外设驱动,时钟控制器驱动是SoC厂商的事,跟我没关系。但实际工作中,你至少会遇到两种情况需要理解provider侧的逻辑:
第一种是调试时钟问题时。当clk_set_rate不生效,你需要知道provider的round_rate和set_rate是怎么实现的,才能判断是硬件限制还是驱动bug。比如RK3568的clk_uart2_div是一个特殊的divider,它的分频比不是线性的,而是有一组离散值。如果你不知道这个,就会奇怪为什么请求3分频得到了4分频。
第二种是SoC厂商的时钟驱动有bug时。国产SoC的时钟驱动质量参差不齐,有些provider驱动没有正确实现determine_rate,导致clk_set_rate总是失败。这时候你需要能看懂provider代码,甚至打补丁。我遇到过某SoC的I2S时钟provider把round_rate写成了直接返回请求值,但set_rate又设不了那么高,结果就是clk_set_rate返回成功但频率不对。这种问题只能改provider驱动。
5.2 设备树中的时钟provider节点长什么样
一个典型的时钟控制器节点:
cru: clock-controller@fdd20000 { compatible = "rockchip,rk3568-cru"; reg = <0x0 0xfdd20000 0x0 0x1000>; #clock-cells = <1>; #reset-cells = <1>; clocks = <&xin24m>; clock-names = "xin24m"; };#clock-cells = <1>表示这个provider用1个cell来标识具体的时钟,所以consumer引用时写成<&cru SCLK_UART2>。如果是#clock-cells = <0>,说明provider只有一个时钟,引用时写<&clk_single>就行。
clocks和clock-names属性说明这个provider自己也需要输入时钟(通常是晶振)。这就是时钟树的根节点。
5.3 时钟注册的两种方式:CLK_OF_DECLARE与platform driver
时钟provider的注册有两种方式:
CLK_OF_DECLARE:在设备树扫描的早期阶段注册,适用于系统启动早期就需要使用的时钟(比如定时器时钟)。- platform driver:在设备模型初始化后注册,适用于大多数外设时钟。
CLK_OF_DECLARE的时钟在of_clk_init阶段就被注册,这时候内存管理还没完全初始化,所以不能用devm_系列API。这也是为什么有些早期时钟驱动看起来写法很"原始"。
对于consumer驱动开发者来说,需要知道的是:如果你的驱动在probe时发现某个时钟还没注册(devm_clk_get返回-EPROBE_DEFER),不要慌,返回-EPROBE_DEFER让内核稍后重试就行。这是正常的依赖处理机制。我见过有驱动在devm_clk_get失败时直接返回错误,导致设备永远probe不了,就是因为没有正确处理-EPROBE_DEFER。
正确的写法:
clk = devm_clk_get(&pdev->dev, "baudclk"); if (IS_ERR(clk)) { if (PTR_ERR(clk) == -EPROBE_DEFER) return -EPROBE_DEFER; dev_err(&pdev->dev, "failed to get clock\n"); return PTR_ERR(clk); }这个细节在时钟provider注册顺序不确定时特别重要。比如你的驱动依赖一个I2C时钟芯片提供的时钟,而I2C总线还没初始化完,devm_clk_get就会返回-EPROBE_DEFER。返回这个错误码让内核延迟probe,等时钟provider就绪后再试。
6. 把时钟API用对:一份来自实战的检查清单
6.1 probe阶段的时钟操作顺序
把前面讲的内容整理成一个可执行的检查清单。每次写新驱动或者review别人的驱动时,按这个顺序过一遍:
- 获取时钟句柄:用
devm_clk_get,检查IS_ERR,处理-EPROBE_DEFER。 - 使能总线时钟:先使能pclk/apb_pclk这类寄存器接口时钟。
- 使能功能时钟:再使能baudclk/mclk这类功能时钟。
- 设置频率:用
clk_set_rate,之后用clk_get_rate确认。 - 配置父时钟:如果需要,用
clk_set_parent选择时钟源。 - 错误处理:任何一步失败,逆序关闭已使能的时钟。
这个顺序不是绝对的,但覆盖了大多数场景。特殊情况下(比如时钟必须在寄存器访问之前设置频率),需要根据硬件手册调整。
6.2 remove和suspend/resume的对称性检查
remove和suspend/resume里的时钟操作必须与probe和resume严格对称。我习惯在代码里用注释标出配对关系:
/* probe: enable pclk -> enable baudclk -> set rate */ /* remove: disable baudclk -> disable pclk */ /* suspend: disable baudclk -> disable pclk */ /* resume: enable pclk -> enable baudclk */这样review时一眼就能看出是否配对。另外,suspend里不需要重新设置频率,因为resume后时钟频率会保持suspend前的值(除非硬件掉电)。如果硬件掉电导致频率丢失,需要在resume里重新clk_set_rate。
6.3 用clk_summary做回归验证
每次修改时钟相关代码后,用clk_summary做一次回归验证:
- 加载驱动前,记录目标时钟的
enable_cnt和rate。 - 加载驱动后,确认
enable_cnt增加了正确的次数,rate是期望值。 - 卸载驱动后,确认
enable_cnt回到加载前的值。 - 执行一次suspend/resume,确认计数和频率不变。
这个流程能抓住大多数引用计数不平衡和频率设置错误的问题。我把它写成了一个简单的shell脚本,在CI里自动跑:
#!/bin/bash # 记录加载前的状态 cat /sys/kernel/debug/clk/clk_summary | grep uart2 > /tmp/clk_before.txt # 加载驱动 modprobe my_uart # 检查加载后的状态 cat /sys/kernel/debug/clk/clk_summary | grep uart2 > /tmp/clk_after.txt # 对比enable_cnt diff /tmp/clk_before.txt /tmp/clk_after.txt # 卸载驱动 rmmod my_uart # 确认恢复 cat /sys/kernel/debug/clk/clk_summary | grep uart2 > /tmp/clk_final.txt diff /tmp/clk_before.txt /tmp/clk_final.txt如果最后的diff不为空,说明有时钟泄漏。
6.4 常见错误码的含义与处理
| 错误码 | 含义 | 处理方式 |
|---|---|---|
| -EPROBE_DEFER | 时钟provider还没注册 | 返回-EPROBE_DEFER让内核重试 |
| -ENOENT | 设备树里没有对应的clock-names | 检查设备树配置 |
| -EINVAL | 频率参数无效或时钟不支持该操作 | 用clk_round_rate查可用范围 |
| -EBUSY | 时钟被占用,无法修改 | 检查是否有其他驱动在用 |
| -ENOMEM | 内存分配失败 | 检查系统内存状态 |
其中-ENOENT最常见,通常是设备树里clock-names拼写错误,或者clocks属性里少写了一个时钟。我遇到过把clock-names写成clock-name的,找了半天才发现是拼写问题。所以设备树修改后一定要用dtc编译一遍,确认没有语法错误。
7. 写在最后:一些个人体会
时钟框架的API不多,常用的就那么七八个,但要用对、用好,需要对硬件时钟树和CCF的实现机制都有理解。我刚开始做驱动时,也觉得clk_prepare_enable和clk_enable的区别很绕,直到有一次在中断里调用了clk_prepare_enable导致系统休眠警告,才真正明白这个设计的必要性。
另一个深刻的体会是:时钟问题一定要用工具定位,不要靠猜。clk_summary、clk_get_rate、示波器,这三个工具能解决90%以上的时钟问题。剩下的10%,需要去看provider驱动的实现,理解时钟树的硬件结构。
最后分享一个习惯:每次写新的外设驱动,我会先在设备树里把时钟配置写清楚,用assigned-clocks把初始频率和父时钟设好,然后在驱动里只做必要的运行时调整。这样驱动代码更简洁,时钟树的初始化也更可靠。设备树是描述硬件连接的地方,把时钟关系放在设备树里,比在驱动代码里硬编码要清晰得多。