查看“设备树详解”的源代码
←
设备树详解
跳转至:
导航
、
搜索
因为以下原因,你没有权限编辑本页:
您所请求的操作仅限于该用户组的用户使用:
用户
您可以查看与复制此页面的源代码。
==简介== 在传统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可选值。 {|align=center border=1 |- !值!!描述 |- |“okay” |表示设备正在运行 |- |“disabled” |表示该设备目前尚未运行,但将来可能会运行 |- |“fail” |表示设备无法运行。在设备中检测到严重错误,确实如此没有修理就不可能投入运营 |- |“fail-sss” |表示设备无法运行。在设备中检测到严重错误,它是没有修理就不可能投入运营。值的sss部分特定于设备并指示检测到的错误情况。 |} ===中断映射=== 与遵循树的自然结构而进行的地址转换不同,机器上的任何设备都可以发起和终止中断信号。另外地址的编址也不同于中断信号,前者是设备树的自然表示,而后者者表现为独立于设备树结构的节点之间的链接。 [[Image:23-5-8-1.png]]<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"; }; {|class="wikitable" |- !属性!!属性值!!描述 |- |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。
返回至
设备树详解
。
导航菜单
个人工具
登录
命名空间
页面
讨论
变种
视图
阅读
查看源代码
查看历史
更多
搜索
导航
首页
关于我们
联系我们
资料下载
STM32F103开发板
STM32U575开发板
STM32MP157开发板
Hi3861鸿蒙开发板
HaaS EDU开发板
ESP32开发板
i.MX8M Plus开发板
图书下载
嵌入式系列图书
物联网系列图书
Android系列图书
高校教仪
嵌入式实验室产品
物联网实验室产品
人工智能实验室产品
虚拟仿真实验室产品
行业应用实训室产品
虚拟仿真
嵌入式虚拟仿真平台
物联网虚拟仿真平台
人工智能虚拟仿真
友情链接
华清远见研发中心
元宇宙实验中心
华清远见硬件商城
工具
链入页面
相关更改
特殊页面
页面信息