Linux 驱动之 CCF 子系统

概述

CCF 主要用于系统 clock 的管理等操作。clk 的种类说明:

如上图所示,时钟源大概可分为如下几种:

  • 提供基础时钟源的晶振(可分为有源晶振、无源晶振两种)
  • 用于倍频的锁相环
  • 用于分频的 divider
  • 用于多路时钟源选择的 mux
  • 用于时钟使能的与门电路等

而在 CCF 子系统的抽象中,这五种均抽象为 clk,但是针对这 5 种类型的时钟也提供了单独的时钟注册函数(也就是对 clk_register 函数的封装,并针对不同的时钟类型定义了不同的结构体)。

在 CCF 子系统中,针对硬件时钟的操作接口,也抽象了对应的结构体 struct clk_ops,包含时钟的使能接口、时钟频率的修改接口等等。而针对上述所说的不同种类的时钟源,其并不需要实现所有 struct clk_ops 中定义的接口。针对“时钟使能的与门电路”而言,仅需要实现 enabel、disable、is_enable 接口即可;针对多路时钟源选择的 mux 而言,则需要实现父时钟源的设置及获取的接口 set_parent、get_parent 等;对于倍频、分频而言,则需要实现时钟频率相关的接口 set_rate、recalc_rate 等。

API(consumer)

clock 获取有关的 API

  • 以 device 指针或者 id 字符串(可以看作 name)为参数,查找 clock。devm_clk_get 使用了 device resource management,可以自动释放
1
2
struct clk *clk_get(struct device *dev, const char *id);
struct clk *devm_clk_get(struct device *dev, const char *id);
dev 和 id 的任意一个可以为空。如果 id 为空,则必须有 device tree 的支持才能获得 device 对应的 clk;
根据具体的平台实现,id 可以是一个简单的名称,也可以 是一个预先定义的、唯一的标识(一般在平台提供的头文件中定义,如 mach/clk.h);
不可以在中断上下文调用
  • get 的反向操作,一般和对应的 get API 成对调用
1
2
void clk_put(struct clk *clk);
void devm_clk_put(struct device *dev, struct clk *clk);
  • 使用 device 的 name 查找 clock
1
struct clk *clk_get_sys(const char *dev_id, const char *con_id);
  • device tree 相关的接口,直接从相应的 DTS node 中,以 index、name 等为索引,获取 clk
1
2
3
struct clk *of_clk_get(struct device_node *np, int index);
struct clk *of_clk_get_by_name(struct device_node *np, const char *name);
struct clk *of_clk_get_from_provider(struct of_phandle_args *clkspec);

clock 控制有关的 API

  • 启动/停止 clock。不会睡眠
1
2
int clk_enable(struct clk *clk);
void clk_disable(struct clk *clk);
  • 启动 clock 前的准备工作/停止 clock 后的善后工作
1
2
inline int clk_prepare(struct clk *clk)
inline void clk_unprepare(struct clk *clk)
  • clock 频率的获取和设置,其中 clk_set_rate 可能会不成功(例如没有对应的分频比),此时会返回错误。如果要确保设置成功,则需要先调用 clk_round_rate 接口,得到和需要设置的 rate 比较接近的那个值
1
2
3
unsigned long clk_get_rate(struct clk *clk);
int clk_set_rate(struct clk *clk, unsigned long rate)
long clk_round_rate(struct clk *clk, unsigned long rate);
  • 获取/选择 clock 的 parent clock
1
2
struct clk *clk_get_parent(struct clk *clk);
int clk_set_parent(struct clk *clk, struct clk *parent);
  • 将 clk_prepare 和 clk_enable 组合起来,一起调用
1
int clk_prepare_enable(struct clk *clk)
  • 将 clk_disable 和 clk_unprepare 组合起来,一起调用
1
void clk_disable_unprepare(struct clk *clk)

prepare/unprepare,enable/disable 的说明

这两套 API 的本质,是把 clock 的启动/停止分为 atomic 和 non-atomic 两个阶段,以方便实现和调用。因此上面所说的“不会睡眠/可能会睡眠”,有两个角度的含义:一是告诉底层的 clock driver,请把可能引起睡眠的操作,放到 prepare/unprepare 中实现,一定不能放到 enable/disable 中;二是提醒上层使用 clock 的 driver,调用 prepare/unprepare 接口时可能会睡眠哦,千万不能在 atomic 上下文(例如中断处理中)调用哦,而调用 enable/disable 接口则可放心。

另外,clock 的开关为什么需要睡眠呢?这里举个例子,例如 enable PLL clk,在启动 PLL 后,需要等待它稳定。而 PLL 的稳定时间是很长的,这段时间要把 CPU 交出(进程睡眠),不然就会浪费 CPU。

最后,为什么会有合在一起的 clk_prepare_enable/clk_disable_unprepare 接口呢?如果调用者能确保是在 non-atomic 上下文中调用,就可以顺序调用 prepare/enable、disable/unprepared,为了简单,framework 就帮忙封装了这两个接口。

其它接口

  • 注册/注销 clock rate 改变的通知。例如某个 driver 关心某个 clock,期望这个 clock 的 rate 改变时,通知到自己,就可以注册一个 notify
    1
    2
    int clk_notifier_register(struct clk *clk, struct notifier_block *nb);
    int clk_notifier_unregister(struct clk *clk, struct notifier_block *nb);

通用 API 的使用说明

  • 首先,在 DTS(device tree source)中,指定 device 需要使用的 clock,如下:

    1
    2
    3
    4
    5
    /* DTS */
    device {
    clocks = <&osc 1>, <&ref 0>;
    clock-names = "baud", "register";
    };

    该 DTS 的含义是:

    device 需要使用两个 clock,“baud”和“regitser”,由 clock-names 关键字指定;

    baud 取自“osc”的输出 1,register 取自“ref”的输出 0,由 clocks 关键字指定。

  • 系统启动后,device tree 会解析 clock 有关的关键字,并将解析后的信息放在 platform_device 相关的字段中

  • 具体的 driver 可以在 probe 时,以 clock 的名称(不提供也行)为参数,调用 clk get 接口,获取 clock 的句柄,然后利用该句柄,可直接进行 enable、set rate 等操作,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* driver */
int xxx_probe(struct platform_device *pdev)
{
struct clk *baud_clk;
int ret;

baud_clk = devm_clk_get(&pdev->dev, “baud”);
if (IS_ERR(clk)) {

}

ret = clk_prepare_enable(baud_clk);
if (ret) {

}
}

数据结构(provider)

CCF 实现的数据结构如下:原图

  • clk_hw: 为硬件时钟源的逻辑抽象可以理解为 clk provider

  • clk_core: 可以理解为 clk provider 的 driver,包含了 clk provider 的访问方法、时钟频率设定、支持的时钟频率、父时钟源的个数以及父时钟源的名称等内容

  • clk: 可以理解 clk consumer,包含了指向 clk provider 的指针、使用者的设备名称、使用者所定义的时钟别名(con_id)

  • clk_ops: 主要是 clk provider 的操作接口,包含 prepare 与 unprepare 操作接口(这两个接口执行时允许 sleep,主要是用的互斥锁)、enable 与 disable 接口(这两个接口执行时不允许 sleep,里面使用了自旋锁),也包含了速率设置接口、重新计算速率接口、parent 设置与获取接口等等,通过这些接口可实现时钟频率的修改、父时钟源的选择、时钟的使能与否等功能

  • clk_init_data: 描述该 clock 的静态数据,clock provider 负责把系统中每个 clock 的静态数据准备好,然后交给 clock framework 的核心逻辑,剩下的事情,clock provider 就不用操心了。这个过程,就是 clock driver 的编写过程

ccf 的驱动框架也是与设备驱动模型类似的结构,clk_core 用于抽象设备树驱动的方法类似于 device_driver 而 clk 则更像是一个 device,在 soc 内部有很多控制器,这些控制器都挂载在时钟树下面,这些控制器就是 consumer,他们会用到 clk 数据结构用于使能失能时钟,以及设置时钟频率等操作。而 clk_core 结构体则抽象了时钟树中的每一个节点组织成树状结构来描述整个时钟树。下面看下 clk_core 在内核中的组织关系结构图,如下:原图

实现

clock 分类及 register

根据 clock 的特点,clock framework 将 clock 分为 fixed rate、gate、devider、mux、fixed factor、composite 六类,每一类 clock 都有相似的功能、相似的控制方式,因而可以使用相同的逻辑 s,统一处理,这充分体现了面向对象的思想。

  • fixed rate cloc: 这一类 clock 具有固定的频率,不能开关、不能调整频率、不能选择 parent、不需要提供任何的 clk_ops 回调函数,是最简单的一类 clock。 可以直接通过 DTS 配置的方式支持,clock framework core 能直接从 DTS 中解出 clock 信息,并自动注册到 kernel,不需要任何 driver 支持。 clock framework 使用 struct clk_fixed_rate 结构抽象这一类 clock,另外提供了一个接口,可以直接注册 fixed rate clock,如下:
1
2
3
4
5
6
7
8
9
10
struct clk_fixed_rate {
struct clk_hw hw;
unsigned long fixed_rate;
unsigned long fixed_accuracy;
unsigned long flags;
};
...
struct clk *clk_register_fixed_rate(struct device *dev, const char *name,
const char *parent_name, unsigned long flags,
unsigned long fixed_rate);
  • gate clock
    这一类 clock 只可开关(会提供。enable/.disable 回调),可使用下面接口注册:
1
2
3
4
struct clk *clk_register_gate(struct device *dev, const char *name,
const char *parent_name, unsigned long flags,
void __iomem *reg, u8 bit_idx,
u8 clk_gate_flags, spinlock_t *lock);
  • divider clock
    这一类 clock 可以设置分频值(因而会提供。recalc_rate/.set_rate/.round_rate 回调),可通过下面两个接口注册:
1
2
3
4
5
#define clk_register_divider(dev, name, parent_name, flags, reg, shift, width, \
clk_divider_flags, lock) \
clk_register_divider_table((dev), (name), (parent_name), (flags), \
(reg), (shift), (width), \
(clk_divider_flags), NULL, (lock))
  • mux clock
    这一类 clock 可以选择多个 parent,因为会实现。get_parent/.set_parent/.recalc_rate 回调,可通过下面两个接口注册:
1
2
3
4
5
#define clk_register_mux(dev, name, parent_names, num_parents, flags, reg,    \
shift, width, clk_mux_flags, lock) \
clk_register_mux_table((dev), (name), (parent_names), (num_parents), \
(flags), (reg), (shift), BIT((width)) - 1, \
(clk_mux_flags), NULL, (lock))
  • fixed factor clock
    这一类 clock 具有固定的 factor(即 multiplier 和 divider),clock 的频率是由 parent clock 的频率,乘以 mul,除以 div,多用于一些具有固定分频系数的 clock。由于 parent clock 的频率可以改变,因而 fix factor clock 也可该改变频率,因此也会提供。recalc_rate/.set_rate/.round_rate 等回调。
1
2
3
struct clk *clk_register_fixed_factor(struct device *dev, const char *name,
const char *parent_name, unsigned long flags,
unsigned int mult, unsigned int div);
  • composite clock
    顾名思义,就是 mux、divider、gate 等 clock 的组合,可通过下面接口注册:
1
2
3
4
5
6
struct clk *clk_register_composite(struct device *dev, const char *name,
const char * const *parent_names, int num_parents,
struct clk_hw *mux_hw, const struct clk_ops *mux_ops,
struct clk_hw *rate_hw, const struct clk_ops *rate_ops,
struct clk_hw *gate_hw, const struct clk_ops *gate_ops,
unsigned long flags);

上面这些注册接口的实现逻辑

clk 注册流程如下:原图

驱动针对不同类型的 clock 节点提供了几种不同的注册函数,驱动开发人员一般不需要自己编写对应的驱动而是尽可能调用系统提供的接口,而对于系统而言也是尽可能的复用函数,这些注册函数都是通过调用 clk_hw_register 函数来实现自己的功能。

这里有一个 map 映射没有涉及,ccf 子系统提供了 clk provider 的 map,这种 map 机制可以理解为定义了 clk consumer 与 clk_provider 的映射关系,即该 clk provider 可以给哪些 clk consumer 提供时钟(如针对非设备树模式,则定义了 clk consumer 的设备名称、clk consumer 的时钟使用名称)。

系统启动阶段对设备树的处理

clk 初始化流程如下:原图

上面介绍了 clk 的注册流程,在上图中展示了系统启动过程中,解析设备树按照默认配置来设置系统时钟树的流程,下面给出详细说明:

  • 通常在 Clock 驱动中会有一个 CLK_OF_DECLARE 宏,该宏用于声明一个 struct of_device_id 数据结构,并且最终在链接的时候会将数据结构链接在一个叫做__of_clk_table 的段中,这个段的定义位于 vmlinux.lds.h 文件中。
  • 在内核启动过程中,最终会调用到 of_clk_init 函数,在该函数中会去访问__of_clk_table 段中的内容,由于此时内核已经调用了 unflatten_device_tree() 接口,完成了 Device Tree 的解析过程,在 of_clk_init 函数中,会根据 struct of_device_id 中的内容去匹配 Device Node。
  • 最终调用__of_clk_table 中匹配上的 struct of_device_id 中的函数,也就是 Clock 驱动的入口函数了,此时顺着这个入口函数,可以完成一系列的操作,比如 clk_ops 中的函数实现,注册进 CCF 框架等。
  • 当驱动完成注册之后,Consumer Driver 的 Device Node 和 Clock Provider 建立了联系,Consumer Driver 便可通过 CCF 接口来操作时钟了。

参考文献

http://www.wowotech.net/pm_subsystem/clk_overview.html
http://www.wowotech.net/pm_subsystem/clock_provider.html
http://www.wowotech.net/pm_subsystem/clock_framework_core.html
https://blog.csdn.net/lickylin/article/details/107729156?spm=1001.2014.3001.5501
https://www.qter.org/portal.php?mod=view&aid=7774