linux clock driver framework

  • Linux要管理电源(休眠唤醒)、动态调频(DVFS),还要处理多个外设之间的时钟依赖关系。因此,Linux 引入了 CCF (Common Clock Framework,通用时钟框架)。

时钟树

  • 所有的时钟树,都只有 5 种基本组成,这也是 Linux CCF 框架定义的最基础的 5 种时钟类型

    • Oscillator (晶振/固定时钟源)
      • 产生最原始的时钟频率。K1的 32 kHz RTC 和 24 MHz 晶振 (OSC)。在 Linux 中通常用 clk-fixed-rate 表示。
    • PLL (锁相环)
      • 把低频的晶振“倍频”成很高频的时钟。图中的 PLL1、PLL2、PLL3。
    • Divider (分频器)
      • 把高频时钟除以一个系数(比如 /2, /4),降频给低速外设用。图里那些标着 DIV 或者 div 的方块。
    • Multiplexer (MUX / 多路选择器)
      • 有多个时钟源输入,通过寄存器选择其中一个输出(像铁轨的道岔)。手册里提到的 “无毛刺 (Glitch-Free) 时钟切换” 就是指高级的 MUX 切换时不会产生破坏系统的错误波形。图中那些梯形的符号(比如图里 APB clk 前面的选择器)。
    • Gate (门控)
      • 就是一个开关。关掉时钟可以省电。k1手册里提到的 “细粒度时钟门控 (Clock Gating)” 就是指这个。图中那些带有使能端(通常带个与门 & 符号或者开关符号)的模块。
  • CLOCK 驱动,本质工作就是把手册里这些 晶振、PLL、MUX、DIV、GATE,一个一个地用 Linux 规定的 C 语言结构体描述出来,然后“注册”给内核。

k1 dts分析

  1. 基础时钟源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
clocks {
#address-cells = <0x2>;
#size-cells = <0x2>;
ranges;

// 1. 定义一个 1MHz 的固定时钟源
vctcxo_1m: clock-1m {
compatible = "fixed-clock"; // 告诉内核:这是一个频率永远不变的时钟
clock-frequency = <1000000>; // 频率是 1000000 Hz (1MHz)
clock-output-names = "vctcxo_1m"; // 命名
#clock-cells = <0>; // 0代表它没有子时钟输出端口,就它自己
};

// 2. 这里就是手册里写的 24MHz 晶振 (OSC)
vctcxo_24m: clock-24m {
compatible = "fixed-clock";
clock-frequency = <24000000>;
clock-output-names = "vctcxo_24m";
#clock-cells = <0>;
};

vctcxo_3m: clock-3m {
compatible = "fixed-clock";
clock-frequency = <3000000>;
clock-output-names = "vctcxo_3m";
#clock-cells = <0>;
};

osc_32k: clock-32k {
compatible = "fixed-clock";
clock-frequency = <32000>;
clock-output-names = "osc_32k";
#clock-cells = <0>;
};
};
  1. 时钟控制器节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
soc {
......
// 系统控制节点,名字叫 syscon_apbc,寄存器基地址是 0xd4015000
syscon_apbc: system-control@d4015000 {
compatible = "spacemit,k1-syscon-apbc";
reg = <0x0 0xd4015000 0x0 0x1000>;
// 它的输入时钟有哪些?引用了上面定义的四个固定晶振
clocks = <&osc_32k>, <&vctcxo_1m>, <&vctcxo_3m>, <&vctcxo_24m>;
clock-names = "osc", "vctcxo_1m", "vctcxo_3m", "vctcxo_24m";
// 这个参数很关键!<1> 表示如果有其他外设想用这个控制器提供的时钟,
// 需要提供 1 个参数(即时钟的 ID 号,比如 0 代表 UART0 时钟)
#clock-cells = <1>;
#reset-cells = <1>;
};
......

syscon_mpmu: system-controller@d4050000 {
compatible = "spacemit,k1-syscon-mpmu";
reg = <0x0 0xd4050000 0x0 0x209c>;
clocks = <&osc_32k>, <&vctcxo_1m>, <&vctcxo_3m>,
<&vctcxo_24m>;
clock-names = "osc", "vctcxo_1m", "vctcxo_3m",
"vctcxo_24m";
#clock-cells = <1>;
#power-domain-cells = <1>;
#reset-cells = <1>;
};

// PLL 控制器节点
pll: system-control@d4090000 {
compatible = "spacemit,k1-pll";
reg = <0x0 0xd4090000 0x0 0x1000>;
clocks = <&vctcxo_24m>; // PLL 的输入源只有 24M 晶振

// 这里是一个特例(Quirk):PLL 的锁定状态(Lock)寄存器不在自己的地址段里,
// 而是在 MPMU 的寄存器里,所以这里用指针 <&syscon_mpmu> 引用了 MPMU 节点。
spacemit,mpmu = <&syscon_mpmu>;
#clock-cells = <1>;
};

syscon_apmu: system-control@d4282800 {
compatible = "spacemit,k1-syscon-apmu";
reg = <0x0 0xd4282800 0x0 0x400>;
clocks = <&osc_32k>, <&vctcxo_1m>, <&vctcxo_3m>,
<&vctcxo_24m>;
clock-names = "osc", "vctcxo_1m", "vctcxo_3m",
"vctcxo_24m";
#clock-cells = <1>;
#power-domain-cells = <1>;
#reset-cells = <1>;
};
  • k1的四个时钟控制器:APBS(专门管 PLL)、APBC(管 APB 总线外设)、MPMU(主电源/时钟管理)、APMU(应用电源/时钟管理)

    • APBS(PLL部分):相当于“发动机”,负责升高频率(比如把 24M 晶振倍频到 1.2GHz 给 CPU,或者 300MHz 给总线)。
    • APBC / MPMU / APMU:相当于“变速箱和开关”,它们接收来自 PLL 的高频时钟,然后通过各自内部的 MUX(选择器)和 DIV(分频器)分配和控制给各个具体外设的频率大小,并通过 GATE(门控)来开关。
      • PMU: Power Management Unit,电源管理单元, 电源和时钟是高度绑定的。
        • MPMU (Main PMU):负责管理芯片里最核心、最基础、甚至休眠时也不能断电的模块(比如看门狗、RTC、唤醒逻辑)的时钟和电源。
        • APMU (Application PMU):负责管理那些极其耗电的“应用级”大模块(比如 CPU核心、GPU、VPU视频编解码、PCIe、USB)的时钟和电源。
  • 大多数现代复杂应用处理器(如瑞芯微、全志、NXP i.MX 以及这款 K1)都是这样的架构,只有结构简单的单片机才会把时钟全塞进一个大杂烩寄存器(RCC)里。

    • 简化了电源处理逻辑,例如休眠时主要切断 APMU 所在的时钟和电源。

MFD Syscon 架构 : 虽然物理上分为四个区域,但在 Linux 内核的 CCF 框架看来,它们构成了一棵完整的、互相依赖的时钟树(比如 APBC 里的 UART 时钟,父节点可能就是 APMU 里的某个时钟,源头是 APBS 里的 PLL)。为了方便在这个驱动内统一解析这些依赖、统一向内核注册,写在一个 spacemit,k1-ccu 驱动里是最高效的。

驱动代码

  • ccu_common.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
struct ccu_common {
struct regmap *regmap; // 寄存器映射句柄。这就是我们在 probe 里拿到的“钥匙”,
// 所有的读写操作都靠它。
struct regmap *lock_regmap; // 锁寄存器句柄。专门给 PLL 用的,用来读取那个
// 不在自己地盘里的“锁定状态位”。

union { // 这是一个联合体(Union),它里面的两组成员共享内存,
// 因为一个时钟要么是 MIX/DDN 类型,要么是 PLL 类型。
/* For DDN and MIX */
struct {
u32 reg_ctrl; // 控制寄存器的偏移地址(比如开关位、分频位都在这)。
u32 reg_fc; // 频率改变(Frequency Change)标志寄存器的偏移。
u32 fc; // 频率改变标志位(具体是哪一位)。
};

/* For PLL */
struct {
u32 reg_swcr1; // PLL 控制寄存器 1(软件配置位)。
u32 reg_swcr2; // PLL 控制寄存器 2(分频配置位)。
u32 reg_swcr3; // PLL 控制寄存器 3(使能配置位)。
};
};

struct clk_hw hw; // 这是 Linux CCF 框架要求的标准结构体。
// 内核就是通过这个成员把我们的驱动挂载到时钟树上的。
};

// 内核框架只认识 struct clk_hw,当内核调用我们的函数并传入 hw 指针时,我们通过这个宏就能找回我们自定义的 regmap 和寄存器地址。
static inline struct ccu_common *hw_to_ccu_common(struct clk_hw *hw)
{
return container_of(hw, struct ccu_common, hw);
}
// 从相应的reg_##reg寄存器中取出一个值
#define ccu_read(reg, c, val) regmap_read((c)->regmap, (c)->reg_##reg, val)
// 只修改寄存器中的 mask 位, 不影响其它位
#define ccu_update(reg, c, mask, val) \
regmap_update_bits((c)->regmap, (c)->reg_##reg, mask, val)
// 等待寄存器状态:一直读取寄存器,直到满足条件cond或者超时
#define ccu_poll(reg, c, tmp, cond, sleep, timeout) \
regmap_read_poll_timeout_atomic((c)->regmap, (c)->reg_##reg, \
tmp, cond, sleep, timeout)
  • probe 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static int k1_ccu_probe(struct platform_device *pdev)
{
struct regmap *base_regmap, *lock_regmap = NULL;
struct device *dev = &pdev->dev; // 获取当前设备结构体
int ret;

// 【重点 1】: regmap 是什么?
// 在以前,我们用 ioremap 把物理地址映射为虚拟地址,然后用 readl/writel 读写。
// 现在内核流行用 regmap,它封装了自旋锁等并发保护机制,读写寄存器更安全。
// 这一行直接从设备树节点拿到了这个硬件的寄存器操作句柄。
base_regmap = device_node_to_regmap(dev->of_node);
if (IS_ERR(base_regmap))
return dev_err_probe(dev, PTR_ERR(base_regmap), "failed to get regmap\n");

// 【重点 2】: 针对 PLL 的特殊处理(对应 DTS 里的 spacemit,mpmu 属性)
if (of_device_is_compatible(dev->of_node, "spacemit,k1-pll")) {
// 去设备树里解析 "spacemit,mpmu" 这个属性,拿到指向 mpmu 节点的指针
struct device_node *mpmu = of_parse_phandle(dev->of_node, "spacemit,mpmu", 0);
if (!mpmu)
return dev_err_probe(dev, -ENODEV, "Cannot parse MPMU region\n");

// 为这个 mpmu 节点也申请一个 regmap,因为我们要去那里读 PLL_LOCK 的状态
lock_regmap = device_node_to_regmap(mpmu);
of_node_put(mpmu); // 用完节点指针释放掉
// ... 错误检查省略
}

// 【重点 3】: 开始干活,去注册时钟!
// of_device_get_match_data(dev) 就是拿到我们刚才在 match table 里绑定的那个时钟数组(比如 k1_ccu_apbc_clks)
ret = spacemit_ccu_register(dev, base_regmap, lock_regmap,
of_device_get_match_data(dev));
// ... 错误检查省略
return 0;
}
  • 把数组里的几百个时钟,一个一个向 Linux 的通用时钟框架(CCF)报备。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
static int spacemit_ccu_register(struct device *dev,
struct regmap *regmap, struct regmap *lock_regmap,
const struct spacemit_ccu_clk *clks) // 传进来的时钟数组
{
const struct spacemit_ccu_clk *clk;
int i, ret, max_id = 0;

// 1. 遍历数组,找出最大的时钟 ID 号(为了后面申请内存空间)
for (clk = clks; clk->hw; clk++)
max_id = max(max_id, clk->id);

struct clk_hw_onecell_data *clk_data;

// 2. 申请一个大数组结构体 clk_hw_onecell_data
// 这个结构体用来存放所有实例化后的 clk_hw 指针。这就好比建了一个时钟储物柜。
clk_data = devm_kzalloc(dev, struct_size(clk_data, hws, max_id + 1), GFP_KERNEL);
if (!clk_data)
return -ENOMEM;

// 3. 把储物柜所有格子先初始化为空(-ENOENT)
for (i = 0; i <= max_id; i++)
clk_data->hws[i] = ERR_PTR(-ENOENT);

// 4. 核心循环:真正向内核注册每一个时钟!
for (clk = clks; clk->hw; clk++) {
struct ccu_common *common = hw_to_ccu_common(clk->hw); // 拿到我们自定义的公共结构体
const char *name = clk->hw->init->name; // 时钟名字,比如 "uart0_clk"

// 把从 probe 里搞到的 regmap 赋值给具体的时钟,这样每个时钟内部的 clk_ops
// 执行时(比如 enable),就知道该去哪个地址写寄存器了!
common->regmap = regmap;
common->lock_regmap = lock_regmap;

// 真正向 Linux CCF 核心层注册这个时钟的硬件实体
ret = devm_clk_hw_register(dev, clk->hw);
if (ret) {
dev_err(dev, "Cannot register clock %d - %s\n", i, name);
return ret;
}

// 注册成功后,把这个时钟的指针放进刚才建好的“储物柜”对应的 ID 格子里
clk_data->hws[clk->id] = clk->hw;
}

clk_data->num = max_id + 1;

// 5. 最后一步!把这个“储物柜”发布出去。
// 以后 UART 驱动在设备树里写: clocks = <&syscon_apbc CLK_UART0>;
// 内核就会来到这里,通过 of_clk_hw_onecell_get 函数,在“储物柜”里找到对应 CLK_UART0 的指针,交给 UART 驱动!
return devm_of_clk_add_hw_provider(dev, of_clk_hw_onecell_get, clk_data);
}
  • data 数组:键值对
1
2
3
4
5
struct spacemit_ccu_clk {
int id;
struct clk_hw *hw; // 指向硬件时钟结构体的指针
// 包含了这个时钟的全部物理信息(在哪个寄存器、控制第几位、父时钟是谁、支持哪些操作等)
};
  • 驱动只有一个: k1_ccu_driver, data 数组里的每一个元素是具体的硬件资源

clk 注册完成后申请

  • 绝大部分时钟就是指定使用用途的(硬件电路决定了)
    • 我自己写个 LED 驱动,能申请时钟吗?
      • 如果你想申请 uart0_clk 给 LED 用:技术上可以(只要你拿到它的 ID),但逻辑上很奇怪。你开了这个时钟,UART0 模块内部的逻辑电路就开始跳动了,而你的 LED 硬件并不受这个时钟线控制,所以 LED 不会亮,反而白白增加了 UART0 模块的功耗。
      • 能申请“没指定给谁用”的时钟吗?:通常没有这种“闲置”时钟。不过,芯片里会有一些通用的 GPIO 时钟或辅助时钟(Dummy Clocks)。如果你的硬件(比如 LED)是通过 GPIO 控制的,你申请的其实是 GPIO 控制器的时钟。
  • 如果这时候 UART 驱动要开启 UART0 时钟(clk_enable)! 内核怎么知道去动哪个寄存器的哪个位呢?
    • 内核会去调用这个时钟对应的 操作函数集(clk_ops)。而 ccu_pll.c、ccu_ddn.c、ccu_mix.c 这三个文件里写的,正是一天天具体的“底层干活指令”:
      • ccu_mix.c 里面有 ccu_gate_enable() 函数,里面写的正是如何调用 regmap 往特定的寄存器写 1。
      • ccu_pll.c 里面有 ccu_pll_set_rate() 函数,里面写的正是怎么根据目标频率去配置锁相环的参数。
  • 如果 UART0 一直不用,它的时钟硬件就一直不使用吗?
    • 是的,这正是 Linux CCF 框架存在的意义。默认情况下,为了省电,内核会把不使用的时钟全关了(Gate)。只有当 UART0 的驱动程序调用 clk_prepare_enable() 时,这块硬件才会真正开始震荡输出。一旦驱动卸载或进入休眠,时钟又会被关掉。

详:

  • ccu_pll.c: 锁相环操作
    • PLL 驱动代码中,写完寄存器后不能立即返回。必须有一个 read 循环去检测“锁定状态位(Lock Bit)”,直到硬件确认“我稳了”,驱动才能继续往下走。
  • ccu_ddn.c: 处理复杂的小数分频器
  • ccu_mix.c: 杂合了门控/分频/多路复用的基本操作: 在硬件上,一个时钟寄存器可能既是多路选择器(MUX),又是分频器(DIV),还带个开关(GATE)。这种“多项全能”的硬件位,被抽象成了一种 MIX 类型。

调试

    1. clk_summary
    • cat /sys/kernel/debug/clk/clk_summary
      • 查看时钟层级
      • 开关enable_cnt
      • 最终频率rate :div/pll

clk 相关

一、 硬件组件类

  • OSC (Oscillator / Crystal Oscillator):晶体振荡器。负责产生原始的、固定频率的脉冲信号,是整个芯片所有时钟的源头(源泉)。
  • PLL (Phase Locked Loop):锁相环。负责将低频信号(如 24MHz 晶振)倍频成高频信号(如 1.6GHz),是芯片动力的“心脏”。
  • MUX (Multiplexer):多路选择器。负责在多个时钟源中选择一个输出。比如串口时钟可以选择来自 24MHz 晶振,也可以选择来自某个分频器。
  • DIV (Divider):分频器。负责将高频信号除以一个系数。比如将 100MHz 降频为 50MHz 给外设使用。
  • GATE (Clock Gating):时钟门控。负责时钟的“开关”。关闭不使用的模块时钟可以显著降低芯片功耗(省电的关键)。

二、 K1 芯片特定模块

  • APBS (Application Peripheral Bus Spare/Special):应用外设总线备用/专用单元。在 K1 中,它主要负责管理最底层的 PLL(锁相环) 控制寄存器。
  • APBC (APB Bus Clock Unit):负责 APB 总线上的时钟单元(比如管 UART、PWM、IIC)。
  • MPMU (Main Power Management Unit):主电源管理单元。负责芯片最核心、最基础部分(如 RTC、看门狗、系统总线)的时钟和电源控制。
  • APMU (Application Power Management Unit):应用电源管理单元。负责高性能应用模块(如 CPU、GPU、USB、PCIe)的时钟和电源控制。

三、 软件架构类(驱动开发的“框架”)

  • CCF (Common Clock Framework):通用时钟框架。Linux 内核提供的一套标准接口。它把时钟抽象成树状结构,自动处理父子时钟的依赖关系(比如你开 UART 时钟,CCF 会自动帮你把它的父级 PLL 也开了)。
  • MFD (Multi-Function Device):多功能设备。Linux 驱动的一种架构,用于处理一个硬件块同时具有多种功能(比如一个寄存器基地址里既有时钟控制,又有复位控制,还有电源控制)。
  • Syscon (System Controller):系统控制器。内核中一种特殊的 MFD 设备。它通常代表一组通用的寄存器,可以被多个不同的驱动(时钟驱动、复位驱动、电源驱动)通过 Regmap 共同访问。
  • Regmap (Register Map):寄存器映射层。Linux 提供的一套抽象接口。它给读写寄存器加了一层“保险”,提供自动加锁(防止并发冲突)、缓存等功能,让不同的驱动可以安全地共享同一个硬件控制块。

四.

  • CMU: 中央模块单元: APBC…

硬件上,OSC 产生信号,PLL 升频,MUX/DIV/GATE 分配频率。这些硬件被物理上安置在 APBS/APBC/MPMU/APMU 不同的房间里。
软件上,我们利用 MFD Syscon 架构找到这些房间,通过 Regmap 拿到钥匙,最后按照 CCF 框架的要求,把这些零件组装成一棵“时钟树”。

作者

GoKo Mell

发布于

2026-02-02

更新于

2026-04-14

许可协议

# 相关文章
  1.linux pinctrl and gpio driver
评论

:D 一言句子获取中...