Exynos4412的Linux5.4.174时钟驱动开发(四)——clk API的调用方法

系列文章目录

Exynos4412的Linux时钟驱动开发(一)——Exynos4412的时钟管理单元CMU

Exynos4412的Linux时钟驱动开发(二)——时钟驱动的初始化(CLK_OF_DECLARE的机制)

Exynos4412的Linux时钟驱动开发(三)——Common Clock Framework(CCF)简介

Exynos4412的Linux5.4.174时钟驱动开发(四)——clk API的调用方法

Exynos4412的Linux5.4.174时钟驱动开发(五)——时钟设备树的修改方法



提示:对于刚刚想了解时钟驱动的初学者而言,不必去深究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 imx8m2clock bindings 3

Linux clock子系统【1】- 对clock时钟框架见解 4

二、clock provider是如何构建的?

主要过程是这样的:

  1. 设备树中建立时钟控制器的设备节点。示例代码如下:
		clock: clock-controller@10030000 {
			compatible = "samsung,exynos4412-clock";
			reg = <0x10030000 0x18000>;
			#clock-cells = <1>;
		};
  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()进行时钟初始化,注册时钟硬件。

  1. 构建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结构体。

  1. 后续代码还会调用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是怎么创建的?主要过程是这样的:

  1. 设备树中建立时钟使用者的设备节点。例如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>;
		};
  1. 根据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时钟频率的主要流程是:

  1. 获取PMS数值。 调用samsung_get_pll_settings函数获取与需要设置频率所对应的PMS数值(就是samsung_pll_rate_table结构体)
  2. 设置PLL锁定时长。调用writel_relaxed函数,向PLL_LOCK寄存器写入参数
  3. 设置PLL的PMS数值。调用writel_relaxed函数,向PLL_CON0寄存器写入参数

可见,操作时钟,就是配置时钟寄存器。

六、clk API的调用方法


调用clk API需要与时钟设备树相一致。设备树的修改方法在下篇博文Exynos4412的Linux5.4.174时钟驱动开发(五)——时钟设备树的修改方法中介绍。


  1. 在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来配置内核。

  1. 调用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(时钟)的注册


  1. Linux common clock framework(2)_clock provider——蜗窝科技 ↩︎

  2. [ARM]修改clock freq on imx8m ↩︎

  3. clock bindings ↩︎

  4. Linux clock子系统【1】- 对clock时钟框架见解 ↩︎

  5. clk子系统 - 代码分析 ↩︎

  6. [Linux clock driver(2) clk_register 详解](http ** s://blog.csdn.net/yinjian1013/article/details/78552586) ↩︎

  7. 设备树中时钟的使用 ↩︎

  8. 设备树学习之(三)——Clock ↩︎

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值