linux通用时钟框架(CCF)

前言

linux 内核版本 v4.19
嵌入式平台rv1109 , 文中代码出处。

CCF 介绍

提供者和消费者的概念

CCF背后的主要思想是统一和抽象分布在不同SoC时钟驱动程序中的类似代码。这种标准化的方法引入了时钟提供者时钟消费者的概念:

  • 提供者是Linux内核驱动程序,它连接到框架并提供对硬件的访问,从而根据SoC数据表提供(使这些对消费者可用)时钟树(由于可以转储整个时钟树);

  • 消费者是通过公共API访问框架的Linux内核驱动程序或子系统;

也就是说,驱动程序既可以是提供者,也可以是消费者(然后它可以使用它提供的一个或多个时钟,也可以使用其他人提供的一个或多个时钟)。

CCF 框架组成关系

在使用CCF之前,需要通过CONFIG_COMMON_CLK选项将其支持加入内核,CCF 本身分为两部分:

  • 公共时钟框架核心:这是框架的核心,当您添加新的驱动程序并提供struct clk的公共定义时,不应该修改它,它统一了框架级代码和传统的依赖于平台的实现,这些实现过去常常在各种平台上复制。这一半还允许我们将消费者接口(也称为clk实现)包装在结构体clk_ops之上,该结构体必须由每个时钟提供程序提供。

  • 特定于硬件的那一半:它的目标是必须为每个新的硬件时钟写入的时钟设备。这需要驱动程序提供clk_ops结构体,该结构体对应于用于对底层硬件进行操作的回调函数(这些回调函数由时钟的核心实现调用),以及包装和抽象时钟硬件的相应硬件特定结构。

这两部分通过struct clk_hw连接在一起。

CCF 程序关键结构体

struct clk_hw是CCF中每种时钟类型的基本结构。它可以看作是一个句柄,用于从struct clk遍历到相应的特定于硬件的结构。

include/linux/clk-provider.h
struct clk_hw {
	struct clk_core *core;
	struct clk *clk; //时钟的消费者表示,每个消费者API都依赖于这个结构体。
	const struct clk_init_data *init;
};
  • clk 它由时钟框架分配和维护,并在需要时提供给时钟使用者。每当消费者通过clk_get启动对CCF中的时钟设备(即clk_core)的访问时,它都需要获得一个句柄,即clk.

  • init 在初始化底层时钟提供程序驱动程序的过程中,调用clk_register()接口来注册时钟硬件。在此之前,需要设置一些初始数据,这些初始数据被抽象为struct clk_init_data数据结构。在初始化过程中,clk_init_data中的数据用于初始化clk_core数据结构,该数据结构对应于clk_hw。初始化完成后,clk_init_data没有任何意义。

CCF 重要组成

注册时钟

struct clk *clk_register(struct device *dev, struct clk_hw *hw)
int clk_hw_register(struct device *dev, struct clk_hw *hw)
  • 调用clk_hw_register()(它在内部调用__clk_core_init()来初始化时钟)时,如果这个时钟有一个有效的父时钟,它将在父时钟的子列表中结束。另一方面,如果num_parent为0,则将其放在clk_root_list中。否则,它将挂起在clk_orphan_list中,这意味着它没有有效的父节点。
  • 此外,每当一个新的时钟被clk_init时,CCF将遍历clk_orphan_list(孤儿时钟列表),并重新父化当前正在初始化的时钟的子时钟。这就是CCF保持时钟树与硬件拓扑一致的方式。
  • 另一方面,struct clk是时钟设备的消费者端实例。
    基本上,所有用户对时钟设备的访问都会创建一个结构clk类型的访问句柄。当不同的用户访问相同的时钟设备时,尽管在底层使用相同的struct clk_core实例,但他们访问的句柄(struct clk)是不同的。

clk_hw_register 封装了clk_register,只是为了兼容,推荐使用clk_hw_register, (不应该直接使用clk_reregister(),因为clk_reregister返回结构clk。这可能会导致混乱,并打破提供者和使用者接口之间的严格分离)。

clk_hw_register / clk_register 的实现逻辑如下(clk/clk.c 代码略):

  • 分配struct clk_core空间(clk_hw->core):
  1. 根据struct clk_hw指针提供的信息初始化clk的字段名称、ops、hw、flags、num_parents和parents_name。
  2. 调用内核接口__clk_core_init()来执行后续初始化操作,包括构建时钟树层次结构。
  • 通过内部内核接口clk_create_clk()分配struct clk空间(clk_hw->clk),并返回此结构clk变量。

CCF框架负责建立整个抽象时钟树的树结构并维护其数据,因此它通过drivers/clk/clk.c中定义的两个静态链表来实现这一点,如下所示:

static HLIST_HEAD(clk_root_list);
static HLIST_HEAD(clk_orphan_list);

每当您在时钟hw上调用clk_hw_register()(它在内部调用__clk_core_int()来初始化时钟)时,如果该时钟有一个有效的父级,它将最终出现在父级的子级列表中。另一方面,若num_parent为0,则将其放置在clk_root_list中。否则,它将挂在clk_orpan_list中,这意味着它没有有效的父级。此外,每次新的clk为clk_init时,CCF都会遍历clk_orpan_list(孤立时钟的列表),并为当前正在初始化的时钟的子级重新设置父级。这就是CCF保持时钟树与硬件拓扑一致的方式。

未使用设备树的时钟注册操作

  1. 由于知道clk_register()的目的只是注册到公共时钟框架,因此消费者无法知道如何定位clk。因此,对于底层时钟提供程序驱动程序,除了调用clk_register()函数以注册到公共时钟框架之外,还必须在clk_register()之后立即调用clk_register_clkdev(),以便用名称绑定时钟(否则,时钟使用者将不知道如何定位时钟)。因此,内核使用struct clk_lookup(顾名思义)来查找可用的时钟。
  2. 为了使用基于hw的API强制实现提供者和使用者代码之间的分离,代码中的clk_hw_register_clkdev()和clk_register_clkdev。

clk_lookup 结构

struct clk_lookup {
	struct list_head	node;
	const char		*dev_id;
	const char		*con_id;
	struct clk		*clk;
	struct clk_hw		*clk_hw;
};

dev_id和con_id用于识别/查找适当的clk。这个clk是相应的底层时钟。node是挂在全局时钟列表中

clk_hw_register_clkdev --> _clkdev_add

static void __clkdev_add(struct clk_lookup *cl)
{
	mutex_lock(&clocks_mutex);
	list_add_tail(&cl->node, &clocks);
	mutex_unlock(&clocks_mutex);
}

void clkdev_add(struct clk_lookup *cl)
{
	if (!cl->clk_hw)
		cl->clk_hw = __clk_get_hw(cl->clk);
	__clkdev_add(cl);
}
EXPORT_SYMBOL(clkdev_add);

使用设备树的时钟注册操作

使用设备树后,每个时钟提供程序都成为DTS中的一个节点;也就是说,每个clk在其对应的设备树中都有一个设备节点。在这种情况下,与其将clk和名称绑定在一起,不如通过一个新的数据结构struct of_clk_provider来绑定clk和你的设备节点。具体数据结构如下:

struct of_clk_provider {
	struct list_head link;//Entry in global list of clock providers

	struct device_node *node;//表示时钟设备的DTS节点
	struct clk *(*get)(struct of_phandle_args *clkspec, void *data);
	struct clk_hw *(*get_hw)(struct of_phandle_args *clkspec, void *data);
	void *data;
};

of_clk_provider 注释中的 “Entry” 有 “条目,账目,记录” 的意思,可理解为条目。
get_hw是时钟的回调。对于设备(使用者),通过clk_get() 调用它来返回与节点相关联的时钟或NULL。这里后会代码会讲解
get 为老的API(兼容老的驱动代码),与get_hw功能一样。

CCF 引入了一个新的list 帮助管理所有DTS节点和时钟之间的对应关系

static LIST_HEAD(of_clk_providers);

of_clk_add_hw_provider 代替 clk_hw_register_clkdev

/**
 * of_clk_add_hw_provider() - Register a clock provider for a node
 * @np: Device node pointer associated with clock provider
 * @get: callback for decoding clk_hw
 * @data: context pointer for @get callback.
 */
int of_clk_add_hw_provider(struct device_node *np,
			   struct clk_hw *(*get)(struct of_phandle_args *clkspec,
						 void *data),
			   void *data)

这在后面的例子中会看到of_clk_add_hw_provider 的使用

设备树分析与使用
clocks 分析举例

ti芯片的cdce706 为例

	clocks {
		clk54: clk54 {
			#clock-cells = <0>;
			compatible = "fixed-clock";
			clock-frequency = <54000000>;
			clock-output-names = 'osc';
		};
	};
	
	i2c0: i2c-master@0d090000 {
			......
			cdce706: clock-synth@69 {
				compatible = "ti,cdce706";
				#clock-cells = <1>;
				reg = <0x69>;
				clocks = <&clk54>;
				clock-names = "clk_in0";
			};
		};

时钟是通过clocks属性分配给使用者的,时钟提供者也可以是消费者。clk54是一个固定时钟;cdce706是一个时钟提供者,它也使用clk54(在clocks属性中作为phandle给出)。

时钟提供程序节点需要指定的最重要的信息是 #clock-cells 属性,它决定了时钟说明符的长度:

  1. 当它为0时,这意味着只需要将该提供程序的phandle属性提供给使用者
  2. 当它为1(或更大)时,这意味着phandle属性具有多个输出,并且需要提供附加信息,例如指示需要使用什么输出的ID。此ID直接由立即值表示。最好在头文件中定义系统中所有时钟的ID。设备树可以包括这个头文件,例如clocks=<&clock CLK_SPI0>,其中CLK_SPI0是在头文件中定义的宏。
时钟输出的名称

让我们来看看时钟输出名称。这是一个可选但推荐的属性,应该是与输出(即提供的)时钟线名称相对应的字符串列表

osc {
 	#clock-cells = <1>;
 	clock-output-names = 'ckout1', 'ckout2'; //注意这里的clock-output-names  output 名字固定,表明输出时钟
};

定义了一个设备,该设备提供两条时钟输出线,分别命名为ckout1和ckout2

消费者的节点不应直接使用这些clout1 等名称来引用这些时钟线,应该使用适应的时钟说明符(即 提供者的#clock-cells ),根据设备的需求命名输入时钟线,如下

device {
 	clocks = <&osc 0>, <&osc 1>;	
 	clock-names = 'baud', 'register'; //消费者时钟名clock-names
};

当一条时钟线被分配给一个消费者设备时,当该消费者的驱动程序调用clk_get(或用于获取时钟的类似接口)时,该接口调用of_clk_get_by_name(),后者反过来调用__of_clk_get()。

static struct clk *__of_clk_get(struct device_node *np, int index,
			       const char *dev_id, const char *con_id)
{
	struct of_phandle_args clkspec;
	struct clk *clk;
	int rc;

	rc = of_parse_phandle_with_args(np, "clocks", "#clock-cells", index,
					&clkspec);
	if (rc)
		return ERR_PTR(rc);

	clk = __of_clk_get_from_provider(&clkspec, dev_id, con_id, true);
	of_node_put(clkspec.np);

	return clk;
}

关注 一下of_parse_phandle_with_args 函数中的clkspec

//of.h
#define MAX_PHANDLE_ARGS 16
struct of_phandle_args {
	 struct device_node *np;
	 int args_count;
	 uint32_t args[MAX_PHANDLE_ARGS];
};

在struct of_phandle_args中,np元素是指向与phandle属性相对应的节点的指针。在时钟说明符中,它将是时钟提供程序的设备树节点。args_count元素对应于说明符中phandle后面的单元格数。它可以用于遍历args,args是一个包含有内容的参数的数组。

of_parse_phandle_with_args 的示例说明

上面的文字描述难以说明白,下面用代码举例说明of_parse_phandle_with_args

phandle1: node1 {
	 #gpio-cells = <2>;
};

phandle2: node2 {
 	#list-cells = <1>;
};

node3 {
 list = <&phandle1 1 2 &phandle2 3>;
};

//或者下面的写法
node3 {
 list = <&phandle1 1 2>, <&phandle2 3>;
}

这里,node3是一个消费者。要获得指向node2节点的设备节点指针,可以调用of_parse_phandle_with_args(node3 ,‘list’,‘#list-cells’,1,&args);。由于&phandle2位于列表(list)中的索引1(从0开始),因此我们在index参数中指定了1。

同样,要获取node1节点的关联设备节点,可以调用of_parse_phandle_with_args(node3,‘list’,‘#gpio-cells’,0,&args);。对于第二种情况,如果我们查看args输出参数,我们将看到args->np对应于node3,args->args_count的值为2(因为这个说明符需要2个参数),args–>args[0]的值为1,args->args[1]的值为2中,这将对应于说明符中的2个参数。

__of_clk_get_from_provider 的分析

对于of_parse_phandle_with_args 的理解到这里,接下来看一下 __of_clk_get_from_provider

struct clk *__of_clk_get_from_provider(struct of_phandle_args *clkspec,
				       const char *dev_id, const char *con_id,
				       bool with_orphans)
{
	struct of_clk_provider *provider;
	struct clk *clk = ERR_PTR(-EPROBE_DEFER);
	struct clk_hw *hw;

	if (!clkspec)
		return ERR_PTR(-EINVAL);

	/* Check if we have such a provider in our array */
	mutex_lock(&of_clk_mutex);
	list_for_each_entry(provider, &of_clk_providers, link) {
		if (provider->node == clkspec->np) {
			hw = __of_clk_get_hw_from_provider(provider, clkspec);
			clk = __clk_create_clk(hw, dev_id, con_id,
					       with_orphans);
		}

		if (!IS_ERR(clk)) {
			if (!__clk_get(clk)) {
				__clk_free_clk(clk);
				clk = ERR_PTR(-ENOENT);
			}

			break;
		}
	}
	mutex_unlock(&of_clk_mutex);

	return clk;
}

这个函数只是遍历时钟提供程序(在of_clk_providers列表中),当找到合适的提供程序时,它会调用作为of_clk_add_provider()的第二个参数给定的底层回调,以解码底层时钟。这里,of_parse_phandle_with_args()返回的时钟说明符作为参数给出(上文中的of_phandle_args 结构体参数)。当你必须向其他设备公开时钟提供程序时,我们不得不使用_clk_add_hw_provider()。作为第二个参数,每当使用者调用clk_get()时,该接口接受CCF用来解码底层时钟的回调。

此回调的结构如下中的clk_src_get 回调

int of_clk_add_provider(struct device_node *np,
			struct clk *(*clk_src_get)(struct of_phandle_args *clkspec,
						   void *data),
			void *data)
CCF 时钟获取机制的总结

当消费者调用clk_get()时,CCF内部调用__of_clk_get(struct device_node *np, int index,const char *dev_id, const char *con_id)。这是作为该使用者的设备节点属性的第一个参数给出的,因此CCF可以获取时钟说明符并找到与提供程序对应的设备节点特性(通过of_parse_phandle_with_args() )。然后它以of_phandle_args的形式返回这个值。这个of_phandle_args对应于时钟说明符,并作为参数提供给__of_clk_ get_from_provider(),该参数只是将of_phandle_args(即of_phandler_args->np)中提供程序的设备节点属性与of_clk_provider中存在的属性进行比较,后者是设备树时钟提供程序的列表。一旦找到匹配,就会调用该提供程序的相应of_clk_provider->get()回调,并返回底层时钟。

尽管可以编写自己的回调,但CCF框架提供了两个通用的解码回调,涵盖了大多数情况。它们分别是of_clk_src_onecell_get() 和of_clk_src_simple_get()

of_clk_hw_simple_get() 用于简单的时钟提供程序,其中除了时钟本身之外,不需要特殊的上下文数据结构,例如时钟gpio驱动程序(在drivers/clk/clk-gpio.c中)。

struct clk_hw *of_clk_hw_simple_get(struct of_phandle_args *clkspec, void *data)
{
 	return data;
}
EXPORT_SYMBOL_GPL(of_clk_hw_simple_get);

of_clk_src_onecell_get 的分析略

编写时钟提供者驱动

略… 后面的文章中再写

实际使用示例

前面提到的 of_clk_add_hw_provider 使用示例,下面为rk809 pmic 中clk的部分代码

struct rk808_clkout {
	struct rk808 *rk808;
	struct clk_hw		clkout1_hw;
	struct clk_hw		clkout2_hw;
};

static struct clk_hw *
of_clk_rk808_get(struct of_phandle_args *clkspec, void *data)
{
	struct rk808_clkout *rk808_clkout = data;
	unsigned int idx = clkspec->args[0];

	if (idx >= 2) {
		pr_err("%s: invalid index %u\n", __func__, idx);
		return ERR_PTR(-EINVAL);
	}

	return idx ? &rk808_clkout->clkout2_hw : &rk808_clkout->clkout1_hw;
}

static int rk808_clkout_probe(struct platform_device *pdev)
{
	struct rk808 *rk808 = dev_get_drvdata(pdev->dev.parent);
	struct i2c_client *client = rk808->i2c;
	struct device_node *node = client->dev.of_node;
	struct clk_init_data init = {};
	struct rk808_clkout *rk808_clkout;
	int ret;

	rk808_clkout = devm_kzalloc(&client->dev,
				    sizeof(*rk808_clkout), GFP_KERNEL);
	if (!rk808_clkout)
		return -ENOMEM;

	rk808_clkout->rk808 = rk808;

	init.parent_names = NULL;
	init.num_parents = 0;
	init.name = "rk808-clkout1";
	init.ops = &rk808_clkout1_ops;
	rk808_clkout->clkout1_hw.init = &init;

	/* optional override of the clockname */
	of_property_read_string_index(node, "clock-output-names",
				      0, &init.name);

	ret = devm_clk_hw_register(&client->dev, &rk808_clkout->clkout1_hw);
	if (ret)
		return ret;

	init.name = "rk808-clkout2";
	init.ops = rkpmic_get_ops(rk808->variant);
	rk808_clkout->clkout2_hw.init = &init;

	/* optional override of the clockname */
	of_property_read_string_index(node, "clock-output-names",
				      1, &init.name);

	ret = devm_clk_hw_register(&client->dev, &rk808_clkout->clkout2_hw);
	if (ret)
		return ret;

	return of_clk_add_hw_provider(node, of_clk_rk808_get, rk808_clkout);
}

static int rk808_clkout_remove(struct platform_device *pdev)
{
	struct rk808 *rk808 = dev_get_drvdata(pdev->dev.parent);
	struct i2c_client *client = rk808->i2c;
	struct device_node *node = client->dev.of_node;

	of_clk_del_provider(node);

	return 0;
}

static struct platform_driver rk808_clkout_driver = {
	.probe = rk808_clkout_probe,
	.remove = rk808_clkout_remove,
	.driver		= {
		.name	= "rk808-clkout",
	},
};

module_platform_driver(rk808_clkout_driver);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值