目录
前言
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):
- 根据struct clk_hw指针提供的信息初始化clk的字段名称、ops、hw、flags、num_parents和parents_name。
- 调用内核接口__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保持时钟树与硬件拓扑一致的方式。
未使用设备树的时钟注册操作
- 由于知道clk_register()的目的只是注册到公共时钟框架,因此消费者无法知道如何定位clk。因此,对于底层时钟提供程序驱动程序,除了调用clk_register()函数以注册到公共时钟框架之外,还必须在clk_register()之后立即调用clk_register_clkdev(),以便用名称绑定时钟(否则,时钟使用者将不知道如何定位时钟)。因此,内核使用struct clk_lookup(顾名思义)来查找可用的时钟。
- 为了使用基于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 属性,它决定了时钟说明符的长度:
- 当它为0时,这意味着只需要将该提供程序的phandle属性提供给使用者。
- 当它为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);