Linux设备驱动开发---设备树的概念

文章目录

  • 1 设备树机制
    • 命名约定
    • 别名、标签和phandle
    • DT编译器
  • 2 表示和寻址设备
    • SPI和I2C寻址
    • 平台设备寻址
  • 3 处理资源
    • 提取特定应用数据
      • 文本字符串
      • 单元格和无符号的32位整数
      • 布尔
      • 提取并分析子节点
  • 4 平台驱动程序与DT
    • OF匹配风格
      • 处理非设备树平台
    • 平台数据与DT

设备树(DT)是易于阅读的硬件描述文件,它采用JSON式的格式化风格,在这种简单的树形结构中, 设备表示为带有属性的节点。属性可以为空(只有键,用来描述布尔值),也可以是键值对,其中的值可以是任意的字节流。

1 设备树机制

将选项CONFIG_OF设置为Y即可在内核中启用DT。要在驱动程序中调用DT API,必须添加以下头文件

#include 
#include 

DT支持一些数据类型:

  • 文本字符串用双引号表示。可以使用逗号来创建字符串列表
  • 单元格是由尖括号分隔的无符号32位整数
  • 布尔类型不过是空属性,其值是true或false取决于属性存在于否

比如:

node_lable:nodename@reg{
	srting-property="a string";					//一个字符串
	string-list="read","write";					//字符串列表
	onw-int-property=<197>;						//一个整型数字
	int-list-property=<0xbeef 123 0xabcd 4>;	//整型列表
	byte-array-property=[0x01 0x02 0x35 0x47];	//字节数组
	bool-property;								//布尔数据
	mixed-list-property="a string",<oxabcd 45>,<35>,[0x01 0x23 0x45];	//混合类型	
};

命名约定

每个节点都必须由[@

]形式的名称,其中是一个字符串,其最多可以为31个字符,[@
]是可选的,具体取决于节点代表的设备是否为可寻址的。
是用来访问设备的主要地址。设备命名的一个例子如下:

i2c@021a0000{
	compatible = "fls,imx6q-i2c","fls,ima21-i2c";
	reg = <0x021a0000 0x4000>;
	...
}

别名、标签和phandle

node_lable:nodename@reg{
	...
};

gpio1:gpio@0209c000{
	...
};
aliases{
	...
	gpio1 = &gpio1;
	...
};

标签不过是标记节点的方法,可以用唯一的名称来标识节点。DT编译器将该名称转换为唯一的32位值。在上面的例子中,gpio1和node_label都是标签。之后可以用标签来引用节点,因为标签对于节点是唯一的。

指针句柄(point handle,简写为phandle)是与节点相关联的32位值,用于唯一标识该结点,以便可以从另一个结点的属性引用该节点。标签用于一个指向节点的指针,使用&mylabel可以指向标签为mylabel的节点,比如gpio1 = &gpio1。&gpio1被转换为phandle,以便引用gpio1节点。

为了在查找节点时不遍历整棵树,引入了别名的概念。在DT中,别名节点可以看做是快速查找表,可以使用函数find_node_by_alias()来查找指定别名的节点。别名不是直接在DT源中使用,而是由Linux内核来引用。

DT编译器

DT有两种形式:文本形式(代表源,也称作DTS)和二进制块形式(代表编译后的DT),也称作DTB。源文件的拓展名是.dts。.dtsi是SOC级定义,.dts文件代表开发板定义。就像源文件(.c)和包含头文件(.h)一样,应该把.dtsi作为头文件包含在.dts文件中。而二进制文件使用.dtb拓展名。

2 表示和寻址设备

每个设备在DT中至少有一个节点。某些属性对于设备是通用的,这些属性是reg、#address-cells和#size-cells,它们的用途是在其所在总线上进行设备寻址。主要的寻址属性是reg,其含义取决于设备所在的总线。size-cells和address-cells的前缀#可以翻译为length。
每个可寻址设备都具有reg属性,该属性是reg = …形式的元组列表,其中每个元组代表设备的地址范围。#size-cells(长度单元)指示使用多少个32位单元来表示size0,如果与大小无关,则可以是0。 #address-cells(地址单元)指示用多少个32位单元来表示address0 。

可寻址设备继承它们父节点的#size-cells和#address-cells,其指定的#size-cells和#address-cells影响子设备。

SPI和I2C寻址

SPI和I2C设备都属于非内存映射设备,因为它们的地址对CPU不可访问。而总线控制器驱动程序将代表CPU执行间接访问。每个I2C/SPI设备都表示为在I2C/SPI总线节点上的子节点。对于非存储映射的设备,#size-cells表示为0,寻址元组中的size元素为空。这意味着这种设备的reg属性总是只有一个单元:

&i2c3{
	...
	temperature-sensor@49{
		...
		reg=<0x49>;
		...
	};
	
	pcf8523:rtc@68{
	...
	reg = <0x68>;
	...
	};
	...
};


&ecspi{
	...
	cs-gpios = <&gpio4 17 0>,<&gpio5 17 0>,<&gpio6 17 0>;
	...
	ad7606r8_0:ad7606r8@1{
		...
		reg = <1>;
		...
	};
	...
};

reg属性只是一个保存地址值的单元格,I2C设备的reg属性用于指定总线上设备的地址。对于SPI,reg表示从控制器节点 所具有的芯片选择列表中 分配给设备的芯片选择线的索引。例如对于ad7606r8 ADC,芯片选择索引是1,对应于cs-gpios中的<&gpio5 17 0>,cs-gpios是控制器节点的芯片选择列表。

平台设备寻址

下面介绍的内存映射设备,其内存可由CPU访问。在这里,reg属性仍然定义设备的地址,这是可以访问设备的内存区域列表。每个区域用单元格元组表示,其中第一个单元格是内存区域的基地址,第二个单元格是该区域的大小。每个元组代表设备使用的地址范围。

这种设备应该在具有特殊值compatible= "simple-bus"的节点内声明,这意味着简单的内存映射总线,没有特定的处理和驱动程序:

soc{
	#address-cells = <1>;
	#size-cells = <1>;
	compatible = "simple-bus";
	aips-bus@02000000{
		...
		#address-cells = <1>;
		#size-cells = <1>;
		reg = <0x02000000 0x100000>;
		...
		spba-bus@02000000{
			...
			#address-cells = <1>
			#size-cells = <1>;
			reg = <0x02000000 0x400000>;
			...
			ecspi:ecspi@02008000
			{
				...
				#address-cells = <1>;
				#size-cells = <0>;
				reg = <0x02008000 0x400>;
				...
			};
		...
		};
		...
	};
	...
};

父结点compatible= “simple-bus”,其在节点将被注册为平台设备。设置#size-cells=<0>也能够看到SPI总线控制器怎样改变其子节点的寻址方式的。从内核设备树文档可以查找所有 绑定信息:Document/devicetree/binding/。

3 处理资源

当驱动程序期望某种类型的资源列表时,由于编写开发板设备树的人通常不是写驱动程序的人,因此不能保证该列表是以驱动程序期望的方式排序。例如,驱动程序可能期望其设备节点具有2条IRQ线路,一条用于索引0处的Tx事件,另一条用于索引1处的Rx。如果这种顺序得不到满足,驱动就会发生异常行为。为了避免这种不匹配,引入了命名资源的概念,它由定义资源列表命名组成,因此无论索引什么,给定的名称总将与资源相匹配。
命名资源的相关属性如下:

  • reg-names:reg属性中的内存区域列表
  • clock-names:clocks属性中命名clocks
  • interrupt-names:为interrupts属性中的每个中断指定一个名称
  • dma-names:用于dma属性

例如:

fake_device{
	...
	reg=<0x4a06400 0x800>,<0x4a064800 0x200>,<0x4a064c00 0x200>;
	reg-name="config","ohci","ehci";
	interrupters=<0 66 IRQ_TYPE_LEVEL_HIGH>,<0 67 IRQ_TYPE_LEVEL_HIGH>;
	interrupter-names="ohci","ehci";
	clocks=<&clks IMAX6QDL_CLK_UART_IPG>,<&clks IMAX6QDL_CLK_UART_SERTAL>;
	clock-names="ipg","per";
	dmas=<&sdma 25 4 0>,<&sdma 26 4 0>;
	dma-names="rx","tx";
};	

驱动程序中提取每个命名资源代码如下所示:

struct resource *res1,*res2;
res1 = platform_get_resource_byname(pdev,IORESOURCE_MEM,"ohci");
res2 = platform_get_resource_byname(pdev,IORESOURCE_MEM,"config");

struct dma_chan *dma_chan_rx,*dma_chan_tx;
dma_chan_rx=dma_request_slave_channel(&pdev->dev,"rx");
dma_chan_tx=dma_request_slave_channel(&pdev->dev,"tx");


int txirq,rxirq;
txirq = platform_get_irq_byname(pdev,"ohci");
rxirq = platform_get_irq_byname(pdev,"ehci");

struct *clk_ipg,*clk_per;
clk_ipg = devm_clk_get(&pdev->dev,"ipg");
clk_per = devm_clk_get(&pdev->dev,"per");

这样,就可以确保把正确的名字映射到正确的资源上,而不用再使用索引了。

提取特定应用数据

特定应用数据时公共属性之外的数据(既不是资源,也不是IO、调度器等),可以分配给设备的任意属性和子节点,这样的属性名称通常以制造商代码作前缀。它们可以是任何字符串、布尔值或整型值。

文本字符串

下面是一个string属性:

string-property = "a string";

驱动程序使用of_property_read_string()读取字符串值,其原型定义如下:

int of_property_read_string(const struct device_node *np,const char *propname,const char **out_string);

以下代码说明如何使用它:

const char *my_string = NULL;
of_property_read_string(pdev->dev.of_node,"string-property",&my_string);

单元格和无符号的32位整数

下面是unsigned int属性:

one-int-property=<197>;
int-list-property = <1350000 0x54dae47 1250000 1200000>;

应该使用of_property_read_u32()读取单元格值。其原型定义如下:

int of_property_read_u32(const struct device_node *np,const char *propname,u32 *out_value);

驱动中使用:

unsigned int number;
of_property_read_u32(pdev->dev.of_node,"one-cell-property",&number);

可以使用of_property_read_u32_array()读取单元格列表,其原型如下:

int of_property_read_u32_array(const struct device_node *np,
					     const char *propname,
					     u32 *out_values, size_t sz);

sz是要读取数组元素的数量。比如:

unsigned int cells_array[4];
of_property_read_u32_array(pdev->dev.of_node,"int-list-property",cells_array,4);

布尔

使用of_property_read_bool()读取布尔属性。属性的名称在函数的第二个参数中给出:

bool my_bool = of_property_read_bool(pdev->dev.of_node,"bool-property");
if(my_bool)
{
	/* 布尔值为真 */
}
else{
	/* 布尔值为假 */
}

提取并分析子节点

使用for_each_child_of_node()遍历指定节点的子节点:

struct device_node *np = pdev->dev.of_node;
struct device_node *sub_np;
for_each_child_of_node(np,sub_np)
{
	...
}

4 平台驱动程序与DT

平台驱动程序也使用DT,也就是说,这是现在推荐的处理平台设备的方法,不在需要使用开发板文件,甚至不需要在设备的属性更改时重新编译内核。

OF匹配风格

OF匹配风格是平台核心执行的第一种匹配机制,以匹配设备及其驱动程序。它使用设备树的compatible属性来匹配of_match_table中的设备项,设备项是struct driver子结构的一个字段。每个设备节点都有compatible属性,它是字符串或字符串列表。任何驱动程序只要声明compatible属性中列出的字符串之一,就将触发匹配,并看到probe函数执行。

DT匹配项在内核中被描述为struct of_device_id结构的实例,该结构定义在liunx/mod_devicetable.h中,如下所示:

struct of_device_id{
	...
	char compatible[128];
	const void *data;
};
  • compatible:这是用来匹配DT设备节点兼容属性的字符串字符串,它们必须完全相同才可以匹配
  • data:这可以指向任何结构,这个结构可以用作每个设备类型的配置数据。

of_match_table是指针,可以传递struct of_device_id数组,使驱动程序兼容多个设备:

static const struct of_device_id imx_uart_dt_ids[] = {
	{ .compatible = "fsl,imx6q-uart",},
	{ .compatible = "fsl,imx1-uart",},
	{ .compatible = "fsl,imx21-uart",},
	....
};

填充了of_device_id 数组,就必须把它传递到平台驱动程序的of_match_table字段:

static struct platform_driver serial_imx_driver = {
	...
	.driver = {
		.name = "imax-uart",
		.of_match_table = imx_uart_dt_ids,
		...
	};
};

这一步,只有驱动程序知道of_device_id数组,要使内核也或得通知(这样可以把of_device_id 数组存储到平台内核维护的设备列表中),该数组必须用MODULE_DEVICE_TABLE(设备类型,设备表)注册:

MODULE_DEVICE_TABLE(of,imx_uart_dt_ids);

这样,驱动程序就兼容DT,接下来声明与驱动程序兼容的设备:

uart1:serial@02020000{
	compatible = "fsl,imx6q-uart","fls,imx21-uart";
	reg = <0x02020000 0x4000>;
	...
};

这里提供了两个兼容的字符串,如果第一个与任何驱动程序都不匹配,则平台核心将执行第二个匹配。
当发生匹配时,将以struct platform_device 结构作为参数调用驱动程序的probe函数。

处理非设备树平台

使用CONFIG_OF选项在内核中启用DT支持。如果内核没有启用DT支持,则可能会希望避免使用DT API。其实现的方法是检查CONFIG_OF是否设置。过去常常这样做:

#ifdef CONFIG_OF
	static const struct of_device_id imx_uart_dt_ids[] = {
	{ .compatible = "fsl,imx6q-uart",},
	{ .compatible = "fsl,imx1-uart",},
	{ .compatible = "fsl,imx21-uart",},
	....
	};
	....
#endif

这不是唯一的选择,还可以使用of_match_ptr宏,当OF被禁用时它简单地返回NULL,查看源代码,在include/linux/of.h里面。

#ifdef CONFIG_OF
...
#define of_match_ptr(_ptr)	(_ptr)
...
#else /* CONFIG_OF */
...
#define of_match_ptr(_ptr)	NULL
...
#endif /* CONFIG_OF */

平台数据与DT

如果驱动程序需要平台数据,则应该检查dev.platfrom_data指针。非空值表示驱动程序已经在开发板配置文件中以旧的方法实例化,并且DT不会处理它。对于从DT实例化的驱动程序,dev.platform_data将为NULL,平台设备将在DT项上获得一个指针,该指针对应于dev.of_node指针中的设备,可以从该指针中提取资源并使用OF API分析和提取应用数据

你可能感兴趣的:(Linux设备驱动开发,linux,驱动开发)