“设备树详解”的版本间的差异
(创建页面,内容为“==简介== ==基础知识介绍== ==DTS结构== ==DTS语法介绍== ==dts的组成== ===compatible=== ===#address-cells和#size-cells=== ===CPU addressing=== ===Memo...”) |
(→中断映射) |
||
(未显示同一用户的17个中间版本) | |||
第1行: | 第1行: | ||
==简介== | ==简介== | ||
+ | 在传统Linux内核中,ARM架构的板极硬件细节过多地被硬编码在arch/arm/plat-xxx和arch/arm/mach-xxx,比如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data,这些板级细节代码对内核来讲只不过是垃圾代码。而采用Device Tree后,许多硬件的细节可以直接透过它传递给Linux,而不再需要在kernel中进行大量的冗余编码。导致ARM的merge工作量较大。 | ||
+ | |||
+ | 之后经过一些讨论,对ARM平台的相关code做出如下相关规范调整,这个也正是引入DTS的原因。 | ||
+ | |||
+ | 1、ARM的核心代码仍然保存在arch/arm目录下 | ||
+ | |||
+ | 2、ARM SoC core architecture code保存在arch/arm目录下 | ||
+ | |||
+ | 3、ARM SOC的周边外设模块的驱动保存在drivers目录下 | ||
+ | |||
+ | 4、ARM SOC的特定代码在arch/arm/mach-xxx目录下 | ||
+ | |||
+ | 5、ARM SOC board specific的代码被移除,由DeviceTree机制来负责传递硬件拓扑和硬件资源信息。 | ||
+ | |||
+ | 本质上,Device Tree改变了原来用code方式将硬件配置信息嵌入到内核代码的方法,改用bootloader传递一个DB的形式。对于嵌入式系统,在系统启动阶段,bootloader会加载内核并将控制权转交给内核,此外,还需要把上述的三个参数信息传递给kernel,以便kernel可以有较大的灵活性。在linux kernel中,Device Tree的设计目标就是如此。 | ||
+ | |||
+ | 在devie tree中,可描述的信息包括: | ||
+ | |||
+ | 1、CPU的数量和类别 | ||
+ | |||
+ | 2、内存基地址和大小 | ||
+ | |||
+ | 3、总线和桥 | ||
+ | |||
+ | 4、外设连接 | ||
+ | |||
+ | 5、中断控制器和中断的使用情况 | ||
+ | |||
+ | 6、GPIO控制器和GPIO使用情况 | ||
+ | |||
+ | 7、clock控制器和clock使用情况 | ||
+ | |||
+ | 它基本就是一棵电路板上的CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核来识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给内核,内核会将这些资源绑定给展开的相应设备。 | ||
+ | |||
+ | Linux内核从3.x开始引入设备树的概念,用于实现驱动代码与设备信息相分离。在设备树出现以前,所有关于设备的具体信息都要写在驱动里,一旦外围设备变化,驱动代码就要重写。引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。比如在ARM Linux内,一个.dts(device tree source)文件对应一个ARM的machine,一般放置在内核的"arch/arm/boot/dts/"目录内,比如stmp1a-dk1参考板的板级设备树文件就是"arch/arm/boot/dts/ stm32mp157a-dk1.dts"。这个文件可以通过make dtbs命令编译成二进制的.dtb文件供内核驱动使用。 | ||
+ | |||
+ | 基于同样的软件分层设计的思想,由于一个SoC可能对应多个machine,如果每个machine的设备树都写成一个完全独立的.dts文件,那么势必相当一些.dts文件有重复的部分,为了解决这个问题,Linux设备树目录把一个SoC公用的部分或者多个machine共同的部分提炼为相应的.dtsi文件。这样每个.dts就只有自己差异的部分,公有的部分只需要"include"相应的.dtsi文件, 这样就是整个设备树的管理更加有序。我这里用Linux4.19.94源码自带的goodix touchscreen触摸芯片为例来分析设备树的使用和移植。这个触摸芯片的设备树节点信息在" Documentation/devicetree/bindings/input/touchscreen/goodix.txt"有详细说明,其驱动源码是" drivers/input/touchscreen/goodix.c"。 | ||
+ | |||
==基础知识介绍== | ==基础知识介绍== | ||
+ | *dts | ||
+ | 硬件的相应信息都会写在.dts为后缀的文件中,每一款硬件可以单独写一份例如stm32mp157a-dk1.dts,一般在Linux源码中存在大量的dts文件,对于arm架构可以在arch/arm/boot/dts找到相应的dts,一个dts文件对应一个ARM的machie。 | ||
+ | *dtsi | ||
+ | 值得一提的是,对于一些相同的dts配置可以抽象到dtsi文件中,然后类似于C语言的方式可以include到dts文件中,对于同一个节点的设置情况,dts中的配置会覆盖dtsi中的配置。 | ||
+ | *dtc | ||
+ | dtc是编译dts的工具,可以在Ubuntu系统上通过指令apt-get install device-tree-compiler安装dtc工具,不过在内核源码scripts/dtc路径下已经包含了dtc工具; | ||
+ | *dtb | ||
+ | dtb(Device Tree Blob),dts经过dtc编译之后会得到dtb文件,dtb通过Bootloader引导程序加载到内核。所以Bootloader需要支持设备树才行;Kernel也需要加入设备树的支持; | ||
+ | |||
==DTS结构== | ==DTS结构== | ||
+ | /dts-v1/; | ||
+ | / { | ||
+ | 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 { | ||
+ | }; | ||
+ | }; | ||
+ | }; | ||
+ | device tree的基本单元是node。这些node被组织成树状结构,除了root node,每个node都只有一个parent。一个device tree文件中只能有一个root node。每个node中包含了若干的property/value来描述该node的一些特性。每个node用节点名字(node name)标识,节点名字的格式是node-name@unit-address。如果该node没有reg属性(后面会描述这个property),那么该节点名字中必须不能包括@和unit-address。unit-address的具体格式是和设备挂在那个bus上相关。例如对于cpu,其unit-address就是从0开始编址,以此加一。而具体的设备,例如以太网控制器,其unit-address就是寄存器地址。root node的node name是确定的,必须是“/”。 | ||
+ | |||
+ | 也就是说设备树源文件的结构为: | ||
+ | |||
+ | *1个root节点”/”; | ||
+ | *root节点下面含一系列子节点,“node1” and “node2” | ||
+ | *节点node1和下又含有一系列子节点,“child-node1” and “child-node2” | ||
+ | *各个节点都有一系列属性 | ||
+ | **这些属性可能为空,如an-empty-property | ||
+ | **可能为字符串,如a-string-property | ||
+ | **可能为字符串树组,如a-string-list-property | ||
+ | **可能为Cells(由u32整数组成),如second-child-property | ||
+ | |||
==DTS语法介绍== | ==DTS语法介绍== | ||
+ | 了解了基本的device tree的结构后,我们总要把这些结构体现在device tree source code上来。在linux kernel中,扩展名是dts的文件就是描述硬件信息的device tree source file,在dts文件中,一个node被定义成如下格式: | ||
+ | |||
+ | [label:] node-name[@unit-address] { | ||
+ | [properties definitions] | ||
+ | [child nodes] | ||
+ | } | ||
+ | “[]”表示option,因此可以定义一个只有node name的空节点,label方便在dts文件中引用。 | ||
+ | |||
+ | 基本数据类型: | ||
+ | |||
+ | *text string(以null结束),以双引号括起来,如:string-property = “a string”; | ||
+ | *cells 是32位无符号整形数,以尖括号括起来,如:cell-property = <0xbeef 123 0xabcd1234>; | ||
+ | *binary data 以方括号括起来,如:binary-property = [0x01 0x23 0x45 0x67]; | ||
+ | *不同类型数据可以在同一个属性中存在,以逗号分格,如:mixed-property = “a string”, [0x01 0x23 0x45 0x67],<0x12345678>; | ||
+ | *多个字符串组成的列表也使用逗号分格,如:string-list = “red fish”,”blue fish”; | ||
+ | |||
==dts的组成== | ==dts的组成== | ||
===compatible=== | ===compatible=== | ||
+ | 每一个dts文件都是由一个root的根节点组成,内核通过根节点“/”的兼容性即可判断它启动的是什么设备,其代码结构如下 | ||
+ | |||
+ | C++ Code | ||
+ | |||
+ | / { | ||
+ | model = "HQYJ FS-MP1A Discovery Board"; | ||
+ | compatible = "st,stm32mp157a-dk1", "st,stm32mp157", "hqyj,fsmp1a"; | ||
+ | aliases { | ||
+ | ethernet0 = ðernet0; | ||
+ | serial0 = &uart4; | ||
+ | serial5 = &usart3; | ||
+ | }; | ||
+ | chosen { | ||
+ | stdout-path = "serial0:115200n8"; | ||
+ | }; | ||
+ | ... ... | ||
+ | }; | ||
+ | model属性值是,它指定制造商的设备型号。推荐的格式是:“manufacturer,model”,其中manufacturer是一个字符串描述制造商的名称,而型号指定型号。 | ||
+ | |||
+ | compatible属性值是,指定了系统的名称,是一个字符串列表,它包含了一个“<制造商>,<型号>”形式的字符串。重要的是要指定一个确切的设备,并且包括制造商的名字,以避免命名空间冲突。 | ||
+ | |||
+ | chosen 节点不代表一个真正的设备,但功能与在固件和操作系统间传递数据的地点一样,如根参数,取代以前bootloader的启动参数,控制台的输入输出参数等。 | ||
+ | |||
===#address-cells和#size-cells=== | ===#address-cells和#size-cells=== | ||
+ | |||
+ | port { | ||
+ | #address-cells = <1>; | ||
+ | #size-cells = <0>; | ||
+ | ltdc_ep0_out: endpoint@0 { | ||
+ | reg = <0>; | ||
+ | remote-endpoint = <&sii9022_in>; | ||
+ | }; | ||
+ | }; | ||
+ | |||
+ | #address-cells = <1>: 基地址、片选号等绝对起始地址所占字长,单位uint32。 | ||
+ | #size-cells = <1>: 长度所占字长,单位uint32。 | ||
+ | |||
===CPU addressing=== | ===CPU addressing=== | ||
+ | 在讨论寻址时,CPU节点代表了最简单的情况。 每个CPU都分配有一个唯一的ID,并且没有与CPU ID相关联的大小。 | ||
+ | |||
+ | cpus { | ||
+ | #address-cells = <1>; | ||
+ | #size-cells = <0>; | ||
+ | cpu0: cpu@0 { | ||
+ | compatible = "arm,cortex-a7"; | ||
+ | device_type = "cpu"; | ||
+ | reg = <0>; | ||
+ | clocks = <&rcc CK_MPU>; | ||
+ | clock-names = "cpu"; | ||
+ | operating-points-v2 = <&cpu0_opp_table>; | ||
+ | nvmem-cells = <&part_number_otp>; | ||
+ | nvmem-cell-names = "part_number"; | ||
+ | }; | ||
+ | cpu1: cpu@1 { | ||
+ | compatible = "arm,cortex-a7"; | ||
+ | device_type = "cpu"; | ||
+ | reg = <1>; | ||
+ | clocks = <&rcc CK_MPU>; | ||
+ | clock-names = "cpu"; | ||
+ | operating-points-v2 = <&cpu0_opp_table>; | ||
+ | }; | ||
+ | }; | ||
+ | |||
+ | 在cpus节点,#address-cells被设置成了1,#size-cells被设置成了0。这是说子reg值是单独的uint32,它用无大小字段表示地址。在此情况下,这两个cpu分配到的地址为0和1。cpu节点的#size-cells是0因为每个cpu只分配到了一个单独的地址。 | ||
+ | |||
+ | 你仍然需要注意reg值班需要与节点名的值相匹配。按照惯例,如果一个节点有一个reg属性,那么这个节点名称必须包括unit-address,这是reg属性的第一个address值。 | ||
+ | |||
===Memory Mapped Devices=== | ===Memory Mapped Devices=== | ||
+ | 与在cpu节点中单独的address值不同,内存映射设备被分配了一系列将要响应的地址,因此不仅需要包含内存的基地址而且还需要映射地址的长度,因此需要使用#size-cells用来表示在每个子reg元组中长度字段的大小。在以下示例中,每个address值为1 cell(32 bits),每个长度值也是1 cell,这在32 bit系统是比较典型的。64 bit设备也许会为#address-cells和#size-cells使用数值2,在device tree中获取64 bit addressing。 | ||
+ | |||
+ | /dts-v1/; | ||
+ | / { | ||
+ | #address-cells = <1>; | ||
+ | #size-cells = <1>; | ||
+ | ... | ||
+ | timers2: timer@40000000 { | ||
+ | compatible = "st,stm32-timers"; | ||
+ | reg = <0x40000000 0x400>; | ||
+ | }; | ||
+ | timers3: timer@40001000 { | ||
+ | compatible = "st,stm32-timers"; | ||
+ | reg = <0x40001000 0x400>; | ||
+ | }; | ||
+ | spi2: spi@4000b000 { | ||
+ | compatible = "st,stm32h7-spi"; | ||
+ | reg = <0x4000b000 0x400>; | ||
+ | }; | ||
+ | uart7: serial@40018000 { | ||
+ | compatible = "st,stm32h7-uart"; | ||
+ | reg = <0x40018000 0x400>; | ||
+ | }; | ||
+ | sai1: sai@4400a000 { | ||
+ | compatible = "st,stm32h7-sai"; | ||
+ | reg = <0x4400a000 0x4>; | ||
+ | }; | ||
+ | ... | ||
+ | }; | ||
+ | |||
===Non Memory Mapped Devices=== | ===Non Memory Mapped Devices=== | ||
+ | 处理器总线的其它设备为非内存映射设备。他们有地址范围,但不能被CPU直接寻址。母设备的驱动程序将代替CPU进行间接访问。以i2c设备为例,每个设备都分配了一个地址,但没有长度或范围与之相匹配。这与CPU地址分配很相似。 | ||
+ | |||
+ | C++ Code | ||
+ | etf: etf@50092000 { | ||
+ | compatible = "arm,coresight-tmc", "arm,primecell"; | ||
+ | reg = <0x50092000 0x1000>; | ||
+ | clocks = <&rcc CK_TRACE>; | ||
+ | clock-names = "apb_pclk"; | ||
+ | ports { | ||
+ | #address-cells = <1>; | ||
+ | #size-cells = <0>; | ||
+ | port@0 { | ||
+ | reg = <0>; | ||
+ | etf_in_port: endpoint { | ||
+ | slave-mode; | ||
+ | remote-endpoint = <&funnel_out_port0>; | ||
+ | }; | ||
+ | }; | ||
+ | port@1 { | ||
+ | reg = <0>; | ||
+ | etf_out_port: endpoint { | ||
+ | remote-endpoint = <&tpiu_in_port>; | ||
+ | }; | ||
+ | }; | ||
+ | }; | ||
+ | }; | ||
+ | |||
===Ranges (Address Translation)=== | ===Ranges (Address Translation)=== | ||
+ | 前面已经讨论过如何向设备分配地址,但此时这些地址只是本地设备节点,还没有说明如何从那些地址里映射到cpu可以使用的地址。根节点经常描述地址空间的CPU视图。根节点的子节点已经使用了CPU的address domain,所以不需要任何明确的映射。例如,serial@101f0000设备被直接分配了地址0x101f0000。 | ||
+ | |||
+ | 根节点的非直接子节点是无法使用CPU的address domain的。为了在deivce tree获取内存映射地址必须指定如何从一个域名将地址转换到另一个。Ranges属性就用于此目的。以下是添加了ranges属性的device tree示例。 | ||
+ | |||
+ | m4_rproc: m4@0 { | ||
+ | compatible = "st,stm32mp1-rproc"; | ||
+ | #address-cells = <1>; | ||
+ | #size-cells = <1>; | ||
+ | ranges = <0x00000000 0x38000000 0x10000>, | ||
+ | <0x30000000 0x30000000 0x60000>, | ||
+ | <0x10000000 0x10000000 0x60000>; | ||
+ | resets = <&rcc MCU_R>; | ||
+ | reset-names = "mcu_rst"; | ||
+ | st,syscfg-pdds = <&pwr 0x014 0x1>; | ||
+ | st,syscfg-holdboot = <&rcc 0x10C 0x1>; | ||
+ | st,syscfg-tz = <&rcc 0x000 0x1>; | ||
+ | st,syscfg-rsc-tbl = <&tamp 0x144 0xFFFFFFFF>; | ||
+ | status = "disabled"; | ||
+ | m4_system_resources { | ||
+ | compatible = "rproc-srm-core"; | ||
+ | status = "disabled"; | ||
+ | }; | ||
+ | }; | ||
+ | |||
+ | ranges是一个地址转换列表。每个输入ranges表格的是包含子地址的元组,母地址和子地址空间的范围大小。每个字段的大小都由获取的子地址的#address-cells值,母地址的#address-cell值和子地址的#size-cells值而定。以外部总线为例,子地址是2 cells,母地址是1 cell,大小也为1 cell。 | ||
+ | |||
===status=== | ===status=== | ||
+ | device tree中的status标识了设备的状态,使用status可以去禁止设备或者启用设备,看下设备树规范中的status可选值。 | ||
+ | |||
+ | {|align=center border=1 | ||
+ | |- | ||
+ | !值!!描述 | ||
+ | |- | ||
+ | |“okay” | ||
+ | |表示设备正在运行 | ||
+ | |- | ||
+ | |“disabled” | ||
+ | |表示该设备目前尚未运行,但将来可能会运行 | ||
+ | |- | ||
+ | |“fail” | ||
+ | |表示设备无法运行。在设备中检测到严重错误,确实如此没有修理就不可能投入运营 | ||
+ | |- | ||
+ | |“fail-sss” | ||
+ | |表示设备无法运行。在设备中检测到严重错误,它是没有修理就不可能投入运营。值的sss部分特定于设备并指示检测到的错误情况。 | ||
+ | |} | ||
+ | |||
===中断映射=== | ===中断映射=== | ||
+ | 与遵循树的自然结构而进行的地址转换不同,机器上的任何设备都可以发起和终止中断信号。另外地址的编址也不同于中断信号,前者是设备树的自然表示,而后者者表现为独立于设备树结构的节点之间的链接。 | ||
+ | |||
+ | <center>[[Image:23-5-8-1.png]]</center><br> | ||
+ | |||
+ | 下面显示了ADC控制器中断设备片段 | ||
+ | |||
+ | adc: adc@48003000 { | ||
+ | compatible = "st,stm32mp1-adc-core"; | ||
+ | reg = <0x48003000 0x400>; | ||
+ | interrupts = <GIC_SPI 18 IRQ_TYPE_LEVEL_HIGH>, | ||
+ | <GIC_SPI 90 IRQ_TYPE_LEVEL_HIGH>; | ||
+ | clocks = <&rcc ADC12>, <&rcc ADC12_K>; | ||
+ | clock-names = "bus", "adc"; | ||
+ | interrupt-controller; | ||
+ | st,syscfg-vbooster = <&syscfg 0x4 0x100>; | ||
+ | st,syscfg-vbooster-clr = <&syscfg 0x44 0x100>; | ||
+ | st,syscfg-anaswvdd = <&syscfg 0x4 0x200>; | ||
+ | st,syscfg-anaswvdd-clr = <&syscfg 0x44 0x200>; | ||
+ | #interrupt-cells = <1>; | ||
+ | #address-cells = <1>; | ||
+ | #size-cells = <0>; | ||
+ | status = "disabled"; | ||
+ | adc1: adc@0 { | ||
+ | compatible = "st,stm32mp1-adc"; | ||
+ | #io-channel-cells = <1>; | ||
+ | reg = <0x0>; | ||
+ | interrupt-parent = <&adc>; | ||
+ | interrupts = <0>; | ||
+ | dmas = <&dmamux1 9 0x400 0x05>; | ||
+ | dma-names = "rx"; | ||
+ | status = "disabled"; | ||
+ | }; | ||
+ | adc2: adc@100 { | ||
+ | compatible = "st,stm32mp1-adc"; | ||
+ | #io-channel-cells = <1>; | ||
+ | reg = <0x100>; | ||
+ | interrupt-parent = <&adc>; | ||
+ | interrupts = <1>; | ||
+ | dmas = <&dmamux1 10 0x400 0x05>; | ||
+ | dma-names = "rx"; | ||
+ | /* temperature sensor */ | ||
+ | st,adc-channels = <12>; | ||
+ | st,min-sample-time-nsecs = <10000>; | ||
+ | status = "disabled"; | ||
+ | }; | ||
+ | jadc1: jadc@0 { | ||
+ | compatible = "st,stm32mp1-adc"; | ||
+ | st,injected; | ||
+ | #io-channel-cells = <1>; | ||
+ | reg = <0x0>; | ||
+ | interrupt-parent = <&adc>; | ||
+ | interrupts = <3>; | ||
+ | status = "disabled"; | ||
+ | }; | ||
+ | jadc2: jadc@100 { | ||
+ | compatible = "st,stm32mp1-adc"; | ||
+ | st,injected; | ||
+ | #io-channel-cells = <1>; | ||
+ | reg = <0x100>; | ||
+ | interrupt-parent = <&adc>; | ||
+ | interrupts = <4>; | ||
+ | /* temperature sensor */ | ||
+ | st,adc-channels = <12>; | ||
+ | st,min-sample-time-nsecs = <10000>; | ||
+ | status = "disabled"; | ||
+ | }; | ||
+ | adc_temp: temp { | ||
+ | compatible = "st,stm32mp1-adc-temp"; | ||
+ | io-channels = <&adc2 12>; | ||
+ | nvmem-cells = <&ts_cal1>, <&ts_cal2>; | ||
+ | nvmem-cell-names = "ts_cal1", "ts_cal2"; | ||
+ | #io-channel-cells = <0>; | ||
+ | #thermal-sensor-cells = <0>; | ||
+ | status = "disabled"; | ||
+ | }; | ||
+ | }; | ||
+ | |||
+ | 下面显示了ethernet控制器中断设备片段 | ||
+ | |||
+ | ethernet0: ethernet@5800a000 { | ||
+ | compatible = "st,stm32mp1-dwmac", "snps,dwmac-4.20a"; | ||
+ | reg = <0x5800a000 0x2000>; | ||
+ | reg-names = "stmmaceth"; | ||
+ | interrupts-extended = <&intc GIC_SPI 61 IRQ_TYPE_LEVEL_HIGH>, | ||
+ | <&intc GIC_SPI 62 IRQ_TYPE_LEVEL_HIGH>, | ||
+ | <&exti 70 1>; | ||
+ | interrupt-names = "macirq", | ||
+ | "eth_wake_irq", | ||
+ | "stm32_pwr_wakeup"; | ||
+ | clock-names = "stmmaceth", | ||
+ | "mac-clk-tx", | ||
+ | "mac-clk-rx", | ||
+ | "ethstp"; | ||
+ | clocks = <&rcc ETHMAC>, | ||
+ | <&rcc ETHTX>, | ||
+ | <&rcc ETHRX>, | ||
+ | <&rcc ETHSTP>; | ||
+ | st,syscon = <&syscfg 0x4>; | ||
+ | snps,mixed-burst; | ||
+ | snps,pbl = <2>; | ||
+ | snps,en-tx-lpi-clockgating; | ||
+ | snps,axi-config = <&stmmac_axi_config_0>; | ||
+ | snps,tso; | ||
+ | power-domains = <&pd_core>; | ||
+ | status = "disabled"; | ||
+ | }; | ||
+ | |||
+ | {|align=center border=1 | ||
+ | |- | ||
+ | !属性!!属性值!!描述 | ||
+ | |- | ||
+ | |interrupts | ||
+ | |prop-encoded-array | ||
+ | |一个设备节点属性,该属性主要描述了中断的HW interrupt ID以及类型 | ||
+ | |- | ||
+ | |interrupt-parent | ||
+ | |phandle | ||
+ | |该属性主要描述了该设备的interrupt request line连接到哪一个interrupt controller,那些没有 interrupt-parent 的节点则从它们的父节点中继承该属性 | ||
+ | |- | ||
+ | |interrupts-extended | ||
+ | |phandle prop-encoded-array | ||
+ | |列出了设备生成的中断,当设备连接到多个中断控制器 | ||
+ | |- | ||
+ | |interrupt-cells | ||
+ | |u32 | ||
+ | |这是中断控制器节点的属性,用来标识这个控制器需要几个单位做中断描述符(类似于 #address-cells 和 #size-cells),则子节点的interrupts一个cell三个32bits整型值: <中断域 中断 触发方式> | ||
+ | |- | ||
+ | |interrupt-controller | ||
+ | |empty | ||
+ | |一个空属性用来声明这个node接收中断信号 | ||
+ | |} | ||
+ | |||
===特殊节点=== | ===特殊节点=== | ||
+ | aliases节点为了解决节点路径名过长的问题,引入了节点别名的概念,可以引用到一个全路径的节点。如/external-bus/ethernet@0,0,但当用户想知道具体内容的时候显得太累赘。 | ||
+ | |||
+ | aliases { | ||
+ | ethernet0 = ðernet0; | ||
+ | serial0 = &uart4; | ||
+ | serial5 = &usart3; | ||
+ | }; | ||
+ | |||
+ | 当为设备分配一个标识符的时候,操作系统更倾向于使用aliases。 |
2021年3月24日 (三) 11:26的最新版本
目录
简介
在传统Linux内核中,ARM架构的板极硬件细节过多地被硬编码在arch/arm/plat-xxx和arch/arm/mach-xxx,比如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data,这些板级细节代码对内核来讲只不过是垃圾代码。而采用Device Tree后,许多硬件的细节可以直接透过它传递给Linux,而不再需要在kernel中进行大量的冗余编码。导致ARM的merge工作量较大。
之后经过一些讨论,对ARM平台的相关code做出如下相关规范调整,这个也正是引入DTS的原因。
1、ARM的核心代码仍然保存在arch/arm目录下
2、ARM SoC core architecture code保存在arch/arm目录下
3、ARM SOC的周边外设模块的驱动保存在drivers目录下
4、ARM SOC的特定代码在arch/arm/mach-xxx目录下
5、ARM SOC board specific的代码被移除,由DeviceTree机制来负责传递硬件拓扑和硬件资源信息。
本质上,Device Tree改变了原来用code方式将硬件配置信息嵌入到内核代码的方法,改用bootloader传递一个DB的形式。对于嵌入式系统,在系统启动阶段,bootloader会加载内核并将控制权转交给内核,此外,还需要把上述的三个参数信息传递给kernel,以便kernel可以有较大的灵活性。在linux kernel中,Device Tree的设计目标就是如此。
在devie tree中,可描述的信息包括:
1、CPU的数量和类别
2、内存基地址和大小
3、总线和桥
4、外设连接
5、中断控制器和中断的使用情况
6、GPIO控制器和GPIO使用情况
7、clock控制器和clock使用情况
它基本就是一棵电路板上的CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核来识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给内核,内核会将这些资源绑定给展开的相应设备。
Linux内核从3.x开始引入设备树的概念,用于实现驱动代码与设备信息相分离。在设备树出现以前,所有关于设备的具体信息都要写在驱动里,一旦外围设备变化,驱动代码就要重写。引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。比如在ARM Linux内,一个.dts(device tree source)文件对应一个ARM的machine,一般放置在内核的"arch/arm/boot/dts/"目录内,比如stmp1a-dk1参考板的板级设备树文件就是"arch/arm/boot/dts/ stm32mp157a-dk1.dts"。这个文件可以通过make dtbs命令编译成二进制的.dtb文件供内核驱动使用。
基于同样的软件分层设计的思想,由于一个SoC可能对应多个machine,如果每个machine的设备树都写成一个完全独立的.dts文件,那么势必相当一些.dts文件有重复的部分,为了解决这个问题,Linux设备树目录把一个SoC公用的部分或者多个machine共同的部分提炼为相应的.dtsi文件。这样每个.dts就只有自己差异的部分,公有的部分只需要"include"相应的.dtsi文件, 这样就是整个设备树的管理更加有序。我这里用Linux4.19.94源码自带的goodix touchscreen触摸芯片为例来分析设备树的使用和移植。这个触摸芯片的设备树节点信息在" Documentation/devicetree/bindings/input/touchscreen/goodix.txt"有详细说明,其驱动源码是" drivers/input/touchscreen/goodix.c"。
基础知识介绍
- dts
硬件的相应信息都会写在.dts为后缀的文件中,每一款硬件可以单独写一份例如stm32mp157a-dk1.dts,一般在Linux源码中存在大量的dts文件,对于arm架构可以在arch/arm/boot/dts找到相应的dts,一个dts文件对应一个ARM的machie。
- dtsi
值得一提的是,对于一些相同的dts配置可以抽象到dtsi文件中,然后类似于C语言的方式可以include到dts文件中,对于同一个节点的设置情况,dts中的配置会覆盖dtsi中的配置。
- dtc
dtc是编译dts的工具,可以在Ubuntu系统上通过指令apt-get install device-tree-compiler安装dtc工具,不过在内核源码scripts/dtc路径下已经包含了dtc工具;
- dtb
dtb(Device Tree Blob),dts经过dtc编译之后会得到dtb文件,dtb通过Bootloader引导程序加载到内核。所以Bootloader需要支持设备树才行;Kernel也需要加入设备树的支持;
DTS结构
/dts-v1/; / { 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 { }; }; };
device tree的基本单元是node。这些node被组织成树状结构,除了root node,每个node都只有一个parent。一个device tree文件中只能有一个root node。每个node中包含了若干的property/value来描述该node的一些特性。每个node用节点名字(node name)标识,节点名字的格式是node-name@unit-address。如果该node没有reg属性(后面会描述这个property),那么该节点名字中必须不能包括@和unit-address。unit-address的具体格式是和设备挂在那个bus上相关。例如对于cpu,其unit-address就是从0开始编址,以此加一。而具体的设备,例如以太网控制器,其unit-address就是寄存器地址。root node的node name是确定的,必须是“/”。
也就是说设备树源文件的结构为:
- 1个root节点”/”;
- root节点下面含一系列子节点,“node1” and “node2”
- 节点node1和下又含有一系列子节点,“child-node1” and “child-node2”
- 各个节点都有一系列属性
- 这些属性可能为空,如an-empty-property
- 可能为字符串,如a-string-property
- 可能为字符串树组,如a-string-list-property
- 可能为Cells(由u32整数组成),如second-child-property
DTS语法介绍
了解了基本的device tree的结构后,我们总要把这些结构体现在device tree source code上来。在linux kernel中,扩展名是dts的文件就是描述硬件信息的device tree source file,在dts文件中,一个node被定义成如下格式:
[label:] node-name[@unit-address] { [properties definitions] [child nodes] }
“[]”表示option,因此可以定义一个只有node name的空节点,label方便在dts文件中引用。
基本数据类型:
- text string(以null结束),以双引号括起来,如:string-property = “a string”;
- cells 是32位无符号整形数,以尖括号括起来,如:cell-property = <0xbeef 123 0xabcd1234>;
- binary data 以方括号括起来,如:binary-property = [0x01 0x23 0x45 0x67];
- 不同类型数据可以在同一个属性中存在,以逗号分格,如:mixed-property = “a string”, [0x01 0x23 0x45 0x67],<0x12345678>;
- 多个字符串组成的列表也使用逗号分格,如:string-list = “red fish”,”blue fish”;
dts的组成
compatible
每一个dts文件都是由一个root的根节点组成,内核通过根节点“/”的兼容性即可判断它启动的是什么设备,其代码结构如下
C++ Code
/ { model = "HQYJ FS-MP1A Discovery Board"; compatible = "st,stm32mp157a-dk1", "st,stm32mp157", "hqyj,fsmp1a"; aliases { ethernet0 = ðernet0; serial0 = &uart4; serial5 = &usart3; }; chosen { stdout-path = "serial0:115200n8"; }; ... ... };
model属性值是,它指定制造商的设备型号。推荐的格式是:“manufacturer,model”,其中manufacturer是一个字符串描述制造商的名称,而型号指定型号。
compatible属性值是,指定了系统的名称,是一个字符串列表,它包含了一个“<制造商>,<型号>”形式的字符串。重要的是要指定一个确切的设备,并且包括制造商的名字,以避免命名空间冲突。
chosen 节点不代表一个真正的设备,但功能与在固件和操作系统间传递数据的地点一样,如根参数,取代以前bootloader的启动参数,控制台的输入输出参数等。
#address-cells和#size-cells
port { #address-cells = <1>; #size-cells = <0>; ltdc_ep0_out: endpoint@0 { reg = <0>; remote-endpoint = <&sii9022_in>; }; };
#address-cells = <1>: 基地址、片选号等绝对起始地址所占字长,单位uint32。 #size-cells = <1>: 长度所占字长,单位uint32。
CPU addressing
在讨论寻址时,CPU节点代表了最简单的情况。 每个CPU都分配有一个唯一的ID,并且没有与CPU ID相关联的大小。
cpus { #address-cells = <1>; #size-cells = <0>; cpu0: cpu@0 { compatible = "arm,cortex-a7"; device_type = "cpu"; reg = <0>; clocks = <&rcc CK_MPU>; clock-names = "cpu"; operating-points-v2 = <&cpu0_opp_table>; nvmem-cells = <&part_number_otp>; nvmem-cell-names = "part_number"; }; cpu1: cpu@1 { compatible = "arm,cortex-a7"; device_type = "cpu"; reg = <1>; clocks = <&rcc CK_MPU>; clock-names = "cpu"; operating-points-v2 = <&cpu0_opp_table>; }; };
在cpus节点,#address-cells被设置成了1,#size-cells被设置成了0。这是说子reg值是单独的uint32,它用无大小字段表示地址。在此情况下,这两个cpu分配到的地址为0和1。cpu节点的#size-cells是0因为每个cpu只分配到了一个单独的地址。
你仍然需要注意reg值班需要与节点名的值相匹配。按照惯例,如果一个节点有一个reg属性,那么这个节点名称必须包括unit-address,这是reg属性的第一个address值。
Memory Mapped Devices
与在cpu节点中单独的address值不同,内存映射设备被分配了一系列将要响应的地址,因此不仅需要包含内存的基地址而且还需要映射地址的长度,因此需要使用#size-cells用来表示在每个子reg元组中长度字段的大小。在以下示例中,每个address值为1 cell(32 bits),每个长度值也是1 cell,这在32 bit系统是比较典型的。64 bit设备也许会为#address-cells和#size-cells使用数值2,在device tree中获取64 bit addressing。
/dts-v1/; / { #address-cells = <1>; #size-cells = <1>; ... timers2: timer@40000000 { compatible = "st,stm32-timers"; reg = <0x40000000 0x400>; }; timers3: timer@40001000 { compatible = "st,stm32-timers"; reg = <0x40001000 0x400>; }; spi2: spi@4000b000 { compatible = "st,stm32h7-spi"; reg = <0x4000b000 0x400>; }; uart7: serial@40018000 { compatible = "st,stm32h7-uart"; reg = <0x40018000 0x400>; }; sai1: sai@4400a000 { compatible = "st,stm32h7-sai"; reg = <0x4400a000 0x4>; }; ... };
Non Memory Mapped Devices
处理器总线的其它设备为非内存映射设备。他们有地址范围,但不能被CPU直接寻址。母设备的驱动程序将代替CPU进行间接访问。以i2c设备为例,每个设备都分配了一个地址,但没有长度或范围与之相匹配。这与CPU地址分配很相似。
C++ Code
etf: etf@50092000 { compatible = "arm,coresight-tmc", "arm,primecell"; reg = <0x50092000 0x1000>; clocks = <&rcc CK_TRACE>; clock-names = "apb_pclk"; ports { #address-cells = <1>; #size-cells = <0>; port@0 { reg = <0>; etf_in_port: endpoint { slave-mode; remote-endpoint = <&funnel_out_port0>; }; }; port@1 { reg = <0>; etf_out_port: endpoint { remote-endpoint = <&tpiu_in_port>; }; }; }; };
Ranges (Address Translation)
前面已经讨论过如何向设备分配地址,但此时这些地址只是本地设备节点,还没有说明如何从那些地址里映射到cpu可以使用的地址。根节点经常描述地址空间的CPU视图。根节点的子节点已经使用了CPU的address domain,所以不需要任何明确的映射。例如,serial@101f0000设备被直接分配了地址0x101f0000。
根节点的非直接子节点是无法使用CPU的address domain的。为了在deivce tree获取内存映射地址必须指定如何从一个域名将地址转换到另一个。Ranges属性就用于此目的。以下是添加了ranges属性的device tree示例。
m4_rproc: m4@0 { compatible = "st,stm32mp1-rproc"; #address-cells = <1>; #size-cells = <1>; ranges = <0x00000000 0x38000000 0x10000>, <0x30000000 0x30000000 0x60000>, <0x10000000 0x10000000 0x60000>; resets = <&rcc MCU_R>; reset-names = "mcu_rst"; st,syscfg-pdds = <&pwr 0x014 0x1>; st,syscfg-holdboot = <&rcc 0x10C 0x1>; st,syscfg-tz = <&rcc 0x000 0x1>; st,syscfg-rsc-tbl = <&tamp 0x144 0xFFFFFFFF>; status = "disabled"; m4_system_resources { compatible = "rproc-srm-core"; status = "disabled"; }; };
ranges是一个地址转换列表。每个输入ranges表格的是包含子地址的元组,母地址和子地址空间的范围大小。每个字段的大小都由获取的子地址的#address-cells值,母地址的#address-cell值和子地址的#size-cells值而定。以外部总线为例,子地址是2 cells,母地址是1 cell,大小也为1 cell。
status
device tree中的status标识了设备的状态,使用status可以去禁止设备或者启用设备,看下设备树规范中的status可选值。
值 | 描述 |
---|---|
“okay” | 表示设备正在运行 |
“disabled” | 表示该设备目前尚未运行,但将来可能会运行 |
“fail” | 表示设备无法运行。在设备中检测到严重错误,确实如此没有修理就不可能投入运营 |
“fail-sss” | 表示设备无法运行。在设备中检测到严重错误,它是没有修理就不可能投入运营。值的sss部分特定于设备并指示检测到的错误情况。 |
中断映射
与遵循树的自然结构而进行的地址转换不同,机器上的任何设备都可以发起和终止中断信号。另外地址的编址也不同于中断信号,前者是设备树的自然表示,而后者者表现为独立于设备树结构的节点之间的链接。
下面显示了ADC控制器中断设备片段
adc: adc@48003000 { compatible = "st,stm32mp1-adc-core"; reg = <0x48003000 0x400>; interrupts = <GIC_SPI 18 IRQ_TYPE_LEVEL_HIGH>, <GIC_SPI 90 IRQ_TYPE_LEVEL_HIGH>; clocks = <&rcc ADC12>, <&rcc ADC12_K>; clock-names = "bus", "adc"; interrupt-controller; st,syscfg-vbooster = <&syscfg 0x4 0x100>; st,syscfg-vbooster-clr = <&syscfg 0x44 0x100>; st,syscfg-anaswvdd = <&syscfg 0x4 0x200>; st,syscfg-anaswvdd-clr = <&syscfg 0x44 0x200>; #interrupt-cells = <1>; #address-cells = <1>; #size-cells = <0>; status = "disabled"; adc1: adc@0 { compatible = "st,stm32mp1-adc"; #io-channel-cells = <1>; reg = <0x0>; interrupt-parent = <&adc>; interrupts = <0>; dmas = <&dmamux1 9 0x400 0x05>; dma-names = "rx"; status = "disabled"; }; adc2: adc@100 { compatible = "st,stm32mp1-adc"; #io-channel-cells = <1>; reg = <0x100>; interrupt-parent = <&adc>; interrupts = <1>; dmas = <&dmamux1 10 0x400 0x05>; dma-names = "rx"; /* temperature sensor */ st,adc-channels = <12>; st,min-sample-time-nsecs = <10000>; status = "disabled"; }; jadc1: jadc@0 { compatible = "st,stm32mp1-adc"; st,injected; #io-channel-cells = <1>; reg = <0x0>; interrupt-parent = <&adc>; interrupts = <3>; status = "disabled"; }; jadc2: jadc@100 { compatible = "st,stm32mp1-adc"; st,injected; #io-channel-cells = <1>; reg = <0x100>; interrupt-parent = <&adc>; interrupts = <4>; /* temperature sensor */ st,adc-channels = <12>; st,min-sample-time-nsecs = <10000>; status = "disabled"; }; adc_temp: temp { compatible = "st,stm32mp1-adc-temp"; io-channels = <&adc2 12>; nvmem-cells = <&ts_cal1>, <&ts_cal2>; nvmem-cell-names = "ts_cal1", "ts_cal2"; #io-channel-cells = <0>; #thermal-sensor-cells = <0>; status = "disabled"; }; };
下面显示了ethernet控制器中断设备片段
ethernet0: ethernet@5800a000 { compatible = "st,stm32mp1-dwmac", "snps,dwmac-4.20a"; reg = <0x5800a000 0x2000>; reg-names = "stmmaceth"; interrupts-extended = <&intc GIC_SPI 61 IRQ_TYPE_LEVEL_HIGH>, <&intc GIC_SPI 62 IRQ_TYPE_LEVEL_HIGH>, <&exti 70 1>; interrupt-names = "macirq", "eth_wake_irq", "stm32_pwr_wakeup"; clock-names = "stmmaceth", "mac-clk-tx", "mac-clk-rx", "ethstp"; clocks = <&rcc ETHMAC>, <&rcc ETHTX>, <&rcc ETHRX>, <&rcc ETHSTP>; st,syscon = <&syscfg 0x4>; snps,mixed-burst; snps,pbl = <2>; snps,en-tx-lpi-clockgating; snps,axi-config = <&stmmac_axi_config_0>; snps,tso; power-domains = <&pd_core>; status = "disabled"; };
属性 | 属性值 | 描述 |
---|---|---|
interrupts | prop-encoded-array | 一个设备节点属性,该属性主要描述了中断的HW interrupt ID以及类型 |
interrupt-parent | phandle | 该属性主要描述了该设备的interrupt request line连接到哪一个interrupt controller,那些没有 interrupt-parent 的节点则从它们的父节点中继承该属性 |
interrupts-extended | phandle prop-encoded-array | 列出了设备生成的中断,当设备连接到多个中断控制器 |
interrupt-cells | u32 | 这是中断控制器节点的属性,用来标识这个控制器需要几个单位做中断描述符(类似于 #address-cells 和 #size-cells),则子节点的interrupts一个cell三个32bits整型值: <中断域 中断 触发方式> |
interrupt-controller | empty | 一个空属性用来声明这个node接收中断信号 |
特殊节点
aliases节点为了解决节点路径名过长的问题,引入了节点别名的概念,可以引用到一个全路径的节点。如/external-bus/ethernet@0,0,但当用户想知道具体内容的时候显得太累赘。
aliases { ethernet0 = ðernet0; serial0 = &uart4; serial5 = &usart3; };
当为设备分配一个标识符的时候,操作系统更倾向于使用aliases。