赞
踩
时钟就是 SoC 中的脉搏,由它来控制各个部件按各自的节奏跳动。比如,CPU主频设置,串口的波特率设置,I2S的采样率设置,I2C的速率设置等等。这些不同的 clock 设置,都需要从某个或某几个时钟源头而来,最终开枝散叶,形成一棵时钟树。
Linux的时钟子系统由CCF(common clock framework) 框架管理,CCF向上给其他使用时钟的 IP 提供了通用的时钟接口,向下给驱动开发者提供硬件操作的接口。
这个也是一个consumer、framework、provider的模式。
Provider
是时钟的模块的具体实现者,系统开机时,需要通过驱动的时钟框架向系统注册不同的时钟。Provider,其他需要时钟的模块通过通用接口获取,使能,设置时钟。
Framework
是内核提供的一套通用时钟实现框架,包含了注册和使用的通用接口。
Consumer
是时钟模块的使用者,比如上面提到的 I2C 模块,Uart 模块的等等。
电源管理的两大主要方面就是时钟和电压
在SoC上的模块很多,为了适应不同模块的时钟要求,会形成一课时钟树,如下所示:
根节点一般是 Oscillator(有源振荡器)或者 Crystal(无源振荡器),表示从芯片外部输入的基准时钟,
中间节点有很多种,包括 PLL(锁相环,用于提升频率的),Divider(分频器,用于降频的),Mux(从多个clock path中选择一个),Gate(只能被控制ON/OFF的)。
根据不同时钟的特点,clock framework 将 clock 分为 **Fixed rate、gate、Divider、Mux、Fixed factor、composite **六类,这六类的含义如下:
Fixed rate clock
:Gated clock
:Divider clock
:Muxed clock
:Fixed factor clock
Composite clock
Linux 下 cat /sys/kernel/debug/clk/clk_summary
可以查看当前 Soc 的时钟树
Linux 内核将上面六类设备特点抽象出来,用 struct clk_hw
表示
struct clk_hw { //指向CCF模块中对应 clock device 实例 struct clk_core *core; //clk是访问clk_core的实例 每当consumer通过clk_get对CCF中的clock device(也就是clk_core)发起访问的时候都需要获取一个句柄,也就是clk struct clk *clk; //clock provider driver初始化时的数据,数据被用来初始化clk_hw对应的clk_core数据结构。 const struct clk_init_data *init; }; struct clk_init_data { //该clock设备的名字 const char *name; //clock provider driver 进行的具体的 HW 操作 const struct clk_ops *ops; //描述该clk_hw的拓扑结构 const char * const *parent_names; const struct clk_parent_data *parent_data; const struct clk_hw **parent_hws; u8 num_parents; unsigned long flags; };
以 Fixed rate clock
和 gate clock
为例,它就包含一个 struct clk_hw
结构作为核心:
struct clk_fixed_rate {
// 包含的 clk_hw 结构
struct clk_hw hw;
unsigned long fixed_rate;
unsigned long fixed_accuracy;
u8 flags;
};
struct clk_gate {
struct clk_hw hw;
void __iomem *reg;
u8 bit_idx;
u8 flags;
spinlock_t *lock;
};
由此可以知道:
clock device
设备,都会包含一个 struct clk_hw
结构strutc clk_hw
包含一个重要的结构体成员 const struct clk_init_data *init
,里面包含了注册进入内核的时钟的具体操作方法struct clk_init_data 包含一个重要成员 clk_ops
,里面就是时钟设备的具体操作方法函数:
struct clk_ops { int (*prepare)(struct clk_hw *hw); void (*unprepare)(struct clk_hw *hw); int (*is_prepared)(struct clk_hw *hw); void (*unprepare_unused)(struct clk_hw *hw); int (*enable)(struct clk_hw *hw); void (*disable)(struct clk_hw *hw); int (*is_enabled)(struct clk_hw *hw); void (*disable_unused)(struct clk_hw *hw); unsigned long (*recalc_rate)(struct clk_hw *hw, unsigned long parent_rate); long (*round_rate)(struct clk_hw *hw, unsigned long rate, unsigned long *parent_rate); int (*determine_rate)(struct clk_hw *hw, struct clk_rate_request *req); int (*set_parent)(struct clk_hw *hw, u8 index); u8 (*get_parent)(struct clk_hw *hw); int (*set_rate)(struct clk_hw *hw, unsigned long rate, unsigned long parent_rate); int (*set_rate_and_parent)(struct clk_hw *hw, unsigned long rate, unsigned long parent_rate, u8 index); unsigned long (*recalc_accuracy)(struct clk_hw *hw, unsigned long parent_accuracy); int (*get_phase)(struct clk_hw *hw); int (*set_phase)(struct clk_hw *hw, int degrees); void (*init)(struct clk_hw *hw); int (*debug_init)(struct clk_hw *hw, struct dentry *dentry); }; struct clk_init_data { const char *name; const struct clk_ops *ops; const char * const *parent_names; u8 num_parents; unsigned long flags; };
clock_provider 注册的流程如下图:
这里的注册是指将不同的 clock_device
注册到内核的 CCF 框架中,具体时钟驱动的匹配和注册在下面讲介绍:
clock_device
节点,匹配到不同的 clock_init
函数clock_init
函数中,根据设备树的配置,注册不同类型的 clock_device
到 CCF 中不同的 clock_device 都会提供 register 函数用于注册,举例如下:
// include/linux/clock-provider.h 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); struct clk *clk_register_divider(struct device *dev, const char *name, const char *parent_name, unsigned long flags, void __iomem *reg, u8 shift, u8 width, u8 clk_divider_flags, spinlock_t *lock); struct clk *clk_register_divider_table(struct device *dev, const char *name, const char *parent_name, unsigned long flags, void __iomem *reg, u8 shift, u8 width, u8 clk_divider_flags, const struct clk_div_table *table, spinlock_t *lock); ... clk_register_mux(...); clk_register_mux_table(...); clk_register_fixed_factor(...); clk_register_composite(...);
这些注册函数最终都会通过函数 clk_register
注册到 Common Clock Framework 中,返回为 struct clk
指针。如下所示:
在内核的drivers/clk目录下,可以看到各个芯片厂商对各自芯片 clock 驱动的实现
在设备数中,需要首先定义 clock_provider
的特性节点,以 rk3399 为例,下面的 cru(clock reset unit) 的配置如下:
cru: clock-controller@ff760000 { compatible = "rockchip,rk3399-cru"; reg = <0x0 0xff760000 0x0 0x1000>; rockchip,grf = <&grf>; #clock-cells = <1>; #reset-cells = <1>; assigned-clocks = <&cru PLL_GPLL>, <&cru PLL_CPLL>, <&cru PLL_NPLL>, <&cru ACLK_PERIHP>, <&cru HCLK_PERIHP>, <&cru PCLK_PERIHP>, <&cru ACLK_PERILP0>, <&cru HCLK_PERILP0>, <&cru PCLK_PERILP0>, <&cru ACLK_CCI>, <&cru HCLK_PERILP1>, <&cru PCLK_PERILP1>; assigned-clock-rates = <594000000>, <800000000>, <1000000000>, <150000000>, <75000000>, <37500000>, <100000000>, <100000000>, <50000000>, <600000000>, <100000000>, <50000000>; };
不同属性的的含义如下:
属性 | 含义 |
---|---|
compatible | 驱动的匹配名称,内核通过字段匹配到不同的初始化函数 |
clock-output-names | 输出时钟的名字,当consumer 使用此时钟时,使用该属性的值 |
clock-frequency | 输出时钟的频率 |
clock-cells | 输出的时钟的路数,当#clock-cells为0时,代表仅输出1路时钟,若大于等于1,则代表输出多路时钟,Clock consumers通过编号索引使用。 |
assigned-clocks | 表示该设备需要使用时钟信号,这个属性的值是一个整数数组,每一个元素对应一个时钟信号 |
assigned-clock-rates | 和 assigned-clocks 成对使用,表述输入时钟的频率 |
在 Linux 内核代码中,还需要声明相匹配的时钟驱动,这样在初始化阶段,内核就可以自动匹配 DTS 中的 compatible 字段,向系统注册时钟设备
CLK_OF_DECLARE
宏用于声明与设备树(DeviceTree)绑定的时钟控制器驱动
CLK_OF_DECLARE
宏的定义如下:
#define CLK_OF_DECLARE(name, compat, fn) \
OF_DECLARE_1(clk_of_match, name, compat, fn)
//name:时钟提供者的名称,通常是一个结构体的实例。
//compat:与设备树中兼容性字段匹配的字符串,用于识别时钟控制器。
//fn:一个函数指针,指向用于初始化时钟控制器的函数。
//当内核解析设备树时,它会查找与compat参数匹配的节点,并调用fn参数指定的函数来初始化时钟控制器。这样,内核就可以通过设备树来配置和管理设备的时钟
继续跟踪 CLK_OF_DECLARE 的代码可以看到
#define CLK_OF_DECLARE(name, compat, fn) OF_DECLARE_1(clk, name, compat, fn) #define OF_DECLARE_1(table, name, compat, fn) \ _OF_DECLARE(table, name, compat, fn, of_init_fn_1) #define _OF_DECLARE(table, name, compat, fn, fn_type) \ static const struct of_device_id __of_table_##name \ __used __section(__##table##_of_table) \ = { .compatible = compat, \ .data = (fn == (fn_type)NULL) ? fn : fn } // 如果将 fn 传入参数 hi6220_clk_media,那么展开后可以看到: static const struct of_device_id __of_table_hi6220_clk_media __used __section(__clk_of_table) = { .compatible = compat; .data = fn; // check fn type __init 函数 }
本质上定义了 struct of_device_id
结构,使用 compatible 字段进行匹配
所以 clock 驱动编写的一般步骤是:
struct clk_ops
相关成员函数struct clk_onecell_data
结构体,初始化相关数据struct clk_init_data
结构体,初始化相关数据clk_register
将时钟注册进框架clk_register_clkdev
注册时钟设备of_clk_add_provider
,将 clk provider
存放到 of_clk_provider
链表中管理CLK_OF_DECLARE
声明驱动Clock consumers意为时钟使用者,通常是CPU核心部件或者其他外设。
下面是一个 clock consumer 的配置:
i2c8: i2c@ff3e0000 {
compatible = "rockchip,rk3399-i2c";
reg = <0x0 0xff3e0000 0x0 0x1000>;
assigned-clocks = <&pmucru SCLK_I2C8_PMU>;
assigned-clock-rates = <200000000>;
clocks = <&pmucru SCLK_I2C8_PMU>, <&pmucru PCLK_I2C8_PMU>;
clock-names = "i2c", "pclk";
interrupts = <GIC_SPI 58 IRQ_TYPE_LEVEL_HIGH 0>;
pinctrl-names = "default";
pinctrl-0 = <&i2c8_xfer>;
#address-cells = <1>;
#size-cells = <0>;
status = "disabled";
};
clocks 属性:它代表了设备的时钟源,通常以 phandle + specifier
组合进行引用,比如在本例中使用的时钟是pmcru 中的 SCLK_I2C8_PMU 和 PCLK_I2C8_PMU 作为时钟源
clock-names:这代表了Clock consumers中使用的时钟名字,方便设备驱动代码进行相应的时钟解析
比如使用下面的代码进行解析:
mdev->aclk = devm_clk_get(dev, "aclk");
if (IS_ERR(mdev->aclk)) {
DRM_ERROR("Get engine clk failed.\n");
err = PTR_ERR(mdev->aclk);
mdev->aclk = NULL;
goto err_cleanup;
}
clk_prepare_enable(mdev->aclk);
clock comsumer 的架构如下所示:
主要就是获取和操作 clk,即通过 clock 名称获取 struct clk 指针的过程,由 clk_get、devm_clk_get、clk_get_sys、of_clk_get、of_clk_get_by_name、of_clk_get_from_provider
等接口负责实现,
//启动clock前的准备工作/停止clock后的善后工作。可能会睡眠。 int clk_prepare(struct clk *clk) void clk_unprepare(struct clk *clk) //启动/停止clock。不会睡眠。 static inline int clk_enable(struct clk *clk) static inline void clk_disable(struct clk *clk) //clock频率的获取和设置 static inline unsigned long clk_get_rate(struct clk *clk) static inline int clk_set_rate(struct clk *clk, unsigned long rate) static inline long clk_round_rate(struct clk *clk, unsigned long rate) //获取/选择clock的parent clock static inline int clk_set_parent(struct clk *clk, struct clk *parent) static inline struct clk *clk_get_parent(struct clk *clk) //将clk_prepare和clk_enable组合起来,一起调用。将clk_disable和clk_unprepare组合起来,一起调用 static inline int clk_prepare_enable(struct clk *clk) static inline void clk_disable_unprepare(struct clk *clk)
注意 clk_prepare()
和 clk_enable()
是两个不同的函数
clk_prepare()
函数的作用是准备时钟,它执行时钟的初始化操作,但并不立即启用时钟。这个函数的主要目的是为了确保时钟在启用之前所有的准备工作都已经完成,比如分配必要的资源、设置初始参数等。调用clk_prepare
后,时钟处于“已准备”状态,但还没有开始运行。
clk_enable
函数的作用是启用时钟,它使得时钟开始运行,并提供时钟信号给相关的硬件。在调用clk_enable
之前,必须先调用clk_prepare
来确保时钟已经准备好。一旦时钟被启用,它就可以被硬件设备使用。
这里分析一下 clk_get 函数的实现过程:
struct clk *clk_get(struct device *dev, const char *con_id)
{
const char *dev_id = dev ? dev_name(dev) : NULL;
struct clk *clk;
if (dev) {
// 通过扫描所有 "clock-names" 中的值,和传入的 name 比较,如果相同,获得它的 index(即 clock-names 中的第几个),调用 of_clk_get,取得 clock指针。
clk = __of_clk_get_by_name(dev->of_node, dev_id, con_id);
if (!IS_ERR(clk) || PTR_ERR(clk) == -EPROBE_DEFER)
return clk;
}
return clk_get_sys(dev_id, con_id);
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。