Linux设备树简析

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 设备树的来源

Linux 中,每个设备驱动,管理一组设备数据,类似面向对象编程中类和其实例对象的关系。一段时间以来,这些设备数据硬编码在内核中,导致了内核代码的急剧膨胀(尤其是在ARM架构下),同时影响了维护的便利性。社区总瓢把子 Linus 对此表达了强烈的不满,要求整改。针对该问题,社区经过一系列地讨论,引入了设备树。
设备树是数据驱动逻辑思想(即数据与逻辑分离)的一个典型应用。通过将设备数据从内核代码迁移到设备树文件(.dts)中,然后经由 DTC(Device Tree Compiler) 编译器,将设备树文件 .dts 编译成 DTB(Device Tree Blob) 数据文件;内核通过对 DTB 数据文件的解析展开,最终以展开的设备树为基础,创建设备驱动的设备对象。我们用下图来描述整个过程:

     dtc       unflatten_device_tree()               of_platform_populate()
.dts ---> .dtb ----------------------> 设备树 of_root --------------------> 创建驱动设备对象

3. 设备树文件的创建

《Power_ePAPR_APPROVED_v1.1.pdf》规范文档定义的语法,根据系统中实际硬件设备的拓扑,构建设备树文件。
Linux 内核的设备树文件,按不同的硬件架构和硬件,组织定义在内核源码目录 arch/arch-XXX/boot/dts/* 下。
设备树文件组织成树形结构,只有1个根节点。如:
在这里插入图片描述

4. 设备树文件的编译

设备树文件 .dts ,经由内核代码目录下的编译器 scripts/dtc/dtc , 编译成 .dtb 文件:

      dtc
.dts -----> .dtb

5. 设备树的展开

BootLoader 将设备树数据文件 .dtb 在内存中的物理地址传递给内核,内核解析该数据文件,然后展开它,具体代码流程如下:

start_kernel()
	setup_arch()
		unflatten_device_tree()
			/* 将 .dtb 展开为以 of_root 为根的设备树 */
			__unflatten_device_tree(initial_boot_params, NULL, &of_root, 
									early_init_dt_alloc_memory_arch, false)
				/* 第1遍,计算设备树展开后的大小 */
				size = unflatten_dt_nodes(blob, NULL, dad, NULL);
				
				/* 为展开后的设备树分配空间: 额外4字节存储展开后设备树的魔数 */
				mem = dt_alloc(size + 4, __alignof__(struct device_node));
				
				/* 在最后4字节,存储展开后设备树的魔数 */
				*(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);
				
				/* 第2遍,做设备树实际的展开动作 */
				unflatten_dt_nodes(blob, mem, dad, mynodes)
					for (offset = 0;
					     offset >= 0 && depth >= initial_depth;
					     offset = fdt_next_node(blob, offset, &depth)) {
					     populate_node(blob, offset, &mem, ...)
					     	/* 分配节点空间 */
					     	struct device_node *np = unflatten_dt_alloc(mem, sizeof(struct device_node) + allocl, ...);
							of_node_init(np)
					     		kobject_init(&node->kobj, &of_node_ktype);
								node->fwnode.ops = &of_fwnode_ops;
							/* 展开节点属性 */
							populate_properties(blob, offset, mem, np, pathp, dryrun);
					}

通过上面的代码分析,可以将 .dtsdtc 编译器生成的 .dtb 数据文件的结构总结如下图:
在这里插入图片描述
其中,.dtb 文件头部用数据结构 struct fdt_header 描述:

struct fdt_header {
	fdt32_t magic; /* .dtb文件魔数: 0xd00dfeed */
	fdt32_t totalsize; /* .dtb文件字节数总大小 */
	fdt32_t off_dt_struct; /* .dts 节点和属性数据区间偏移 */
	fdt32_t off_dt_strings;	/* .dts 节点属性名区间偏移 */
	fdt32_t off_mem_rsvmap;	/* .dts 保留内存定义区间偏移 */
	fdt32_t version;		 /* format version */
	fdt32_t last_comp_version;	 /* last compatible version */

	/* version 2 fields below */
	fdt32_t boot_cpuid_phys;	 /* Which physical CPU id we're
					    booting on */
	/* version 3 fields below */
	fdt32_t size_dt_strings; /* .dts 节点属性名区间字节数大小 */

	/* version 17 fields below */
	fdt32_t size_dt_struct;		 /* .dts 节点和属性数据区间字节数大小 */
};

dts 设备树节点数据以 struct fdt_node_header 描述,展开后以 struct device_node 描述:

struct fdt_node_header {
	fdt32_t tag; /* 节点 tag: FDT_BEGIN_NODE */
	char name[0]; /* 节点名称,以 \0 结尾 */
};
struct device_node {
	const char *name;
	const char *type;
	phandle phandle; /* dts 节点的句柄,经常在节点间相互引用时使用。通常由 dtc 编译器隐式添加 */
	const char *full_name; /* dts 节点全路径名 */
	struct fwnode_handle fwnode;

	struct	property *properties; /* dts 属性节点 */
	struct	property *deadprops;	/* removed properties */
	struct	device_node *parent; /* dts 父节点 */
	struct	device_node *child; /* dts 子节点 */
	struct	device_node *sibling; /* dts 兄弟节点 */
	struct	kobject kobj;
	unsigned long _flags;
	void	*data;
	...
};

dts 设备树节点属性数据以 struct fdt_property 描述,展开后以 struct property 描述:

struct fdt_property {
	fdt32_t tag; /* FDT_PROP */
	fdt32_t len; /* data[] 的长度 */
	fdt32_t nameoff; /* 节点属性名称偏移  */
	/*
	 * 节点属性值, 如有 dts 定义:
	 * /{
	 * 		model = "FriendlyElec NanoPi-M1-Plus";
	 * 		......
	 * };
	 * 则 data[] 的值为 FriendlyElec NanoPi-M1-Plus\0 
	 */
	char data[0];
};
struct property {
	char	*name; /* dts 节点属性名称 */
	int	length; /* dts 节点属性数据长度: 即 @value 长度 */
	void	*value; /* dts 节点属性数据,长度为 @length */
	struct property *next; /* dts 节点的下一属性 */
	unsigned long _flags;
	unsigned int unique_id;
	struct bin_attribute attr;
};

6. 创建设备树节点的设备对象

以章节 5. 设备树的展开 展开的设备树为基础,内核创建设备对象、并绑定到对应的驱动,其具体流程如下:

/* 启动内核初始化线程 */
start_kernel()
	rest_init()
		kernel_thread(kernel_init, NULL, CLONE_FS)

/* 进入内核初始化线程 */
kernel_init()
	kernel_init_freeable()
		...
		of_platform_default_populate_init()
			of_platform_default_populate(NULL, NULL, NULL)
				of_platform_populate(root, of_default_bus_match_table, lookup, parent)
					for_each_child_of_node(root, child) {
						of_platform_bus_create(child, matches, lookup, parent, true)
							/* 创建 platform bus 设备,绑定设备到驱动 */
							dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
							/* 递归创建 platform bus 设备 */
							for_each_child_of_node(bus, child) {
								of_platform_bus_create(child, matches, lookup, &dev->dev, strict)
							}
							of_node_set_flag(bus, OF_POPULATED_BUS);
					}

注意到,这里并没有为所有 DTS 设备树里的节点创建设备对象。像 i2c 总线上的设备,是通过注册 i2c 总线驱动时,触发的 i2c client 设备的创建和相应驱动的绑定动作,其它的如 mmc 总线设备,是通过总线注册或定时扫描完成的从设创建和驱动绑定流程,感兴趣的读者可自行阅读相关内核代码。
接下来,我们来更深入了解下 platform_device 设备对象创建过程中,几个重要方面的细节。

6.1 设备内存空间解析

设备自身通常带有一些 IO 内存空间,来看下它们的解析过程:

/* drivers/of/platform.c */

static int of_platform_bus_create(struct device_node *bus,
			const struct of_device_id *matches,
			const struct of_dev_auxdata *lookup,
			struct device *parent, bool strict)
{
	struct platform_device *dev;

	...

	dev = of_platform_device_create_pdata(bus, bus_id, platform_data, parent);
	
	...
}

static struct platform_device *of_platform_device_create_pdata(
					struct device_node *np,
					const char *bus_id,
					void *platform_data,
					struct device *parent)
{
	struct platform_device *dev;

	/* 创建并初始化设备(包括 IO 内存解析: 解析 "reg" 配置) */
	dev = of_device_alloc(np, bus_id, parent);
	dev->dev.bus = &platform_bus_type;
	dev->dev.platform_data = platform_data;

	/* 添加设备到 driver core */
	if (of_device_add(dev) != 0) {
		...
	}

	return dev;
}

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);

	/* count the io and irq resources */
	while (of_address_to_resource(np, num_reg, &temp_res) == 0) /* 解析设备的 io 配置项数目(即解析 "reg") */
		num_reg++;
	num_irq = of_irq_count(np); /* 解析 设备的 中断配置数目 */

	/* Populate the resource table */
	if (num_irq || num_reg) {
		res = kzalloc(sizeof(*res) * (num_irq + num_reg), GFP_KERNEL);

		dev->num_resources = num_reg + num_irq;
		dev->resource = res;
		for (i = 0; i < num_reg; i++, res++) {
			rc = of_address_to_resource(np, i, res); /* 解析设备 io 内存区间到 @res */
		}
		/* 解析设备所有中断配置到 @res */
		if (of_irq_to_resource_table(np, res, num_irq) != num_irq)
			...
	}

	dev->dev.of_node = of_node_get(np);
	dev->dev.fwnode = &np->fwnode;
	dev->dev.parent = parent ? : &platform_bus;

	...

	return dev;
}

/* drivers/of/address.c */
int of_address_to_resource(struct device_node *dev, int index,
			struct resource *r)
{
	const __be32 *addrp;
	u64  size;
	unsigned int flags;
	const char *name = NULL;

	addrp = of_get_address(dev, index, &size, &flags);

	/* Get optional "reg-names" property to add a name to a resource */
	of_property_read_string_index(dev, "reg-names", index, &name);

	return __of_address_to_resource(dev, addrp, size, flags, name, r);
}

const __be32 *of_get_address(struct device_node *dev, int index, u64 *size,
		unsigned int *flags)
{
	struct of_bus *bus;

	bus = of_match_bus(parent); /* 决定设备的地址总线类型: PCI, ISA, 缺省 */
	bus->count_cells(dev, &na, &ns);
	...

	/* Get "reg" or "assigned-addresses" property */
	prop = of_get_property(dev, bus->addresses, &psize); /* 读取设备的 "reg" 配置 */

	...
}

/* DTS 地址总线类型定义: PCI, ISA, 其它 */
static struct of_bus of_busses[] = {
#ifdef CONFIG_OF_ADDRESS_PCI
	/* PCI */
	{
		.name = "pci",
		.addresses = "assigned-addresses",
		.match = of_bus_pci_match,
		.count_cells = of_bus_pci_count_cells,
		.map = of_bus_pci_map,
		.translate = of_bus_pci_translate,
		.get_flags = of_bus_pci_get_flags,
	},
#endif /* CONFIG_OF_ADDRESS_PCI */
	/* ISA */
	{
		.name = "isa",
		.addresses = "reg",
		.match = of_bus_isa_match,
		.count_cells = of_bus_isa_count_cells,
		.map = of_bus_isa_map,
		.translate = of_bus_isa_translate,
		.get_flags = of_bus_isa_get_flags,
	},
	/* Default */
	{
		.name = "default",
		.addresses = "reg",
		.match = NULL,
		.count_cells = of_bus_default_count_cells,
		.map = of_bus_default_map,
		.translate = of_bus_default_translate,
		.get_flags = of_bus_default_get_flags,
	},
};

/* 决定设备的地址总线类型: PCI, ISA, 缺省 */
static struct of_bus *of_match_bus(struct device_node *np)
{
	int i;

	for (i = 0; i < ARRAY_SIZE(of_busses); i++)
		if (!of_busses[i].match || of_busses[i].match(np))
			return &of_busses[i];
	...
}

6.2 设备树常见属性

6.2.1 status

启用 设备树节点:

status = "okay";
status = "ok";
[不定义 status 属性]

禁用 设备树节点:

status = "disabled";

细节参考函数 of_device_is_available()

6.2.2 其它

(待续)

7. 设备树相关工具

fdtget : 读取设备树的内容
fdtput : 写属性数据到 .dtb 文件
fdtdump: 导出设备树

8. 参考资料

《Power_ePAPR_APPROVED_v1.1.pdf》
https://manpages.debian.org/testing/device-tree-compiler/
  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值