【北京迅为】《STM32MP157开发板嵌入式开发指南》- 第五十章 Linux设备树
iTOP-STM32MP157开发板采用ST推出的双核cortex-A7+单核cortex-M4异构处理器,既可用Linux、又可以用于STM32单片机开发。开发板采用核心板+底板结构,主频650M、1G内存、8G存储,核心板采用工业级板对板连接器,高可靠,牢固耐用,可满足高速信号环境下使用。共240PIN,CPU功能全部引出:底板扩展接口丰富底板板载4G接口(选配)、千兆以太网、WIFI蓝牙模块HDMI、CAN、RS485、LVDS接口、温湿度传感器(选配)光环境传感器、六轴传感器、2路USB OTG、3路串口,CAMERA接口、ADC电位器、SPDIF、SDIO接口等
第四部分 Linux驱动进阶
第五十章 Linux设备树
本章导读
在前面章节中提到了设备树的相关内容。但是设备树具体是什么,有什么作用,在本章节中具体讲解一下。掌握设备树是编写设备驱动的一个重点内容,在旧版本的 Linux 内核中没有使用设备树,使用的是平台文件 arch/arm/plat-xxx 和 arch/arm/mach-xxx。但是随着内核的发展,平台文件变得非常冗余和复杂,因此在新版本的 Linux 内核代码中便使用设备树来描述硬件信息。
50.1章节讲解了设备树的由来
50.2章节讲解了什么是设备树
50.3章节讲解了DTS,DTC DTB的概念及关系
50.4章节讲解了DTS设备树语法结构
本章内容对应视频讲解链接(在线观看):
设备树的由来以及基本概念 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=24
设备树基本语法 → https://www.bilibili.com/video/BV1Vy4y1B7ta?p=25
50.1 设备树的由来
要想了解为什么会有设备树,设备树是怎么来的,我们就要先来回顾一下在没有设备树之前我们是怎么来写一个驱动程序的。以字符设备驱动代码框架为例,我们一起来回顾下。
任何的设备驱动的编写,Linux已经为我们打好了框架,我们只要像做完形填空一样填写进去就可以了。
字符设备驱动框架如下图所示:
杂项设备驱动框架:
通过这些框架,我们可以很容易编写驱动代码,但是,当我们用这个框架非常熟练的时候,我们就会发现虽然这个方法很简单,但是非常不容易扩展,当我们有很多很多相似设备的时候,如果我们都是按照这个框架来完成,那就要写很多遍这个流程,但是多个相似设备之间真正有差异的地方只有框架的初始化硬件的部分,其他步骤的代码基本都是一样的。这样就会造成大量的重复代码。但是,我们在编写驱动代码的时候,我们要尽量做到代码的复用,也就是一套驱动尽量可以兼任很多设备,如果我们还按照这个来编写就不太符合规则了。
为了实现这个目标,我们就要把通用的代码和有差异的代码分离出来,来增强我们驱动代码的可移植性。所以,设备驱动分离的思想也就应运而生了,在Linux中,我们是在写代码的时候进行分离,分离是把一些不相似的东西放到了device.c,把相似的东西放在了driver.c,如果我们有很多相似的设备或者平台,我们只要修改device.c就可以了,这样我们重复性的工作就大大的减少了。这个就是平台总线的由来。
平台总线这个方法有什么弊端呢?
当我们用这个方法用习惯以后就会发现,假如soc不变,我们每换一个平台,都要修改C文件,并且还要重新编译。而且会在arch/arm/plat-xxx和arch/arm/mach-xxx下面留下大量的关于板级细节的代码。并不是说这个方法不好,只是从Linux的发展来看,这些代码相对于Linux内核来说就是“垃圾代码”,而且这些“垃圾代码”非常多,于是就有了Linux Torvalds那句简单粗暴的话:
为了改变这个现状,设备树也就被引进到Linux上了,用来剔除相对内核来说的“垃圾代码”,即用设备树文件来描述这些设备信息,也就是代替device.c文件,platform匹配上基本不变,并且相比于之前的方法,使用设备树不仅可以去掉大量“垃圾代码”,并且采用文本格式,方便阅读和修改,如果需要修改部分资源,我们也不用在重新编译内核了,只需要把设备树源文件编译成二进制文件,在通过bootloader传递给内核就可以了。内核在对其进行解析和展开得到一个关于硬件的拓扑图。我们通过内核提供的接口获取设备树的节点和属性就可以了。即内核对于同一soc的不同主板,只需更换设备树文件dtb即可实现不同主板的无差异支持,而无需更换内核文件。
50.2 什么是设备树?
Device Tree是一种描述硬件的数据结构,由一系列被命名的节点(node)和属性(property)组成,而节点本身可包含子节点。所谓属性,其实就是成对出现的name和value。
在Device Tree中,可描述的信息包括:CPU的数量和类别,内存基地址和大小,总线和桥,外设连接,中断控制器和中断使用情况,GPIO控制器和GPIO使用情况,Clock控制器和Clock使用情况。设备树基本上就是画一棵电路板上由CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。
50.3 DTS 、DTC 和 DTB
文件.dts是一种ASCII文件格式设备树描述,在Linux中,一个.dts文件对应一个ARM设备,一般放置在arch/arm/boot/dts目录下。
dtb文件是dts文件被编译后生成的二进制文件,由Linux内核解析,有了设备树文件就可以在不改动Linux内核的情况下,对不同的平台实现无差异的支持,只需更换相应的dts文件,即可满足。
dtc是将dts编译为dtb的工具。在Linux内核下可以单独编译设备树文件,那么如何确定去编译我们自己的板子对应的dts文件? STM32MP157开发板为例,我们来看一下内核源码下的/home/topeet/work/linux-5.4.31/arch/arm/boot/dts/Makefile这个文件的内容:
dtb-$(CONFIG_ARCH_STM32) += \
stm32f429-disco.dtb \
stm32f469-disco.dtb \
stm32f746-disco.dtb \
stm32f769-disco.dtb \
stm32429i-eval.dtb \
stm32746g-eval.dtb \
stm32h743i-eval.dtb \
stm32h743i-disco.dtb \
stm32mp157a-avenger96.dtb \
stm32mp157a-dk1.dtb \
stm32mp157a-itop.dtb \
stm32mp157a-itop-rgb-043.dtb \
stm32mp157a-itop-rgb-050.dtb \
stm32mp157a-itop-rgb-070.dtb \
stm32mp157a-itop-lvds-070.dtb\
stm32mp157a-itop-lvds-097.dtb\
stm32mp157a-itop-lvds-101.dtb\
stm32mp157d-dk1.dtb \
stm32mp157c-dk2.dtb \
stm32mp157f-dk2.dtb \
stm32mp157c-dk2-a7-examples.dtb \
stm32mp157c-dk2-m4-examples.dtb \
stm32mp157f-dk2-a7-examples.dtb \
stm32mp157f-dk2-m4-examples.dtb \
stm32mp157a-ed1.dtb \
stm32mp157c-ed1.dtb \
stm32mp157d-ed1.dtb \
stm32mp157f-ed1.dtb \
stm32mp157a-ev1.dtb \
stm32mp157c-ev1.dtb \
stm32mp157d-ev1.dtb \
stm32mp157f-ev1.dtb \
stm32mp157c-ev1-a7-examples.dtb \
stm32mp157c-ev1-m4-examples.dtb \
stm32mp157f-ev1-a7-examples.dtb \
stm32mp157f-ev1-m4-examples.dtb
可以看出,当选中(CONFIG_ARCH_STM32=y),所有使用到这个SOC的板子对应的.dts文件都会被编译为.dtb。如果我们使用STM32MP157新做了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到 dtb- $(CONFIG_ARCH_STM32)下,这样在编译设备树的时候就会将对应的.dts 编译为二进制的.dtb 文件。
其中,DTS,DTSI,DTB,DTC,他们之间的关系如下:
50.4 DTS 设备树语法结构
一般情况下,我们不会从头编写一个完整的dts文件,SOC厂商一般会直接提供一个有着基本框架的dts文件,当需要添加自己的板子设备树文件时,基于厂商提供的dts文件修改即可。所以我们要了解dts设备树文件的语法,这样我们才清楚如何添加我们自己的设备。
50.4.1 dtsi 头文件
由于一个 SOC 可能对应多个 ARM 设备,这些 dts 文件势必包含许多共同的部分,Linux 内核为了简化,把 SOC 公用的部分或者多个设备共同的部分提炼为.dtsi 文件,类似于 C 语言的头文件。device tree source include(dtsi)是更通用的设备树代码,也就是相同芯片但不同平台都可以使用的代码。
.dtsi 文件也可以包含其他的.dtsi。在/home/topeet/work/linux-5.4.31/arch/arm/boot/dts/stm32mp157a-itop-lvds-070.dts 文件中有如下内容:
#include "stm32mp15xx-itop.dtsi"用“#include”关键字来引用了stm32mp15xx-itop.dtsi文件,也可以像C语言那样来引用.h文件
一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如
UART、IIC 等等。比如stm32mp15xx-itop.dtsi 就是描述STM32MP157这个 SOC 内部外设情况信息的,内容如下:
// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause)
/*
* Copyright (C) STMicroelectronics 2019 - All Rights Reserved
* Author: Alexandre Torgue <alexandre.torgue@st.com> for STMicroelectronics.
*/
#include "stm32mp157-m4-srm.dtsi"
#include "stm32mp157-m4-srm-pinctrl.dtsi"
#include <dt-bindings/mfd/st,stpmic1.h>
#include <dt-bindings/rtc/rtc-stm32.h>
#include <dt-bindings/input/input.h>
/ {
memory@c0000000 {
device_type = "memory";
reg = <0xc0000000 0x40000000>;
};
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
mcuram2: mcuram2@10000000 {
compatible = "shared-dma-pool";
reg = <0x10000000 0x40000>;
no-map;
};
vdev0vring0: vdev0vring0@10040000 {
compatible = "shared-dma-pool";
reg = <0x10040000 0x1000>;
no-map;
};
vdev0vring1: vdev0vring1@10041000 {
compatible = "shared-dma-pool";
reg = <0x10041000 0x1000>;
no-map;
};
vdev0buffer: vdev0buffer@10042000 {
compatible = "shared-dma-pool";
reg = <0x10042000 0x4000>;
.................
50.4.2 设备节点信息
设备树从根节点开始,每个设备都是一个节点。根节点就相当于树根。节点和节点之间可以互相嵌套,形成父子关系。可以理解为树枝可以分成好几个小的树枝。设备的属性用key-value对(键值对)来描述,每个属性用分号结束。下面先来看一个设备树结构模板:
1 / {
2 node1 {
3 a-string-property = "A string";
4 a-string-list-property = "first string", "second string";
5 a-byte-data-property = [0x01 0x23 0x34 0x56];
6 child-node1 {
7 first-child-property;
8 second-child-property = <1>;
9 a-string-property = "Hello, world";
10 };
11 child-node2 {
12 };
13 };
14 node2 {
15 an-empty-property;
16 a-cell-property = <1 2 3 4>;
17 child-node1 {
18 };
19 };
20 }
上面的 dts 文件内容并没有实际的用途,只是基本表示了一个设备树源文件的结构。但是这里面体现了一些属性:
- 一个单独的根节点:“/”
- 两个子节点:“node1”和“node2”
- 两个 node1 的子节点:“child-node1”和“child-node2”
- 一些分散在树里的属性,属性是最简单的键-值对,它的值可以为空或者包含一个任意的字节流。
虽然数据类型并没有编码进数据结构,但是设备树源文件中仍有几个基本的数据表示形式:
1) 文本字符串(无结束符),可以用双引号表示:
a-string-property = "A string";
2) “cells”是 32 位无符号整数,用尖括号限定:
cell-property = <0xbeef 123 0xabcd1234>;
3) 二进制数据用方括号限定:
binary-property = [0x01 0x23 0x45 0x67];
4) 不同表示形式的数据可以用逗号连在一起:
mixed-property = "a string", [0x01 0x23 0x45 0x67], <0x12345678>;
5) 逗号也可以用于创建字符串列表:
string-list = "red fish", "blue fish";
下面我们看一下简化之后结构:
7 #include "stm32mp151.dtsi"
8
9 / {
10 cpus {
11 cpu1: cpu@1 {
12 compatible = "arm,cortex-a7";
13 device_type = "cpu";
14 reg = <1>;
15 clocks = <&scmi0_clk CK_SCMI0_MPU>;
16 clock-names = "cpu";
17 operating-points-v2 = <&cpu0_opp_table>;
18 };
19 };
20
21 arm-pmu {
22 interrupts = <GIC_SPI 200 IRQ_TYPE_LEVEL_HIGH>,
23 <GIC_SPI 201 IRQ_TYPE_LEVEL_HIGH>;
24 interrupt-affinity = <&cpu0>, <&cpu1>;
25 };
26
27 soc {
28 m_can1: can@4400e000 {
29 compatible = "bosch,m_can";
30 reg = <0x4400e000 0x400>, <0x44011000 0x1400>;
31 reg-names = "m_can", "message_ram";
32 interrupts = <GIC_SPI 19 IRQ_TYPE_LEVEL_HIGH>,
33 <GIC_SPI 21 IRQ_TYPE_LEVEL_HIGH>;
34 interrupt-names = "int0", "int1";
35 clocks = <&scmi0_clk CK_SCMI0_HSE>, <&rcc FDCAN_K>;
36 clock-names = "hclk", "cclk";
37 bosch,mram-cfg = <0x0 0 0 32 0 0 2 2>;
38 status = "disabled";
39 };
40
41 m_can2: can@4400f000 {
42 compatible = "bosch,m_can";
43 reg = <0x4400f000 0x400>, <0x44011000 0x2800>;
44 reg-names = "m_can", "message_ram";
45 interrupts = <GIC_SPI 20 IRQ_TYPE_LEVEL_HIGH>,
46 <GIC_SPI 22 IRQ_TYPE_LEVEL_HIGH>;
47 interrupt-names = "int0", "int1";
48 clocks = <&scmi0_clk CK_SCMI0_HSE>, <&rcc FDCAN_K>;
49 clock-names = "hclk", "cclk";
50 bosch,mram-cfg = <0x1400 0 0 32 0 0 2 2>;
51 status = "disabled";
52 };
53 };
54 };
第 9 行,“/”是根节点。
第 10、21 和 27 行,cpus 、 arm-pmu 和soc 是三个子节点。
第 28、41行,cpu0 是 soc的子节点。
简单来说,节点就好比一棵大树,从树的主干开始,然后有一节一节的树枝,这个就叫节点。在代码中的节点是什么样子的呢。我们把上面模板中的根节点摘出来,如下图所示,这个就是根节点,相当于大树的树干。
/{
};
而树枝就相当于设备树的子节点,同样我们把子节点摘出来就是根节点里面的node1和node2,如下图所示:
/{ //根节点
node1//子节点node1
{
};
node2//子节点node2
{
};
};
一个树枝是不是也可以继续分成好几个树枝呢,也就是说子节点里面可以包含子子节点。所以child-node1和child-node2是node1的子节点,如下图所示:
/{ //根节点
node1//子节点node1
{
child-node1 //子子节点
{
};
};
node2//子节点node2
{
child-node2 //子子节点
{
};
};
};
0.4.3 设备节点及lable命名
在前面的代码中,我们注意到节点和子节点之间的命名有所不同,它们都遵循了下面的命名格式:
格式:<名称>[@<设备地址>]
<名称>节点的名称也不是任意起的,一般要体现设备的类型而不是特点的型号,比如网口,应该命名为ethernet,而不是随意起一个,比如111。
<设备地址>就是用来访问该设备的基地址。但并不是说在操作过程中来描述一个地址,他主要用来区分用。
注意事项:
- 同一级的节点只要地址不一样,名字是可以不唯一的。
- 设备地址是一个可选选项,可以不写。但为了容易区分和理解,一般是都写的。
当我们找一个节点的时候,我们必须书写完整的节点路径,如果我们的节点名很长,那么我们在引用的时候就十分不方便,所以,设备树允许我们用下面的形式为节点标注引用(起别名)。比如一个动漫人物的名字是蒙其·D·路飞,他的小名是路飞,那是不是小名要比我们的全名更容易记忆了。这个就是别名。
举例:
uart8: serial@02288000
其中,uart8就是这个节点名称的别名,serial@02288000就是节点名称。
一般我往一个节点里面添加内容的时候,不会直接把添加的内容写到节点里面,而是通过节点的引用来添加。
举例
&uart8 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart8>;
status = "okay";
};
&uart8表示引用节点别名为uart8的节点,并往这个节点里面添加以下内容:
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart8>;
status = "okay";
注意事项:
编译设备树的时候,相同的节点的不同属性信息都会被合并,相同节点的相同的属性会被重写,使用引用可以避免四处找节点。如dts和dtsi里面都有根节点,但最终会合并成一个根节点。
50.4.4 标准属性
address-cells和size-cells属性
不同的平台,不同的总线,地址位长度可能不同,有 32 位地址,有 64 位地址,为了适应这个,规范规定一个 32 位的长度为一个 cell。
"#address-cells"属性用来表示总线地址需要几个 cell 表示,该属性本身是u32 类型的。
"#size-cells"属性用来表示子总线地址空间的长度需要几个cell 表示,属性本身的类型也是 u32。
可以这么理解父节点表示总线,总线上每个设备的地址长度以及地址范围是总线的一个特性,用
"#address-cells","#size-cells"属性表示,比如总线是 32 位,那么"#address-cells"设置成 1 就可以了。这两个属性不可以继承,就是说在未定义这两个属性的时候,不会继承更高一级父节点的设置,如果没有设置的话,内核默认认为"#address-cells"为 2,"#size-cells"为 1。举例来说,如下所示:
1 spi4 {
2 compatible = "spi-gpio";
3 #address-cells = <1>;
4 #size-cells = <0>;
5
6 gpio_spi: gpio_spi@0 {
7 compatible = "fairchild,74hc595";
8 reg = <0>;
9 };
10 };
11
12 aips3: aips-bus@02200000 {
13 compatible = "fsl,aips-bus", "simple-bus";
14 #address-cells = <1>;
15 #size-cells = <1>;
16
17 dcp: dcp@02280000 {
18 compatible = "fsl,imx6sl-dcp";
19 reg = <0x02280000 0x4000>;
20 };
21 };
第 3,4 行,节点 spi4 的#address-cells = <1>,#size-cells = <0>,说明 spi4 的子节点 reg 属性中起始
地址所占用的字长为 1,地址长度所占用的字长为 0。因此第 8 行 reg 属性值为 <0>,相当于设置了起始
地址,而没有设置地址长度。
第 14,15 行,设置 aips3: aips-bus@02200000 节点#address-cells = <1>,#size-cells = <1>,说明 aips3:
aips-bus@02200000 节点起始地址长度所占用的字长为 1,地址长度所占用的字长也为 1。因此第 19 行,
子节点 dcp: dcp@02280000 的 reg 属性值为<0x02280000 0x4000>,相当于设置了起始地址为 0x02280000,
地址长度为 0x40000。
reg属性
"reg"属性用来表示节点地址资源的,比如常见的就是寄存器的起始地址及大小。要想表示一块连续地
址,必须包含起始地址和空间大小两个参数,如果有多块地址,那么就需要多组这样的值表示。对于'reg'
属性,每个元素是一个二元组,包含起始地址和大小。还有另外一个问题,地址和大小用几个 u32 表示呢?
这个就由父节点的"#address-cells","#size-cells"属性确定。
例如:
uart4: serial@40010000 {
compatible = "st,stm32h7-uart";
reg = <0x40010000 0x400>;
interrupts-extended = <&exti 30 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rcc UART4_K>;
resets = <&rcc UART4_R>;
wakeup-source;
power-domains = <&pd_core>;
dmas = <&dmamux1 63 0x400 0x5>,
<&dmamux1 64 0x400 0x1>;
dma-names = "rx", "tx";
status = "disabled";
};
上述代码是节点 uart4,uart4 节点描述了 UART4 相关信息,重点是第 3 行的 reg 属性。其中 uart4 的父节点 soc 设置了#address-cells = <1>、#size-cells = <1>,因此 reg属性中 address=0x40010000,length=0x400。
compatible属性
设备树中的每个表示一个设备的节点都需要一个 compatible 属性,compatible 属性是操作系统用来决定设备和驱动绑定的关键因素。compatible 属性也叫做兼容性属性,属性的值是一个字符串列表,用于表示是何种设备,可以在代码中进行匹配。
举例:
compatible = "manufacturer,model";
第一个字符串表示厂商,后面的字符串表示确切的设备名称。比如在stm32mp15xx-itop.dtsi文件中 sound节点表示开发板的音频设备节点,STM32MP157开发板上的音频芯片是欧胜(WOLFSON)出品的 WM8960,sound 节点的 compatible 属性值如下:
compatible = "wlf,wm8960";
属性值为“wlf,wm8960”和“fsl,imx-audio-wm8960”,其中“wlf” 表示厂商是欧胜,“wm8960”表示设备驱动的名字。sound 这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查找,直到找到或者查找完整个 Linux 内核也没有找到对应的驱动。
status属性
status 属性用来表示节点的状态,其实就是硬件的状态,用字符串表示。
- “okay”表示硬件正常工作
- “disable”表示当前硬件不可用
- “fail”表示因为出错不可用
- “fail-sss”表示某种原因出错不可用,sss 表示具体出错的原因。
实际中,基本只用“okay”和“disabl”。