1)摘自【正点原子】领航者 ZYNQ 之linux驱动开发指南
2)实验平台:正点原子领航者ZYNQ开发板3)平台购买地址:https://item.taobao.com/item.htm?&id=6061601087614)全套实验源码+手册+视频*载下**:http://www.openedv.com/docs/boards/fpga/zdyz_linhanz.html5)对正点原子FPGA感兴趣的同学可以加群讨论:8767449006)关注正点原子公众号,获取最新资料

通过&cpu0就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点“intc: interrupt-controller@00a01000”,节点label是intc,而节点名字就很长了,为“interrupt-controller@00a01000”。很明显通过&intc来访问“interrupt-controller@00a01000”这个节点要方便很多!所以如果我们要在设备树中引用其它的节点,那么就可以在这个被引用的节点前加上“label:”,这样我们就可以很方便的通过“&label”的方式进行引用了。24.3.7向节点追加或修改内容这里面有两个知识点:向节点追加内容,也就是添加属性;另一个就是修改节点的内容。我相信大家都理解我这里说的意思。在实际的开发当中肯定是有这样的需求存在的,例如在我们的领航者开发板上有一个eeprom器件(24c64)和一个rtc器件(pcf8563),假如它俩都是挂在ZYNQ的i2c0总线下的。那么现在要把这两个设备添加到i2c0总线下,打开zynq-7000.dtsi文件,可以看到PS的两组i2c控制器节点定义,如下所示:示例代码24.3.7.1 zynq-7000.dtsi i2c节点
- 122 i2c0: i2c@e0004000 {
- 123 compatible = "cdns,i2c-r1p10";
- 124 status = "disabled";
- 125 clocks = <&clkc 38>;
- 126 interrupt-parent = <&intc>;
- 127 interrupts = <0 25 4>;
- 128 reg = <0xe0004000 0x1000>;
- 129 #address-cells = <1>;
- 130 #size-cells = <0>;
- 131 };
- 132
- 133 i2c1: i2c@e0005000 {
- 134 compatible = "cdns,i2c-r1p10";
- 135 status = "disabled";
- 136 clocks = <&clkc 39>;
- 137 interrupt-parent = <&intc>;
- 138 interrupts = <0 48 4>;
- 139 reg = <0xe0005000 0x1000>;
- 140 #address-cells = <1>;
- 141 #size-cells = <0>;
- 142 };
复制代码
因为现在要把开发板的两个i2c器件添加到i2c0总线下,直接在i2c0节点下创建两个子节点即可,一个子节点对应的是eeprom,另一个子节点对应的是rtc,那么最简单的方法就是直接在zynq-7000.dtsi文件的i2c0节点中添加这两个节点子节点即可,如下所示:示例代码24.3.7.2 zynq-7000.dtsi 添加i2c器件
- 122 i2c0: i2c@e0004000 {
- 123 compatible = "cdns,i2c-r1p10";
- 124 status = "disabled";
- 125 clocks = <&clkc 38>;
- 126 interrupt-parent = <&intc>;
- 127 interrupts = <0 25 4>;
- 128 reg = <0xe0004000 0x1000>;
- 129 #address-cells = <1>;
- 130 #size-cells = <0>;
- 131
- 132 24c64@50 {
- 133 compatible = "atmel,24c64";
- 134 reg = <0x50>;
- 135 pagesize = <32>;
- 136 };
- 137
- 138 rtc@51 {
- 139 compatible = "nxp,pcf8563";
- 140 reg = <0x51>;
- 141 };
- 142 };
- 143
- 144 i2c1: i2c@e0005000 {
- 145 compatible = "cdns,i2c-r1p10";
- 146 status = "disabled";
- 147 clocks = <&clkc 39>;
- 148 interrupt-parent = <&intc>;
- 149 interrupts = <0 48 4>;
- 150 reg = <0xe0005000 0x1000>;
- 151 #address-cells = <1>;
- 152 #size-cells = <0>;
- 153 };
复制代码
第132~136行就是在i2c0总线下添加了eeprom设备,138~141行添加了rtc设备(注意:我这里只是给大家做演示,你们不要去改这个文件);但是这样会有个问题,i2c0节点是定义在zynq-7000.dtsi文件中的,而zynq-7000.dtsi是设备树头文件,前面也跟大家说到过,该文件是zynq-7000系列处理器的一个通用设备树头文件,也就是说它是会被其他dts文件所包含的,直接在i2c0节点中添加这两个子节点就相当于在所有的zynq-7000系列处理器开发板上都添加了这两个设备,如果其他的板子并没有这两个设备呢!因此,按照示例代码24.3.12这样写肯定是不行的。这里就要引入另外一个内容,那就是向节点追加数据,我们现在要解决的就是如何向i2c0节点追加两个子节点,而且不能影响到其它使用zynq-7000系列处理器的开发板。在本篇中我们使用的设备树文件为system-top.dts,因此我们需要在system-top.dts文件中完成数据追加的内容,方式如下:示例代码24.3.7.3 节点追加数据方法
- 1 &i2c0 {
- 2 /* 要追加或修改的内容 */
- 3 };
复制代码
第1行,&i2c0表示要引用到i2c0这个label所对应的节点,也就是zynq-7000.dtsi文件中的“i2c0: i2c@e0004000”。第2行,花括号内就是要向i2c0这个节点添加的内容,包括修改某些属性的值。打开system-top.dts,这样我们就可以直接在该文件中追加内容了:示例代码24.3.7.4 system-top.dts 向i2c0节点追加内容
- 8 /dts-v1/;
- 9 #include "zynq-7000.dtsi"
- 10 #include "pl.dtsi"
- 11 #include "pcw.dtsi"
- 12 / {
- 13 model = "Alientek ZYNQ Development Board";
- 14
- 15 chosen {
- 16 bootargs = "console=ttyPS0,115200 earlyprintk root=/dev/mmcblk0p2 rw rootwait";
- 17 stdout-path = "serial0:115200n8";
- 18 };
- 19 aliases {
- 20 ethernet0 = &gem0;
- 21 i2c0 = &i2c_2;
- 22 i2c1 = &i2c0;
- 23 i2c2 = &i2c1;
- 24 serial0 = &uart0;
- 25 serial1 = &uart1;
- 26 spi0 = &qspi;
- 27 };
- 28 memory {
- 29 device_type = "memory";
- 30 reg = <0x0 0x20000000>;
- 31 };
- 32 };
- 33
- 34 &i2c0 {
- 35 clock-frequency = <100000>;
- 36 status = "okay";
- 37
- 38 24c64@50 {
- 39 compatible = "atmel,24c64";
- 40 reg = <0x50>;
- 41 pagesize = <32>;
- 42 };
- 43
- 44 rtc@51 {
- 45 compatible = "nxp,pcf8563";
- 46 reg = <0x51>;
- 47 };
- 48 };
- 49
- 50 &gem0 {
- 51 local-mac-address = [00 0a 35 00 1e 53];
- 52 };
复制代码
第34~48行就是向i2c0节点添加/修改数据,比如35的属性“clock-frequency = <100000>”就表示将i2c0的时钟设置为100KHz,“clock-frequency”就是新添加的属性。第36行,将status属性的值由原来的disabled改为okay,这是修改节点的属性值。第38~47行,我们向i2c0子节点追加了两个子节点,“24c64@50”和“rtc@51”。除此之外,第12~32行,其实就是向zynq-7000.dtsi中定义的根节点中追加了一些节点。注意,这里只是给大家延时,大家不要去修改这些文件,后面用到的时候我会再说!!!因为示例代码24.3.14中的内容是system-top.dts这个文件内的,所以不会对使用ZYNQ-7000系列处理器的其它板子造成任何影响。这个就是向节点追加或修改内容,重点就是通过&label来访问节点,然后直接在里面编写要追加或者修改的内容。例如在pcw.dtsi文件中,可以看到很多的节点引用、向节点追加内容、修改节点内容的示例,如下所示:

图 35.3.5 pcw.dtsi示例
24.3.8特殊节点在根节点“/”中有那么几个特殊的子节点:aliases、chosen以及memory,我们接下来看一下这三个比较特殊的节点,我们会发现这三个节点都是没有compatible属性,也就是说它们对应的并不是一个真实的设备。1、aliases节点打开system-top.dts文件,可以看到aliases节点的内容如下所示:示例代码24.3.8.1 system-top.dts aliases节点
- 19 aliases {
- 20 ethernet0 = &gem0;
- 21 i2c0 = &i2c_2;
- 22 i2c1 = &i2c0;
- 23 i2c2 = &i2c1;
- 24 serial0 = &uart0;
- 25 serial1 = &uart1;
- 26 spi0 = &qspi;
- 27 };
复制代码
单词aliases的意思是“别名”,因此aliases节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。但是需要注意的是,这里说的方便访问节点并不是在设备树中访问节点,例如前面说到的使用“&label”的方式访问设备树中的节点,而是内核当中方便定位节点,例如在内核中通过ethernet0就可以定位到gem0节点(&gem0引用的节点),再例如内核通过serial0就可以找到uart0节点。2、chosen节点chosen节点一般会有两个属性,“bootargs”和“stdout-path”。打开system-top.dts文件,找到chosen节点,内容如下所示:示例代码24.3.8.2 chosen节点
- 15 chosen {
- 16 bootargs = "console=ttyPS0,115200 earlyprintk root=/dev/mmcblk0p2 rw rootwait";
- 17 stdout-path = "serial0:115200n8";
- 18 };
复制代码
在chosen节点当中,属性stdout-path = “serial0:115200n8”,表示标准输出设备使用串口serial0,在system-top.dts文件当中,serial0其实是一个别名,指向的就是uart0;“115200”则表示串口的波特率为115200,“n”表示无校验位,“8”则表示有8位数据位,相信大家都明白这些是什么意思。当你看到chosen节点中的bootargs属性的时候有没有想到U-Boot的bootargs环境变量呢?内核的bootargs参数不是由U-Boot传给它的吗?为什么要在内核设备树根节点下的chosen节点中定义呢?他们俩有什么区别呢?那么关于这些问题稍后再给大家解释,这里大家想思考另一个问题:“stdout-path”属性指定了标准输出设备,而bootargs参数当中也指定了标准输出设备(console=ttyPS0,115200,ttyPS0其实指的就是根文件系统下的/dev/ttyPS0这个设备文件,那么它对应的硬件设备其实就是板子的uart0),那么内核在初始化标准输出设备的时候到底听谁的呢?关于这个问题,笔者开始也想不明白,于是呼去内核源码中找了找,在内核源码drivers/of/base.c文件中看到了下面这段代码:示例代码24.3.8.3 of_console_check函数
- 1822 /**
- 1823 * of_console_check() - Test and setup console for DT setup
- 1824 * @dn - Pointer to device node
- 1825 * @name - Name to use for preferred console without index. ex. "ttyS"
- 1826 * @index - Index to use for preferred console.
- 1827 *
- 1828 * Check if the given device node matches the stdout-path property in the
- 1829 * /chosen node. If it does then register it as the preferred console and return
- 1830 * TRUE. Otherwise return FALSE.
- 1831 */
- 1832 bool of_console_check(struct device_node *dn, char *name, int index)
- 1833 {
- 1834 if (!dn || dn != of_stdout || console_set_on_cmdline)
- 1835 return false;
- 1836
- 1837 /*
- 1838 * XXX: cast `options' to char pointer to suppress complication
- 1839 * warnings: printk, UART and console drivers expect char pointer.
- 1840 */
- 1841 return !add_preferred_console(name, index, (char *)of_stdout_options);
- 1842 }
复制代码
看这个函数的名字“of_console_check”,意思是控制台校验(控制台大家可以理解为linux的标准输入、输入终端),第1834行当中的of_stdout其实是内核解析stdout-path = “serial0:115200n8”时得到的serial0指向的设备节点,也就是我们的串口0,;而console_set_on_cmdline是一个int类型的变量,如果bootargs字符串当中指定了console=xxxxx,那么内核也会解析到,并且将console_set_on_cmdline变量设置为1;所以根据代码中的第1834行以及函数定义前面的注释信息,我的猜想如下:在of_console_check函数中会判断设备树stdout-path属性是否定义了,如果定义了则它拥有优先级。当然这是我的猜测,我并没有去验证,不想花这个时间去研究了,如果大家有时间可以去找找看,这里就不说这个问题了。现在给大家解释前面说到的那些问题:内核的bootargs参数不是由U-Boot传给它的吗?为什么还要在内核设备树根节点下的chosen节点中定义bootargs呢?他们俩有什么区别呢?下面给大家一一解释一下。前面讲解uboot的时候说过,uboot在启动Linux内核的时候会将bootargs的值传递给Linux内核,bootargs会作为Linux内核的命令行参数,Linux内核启动的时候会打印出命令行参数(也就是uboot传递进来的bootargs的值),如所示:

图 35.3.6 内核启动打印命令行参数
但是我们使用的这个U-Boot,它的环境变量当中并没有定义bootargs变量,大家可以进入U-Boot命令行,通过print命令打印出所有的环境变量,你会发现并没有定义bootargs,那这跟我们前面说的不相符了呀,而事实并不如此。在uboot源码中全局搜索“chosen”这个字符串,看看能不能找到一些蛛丝马迹,果然在U-Boot源码目录的common/fdt_support.c文件中有个fdt_chosen函数,此函数内容如下所示:示例代码24.3.8.4 uboot源码中的fdt_chosen函数
- 275 int fdt_chosen(void *fdt)
- 276 {
- 277 int nodeoffset;
- 278 int err;
- 279 char *str; /* used to set string properties */
- 280
- 281 err = fdt_check_header(fdt);
- 282 if (err < 0) {
- 283 printf("fdt_chosen: %s\n", fdt_strerror(err));
- 284 return err;
- 285 }
- 286
- 287 /* find or create "/chosen" node. */
- 288 nodeoffset = fdt_find_or_add_subnode(fdt, 0, "chosen");
- 289 if (nodeoffset < 0)
- 290 return nodeoffset;
- 291
- 292 str = getenv("bootargs");
- 293 if (str) {
- 294 err = fdt_setprop(fdt, nodeoffset, "bootargs", str,
- 295 strlen(str) + 1);
- 296 if (err < 0) {
- 297 printf("WARNING: could not set bootargs %s.\n",
- 298 fdt_strerror(err));
- 299 return err;
- 300 }
- 301 }
- 302
- 303 return fdt_fixup_stdout(fdt, nodeoffset);
- 304 }
复制代码
第288行,调用函数fdt_find_or_add_subnode从内核设备树(.dtb,因为此时内核dtb文件已经被拷贝到DDR中了)中找到chosen节点,如果没有找到的话就会自己创建一个chosen节点。第292行,读取uboot中bootargs环境变量的内容。第293行,判断如果读取bootargs环境变量成功,则执行if { }中的代码。第294行,调用函数fdt_setprop向内核设备的chosen节点添加bootargs属性,并且bootargs属性的值就是环境变量bootargs的内容。(因为此时内核dtb文件已经被拷贝到DDR中了,U-Boot可以通过内核设备树dtb的起始地址对dtb数据进行修改)。所以从上面这段代码可以看出来,如果U-Boot定义了bootargs环境变量,则会通过fdt_setprop函数在内核设备树的chosen节点追加bootargs属性,它的值就是U-Boot环境变量bootargs的值,如果是这样,那么内核设备树chosen节点的bootargs属性就会被修改。但是对于我们使用这个U-Boot来说,它并没有定义bootargs环境变量,所以使用的就是内核设备树chosen节点下的bootargs属性,也就是说U-Boot的环境变量bootargs拥有最高的优先级。接下来我们顺着fdt_chosen函数一点点的抽丝剥茧,看看都有哪些函数调用了fdt_chosen,一直找到最终的源头。这里我就不卖关子了,直接告诉大家整个流程是怎么样的,见图 35.3.7:

图 35.3.7 fdt_chosen函数调用流程
图 35.3.7中框起来的部分就是函数do_bootm_linux函数的执行流程,也就是说do_bootm_linux函数会通过一系列复杂的调用,最终通过fdt_chosen函数在内核设备树chosen节点中添加bootargs属性。而U-Boot的bootcmd命令最终会执行bootz命令,而bootz命令启动Linux内核的时候会运行do_bootm_linux函数,至此,真相大白! 3、memory节点memory节点看名字就知道跟内存是有关系的,如下所示:示例代码24.3.8.5 memory节点
- 28 memory {
- 29 device_type = "memory";
- 30 reg = <0x0 0x20000000>;
- 31 };
复制代码
memory节点描述了系统内存的基地址以及系统内存大小,“reg = <0x0 0x20000000>”就表示系统内存的起始地址为0x0,大小为0x20000000,也就是512MB,该节点一般只有这两个属性,device_type属性的值固定为“memory”。24.3.9常用节点本来这小节给大家讲一些常用到的节点,例如中断控制器、GPIO控制器以及在节点当中如何使用中断、如何使用gpio等。当想了想还是放在后面我们用到的时候再给大家介绍。24.4驱动与设备节点的匹配这部分内容已经在前面跟大家讲过了,具体请看35.3.4小节中的第一个小点compatible属性介绍。24.5内核启动过程中解析设备树Linux内核在启动的时候会解析内核DTB文件,然后在根文件系统的/proc/device-tree(后面给大家演示)目录下生成相应的设备树节点文件。接下来我们简单分析一下Linux内核是如何解析DTB文件的,流程如图43.7.1所示:

图 35.5.1 设备树中节点解析流程
从上图中可以看出,在start_kernel函数中完成了设备树节点解析的工作,最终实际工作的函数为unflatten_dt_node。那么具体如何进行设备树解析的这里就不给大家进行一一分析了,如果大家有时间可以自个去研究研究!24.6设备树在系统中的体现Linux内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree目录下根据节点名字创建不同文件夹,如下图所示:

图 35.6.1 根节点的属性以及子节点
上图列出来就是目录/proc/device-tree目录下的内容,/proc/device-tree目录下是根节点“/”的所有属性和子节点,我们依次来看一下这些属性和子节点。1、根节点“/”各个属性在图 35.6.1中,根节点下的属性表现为一个个的文件(大家可以用ls -l查看到文件的类型),比如图 35.6.1中的“#address-cells”、“#size-cells”、“compatible”、“model”和“name”这5个文件,它们在设备树中就是根节点的5个属性。既然是文件那么肯定可以查看其内容,输入cat命令来查看model和compatible这两个文件的内容,结果如下图所示:

图 35.6.2 model和compatible文件内容
从图 35.6.2可以看出,文件model的内容是“Alientek ZYNQ Development Board”,文件compatible的内容为“xlnx,zynq-7000”。这跟system-top.dts文件根节点的model属性值、以及zynq-7000.dtsi文件根节点的compatible属性值是完全一样的。2、根节点“/”各子节点图 35.6.1中列出的各个文件夹就是根节点“/”的各个子节点,比如“aliases”、“cpus”、“chosen”和“amba”等等。大家可以查看我们用到的设备树文件,看看根节点的子节点都有哪些,看看是否和图 35.6.1中的一致。/proc/device-tree目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进入/proc/device-tree/amba目录中就可以看到amba节点的所有子节点,如所示:

图 35.6.3 amba节点的所有属性和子节点
和根节点“/”一样,图 35.6.3中的所有文件分别为amba节点的属性文件和子节点文件夹。大家可以自行查看一下这些属性文件的内容是否和我们使用的设备树中amba节点的属性值相同。24.7绑定信息文档设备树是用来描述板子上的硬件设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在Linux内核源码中有详细的.txt文档描述了如何添加节点,这些.txt文档叫做绑定文档,路径为:Linux源码目录/Documentation/devicetree/bindings,如所示:

图 35.7.1 绑定文档
比如我们现在要想在ZYNQ 7010/7020这颗SOC的I2C下添加一个节点,那么就可以查看Documentation/devicetree/bindings/i2c/i2c-cadence.txt(文件的名字一般都是以i2c-xxx.txt命名的,xxx一般是制造商),此文档详细的描述了ZYNQ-7000系列处理器如何在设备树中添加I2C设备节点,文档内容如下所示:
- Binding for the Cadence I2C controller
- Required properties:
- - reg: Physical base address and size of the controller's register area.
- - compatible: Should contain one of:
- * "cdns,i2c-r1p10"
- Note: Use this when cadence i2c controller version 1.0 is used.
- * "cdns,i2c-r1p14"
- Note: Use this when cadence i2c controller version 1.4 is used.
- - clocks: Input clock specifier. Refer to common clock bindings.
- - interrupts: Interrupt specifier. Refer to interrupt bindings.
- - #address-cells: Should be 1.
- - #size-cells: Should be 0.
- Optional properties:
- - clock-frequency: Desired operating frequency, in Hz, of the bus.
- - clock-names: Input clock name, should be 'pclk'.
- Example:
- i2c@e0004000 {
- compatible = "cdns,i2c-r1p10";
- clocks = <&clkc 38>;
- interrupts = <GIC_SPI 25 IRQ_TYPE_LEVEL_HIGH>;
- reg = <0xe0004000 0x1000>;
- clock-frequency = <400000>;
- #address-cells = <1>;
- #size-cells = <0>;
- };
复制代码
有时候使用的一些芯片在Documentation/devicetree/bindings目录下找不到对应的文档,这个时候就要咨询芯片的提供商,让他们给你提供参考的设备树文件。24.8设备树常用of操作函数设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用reg属性描述了某个外设的寄存器地址为0X02005482,长度为0X400,我们在编写驱动的时候需要获取到reg属性的0X02005482和0X400这两个值,然后初始化外设。Linux内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做OF函数。这些OF函数原型都定义在include/linux/of.h文件中。24.8.1查找节点的OF函数设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的属性信息,必须先获取到这个设备的节点。Linux内核使用device_node结构体来描述一个节点,此结构体定义在文件include/linux/of.h中,定义如下:示例代码24.8.1.1 device_node节点
- 49 struct device_node {
- 50 const char *name; /* 节点名字 */
- 51 const char *type; /* 设备类型 */
- 52 phandle phandle;
- 53 const char *full_name; /* 节点全名 */
- 54 struct fwnode_handle fwnode;
- 55
- 56 struct property *properties; /* 属性 */
- 57 struct property *deadprops; /* removed属性 */
- 58 struct device_node *parent; /* 父节点 */
- 59 struct device_node *child; /* 子节点 */
- 60 struct device_node *sibling;
- 61 struct kobject kobj;
- 62 unsigned long _flags;
- 63 void *data;
- 64 #if defined(CONFIG_SPARC)
- 65 const char *path_component_name;
- 66 unsigned int unique_id;
- 67 struct of_irq_controller *irq_trans;
- 68 #endif
- 69 };
复制代码
与查找节点有关的OF函数有5个,我们依次来看一下。1、of_find_node_by_name函数of_find_node_by_name函数通过节点名字查找指定的节点,函数原型如下:
- struct device_node *of_find_node_by_name(struct device_node *from,
- const char *name);
复制代码
函数参数和返回值含义如下:from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。name:要查找的节点名字。返回值:找到的节点,如果为NULL表示查找失败。2、of_find_node_by_type函数of_find_node_by_type函数通过device_type属性查找指定的节点,函数原型如下:struct device_node *of_find_node_by_type(struct device_node *from, const char *type)函数参数和返回值含义如下:from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。type:要查找的节点对应的type字符串,也就是device_type属性值。返回值:找到的节点,如果为NULL表示查找失败。3、of_find_compatible_node函数of_find_compatible_node函数根据device_type和compatible这两个属性查找指定的节点,函数原型如下:
- struct device_node *of_find_compatible_node(struct device_node *from,
- const char *type,
- const char *compatible)
复制代码
函数参数和返回值含义如下:from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。type:要查找的节点对应的type字符串,也就是device_type属性值,可以为NULL,表示忽略掉device_type属性。compatible:要查找的节点所对应的compatible属性列表。返回值:找到的节点,如果为NULL表示查找失败4、of_find_matching_node_and_match函数of_find_matching_node_and_match函数通过of_device_id匹配表来查找指定的节点,函数原型如下:
- struct device_node *of_find_matching_node_and_match(struct device_node *from,
- const struct of_device_id *matches,
- const struct of_device_id **match)
复制代码
函数参数和返回值含义如下:from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。matches:of_device_id匹配表,也就是在此匹配表里面查找节点。match:找到的匹配的of_device_id。返回值:找到的节点,如果为NULL表示查找失败5、of_find_node_by_path函数of_find_node_by_path函数通过节点路径来查找指定的节点,函数原型如下:
- inline struct device_node *of_find_node_by_path(const char *path)
复制代码
函数参数和返回值含义如下:path:带有全路径的节点名,可以使用节点的别名(用aliens节点中定义的别名)。返回值:找到的节点,如果为NULL表示查找失败24.8.2查找父/子节点的OF函数Linux内核提供了几个查找节点对应的父节点或子节点的OF函数,我们依次来看一下。1、of_get_parent函数of_get_parent函数用于获取指定节点的父节点(如果有父节点的话),函数原型如下:
- struct device_node *of_get_parent(const struct device_node *node)
复制代码
函数参数和返回值含义如下:node:要查找的父节点的节点。 返回值:找到的父节点。2、of_get_next_child函数of_get_next_child函数用迭代的查找子节点,函数原型如下:
- struct device_node *of_get_next_child(const struct device_node *node,
- struct device_node *prev)
复制代码
函数参数和返回值含义如下:node:父节点。prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始。返回值:找到的下一个子节点。24.8.3提取属性值的OF函数设备树节点的属性保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux内核中使用结构体property表示属性,此结构体同样定义在文件include/linux/of.h中,内容如下:示例代码24.8.3.1 property结构体
- 35 struct property {
- 36 char *name; /* 属性名字 */
- 37 int length; /* 属性长度 */
- 38 void *value; /* 属性值 */
- 39 struct property *next; /* 下一个属性 */
- 40 unsigned long _flags;
- 41 unsigned int unique_id;
- 42 struct bin_attribute attr;
- 43 };
复制代码
Linux内核也提供了提取属性值的OF函数,我们依次来看一下。1、of_find_property函数of_find_property函数用于查找指定的属性,函数原型如下:
- property *of_find_property(const struct device_node *np,
- const char *name,
- int *lenp)
复制代码
函数参数和返回值含义如下:np:设备节点。name: 属性名字。lenp:属性值的字节数返回值:找到的属性。2、of_property_count_elems_of_size函数of_property_count_elems_of_size函数用于获取属性中元素的数量,比如reg属性值是一个数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:
- int of_property_count_elems_of_size(const struct device_node *np,
- const char *propname,
- int elem_size)
复制代码
函数参数和返回值含义如下:np:设备节点。proname: 需要统计元素数量的属性名字。elem_size:元素长度。返回值:得到的属性元素数量。3、of_property_read_u32_index函数of_property_read_u32_index函数用于从属性中获取指定下标(属性值是一个u32数据组成的数组)的u32类型数据值(无符号32位),比如某个属性有多个u32类型的值,那么就可以使用此函数来获取指定下标的数据值,此函数原型如下:
- int of_property_read_u32_index(const struct device_node *np,
- const char *propname,
- u32 index,
- u32 *out_value)
复制代码
函数参数和返回值含义如下:np:设备节点。proname: 要读取的属性名字。index:要读取的值的下标。out_value:读取到的值返回值:0读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA表示没有要读取的数据,-EOVERFLOW表示属性值列表太小。4、of_property_read_u8_array函数 of_property_read_u16_array函数of_property_read_u32_array函数 of_property_read_u64_array函数这4个函数分别是读取属性中u8、u16、u32和u64类型的数组数据,比如大多数的reg属性都是数组数据,可以使用这4个函数一次读取出reg属性中的所有数据。这四个函数的原型如下:
- int of_property_read_u8_array(const struct device_node *np,
- const char *propname,
- u8 *out_values,
- size_t sz)
- int of_property_read_u16_array(const struct device_node *np,
- const char *propname,
- u16 *out_values,
- size_t sz)
- int of_property_read_u32_array(const struct device_node *np,
- const char *propname,
- u32 *out_values,
- size_t sz)
- int of_property_read_u64_array(const struct device_node *np,
- const char *propname,
- u64 *out_values,
- size_t sz)
复制代码
函数参数和返回值含义如下:np:设备节点。proname: 要读取的属性名字。out_value:读取到的数组值,分别为u8、u16、u32和u64。sz:要读取的数组元素数量。返回值:0,读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA表示没有要读取的数据,-EOVERFLOW表示属性值列表太小。5、of_property_read_u8函数of_property_read_u16函数of_property_read_u32函数of_property_read_u64函数有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用于读取u8、u16、u32和u64类型属性值,函数原型如下:
- int of_property_read_u8(const struct device_node *np,
- const char *propname,
- u8 *out_value)
- int of_property_read_u16(const struct device_node *np,
- const char *propname,
- u16 *out_value)
- int of_property_read_u32(const struct device_node *np,
- const char *propname,
- u32 *out_value)
- int of_property_read_u64(const struct device_node *np,
- const char *propname,
- u64 *out_value)
复制代码
函数参数和返回值含义如下:np:设备节点。proname: 要读取的属性名字。out_value:读取到的数组值。返回值:0,读取成功,负值,读取失败,-EINVAL表示属性不存在,-ENODATA表示没有要读取的数据,-EOVERFLOW表示属性值列表太小。6、of_property_read_string函数of_property_read_string函数用于读取属性中字符串值,函数原型如下:
- int of_property_read_string(struct device_node *np,
- const char *propname,
- const char **out_string)
复制代码
函数参数和返回值含义如下:np:设备节点。proname: 要读取的属性名字。out_string:读取到的字符串值。返回值:0,读取成功,负值,读取失败。7、of_n_addr_cells函数of_n_addr_cells函数用于获取#address-cells属性值,函数原型如下:int of_n_addr_cells(struct device_node *np)函数参数和返回值含义如下:np:设备节点。返回值:获取到的#address-cells属性值。8、of_n_size_cells函数of_size_cells函数用于获取#size-cells属性值,函数原型如下:int of_n_size_cells(struct device_node *np)函数参数和返回值含义如下:np:设备节点。返回值:获取到的#size-cells属性值。24.8.4其他常用的OF函数1、of_device_is_compatible函数of_device_is_compatible函数用于查看节点的compatible属性是否有包含compat指定的字符串,也就是检查设备节点的兼容性,函数原型如下:
- int of_device_is_compatible(const struct device_node *device,
- const char *compat)
复制代码
函数参数和返回值含义如下:device:设备节点。compat:要查看的字符串。返回值:0,节点的compatible属性中不包含compat指定的字符串;正数,节点的compatible属性中包含compat指定的字符串。2、of_get_address函数of_get_address函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值,函数属性如下:
- const __be32 *of_get_address(struct device_node *dev,
- int index,
- u64 *size,
- unsigned int *flags)
复制代码
函数参数和返回值含义如下:dev:设备节点。index:要读取的地址标号。size:地址长度。flags:参数,比如IORESOURCE_IO、IORESOURCE_MEM等返回值:读取到的地址数据首地址,为NULL的话表示读取失败。3、of_translate_address函数of_translate_address函数负责将从设备树读取到的地址转换为物理地址,函数原型如下:
- u64 of_translate_address(struct device_node *dev,
- const __be32 *in_addr)
复制代码
函数参数和返回值含义如下:dev:设备节点。in_addr:要转换的地址。返回值:得到的物理地址,如果为OF_BAD_ADDR的话表示转换失败。4、of_address_to_resource函数IIC、SPI、GPIO等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux内核使用resource结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用resource结构体描述的都是设备资源信息,resource结构体定义在文件include/linux/ioport.h中,定义如下:示例代码24.8.4.1 resource结构体
- 18 struct resource {
- 19 resource_size_t start;
- 20 resource_size_t end;
- 21 const char *name;
- 22 unsigned long flags;
- 23 struct resource *parent, *sibling, *child;
- 24 };
复制代码
对于32位的SOC来说,resource_size_t是u32类型的。其中start表示开始地址,end表示结束地址,name是这个资源的名字,flags是资源标志位,一般表示资源类型,可选的资源标志定义在文件include/linux/ioport.h中,如下所示:示例代码24.8.4.2 资源标志
- 1 #define IORESOURCE_BITS 0x000000ff
- 2 #define IORESOURCE_TYPE_BITS 0x00001f00
- 3 #define IORESOURCE_IO 0x00000100
- 4 #define IORESOURCE_MEM 0x00000200
- 5 #define IORESOURCE_REG 0x00000300
- 6 #define IORESOURCE_IRQ 0x00000400
- 7 #define IORESOURCE_DMA 0x00000800
- 8 #define IORESOURCE_BUS 0x00001000
- 9 #define IORESOURCE_PREFETCH 0x00002000
- 10 #define IORESOURCE_READONLY 0x00004000
- 11 #define IORESOURCE_CACHEABLE 0x00008000
- 12 #define IORESOURCE_RANGELENGTH 0x00010000
- 13 #define IORESOURCE_SHADOWABLE 0x00020000
- 14 #define IORESOURCE_SIZEALIGN 0x00040000
- 15 #define IORESOURCE_STARTALIGN 0x00080000
- 16 #define IORESOURCE_MEM_64 0x00100000
- 17 #define IORESOURCE_WINDOW 0x00200000
- 18 #define IORESOURCE_MUXED 0x00400000
- 19 #define IORESOURCE_EXCLUSIVE 0x08000000
- 20 #define IORESOURCE_DISABLED 0x10000000
- 21 #define IORESOURCE_UNSET 0x20000000
- 22 #define IORESOURCE_AUTO 0x40000000
- 23 #define IORESOURCE_BUSY 0x80000000
复制代码
大家一般最常见的资源标志就是IORESOURCE_MEM、IORESOURCE_REG和IORESOURCE_IRQ等。接下来我们回到of_address_to_resource函数,此函数看名字像是从设备树里面提取资源值,但是本质上就是将reg属性值,然后将其转换为resource结构体类型,函数原型如下所示
- int of_address_to_resource(struct device_node *dev,
- int index,
- struct resource *r)
复制代码
函数参数和返回值含义如下:dev:设备节点。index:地址资源标号。r:得到的resource类型的资源值。返回值:0,成功;负值,失败。5、of_iomap函数of_iomap函数用于直接内存映射,以前我们会通过ioremap函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过of_iomap函数来获取内存地址所对应的虚拟地址,不需要使用ioremap函数了。当然了,你也可以使用ioremap函数来完成物理地址到虚拟地址的内存映射,只是在采用设备树以后,大部分的驱动都使用of_iomap函数了。of_iomap函数本质上也是将reg属性中地址信息转换为虚拟地址,如果reg属性有多段的话,可以通过index参数指定要完成内存映射的是哪一段,of_iomap函数原型如下:
- void __iomem *of_iomap(struct device_node *np,
- int index)
复制代码
函数参数和返回值含义如下:np:设备节点。index:reg属性中要完成内存映射的段,如果reg属性只有一段的话index就设置为0。返回值:经过内存映射后的虚拟内存首地址,如果为NULL的话表示内存映射失败。关于设备树常用的OF函数就先讲解到这里,Linux内核中关于设备树的OF函数不仅仅只有前面讲的这几个,还有很多OF函数我们并没有讲解,这些没有讲解的OF函数要结合具体的驱动,比如获取中断号的OF函数、获取GPIO的OF函数等等,这些OF函数我们在后面的驱动实验中再详细的讲解。关于设备树就讲解到这里,关于设备树我们重点要了解一下几点内容:①、DTS、DTB和DTC之间的区别,如何将.dts文件编译为.dtb文件。②、设备树语法,这个是重点,因为在实际工作中我们是需要修改设备树的。③、设备树的几个特殊子节点。④、关于设备树的OF操作函数,也是重点,因为设备树最终是被驱动文件所使用的,而驱动文件必须要读取设备树中的属性信息,比如内存信息、GPIO信息、中断信息等等。要想在驱动中读取设备树的属性值,那么就必须使用Linux内核提供的众多的OF函数。从下一章开始所以的Linux驱动实验都将采用设备树,从最基本的点灯,到复杂的音频、网络或块设备等驱动。将会带领大家由简入深,深度剖析设备树,最终掌握基于设备树的驱动开发技能。