系列文章目录
Exynos4412的Linux时钟驱动开发(一)——Exynos4412的时钟管理单元CMU
Exynos4412的Linux时钟驱动开发(二)——clock的初始化(CLK_OF_DECLARE的机制)
Exynos4412的Linux时钟驱动开发(三)——Common Clock Framework(CCF)简介
Exynos4412的Linux5.4.174时钟驱动开发(四)——clk API的调用方法
Exynos4412的Linux5.4.174时钟驱动开发(五)——时钟设备树的修改方法
Exynos4412的Linux5.4.174时钟驱动开发(四)——clk API的调用方法
- 系列文章目录
- 一、时钟使用者clock consumer与时钟提供者clock provider
- 二、clock provider是如何构建的?
- 三、clock consumer是如何构建的
- 四、clk API是如何与底层时钟驱动相关联的
- 五、时钟操作函数的基本原理
- 六、clk API的调用方法
提示:
对于刚刚想了解时钟驱动的初学者而言,不必去深究CCF的代码实现,例如clk_register()函数的代码。但是,一定要知道时钟使用者和时钟提供者这2种设备的设备树写法,以及如何调用clock API。当然,这需要了解一些CCF的基本知识。
一、时钟使用者clock consumer与时钟提供者clock provider
在上一篇博文《Exynos4412的Linux时钟驱动开发(三)——Common Clock Framework(CCF)简介》中提到:在CCF通用时钟框架下,用户编写设备驱动模块和设备树,可以调用clk_get(), clk_put(),clk_prepare(), clk_unprepare(),clk_enable(),clk_disable(),clk_get_rate()等clock API
,就能够操作时钟。例如,Exynos4412驱动开发程序员,调用clk_set_rate()
这个clk API设置PLL频率,实际上是调用samsung_pll35xx_set_rate
函数。
Common Clock Framework(CCF)的概述框图,如下:
实际上,上图黄色部分的CCF接口由2部分组成:第1个部分是CCF core
,充当consumer的角色,为Device driver提供clk API。CCF接口的第2个部分是特定时钟硬件驱动
(底层时钟设备驱动),充当provider的角色,由厂商(manufacturer)编写底层时钟驱动函数。
- 时钟提供者(clock provider)
当时钟节点中有#clock-cells
属性时,说明该节点是clock provider。那么,匹配compatible属性,调用时钟初始化函数,就能够构建clk_hw结构体,并把该节点添加到of_clk_provider
的list链表中。 - 时钟使用者(clock consumer)
当设备节点中有clocks
属性时,说明该节点是clock consumer。那么,就能够根据compatible匹配probe函数,调用devm_clk_get()
函数根据clock-names来查找并获取对时钟生产者的引用,获取时钟生产者的clk结构体。
参考文献:
①Linux common clock framework(2)_clock provider——蜗窝科技 1
②[ARM]修改clock freq on imx8m2、 clock bindings 3
③Linux clock子系统【1】- 对clock时钟框架见解 4
二、clock provider是如何构建的?
主要过程是这样的:
- 设备树中建立时钟控制器的设备节点。示例代码如下:
clock: clock-controller@10030000 {
compatible = "samsung,exynos4412-clock";
reg = <0x10030000 0x18000>;
#clock-cells = <1>;
};
- 根据
compatible
匹配初始化函数
在drivers/clk/samsung/clk-exynos4.c中,有如下代码:
static void __init exynos4412_clk_init(struct device_node *np)
{
exynos4_clk_init(np, EXYNOS4X12);
}
CLK_OF_DECLARE(exynos4412_clk, "samsung,exynos4412-clock", exynos4412_clk_init);
也就是说,会调用exynos4_clk_init()
进行时钟初始化,注册时钟硬件。
- 构建
struct clk_provider
exynos4_clk_init()的部分代码如下:
/* register exynos4 clocks */
static void __init exynos4_clk_init(struct device_node *np,
enum exynos4_soc soc)
{
struct samsung_clk_provider *ctx;
exynos4_soc = soc;
reg_base = of_iomap(np, 0);
[......]
ctx = samsung_clk_init(np, reg_base, CLK_NR_CLKS);
samsung_clk_of_register_fixed_ext(ctx, exynos4_fixed_rate_ext_clks,
ARRAY_SIZE(exynos4_fixed_rate_ext_clks),
ext_clk_match);
exynos4_clk_register_finpll(ctx);
if (exynos4_soc == EXYNOS4210) {
[......]
} else {
if (_get_rate("fin_pll") == 24000000) {
exynos4x12_plls[apll].rate_table =
exynos4x12_apll_rates;
exynos4x12_plls[epll].rate_table =
exynos4x12_epll_rates;
exynos4x12_plls[vpll].rate_table =
exynos4x12_vpll_rates;
}
samsung_clk_register_pll(ctx, exynos4x12_plls,
ARRAY_SIZE(exynos4x12_plls), reg_base);
}
samsung_clk_register_fixed_rate(ctx, exynos4_fixed_rate_clks,
ARRAY_SIZE(exynos4_fixed_rate_clks));
samsung_clk_register_mux(ctx, exynos4_mux_clks,
ARRAY_SIZE(exynos4_mux_clks));
samsung_clk_register_div(ctx, exynos4_div_clks,
ARRAY_SIZE(exynos4_div_clks));
samsung_clk_register_gate(ctx, exynos4_gate_clks,
ARRAY_SIZE(exynos4_gate_clks));
samsung_clk_register_fixed_factor(ctx, exynos4_fixed_factor_clks,
ARRAY_SIZE(exynos4_fixed_factor_clks));
[......]
samsung_clk_of_add_provider(np, ctx);
[......]
可以看出,通过调用samsung_clk_init
构建ctx(结构体类型为struct samsung_clk_provider *ctx)。
然后,通过调用samsung_clk_register_XXX等函数注册PLL、fix_rate、mux、div、gate、fixed_factor等时钟硬件,添加到ctx结构体中。同时,构建了clk_hw
结构体。
- 后续代码还会调用
samsung_clk_of_add_provider()
将控制器节点(device node)加入到of_clk_provider
的list链表中。
类似文献:
①clk子系统 - 代码分析 5
②Linux clock driver(2) clk_register 详解 6
三、clock consumer是如何构建的
来看看 struct clk *clk是怎么创建的?主要过程是这样的:
- 设备树中建立时钟使用者的设备节点。例如pwm的代码如下:
pwm: pwm@139d0000 {
compatible = "samsung,exynos4210-pwm";
reg = <0x139D0000 0x1000>;
interrupts = <GIC_SPI 37 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 38 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 39 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 40 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 41 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clock CLK_PWM>;
clock-names = "timers";
#pwm-cells = <3>;
};
- 根据
compatible
匹配probe函数
在driver/pwm/pwm-samsung.c中可以匹配关键字 “samsung,exynos4210-pwm”。
也就是说,pwm-samsung.c有pwm的驱动程序。在pwm_samsung_probe()
函数中,调用devm_clk_get(&pdev->dev, "timers")
根据clock-names
来查找并获取对时钟生产者的引用,获取时钟生产者的clk结构体。其中形参dev是时钟使用者设备(device for clock consumer),"timers"是时钟使用者的ID号。devm_clk_get
的定义在drivers/clk/clk-devres.c。
至此,时钟使用者(consumer)与时钟生产者(provider)之间建立了联系,使用者已经获取了生产者的寄存器地址,可以对生产者时钟的配置寄存器进行操作。例如,在pwm_samsung_enable
函数中,就调用了readl和writel函数对REG_TCON
寄存器进行读写操作。
四、clk API是如何与底层时钟驱动相关联的
- 问题来了:clock provider与clock consumer的操作是如何关联的呢?例如,
clk_set_rate()
这个clk API是如何与samsung_pll35xx_set_rate
函数相关联的呢?
clk_set_rate()
的定义在drivers/clk/clk.c中。clk_set_rate()
调用clk_core_rate_protect
函数,再调用clk_change_rate
函数,最终调用回调函数指针set_rate
。
- clk_set_rate() ——>clk_core_rate_protect()——>clk_change_rate()—>set_rate
回调函数set_rate
是结构体clk_ops
的成员函数指针,声明在include/linux/clk-provider.h中,由时钟提供者(provider)实现(也就是底层时钟硬件驱动)。
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);
int (*save_context)(struct clk_hw *hw);
void (*restore_context)(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);
[...]
在调用exynos4_clk_init
对时钟初始化时,会调用_samsung_clk_register_pll
注册PLL时钟硬件,这个函数定义在drivers/clk/samsung/clk-pll.c中。这个函数就会把samsung_pll35xx_set_rate
的指针填充到set_rate
中。
这样,samsung_pll35xx_set_rate
就与set_rate
相互关联了,也就回答了刚才提出的问题。
- exynos4_clk_init()—>_samsung_clk_register_pll()—>samsung_pll35xx_set_rate()
代码如下:
static const struct clk_ops samsung_pll36xx_clk_ops = {
.recalc_rate = samsung_pll36xx_recalc_rate,
.set_rate = samsung_pll36xx_set_rate,
.round_rate = samsung_pll_round_rate,
.enable = samsung_pll3xxx_enable,
.disable = samsung_pll3xxx_disable,
};
可以看出,对于Exynos4412的PLL时钟硬件的操作,只有recalc_rate、set_rate、round_rate、enable、disable这5种操作。
五、时钟操作函数的基本原理
操作时钟,本质上是配置clock management unit(CMU)相应的寄存器。以修改Exynos4412的PLL频率为例。
修改Exynos4412的PLL频率是调用底层时钟驱动函数samsung_pll35xx_set_rate
。该函数的定义在drviers/clk/samsung/clk-pll.c中。代码如下:
static int samsung_pll35xx_set_rate(struct clk_hw *hw, unsigned long drate,
unsigned long prate)
{
struct samsung_clk_pll *pll = to_clk_pll(hw);
const struct samsung_pll_rate_table *rate;
u32 tmp;
/* Get required rate settings from table */
rate = samsung_get_pll_settings(pll, drate);
if (!rate) {
pr_err("%s: Invalid rate : %lu for pll clk %s\n", __func__,
drate, clk_hw_get_name(hw));
return -EINVAL;
}
tmp = readl_relaxed(pll->con_reg);
if (!(samsung_pll35xx_mp_change(rate, tmp))) {
/* If only s change, change just s value only*/
tmp &= ~(PLL35XX_SDIV_MASK << PLL35XX_SDIV_SHIFT);
tmp |= rate->sdiv << PLL35XX_SDIV_SHIFT;
writel_relaxed(tmp, pll->con_reg);
return 0;
}
/* Set PLL lock time. */
writel_relaxed(rate->pdiv * PLL35XX_LOCK_FACTOR,
pll->lock_reg);
/* Change PLL PMS values */
tmp &= ~((PLL35XX_MDIV_MASK << PLL35XX_MDIV_SHIFT) |
(PLL35XX_PDIV_MASK << PLL35XX_PDIV_SHIFT) |
(PLL35XX_SDIV_MASK << PLL35XX_SDIV_SHIFT));
tmp |= (rate->mdiv << PLL35XX_MDIV_SHIFT) |
(rate->pdiv << PLL35XX_PDIV_SHIFT) |
(rate->sdiv << PLL35XX_SDIV_SHIFT);
writel_relaxed(tmp, pll->con_reg);
/* Wait until the PLL is locked if it is enabled. */
if (tmp & BIT(pll->enable_offs)) {
do {
cpu_relax();
tmp = readl_relaxed(pll->con_reg);
} while (!(tmp & BIT(pll->lock_offs)));
}
return 0;
}
可以看出,修改PLL时钟频率的主要流程是:
- 获取PMS数值。 调用
samsung_get_pll_settings
函数获取与需要设置频率所对应的PMS数值(就是samsung_pll_rate_table
结构体) - 设置PLL锁定时长。调用
writel_relaxed
函数,向PLL_LOCK寄存器写入参数 - 设置PLL的PMS数值。调用
writel_relaxed
函数,向PLL_CON0寄存器写入参数
可见,操作时钟,就是配置时钟寄存器。
六、clk API的调用方法
调用clk API需要与时钟设备树相一致。设备树的修改方法在下篇博文Exynos4412的Linux5.4.174时钟驱动开发(五)——时钟设备树的修改方法中介绍。
- 在include/linux/clk.h中,声明了clk API,其定义在drivers/clk/clk.c和drivers/clk/clkdev.c中。
在clk.h中,可以看到有很多的条件编译,例如
#ifdef CONFIG_COMMON_CLK
#ifdef CONFIG_HAVE_CLK
[......]
所以,在编译Linux内核之前make menucongfig时,要注意需要在defconfig中使能以上的几个CONFIG来配置内核。
- 调用clk API重点是传递正确的实参。我们可以查阅clk.h,看看API的注释。例如,
clk_set_rate
的声明如下:
/**
* clk_set_rate - set the clock rate for a clock source
* @clk: clock source
* @rate: desired clock rate in Hz
*
* Returns success (0) or negative errno.
*/
int clk_set_rate(struct clk *clk, unsigned long rate);
可以看出,clk_set_rate
的2个形参分别是想要配置时钟的struct clk和频率(rate Hz)。
- 那么,问题来了:怎样获得
struct clk *clk
? - 答案:
首先通过clk_get函数,根据clk节点的名字,获取clk节点。然后,使用clk_set_rate()函数设置clk节点的时钟。clk_set_rate() 函数最终将会调用clk_ops->set_rate() 设置时钟。
还有其他一些获取clk结构体的函数,如下:
/*从一个时钟list链表中以dev或者字符id名称查找一个时钟clk结构体*/
struct clk *clk_get(struct device *dev, const char *id);
struct clk *devm_clk_get(struct device *dev, const char *id);
/*该函数与clk_get函数对应,释放对应时钟结构体,即对结构体的引用计数减1*/
void clk_put(struct clk *clk);
void devm_clk_put(struct device *dev, struct clk *clk);
struct clk *clk_get_sys(const char *dev_id, const char *con_id);
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);
例1:
根据clk节点的name,通过clk_get 获取时钟节点。
clk1 = clk_get(&dev, " snr_clk");
clk2 = clk_get(&dev, " dpi_pixel_clk ");
clk3 = clk_get(&dev, " cvbs_pixel_clk ");
clk4= clk_get(&dev, " vdac_pixel_clk ");
然后,就可以调用clk API来设置各个时钟节点。
clk_disable(clk1);
clk_set_rate(clk2, 24*1000000);
clk_enable(clk3);
例2:
// 确定时钟个数
int nr_pclks = of_count_phandle_with_args(dev->of_node, "clocks",
"#clock-cells");
// 获得时钟
for (i = 0; i < nr_pclks; i++) {
struct clk *clk = of_clk_get(dev->of_node, i);
}
// 使能时钟
clk_prepare_enable(clk);
// 禁止时钟
clk_disable_unprepare(clk);
参考文献:
①设备树中时钟的使用 7
②设备树学习之(三)——Clock8
对链表的理解:
浅析linux设备驱动的clock(时钟)的注册
Linux common clock framework(2)_clock provider——蜗窝科技 ↩︎
[ARM]修改clock freq on imx8m ↩︎
clock bindings ↩︎
Linux clock子系统【1】- 对clock时钟框架见解 ↩︎
clk子系统 - 代码分析 ↩︎
[Linux clock driver(2) clk_register 详解](http ** s://blog.csdn.net/yinjian1013/article/details/78552586) ↩︎
设备树中时钟的使用 ↩︎
设备树学习之(三)——Clock ↩︎