驱动进化之路:设备树的引入及简明的教程(imx6ull)

设备树

设备树只是用来给内核里的驱动程序, 指定硬件的信息。比如 LED 驱动,在内核的驱动程序里去操作寄
存器,但是操作哪一个引脚?这由设备树指定。

设备树的格式

/{         //根节点用什么表示呢?和内核一样 用"/"表示,根节点的表示就是“ /{}; ”
    cpu{    //根节点中的CPU节点表示方法
        //节点里面有属性
        name = val;//name可以随便取,但是val不行
        /*   val有三种取值方法
         *   1."string"  //双引号里面表示字符串,可以随便写
         *   2.  //尖括号里面表示32的数字(可以用十进制表示也可以用十六进制表示),放多少个数值都可以
         *   3. [12 34 45] //中括号表示十六进制
         *   4.还可以组合 <0x123>, "zhaohiap", [34]
         */
    };
    
    memory{
        
    };
    
    iic{
         at24c02{
         };   
    };
}

设备树中的基本单元,被称为节点“node”。节点的格式为

[label:] node-name[@unit-address] {// 标号:节点名(必须有)@节点地址
    [properties definitions] //各种属性
    [child nodes] //子节点
};

label 是标号,可以省略。 label 的作用是为了方便地引用 node,比如

/ {
uart0: uart@fe001000 {
    compatible="ns16550";
    reg=<0xfe001000 0x100>;
    };
};

可以使用下面 2 种方法来修改 uart@fe001000 这个 node:

// 在根节点之外使用 label 引用 node:
&uart0 {
    status = “disabled”;
};
或在根节点之外使用全路径:
&{/uart@fe001000} {
    status = “disabled”;
};

设备树常用的节点

1、根节点

dts文件中必须有一个根节点:
/ {
    model = "SMDK24440";
    compatible = "samsung,smdk2440";
    #address-cells = <1>;
    #size-cells = <1>;
};

根节点中必须有这些属性:

#address-cells // 在它的子节点的 reg 属性中, 使用多少个 u32 整数来描述地址(address)
#size-cells // 在它的子节点的 reg 属性中, 使用多少个 u32 整数来描述大小(size)
compatible // 定义一系列的字符串, 用来指定内核中哪个 machine_desc 可以支持本设备
// 即这个板子兼容哪些平台
// uImage : smdk2410 smdk2440 mini2440 ==> machine_desc
model // 咱这个板子是什么
// 比如有 2 款板子配置基本一致, 它们的 compatible 是一样的

2、CPU节点
一般不需要我们设置,在 dtsi 文件中都定义好了:

cpus {
    #address-cells = <1>;
    #size-cells = <0>;
    cpu0: cpu@0 {
        .......
    }
};

3、memory 节点
芯片厂家不可能事先确定你的板子使用多大的内存,所以 memory 节点需要板厂设置,比如:

memory {
    reg = <0x80000000 0x20000000>;
};

4、chosen 节点(虚拟节点来的)
我们可以通过设备树文件给内核传入一些参数,这要在 chosen 节点中设置 bootargs 属性:

chosen {
    bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200";
};

常用的属性

1. #address-cells、 #size-cells
cell 指一个 32 位的数值,

  • address-cells: address 要用多少个 32 位数来表示;
  • size-cells: size 要用多少个 32 位数来表示。

例子:

/ {
    #address-cells = <1>;//表示用一个32位的数据表示地址
    #size-cells = <1>;//表示用一个32位的数据表示大小
    memory {
        reg = <0x80000000 0x20000000>;//0x80000000 来表示地址,0x20000000 表示大小
    };
};

2. compatible
“compatible” 表示“兼容”,对于某个 LED,内核中可能有 A、 B、 C 三个驱动都支持它,那可以这样写:

led {
    compatible = “A”, “B”, “C”;//它的值是一个字符串链表
};
  • 内核启动时,就会为这个 LED 按这样的优先顺序为它找到驱动程序: A、 B、
    C。一般引用“compatible”都是为了寻找驱动使用的。
  • 根节点下也有 compatible 属性,用来选择哪一个“ machine desc” :一个内核可以支持 machine A,也支持
    machine B,内核启动后会根据根节点的 compatible 属性找到对应的 machine desc
    结构体,执行其中的初始化函数。
  • compatible 的值,建议取这样的形式: “manufacturer,model”,即“厂家名,模块名”。
    注意: machine desc 的意思就是“机器描述”,学到内核启动流程时才涉及。

3. model
model 属性与 compatible 属性有些类似,但是有差别。
compatible 属性是一个字符串列表,表示可以你的硬件兼容 A、 B、 C 等驱动;
model 用来准确地定义这个硬件是什么。
比如根节点中可以这样写:

/ {
compatible = "samsung,smdk2440", "samsung,mini2440";
model = "jz2440_v3";
};

它表示这个单板,可以兼容内核中的“ smdk2440”,也兼容“ mini2440”。
从 compatible 属性中可以知道它兼容哪些板,但是它到底是什么板?用 model 属性来明确。

4. status
dtsi 文件中定义了很多设备,但是在你的板子上某些设备是没有的。这时你可以给这个设备节点添加一个 status 属性,设置为“ disabled”:

&uart1 {
    status = "disabled";
};

5. reg
reg 的本意是 register,用来表示寄存器地址。
但是在设备树里,它可以用来描述一段空间。反正对于 ARM 系统,寄存器和内存是统一编址的,即访寄存器时用某块地址,访问内存时用某块地址,在访问方法上没有区别。
reg 属性的值,是一系列的“ address size”,用多少个 32 位的数来表示 address 和 size,由其父节点的#address-cells、 #size-cells 决定。
示例:

/dts-v1/;
/ {
    #address-cells = <1>;
    #size-cells = <1>;
    memory {
        reg = <0x80000000 0x20000000>;//一段一段的表示空间,也可以用来表示CPU
    };
};

内核对设备树的处理

从源代码文件 dts 文件开始,设备树的处理过程为:DTS----->DTB---->device_node---->platform_device
① dts 在 PC 机上被编译为 dtb 文件;
② u-boot 把 dtb 文件传给内核;
③ 内核解析 dtb 文件,把每一个节点都转换为 device_node 结构体;
④ 对于某些 device_node 结构体,会被转换为 platform_device 结构体。

dtb 中每一个节点都被转换为 device_node 结构体

struct device_node {
	const char *name;
	const char *type;
	phandle phandle;
	const char *full_name;
	struct fwnode_handle fwnode;

	struct	property *properties;//properties是节点的属性,节点的属性里面有哪些内容呢?有名字,有值等等内容。
	struct	property *deadprops;	/* removed properties */
	struct	device_node *parent;
	struct	device_node *child;
	struct	device_node *sibling;
	struct	kobject kobj;
	unsigned long _flags;
	void	*data;
#if defined(CONFIG_SPARC)
	const char *path_component_name;
	unsigned int unique_id;
	struct of_irq_controller *irq_trans;
#endif
}

struct property {
	char	*name;
	int	length;
	void	*value;
	struct property *next;
	unsigned long _flags;
	unsigned int unique_id;
	struct bin_attribute attr;
};

注意:并不是所以的device_node都会转换成platform_device
那么哪些设备树的节点会被转换成platform_device呢?

1、根节点下含有 compatile 属性的子节点

i2c {
	compatile = "samsung,i2c";
	at24c02 {
		compatile = "at24c02";
	};
};

/i2c 节点一般表示 i2c 控制器, 它会被转换为 platform_device, 在内核中有对应的 platform_driver;
/i2c/at24c02 节点不会被转换为 platform_device, 它被如何处理完全由父节点的 platform_driver决定, 一般是被创建为一个 i2c_client

2、含有特定 compatile 属性的节点的子节点
如 果 一 个 节 点 的 compatile 属 性 , 它 的 值 是 这 4 者 之 一 : “simple-bus”,“simplemfd”,“isa”,“arm,amba-bus”,那么它的子结点(需含 compatile 属性)也可以转换为 platform_device。

mytest {
	compatile = "mytest", "simple-bus";
	mytest@0 {
		compatile = "mytest_0";
	};
};

/mytest 会被转换为 platform_device, 因为它兼容"simple-bus";它的子节点/mytest/mytest@0 也会被转换为 platform_device。

3、 总线 I2C、 SPI 节点下的子节点: 不转换为 platform_device
某个总线下到子节点, 应该交给对应的总线驱动程序来处理, 它们不应该被转换为 platform_device。

spi {
	compatile = "samsung,spi";
	flash@0 {
		compatible = "winbond,w25q32dw";
		spi-max-frequency = <25000000>;
		reg = <0>;
	};
};

/spi 节点, 它一般也是用来表示 SPI 控制器, 它会被转换为 platform_device, 在内核中有对应的 platform_driver;
/spi/flash@0 节点不会被转换为 platform_device, 它被如何处理完全由父节点的 platform_driver决定, 一般是被创建为一个 spi_device。

platform_device 与 platform_driver 配对

从设备树转换得来的 platform_device 会被注册进内核里, 以后当我们每注册一个 platform_driver
时,它们就会两两确定能否配对,如果能配对成功就调用 platform_driver 的 probe 函数。

static int platform_match(struct device *dev, struct device_driver *drv)
{
	struct platform_device *pdev = to_platform_device(dev);
	struct platform_driver *pdrv = to_platform_driver(drv);
 
	/* When driver_override is set, only bind to the matching driver */
	if (pdev->driver_override)
		return !strcmp(pdev->driver_override, drv->name);
 
	/* Attempt an OF style match first */
	if (of_driver_match_device(dev, drv))
		return 1;
 
	/* Then try ACPI style match */
	if (acpi_driver_match_device(dev, drv))
		return 1;
 
	/* Then try to match against the id table */
	if (pdrv->id_table)
		return platform_match_id(pdrv->id_table, pdev) != NULL;
 
	/* fall-back to driver name match */
	return (strcmp(pdev->name, drv->name) == 0);
}

struct platform_device {
	const char	*name;
	int		id;
	bool		id_auto;
	struct device	dev;
	u32		num_resources;
	struct resource	*resource;

	const struct platform_device_id	*id_entry;
	char *driver_override; /* Driver name to force a match */

	/* MFD cell pointer */
	struct mfd_cell *mfd_cell;

	/* arch specific additions */
	struct pdev_archdata	archdata;
};

struct platform_driver {
	int (*probe)(struct platform_device *);
	int (*remove)(struct platform_device *);
	void (*shutdown)(struct platform_device *);
	int (*suspend)(struct platform_device *, pm_message_t state);
	int (*resume)(struct platform_device *);
	struct device_driver driver;
	const struct platform_device_id *id_table;
	bool prevent_deferred_probe;
};

1、最先比较:是否强制选择某个 driver
比较 结构体platform_device里的. driver_override成员 和 结构体platform_driver里的成员.driver结构体里的.name成员
可以设置 platform_device 的 driver_override,强制选择某个 platform_driver。

struct device_driver {
	const char		*name;
	struct bus_type		*bus;

	struct module		*owner;
	const char		*mod_name;	/* used for built-in modules */

	bool suppress_bind_attrs;	/* disables bind/unbind via sysfs */
	enum probe_type probe_type;

	const struct of_device_id	*of_match_table;
	const struct acpi_device_id	*acpi_match_table;

	int (*probe) (struct device *dev);
	int (*remove) (struct device *dev);
	void (*shutdown) (struct device *dev);
	int (*suspend) (struct device *dev, pm_message_t state);
	int (*resume) (struct device *dev);
	const struct attribute_group **groups;

	const struct dev_pm_ops *pm;

	struct driver_private *p;
};

2、然后比较:设备树信息
比较: platform_device. dev.of_node 和 platform_driver.driver.of_match_table。
由设备树节点转换得来的 platform_device 中,含有一个结构体: of_node。
它的类型如下:

struct device_node {
	const char *name;//来自节点的name属性
	const char *type;//来自节点的device_type属性
	phandle phandle;
	const char *full_name;
	struct fwnode_handle fwnode;

	struct	property *properties;//含有compatible属性
}

如果一个 platform_driver 支持设备树,它的 platform_driver.driver.of_match_table 是一个数组,
类型如下:

/*
 * Struct used for matching a device
 */
struct of_device_id {
	char	name[32];
	char	type[32];
	char	compatible[128];
	const void *data;
};

使用设备树信息来判断 dev 和 drv 是否配对

首先,如果 of_match_table 中含有 compatible 值,就跟 dev 的 compatile 属性比较,若一致则成功,否则返回失败;
也就是compatible和含有compatible属性的properties比较
驱动进化之路:设备树的引入及简明的教程(imx6ull)_第1张图片
其次,如果 of_match_table 中含有 type 值,就跟 dev 的 device_type 属性比较,若一致则成功,否则返回失败;

最后,如果 of_match_table 中含有 name 值,就跟 dev 的 name 属性比较,若一致则成功,否则返回失败。

而设备树中建议不再使用 devcie_type 和 name 属性,所以基本上只使用设备节点的 compatible 属性来寻找匹配的 platform_driver。

假设节点转换成的platform_device与platform_driver匹配成功了,那platform_driver怎么获得platform_device里面的资源呢?

拿个例子
在这个例子当中/mytest /mytest@0会转换成platform_device

mytest {
    compatile = "mytest", "simple-bus";
    mytest@0 {
        compatile = "mytest_0";
        如果这个节点中有reg属性,这个reg属性就会转换成MEM资源
        如果有interrupt属性,就会转换成IRQ(中断)资源
        就可以使用platform_get_resource获得这里的资源
        对于其他属性呢?
        例如:
        pin = xxx这一个属性并不是标准属性,我们要用什么来获取呢?
        可以使用内核提供的函数。
        在根节点中,有一个device_node结构体,这个结构体保存在全局变量of_root里面。
        我们就可以去访问这个全局变量根节点,就能访问各个节点了
        具体怎么找到节点,怎么获取属性,怎么获取值查看开发手册      
    };
};
  • 对于设备树节点中的 reg 属性,它对应 IORESOURCE_MEM 类型的资源;
  • 对于设备树节点中的 interrupts 属性,它对应IORESOURCE_IRQ 类型的资源。

(imx6ull)LED模板驱动程序的改造:设备树

小提示:内核源码我使用的是百问网的

1 设备树节点要与 platform_driver 能匹配

在我们的工作中,驱动要求设备树节点提供什么,我们就得按这要求去编写设备树。但是,匹配过程所要求的东西是固定的:
① 设备树要有 compatible 属性,它的值是一个字符串
② platform_driver 中要有 of_match_table,其中一项的.compatible 成员设置为一个字符串
③ 上述 2 个字符串要一致
驱动进化之路:设备树的引入及简明的教程(imx6ull)_第2张图片
驱动进化之路:设备树的引入及简明的教程(imx6ull)_第3张图片
2、设备树节点指定资源, platform_driver 获得资源
如果在设备树节点里使用 reg 属性,那么内核生成对应的 platform_device 时会用 reg 属性来设置IORESOURCE_MEM 类型的资源。

如果在设备树节点里使用 interrupts 属性,那么内核生成对应的platform_device 时会用 reg 属性来设置 IORESOURCE_IRQ 类型的资源。对于 interrupts 属性,内核会检查它的有效性,所以不建议在设备树里使用该属性来表示其他资源

在我们的工作中,驱动要求设备树节点提供什么,我们就得按这要求去编写设备树。驱动程序中根据pin 属性来确定引脚,那么我们就在设备树节点中添加 pin 属性。设备树节点中:

#define GROUP_PIN(g,p) ((g<<16) | (p))

 /{
 	100ask_led@0 {
        compatible = "imx6ull,leddrv";
        pin = <GROUP_PIN(3, 1)>;
    };
    100ask_led@1 {
        compatible = "imx6ull,leddrv";
        pin = <GROUP_PIN(5, 3)>;
    };
  };

驱动程序中,在platform_device 中添加了属性,就能在platform_driver获得属性,可以从 platform_device 中得到 device_node,再用 of_property_read_u32 得到属性的值:

struct device_node* np = pdev->dev. of_node;
int led_pin;
int err = of_property_read_u32(np, “pin”, &led_pin);

想让内核支持设备树的话得让platform_driver结构体中加一个成员变量of_match_table
需要从platform_device结构体里面找到设备节点取出里面的pin属性

demo

/* 创建设备 */
static int chip_demo_gpio_probe(struct platform_device *pdev)
{
	struct device_node *np;
	int err = 0;
	int led_pin;

	np = pdev->dev.of_node;
	if(!np)
		return -1;

	err = of_property_read_u32(np, "pin", &led_pin);
	//当我们装载驱动程序时,对于 led 设备节点都会调用probe函数。会调用两次probe函数。   

	/* 记录设备资源中的引脚信息 */
	g_ledpins[g_ledcnt] = led_pin;

	/* device_create 
	 * 由于device_create函数中led_class变量在上层文件中,不能够直接调用device_create
	 * 可以将device_create封装进函数中
	 */
	led_class_create_device(g_ledcnt);
	g_ledcnt++;
	
	return 0;
}

/* 销毁设备 */
static int chip_demo_gpio_remove(struct platform_device *pdev)
{
	struct device_node *np;
	int err = 0;
	int led_pin;
	int i;

	np = pdev->dev.of_node;
	if(!np)
		return -1;
	
	err = of_property_read_u32(np, "pin", &led_pin);

	for(i=0; i<g_ledcnt; i++){
		if(g_ledpins[i] == led_pin){
			led_class_destroy_device(i);
			g_ledpins[i] = -1;
		}
	}

	for(i=0; i<g_ledcnt; i++){
		if(g_ledpins[i] != -1)
			break;
	}

	if(i == g_ledcnt)
		g_ledcnt = 0;
	
	return 0;
}

static struct led_operations board_demo_led_opr = {
	.init = board_demo_led_init,
	.ctl = board_demo_led_ctl
};

struct led_operations *get_board_led_opr(void)
{
	return &board_demo_led_opr;
}

static const struct of_device_id imx6ull_leds[] = {
    { .compatible = "imx6ull,leddrv" },
    { },
};

static struct platform_driver chip_demo_gpio_driver = {
	/* 当设备平台与驱动平台匹配成功之后,会调用probe函数 */
	.probe = chip_demo_gpio_probe,
	/* 当移除设备的时候会调用remove函数,与probe函数对应 */
	.remove = chip_demo_gpio_remove,
	.driver = {
		.name = "imx6ull_led",/* 需要与platform_device .name一致,这样才能够匹配 */
		.of_match_table = imx6ull_leds,
	},
};

编译设备树

对百问网 imx6ull 全功能板
设备树文件是:内核源码目录中 arch/arm/boot/dts/100ask_imx6ull-14x14.dts
修改、编译后得到 arch/arm/boot/dts/100ask_imx6ull-14x14.dtb 文件。

vi 100ask_imx6ull-14x14.dts

驱动进化之路:设备树的引入及简明的教程(imx6ull)_第4张图片
修改后保存退出,进入到/home/book/100ask_imx6ull-sdk/Linux-4.9.88目录下编译

make dtbs

就会得到一个新的设备树文件
驱动进化之路:设备树的引入及简明的教程(imx6ull)_第5张图片
将这个设备树文件拷贝到板子里去

cp arch/arm/boot/dts/100ask_imx6ull-14x14.dtb ~/nfs_rootfs/

登录板子,进入到包子的/boot目录,就能查看到设备树文件,先我们要备份一份板子自身的设备树文件。

cp 100ask_imx6ull-14x14.dtb 100ask_imx6ull-14x14_bak.dtb 

在这里插入图片描述
随后将自己写的设备树文件拷贝进来

cp /mnt/100ask_imx6ull-14x14.dtb .

然后重启(reboot),查看自己写的设备树文件是否写进来了。

进入目录

cd /sys/firmware/devicetree/base

可以看到我们写的设备树节点
驱动进化之路:设备树的引入及简明的教程(imx6ull)_第6张图片
驱动进化之路:设备树的引入及简明的教程(imx6ull)_第7张图片

你可能感兴趣的:(imx6ull)