一、介绍
1.1 为什么引入设备树
我们首先回顾一下我们之前学习过的驱动程序。比如:
linux驱动移植-lcd驱动基础;在arch/arm/plat-samsung/devs.c文件中定义了platform设备s3c_device_lcd,在arch/arm/mach-s3c24xx/mach-smdk2440.c文件定义了platform数据smdk2440_fb_info。
linux驱动移植-linux块设备驱动Nand Flash;在arch/arm/plat-samsung/devs.c文件中定义了platform设备s3c_device_nand ,在 arch/arm/mach-s3c24xx/common-smdk.c文件定义了platform数据smdk_nand_info。
linux驱动移植-linux块设备驱动Nand Flash;在arch/arm/plat-samsung/devs.c文件中定义了platform设备s3c_device_nand ,在 arch/arm/mach-s3c24xx/common-smdk.c文件定义了platform数据smdk_nand_info。
linux驱动移植-linux块设备驱动Nor Flash;在drivers/mtd/maps/physmap-core.c文件中定义了platform设备physmap_flash,同时在文件内定义了platform数据physmap_flash_data。
linux驱动移植-DM9000网卡驱动;在arch/arm/mach-s3c24xx/mach-smdk2440.c文件中定义了platform设备smdk2440_device_eth,同时在文件内定义了platform数据smdk2440_dm9k_pdata 。
linux驱动移植-I2C适配器驱动移植;在arch/arm/mach-s3c24xx/mach-smdk2440.c文件中定义了platform设备s3c_device_i2c0,同时在文件内定义了platform数据default_i2c_data 。
linux驱动移植-I2C设备驱动移植(AT24C08);在arch/arm/mach-s3c24xx/mach-smdk2440.c文件中定义了I2C从设备信息i2c_board_info 。
linux驱动移植-SPI控制器驱动;在arch/arm/plat-samsung/devs.c文件中定义了platform设备s3c_device_spi1,同时在arch/arm/mach-s3c24xx/mach-smdk2440.c文件内定义了platform数据s3c2440_spi1_data 。
linux驱动移植-SPI驱动移植(OLED SSD1306);在arch/arm/mach-s3c24xx/mach-smdk2440.c文件中定义了SPI从设备信息spi_board_info 。
linux驱动移植-UART设备驱动;在arch/arm/plat-samsung/init.c文件中定义了platform设备s3c24xx_uart_device0,同时在该文件内定义了platform数据uart_cfgs。
linux驱动移植-RTC驱动;在arch/arm/plat-samsung/devs.c文件中定义了platform设备s3c_device_rtc ,同时在drivers/rtc/rtc-s3c.c文件内定义了设置platform数据类型为s3c_rtc 。
在之前我们介绍的驱动程序中,Mini2440开发板板极硬件资源都是被硬编码在arch/arm/plat-samsung/devs.c和arch/arm/mach-s3c24xx/mach-smdk2440.c文件,比如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data,这些板级细节代码基本都是和开发外围设备相关的,开发板不同这些信息也会因此不同,与之而来就带来了诸多问题。
比如ARM的merge工作量较大,随着着芯片的发展,linux内核中就包含着越来越多这些描述设备的代码,导致Linux内核代码会很臃肿。为此linux还对ARM平台相关代码做出了相关的规范调整:
- ARM的核心代码保存在arch/arm目录下;
- ARM SoC core architecture code保存在arch/arm目录下;
- ARM SoC的周边外设模块的驱动保存在drivers目录下;
- ARM SoC的特定machine代码在arch/arm/mach-xxx目录下;
- ARM SoC board specific的代码(arch/arm/plat-xxx)被移除,由Device Tree机制来负责传递硬件拓扑和硬件资源信息;硬件的细节可以直接通过Device Tree传递给linux驱动程序,而不再需要在kernel中进行大量的冗余编码。
1.2 设备树(Device Tree)
linux内核从3.x开始引入设备树的概念,用于将设备信息与驱动代码分离开来。在设备树出现之前,所有关于设备的硬件信息都要编写在驱动程序里,一旦外围设备变化,驱动代码就要重写。
引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。
比如在ARM Linux内,一个.dts(device tree source)文件对应一个ARM的machine,一般放置在内核的"arch/arm/boot/dts/"目录内,比如firefly rk3288参考板的板级设备树文件就是"arch/arm/boot/dts/rk3288-firefly.dts"。这个文件可以通过$make dtbs命令编译成二进制的.dtb文件供内核驱动使用。
基于同样的软件分层设计的思想,由于一个SoC可能对应多个machine,如果每个machine的设备树都写成一个完全独立的.dts文件,那么势必相当一些.dts文件有重复的部分,为了解决这个问题,Linux设备树目录把一个SoC公用的部分或者多个machine共同的部分提炼为相应的.dtsi文件。这样每个.dts就只有自己差异的部分,公有的部分只需要"include"相应的.dtsi文件, 这样就是整个设备树的管理更加有序。
一般.dtsi文件一般是芯片外设的一些信息,比如CPU架构、主频、外设寄存器地址范围,比如UART、IIC等等。这些信息一般是不能修改的,而我们的板载外设信息一般放在dts文件中,使我们根据我们板子的外设自己添加的,是可以修改的。
1.3 设备树文件
设备树源文件格式支持dts、dtsi:
- dts:硬件的相关信息都会写在.dts为后缀的文件中,每一款硬件可以单独写一份xxxx.dts,一般在linux源码中存在大量的dts文件,对于arm架构可以在arch/arm/boot/dts找到相应的dts,一个dts文件对应一个ARM的machie。
- dtsi:对于一些相同的dts配置可以抽象到dtsi文件中,然后类似于C语言的方式可以include到dts文件中,对于同一个节点的设置情况,dts中的配置会覆盖dtsi中的配置;
这些源文件同我们的C代码一样,并不能直接使用的,而是得经过一个编译过程生成机器可运行的二进制文件:
- dtb:dtb(Device Tree Blob),dts经过dtc编译之后会得到dtb文件,dtb通过bootloader引导程序加载到内核。所以bootloader需要支持设备树才行;linux kernel也需要加入设备树的支持;
dts文件使用工具dtc进行编译,可以在ubuntu系统上通过指令apt-get install device-tree-compiler安装dtc工具,不过在内核源码scripts/dtc路径下已经包含了dtc工具。
uboot从v1.1.3开始支持设备树,其对ARM的支持则是和ARM内核支持设备树同期完成。
为了使能设备树,需要在编译uboot的时候在config文件中加入:
#define CONfiG_OF_LIBFDT·
在uboot中,可以从NAND、SD或者TFTP等任意介质中将.dtb读入内存,假设.dtb放入的内存地址为0x71000000,之后可在uboot中运行fdt addr命令设置.dtb的地址,如:
fdt addr 0x71000000·
fdt的其他命令就变得可以使用,如fdt resize、fdt print等。
然后通过以下命令来启动内核:
bootz kernel_addr initrd_address dtb_address
其中:
- 第一个参数为内核映像的地址;
- 第二个参数为initrd的地址,若不存在initrd,可以用“-”符号代替;
- 第三个参数dtb_address为.dtb文件在内存的地址
二、设备树结构
设备树源文件也是需要根据一定规则来编写的,同C语言一样,也要遵循一些语法规则。
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点;下面是一个典型的设备树结构:
/dts-v1/; /* 版本 */
#include <dt-bindings/input/input.h> /* 包含c头文件 */
#include "xxx.dtsi" /* 包含设备树头文件 */ / { /* 根节点 */ node1 { a-string-property = "A string"; a-string-list-property = "first string", "second string"; // hex is implied in byte arrays. no '0x' prefix is required a-byte-data-property = [01 23 34 56]; child-node1 { first-child-property; second-child-property = <1>; a-string-property = "Hello, world"; }; child-node2 { }; }; node2 { an-empty-property; a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */ child-node1 { }; }; };
设备树文件具有以下几种特性:
- 每个设备树文件都有一个根节点,除了根节点,每个节点都只有一个父节点;
- 每个节点用节点名字标识,节点名字的格式是node-name@unit-address;如果该节点没有reg属性,那么该节点名字中必须不能包括@和unit-address;
- 根节点的名字是确定的,必须是"/";
- 每个节点都包含了若干个key-value对(属性)来描述该节点的一些特性,每个属性的描述用
;
结束;
在dts文件中,一个节点被定义成:
[label:] node-name[@unit-address] { [properties definitions] [child nodes] }
上图中:
- []:表示该项可以省略;
- label:标签,方便在dts文件中引用;
- node-name:节点名;
- unit-address:地址;
- properties:属性定义;
- child nodes:子节点;
2.1 标签
在设备树中,如果我们想引用一个节点,必须要使用全路径,比如/node-name-1/node-name-2/node-name-N,这样当一个节点嵌套比较深的时候就不是很方便。
所以,设备树允许我们为一个节点起一个别名,也就是标签,借以省去冗长的路径。这样就可以实现类似函数调用的效果。
编译设备树的时候,相同的节点的不同属性信息都会被合,相同节点的相同的属性会被重写,使用引用可以避免移植者四处找节点,直接在板级.dts增改即可。
下面的例子中就是直接引用了dtsi中的节点cpu0,并向其中添加/修改新的属性信息:
&cpu0 { cpu0-supply = <&vdd_cpu>; };
2.2 node-name[@unit-address]
linux内核约定节点名应写成形如node-name[@unit_address]的形式,其中:
- node-name是节点名字,为ASCII字符串,最长可以是31个字符长度;节点名字应该能够清晰的描述出节点的功能,比如uart0就表示这个节点是UART0外设;
- unit_address一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话unit-address可以不要,比如cpu@0、interrupt-controller@00a01000;
下面就是典型节点的写法,该节点用于描述rtc设备硬件信息:
rtc@57000000 { compatible = "samsung,s3c2410-rtc"; reg = <0x57000000 0x100>; interrupts = <0 0 30 3>, <0 0 8 3>; status = "disabled"; };
linux中的设备树还包括一些特殊的节点,比如chosen,aliases等。
2.2.1 choosen节点
chosen 并不是一个真实的设备,chosen节点主要目的就是将uboot里面bootargs环境变量值传递给linux内核作为命令行参数,比如:
chosen { bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200"; };
2.2.2 aliases节点
aliases节点为了解决节点路径名过长的问题,引入了节点别名的概念,可以引用到一个全路径的节点。
aliases { pinctrl0 = &pinctrl_0; serial0 = &uart0; serial1 = &uart1; serial2 = &uart2; };
2.2.3 CPU节点
cpus节点下有1个或多个cpu子节点,,cpu子节点中用reg属性用来标明自己是哪一个cpu:
cpus { #address-cells = <1>; #size-cells = <0>; enable-method = "rockchip,rk3066-smp"; rockchip,pmu = <&pmu>; cpu0: cpu@500 { device_type = "cpu"; compatible = "arm,cortex-a12"; reg = <0x500>; resets = <&cru SRST_CORE0>; operating-points-v2 = <&cpu_opp_table>; #cooling-cells = <2>; /* min followed by max */ clock-latency = <40000>; clocks = <&cru ARMCLK>; dynamic-power-coefficient = <370>; }; cpu1: cpu@501 { device_type = "cpu"; compatible = "arm,cortex-a12"; reg = <0x501>; resets = <&cru SRST_CORE1>; operating-points-v2 = <&cpu_opp_table>; #cooling-cells = <2>; /* min followed by max */ clock-latency = <40000>; clocks = <&cru ARMCLK>; dynamic-power-coefficient = <370>; }; cpu2: cpu@502 { device_type = "cpu"; compatible = "arm,cortex-a12"; reg = <0x502>; resets = <&cru SRST_CORE2>; operating-points-v2 = <&cpu_opp_table>; #cooling-cells = <2>; /* min followed by max */ clock-latency = <40000>; clocks = <&cru ARMCLK>; dynamic-power-coefficient = <370>; }; cpu3: cpu@503 { device_type = "cpu"; compatible = "arm,cortex-a12"; reg = <0x503>; resets = <&cru SRST_CORE3>; operating-points-v2 = <&cpu_opp_table>; #cooling-cells = <2>; /* min followed by max */ clock-latency = <40000>; clocks = <&cru ARMCLK>; dynamic-power-coefficient = <370>; }; };
2.2.4 memory节点
所有设备树都需要一个memory设备节点,它描述了系统的物理内存布局。如果系统有多个内存块,可以创建多个memory节点,或者可以在单个memory节点的reg属性中指定这些地址范围和内存空间大小。
例如:一个64位的系统有两块内存空间:
- RAM1:起始地址是0x0,地址空间是大小0x08000000;
- RAM2:起始地址是0x10000000,地址空间大小也是0x08000000;
同时根节点下的 #address-cells = <2>和#size-cells = <2>,这个memory节点描述为:
memory@0 { device_type = "memory"; reg = <0x00000000 0x00000000 0x00000000 0x08000000>; }; memory@10000000 { device_type = "memory"; reg = <0x00000000 0x10000000 0x00000000 0x08000000>; };
2.3 key
在设备树中,键值对是描述属性的方式,比如linux驱动中可以通过设备节点中的"compatible"这个属性查找设备节点。
linux设备树语法中定义了一些具有规范意义的属性,包括:compatible, address, interrupts等,这些信息能够在内核初始化找到节点的时候,自动解析生成相应的设备信息。
此外,还有一些Linux内核定义好的,一类设备通用的有默认意义的属性,这些属性一般不能被内核自动解析生成相应的设备信息,但是内核已经编写的相应的解析提取函数,常见的有 "mac_addr","gpio","clock","power"。"regulator" 等等。
2.3.1 compatible
compatible属性也叫做兼容性属性,这是非常重要的一个属性。compatible属性的值是一个字符串列表,compatible属性用于将设备和驱动绑定起来。compatible属性的值格式如下所示:
"manufacturer,model"
其中:
- manufacturer:表示厂商;
- model:一般是模块对应的驱动名字。
比如S3C2440 RTC驱动和设备节点就是通过"compatible"进行匹配,arch/arm/boot/dts/s3c24xx.dtsi中定义有设备节点:
rtc@57000000 { compatible = "samsung,s3c2410-rtc"; /* 支持字符串数组 */ reg = <0x57000000 0x100>; interrupts = <0 0 30 3>, <0 0 8 3>; status = "disabled"; };
然后我们在内核源码中找到RTC驱动定义,在drivers/rtc/rtc-s3c.c文件,可以发现这个驱动使用了设备树描述的设备信息,我们可以找到它用来描述设备信息的结构体。
可以看出,驱动中用于匹配的结构使用的compatible和设备树中一模一样,否则就可能无法匹配,这里另外的一点是struct of_device_id数组的最后一个成员一定是空,因为相关的操作API会读取这个数组直到遇到一个空。
static const struct of_device_id s3c_rtc_dt_match[] = { // 存储设备驱动程序和设备节点之间得匹配信息 { .compatible = "samsung,s3c2410-rtc", .data = &s3c2410_rtc_data, }, ...... { /* sentinel */ }, }; MODULE_DEVICE_TABLE(of, s3c_rtc_dt_match); static struct platform_driver s3c_rtc_driver = { .probe = s3c_rtc_probe, // rtc探测函数 .remove = s3c_rtc_remove, // rtc移除函数 .driver = { .name = "s3c-rtc", .pm = &s3c_rtc_pm_ops, .of_match_table = of_match_ptr(s3c_rtc_dt_match), }, };
2.3.2 address
几乎所有的设备都需要与CPU的IO口相连,所以其IO端口信息就需要在设备节点节点中说明。常用的属性有:
- #address-cells:用来描述子节点"reg"属性的地址表中用来描述首地址的cell的数量;也就是说需要用多少个u32位数来描述该地址;
- #size-cells:用来描述子节点"reg"属性的地址表中用来描述地址长度的cell的数量;也就是说需要用多少个u32的位数来描述地址长度;
有了这两个属性,子节点中的"reg"就可以描述一块连续的地址区域。
下例中,父节点中指定了#address-cells = <1>; #size-cells = <1>,则子节点i2c@54000000中的reg中:
- 使用1个u32数来表示地址,,即0x54000000,也就是I2C寄存器寄基地址;
- 使用1个u32数来表示地址跨度,即是0x100:
/ { #address-cells = <1>; #size-cells = <1>; i2c@54000000 { compatible = "samsung,s3c2410-i2c"; reg = <0x54000000 0x100>; interrupts = <0 0 27 3>; #address-cells = <1>; #size-cells = <0>; status = "disabled"; }; }; };
2.3.3 interrupts
一个计算机系统中大量设备都是通过中断请求CPU服务的,所以设备节点中就需要在指定中断号。常用的属性有:interrupt-controller、#interrupt-cells、interrupt-parent、interrupts。
interrupt-controller表示这个节点是一个中断控制器,需要注意的是,一个SoC中可能有不止一个中断控制器,下面是在文件"arch/arm/boot/dts/s3c24xx.dtsi"中对S3C2440的中断控制器节点描述:
intc:interrupt-controller@4a000000 { compatible = "samsung,s3c2410-irq"; reg = <0x4a000000 0x100>; interrupt-controller; #interrupt-cells = <4>; };
#interrupt-cells是中断控制器节点的属性,用来描述子节点中"interrupts"属性使用了几个cell才能确定所使用的中断。以S3C2440位列,该属性的值是4,则子节点的interrupts一个cell的4个32bits整数值分别为:
- 第0位:中断控制器编号:0表示主中断源、1表示带有子中断的内部中断、2表示外部中断(也是子中断);
- 第1位:表示子中断所属的主中断的硬件中断号:对于子中断,这个参数才有意义;
- 第2位:硬件中断编号:表示位于当前中断控制器中的中断号,比如中断控制器编号为0时,对应的硬件中断编号为0~31;中断控制器编号为1时,对应的硬件中断编号为0~14;中断控制器编号为2时,对应的硬件中断编号为0~23;
- 第3位:中断类型:1为上升沿触发、2位下降沿触发、3位双边沿触发、4位高电平触发、8位低电平触发、12位高低电平触发;
注意:S3C2440物理只有一个中断控制器,软件层面上抽象出来3个中断控制器,具体参考linux驱动移植-中断子系统执行流程:
- 一个根中断控制器管理主中断源;
- 两个子中断控制器,一个用于管理外部中断源、另一个管理带有子中断的内部中断源;
示例:
uart0: serial@50000000 { compatible = "samsung,s3c2410-uart"; reg = <0x50000000 0x4000>; interrupts = <1 28 0 4>, <1 28 1 4>; status = "disabled"; };
这里配置的interrupts描述了串口0的收发中断:
- <1 28 0 4>:描述的是带有子中断内部中断,串口0主中断硬件中断号为28,串口0接收硬件中断编号为0,高定平触发;
- 串口0主中断硬件编号28,对应的软件中断号位为 44,即IRQ_UART0;
- 串口0接收硬件中断编号为0,对应的软件中断号位为74,即IRQ_S3CUART_RX0;
- <1 28 1 4>:描述的是带有子中断内部中断,串口0主中断硬件中断号为28,串口0发送硬件中断编号为1,高定平触发;
- 串口0主中断硬件编号28,对应的软件中断号位为 44,即IRQ_UART0;
- 串口0发送硬件中断编号为0,对应的软件中断号位为75,即IRQ_S3CUART_TX0;
interrupt-parent:标识此设备节点属于哪一个中断控制器,如果没有设置这个属性,会自动依附父节点的;
/ { compatible = "samsung,s3c24xx"; interrupt-parent = <&intc>; intc:interrupt-controller@4a000000 { compatible = "samsung,s3c2410-irq"; reg = <0x4a000000 0x100>; interrupt-controller; #interrupt-cells = <4>; }; ... }
2.3.4 reg
reg属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息。
reg属性的值,是一系列的“address size”,用多少个32位的数来表示address和size,由其父节点的#address-cells、#size-cells决定。示例:
/ { #address-cells = <1>; #size-cells = <1>; timer@51000000 { compatible = "samsung,s3c2410-pwm"; reg = <0x51000000 0x1000>; interrupts = <0 0 10 3>, <0 0 11 3>, <0 0 12 3>, <0 0 13 3>, <0 0 14 3>; #pwm-cells = <4>; }; };
上图描述了节点timer@51000000,timer节点描述了S3C2440的timer相关信息,重点是reg属性。
其中timer的父节点设置了#address-cells = <1>、#size-cells = <1>,因此reg属性中address=0x51000000,length=0x100。查阅S3C2440 datasheet可知,S3C2440的定时器寄存器首地址为0x51000000,但是定时器的地址长度(范围)并没有0x100这么多,这里我们重点是获取定时器寄存器首地址。
2.3.5 status
status标识了设备的状态,使用status可以去禁止设备或者启用设备,看下设备树规范中的status可选值:
- okay :表示设备正在运行;
- disabled :表示该设备目前尚未运行,但将来可能会运行;
- fail :表示设备无法运行。 在设备中检测到严重错误,确实如此没有修理就不可能投入运营;
- fail-sss:表示设备无法运行。 在设备中检测到严重错误,它是没有修理就不可能投入运营。 值的sss部分特定于设备并指示检测到的错误情况;
2.3.6 model
model属性值是一个字符串,一般model属性描述设备模块信息,比如名字什么的,比如:
model = "wm8960-audio";
2.3.7 name
name属性值为字符串, name属性用于记录节点名字, name属性已经被弃用,不推荐使用name属性,一些老的设备树文件可能会使用此属性。
2.3.8 device_type
device_type属性值为字符串, IEEE 1275会用到此属性,用于描述设备的 FCode,但是设备树没有 FCode,所以此属性也被抛弃了。此属性只能用于cpu节点或者memory节点。
memory@30000000 { device_type = "memory"; reg = <0x30000000 0x4000000>; };
2.3.9 ranges
ranges属性值可以为空或者按照“child-bus-address,parent-bus-address,length”格式编写的数字矩阵,ranges是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成:
- child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells确定此物理地址所占用的字长;
- parent-bus-address :父总线地址空间的物理地址,同样由父节点的#address-cells确定此物理地址所占用的字长;
- length:子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长;
示例:
soc { compatible = "simple-bus"; #address-cells = <1>; #size-cells = <1>; ranges = <0x0 0xe0000000 0x00100000>; serial { device_type = "serial"; compatible = "ns16550"; reg = <0x4600 0x100>; clock-frequency = <0>; interrupts = <0xA 0x8>; interrupt-parent = <&ipic>; }; };
节点soc定义的ranges属性,值为 <0x0 0xe0000000 0x00100000>,此属性值指定了一个 1024KB(0x00100000) 的地址范围,子地址空间的物理起始地址为0x0,父地址空间的物理起始地址为 0xe0000000。
serial是串口设备节点,reg属性定义了serial设备寄存器的起始地址为0x4600,寄存器长度为0x100。经过地址转换, serial设备可以从0xe0004600开始进行读写操作,0xe0004600 = 0x4600 + 0xe0000000。
2.4 节点值
节点属性(property)值标识了设备的特性,它的值(value)是多种多样的。
2.4.1 空
可能是空,也就是没有值的定义,如下:
interrupt-controller;
2.4.2 字符串
可能为字符串,如下
compatible = "samsung,s3c24xx";
也可以为字符串数组,如下:
compatible = "firefly,firefly-rk3288", "rockchip,rk3288";
2.4.3 u32、u64数值
可能是一个u32数值,一个32位的数据,用尖括号包围起来,如:
#interrupt-cells = <4>;
一个64位数据(使用2个32位数据表示),用尖括号包围起来,如:
clock-frequency = <0x00000001 0x00000000>;
也可以是一个数值数组,如下:
interrupts = <0 0 0 3>,<0 0 1 3>,<0 0 2 3>,<0 0 3 3>,<0 0 4 4>,<0 0 5 4>;
2.4.4 16进制字节序列
字节序列,用中括号包围起来,如:
local-mac-address = [000 00 de ad be ef]
参考文章
[2]Linux设备树语法详解
[5]【正点原子I.MX6U-MINI驱动篇】4、Linux设备树详解
标签:中断,cells,linux,介绍,address,属性,节点,设备 From: https://www.cnblogs.com/zyly/p/17266960.html