☰
Linux通用时钟框架CCF:时钟使用者API详解与驱动实战避坑指南
2026/9/26 1:38:50 网站建设 项目流程

1. 从设备树到驱动:Linux通用时钟框架到底在管什么

搞嵌入式Linux的人,迟早会跟时钟打交道。你写一个I2C驱动,发现寄存器写不进去,查了半天发现是I2C控制器的时钟没使能;你调一个音频Codec,采样率死活对不上,最后发现是MCLK的分频系数配错了。这类问题在RK3568、i.MX、全志、瑞芯微这些平台上反复出现,而解决它们的核心工具,就是Linux的通用时钟框架(Common Clock Framework,简称CCF)。

CCF是Linux内核在2012年前后引入的一套子系统,位于drivers/clk/目录下。它的核心目标只有一个:把SoC内部错综复杂的时钟树抽象成统一的模型,让驱动开发者用一套标准API就能获取时钟、使能时钟、设置频率和选择父时钟,而不用关心底层是PLL、MUX、DIVIDER还是GATE。在没有CCF之前,每个SoC厂商都自己搞一套时钟管理代码,驱动里充斥着#ifdef CONFIG_ARCH_XXX,移植成本极高。CCF出现之后,时钟消费者(consumer)和时钟提供者(provider)彻底解耦,设备树(Device Tree)成为两者之间的桥梁。

这篇文章面向的是已经能写基本字符设备驱动、但对时钟框架还停留在“照抄dts里clk配置”阶段的开发者。我会从时钟使用者的角度出发,把clk_get、clk_prepare_enable、clk_set_rate、clk_set_parent这些API的用法、坑点和底层逻辑讲透。读完之后,你应该能做到:拿到一份陌生的设备树,能看懂时钟树的拓扑关系;写驱动时,能正确申请和释放时钟资源;调频率时,知道为什么clk_set_rate有时候“不生效”。

先明确一个概念区分。在CCF的语境里,时钟提供者(provider)是SoC时钟控制器驱动,它注册struct clk_hw和struct clk_ops,描述PLL、分频器、选择器、门控这些硬件单元。时钟使用者(consumer)是各种外设驱动,比如UART、SPI、I2C、LCD控制器、音频接口,它们通过struct clk *句柄来操作时钟。本文聚焦后者,也就是“时钟使用者API”。你不需要去写clk_ops,但你必须知道每个API调用背后发生了什么,否则调试时会非常被动。

提示:CCF的API分为两套,一套是老的clk_enable/clk_disable,一套是clk_prepare_enable/clk_disable_unprepare。新代码一律用后者,原因后面会详细讲。

2. 时钟使用者API的核心接口与选型逻辑

2.1 获取时钟句柄:clk_get与devm_clk_get的区别

驱动里拿时钟句柄,最原始的方式是clk_get:

struct clk *clk_get(struct device *dev, const char *id);

dev是设备指针,id是时钟在设备树里的名字,对应clock-names属性。比如设备树里写了:

uart2: serial@fe650000 { clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>; clock-names = "baudclk", "apb_pclk"; };

驱动里就要这样拿:

struct clk *baudclk = clk_get(dev, "baudclk"); struct clk *apbclk = clk_get(dev, "apbclk");

但实际项目中,我几乎不用clk_get,而是用devm_clk_get:

struct clk *devm_clk_get(struct device *dev, const char *id);

区别在于devm_前缀代表“设备资源管理”(Device Managed)。用devm_clk_get获取的时钟,在设备卸载或驱动probe失败时,内核会自动调用clk_put释放,不需要你手动写错误处理路径。我踩过的坑是:早期用clk_get,probe函数里有五个错误分支,每个分支都要记得clk_put,漏一个就造成引用计数泄漏,时钟永远关不掉,功耗下不去。换成devm_clk_get之后,这类问题直接消失。

还有一个变体devm_clk_get_optional,它在时钟不存在时返回NULL而不是ERR_PTR(-ENOENT)。这个API适合那些“时钟可选”的场景,比如某些低速外设可以走内部RC振荡器,也可以走外部晶振。用devm_clk_get的话,时钟不存在会直接报错,驱动加载失败;用devm_clk_get_optional则允许你判断if (clk)来决定后续行为。

2.2 使能时钟:为什么必须用clk_prepare_enable

这是新手最容易犯错的地方。老API是:

int clk_enable(struct clk *clk); void clk_disable(struct clk *clk);

新API是:

int clk_prepare_enable(struct clk *clk); void clk_disable_unprepare(struct clk *clk);

为什么要有prepare这一层?因为有些时钟的使能操作可能睡眠。比如一个PLL的锁定需要等待一段时间,这期间要调用usleep_range;或者时钟控制器挂在I2C/SPI总线上,使能时钟需要发起一次总线传输。这些操作不能在原子上下文(中断处理函数)里执行。CCF把使能拆成两步:

  • clk_prepare:可以睡眠的部分,比如等待PLL锁定、配置寄存器。
  • clk_enable:不能睡眠的部分,通常是简单的寄存器位操作。

clk_prepare_enable就是两者依次调用。对应的clk_disable_unprepare先clk_disable再clk_unprepare。

注意:在中断上下文里只能调用clk_enable,不能调用clk_prepare_enable。但实践中,绝大多数驱动都在probe或resume路径里使能时钟,这些路径允许睡眠,所以直接用clk_prepare_enable是安全的。

我实测过一个案例:在RK3568平台上,某个SPI屏的驱动在中断里调用clk_enable,结果内核报“scheduling while atomic”。原因是该时钟的clk_ops->enable回调里调用了regmap_read,而regmap底层走了I2C,I2C传输会睡眠。后来改成在probe里clk_prepare_enable,中断里只操作SPI数据,问题解决。

2.3 设置频率:clk_set_rate的“不生效”之谜

clk_set_rate的签名很直观:

int clk_set_rate(struct clk *clk, unsigned long rate);

你传入目标频率(单位Hz),它返回0表示成功。但很多人发现,调用之后用clk_get_rate读回来,频率跟设的不一样。这不是bug,而是CCF的设计逻辑:时钟树有层级,子时钟的频率受父时钟约束。

举个例子。假设时钟树是这样的:

PLL (600MHz) -> DIVIDER (1~32) -> GATE -> UART

你想让UART跑115200波特率,需要时钟频率是14.7456MHz(假设16倍过采样)。你调用clk_set_rate(uart_clk, 14745600)。CCF会从UART这个时钟节点向上遍历,找到最近的可以调频率的节点——也就是那个DIVIDER。DIVIDER的父时钟是600MHz,它只能做整数分频。600MHz / 14745600 = 40.69,取整后分频系数是41,实际输出600MHz / 41 = 14.634MHz。所以clk_get_rate返回的是14634146,而不是14745600。

这就是“不生效”的真相:CCF只能在你给定的时钟树约束下,找一个最接近目标频率的合法值。如果你需要精确频率,要么换一个能产生该频率的父时钟(比如专门的音频PLL),要么用分数分频器(fractional divider)。

clk_set_rate还有一个变体clk_set_rate_exclusive,它会独占该时钟的频率设置权,防止其他驱动同时修改。这个API在多驱动共享同一PLL时很有用,但用不好会导致其他驱动设置频率失败。我的建议是:除非你明确知道自己在做什么,否则不要用exclusive版本。

2.4 选择父时钟:clk_set_parent的使用场景

clk_set_parent用于切换时钟的父节点:

int clk_set_parent(struct clk *clk, struct clk *parent);

典型场景是音频子系统。比如一个I2S控制器,它的MCLK可以来自PLL_A(适合48kHz系列采样率)或PLL_B(适合44.1kHz系列采样率)。播放不同采样率的音频时,驱动需要动态切换父时钟,以得到精确的MCLK。

struct clk *mclk = devm_clk_get(dev, "mclk"); struct clk *pll_a = devm_clk_get(dev, "pll_a"); struct clk *pll_b = devm_clk_get(dev, "pll_b"); if (sample_rate % 48000 == 0) clk_set_parent(mclk, pll_a); else clk_set_parent(mclk, pll_b);

这里有个坑:clk_set_parent可能会失败,如果目标父时钟当前被其他子时钟占用,或者硬件不支持动态切换。失败时返回负值,驱动必须检查返回值。我见过一个驱动直接忽略返回值,结果播放44.1kHz音频时声音变调,查了一天才发现是父时钟没切过去。

2.5 其他常用API速查

API作用是否可睡眠典型调用位置
devm_clk_get获取时钟句柄是probe
clk_prepare_enable准备并使能时钟是probe/resume
clk_disable_unprepare关闭并取消准备是remove/suspend
clk_set_rate设置频率是probe/运行时
clk_get_rate读取当前频率否任意
clk_set_parent切换父时钟是运行时
clk_get_parent读取当前父时钟否任意
clk_is_enabled查询使能状态否调试

提示:clk_get_rate和clk_is_enabled不会睡眠,可以在中断里调用,但不要依赖它们做关键决策,因为时钟状态可能被其他驱动并发修改。

3. 设备树中的时钟绑定与实操解析

3.1 clocks与clock-names的对应关系

设备树是时钟使用者和提供者之间的契约。一个设备节点通过clocks属性引用时钟提供者的phandle,通过clock-names给每个时钟起名字。驱动里devm_clk_get(dev, "name")的name必须和clock-names里的字符串完全匹配。

以RK3568的UART2为例:

uart2: serial@fe650000 { compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart"; reg = <0x0 0xfe650000 0x0 0x100>; interrupts = <GIC_SPI 117 IRQ_TYPE_LEVEL_HIGH>; clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>; clock-names = "baudclk", "apb_pclk"; status = "disabled"; };

这里clocks有两个条目,clock-names也有两个,顺序一一对应。第一个SCLK_UART2是波特率时钟,第二个PCLK_UART2是APB总线时钟。驱动里通常这样写:

struct clk *baudclk, *apbclk; baudclk = devm_clk_get(&pdev->dev, "baudclk"); if (IS_ERR(baudclk)) return PTR_ERR(baudclk); apbclk = devm_clk_get(&pdev->dev, "apbclk"); if (IS_ERR(apbclk)) return PTR_ERR(apbclk);

如果clock-names写错了,比如写成"baud_clk",驱动里用"baudclk"去拿,会返回ERR_PTR(-ENOENT),probe直接失败。这种错误在移植设备树时非常常见,尤其是从其他平台抄dts的时候。

3.2 assigned-clocks的自动配置机制

设备树里还有一组属性:assigned-clocks、assigned-clock-rates、assigned-clock-parents。这些属性让内核在设备初始化时自动配置时钟,不需要驱动写代码。

&i2s1_8ch { assigned-clocks = <&cru SCLK_I2S1_RX>, <&cru SCLK_I2S1_TX>; assigned-clock-rates = <12288000>, <12288000>; assigned-clock-parents = <&cru PLL_I2S>; };

这段配置的意思是:在I2S1设备probe之前,内核自动把SCLK_I2S1_RX和SCLK_I2S1_TX的父时钟设为PLL_I2S,频率设为12.288MHz。驱动里就不需要再调用clk_set_parent和clk_set_rate了。

这个机制的好处是配置与代码分离。同一份驱动,在不同板子上可以用不同的设备树覆盖,时钟配置随板子走。但坑在于:assigned-clocks的处理时机是在of_clk_set_defaults里,发生在设备probe之前。如果你的驱动在probe里又调了一次clk_set_rate,会覆盖掉设备树的配置。所以要么全用设备树,要么全用驱动代码,不要混着来。

3.3 时钟树的调试:从sysfs看时钟状态

调试时钟问题,最直接的工具是sysfs。挂载debugfs之后:

mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/clk/clk_summary

输出类似:

clock enable_cnt prepare_cnt rate accuracy phase -------------------------------------------------------------------------------------- clk_32k 1 1 32768 0 0 sclk_uart2 1 1 24000000 0 0 pclk_uart2 1 1 100000000 0 0

enable_cnt是使能计数,prepare_cnt是准备计数,rate是当前频率。如果某个时钟的enable_cnt是0但你期望它是1,说明驱动没使能时钟。如果rate跟你设的不一样,说明分频系数被约束了。

我习惯在驱动probe前后各dump一次clk_summary,对比哪些时钟的计数变了。这个方法能快速定位“哪个时钟没开”或者“哪个时钟被多开了一次”。

注意:clk_summary的输出格式在不同内核版本略有差异,但核心字段一致。如果debugfs没挂载,先确认内核配置里开了CONFIG_DEBUG_FS和CONFIG_COMMON_CLK_DEBUG。

4. 驱动中的时钟管理实战:从probe到remove

4.1 probe阶段的时钟获取与使能顺序

一个规范的时钟使用者驱动,probe阶段通常按以下顺序操作:

  1. 获取所有需要的时钟句柄(devm_clk_get)。
  2. 检查返回值,任何一个失败就返回错误。
  3. 设置频率和父时钟(如果设备树没配)。
  4. 使能时钟(clk_prepare_enable)。
  5. 访问硬件寄存器,初始化设备。
  6. 注册字符设备、输入设备、网络设备等。

顺序很重要。必须先使能时钟,再访问寄存器。否则访问会触发总线错误(SIGBUS)或者返回全0xFF。我在全志H3平台上遇到过:SPI控制器驱动先读寄存器再使能时钟,结果读出来全是0,驱动误判硬件不存在,直接返回-ENODEV。

代码示例:

static int my_probe(struct platform_device *pdev) { struct my_dev *mdev; int ret; mdev = devm_kzalloc(&pdev->dev, sizeof(*mdev), GFP_KERNEL); if (!mdev) return -ENOMEM; mdev->clk = devm_clk_get(&pdev->dev, "core"); if (IS_ERR(mdev->clk)) return dev_err_probe(&pdev->dev, PTR_ERR(mdev->clk), "failed to get core clock\n"); mdev->pclk = devm_clk_get(&pdev->dev, "apb"); if (IS_ERR(mdev->pclk)) return dev_err_probe(&pdev->dev, PTR_ERR(mdev->pclk), "failed to get apb clock\n"); ret = clk_set_rate(mdev->clk, 50000000); if (ret) return dev_err_probe(&pdev->dev, ret, "failed to set core clock rate\n"); ret = clk_prepare_enable(mdev->clk); if (ret) return dev_err_probe(&pdev->dev, ret, "failed to enable core clock\n"); ret = clk_prepare_enable(mdev->pclk); if (ret) { clk_disable_unprepare(mdev->clk); return dev_err_probe(&pdev->dev, ret, "failed to enable apb clock\n"); } /* 现在可以安全访问寄存器了 */ my_hw_init(mdev); return 0; }

注意dev_err_probe这个辅助函数,它会把错误码转成可读字符串,并且对-EPROBE_DEFER做特殊处理(不打印错误日志,因为这是正常的延迟探测)。在新内核里,推荐用它替代dev_err。

4.2 remove与suspend/resume中的时钟处理

remove阶段要关闭时钟。如果用devm_clk_get获取句柄,clk_put是自动的,但clk_disable_unprepare必须手动调用:

static int my_remove(struct platform_device *pdev) { struct my_dev *mdev = platform_get_drvdata(pdev); clk_disable_unprepare(mdev->pclk); clk_disable_unprepare(mdev->clk); return 0; }

suspend/resume里也要处理时钟。如果设备在suspend时不需要保持时钟,就关闭;resume时重新使能。但要注意:有些时钟关闭后,寄存器的值会丢失,resume时需要重新初始化硬件。

static int my_suspend(struct device *dev) { struct my_dev *mdev = dev_get_drvdata(dev); clk_disable_unprepare(mdev->clk); return 0; } static int my_resume(struct device *dev) { struct my_dev *mdev = dev_get_drvdata(dev); int ret; ret = clk_prepare_enable(mdev->clk); if (ret) return ret; my_hw_reinit(mdev); return 0; }

提示:如果设备支持运行时PM(Runtime PM),时钟管理会更复杂。基本原则是:在runtime_suspend里关时钟,在runtime_resume里开时钟,并且用pm_runtime_get_sync/pm_runtime_put来管理引用计数。

4.3 时钟引用计数与并发保护

CCF内部对每个时钟维护enable_count和prepare_count。每次clk_prepare_enable加1,每次clk_disable_unprepare减1。只有计数降到0时,硬件时钟才真正关闭。这个设计允许多个驱动共享同一个时钟,比如两个SPI控制器共用同一个PCLK。

但引用计数不是线程安全的。如果两个驱动并发调用clk_prepare_enable,可能一个成功一个失败,或者计数出错。CCF内部用了自旋锁保护计数,但驱动层面仍然要注意:不要在多个上下文里同时操作同一个时钟句柄。如果确实需要,用互斥锁保护。

我遇到过一个案例:一个驱动在中断里调用clk_enable,在workqueue里调用clk_disable,结果计数变成负数,内核报“clk: invalid enable count”。后来改成所有时钟操作都在workqueue里串行执行,问题消失。

5. 常见问题排查与避坑指南

5.1 clk_get返回-ENOENT的排查思路

devm_clk_get返回-ENOENT,说明设备树里没有对应的时钟。排查步骤:

  1. 确认设备节点的clock-names属性存在,且字符串拼写正确。
  2. 确认clocks属性的条目数和clock-names一致。
  3. 确认时钟提供者节点(比如&cru)的#clock-cells和phandle正确。
  4. 用of_dump或cat /proc/device-tree/.../clock-names查看实际解析结果。

常见错误是clock-names里用了下划线,驱动里用了连字符,或者大小写不一致。设备树是大小写敏感的。

5.2 clk_set_rate返回-EINVAL的原因

clk_set_rate返回-EINVAL,通常是因为目标频率超出了时钟的可调范围。比如一个固定分频器只能输出1MHz、2MHz、4MHz,你设3MHz就会失败。排查方法:

cat /sys/kernel/debug/clk/clk_summary

看该时钟的rate范围。或者用clk_round_rate先查询:

long rounded = clk_round_rate(clk, target_rate); if (rounded < 0) return rounded; clk_set_rate(clk, rounded);

clk_round_rate返回的是最接近目标频率的合法值,不改变硬件状态。先round再set,可以避免-EINVAL。

5.3 时钟使能后设备仍不工作的检查清单

如果时钟使能了,但设备还是不工作,按以下顺序检查:

检查项方法可能问题
时钟频率clk_get_rate频率不对,分频系数错
父时钟clk_get_parent父时钟选错,频率源不对
复位信号检查reset控制器设备处于复位状态
电源域检查power domain电源未上电
引脚复用检查pinctrl引脚功能没配成外设模式
寄存器写入读回寄存器总线时钟没使能

我遇到过最隐蔽的一个问题:时钟频率、父时钟、复位、电源都正常,但设备就是不工作。最后发现是pinctrl配置里,时钟输出引脚被配成了GPIO输入模式,时钟信号根本没送到外设。所以时钟框架只管“时钟源”,不管“时钟线”的物理连接。

5.4 时钟相关的内核报错速查

报错信息含义解决方法
clk: invalid enable count使能计数为负检查enable/disable是否配对
scheduling while atomic在原子上下文调用了可睡眠API改用clk_enable或移到workqueue
failed to get clockdevm_clk_get失败检查设备树clock-names
clk_set_rate failed频率设置失败用clk_round_rate先查询
clock is not prepared未prepare就enable用clk_prepare_enable

提示:内核启动参数加clk_ignore_unused可以防止未使用的时钟被关闭,适合调试阶段。但生产环境不要加,否则功耗下不去。

6. 从使用者视角理解时钟框架的扩展能力

6.1 clk_notifier:频率变化的异步通知

有些驱动需要在时钟频率变化时做出响应。比如一个定时器驱动,时钟频率变了,定时周期就要重新计算。CCF提供了clk_notifier_register:

struct clk_notifier { struct list_head node; struct clk *clk; struct notifier_block nb; }; int clk_notifier_register(struct clk *clk, struct notifier_block *nb);

当clk_set_rate成功改变频率后,CCF会调用注册的回调函数,传入PRE_RATE_CHANGE、POST_RATE_CHANGE或ABORT_RATE_CHANGE事件。驱动可以在POST_RATE_CHANGE里用clk_get_rate读取新频率,更新内部状态。

这个机制在音频子系统里用得很多。I2S控制器的MCLK频率变了,Codec的采样率就要跟着调。但要注意:notifier回调是在持有自旋锁的上下文里调用的,不能睡眠,不能调用clk_set_rate。

6.2 clk_bulk:批量时钟管理

如果一个设备需要很多时钟(比如LCD控制器可能需要PCLK、HPCLK、VPLL、DPHY等五六个时钟),逐个devm_clk_get和clk_prepare_enable很啰嗦。CCF提供了批量API:

struct clk_bulk_data { const char *id; struct clk *clk; }; int devm_clk_bulk_get(struct device *dev, int num_clks, struct clk_bulk_data *clks); int clk_bulk_prepare_enable(int num_clks, struct clk_bulk_data *clks); void clk_bulk_disable_unprepare(int num_clks, struct clk_bulk_data *clks);

用法:

static const struct clk_bulk_data lcd_clks[] = { { .id = "pclk" }, { .id = "hclk" }, { .id = "vpll" }, { .id = "dphy" }, }; struct clk_bulk_data *clks; int num_clks = ARRAY_SIZE(lcd_clks); clks = devm_kmemdup(&pdev->dev, lcd_clks, sizeof(lcd_clks), GFP_KERNEL); ret = devm_clk_bulk_get(&pdev->dev, num_clks, clks); if (ret) return ret; ret = clk_bulk_prepare_enable(num_clks, clks); if (ret) return ret;

批量API的好处是错误处理简单:任何一个时钟获取失败,devm_clk_bulk_get会自动释放已经获取的时钟。使能时如果中间某个失败,clk_bulk_prepare_enable会回滚已经使能的时钟。这比手动写循环和错误分支可靠得多。

6.3 时钟精度与jitter:什么时候需要关心

大多数驱动不需要关心时钟精度,但音频、视频、射频类驱动必须关心。clk_get_accuracy返回时钟的精度(单位ppb,十亿分之一):

long clk_get_accuracy(struct clk *clk);

如果返回0,表示精度未知或无限精确。对于音频MCLK,精度直接影响采样率误差。一个100ppm的时钟,在48kHz采样率下,每秒误差4.8个采样点,几分钟后就能听出音调偏差。

如果SoC的音频PLL精度不够,可以考虑用外部低jitter晶振作为时钟源。设备树里把assigned-clock-parents指向外部晶振对应的时钟节点即可。

6.4 时钟框架与电源管理的协同

现代SoC里,时钟和电源域是绑定的。一个电源域下电时,其内部的时钟也会丢失。CCF通过clk_pm_runtime相关的API与genpd(Generic Power Domain)协同。

驱动开发者需要知道的是:如果设备在运行时PM里关闭了时钟,那么访问寄存器前必须重新使能时钟。有些驱动在runtime_resume里只调用了pm_runtime_get_sync,忘了clk_prepare_enable,结果访问寄存器时总线报错。

正确的做法是在runtime_resume里同时处理电源域和时钟:

static int my_runtime_resume(struct device *dev) { struct my_dev *mdev = dev_get_drvdata(dev); int ret; ret = clk_prepare_enable(mdev->clk); if (ret) return ret; /* 电源域由pm_runtime自动管理 */ return 0; }

7. 个人实操体会与几个容易忽略的细节

调了这么多年的时钟,我最大的体会是:时钟问题很少是CCF本身的bug,绝大多数是设备树配置和驱动调用顺序的问题。CCF的API设计已经足够健壮,但它的行为高度依赖设备树描述的时钟树拓扑。如果设备树写错了,API调用再正确也没用。

几个我踩过多次的坑,分享出来帮你省时间:

第一,clk_prepare_enable和clk_disable_unprepare必须严格配对。我见过一个驱动在probe里使能了两次,remove里只关闭了一次,结果时钟引用计数永远不为0,系统进入suspend时功耗偏高。用devm_add_action_or_reset可以自动处理这种配对,但需要额外写回调函数。

第二,clk_set_rate之后一定要用clk_get_rate确认实际频率。不要假设设置的值就是生效的值。尤其是在有多个驱动共享PLL的场景下,你的设置可能被其他驱动覆盖。

第三,设备树里的assigned-clock-rates和驱动里的clk_set_rate不要同时用。如果设备树配了,驱动里就不要再设;如果驱动里设了,设备树里就留空。两者同时存在时,执行顺序是设备树先、驱动后,驱动会覆盖设备树,但设备树的配置仍然会消耗一次时钟操作,可能触发不必要的PLL重锁。

第四,调试时钟问题时,clk_summary比printk好用。在驱动里加printk需要重新编译内核,而clk_summary随时可以看。养成在probe前后dump时钟状态的习惯,能快速缩小问题范围。

最后分享一个实用技巧:如果怀疑某个时钟没使能,可以在内核命令行加clk_ignore_unused,让所有时钟保持开启。如果加了之后设备正常工作,说明确实是时钟被意外关闭了。然后逐步去掉这个参数,用clk_summary观察哪个时钟的enable_cnt变成了0,就能定位到是哪个驱动提前关闭了时钟。这个方法我在RK3568和i.MX8M上都用过,百试百灵。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询