Linux CCF Consumer

如果没有时钟消费者来使用时钟,那么提供时钟的驱动程序将不起任何作用。提供者启动的主要目的就是为消费者分配时钟资源,这些时钟被用于各种用途。Linux内核提供了相应的API来完成时钟的使用。消费者需要在其代码中包含。另外,如今主流的驱动代码中都使用了设备树来为消费者提供时钟。在设备树中,时钟消费者的时钟绑定应该跟在提供者后面,因为消费者的指定符号是由提供者的#clock-cells属性决定的,例如以下UART节点的时钟描述

1
2
3
4
5
6
7
8
9
10
uart1: serial@02020000 {
compatible = 'fsl,imx6sx-uart', 'fsl,imx21-uart';
reg = <0x02020000 0x4000>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6SX_CLK_UART_IPG>,
<&clks IMX6SX_CLK_UART_SERIAL>;
clock-names = 'ipg', 'per';
dmas = <&sdma 25 4 0>, <&sdma 26 4 0>;
status = 'disable';
};

以上设备树节点描述了UART需要两路时钟,clocks属性用于声明设备需要的时钟源,关联到提供者的#clock-cells属性。clock-names用于命名时钟,这个名字可以被时钟消费者的驱动程序在代码中匹配时钟

消费者有一个基于底层硬件时钟的精简和可移植的API。接下来,我们将看看消费者驱动程序执行的常见操作及其相关的API

Grabbing and releasing clocks

以下函数用于获取和释放时钟资源

1
2
3
4
5
6
7
8
9
10
11
struct clk *clk_get(struct device *dev, const char *id);
void clk_put(struct clk *clk);

struct clk *devm_clk_get(struct device *dev, const char *id);
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);

dev是使用时钟的设备,id是设备树中时钟的编号,可以在平台相关的头文件中或dts中。clk_get获取成功则返回一个struct clk的结构体指针。对于消费者而言,该结构体可以理解为一个操作时钟的句柄,用来引用一个时钟节点,不需要关心内部的成员结构。其他的所有API都需要该句柄。clk_put释放时钟线。前两个API定义在drivers/clk/clkdev.c中,另外两个API定义在drivers/clk/clk.c中。devm_clk_get/devm_clk_put是资源管理版本的clk_get/clk_putclk_get_sys类似clk_get,不过使用device的name替代device结构。of_clk_getof_clk_get_by_nameof_clk_get_from_provider是设备树相关的接口,直接从相应的DTS node中以index、name等为索引,获取clk

Preparing/unpreparing clocks

以下函数用于准备和去准备时钟资源

1
2
int clk_prepare(struct clk *clk);
void clk_unprepare(struct clk *clk);

这两个函数可能会睡眠,意味着不能在原子上下文中调用。应该总是在clk_enable之前调用clk_prepare,在某些场景中驱动程序在使能或禁用时钟时的上下文是不能睡眠的

Enabling/disabling

当获取到时钟后,要使能/禁用时钟,可以使用以下函数

1
2
static inline int clk_enable(struct clk *clk);
static inline void clk_disable(struct clk *clk);

clk_enable不能睡眠,用于使能时钟,返回0表示成功,clk_disable用于禁用时钟。内核还提供了以下两个API用于合并prepare+enable以及disable/unprepare操作

1
2
static inline int clk_prepare_enable(struct clk *clk);
static inline void clk_disable_unprepare(struct clk *clk);

Rate functions

对于可以更改频率的时钟,可以使用如下函数来设置时钟频率

1
2
3
static inline unsigned long clk_get_rate(struct clk *clk);
static inline int clk_set_rate(struct clk *clk, unsigned long rate);
static inline long clk_round_rate(struct clk *clk, unsigned long rate);

如果clk为空,clk_get_rate会返回0,否则将会返回时钟的频率值,单位为Hz,该函数返回的是在内部缓存的频率。如果时钟设置了CLK_GET_RATE_NOCACHE标志,将会重新计算(通过调用recalc_rate)并返回真实的频率。clk_set_rate用于设置频率,然而并不是所有值都是可设置的,为了检查输入目标值是否支持,需要先使用clk_round_rate

1
rounded_rate = clk_round_rate(clk, target_rate);

返回值是与目标值最为接近的该时钟实际支持的频率,然后将其设置给时钟

1
ret = clk_set_rate(clkp, rounded_rate);

修改时钟频率发生错误可能是以下几种情况

  • 该时钟节点是一个固定时钟
  • 该时钟被多个驱动使用,引用计数大于1
  • 该时钟被多个子节点使用

Parent functions

有些时钟节点是其他节点的子节点,存在一个父子关系,要修改节点的父节点,可以使用以下函数

1
2
int clk_set_parent(struct clk *clk, struct clk *parent);
struct clk *clk_get_parent(struct clk *clk);

clk_set_parent将时钟的父节点改为parentclk_get_parent获取时钟的父节点

示例

以下以一个示例说明消费者API如何使用。首先在DTS中指定某个设备要使用的clock

1
2
3
4
device {
clocks = <&osc 1>, <&ref 0>;
clock-names = "baud", "register";
};

该DTS表示device需要两个时钟,id分别为”baud”和”register”,”baud”来自时钟”osc”的输出1,”register”来自”ref”的输出0。而”osc”和”ref”的定义会在CCF设备树相关文章中介绍

在该设备的probe函数中,可以通过如下方式使用时钟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int xxx_probe(struct platform_device *pdev)
{
int ret;
struct clk *baud_clk;

baud_clk = devm_clk_get(&pdev->dev, “baud”);
if (IS_ERR(clk)) {

}

ret = clk_prepare_enable(baud_clk);
if (ret) {

}
}