驱动开发硬核特训 · Day 25 (附加篇):从设备树到驱动——深入理解Linux时钟子系统的实战链路


一、前言

在嵌入式Linux开发中,无论是CPU、外设控制器,还是简单的GPIO扩展器,大多数硬件模块都离不开时钟信号的支撑。

时钟子系统(Clock Subsystem) ,作为Linux内核中基础设施的一部分,为设备提供统一、灵活、可控的时钟管理机制。

然而,时钟子系统的工作方式常常隐藏在设备树配置与驱动框架之下,让初学者难以直观感知。

今天,我们将通过一个完整、清晰、实战化的例子 ,带你从设备树定义 ,到内核驱动调用核心API全面理解时钟子系统的核心知识点和实际用途


二、实战场景介绍

本次实战选取的例子是 ------ UART 控制器(串口)

在 i.MX8MP 平台上,UART1 控制器是一个非常典型的时钟消费者。它需要一个稳定的时钟源来驱动波特率生成器,确保数据收发的准确性。


三、设备树中的时钟配置

在设备树中,为每个时钟消费者(比如 UART)指定它需要的时钟源。来看具体的设备树片段(源自 i.MX8MP EVK 板):

dts 复制代码
&uart1 { /* BT UART */
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart1>;
    assigned-clocks = <&clk IMX8MP_CLK_UART1>;
    assigned-clock-parents = <&clk IMX8MP_SYS_PLL1_80M>;
    uart-has-rtscts;
    status = "okay";
};

解释

  • assigned-clocks:指定要配置的时钟节点(这里是UART1的根时钟)。
  • assigned-clock-parents:指定该时钟的上游父时钟(这里是系统PLL1输出的80MHz频率)。
  • status = "okay":使能设备。

小结

在设备树中,UART1通过 assigned-clocks 指定了自己依赖的时钟源。

这一步是让内核启动时,能够正确配置 UART 所需时钟。


四、时钟子系统在内核中的处理流程

内核解析设备树时,会执行以下步骤:

  1. 解析 assigned-clocks 属性

    • 调用 of_clk_get() 获取时钟phandle。
  2. 设置父时钟(if assigned-clock-parents)

    • 调用 clk_set_parent()
  3. 设置频率(if assigned-clock-rates)

    • 调用 clk_set_rate()
  4. 将时钟与设备绑定

    • 供后续驱动使用 clk_get() 提取并控制时钟。

总结一句话:内核在启动时,根据设备树,把设备需要的时钟准备好,驱动层可以直接使用。


五、驱动中的核心函数调用链

进入驱动注册时,通常在 probe() 函数中,设备驱动需要主动申请并启用时钟。

来看典型的 Linux 串口驱动核心代码(以drivers/tty/serial/fsl_lpuart.c为例):

c 复制代码
static int lpuart_probe(struct platform_device *pdev)
{
    struct resource *res;
    struct clk *clk;
    struct lpuart_port *sport;

    // ... 省略其他初始化代码

    clk = devm_clk_get(&pdev->dev, NULL);
    if (IS_ERR(clk))
        return PTR_ERR(clk);

    clk_prepare_enable(clk);

    // 后续设置UART寄存器,开始工作

    return 0;
}

解释

  • devm_clk_get(&pdev->dev, NULL):从时钟子系统中获取设备绑定的时钟(根据设备树内容解析)。
  • clk_prepare_enable(clk):准备并使能时钟,确保模块时钟打开,才能访问寄存器或工作。

这两个函数是驱动中最核心的标准时钟处理流程


六、核心知识点总结

过程 内容 说明
设备树定义 通过 assigned-clocks assigned-clock-parents 指定设备需要使用的时钟及父时钟关系
内核解析设备树 使用 of_clk_get()clk_set_parent() 等接口 完成设备初步时钟配置
驱动中申请时钟 使用 devm_clk_get() 获取时钟句柄 与设备生命周期绑定,自动管理释放
启用时钟 使用 clk_prepare_enable() 确保时钟开启,模块能够正常读写工作
关闭时钟 使用 clk_disable_unprepare()(通常在 remove 时) 释放功耗资源,避免悬空开启

七、完整链路小结(可视化)

复制代码
[设备树]
    |
    v
解析 assigned-clocks → 绑定 clk_hw → 初始化频率和父时钟
    |
    v
驱动 probe 阶段
    |
    v
devm_clk_get() 获取时钟
    |
    v
clk_prepare_enable() 打开时钟
    |
    v
模块正常运行

八、实战小问答(加深记忆)

  • Q:如果不调用 clk_prepare_enable() 会怎么样?

    ➔ A:设备可能无法正常访问寄存器(时钟门控),导致挂死或者硬件超时错误。

  • Q:为什么用 devm_clk_get()

    ➔ A:简化驱动资源管理,避免忘记释放时钟资源导致内存泄漏。

  • Q:时钟源一定是固定的吗?

    ➔ A:不一定,有些时钟(如CPU频率)是动态可变的,需要动态控制频率。

九、driver/clk/子系统的核心组成

在 Linux 内核中,driver/clk/ 目录下是整个 Common Clock Framework(CCF) 的具体实现。

它包含两大部分:

部分 作用
核心框架代码(Generic) 提供统一时钟注册、获取、启用、频率调整等API
平台时钟驱动(Platform-specific) 各芯片平台的具体时钟树实现,比如 imx8mp

十、实战中最关键的核心文件和功能

我们只讲跟设备树解析、驱动使用真正相关的代码,不浪费篇幅。

目录/文件 作用 备注
drivers/clk/clk.c 核心通用时钟框架逻辑 所有 clk_register, clk_get, clk_prepare_enable 都在这里最终处理
drivers/clk/clk-fixed-rate.c 实现固定频率的时钟(如设备树里的 fixed-clock) 常用于 camera、USB、PCIE refclk
drivers/clk/imx/clk-imx8mp.c i.MX8MP 平台特有的时钟树搭建 把 IMX8MP 平台的所有时钟源、分频器、复用器建立起来

十一、真正重要的核心函数讲解(来自 driver/clk/clk.c)

11.1 clk_register()

🔵 作用:

在内核启动时,或者平台驱动初始化时,把一个新的时钟节点注册到内核的时钟树中。

🔵 主要流程:

  • struct clk_hw 转换为 struct clk_core
  • clk_core 挂到内核全局时钟链表
  • 处理时钟父子关系

🔵 关键源码位置:

c 复制代码
struct clk *clk_register(struct device *dev, struct clk_hw *hw)
{
    struct clk_core *core;
    ...
    core = kzalloc(sizeof(*core), GFP_KERNEL);
    core->hw = hw;
    ...
    hlist_add_head(&core->node, &clk_root_list);
    return __clk_create_clk(core, NULL);
}

这里的 clk_core 是真正管理时钟状态的内部结构。


11.2 of_clk_get()devm_clk_get()

🔵 作用:

  • of_clk_get():从设备树 phandle 提取时钟节点。
  • devm_clk_get():在驱动中调用,帮你自动结合生命周期管理。

🔵 主要流程:

  • 根据设备树 phandle 查找 clk_provider
  • 通过 provider的 of_clk_src_onecell_get() 或类似接口拿到 clk

🔵 关键源码位置:

c 复制代码
struct clk *of_clk_get(struct device_node *np, int index)
{
    ...
    provider = of_parse_phandle(np, "clocks", index);
    ...
    return provider->get(provider->data, index);
}

最终就是在驱动层 devm_clk_get() 里面,调用 of_clk_get(),并返回一个 clk 结构体给设备驱动用。


11.3 clk_prepare_enable()clk_disable_unprepare()

🔵 作用:

  • clk_prepare_enable():打开时钟,并确保硬件模块有时钟供给。
  • clk_disable_unprepare():关闭时钟,释放资源。

🔵 核心调用逻辑:

c 复制代码
int clk_prepare_enable(struct clk *clk)
{
    int ret;
    ret = clk_prepare(clk);
    if (ret)
        return ret;
    ret = clk_enable(clk);
    if (ret)
        clk_unprepare(clk);
    return ret;
}

本质 就是调用底层时钟驱动注册时定义好的 enable/disable 函数指针。

比如:

  • PLL的 enable
  • gate clock 的开关
  • mux clock 的切换

十二、fixed-clock 举例(drivers/clk/clk-fixed-rate.c)

在设备树中,如果定义了:

dts 复制代码
pcie0_refclk: pcie0-refclk {
    compatible = "fixed-clock";
    #clock-cells = <0>;
    clock-frequency = <100000000>;
};

内核会自动匹配到 drivers/clk/clk-fixed-rate.c 中的 of_fixed_clk_setup()

c 复制代码
static void __init of_fixed_clk_setup(struct device_node *node)
{
    struct clk_fixed_rate *fixed;
    ...
    fixed->hw.init = &init;
    clk_register(NULL, &fixed->hw);
}

➔ 最后也是通过 clk_register() 注册到内核时钟树。

所以:

  • fixed-clock → 固定频率的时钟节点
  • 设备(比如 PCIe PHY)可以直接引用它做为参考时钟

✨ 最重要总结(一句话)

✅ 无论是外设驱动,还是内核框架,

✅ 设备树 → of_clk_get → clk_register → clk_prepare_enable

✅ 整个链路最终就是围绕 clk_coreclk_ops 把时钟管理起来,

✅ 保证所有设备能统一管理时钟、动态调频、动态开关。


📋 最后再给你一张核心流程小图(助记忆)

复制代码
[设备树 clocks属性]
        ↓
[of_clk_get()]
        ↓
[clk_register() 把hw挂到core]
        ↓
[devm_clk_get()]
        ↓
[clk_prepare_enable()]
        ↓
[设备驱动正常工作]

相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式