将选项CONFIG_OF设置为Y即可在内核中启用DT。要在驱动程序中调用DT API,必须添加以下头文件
#include
#include
DT支持一些数据类型:
比如:
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]; //混合类型
};
每个节点都必须由
i2c@021a0000{
compatible = "fls,imx6q-i2c","fls,ima21-i2c";
reg = <0x021a0000 0x4000>;
...
}
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有两种形式:文本形式(代表源,也称作DTS)和二进制块形式(代表编译后的DT),也称作DTB。源文件的拓展名是.dts。.dtsi是SOC级定义,.dts文件代表开发板定义。就像源文件(.c)和包含头文件(.h)一样,应该把.dtsi作为头文件包含在.dts文件中。而二进制文件使用.dtb拓展名。
每个设备在DT中至少有一个节点。某些属性对于设备是通用的,这些属性是reg、#address-cells和#size-cells,它们的用途是在其所在总线上进行设备寻址。主要的寻址属性是reg,其含义取决于设备所在的总线。size-cells和address-cells的前缀#可以翻译为length。
每个可寻址设备都具有reg属性,该属性是reg =
可寻址设备继承它们父节点的#size-cells和#address-cells,其指定的#size-cells和#address-cells影响子设备。
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/。
当驱动程序期望某种类型的资源列表时,由于编写开发板设备树的人通常不是写驱动程序的人,因此不能保证该列表是以驱动程序期望的方式排序。例如,驱动程序可能期望其设备节点具有2条IRQ线路,一条用于索引0处的Tx事件,另一条用于索引1处的Rx。如果这种顺序得不到满足,驱动就会发生异常行为。为了避免这种不匹配,引入了命名资源的概念,它由定义资源列表和命名组成,因此无论索引什么,给定的名称总将与资源相匹配。
命名资源的相关属性如下:
例如:
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);
下面是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)
{
...
}
平台驱动程序也使用DT,也就是说,这是现在推荐的处理平台设备的方法,不在需要使用开发板文件,甚至不需要在设备的属性更改时重新编译内核。
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;
};
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 */
如果驱动程序需要平台数据,则应该检查dev.platfrom_data指针。非空值表示驱动程序已经在开发板配置文件中以旧的方法实例化,并且DT不会处理它。对于从DT实例化的驱动程序,dev.platform_data将为NULL,平台设备将在DT项上获得一个指针,该指针对应于dev.of_node指针中的设备,可以从该指针中提取资源并使用OF API分析和提取应用数据