news 2026/9/26 8:56:49

嵌入式Linux时钟框架实战:consumer API详解与调试指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式Linux时钟框架实战:consumer API详解与调试指南

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别人的驱动时,按这个顺序过一遍:

  1. 获取时钟句柄:用devm_clk_get,检查IS_ERR,处理-EPROBE_DEFER。
  2. 使能总线时钟:先使能pclk/apb_pclk这类寄存器接口时钟。
  3. 使能功能时钟:再使能baudclk/mclk这类功能时钟。
  4. 设置频率:用clk_set_rate,之后用clk_get_rate确认。
  5. 配置父时钟:如果需要,用clk_set_parent选择时钟源。
  6. 错误处理:任何一步失败,逆序关闭已使能的时钟。

这个顺序不是绝对的,但覆盖了大多数场景。特殊情况下(比如时钟必须在寄存器访问之前设置频率),需要根据硬件手册调整。

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做一次回归验证:

  1. 加载驱动前,记录目标时钟的enable_cnt和rate。
  2. 加载驱动后,确认enable_cnt增加了正确的次数,rate是期望值。
  3. 卸载驱动后,确认enable_cnt回到加载前的值。
  4. 执行一次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把初始频率和父时钟设好,然后在驱动里只做必要的运行时调整。这样驱动代码更简洁,时钟树的初始化也更可靠。设备树是描述硬件连接的地方,把时钟关系放在设备树里,比在驱动代码里硬编码要清晰得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 8:56:13

STM32调试失效的根源:BOOT0启动模式与NRST复位链深度解析

1. 这不是教程&#xff0c;是十年焊点烫出来的经验清单STM32开发调试经验总结&#xff1a;那些年踩过的坑——这句话我写在自己第一块蓝 pill 板子背面时&#xff0c;用的是记号笔&#xff0c;油墨被汗洇开&#xff0c;像一道没愈合的疤。后来换到 STM32F407ZGT6 开发板&#x…

作者头像 李华
网站建设 2026/9/26 8:56:12

MATLAB凸轮机构仿真:参数化建模与三线运动分析

简介&#xff1a;本资源是一份面向机械工程、机电一体化专业师生及自动化设计工程师的MATLAB实践教学资料&#xff0c;聚焦凸轮机构运动建模、数值仿真与动态可视化这一典型机械系统分析难点。文档基于华东交通大学罗世民等人的核心研究成果&#xff0c;系统讲解了对心滚子直动…

作者头像 李华
网站建设 2026/9/26 8:55:14

组态屏替代PLC:Linux嵌入式控制实战指南

1. 项目概述&#xff1a;当组态屏不再只是“显示器”&#xff0c;而是真正的控制中枢“组态屏写脚本&#xff0c;PLC直接省掉”——这句话在自动化圈子里传开时&#xff0c;我第一反应是皱眉。不是质疑技术可行性&#xff0c;而是太熟悉那种“省掉PLC”的诱惑背后&#xff0c;往…

作者头像 李华
网站建设 2026/9/26 8:54:02

腾讯数字人+大模型知识引擎:从形象驱动到知识驱动的落地实战

数字人这两年从"能说会动"的演示阶段&#xff0c;快速滑向了"能答会办"的生产阶段。我所在的团队从去年开始陆续接触了几套数字人方案&#xff0c;踩过的坑不算少&#xff1a;形象做得再精致&#xff0c;一旦用户问出知识库之外的问题&#xff0c;整个交互…

作者头像 李华
网站建设 2026/9/26 8:52:25

SVG图标实战指南:从选型、压缩到版权与兼容性避坑

1. 为什么现在还在用PNG做图标&#xff1f;SVG才是现代UI的底层基建你有没有遇到过这样的情况&#xff1a;在给一个响应式网站加图标时&#xff0c;设计师扔过来一套PNG&#xff0c;结果在Retina屏上糊成一片&#xff1b;或者想改个颜色&#xff0c;得重新切图、换资源、清缓存…

作者头像 李华
网站建设 2026/9/26 8:52:24

金融级系统设计:从确定性、合规性到可审计性的工程实践

1. 项目概述&#xff1a;这不是一个“服务”&#xff0c;而是一套可落地的金融业务支撑体系“financial-services”这个标题乍看像一个宽泛的行业分类词&#xff0c;甚至可能被误认为是某家银行官网的导航栏标签。但在我过去十年跑遍全国27个省市、参与过43个金融类系统交付项目…

作者头像 李华