驱动开发硬核特训 · 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()]
        ↓
[设备驱动正常工作]

相关推荐
桦01 分钟前
【Linux】g++安装教程
linux·运维·服务器
Once_day40 分钟前
Linux之netlink(2)libnl使用介绍(1)
linux·netlink·libnl3
Hfc.1 小时前
rabbitmq-集群部署
linux·运维·服务器
小草cys2 小时前
[零基础]内网ubuntu映射到云服务器上,http访问(frp内网穿透)
运维·服务器
YIBO04082 小时前
WSL2下Docker desktop的Cadvisor容器监控
运维·docker·容器·wsl·wsl2
Python少年班3 小时前
vim粘贴代码格式错乱 排版错乱 缩进错乱 解决方案
linux·编辑器·vim·排版错乱·缩进错乱·格式错乱·换行错乱
一眼青苔3 小时前
如何知道Ubuntu的端口是否被占用,被那个进程占用?如何终止进程
linux·运维·ubuntu
冼紫菜3 小时前
[特殊字符] Docker 从入门到实战:全流程教程 + 项目部署指南(含镜像加速)
运维·分布式·后端·docker·云原生·容器
编程绿豆侠4 小时前
Win11安装Ubuntu20.04简记
linux