• 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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 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)
    
    • 1
    • 2
    • 调用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);
    
    • 1
    • 2

    每当您在时钟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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

    使用设备树后,每个时钟提供程序都成为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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

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

    static LIST_HEAD(of_clk_providers);
    
    • 1

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这在后面的例子中会看到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";
    			};
    		};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    时钟是通过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 名字固定,表明输出时钟
    };
    
    • 1
    • 2
    • 3
    • 4

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

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

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

    当一条时钟线被分配给一个消费者设备时,当该消费者的驱动程序调用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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    关注 一下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];
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在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>;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这里,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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    这个函数只是遍历时钟提供程序(在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)
    
    • 1
    • 2
    • 3
    • 4
    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
  • 相关阅读:
    直方图统计增强方法
    asyncio.sleep
    Ubuntu screen命令,使终端在断开或关闭后依然存在
    java高级篇 Mybatis-Plus
    计算机视觉40例之案例13车牌识别
    Bear and Prime 100(交互题)
    无网络机器上,win下vscode客户端通过ssh连接linux服务器
    05 robotFrameWork+selenium2library 一维数组的使用
    5G NR Polar码简介(一)
    【C#每日一记】C#中常用的特性
  • 原文地址:https://blog.csdn.net/liuyinggui163/article/details/132765113