ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux时钟使用者API详解:从clk_get到clk_round_rate的驱动调试指南

Linux时钟使用者API详解:从clk_get到clk_round_rate的驱动调试指南 先讲个我踩过的坑。去年调试一块带USB PHY的板子PHY需要从参考时钟拿到48MHz我参照芯片手册直接套驱动结果接上设备就是枚举失败示波器量出来的频率整整偏了3.2%。查了两天最后定位到根因不是PHY的问题而是我根本没搞懂clk_round_rate到底在告诉我什么。这件事逼我把Linux通用时钟框架Common Clock FrameworkCCF的时钟使用者API从头啃了一遍也让我意识到很多人对于驱动和外设“怎么拿到时钟”“怎么用时钟”其实一直停留在“照抄示例代码”的阶段。这篇文章就围绕时钟使用者API来写适合Linux驱动开发、嵌入式系统工程师以及那些正在被“设备树里clocks属性到底怎么解读”折磨的内核学习者。我会从CCF解决什么问题讲起再把clk_get、clk_prepare_enable、clk_set_rate、clk_round_rate这些核心接口逐个拆开最后给出我在真实板子上用到的调试手段和常见翻车点。这里的每一条基本都是从“改一行代码、量一次波形、看一次dump”里得来的。1. 为什么驱动开发者必须读懂这套API一次串口波特率异常排查1.1 从一次波特率不准说起先说那次串口问题。芯片内部有个UART控制器波特率发生器需要从时钟树某一路拿一个整数倍频率典型的公式是波特率 时钟频率 / (16 × 分频系数)。我当时的处理方式是直接在驱动里写死一个目标频率然后调clk_set_rate代码路径看起来完全正常没有任何错误上报但串口出来的数据就是乱码。用示波器和频率计拉出来才发现实际输出频率和预期差了一截。为什么会这样因为芯片内部的PLL和分频器并不能产生任意频率它只能输出若干个离散档位。clk_set_rate(1000000)并不等于“给你精确的1MHz”而是“把时钟切到离1MHz最近的那个可用档位”。如果这个档位是960kHz误差4%对USB这类对频率精度极其敏感的外设就是灾难。这里就是使用者API的第一个认知门槛你请求的频率不一定是你拿到的频率。内核为了统一处理这种问题设计了一套“先询问、后设置、再确认”的流程也就是clk_round_rate、clk_set_rate、clk_get_rate三个函数配合使用。很多驱动只写了中间那个clk_set_rate前面不问、后面不验才会酿成这种“状态码全成功、实际波形全不对”的诡异Bug。1.2 CCF到底在统一什么再往深一层说CCF的诞生背景各平台驱动风格太混乱了。早期的内核代码里每个SoC厂商都自己实现一套时钟管理函数有的叫clk_enable_foo有的叫plat_clk_on有的直接就让你操作寄存器。这些接口互不兼容导致一个问题一个外设驱动如果要适配多个平台代码里得写满#ifdef。CCF从Linux 3.4开始逐步铺开核心思路是把“时钟”抽象成内核对象提供一套统一的使用者接口。无论底层是简单晶振、PLL、分频器还是MUX你在驱动里看到的都是struct clk *。你可以像操作一个黑盒一样调用标准API而真正的寄存器操作被封装到各自的时钟驱动里。这种设计带来的直接好处是驱动代码可以跨厂商复用设备树里一句clocks clkctl 96就把硬件拓扑关系声明清楚了不需要在C代码里维护一堆平台相关的结构体。对于驱动作者来说你不需要把CCF里所有时钟驱动都啃明白但必须把“使用者API”这条链子理清楚获取句柄、准备/使能、查询频率、修改频率、修改父时钟、释放句柄。这条链路掌握得好不好直接决定你的驱动在真机上稳不稳。2. 使用者API全景从获取句柄到频率切换的核心调用链2.1 拿到时钟句柄clk_get与devm_clk_get所有操作的起点是拿到一个struct clk *指针。最传统的方式struct clk *clk; clk clk_get(dev, uart_clk); if (IS_ERR(clk)) return PTR_ERR(clk);这里有个新手经常忽略的点clk_get失败返回的不是NULL而是ERR_PTR类型的错误编码。所以检查要用IS_ERR不是if (clk NULL)。如果拿NULL去比较错误路径会直接跳过后续调用clk_prepare_enable时在内核里触发空指针解引用轻则Oops重则整机重启。更推荐的做法是使用devm_clk_get这个“devm”前缀表示资源由设备驱动模型自动管理驱动在probe里获取时钟后即使后续初始化失败或者驱动被卸载内核会自动帮你释放不需要在remove里手动clk_put。省事且能减少内存泄漏路径。设备树场景下用法struct clk *clk; clk devm_clk_get(pdev-dev, uart_clk); if (IS_ERR(clk)) return PTR_ERR(clk);第二个参数是时钟在设备树里的名字对应uart0: serial10000000 { compatible ...; clocks clkctl 96, clkctl 97; clock-names uart_clk, bus_clk; };clock-names和clocks属性一一对应驱动里就能按名字取而不是靠索引号硬编码。这玩意儿看着简单但我见过很多工程里clock-names拼写不一致驱动里字符串匹配不上返回-ENOENT排查半天发现是dts里少写了个字母。2.2 预备与使能为什么要有两个动作拿到句柄之后很多人直接写clk_enable(clk);然后就完事了。但实际上CCF里“打开时钟”分两步clk_prepare和clk_enable。两者区别非常关键API是否允许睡眠适用上下文典型场景clk_prepare允许睡眠进程上下文可能操作I2C、寄存器锁、等待硬件稳定clk_enable不允许睡眠中断/原子上下文只改寄存器位、维护引用计数clk_prepare_enable允许睡眠进程上下文组合调用驱动里最常见为什么这么分因为时钟硬件本身可能挂在慢速总线上操作寄存器需要等待不能放在中断里。但有些外设又必须在中断处理函数里临时开时钟比如DMA的中断回调要快速启动一个时钟域。那就只能在进程上下文先prepare好中断里只做轻量级的enable。对大多数驱动来说probe阶段处于进程上下文直接用组合接口ret clk_prepare_enable(clk); if (ret) return ret;这里一定要检查返回值。clk_enable在底层会调用clk_ops-enable回调如果硬件起振失败或电源域没上电是会返回错误码的。我见过有人直接忽略返回值结果外设寄存器完全无法访问还以为是I2C驱动的问题。2.3 释放顺序与引用计数有开就有放。关闭时钟时也有两个对应接口clk_disable和clk_unprepare。调用顺序和打开相反clk_disable_unprepare(clk);这里的引用计数逻辑值得一提同一个clk可以被多个驱动获取每次clk_prepare_enable都会增加计数只有引用计数归零时底层时钟驱动才会真正关闭硬件时钟。这意味着你不必担心“我关了这个时钟别的模块会挂”。但也意味着如果你自己多调用了一次clk_prepare_enable而少调用了一次clk_disable_unprepare这个时钟就会一直开着造成系统待机功耗异常。这类问题不太会引起功能故障所以特别容易被忽略等到做功耗优化时才发现时钟树里一片常驻“on”。3. 调频率的正确姿势clk_set_rate并不总是返回你想要的频率3.1 先询问再设置最后确认频率修改是使用者API里最讲究的部分。直接看三函数配合的推荐写法long rounded; unsigned long rate; rounded clk_round_rate(clk, 1000000); if (rounded 0) { dev_err(dev, no valid frequency\n); return rounded; } ret clk_set_rate(clk, rounded); if (ret) { dev_err(dev, failed to set rate %ld\n, rounded); return ret; } rate clk_get_rate(clk); dev_info(dev, actual rate: %lu\n, rate);clk_round_rate返回的是底層时钟驱动给出的、离目标频率最近的可用频率。它不会修改任何硬件状态纯粹是“问路”。clk_set_rate按照这个询问结果去设置。clk_get_rate在设置完成后读取实际频率做最终确认。为什么要多此一举搞三步因为不同SoC的时钟树结构差异太大。有的PLL能输出很细粒度的频率有的只能输出几档。对一个固定分频比的时钟输入频率可能直接决定了输出频率的档位。你在驱动里写死clk_set_rate(1000000)底层可能实际设置成999423Hz或1008000Hz。如果你不确认实际值后面计算分频系数、算波特率全都会基于错误假设。3.2 一个典型的算分频场景拿I2C来举例更直观。I2C控制器需要从时钟树拿到一个源时钟然后内部再分频得到SCL频率。假设驱动想要SCL为400kHz某平台源时钟只能给12.288MHz分频系数就完全不是整数最后SCL实际输出就可能偏离400kHz。严格的做法是unsigned long src_rate clk_get_rate(clk); /* 根据src_rate和target_scl反推出分频系数 */ div DIV_ROUND_CLOSEST(src_rate, target_scl * 2); /* 再写回控制器 */如果这一步没做直接把target_scl400kHz写进控制器的分频寄存器结果就是SCL输出变成看起来还行、但时序余量极差的波形挂在总线上的传感器偶尔冒一个NACK。板上验证时可能一切正常量产之后才暴露。3.3 别把clk_set_rate当成同步精确操作还有一个隐蔽问题即使clk_set_rate返回0也不代表该时钟的硬件频率已经稳定。部分SoC的PLL锁定需要时间锁相环失锁期间输出频率是抖动的。有的时钟驱动会在clk_ops-set_rate里等待PLL_LOCK中断有的不会。所以你在外设初始化时序里如果对时钟稳定性特别敏感最好在clk_set_rate之后加一个小延时或者通过硬件手册确认该PLL的lock时间再继续配置外设。另外clk_get_rate返回的是一个unsigned long值它的计算来源可能是“缓存”。这套API的设计是clk_set_rate成功后框架会调用clk_ops-recalc_rate重新计算频率并缓存。但如果clk_set_rate的底层实现有疑问或者时钟树里父时钟变化了这个缓存不一定立刻同步到你这个分支的视角。稳妥起见关键外设初始化时序里都以设置完成后重新读取的clk_get_rate为准不要依赖你传入clk_set_rate的那个目标值。4. 生命周期管理与资源释放devm_clk_get为什么是首选4.1 probe/remove中的标准动作写一个带时钟的外设驱动probe里最常见的套路是static int foo_probe(struct platform_device *pdev) { struct clk *clk; int ret; clk devm_clk_get(pdev-dev, foo_clk); if (IS_ERR(clk)) return PTR_ERR(clk); ret clk_prepare_enable(clk); if (ret) return ret; /* 外设初始化... */ }这里如果后续初始化失败devm_clk_get获取的时钟会自动释放但enable状态不会自动撤销。严谨的写法是static int foo_probe(struct platform_device *pdev) { ... ret clk_prepare_enable(clk); if (ret) return ret; ret foo_hw_init(); if (ret) { clk_disable_unprepare(clk); return ret; } }有些小伙伴会问既然都用了devm为什么还需要手动clk_disable_unprepare因为devm只管“资源内存释放”和“引用计数减一”并不会保证“时钟处于disable状态”。你在probe里把时钟打开了remove时devm会帮你clk_put但put之前如果没人调用disable底层驱动看到的引用计数仍然是“已经prepare/enable”这只时钟在系统里就被永久占用了。所以一个对称的removestatic void foo_remove(struct platform_device *pdev) { struct foo_dev *foo platform_get_drvdata(pdev); clk_disable_unprepare(foo-clk); /* 其他清理... */ }如果驱动可能被异步移除或者你有多个时钟路径跑一遍clk_disable_unprepare之前最好确认时钟确实处于enabled状态。避免重复disable导致引用计数变成负数内核会有WARN_ON打出来。4.2 多时钟外设的批量操作现代外设越来越夸张一个IP要挂好几个时钟域。比如一个DisplayPort控制器可能有dp_core_clk、dp_link_clk、dp_audio_clk甚至还有一个dp_ahb_clk。如果手动一个一个去clk_get、clk_prepare_enable代码会非常冗长而且错误路径上一旦已经开了三个时钟而第四个失败你得逐个去关。这时内核提供了批量接口struct clk_bulk_data clocks[] { { .id core }, { .id link }, { .id audio }, }; ret devm_clk_bulk_get(pdev-dev, ARRAY_SIZE(clocks), clocks); if (ret) return ret; ret clk_bulk_prepare_enable(ARRAY_SIZE(clocks), clocks); if (ret) return ret;clk_bulk_prepare_enable内部会逐个打开如果中途失败它会自动回滚之前已经打开的那些。这比手写循环可靠得多。我自从在某个PCIe驱动里吃过分批初始化的亏后凡是时钟数量超过两个的外设一律上clk_bulk。remove的时候也不用纠结顺序直接clk_bulk_disable_unprepare(ARRAY_SIZE(clocks), clocks);这套API还有个额外好处设备树里时钟列表的增删驱动代码完全不用改只要clock-names和clk_bulk_data数组里的id匹配就行。对于“同一个驱动兼容多个硬件版本”的场景这是很实用的约定。4.3 共享时钟与功耗的权衡共享时钟的处理是我觉得很多驱动工程师最没概念的地方。一颗SoC上两个外设可能挂在同一路时钟上比如SDIO和eMMC都复用了某个MMC控制器的时钟域。如果A设备驱动在remove时直接clk_disable_unprepareB设备还在用按理说引用计数会阻止真正的硬件关断。但如果A设备驱动在remove时调用了clk_set_rate来“省电”把它改成低频率那B设备就遭殃了SDIO读取直接超时。这就引出一个原则共享时钟的外设驱动不要随意修改频率。如果确实需要改最好通过clk_round_rate先确认当前其他使用者的频率需求或者干脆把“允许改频率”的功能做成可配置项默认关闭。功耗优化应该在系统级统一规划而不是每个驱动各自为政。我在不少项目中见过这种“各自为政”导致的诡异休眠唤醒问题排查起来极其痛苦。5. 中断上下文、时钟缓存与休眠限制三个最容易翻车的细节5.1 中断里到底能调哪些API这是一个高频考差点。很多人写驱动时在中断回调里觉得“开个时钟”是天经地义的。但如果你在中断或spin_lock保护的原子上下文里调用clk_set_rate等待你的大概率是BUG: sleeping function called from invalid context。原因在于clk_set_rate内部可能调用clk_ops-set_rate而底层实现里经常包含互斥锁、I2C操作、甚至msleep等待PLL锁定。这些都是睡眠函数不能在原子上下文使用。中断里安全的操作只有clk_enable/clk_disable前提是时钟已经通过clk_prepare准备好clk_get_rate读取缓存值不涉及硬件操作clk_get_parent等纯查询接口如果确实需要在中断里改频率正确做法是在中断里标记一个标志位然后在threaded irq或者工作队列里执行clk_set_rate。5.2 缓存了就不能信clk_get_rate的时机问题clk_get_rate读取的是该时钟的缓存频率而这个缓存的刷新时机由CCF内部维护。正常情况下clk_set_rate成功后框架会触发recalc_rate并更新到相关时钟对象。但你如果在驱动里直接操作了父时钟的寄存器——比如通过寄存器视图手动改了PLL分频——然后立刻调clk_get_rate拿到的很可能还是旧值。这种状况通常出现在“混合式”工程里一部分时钟配置走CCF一部分直接操作寄存器。我自己吃过一次亏之后凡是涉及手动寄存器操作的场景都会在关键节点主动调用一次clk_get_rate并打印出来与寄存器实测值交叉验证。如果你发现两者对不上大概率是缓存没刷新不是硬件真错了。5.3 父时钟变更与驱动反应CCF里有一个机制叫“时钟通知链”clk_notifier_register当某个时钟的频率发生变化时注册了通知的模块会收到POST_RATE_CHANGE或ABORT_RATE_CHANGE事件。很多驱动在依赖的父时钟可能被其他模块调整时靠这个来重新计算自己的分频参数。例如某外设的时钟源来自一路可调PLL而该PLL同时被另一个更重要的模块控制。你在驱动里配置好外设后对方模块突然把PLL从200MHz调到250MHz你的外设如果不重新计算分频系数输出频率就会偏离。这时候标准的做法是注册一个通知回调static int foo_clk_notifier_cb(struct notifier_block *nb, unsigned long event, void *data) { struct clk_notifier_data *ndata data; switch (event) { case POST_RATE_CHANGE: /* 根据ndata-new_rate重新计算分频 */ break; default: break; } return NOTIFY_OK; }这个机制并不难但很多工程师直到产品出现“某个外设偶发工作异常”才想到去查是不是父时钟被别人动了。这里的教训是如果你的外设对频率精度有要求而它的时钟源不是固定的晶振直连你就应该主动监听变化。“我改完频率后对方应该自己适应”这种期望在CCF设计哲学里是不存在的。6. 板级调试三板斧clk_summary、错误返回码和PTR_ERR检查6.1 debugfs才是你的示波器当频率出问题的时候第一件事不是去改代码而是先打开内核的时钟调试接口。前提是内核开启了CONFIG_DEBUG_FS并且在启动参数里允许访问debugfs。挂载之后直接看mount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/clk/clk_summary输出会给你整棵时钟树的父子关系、当前频率、prepare_count、enable_count、是否自动使能等信息。格式大概是clock enable_cnt prepare_cnt rate accuracy -------------------------------------------------------------------------------- clk_24m 3 5 24000000 0 0 pll_pll0 1 2 1200000000 0 0 mux_foo 1 1 500000000 0 0 clk_div_foo 1 1 125000000 0 0看这个表能快速定位两件事一是某个时钟的enable_cnt为什么不为0二是某个时钟的rate是否和预期一致。特别是当你怀疑“我的外设时钟是不是没开”时直接看对应时钟的计数比到处加printk高效得多。6.2 错误返回码全集时钟API的返回值有一些常见坑。我整理过一张自己排查用的对照表返回码含义常见原因-ENOENT找不到时钟设备树clock-names拼写错误-EPROBE_DEFER依赖的时钟驱动还没ready需要驱动probe延后重试-EINVAL参数非法目标频率超出时钟支持范围-ENOSYS底层未实现该回调某些时钟驱动没实现set_rate-EBUSY时钟被独占有驱动开启了CLK_SET_RATE_NOCACHE之类的保护其中-EPROBE_DEFER特别值得多说一点。Linux的设备驱动模型支持“延后探测”如果某个外设的时钟提供者还没注册devm_clk_get会返回-EPROBE_DEFER内核会在稍后重新尝试probe。但如果你在probe里把-EPROBE_DEFER当成普通错误直接return虽然也不是不行但可能失去重试时机驱动要等下次通知才能再被触发。标准做法clk devm_clk_get(pdev-dev, foo); if (IS_ERR(clk)) { ret PTR_ERR(clk); if (ret -EPROBE_DEFER) return ret; return ret; }其实就是透传-EPROBE_DEFER。很多框架API都有这个约定处理时钟时尤其常见因为时钟驱动本身加载顺序不确定是行业普遍现象。6.3 关于CLK_SET_RATE_NOCACHE与硬件实际值不一致的排查最后一个很实用的排查经验。如果你发现clk_get_rate返回值和示波器对不上先别怀疑缓存看看这个时钟驱动是否设置了CLK_GET_RATE_NOCACHE标志。这个标志的作用是让clk_get_rate每次强制触发底层的recalc_rate回调实时读取硬件寄存器而不是用缓存值。系统调试阶段在设备树或驱动里临时给有疑问的时钟加上这个标志可以大幅度减少“代码说有这个频率、波形说没有”的扯皮。不过要注意CLK_GET_RATE_NOCACHE会对每次查询产生实际寄存器访问开销高频查询场景下性能会受影响量产代码里通常不建议长期保留。我在某个MIPI DSI驱动里遇到过类似情况。面板要求像素时钟精确到小数点后两位而PLL档位和寄存器读回的频率始终有偏差最后就是靠这个标志确认了实际频率再回头修正分频系数终于把画面闪屏问题解决。所以说这套API表面上只是几个函数真正难的是你愿不愿意在这些函数上下细功夫。把这棵树的每一个节点都当成自己能掌控的硬件而不是“内核帮我搞好了”的黑盒很多玄学问题都会变成数学问题。
返回列表