- 内核版本: linux-5.13.5
- 设备树文件所在路径:linux-5.13.5archarmbootdts
- 每个xxx.dts对应一个板子
dts目录下有两种文件:xxx.dts xxx.dtsi, 因为每个板子都有一个xxx.dts文件,在有多个板子共用同一个芯片时,那么dts文件中对于芯片的信息就会重复,这时就需要将这些重复的信息放入一个文件中,也就是dtsi文件,供dts文件调用,调用方式:
#include "omap36xx.dtsi"2. 设备树文件编译
Linux在目录:linux/scripts/dtc下准备了编译设备树文件的工具DTC,在make dtbs的时候,会根据Makefile找到该目录下的DTC工具进行编译
如果把.dts文件当作.c文件,那么DTC工具就相当于gcc编译器,它会将.dts文件编译成.dtb文件,.dtb文件就相当于.bin文件
在Ubuntu中编译设备树文件方法:
export ARCH=arm make deconfig make dtbs
由打印信息可以看出,在arch/arm/boot/dts目录下生成了很多的dtb文件,make ARCH=arm设置的就是编译arch/arm下的dts文件,当然arch下还有很多架构,可以按需修改,进入如下目录可以看到生成的dtb文件。
如果我们自己写了一个dts文件,比如叫做zyuandy-zhang-can.dts,我们需要将该文件添加到arch/arm/boot/dts/目录下,再打开arch/arm/boot/dts/Makefile中添加zyuandy-zhang-can.dtb即可,因为dtc的编译规则是根据名字找到xxx.dts文件编译成xxx.dtb,具体添加到哪个系列下,要看开发板的型号,我添加到了CONFIG_ARCH_EXYNOS4系列下,make dtbs之后就会生成对应的dtb
3. DTS语法 3.1 设备节点设备树以根目录" / "开始,每个根目录可以理解为一个开发板,那么每个节点就是该开发板上的设备
// SPDX-License-Identifier: GPL-2.0
/dts-v1/;
#include "omap36xx.dtsi"
#include "omap3-cm-t3x30.dtsi"
/ {
model = "CompuLab CM-T3730";
compatible = "compulab,omap3-cm-t3730", "ti,omap3630", "ti,omap36xx", "ti,omap3";
...
};
&omap3_pmx_wkup {
dss_dpi_pins_cm_t3730: pinmux_dss_dpi_pins_cm_t3730 {
pinctrl-single,pins = <
OMAP3_WKUP_IOPAD(0x2a08, PIN_OUTPUT | MUX_MODE3)
...
>;
};
};
&omap3_pmx_wkup节点的意思是向omap36xx.dtsi中的omap3_pmx_wkup节点中追加信息,在omap36xx.dtsi文件中没有搜索到omap3_pmx_wkup,但是该文件引入了omap3.dtsi文件,在omap3.dtsi中找到了节点:
omap3_pmx_wkup: pinmux@a00 {
compatible = "ti,omap3-padconf",
"pinctrl-single";
reg = <0xa00 0x5c>;
#address-cells = <1>;
#size-cells = <0>;
#pinctrl-cells = <1>;
#interrupt-cells = <1>;
interrupt-controller;
pinctrl-single,register-width = <16>;
pinctrl-single,function-mask = <0xff1f>;
};
那么追加之后,节点就变成了:
omap3_pmx_wkup: pinmux@a00 {
compatible = "ti,omap3-padconf",
"pinctrl-single";
reg = <0xa00 0x5c>;
#address-cells = <1>;
#size-cells = <0>;
#pinctrl-cells = <1>;
#interrupt-cells = <1>;
interrupt-controller;
pinctrl-single,register-width = <16>;
pinctrl-single,function-mask = <0xff1f>;
dss_dpi_pins_cm_t3730: pinmux_dss_dpi_pins_cm_t3730 {
pinctrl-single,pins = <
OMAP3_WKUP_IOPAD(0x2a08, PIN_OUTPUT | MUX_MODE3)
...
>;
};
};
节点定义格式: 节点名称@节点单元地址
chipid@10000000 { //chipid就是节点名称 1000000为该节点的起始地址
...
};
还有一种格式: 节点标签名: 节点名称@节点单元地址
serial_0: serial@13800000 { //serial_0是标签名 serial是节点名称 13800000是节点的起始地址
...
};
由于有的节点名字过长,使用的时候不方便,所以就有了标签名,例如&serial_0就表示引用serial节点
serial节点就是用来描述数据手册中UART0的信息,在数据手册的Memory Map部分,如下图:
sdhci_0: sdhci@12510000 {
compatible = "samsung,exynos4210-sdhci";
reg = <0x12510000 0x100>;
interrupts = ;
clocks = <&clock CLK_SDMMC0>, <&clock CLK_SCLK_MMC0>;
clock-names = "hsmmc", "mmc_busclk.2";
status = "disabled";
};
compatible属性:该属性用来绑定设备和驱动,格式:compatible = “厂商,驱动名称",一个节点代表一个设备,sdhci设备要想运行就需要驱动程序,在驱动程序中会有一个of_match_table属性,用来表示该驱动程序支持哪些设备,如下图所示,sdhci_s3c_dt_mach[]中的compatible属性支持 samsung公司的s3c6410-sdhci设备与samsung公司的exynos4210-sdhci设备
status属性:该属性表示设备的状态信息,格式:status = “value”; 常用的value有两种
| value | describe |
|---|---|
| okey | 设备可操作 |
| disable | 设备不可操作 |
reg属性:表示该设备的地址信息,格式:reg = addr表示设备的起始地址,len代表地址的长度,合起来就是该设备的地址空间,具体如何配置需要参照数据手册
#address-cells与#size-cells属性
apb: bus@fe000000 { //某些地址为了讲解方便进行了修改
compatible = "simple-bus";
reg = <0x0 0xfe000000 0x0 0x1000000>;
#address-cells = <2>;
#size-cells = <2>;
ranges = <0x1 0x2 0x3 0xfe000000 0x5 0x1000000>;
reset: reset-controller@0 {
compatible = "amlogic,meson-a1-reset";
reg = <0x1 0x3 0x2 0x8c>;
#reset-cells = <1>;
};
这两个属性用来控制子结点中reg属性中addr和len的数量,格式:#address-cells =
rangs属性,该属性是一个地址转换表,格式:rangs =
根节点的compatible属性:前边讲到设备节点的compatible属性是用来匹配设备与驱动的,根节点的compatible属性比较特殊,因为根节点代表的不是设备而更像是开发板,难道Linux内核烧到开发板上就能运行,显然不是的,要看内核支不支持该开发板,在没有引入设备树之前,是通过机器ID来判断的,这里主要讲解设备树的匹配方式,就是根据根节点的compatible属性判断,一个板子对应一个设备树文件,在内核启动阶段会调用start_kernel()函数
start_kernel() -> setup_arch(&command_line) -> setup_machine_fdt() -> of_flat_dt_match_machine() 在该函数中:
dt_root = of_get_flat_dt_root(); //获取设备树的根节点
while ((data = get_next_compat(&compat))) { //获取machine_desc中的.dt_compat属性
score = of_flat_dt_match(dt_root, compat); //将根节点的cpmpatible属性与每个machine_desc中的.dt_compat进行比较,相同就返回machine_desc
if (score > 0 && score < best_score) {
best_data = data;
best_score = score;
}
}
如果返回的best_data不为空,就证明找到了对应开发板的驱动程序
get_next_compat会调用arch_get_next_mach函数,配合while循环到__arch_info_begin段去遍历machine_desc结构体,将取到的machine_desc结构体的.dt_compat属性存入compat中,作为of_flat_dt_match函数的参数
static const void * __init arch_get_next_mach(const char *const **match)
{
static const struct machine_desc *mdesc = __arch_info_begin;
const struct machine_desc *m = mdesc;
if (m >= __arch_info_end)
return NULL;
mdesc++;
*match = m->dt_compat;
return m;
}
of_flat_dt_match会将取到的.dt_compat属性与根节点的compatible属性进行比较,如果相同说明开发板与设备树文件匹配成功
static int __init of_flat_dt_match(unsigned long node, const char *const *compat)
{
unsigned int tmp, score = 0;
if (!compat)
return 0;
while (*compat) {
tmp = of_fdt_is_compatible(initial_boot_params, node, *compat);
if (tmp && (score == 0 || (tmp < score)))
score = tmp;
compat++;
}
return score;
}
前边说到去__arch_info_begin去遍历machine_desc结构体,为什么呢?
在Linux/arch/arm目录下有很多的mach-xxxx文件,这里边的文件基本都是有厂家编写好的,每一个都代表一个或一系列的开发板,那么我们就以mach-imx目录下的mach-imx6q.c举例,该文件就是用来做开发板与设备树匹配的
在该文件中,我们可以看到以下代码
static const char * const imx6q_dt_compat[] __initconst = {
"fsl,imx6dl",
"fsl,imx6q",
"fsl,imx6qp",
NULL,
};
DT_MACHINE_START(IMX6Q, "Freescale i.MX6 Quad/DualLite (Device Tree)")
.l2c_aux_val = 0,
.l2c_aux_mask = ~0,
.smp = smp_ops(imx_smp_ops),
.map_io = imx6q_map_io,
.init_irq = imx6q_init_irq,
.init_machine = imx6q_init_machine,
.init_late = imx6q_init_late,
.dt_compat = imx6q_dt_compat,
MACHINE_END
DT_MACHINE_START在linux/arch/arm/include/asm/mach/arch.h中定义,内容如下
#define DT_MACHINE_START(_name, _namestr)
static const struct machine_desc __mach_desc_##_name
__used
__section(".arch.info.init") = {
.nr = ~0,
.name = _namestr,
#endif
宏定义展开之后其实是定义了一个machine_desc结构体,并将该结构体存到了.arch.info.init段中,这就是去__arch_info_begin去遍历machine_desc结构体的原因
现在machine_desc知道了从何而来,要到哪里去,可是要和设备树的根节点作比较,machine_desc从.arch.info.init段中获取,那设备树文件在哪取出来的?
of_flat_dt_match调用的of_fdt_is_compatible函数中有一个参数initial_boot_params,这个就是设备树文件的起始位置,是uboot提供给内核的,根节点就是从initial_boot_params指向的设备树中取出的,这样设备树文件与设备的匹配就清楚了
那驱动程序与开发板的设备是怎么匹配上的呢?
开发板就是我们说的设备树文件,以linux/arch/arm/boot/dts/s3c64xx.dtsi文件举例,假设我们的开发版已经和该文件匹配成功,所以该文件中每一个节点就代表开发板上的一个设备
sdhci0: sdhci@7c200000 {
compatible = "samsung,s3c6410-sdhci";
...
};
要想使用该sdhci0设备,就需要有对应的驱动程序,在前文通用属性,设备的compatible属性的讲解中,该设备的compatible属性值"samsung,s3c6410-sdhci"与drivers/mmc/host/sdhci-s3c.c文件中的sdhci_s3c_dt_mach[]中的compatible属性相同,所以该设备的驱动程序就是sdhci-s3c.c文件,这样驱动程序与开发板的设备关系就清楚了
5. 设备树解析调用流程start_kernel() -> setup_arch(&command_line) -> unflatten_device_tree()
void __init unflatten_device_tree(void)
{
__unflatten_device_tree(initial_boot_params, NULL, &of_root,
early_init_dt_alloc_memory_arch, false);
of_alias_scan(early_init_dt_alloc_memory_arch);
unittest_unflatten_overlay_base();
}
of_root(linuxdrivers/of/fdt.c)在__unflatten_device_tree执行之后会指向设备树的根节点,__unflatten_device_tree第一次调用unflatten_dt_nodes给设备节点们分配空间,第二次调用才是真正的解析出了节点的内容,并将内容封装成device_node结构体
void *__unflatten_device_tree(const void *blob,
struct device_node *dad,
struct device_node **mynodes,
void *(*dt_alloc)(u64 size, u64 align),
bool detached)
{
int size;
void *mem;
int ret;
size = unflatten_dt_nodes(blob, NULL, dad, NULL);
size = ALIGN(size, 4);
mem = dt_alloc(size + 4, __alignof__(struct device_node));
memset(mem, 0, size);
*(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);
ret = unflatten_dt_nodes(blob, mem, dad, mynodes);
...
return mem;
}
这时,设备树文件已经被解析成多个device_node存储到了内存中,等到之后用到设备树的驱动程序调用
6. 绑定信息文档在linuxdocumentationdevicetreebindings下有很多的绑定信息文档,以.txt形式存在,可以帮助我们更好的编写设备树文件和添加设备节点
Required properties:
- compatible: Every devices present in OMAP SoC should be in the
form: "ti,XXX"
- ti,hwmods: list of hwmod names (ascii strings), that comes from the OMAP
HW documentation, attached to a device. Must contain at least
one hwmod.
Optional properties:
- ti,no_idle_on_suspend: When present, it prevents the PM to idle the module
during suspend.
- ti,no-reset-on-init: When present, the module should not be reset at init
- ti,no-idle-on-init: When present, the module should not be idled at init
- ti,no-idle: When present, the module is never allowed to idle.
Example:
spinlock@1 {
compatible = "ti,omap4-spinlock";
ti,hwmods = "spinlock";
};
7. of函数集的使用
linuxincludelinuxof.h中定义了很多以of开头的函数,这些函数可以用来获取设备的节点信息,要使用这些函数要引入一些头文件
#include#include #include
假设我们要获取下面代码中的uart4节点的compatible属性
/ {
cpus {
cpu: cpu@0 {
operating-points-v2 = <&cpu0_opp_table>;
clock-latency = <300000>;
};
};
ocp@68000000 {
uart4: serial@4809e000 {
compatible = "ti,omap3-uart";
ti,hwmods = "uart4";
status = "disabled";
reg = <0x4809e000 0x400>;
interrupts = <84>;
dmas = <&sdma 55 &sdma 54>;
dma-names = "tx", "rx";
clock-frequency = <48000000>;
};
};
};
函数:
static inline struct device_node *of_find_node_by_path(const char *path);//根据节点路径查找指定结点
用法举例:
struct device_node *node;
node = of_find_node_by_path("/ocp/uart4"); //uart4也可以用serial
节点获取到了不是我们最终的目标,我们的最终目标是获取节点的属性值,可以通过以下函数进行获取
属性结构体(linux/include/linux/of.h):
struct property {
char *name;
int length;
void *value;
struct property *next;
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
unsigned long _flags;
#endif
#if defined(CONFIG_OF_PROMTREE)
unsigned int unique_id;
#endif
#if defined(CONFIG_OF_KOBJ)
struct bin_attribute attr;
#endif
};
函数:
extern struct property *of_find_property(const struct device_node *np, // 节点 const char *name, // 属性名称 int *lenp); // 属性值的字节数,一般为NULL
用法举例:
struct property *prop;
prop = of_find_property(node, "compatible", NULL); // node为之前获取到的节点
printf("compatible = %srn", prop->value); //该输出值等于"ti,omap3-uart"
在of.h中获取节点与其属性的函数还有很多这里不再一一举例
随便找了一个驱动函数的probe函数,看一看它是怎么获取节点与节点的属性的
static int plat_mpc8xxx_spi_probe(struct platform_device *pdev)
{
struct resource *mem;
int irq;
struct spi_master *master;
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!mem)
return -EINVAL;
irq = platform_get_irq(pdev, 0);
if (irq <= 0)
return -EINVAL;
return PTR_ERR_OR_ZERO(master);
}
在上述代码中,我们并没有看到它用of函数获取结点,也没有看到device_node结构体,但是获取到的mem就是节点的reg属性,它是怎么获取到的呢?
of_platform_default_populate_init -> of_platform_populate() -> of_platform_bus_create() -> of_platform_device_create_pdata() -> of_device_alloc()
static int __init of_platform_default_populate_init(void)
{
struct device_node *node;
for_each_matching_node(node, reserved_mem_matches)
of_platform_device_create(node, NULL, NULL);
node = of_find_node_by_path("/firmware");
if (node) {
of_platform_populate(node, NULL, NULL, NULL);
of_node_put(node);
}
of_platform_default_populate(NULL, NULL, NULL);
return 0;
}
arch_initcall_sync(of_platform_default_populate_init);
由于用arch_initcall_sync,该函数会在启动内核的过程中被调用
经过上述的调用过程,最终会调用到of_device_alloc函数
struct platform_device *of_device_alloc(struct device_node *np,
const char *bus_id,
struct device *parent)
{
struct platform_device *dev;
int rc, i, num_reg = 0, num_irq;
struct resource *res, temp_res;
dev = platform_device_alloc("", PLATFORM_DEVID_NONE); //申请platform_device结构体空间
if (!dev)
return NULL;
while (of_address_to_resource(np, num_reg, &temp_res) == 0) //从设备树中获取reg的数量
num_reg++;
num_irq = of_irq_count(np); //获取irq的数量
if (num_irq || num_reg) {
res = kcalloc(num_irq + num_reg, sizeof(*res), GFP_KERNEL); //申请resource结构体空间
if (!res) {
platform_device_put(dev);
return NULL;
}
dev->num_resources = num_reg + num_irq;
dev->resource = res; //将申请的res赋值给platform_device结构体的resource属性
for (i = 0; i < num_reg; i++, res++) { //将device_node的reg属性赋值给resource的属性
rc = of_address_to_resource(np, i, res);
WARN_ON(rc);
}
if (of_irq_to_resource_table(np, res, num_irq) != num_irq)//将device_node的irq属性赋值给resource的属性
pr_debug("not all legacy IRQ resources mapped for %pOFnn",
np);
}
dev->dev.of_node = of_node_get(np); //将device_node赋值给platform_device的dev.of_node属性
dev->dev.fwnode = &np->fwnode;
dev->dev.parent = parent ? : &platform_bus;
if (bus_id)
dev_set_name(&dev->dev, "%s", bus_id);
else
of_device_make_bus_id(&dev->dev);
return dev;
}
EXPORT_SYMBOL(of_device_alloc);
该函数会将device_node转化为platform_device,所以之前的通过platform_device来获取节点的reg属性就很合理了



