设备树 ( DT )是一个易读的硬件描述文件,具有类似 JSON 的格式化风格,这是一个简单的树结构,其中设备由带有属性的节点来表示。属性可以是空的(仅描述布尔值的键),也可以是键值对,其中值可以包含任意字节流。本章是对 DT 的简单介绍。每个内核子系统或框架都有自己的 DT 绑定。我们将在处理相关主题时讨论这些特定的绑定。DT 起源于 OF,这是一个由计算机公司认可的标准,其主要目的是为计算机固件系统定义接口。也就是说,你可以在http://www.devicetree.org/找到更多关于 DT 规格的信息。因此,本章将涵盖 DT 的基础知识,例如:
- 命名约定,以及别名和标签
- 描述数据类型及其应用编程接口
- 管理寻址方案和访问设备资源
- 实现匹配样式并提供特定于应用的数据
通过将选项CONFIG_OF
设置为Y
,在内核中启用 DT。为了从您的驱动中提取 DT 应用编程接口,您必须添加以下头:
#include <linux/of.h>
#include <linux/of_device.h>
DT 支持几种数据类型。让我们用一个示例节点描述来看看它们:
/* This is a comment */
// This is another comment
node_label: nodename@reg{
string-property = "a string";
string-list = "red fish", "blue fish";
one-int-property = <197>; /* One cell in this property */
int-list-property = <0xbeef 123 0xabcd4>; /*each number (cell) is a
*32 bit integer(uint32).
*There are 3 cells in
*/this property
mixed-list-property = "a string", <0xadbcd45>, <35>, [0x01 0x23 0x45]
byte-array-property = [0x01 0x23 0x45 0x67];
boolean-property;
};
以下是设备树中使用的数据类型的一些定义:
- 文本字符串用双引号表示。可以使用逗号来创建字符串列表。
- 单元格是由尖括号分隔的 32 位无符号整数。
- 布尔数据只是一个空属性。真值或假值取决于属性是否存在。
每个节点都必须有一个形式为<name>[@<address>]
的名称,其中<name>
是一个长度可达 31 个字符的字符串,[@<address>]
是可选的,这取决于该节点是否代表可寻址设备。<address>
应该是用来访问设备的主地址。设备命名示例如下:
expander@20 {
compatible = "microchip,mcp23017";
reg = <20>;
[...]
};
或者
i2c@021a0000 {
compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
[...]
};
另一方面,label
是可选的。仅当节点旨在从另一个节点的属性引用时,标记该节点才有用。正如下一节所解释的那样,可以将标签视为指向节点的指针。
理解这三个要素是如何工作的非常重要。它们经常在 DT 中使用。让我们用下面的 DT 来解释它们是如何工作的:
aliases {
ethernet0 = &fec;
gpio0 = &gpio1;
gpio1 = &gpio2;
mmc0 = &usdhc1;
[...]
};
gpio1: gpio@0209c000 {
compatible = "fsl,imx6q-gpio", "fsl,imx35-gpio";
[...]
};
node_label: nodename@reg {
[...];
gpios = <&gpio1 7 GPIO_ACTIVE_HIGH>;
};
标签只不过是标记节点的一种方式,通过唯一的名称来标识节点。在现实世界中,该名称由 DT 编译器转换为唯一的 32 位值。在上例中,gpio1
和node_label
都是标签。标签可以用来引用一个节点,因为标签对于一个节点是唯一的。
一个指针句柄 ( 指针)是一个与节点相关联的 32 位值,用于唯一标识该节点,以便该节点可以从另一个节点的属性中引用。标签用于指向节点的指针。使用<&mylabel>
,指向标签为mylabel
的节点。
The use of &
is just like in the C programming language; to obtain the address of an element.
在前面的例子中,&gpio1
被转换为 phandle,因此它引用了gpio1
节点。以下示例也是如此:
thename@address {
property = <&mylabel>;
};
mylabel: thename@adresss {
[...]
}
为了不遍历整棵树寻找节点,引入了别名的概念。在 DT 中,aliases
节点就像一个快速查找表,是另一个节点的索引。可以使用函数find_node_by_alias()
找到一个给定别名的节点。别名不直接在 DT 源中使用,而是由 Linux 内核来区分。
DT 有两种形式:文本形式和二进制 blob 形式,前者表示源,也称为DTS
,后者表示编译后的 DT,也称为DTB
。源文件有.dts
扩展名。实际上,还有.dtsi
文本文件,代表 SoC 级别定义,而.dts
文件代表板级定义。可以把.dtsi
看做头文件,那应该包含在.dts
一个里面,是源文件,而不是反过来,有点像在源文件(.c
)里面包含头文件(.h
)。另一方面,二进制文件使用.dtb
扩展名。
实际上还有第三种形式,即/proc/device-tree
中 DT 的运行时表示。
顾名思义,用来编译设备树的工具叫做设备树编译器 ( dtc )。从根内核源代码中,可以为特定的体系结构编译独立的特定 DT 或所有的 DTs。
让我们为 arm SoC 编译所有 DT ( .dts)
文件:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make dtbs
对于独立台式机:
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make imx6dl-sabrelite.dtb
在前面的例子中,源文件的名称是imx6dl-sabrelite.dts
。
给定一个编译好的设备树(.dtb
)文件,可以做相反的操作,提取源(.dts
)文件:
dtc -I dtb -O dtsarch/arm/boot/dts imx6dl-sabrelite.dtb >path/to/my_devicetree.dts
For the purpose of debugging, it could be useful to expose the DT to the user space. The CONFIG_PROC_DEVICETREE
configuration variable will do that for you. You can then explore and walk through the DT in /proc/device-tree
.
每个设备在 DT 中至少有一个节点。有些属性是许多设备类型共有的,尤其是位于内核已知的总线上的设备(SPI、I2C、平台、MDIO 等)。这些属性是reg
、#address-cells
和#size-cells
。这些属性的目的是在它们所在的总线上进行设备寻址。也就是说,主要寻址属性是reg
,这是一个通用属性,其含义取决于设备所在的总线。前缀size-cell
、address-cell
的#
(锐)可以翻译成length
。
每个可寻址设备获得一个reg
属性,该属性是一个以reg = <address0size0 [address1size1] [address2size2] ... >
形式表示的元组列表,其中每个元组代表该设备使用的地址范围。#size-cells
表示有多少 32 位单元用于表示大小,如果大小不相关,则可以是 0。另一方面,#address-cells
表示有多少 32 位单元用于表示地址。换句话说,每个元组的地址元素根据#address-cell
解释;尺寸元素也是如此,根据#size-cell
解释。
实际上,可寻址设备从其父节点的#size-cell
和#address-cell
继承,父节点是代表总线控制器的节点。给定设备中存在的#size-cell
和#address-cell
不会影响设备本身,但会影响其子设备。换句话说,在解释给定节点的reg
属性之前,必须知道父节点的#address-cells
和#size-cells
值。父节点可以自由定义任何适合设备子节点(子节点)的寻址方案。
SPI 和 I2C 设备都属于非内存映射设备,因为 CPU 无法访问它们的地址。相反,父设备的驱动(总线控制器驱动)将代表中央处理器执行间接访问。每个 I2C/SPI 设备始终表示为该设备所在的 I2C/SPI 总线节点的一个子节点。对于非内存映射设备,#size-cells
属性为 0,寻址元组时的大小元素为空。这意味着这种设备的reg
属性始终在单元格上:
&i2c3 {
[...]
status = "okay";
temperature-sensor@49 {
compatible = "national,lm73";
reg = <0x49>;
};
pcf8523: rtc@68 {
compatible = "nxp,pcf8523";
reg = <0x68>;
};
};
&ecspi1 {
fsl,spi-num-chipselects = <3>;
cs-gpios = <&gpio5 17 0>, <&gpio5 17 0>, <&gpio5 17 0>;
status = "okay";
[...]
ad7606r8_0: ad7606r8@1 {
compatible = "ad7606-8";
reg = <1>;
spi-max-frequency = <1000000>;
interrupt-parent = <&gpio4>;
interrupts = <30 0x0>;
convst-gpio = <&gpio6 18 0>;
};
};
如果查看arch/arm/boot/dts/imx6qdl.dtsi
处的 SoC 级别文件,会注意到在i2c
和spi
节点中,前者的#size-cells
和#address-cells
分别被设置为0
,后者的1
,这两个节点分别是前一节中列举的 I2C 和 SPI 设备的父节点。这有助于我们理解它们的reg
属性,地址值只有一个单元格,大小值没有单元格。
I2C 设备的reg
属性用于指定设备在总线上的地址。对于 SPI 设备,reg
表示控制器节点拥有的芯片选择列表中分配给该设备的芯片选择线的索引。例如,对于 ad7606r8 ADC,片选索引为1
,对应于cs-gpios
中的<&gpio5 17 0>
,这是控制器节点的片选列表。
你可能会问我为什么使用 I2C/SPI 节点的 phandle:答案是因为 I2C/SPI 设备应该在板级文件(.dts
)中声明,而 I2C/SPI 总线控制器应该在 SoC 级文件(.dtsi
)中声明。
本节介绍内存可由中央处理器访问的简单内存映射设备。在这里,reg
属性仍然定义了设备的地址,这是一个可以访问设备的内存区域列表。每个区域用一组单元表示,其中第一个单元是存储区域的基址,第二个单元是区域的大小。然后它有了形式reg = <base0 length0 [base1 length1] [address2 length2] ... >
。每个元组代表设备使用的地址范围。
在现实世界中,如果不知道另外两个属性#size-cells
和#address-cells
的值,就不应该解释reg
属性。#size-cells
告诉我们每个子reg
元组中的长度字段有多大。#address-cell
也是如此,它告诉我们指定一个地址必须使用多少个单元格。
这种设备应该在具有特殊值compatible = "simple-bus"
的节点中声明,这意味着没有特定处理或驱动的简单内存映射总线:
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
aips-bus@02000000 { /* AIPS1 */
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x100000>;
[...];
spba-bus@02000000 {
compatible = "fsl,spba-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x40000>;
[...]
ecspi1: ecspi@02008000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6q-ecspi", "fsl,imx51-ecspi";
reg = <0x02008000 0x4000>;
[...]
};
i2c1: i2c@021a0000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
[...]
};
};
};
在前面的例子中,父节点在兼容属性中有simple-bus
的子节点将被注册为平台设备。还可以看到 I2C 和 SPI 总线控制器如何通过设置#size-cells = <0>;
来改变其子级的寻址方案,因为这与它们无关。在内核设备树的文档中有一个查找任何绑定信息的著名位置:文档/设备树/绑定/ 。
驱动的主要目的是处理和管理设备,并且在大多数情况下,向用户空间公开它们的功能。这里的目标是收集设备的配置参数,尤其是资源(内存区域、中断线路、DMA 通道、时钟等)。
以下是我们将在本节中使用的设备节点。它是 i.MX6 UART 设备的节点,在arch/arm/boot/dts/imx6qdl.dtsi
中定义:
uart1: serial@02020000 {
compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6QDL_CLK_UART_IPG>,
<&clks IMX6QDL_CLK_UART_SERIAL>;
clock-names = "ipg", "per";
dmas = <&sdma 25 4 0>, <&sdma 26 4 0>;
dma-names = "rx", "tx";
status = "disabled";
};
当驱动期望某一类型的资源列表时,不能保证列表是按照驱动期望的方式排序的,因为编写板级设备树的人通常不是编写驱动的人。例如,驱动可能期望其设备节点具有两条 IRQs 线,一条用于索引 0 处的发送事件,另一条用于索引 1 处的接收事件。如果不遵守秩序会怎么样?司机会有不必要的行为。为了避免这种不匹配,引入了命名资源(clock
、irq
、dma
、reg
)的概念。它包括定义我们的资源列表,并对它们进行命名,这样无论它们的索引是什么,给定的名称都将始终与资源匹配。
命名资源的相应属性如下:
reg-names
:这是用于reg
属性中的存储区域列表clock-names
:这是在clocks
属性中命名时钟interrupt-names
:这给interrupts
属性中的每个中断命名dma-names
:这是dma
属性
现在让我们创建一个假的设备节点条目来解释这一点:
fake_device {
compatible = "packt,fake-device";
reg = <0x4a064000 0x800>, <0x4a064800 0x200>, <0x4a064c00 0x200>;
reg-names = "config", "ohci", "ehci";
interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>, <0 67 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "ohci", "ehci";
clocks = <&clks IMX6QDL_CLK_UART_IPG>, <&clks IMX6QDL_CLK_UART_SERIAL>;
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");
inttxirq, rxirq;
txirq = platform_get_irq_byname(pdev, "ohci");
rxirq = platform_get_irq_byname(pdev, "ehci");
structclk *clck_per, *clk_ipg;
clk_ipg = devm_clk_get(&pdev->dev, "ipg");
clk_ipg = devm_clk_get(&pdev->dev, "pre");
这样,您就可以将正确的名称映射到正确的资源,而不再需要使用索引。
在这里,驱动将获得内存区域的所有权,并将其映射到虚拟地址空间。我们将在第 11 章、内核内存管理中对此进行更多讨论。
struct resource *res;
void __iomem *base;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
/*
* Here one can request and map the memory region
* using request_mem_region(res->start, resource_size(res), pdev->name)
* and ioremap(iores->start, resource_size(iores)
*
* These function are discussed in chapter 11, Kernel Memory Management.
*/
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
platform_get_resource()
将根据第一个(索引 0) reg
赋值中的内存区域设置struct res
的开始和结束字段。请记住platform_get_resource()
的最后一个参数代表资源索引。在前面的示例中,0
为该资源类型的第一个值编制索引,以防设备在 DT 节点中被分配多个内存区域。在我们的示例中,它是reg = <0x02020000 0x4000>
,这意味着分配的区域从物理地址0x02020000
开始,大小为0x4000
字节。platform_get_resource()
将设置res.start = 0x02020000
和res.end = 0x02023fff
。
中断接口实际上分为两部分;消费者侧和控制器侧。DT 中有四个属性用于描述中断连接:
控制器是向消费者展示 IRQ 线路的设备。在控制器端,上具有以下属性:
interrupt-controller
:一个空的(布尔)属性,为了将设备标记为中断控制器,应该定义这个属性#interrupt-cells
:这是中断控制器的一个属性。它说明了有多少个单元用于指定该中断控制器的中断
消费者是产生 IRQ 的设备。使用者绑定需要以下属性:
interrupt-parent
:对于产生中断的设备节点,它是一个属性,包含一个指向设备所连接的中断控制器节点的指针phandle
。如果省略,设备将从其父节点继承该属性。interrupts
:是中断说明符。
中断绑定和中断说明符绑定到中断控制器设备。用于定义中断输入的单元数量取决于中断控制器,它是唯一一个通过其#interrupt-cells
属性来决定的控制器。在 i.MX6 的情况下,中断控制器是全局中断控制器 ( GIC )。其绑定在文档/设备树/绑定/arm/gic.txt 中有很好的解释。
这包括从 DT 中获取 IRQ 号,并将其映射到 Linux IRQ 中,从而为其注册一个函数回调。实现这一点的驱动代码非常简单:
int irq = platform_get_irq(pdev, 0);
ret = request_irq(irq, imx_rxint, 0, dev_name(&pdev->dev), sport);
platform_get_irq()
呼叫将返回irq
号码;这个号码可以被devm_request_irq()
使用(然后irq
可以在/proc/interrupts
看到)。第二个参数0
表示我们需要设备节点中指定的第一个中断。如果有多个中断,我们可以根据我们需要的中断来改变这个索引,或者只使用指定的资源。
在前面的示例中,设备节点包含一个中断说明符,如下所示:
interrupts = <0 66 IRQ_TYPE_LEVEL_HIGH>;
- 根据 ARM GIC 的说法,第一个单元通知我们中断类型:
0
:共享外设中断 ( SPI ),用于内核间共享的中断信号,可由 GIC 路由至任意内核1
: 专用外设中断 ( PPI ),用于单个内核专用的中断信号
文件可在 http://infocenter.arm.com/help/index.jsp?找到 topic =/com . arm . doc . DD 0407 e/cchdebe . html。
- 第二个单元保存中断号。这个数字取决于中断线路是 PPI 还是 SPI。
- 在我们的例子中,第三个单元
IRQ_TYPE_LEVEL_HIGH
代表感觉水平。所有可用的感应等级都在include/linux/irq.h
中定义。
interrupt-controller
属性用于将设备声明为中断控制器。#interrupt-cells
属性定义了定义一条中断线路必须使用多少个单元。我们将在第 16 章、高级 IRQ 管理中对此进行详细讨论。
特定于应用的数据是公共属性之外的数据(既不是资源,也不是 GPIOs、调节器等)。这些是可以分配给设备的任意属性和子节点。此类属性名称通常以制造代码为前缀。这些值可以是任何字符串、布尔值或整数值,以及它们在 Linux 源代码的drivers/of/base.c
中定义的应用编程接口。下面我们讨论的例子并不详尽。现在让我们重用本章前面定义的节点:
node_label: nodename@reg{
string-property = ""a string"";
string-list = ""red fish"", ""blue fish"";
one-int-property = <197>; /* One cell in this property */
int-list-property = <0xbeef 123 0xabcd4>;/* each number (cell) is 32 a * bit integer(uint32). There
* are 3 cells in this property
*/
mixed-list-property = "a string", <0xadbcd45>, <35>, [0x01 0x23 0x45]
byte-array-property = [0x01 0x23 0x45 0x67];
one-cell-property = <197>;
boolean-property;
};
以下是一个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);
以下是我们的int
属性:
one-int-property = <197>;
int-list-property = <1350000 0x54dae47 1250000 1200000>;
应该使用of_property_read_u32()
来读取单元格值。其原型定义如下:
int of_property_read_u32_index(const struct device_node *np,
const char *propname, u32 index, 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_tsz);
这里,sz
是要读取的数组元素的个数。来看看drivers/of/base.c
看看如何解读它的返回值:
unsigned int cells_array[4];
if (of_property_read_u32_array(pdev->dev.of_node, "int-list-property",
cells_array, 4)) {
dev_err(&pdev->dev, "list of cells not specified\n");
return -EINVAL;
}
应该使用of_property_read_bool()
来读取布尔属性,该属性的名称在函数的第二个参数中给出:
bool my_bool = of_property_read_bool(pdev->dev.of_node, "boolean-property");
If(my_bool){
/* boolean is true */
} else
/* Bolean is false */
}
您可以在设备节点中添加任何子节点。给定一个表示闪存设备的节点,分区可以表示为子节点。对于处理一组输入和输出 GPIO 的设备,每组都可以表示为一个子节点。示例节点如下:
eeprom: ee24lc512@55 {
compatible = "microchip,24xx512";
reg = <0x55>;
partition1 {
read-only;
part-name = "private";
offset = <0>;
size = <1024>;
};
partition2 {
part-name = "data";
offset = <1024>;
size = <64512>;
};
};
可以使用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) {
/* sub_np will point successively to each sub-node */
[...]
int size;
of_property_read_u32(client->dev.of_node,
"size", &size);
...
}
平台驱动也与 DT 一起工作。也就是说,这是当今处理平台设备的推荐方法,不再需要接触板文件,甚至在设备属性发生变化时重新编译内核。如果你还记得,在上一章中我们讨论了匹配风格,这是一种基于 DT 的匹配机制。让我们在下一节中看看它是如何工作的:
OF match style 是平台核心执行的第一个匹配机制,目的是将设备与其驱动相匹配。它使用设备树的compatible
属性来匹配of_match_table
中的设备条目,这是struct driver
子结构的一个字段。每个设备节点都有一个compatible
属性,它是一个字符串或字符串列表。任何声明在compatible
属性中列出的字符串之一的平台驱动都将触发匹配,并将看到其probe
函数被执行。
DT 匹配条目在内核中被描述为struct of_device_id
结构的一个实例,它在linux/mod_devicetable.h
中定义,看起来像:
// we are only interested in the two last elements of the structure
struct of_device_id {
[...]
char compatible[128];
const void *data;
};
以下是结构中每个柠檬的含义:
char compatible[128]
:这是用于匹配 DT 中设备节点兼容属性的字符串。在匹配发生之前,它们必须是相同的。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", },
{ /* sentinel */ }
};
一旦你填充了你的 id 数组,它必须被传递到你的平台驱动的of_match_table
字段,在驱动子结构中:
static struct platform_driver serial_imx_driver = {
[...]
.driver = {
.name = "imx-uart",
.of_match_table = imx_uart_dt_ids,
[...]
},
};
这一步,只有你的司机知道你的of_device_id
阵。为了让内核也知道(以便它可以将您的 id 存储在平台内核维护的设备列表中),您的阵列必须向MODULE_DEVICE_TABLE
注册,如第 5 章、*平台设备驱动:*所述
MODULE_DEVICE_TABLE(of, imx_uart_dt_ids);
仅此而已!我们的驱动是 DT 兼容的。回到我们的 DT,让我们声明一个与我们的驱动兼容的设备:
uart1: serial@02020000 {
compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>;
[...]
};
这里提供了两个兼容的字符串。如果第一个不匹配任何驱动,内核将执行与第二个的匹配。
当匹配发生时,您的驱动的probe
功能被调用,以struct platform_device
结构作为参数,它包含一个struct device dev
字段,其中有一个字段struct device_node *of_node
对应于与我们的设备相关联的节点,这样人们可以使用它来提取设备设置:
static int serial_imx_probe(struct platform_device *pdev)
{
[...]
struct device_node *np;
np = pdev->dev.of_node;
if (of_get_property(np, "fsl,dte-mode", NULL))
sport->dte_mode = 1;
[...]
}
可以检查 DT 节点是否被设置为知道驱动是响应于of_match
而被加载的,还是从板的init
文件中被实例化的。然后,您应该使用of_match_device
功能来选择发起匹配的struct *of_device_id
条目,该条目可能包含您已经传递的特定数据:
static int my_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
const struct of_device_id *match;
match = of_match_device(imx_uart_dt_ids, &pdev->dev);
if (match) {
/* Devicetree, extract the data */
my_data = match->data
} else {
/* Board init file */
my_data = dev_get_platdata(&pdev->dev);
}
[...]
}
通过CONFIG_OF
选项在内核中启用 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", },
{ /* sentinel */ }
};
/* other devicetree dependent code */
[...]
#endif
即使在缺少设备树支持时总是定义of_device_id
数据类型,在构建过程中也会省略包装到#ifdef CONFIG_OF ... #endif
中的代码。这用于条件编译。这不是你唯一的选择;还有of_match_ptr
宏,它只是在OF
禁用时返回NULL
。无论你在哪里需要传递你的of_match_table
作为参数,它都应该被包装在of_match_ptr
宏中,这样当OF
被禁用时,它就会返回NULL
。宏在include/linux/of.h
中定义:
#define of_match_ptr(_ptr) (_ptr) /* When CONFIG_OF is enabled */
#define of_match_ptr(_ptr) NULL /* When it is not */
我们可以这样使用它:
static int my_probe(struct platform_device *pdev)
{
const struct of_device_id *match;
match = of_match_device(of_match_ptr(imx_uart_dt_ids),
&pdev->dev);
[...]
}
static struct platform_driver serial_imx_driver = {
[...]
.driver = {
.name = "imx-uart",
.of_match_table = of_match_ptr(imx_uart_dt_ids),
},
};
这消除了当OF
被禁用时返回NULL
的#ifdef
。
有时,一个驱动可以支持不同的硬件,每个都有特定的配置数据。这些数据可以是专用的功能表、特定的寄存器值或每个硬件独有的任何数据。以下示例描述了一种通用方法:
让我们首先记住struct of_device_id
是什么样子,在include/linux/mod_devicetable.h
中。
/*
* Struct used for matching a device
*/
struct of_device_id {
[...]
char compatible[128];
const void *data;
};
我们感兴趣的领域是const void *data
,所以我们可以使用它来传递每个特定设备的任何数据。
假设我们拥有三个不同的设备,每个设备都有特定的私有数据。of_device_id.data
将包含指向特定参数的指针。这个例子的灵感来自drivers/tty/serial/imx.c
。
首先,我们声明私有结构:
/* i.MX21 type uart runs on all i.mx except i.MX1 and i.MX6q */
enum imx_uart_type {
IMX1_UART,
IMX21_UART,
IMX6Q_UART,
};
/* device type dependent stuff */
struct imx_uart_data {
unsigned uts_reg;
enum imx_uart_type devtype;
};
然后,我们用每个设备特定的数据填充一个数组:
static struct imx_uart_data imx_uart_devdata[] = {
[IMX1_UART] = {
.uts_reg = IMX1_UTS,
.devtype = IMX1_UART,
},
[IMX21_UART] = {
.uts_reg = IMX21_UTS,
.devtype = IMX21_UART,
},
[IMX6Q_UART] = {
.uts_reg = IMX21_UTS,
.devtype = IMX6Q_UART,
},
};
每个兼容条目都与特定的数组索引相关联:
static const struct of_device_idimx_uart_dt_ids[] = {
{ .compatible = "fsl,imx6q-uart", .data = &imx_uart_devdata[IMX6Q_UART], },
{ .compatible = "fsl,imx1-uart", .data = &imx_uart_devdata[IMX1_UART], },
{ .compatible = "fsl,imx21-uart", .data = &imx_uart_devdata[IMX21_UART], },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx_uart_dt_ids);
static struct platform_driver serial_imx_driver = {
[...]
.driver = {
.name = "imx-uart",
.of_match_table = of_match_ptr(imx_uart_dt_ids),
},
};
现在在probe
函数中,无论匹配条目是什么,它都将保存一个指向设备特定结构的指针:
static int imx_probe_dt(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
const struct of_device_id *of_id =
of_match_device(of_match_ptr(imx_uart_dt_ids), &pdev->dev);
if (!of_id)
/* no device tree device */
return 1;
[...]
sport->devdata = of_id->data; /* Get private data back */
}
在前面的代码中,devdata
是原始源代码中某个结构的元素,声明方式类似const struct imx_uart_data *devdata
;我们可以在数组中存储任何特定的参数。
OF match 样式可以与任何其他匹配机制相结合。在以下示例中,我们混合了 DT 和设备 ID 匹配样式:
我们为设备标识匹配样式填充一个数组,每个设备都有自己的数据:
static const struct platform_device_id sdma_devtypes[] = {
{
.name = "imx51-sdma",
.driver_data = (unsigned long)&sdma_imx51,
}, {
.name = "imx53-sdma",
.driver_data = (unsigned long)&sdma_imx53,
}, {
.name = "imx6q-sdma",
.driver_data = (unsigned long)&sdma_imx6q,
}, {
.name = "imx7d-sdma",
.driver_data = (unsigned long)&sdma_imx7d,
}, {
/* sentinel */
}
};
MODULE_DEVICE_TABLE(platform, sdma_devtypes);
我们对搭配风格也是如此:
static const struct of_device_idsdma_dt_ids[] = {
{ .compatible = "fsl,imx6q-sdma", .data = &sdma_imx6q, },
{ .compatible = "fsl,imx53-sdma", .data = &sdma_imx53, },
{ .compatible = "fsl,imx51-sdma", .data = &sdma_imx51, },
{ .compatible = "fsl,imx7d-sdma", .data = &sdma_imx7d, },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, sdma_dt_ids);
probe
功能如下:
static int sdma_probe(structplatform_device *pdev)
{
conststructof_device_id *of_id =
of_match_device(of_match_ptr(sdma_dt_ids), &pdev->dev);
structdevice_node *np = pdev->dev.of_node;
/* If devicetree, */
if (of_id)
drvdata = of_id->data;
/* else, hard-coded */
else if (pdev->id_entry)
drvdata = (void *)pdev->id_entry->driver_data;
if (!drvdata) {
dev_err(&pdev->dev, "unable to find driver data\n");
return -EINVAL;
}
[...]
}
然后我们声明我们的平台驱动;馈送前面部分中定义的所有数组:
static struct platform_driversdma_driver = {
.driver = {
.name = "imx-sdma",
.of_match_table = of_match_ptr(sdma_dt_ids),
},
.id_table = sdma_devtypes,
.remove = sdma_remove,
.probe = sdma_probe,
};
module_platform_driver(sdma_driver);
平台设备可以与启用设备树的系统一起工作,无需任何额外的修改。这就是我们在处理资源一节所展示的。通过使用platform_xxx
族函数,核心还遍历 DT(带有of_xxx
族函数)来查找请求的资源。反之则不然,因为of_xxx
家族功能只为 DT 保留。所有的资源数据都可以以通常的方式提供给驱动。驱动现在知道这个设备是否没有用板文件中的硬编码参数初始化。让我们以 uart 设备节点为例:
uart1: serial@02020000 {
compatible = "fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
interrupts = <0 26 IRQ_TYPE_LEVEL_HIGH>;
dmas = <&sdma 25 4 0>, <&sdma 26 4 0>;
dma-names = "rx", "tx";
};
以下摘录描述了其驱动的probe
功能。在probe
中,函数platform_get_resource()
可用于提取任何资源属性(内存区域、dma、irq),或者特定函数,如platform_get_irq()
,提取 DT 中interrupts
属性提供的irq
:
static int my_probe(struct platform_device *pdev)
{
struct iio_dev *indio_dev;
struct resource *mem, *dma_res;
struct xadc *xadc;
int irq, ret, dmareq;
/* irq */
irq = platform_get_irq(pdev, 0);
if (irq<= 0)
return -ENXIO;
[...]
/* memory region */
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
xadc->base = devm_ioremap_resource(&pdev->dev, mem);
/*
* We could have used
* devm_ioremap(&pdev->dev, mem->start, resource_size(mem));
* too.
*/
if (IS_ERR(xadc->base))
return PTR_ERR(xadc->base);
[...]
/* second dma channel */
dma_res = platform_get_resource(pdev, IORESOURCE_DMA, 1);
dmareq = dma_res->start;
[...]
}
综上所述,对于dma
、irq
、mem
等属性,在平台驱动中与dtb
匹配无关。如果你记得,这些数据和你可以作为平台资源传递的数据属于同一类型。为了理解为什么,我们只需要看看这些函数的内部;我们将看到他们每个人如何在内部处理 DT 函数。以下是platform_get_irq
功能的示例:
int platform_get_irq(struct platform_device *dev, unsigned int num)
{
[...]
struct resource *r;
if (IS_ENABLED(CONFIG_OF_IRQ) &&dev->dev.of_node) {
int ret;
ret = of_irq_get(dev->dev.of_node, num);
if (ret > 0 || ret == -EPROBE_DEFER)
return ret;
}
r = platform_get_resource(dev, IORESOURCE_IRQ, num);
if (r && r->flags & IORESOURCE_BITS) {
struct irq_data *irqd;
irqd = irq_get_irq_data(r->start);
if (!irqd)
return -ENXIO;
irqd_set_trigger_type(irqd, r->flags & IORESOURCE_BITS);
}
return r ? r->start : -ENXIO;
}
人们可能想知道platform_xxx
函数是如何从 DT 中提取资源的。这应该是of_xxx
功能家族。你是对的,但是在系统引导期间,内核在每个设备节点上调用of_platform_device_create_pdata()
,这将导致创建一个带有相关资源的平台设备,在这个平台设备上你可以调用platform_xxx
系列函数。其原型如下:
static struct platform_device *of_platform_device_create_pdata(
struct device_node *np, const char *bus_id,
void *platform_data, struct device *parent)
如果你的司机需要平台数据,你应该检查dev.platform_data
指针。非空值意味着您的驱动已经以旧的方式在板配置文件中被实例化,并且 DT 没有进入其中。对于从 DT 实例化的驱动,dev.platform_data
将是NULL
,您的平台设备将在dev.of_node
指针中对应于您的设备的 DT 条目(节点)上获得一个指针,从中可以提取资源并使用 OF API 来解析和提取应用数据。
There's also a hybrid method that one can use to associate platform data declared in the C files to DT nodes, but that's for special cases only: for DMA, IRQ, and memory. This method is used only when the driver expects only resources, and no application-specific data.
可以将 I2C 控制器的传统声明转换为 DT 兼容节点,如下所示:
#define SIRFSOC_I2C0MOD_PA_BASE 0xcc0e0000
#define SIRFSOC_I2C0MOD_SIZE 0x10000
#define IRQ_I2C0
static struct resource sirfsoc_i2c0_resource[] = {
{
.start = SIRFSOC_I2C0MOD_PA_BASE,
.end = SIRFSOC_I2C0MOD_PA_BASE + SIRFSOC_I2C0MOD_SIZE - 1,
.flags = IORESOURCE_MEM,
},{
.start = IRQ_I2C0,
.end = IRQ_I2C0,
.flags = IORESOURCE_IRQ,
},
};
而 DT 节点:
i2c0: i2c@cc0e0000 {
compatible = "sirf,marco-i2c";
reg = <0xcc0e0000 0x10000>;
interrupt-parent = <&phandle_to_interrupt_controller_node>
interrupts = <0 24 0>;
#address-cells = <1>;
#size-cells = <0>;
status = "disabled";
};
从硬编码设备配置切换到 DT 的时候到了。本章为您提供了处理 DTs 所需的一切。现在,您已经具备了必要的技能,可以自定义或添加任何您想要的节点和属性到 DT 中,并从您的驱动中提取它们。在下一章中,我们将讨论 I2C 驱动,并使用 DT API 来枚举和配置我们的 I2C 设备。