前置知识篇
1. 进程
2. 线程
进程间通信篇
1. IPC概述
2. 信号
3. 消息传递
4. 同步
5. 共享内存区
编译相关篇
1. GCC编译
2. 静态链接与动态链接
3. makefile入门基础
设备驱动篇
1. 设备驱动概述
2. 内核模块_理论篇
3. 内核模块_实验篇
4. 字符设备_理论篇1
5. 字符设备_理论篇2
6. 字符设备_实验篇1
7. 字符设备_实验篇2
8. 设备模型
9. 平台设备驱动
10. 设备树1_添加设备节点
本节主要介绍设备树的基本概念与框架,以及节点基本格式与属性,最后进行了一个添加设备节点的实验。
无
《 [野火]i.MX Linux开发实战指南》
百度
《以imx6ull为例深度讲解Linux内核设备树语法 | 设备树实例解析之全网最强科普文》
在platform_device部分有简单说明描述设备有两种方法:一种是使用platform_device结构体来指定;另一种是使用设备树来描述。
Linux3.x以后的版本才引入了设备树,设备树用于描述一个硬件平台的板级细节。
在早些的linux内核,这些“硬件平台的板级细节”保存在linux内核目录“/arch”,
以ARM平台为例“硬件平台的板级细节”保存在“/arch/arm/plat-xxx”和“/arch/arm/mach-xxx”目录下。
随着处理器数量的增多用于描述“硬件平台板级细节”的文件越来越多导致Linux内核非常臃肿, Linux之父发现这个问题之后决定使用设备树解决这个问题。
设备树简单、易用、可重用性强,linux3.x之后大多采用设备树编写驱动。

关于设备树的详细请参考:https://www.devicetree.org/
设备树的作用就是描述一个硬件平台的硬件资源。
这个“设备树”可以被bootloader(uboot)传递到内核,内核可以从设备树中获取硬件信息。


设备树描述硬件资源时有两个特点。
以“树状”结构描述硬件资源。
例如本地总线为树的“主干”在设备树里面称为“根节点”,
挂载到本地总线的IIC总线、SPI总线、UART总线为树的“枝干”在设备树里称为“根节点的子节点”,
IIC 总线下的IIC设备不止一个,这些“枝干”又可以再分。
设备树可以像头文件(.h文件)那样,一个设备树文件引用另外一个设备树文件, 这样可以实现“代码”的重用。
例如多个硬件平台都使用i.MX6ULL作为主控芯片,那么我们可以将i.MX6ULL芯片的硬件资源写到一个单独的设备树文件里面,
一般使用“.dtsi”后缀, 其他设备树文件直接使用“# include xxx”引用即可。
DTS、DTC和DTB它们是文档中常见的几个缩写。

DTS 是指.dts格式的文件,
是一种 ASCII 文本格式的设备树描述,也是我们要编写的设备树源码,
一般一个.dts文件对应一个硬件平台,位于Linux源码的“/arch/arm/boot/dts”目录下。
DTSI
为了减少冗余,设备树头文件格式为.dtsi文件,可以被不同的.dts文件引用。
DTC 是指编译设备树源码的工具,
一般情况下我们需要手动安装这个编译工具。
DTB 是设备树源码编译生成的文件,
类似于我们C语言中“.C”文件编译生成“.bin”文件。
下面的内容将围绕着设备树源码,来讲解设备树框架和基本语法。
设备树 = 根节点(属性 + 子节点 + 子节点的子节点) + 子节点追加内容

设备树 (内核源码/arch/arm/boot/dts/imx6ull-seeed-npi.dts)
/* 设备树是可以像C语言那样使用“#include”引用“.h”后缀的头文件,也可以引用设备树“.dtsi”后缀的头文件 */
#include
/* /imx6ull.dtsi由NXP官方提供,是一个imx6ull平台“共用”的设备树文件 */
#include "imx6ull.dtsi"
/* 设备树节点 */
/ {
model = "Seeed i.MX6 ULL NPi Board";
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
/* 在根节点内部的“aliases {…}”、“chosen {…}”、“memory {…}”等字符,都是根节点的子节点 */
aliases {
pwm0 = &pwm1;
pwm1 = &pwm2;
pwm2 = &pwm3;
pwm3 = &pwm4;
};
chosen {
stdout-path = &uart1;
};
memory {
reg = <0x80000000 0x20000000>;
};
reserved-memory {
#address-cells = <1>;
#size-cells = <1>;
ranges;
linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x14000000>;
linux,cma-default;
};
};
/*-------------以下内容省略-------------*/
};
其中,
设备树追加内容
/* 这些“已经存在的节点”可能定义在“imx6ull-seeed-npi.dts”文件,也可能定义在“imx6ull-seeed-npi.dts”文件所包含的设备树文件里 */
/* 本代码中的“&cpu0 {…}”、“&clks {…}”、“&fec1 {…}”等等追加的目标节点,就是定义在“imx6ull.dtsi”中 */
&cpu0 {
dc-supply = <®_gpio_dvfs>;
clock-frequency = <800000000>;
};
&clks {
assigned-clocks = <&clks IMX6UL_CLK_PLL4_AUDIO_DIV>;
assigned-clock-rates = <786432000>;
};
&fec1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_enet1>;
phy-mode = "rmii";
phy-handle = <ðphy0>;
status = "okay";
};
/*-------------以下内容省略--------------*/
其中
到目前为止我们知道设备树由一个根节点和众多子节点组成,子节点也可以继续包含其他节点,也就是子节点的子节点。
设备树的组成很简单,下面我们一起来看看节点的基本格式和节点属性。
设备树中的每个节点都按照以下约定命名:
node-name@unit-address{
属性1 = …
属性2 = …
属性3 = …
子节点…
}
node-name 节点名称
节点格式中的 node-name 用于指定节点的名称。
它的长度为1至31个字符,只能由如下字符组成

另外,节点名应当使用大写或小写字母开头,并且能够描述设备类别。
注意,根节点没有节点名,它直接使用“/”指代这是一个根节点。
@unit-address
其中的符号“@”可以理解为是一个分割符,
“unit-address”用于指定“单元地址”, 它的值要和节点“reg”属性的第一个地址一致。
如果节点没有“reg”属性值,可以直接省略“@unit-address”,
不过要注意这时要求同级别的设备树下(相同级别的子节点)节点名唯一,
从这个侧面也可以了解到,同级别的子节点的节点名可以相同,但是要求“单元地址”不同,
node-name@unit-address 的整体要求同级唯一。
节点标签
在imx6ull.dtsi头文件中,节点名“cpu”前面多了个“cpu0”,这个“cpu0”就是我们所说的节点标签。
通常节点标签是节点名的简写,
所以它的作用是当其它位置需要引用时可以使用节点标签来向该节点中追加内容。
节点路径
通过指定从根节点到所需节点的完整路径,可以唯一地标识设备树中的节点,不同层次的设备树节点名字可以相同,同层次的设备树节点要唯一。
这有点类似于我们Windows上的文件,一个路径唯一标识一个文件或文件夹,不同目录下的文件文件名可以相同。
节点路径指的是在设备树下的路径,如在根节点下创建,则路径为/led_test,如在子节点中再创建个子节点,则路径为/led_test/red_led
在节点的“{}”中包含的内容是节点属性,
通常情况下一个节点包含多个属性信息,
这些属性信息就是要传递到内核的“板级硬件描述信息”,
驱动中会通过一些API函数获取这些信息。
例如根节点“/”就有属性compatible = “fsl,imx6ull-14x14-evk”, “fsl,imx6ull”。
我们可以通过该属性了解到硬件设备相关的名字叫“imx6ull-14x14-evk”,设备所使用的的是“imx6ull”这颗 SOC。
我们编写设备树最主要的内容是编写节点的节点属性,通常情况下一个节点代表一个设备,
设备有哪些属性、怎么编写这些属性、在驱动中怎么引用这些属性是我们后面讲解的重点,这一小节只讲解设备节点有哪些可设置属性。
有一些节点属性是所有节点共有的,一些作用于特定的节点, 我们这里介绍那些共有的节点属性,其他节点属性使用到时再详细介绍。
节点属性分为标准属性和自定义属性,也就是说我们在设备树中可以根据自己的实际需要定义、添加设备属性。
标准属性的属性名是固定的,自定义属性名可按照要求自行定义。
属性值类型:字符串
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
/* 节点标签: 节点名称@单元地址 */
intc: interrupt-controller@a01000 {
compatible = "arm,cortex-a7-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0xa01000 0x1000>,<0xa02000 0x100>;
};
compatible属性值由一个或多个字符串组成,有多个字符串时使用“,”分隔开。
设备树中的每一个代表了一个设备的节点都要有一个compatible属性。
compatible是系统用来决定绑定到设备的设备驱动的关键。
compatible属性是用来查找节点的方法之一,另外还可以通过节点名或节点路径查找指定节点。
例如系统初始化时会初始化platform总线上的设备时,根据设备节点“compatible”属性和驱动中of_match_table对应的值,匹配了就加载对应的驱动。
属性值类型:字符串
model = "Embedfire i.MX6ULL Board";
model属性用于指定设备的制造商和型号,推荐使用“制造商, 型号”的格式,当然也可以自定义,本例子就没有使用这种格式。
compatible属性是一个字符串列表,表示可以你的硬件兼容A、B、C等驱动;model用来准确地定义这个硬件是什么。
属性值类型:字符串
/* External sound card */
sound: sound {
status = "disabled";
};
状态属性用于指示设备的“操作状态”,通过status可以去禁止设备或者启用设备,可用的操作状态如下表。



默认情况下不设置status属性设备是使能的。
属性值类型:u32
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;
ocrams: sram@900000 {
compatible = "fsl,lpm-sram";
reg = <0x900000 0x4000>;
};
};
#address-cells 和 #size-cells属性同时存在,在设备树ocrams结构中,它们用在有子节点的设备节点(节点),用于设置子节点的“reg”属性的“书写格式”。
补充:
#address-cells,用于指定子节点reg属性“地址字段”所占的长度(单元格cells的个数)。
#size-cells,用于指定子节点reg属性“大小字段”所占的长度(单元格cells的个数)。
例如#address-cells=2,#address-cells=1,
则reg内的数据含义为reg = < address address size address address size >, 因为每个cells是一个32位宽的数字,例如需要表示一个64位宽的地址时,就要使用两个address单元来表示。
而假如#address-cells=1,#size-cells=1,则reg内的数据含义为reg = < address size address size address size>。
总之#size-cells和#address-cells决定了子节点的reg属性中 哪些数据是“地址”,哪些数据是“长度”信息,即描述一块内存的起始地址和大小 。
属性值类型:地址、长度数据对
reg属性描述设备资源在其父总线定义的地址空间内的地址。
通常情况下用于表示一块寄存器的起始地址(偏移地址)和长度, 在特定情况下也有不同的含义。
例如上例中#address-cells = < 1 >,#address-cells = < 1 >,reg = < 0x9000000 x4000 >,
其中0x9000000表示的是地址,0x4000表示的是地址长度,这里的reg属性指定了起始地址为0x9000000,长度为0x4000的一块地址空间。
属性值类型:任意数量的 <子地址、父地址、地址长度>编码
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;
busfreq {
/*-------------以下内容省略--------------*/
};
}
该属性提供了子节点地址空间和父地址空间的映射(转换)方法,常见格式是ranges = <子地址, 父地址, 转换长度>。
如果父地址空间和子地址空间相同则无需转换,如示例中所示,只写了ranges,内容为空,我们也可以直接省略renges属性。
比如对于#address-cells和#size-cells都为1的话,以ranges=< 0x0 0x10 0x20 >为例,
表示将子地址的从0x0 ~ (0x0 + 0x20)的地址空间映射到父地址的0x10~(0x10 + 0x20)。
属性值类型:字符串。
example{
name = "name"
}
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
}
}
这两个属性很少用(已经被废弃),不推荐使用。
name用于指定节点名,在旧的设备树中它用于确定节点名,现在我们使用的设备树已经弃用。
device_type属性也是一个很少用的属性,只用在CPU和内存的节点上。
如上例中所示,device_type用在了CPU节点。
&cpu0 {
dc-supply = <®_gpio_dvfs>;
clock-frequency = <800000000>;
};
这些源码并不包含在根节点“/{…}”内,它们不是一个新的节点,而是向原有节点追加内容。
以上方源码为例,“&cpu0”表示向“节点标签”为“cpu0”的节点追加数据,
这个节点可能定义在本文件也可能定义在本文件所包含的设备树文件中,本例子中源码的“cpu0”定义在“imx6ull.dtsi”文件中。
aliases子节点的作用就是为其他节点起一个别名( 左为别名 ),如下所示
aliases {
can0 = &flexcan1;
can1 = &flexcan2;
ethernet0 = &fec1;
ethernet1 = &fec2;
gpio0 = &gpio1;
gpio1 = &gpio2;
gpio2 = &gpio3;
gpio3 = &gpio4;
gpio4 = &gpio5;
i2c0 = &i2c1;
i2c1 = &i2c2;
/*----------- 以下省略------------*/
}
以“can0 = & flexcan1;”为例。
"flexcan1"是一个节点的名字,设置别名后我们可以使用"can0"来指代 flexcan1节点,与节点标签类似。
在设备树中更多的是为节点添加标签,没有使用节点别名,别名的作用是“快速找到设备树节点”。
在驱动中如果要查找一个节点,通常情况下我们可以使用“节点路径”一步步找到节点。
也可以使用别名“一步到位”找到节点。
chosen子节点位于根节点下,如下所示
chosen {
stdout-path = &uart1;
};
chosen子节点不代表实际硬件,它主要用于给内核传递参数。
这里只设置了“stdout-path =& uart1;”一条属性,表示系统标准输出stdout使用串口uart1。
此外这个节点还用作uboot向linux内核传递配置参数的“通道”, 我们在Uboot中设置的参数就是通过这个节点传递到内核的,这部分内容是uboot和内核自动完成的,作为初学者我们不必深究。
一般不需要我们设置,在 dtsi 文件中都定义好了,如:
cpus {
# address-cells = <1>;
# size-cells = <0>;
cpu0: cpu@0 {
.......
}
};
芯片厂家不可能事先确定你的板子使用多大的内存,所以 memory 节点需要板厂设置,比如:
memory {
reg = <0x80000000 0x20000000>;
};.
在中断、时钟部分也有自己的节点标准属性,随着深入的学习我们会详细介绍这些节点标准属性。
直接查看arch/arm/boot/dts/Makefile文件,在该文件中查找即可

Linux 内核启动的时候会解析设备树 dtb 文件,所以启动以后可以在根文件系统中看到设备树的节点信息,在/proc/device-tree目录中:

这里 device-tree 目录是一个软链接,实际指向/sys/firmware/devicetree/base目录。
在 device-tree 目录中,首先可以看到设备树根节点下的所有一级子节点。
属性是以文件的方式给出,可以直接查看。
比如查看根节点的 model 属性:

节点以目录的方式给出。
比如 interrupts 子节点的内容如下:

在设备树中添加一个新的节点时,添加的格式在 Linux 内核源码中有详细的.txt 文档描述,这些 txt 文档就称为绑定文档。
绑定文档在/Documentation/devicetree/bindings路径中:

比如我们在开发板的 i2c 上新添加了一个设备,需要在设备树的 i2c 节点下新添加一个节点,就可以查看i2c/i2c-imx.txt文档:

通常情况下我们几乎不会从零开始写一个设备树,因为一个功能完善的设备树通常比较庞大,
例如本教程引用的NXP官方编写的设备树“imx6ull.dtsi”就多达1000行,另外官方已经写好了主干的部分,我们只需要引用官方写好的设备树,然后根据自己的实际情况修改即可。
在实际应用中我们最常见的操作是 向设备节点中增加一个节点 、 向现有设备节点追加数据 、和 编写设备树插件 。
根据之前讲解,我们的系统默认使用的是“ebf-buster-linux/arch/arm/boot/dts/imx6ull-seeed-npi.dts”设备树, 我们就在这个设备树里尝试增加一个设备节点,如下所示。
/ {
model = "Seeed i.MX6 ULL NPi Board";
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
/* 别名 */
aliases {
pwm0 = &pwm1;
pwm1 = &pwm2;
pwm2 = &pwm3;
pwm3 = &pwm4;
};
/* 添加led节点 */
led_test{
/* 表示reg属性的数据是“地址”、“长度”交替的 */
#address-cells = <1>;
#size-cells = <1>;
/* led 节点的子节点 */
rgb_led_red@0x0209C000{
/* compatible属性是用来查找节点的方法之一 */
compatible = "fire,rgb_led_red";
/* reg属性 */
reg = <0x0209C000 0x00000020>;
/* 操作状态 */
status = "okay";
};
};
};
在我们在imx6ull-seeed-npi.dts设备树文件的根节点末尾新增了一个节点名为“led_test”的节点, 里面只添加了几个基本属性,我们这里只是学习添加一个设备节点。
需要注意的是rgb属性, 在父节点设置了#address-cells = < 1 >,#size-cells = < 1 >,
所以 这里0x0209C000表示的是地址(这里填写的是GPIO1控制寄存器的首地址), 0x00000020表示的是地址长度 。
“rgb_led_red@0x0209C000”中的单元的地址0x0209C000要和reg属性的第一个地址一致。
编译内核时会自动编译设备树,但是编译内核很耗时,所以我们推荐使用如下命令只编译设备树(在linux源码下进行编译)。
命令:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- npi_v7_defconfig
make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs

编译成功后生成的设备树文件(.dtb)位于源码目录下的/arch/arm/boot/dts/,文件名为“imx6ull-seeed-npi.dtb”
替换/boot/dtbs/4.19.71-imx-r1/imx6ull-seeed-npi.dtb。
uboot在启动的时候负责该目录的设备文件加载到内存,供内核解析使用。
重启开发板。
设备树中的设备树节点在文件系统中有与之对应的文件,位于“/proc/device-tree”目录。进入“/proc/device-tree”目录如下所示。

设备树的作用就是描述一个硬件平台的硬件资源。
这个“设备树”可以被bootloader(uboot)传递到内核,内核可以从设备树中获取硬件信息。
设备树可以像头文件(.h文件)那样,一个设备树文件引用另外一个设备树文件, 这样可以实现“代码”的重用
设备树 = 根节点(属性 + 子节点 + 子节点的子节点) + 子节点追加内容
每一个“{}”都是一个“节点”,“/ {…};”表示“根节点”,每一个设备树只有一个根节点,不同文件的根节点最终会合并为一个
设备树追加内容,比根节点下的子节点多了一个“&”, 这表示该节点在向已经存在的子节点追加数据
#address-cells = < 1 >,#address-cells = < 1 >,reg = < 0x9000000 x4000 >,其中0x9000000表示的是地址,0x4000表示的是地址长度,这里的**reg属性指定了起始地址为0x9000000,长度为0x4000的一块地址空间