系列文章目录
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 ↩︎


















