Linux设备树

x33g5p2x  于2022-06-16 转载在 Linux  
字(11.0k)|赞(0)|评价(0)|浏览(519)

文章链接:Linux驱动开发 — 设备树

设备树的目的

平台识别补充

  1. 平台识别

最重要的是,内核使用设备树去识别特定机器(目标板,在内核中称为machine)。最完美的情况是,内核应该与特定的硬件平台无关,然而,因为所有硬件平台的并不都是完美的。所以内核必须在早期初始化阶段识别机器,这样内核才有机会运行与特定机器相关的初始化序列。

在大多数情况下,机器识别是与设备树无关的,内核通过机器的CPU或SOC 来选择初始化代码
以 ARM 平台为例,setup_arch会调用setup_machine_fdt,后者遍历machine_desc链表,选择最匹配设备树数据machine_desc结构
这是通过查找设备树根节点的compatible属性,并把它和machine_desc中的dt_compat列表中的各项进行比较来决定哪一个machine_desc结构是最适合的。
compatible属性包含一个有序的字符串列表,它以确切的机器名开始,紧跟着一个可选的board列表,从最匹配到其他匹配类型。
以Samsung 的 Exynos4x12系列的SoC芯片为例,在 arch/arm/mach-exynos/mach-exynos4-dt.c文件中的dt_compat列表定义如下:

static char const *exynos4_dt_compat[]__initdata={
"samsung, exynos4210",
"samsung,exynos4212",
"samsung,exynos4412",
NULL
};

而在 origen目标板的设备树源文件arch/arm/boot/dts/exynos4412-origen.dts中包含的exynos4412.dtsi文件中指定的compatible属性如下:

compatible= "samsung, exynos4412";

这样在内核启动过程中就可以通过传递的设备树数据找到匹配的机器所对应的machine_desc结构,如果没找到则返回NULL。

采用这种方式,可以使用一个machine_desc支持多个机器,从而降低了代码的重复率。
当然,对初始化有特殊要求的机器的初始化过程应该有所区别,这可以通过其他的属性或一些钩子函数来解决

实时配置补充

在早期的初始化阶段,页表建立之前,与体系结构初始化相关的代码会多次联合使用不同的辅助回调函数去调用of_scan_flat_dt解析设备树数据
of_scan_flat_dt遍历设备树并利用辅助函数来提取需要的信息
通常,early_init_dt _scan_chosen辅助函数用于解析包括内核参数的chosen节点;
early_init_dt_scan_root辅助函数用于初始化设备树的地址空间模型;
early init dt scan memory辅助函数用于决定可用内存的大小和地址

在ARM平台, setup machine _fdt函数负责在选取到正确的machine_desc结构之后进行早期的设备树遍历

设备植入补充

经过板子识别和早期配置数据解析之后,内核进一步进行初始化。

期间,unflatten_device_tree函数被调用,将设备树的数据转换成一种更有效的实时形式

同时,机器特殊的启动钩子函数也会被调用,例如 machine_desc中的init_early函数init_irq函数init_machine函数等。

通过名称我们可以猜想到,init_early函数会在早期初始化时被执行init_irq函数用于初始化中断处理

利用设备树并没有改变这些函数的行为和功能

如果设备树被提供,那么不管是init_early 函数还是init_irq函数都可以,调用任何设备树查找函数去获取额外的平台信息

不过 init_machine函数却需要更多地关注,在/arch/arm/XXXX/xxxx-dt.c文件中,该函数中有如下一句:

of_platform_populate(NULL,of_default_bus_match_table,NULL,NULL);

of_platform_populate函数的作用是遍历设备树中的节点,把匹配的的节点转换成平台设备,然后注册到内核中

linux设备树的使用

设备树实例解析-了解里面的结构

可编址设备使用属性将地址信息编码进设备树:

5 .可编址设备使用属性将地址信息编码进设备树:

reg
#address-cells
#size-cells

每个可编址设备都有一个reg,它是一个元组表,形式为: reg = <地址1 长度1[地址2 长度2][地址3 长度3]...>

每个元组都表示该设备使用的一个地址范围

每个地址值是一个或多个32位整型数列表,称为cell

同样,长度值也可以是一个cell列表或者为空

由于地址和长度字段都是可变大小的变量,那么父节点的#address-cells和#size-cells属性就用来声明各个字段的cell 的数量

换句话说,正确解释一个reg属性要用到父节点的#address-cells和#size-cells的值

如在arch/arm/bootldts/exynos4412-origen.dts文件中I2C设备的相应描述:

i2c@13860000 {
#address-cells=<1>;
#size-cells =<0>;
samsung,i2c-sda-delay =<100>;
samsung,i2e-max-bus-freq =<20000>;
pinctrl-0 =<&i2c0_bus>;
pinctrl-names = "default";
status="okay";

s5m8767_pmic@66{
compatible= "samsung,s5m8767-pmic";
reg =<0x66>;
.....

其中,I2C主机控制器是一个父节点1,地址的长度为一个32位整型数,地址长度为0。

s5m8767_pmic是 I2C主机控制器下面的一个子节点,其地址为0x66。

按照惯例,如果一个节点有reg属性,那么该节点的名字就必须包含设备地址,这个设备地址就是reg属性里第一个地址值

设备地址

关于设备地址还要谈论下面三个方面的内容:
内存映射设备
内存映射的设备应该有地址范围
对于32位的地址可以用1个cell来指定地址值用一个cell 来指定范围
而对于64位的地址就应该用两个cell 来指定地址值

还有一种内存映射设备的地址表示方式,就是基地址、偏移和长度。在这种方式中,地址也是用两个cell来表示

非内存映射设备
有些设备没有被映射到CPU的存储器总线上,虽然这些设备可以有一个地址范围,但它们并不是由CPU直接访问。
取而代之的是,父设备的驱动程序会代表CPU 执行间接访问

这类设备的典型例子就包括上面提到的I2C 设备,NAND Flash也属于这类设备。

范围(地址转换)
根节点的地址空间是从CPU的视角进行描述的,根节点的直接子节点使用的也是这个地址域,如 chipid@ 10000000。
但是非根节点的直接子节点就没有使用这个地址域,于是需要把这个地址进行转换,ranges这个属性就用于此目的。如在arch/arm/boot/dts.hi3620.dtsi文件中有下面一段描述:

sysctrl: system-controller@802000{
compatible= "hisilicon,sysctrl";
#address-cells=<1>;
#size-cells=<1>;
ranges=<0 0×802000 0×1000>;
reg =<0×802000 0x1000>;
smp-offset=<0x31c>;
resume-offset=<0×308>;
reboot-offset=<0x4>;

clock: clocke@0 {
compatible-"hisilicon,hi3620-clock";
reg =<0 0x10000>;
#clock-cells=<1>;
                };
};

“sysctrl: system-controller@802000”这个节点是“clock: clock@0”的父节点,在父节点中定义了一个地址范围,这个地址范围由“<子地址 父地址 子地址空间区域大小>”这样一个元组来描述。

所以“<0 0x802000 0x1000>”表示的是子地址0被映射在父地址的0x802000-0x0x802FFF处。

“clock: clock@0”这个子节点刚好使用了这个地址

有些时候,这种映射也是一对一的,即子节点使用和父节点一样的地址域,这可以通过一个空的ranges属性来实现。如:

amba {
#address-cells =<1>;
#size-cells =<1>:
compatible= "arm, amba-bus";
interrupt-parent=<&gic>;
ranges;

pdma0:pdma@12680000 {
compatible="arm, p1330","arm, primecell";
reg=<0×12680000 0x1000>;
interrupts =<0 35 0>;
clocks=<&clock 292>;
clock-names ="apb_pclk";
#dma-cells=<1>;
#dma-channels=<8>;
#dma-requests=<32>;
};

"pdma0: pdma@12680000”子节点使用的就是和“amba”父节点一样的地址域。

中断连接属性

(6)描述中断连接需要四个属性。
interrupt-controller:一个空的属性,用来定义该节点是一个接收中断的设备,即是一个中断控制器。
interrupt-cells:一个中断控制器节点的属性,声明了该中断控制器的中断指示符中cell 的个数,类似于#address-cells
interrupt-parent:一个设备节点的属性指向设备所连接的中断控制器。如果这个设备节点没有该属性,那么这个节点继承父节点的这个属性。
interrupts:一个设备节点的属性含一个中断指示符的列表,对应于该设备上的每个中断输出信号

gic: interrupt-controller@10490000 {
compatible="arm,cortex-a9-gic";
#interrupt-cells=<3>;
interrupt-controller;
reg=<0x10490000 0×1000>, <0x10480000 0x100>;
};

上面的节点表示一个中断控制器,用于接受中断。中断指示符占3个cell

amba {
#address-cells=<1>;
#size-cells=<1>;
compatible="arm,amba-bus";
interrupt-parent=<&gic>;
ranges;

pdma0: pdma@12680000 {
compatible="arm, p1330","arm, primecell";
reg=<0x12680000 0x1000>;
interrupts=<0 35 O>;
clocks=<&clock 292>;
clock-names="apb_pclk";
#dma-cells=<1>;
#dma-channels=<8>;
#dma-requests=<32>;
};

“amba”节点是一个中断设备,产生的中断连接到“gic”中断控制器

“pdma0:pdma@12680000”是一个“amba”的子节点继承了父节点的 interrupt-parent属性,即该设备产生的中断也连接在“gic”中断控制器上

中断指示符占3个cell“pdma0:pdma@12680000”节点的中断指示符是“<0 35 0>”,其意义是查看内核中的相应文档。

因为GIC是 ARM公司开发的一款中断控制器,查看Documentation/devicetree/bindings/arm/gic.txt内核文档可知,第一个cell是中断类型 :
0是SPI共享的外设中断,即这个中断由外设产生可以连接到一个 SoC中的多个ARM核;
1是PPI私有的外设中断,即这个中断由外设产生,但只能连接到一个SoC中的特定ARM核

第二个cell 是中断号

第三个cell 是中断的触发类型,0表示不关心

aliases 节点 用于指定节点的别名

( 7) aliases节点用于指定节点的别名。

因为引用一个节点要使用全路径,当子节点离根节点较远时,节点名就会显得比较冗长,定义一个别名则比较方便

下面把 spi_0这个节点定义了一个别名“spi0”

aliases{
spi0=&spi_0;
......

chosen 节点 传递数据

( 8) chosen节点并不代表一个真正的设备,只是一个为固件和操作系统传递数据的地方,如引导参数。

chosen节点里的数据也不代表硬件。如在 arch/arm/boot/dts/exynos4412-origen.dts文件中的chosen节点定义如下:

chosen {
bootargs ="console=ttySAC2,115200";
};

设备特定数据 (查看帮助文档binding)

(9)设备特定数据,用于定义特定于某个具体设备的一些属性。

这些属性可以自由定义,但是新的设备特定属性的名字都应该使用制造商前缀,以避免和现有标准属性名相冲突。另外,属性和子节点的含义必须存档在 binding 文档中,以便设备驱动程序的程序员知道如何解释这些数据。

在内核源码的Documentation/devicetree/bindings/目录中包含了大量的binding文档,当发现设备树中的一些属性不能理解时,在该目录下查看相应的文档都能找到答案。

使用设备树的LED驱动 (例子)

使用设备树是内核的一个必然趋势,目前内核中除了较早的目标板在使用平台设备,新的目标板几乎都使用了设备树。

既然如此,我们接下来就把之前的LED驱动改造过来。

首先要做的就是在设备树源文件中添加相应的LED 设备树节点,修改arch/arm/boot/dts/exynos4412-fs4412.dts,加入以下代码:

fsled2@11000C40{
compatible="fs4412,fsled";
reg=<0×11000C40 0x8>;
id =<2>;
pin=<7>;
};

fsled3@11000c20[
compatible="fs4412,fsled";
reg=<0×11000C20 0x8>;
id=<3>;
pin=<0>;
};

fsled4@114001EO{
compatible="fs4412,fsled";
reg=<0×114001E0 0×8>;
id=<4>;
pin=<4>;
};

fsled5@114001EO{
compatible="fs4412,fsled";
reg=<0×114001EO 0×8>;
id=<5>;
pin=<5>;
};

上面设置了4个LED的设备树节点:

compatible都是"fs4412,fsled";
reg属性是各自的I/O内存;
id是自定义属性,表示设备的id号;
pin也是自定义属性,表示使用的是GPIO管脚;

编译和将结果上传到TFTP服务器上目录:

make ARCH=arm dtbs
cp arch/arm/boot/dts/exynos4412-fs4412.dtb /var/lib/tftpboot/

接下来针对驱动修改,使用设备树,代码如下:

/* fsled.c */
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

#include <linux/fs.h>
#include <linux/cdev.h>

#include <linux/slab.h>
#include <linux/ioctl.h>
#include <linux/uaccess.h>

#include <linux/io.h>
#include <linux/ioport.h>
#include <linux/platform_device.h>

#include <linux/of.h>

#include "fsled.h"

#define FSLED_MAJOR	256
#define FSLED_DEV_NAME	"fsled"

struct fsled_dev {
	unsigned int __iomem *con;
	unsigned int __iomem *dat;
	unsigned int pin;
	atomic_t available;
	struct cdev cdev;
};

static int fsled_open(struct inode *inode, struct file *filp)
{
	struct fsled_dev *fsled = container_of(inode->i_cdev, struct fsled_dev, cdev);

	filp->private_data = fsled;
	if (atomic_dec_and_test(&fsled->available))
		return 0;
	else {
		atomic_inc(&fsled->available);
		return -EBUSY;
	}
}

static int fsled_release(struct inode *inode, struct file *filp)
{
	struct fsled_dev *fsled = filp->private_data;

	writel(readl(fsled->dat) & ~(0x1 << fsled->pin), fsled->dat);

	atomic_inc(&fsled->available);
	return 0;
}

static long fsled_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	struct fsled_dev *fsled = filp->private_data;

	if (_IOC_TYPE(cmd) != FSLED_MAGIC)
		return -ENOTTY;

	switch (cmd) {
	case FSLED_ON:
		writel(readl(fsled->dat) | (0x1 << fsled->pin), fsled->dat);
		break;
	case FSLED_OFF:
		writel(readl(fsled->dat) & ~(0x1 << fsled->pin), fsled->dat);
		break;
	default:
		return -ENOTTY;
	}

	return 0;
}

static struct file_operations fsled_ops = {
	.owner = THIS_MODULE,
	.open = fsled_open,
	.release = fsled_release,
	.unlocked_ioctl = fsled_ioctl,
};

static int fsled_probe(struct platform_device *pdev)
{
	int ret;
	dev_t dev;
	struct fsled_dev *fsled;
	struct resource *res;

	ret = of_property_read_u32(pdev->dev.of_node, "id", &pdev->id);
	if (ret)
		goto id_err;

	dev = MKDEV(FSLED_MAJOR, pdev->id);
	ret = register_chrdev_region(dev, 1, FSLED_DEV_NAME);
	if (ret)
		goto reg_err;

	fsled = kzalloc(sizeof(struct fsled_dev), GFP_KERNEL);
	if (!fsled) {
		ret = -ENOMEM;
		goto mem_err;
	}

	cdev_init(&fsled->cdev, &fsled_ops);
	fsled->cdev.owner = THIS_MODULE;
	ret = cdev_add(&fsled->cdev, dev, 1);
	if (ret)
		goto add_err;

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!res) {
		ret = -ENOENT;
		goto res_err;
	}

	fsled->con = ioremap(res->start, resource_size(res));
	if (!fsled->con) {
		ret = -EBUSY;
		goto map_err;
	}
	fsled->dat = fsled->con + 1;

	ret = of_property_read_u32(pdev->dev.of_node, "pin", &fsled->pin);
	if (ret)
		goto pin_err;

	atomic_set(&fsled->available, 1);
	writel((readl(fsled->con) & ~(0xF  << 4 * fsled->pin)) | (0x1  << 4 * fsled->pin), fsled->con);
	writel(readl(fsled->dat) & ~(0x1 << fsled->pin), fsled->dat);
	platform_set_drvdata(pdev, fsled);

	return 0;

pin_err:
	iounmap(fsled->con);
map_err:
res_err:
	cdev_del(&fsled->cdev);
add_err:
	kfree(fsled);
mem_err:
	unregister_chrdev_region(dev, 1);
reg_err:
id_err:
	return ret;
}

static int fsled_remove(struct platform_device *pdev)
{
	dev_t dev;
	struct fsled_dev *fsled = platform_get_drvdata(pdev);

	dev = MKDEV(FSLED_MAJOR, pdev->id);

	iounmap(fsled->con);
	cdev_del(&fsled->cdev);
	kfree(fsled);
	unregister_chrdev_region(dev, 1);

	return 0;
}

static const struct of_device_id fsled_of_matches[] = {
	{ .compatible = "fs4412,fsled", },
	{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, fsled_of_matches);

struct platform_driver pdrv = { 
	.driver = { 
		.name    = "fsled",
		.owner   = THIS_MODULE,
		.of_match_table = of_match_ptr(fsled_of_matches),
	},  
	.probe   = fsled_probe,
	.remove  = fsled_remove,
};

module_platform_driver(pdrv);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A simple character device driver for LEDs on FS4412 board");

代码第163行至第167行

static const struct of_device_id fsled_of_matches[] = {
	{ .compatible = "fs4412,fsled", },
	{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, fsled_of_matches);

添加了一个fsled_of_matches数组,用于和设备树的节点匹配,并且在平台驱动结构中将of_match_table进行了相应的赋值

获得IO内存资源的方法和以前的一样,但是id和 pin是自定义的属性要获取这两个属性的值,使用了of_property_read_u32函数,原型如下。

int of property read u32(const struct device_node *np,const char *propname,u32*out_value);
  • np:设备节点对象地址。
  • propname:属性的名字。
    -out_value:回传的属性值。函数返回0表示成功,非0则失败。
    测试方法和之前类似,只是不用再加载注册平台设备的模块了。设备树和内核及模块是分开编译的,所以,如果硬件发生改变,则只需要修改设备树并重新编译即可,内核和驱动模块都不需要重新编译,这是设备树的一个显著优点。

相关文章