在新版本的 Linux中, ARM相关的驱动全部采用了设备树 (也有支持老式驱动的,比较少 ),最新出的 CPU其驱动开发也基本都是基于设备树的。因此了解设备树是驱动开发必须的!

设备树简介

设备树的由来:Linux内核中ARM架构下有太多的冗余的垃圾板级信息文件,导致 linus震怒,然后ARM社区从PowerPC中借鉴引入了设备树。目录位于arch/arm/boot/dts

设备树属于DTS文件(device tree source)描述板卡上的信息,比如CPU数量、内存基地址、不同接口上都接了什么设备。如图:
image.png
一个 SOC可以作出很多不同的板子,这些不同的板子肯定是有共同的信息, 将这些共同的信息提取出来作为一个通用的文件,其他的 .dts文件直接引用这个通用文件即可

  • 这个通用文件就是.dtsi文件,类似于 C语言中的头文件。描述SOC级信息(有

几个 CPU、主频是多少、各个外设控制器信息等)

  • 一般.dts描述板级信息 (也就是开发板上有哪些 IIC设备、 SPI设备等 )

    DTC工具

    DTC是将.dts编译为.dtb的工具,DTC工具在内核的scrits/dtc目录下,要编译dts文件可以直接在内核目录下执行make dtbs
    DTC除了可以编译.dts文件以外, 也可以“反汇编”.dtb文件为.dts文件,其指令格式为:./scripts/dtc/dtc -I dtb -O dts -o xxx.dts arch/arm/boot/dts/xxx.dtb

    DTS文件

    dts与dtb

    dts是设备树源码文件,经过DTC工具编译后可以生成dtb二进制文件。
    在Linux内核的arch/arm/boot/dts/Makefile中, 描述了当某种SoC被选中后, 哪些.dtb文件会被编译出来。

    dts语法

    DTS语法非常的人性化,是一种 ASCII文本文件,不管是阅读还是修改都很方便。
    下面以内核中vexpress-v2p-ca9.dts为例,记录dts的基本语法:

    include

    dts文件中可以通过#include引用dts、dtsi和h头文件。不过推荐使用dtsi作为后缀。
    一般 .dtsi文件用于描述 SOC的内部外设信息,比如 CPU架构、主频、外设寄存器地址范围,比如 UART、 IIC等。
    1. //导入头文件,支持dts,dtsi和h头文件
    2. #include "vexpress-v2m.dtsi"

    设备节点

    设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键-值对

在设备树中节点命名格式如下:

  • node-name@unit-address
    • node-name是节点名字,为 ASCII字符串,节点名字应该能够清晰的描述出节点的功能,比如“ uart1”就表示这个节点是 UART1外设。
    • unit-address一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话unit-address可以不要,比如 “cpu@0”、interrupt-controller@00a01000”。
  • label: node-name@unit-address:label是为了方便访问节点,采用格式&label

每个属性都是键值对,常见如下几种类型:

  • 字符串类型:compatible = "arm,cortex-a9";
  • 32bit无符号整数:reg = <0>;
  • 字符串列表:compatible = "arm,vexpress,v2p-ca9", "arm,vexpress";
  1. // 斜杠表示根节点,如果include文件也有根节点则合并为一个
  2. / {
  3. model = "V2P-CA9";
  4. arm,hbi = <0x191>;
  5. arm,vexpress,site = <0xf>;
  6. compatible = "arm,vexpress,v2p-ca9", "arm,vexpress";
  7. interrupt-parent = <&gic>;
  8. #address-cells = <1>;
  9. #size-cells = <1>;
  10. //...
  11. cpus {
  12. #address-cells = <1>;
  13. #size-cells = <0>;
  14. A9_0: cpu@0 {
  15. device_type = "cpu";
  16. compatible = "arm,cortex-a9";
  17. reg = <0>;
  18. next-level-cache = <&L2>;
  19. };
  20. A9_1: cpu@1 {
  21. device_type = "cpu";
  22. compatible = "arm,cortex-a9";
  23. reg = <1>;
  24. next-level-cache = <&L2>;
  25. };
  26. //...
  27. };
  28. //...
  29. }

常见标准属性

  • compatible:用于将设备和驱动绑定,格式为"厂商,驱动名称。格式为字符串列表,可以指定多个驱动,按照先后顺序确定优先级。一般驱动程序有OF匹配表,用于与compatible对比。
  • model:字符串,描述设备模块的信息
  • status:字符串,设备的状态信息,可选如下value:

image.png

  • reg:格式为reg = <address1 length1 address2 length2 address3 length3……>,描述设备的地址空间信息,其中每个address length组个表示一个地址范围
    • address-cells:指定address所占用的字长(32bit)

    • size-cells:指定length所占用的字长(32bit)

  • ranges:地址映射/转换表,ranges由子地址、父地址和地址空间长度组成(也可以为空,表示子地址与父地址空间完全相同,不需要转换):
    • child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells确定字长
    • parent-bus-address:父总线地址空间的物理地址,由父节点的#address-cells确定字长
    • length:子地址空间长度,由父节点#size-cells确定字长
  • device_type:字符串,用于描述设备的FCode,只用于CPU和memory节点
  • chosen:uboot向Linux内核传递数据,除了默认存在的bootargs一般为空。

设备树如何匹配设备

匹配原理

以vexpress开发板为例,我们能看到在vexpress-v2p-ca9.dts设备树源文件中,根节点的compatible定义。而在arch/arm/mach-vexpress/v2m.c中定义了该架构下设备的兼容信息,如果两者相等,则表示内核支持该设备:
image.png

代码流程

1 Linux内核启动流程start_kernel中有一步是解析设备树并判断内核是否支持某个设备。更具体的流程如下:
image.png

Linux解析dtb文件

Linux 内核在启动的时候会解析 DTB 文件,然后在/proc/device-tree 目录下生成相应的设备树节点文件:
image.png

OF函数获取设备树信息

因为设备树最终是被驱动文件所使用的,而驱动文件必须要读取设备树中的属性信息,比如内存信息、 GPIO 信息、中断信息等等。要想在驱动中读取设备树的属性值,那么就必须使用Linux内核提供的众多的OF函数(统一前缀of_)

查找节点

Linux 内核使用device_node结构体来描述一个节点,此结构体定义在文件include/linux/of.h中,OF函数也在该文件定义。
查找节点函数有以下几个:

  1. //from标识开始查找的节点,传入NULL表示从根节点开始
  2. //name为要查找的节点名字
  3. struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
  4. //通过device_type属性查找节点
  5. struct device_node *of_find_node_by_type(struct device_node *from, const char *type);
  6. //通过device_type和compatible两个属性查找节点
  7. struct device_node *of_find_compatible_node(struct device_node *from,
  8. const char *type, const char *compat);
  9. //通过 of_device_id 匹配表来查找指定的节点
  10. //matches是匹配表,可以设置多个需要匹配的id
  11. //match为最终找到的node的id
  12. struct device_node *of_find_matching_node_and_match(
  13. struct device_node *from,
  14. const struct of_device_id *matches,
  15. const struct of_device_id **match);
  16. //根据节点路径查找节点
  17. //path是全路径的节点名,可以使用节点别名
  18. static inline struct device_node *of_find_node_by_path(const char *path)

查找父子节点

Linux 内核提供了几个查找节点对应的父节点或子节点的 OF 函数:

  1. //获取指定节点的父节点
  2. struct device_node *of_get_parent(const struct device_node *node);
  3. //查找指定节点的子节点
  4. //prev是前一个子节点,表示从该子节点开始迭代。NULL表示从第一个子节点开始
  5. struct device_node *of_get_next_child(const struct device_node *node,
  6. struct device_node *prev);

提取属性值

节点的属性信息里面保存了驱动所需要的内容,Linux 内核中使用结构体property表示属性,此结构体同样定义在文件include/linux/of.h中。
相关函数如下:

  1. //获取指定节点的属性
  2. //name为属性名,leno为提取到的属性字节数
  3. struct property *of_find_property(const struct device_node *np,
  4. const char *name,
  5. int *lenp);
  6. //获取属性中元素的数量,比如reg属性值是一个数组,那么使用此函数可以获取到这个数组的大小
  7. int of_property_count_elems_of_size(const struct device_node *np,
  8. const char *propname, int elem_size);
  9. //从属性中获取指定标号的 u32 类型数据值
  10. //index为多个值时,要获取指定的标号的数值
  11. int of_property_read_u32_index(const struct device_node *np,
  12. const char *propname,
  13. u32 index, u32 *out_value);
  14. //如下几个函数,用于获取属性所有值,返回数组,用于多值清空
  15. static inline int of_property_read_u8_array(const struct device_node *np,
  16. const char *propname,
  17. u8 *out_values, size_t sz)
  18. of_property_read_u16_array(...);
  19. of_property_read_u32_array(...);
  20. of_property_read_u64_array(...);
  21. //获取属性的单个值:
  22. static inline int of_property_read_u8(const struct device_node *np,
  23. const char *propname,
  24. u8 *out_value);
  25. of_property_read_u16(...);
  26. of_property_read_u32(...);
  27. of_property_read_u64(...);
  28. //读取字符串值
  29. int of_property_read_string(const struct device_node *np,
  30. const char *propname,
  31. const char **out_string);
  32. //获取address-cells和size-cells值
  33. int of_n_addr_cells(struct device_node *np);
  34. int of_n_size_cells(struct device_node *np);
  35. //将节点的reg寄存器物理地址映射为虚拟地址
  36. //index用于reg包含多个地址范围时,指定要映射哪一段
  37. void __iomem *of_iomap(struct device_node *np, int index);

使用设备树的点灯示例

在udev章节有个使用udev的点灯示例4 内核模块与udev,该例子中使用IO访问函数将外部设备的寄存器地址映射为linux虚拟地址,然后进行修改。

在本章节,使用设备树来向 Linux 内核传递相关的寄存器物理地址, 用OF函数从设备树中获取所需的属性值,然后使用获取到的属性值来初始化相关的 IO

  1. 修改设备树源文件:在根节点下创建子节点,设置外设的各个属性,尤其是寄存器地址。修改后make dtbs生成新的设备树文件

    1. barretled {
    2. #address-cells = <1>;
    3. #size-cells = <1>;
    4. compatible = "atkalpha-led";
    5. status = "okay";
    6. reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */
    7. 0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
    8. 0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
    9. 0X0209C000 0X04 /* GPIO1_DR_BASE */
    10. 0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
    11. };
  2. 驱动程序源代码 ```c

    include

    include

    include

    include

    include

    include

    include

    include

    include

    include

    include

    include

    include

    include

    include

define DTSLED_CNT 1 / 设备号个数 /

define DTSLED_NAME “dtsled” / 名字 /

define LEDOFF 0 / 关灯 /

define LEDON 1 / 开灯 /

/ 映射后的寄存器虚拟地址指针 / static void iomem *IMX6U_CCM_CCGR1; static void iomem SW_MUX_GPIO1_IO03; static void __iomem SW_PAD_GPIO1_IO03; static void iomem *GPIO1_DR; static void iomem *GPIO1_GDIR;

/ dtsled设备结构体 / struct dtsled_dev{ dev_t devid; / 设备号 / struct cdev cdev; / cdev / struct class class; // struct device device; / 设备 / int major; / 主设备号 / int minor; / 次设备号 / struct device_node nd; / 设备节点 */ };

struct dtsled_dev dtsled; / led设备 /

/*

  • @description : LED打开/关闭
  • @param - sta : LEDON(0) 打开LED,LEDOFF(1) 关闭LED
  • @return : 无 */ void led_switch(u8 sta) { u32 val = 0; if(sta == LEDON) {
    1. val = readl(GPIO1_DR);
    2. val &= ~(1 << 3);
    3. writel(val, GPIO1_DR);
    }else if(sta == LEDOFF) {
    1. val = readl(GPIO1_DR);
    2. val|= (1 << 3);
    3. writel(val, GPIO1_DR);
    }
    }

/*

  • @description : 打开设备
  • @param - inode : 传递给驱动的inode
  • @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
  • 一般在open的时候将private_data指向设备结构体。
  • @return : 0 成功;其他 失败 / static int led_open(struct inode inode, struct file filp) { filp->private_data = &dtsled; / 设置私有数据 */ return 0; }

/*

  • @description : 从设备读取数据
  • @param - filp : 要打开的设备文件(文件描述符)
  • @param - buf : 返回给用户空间的数据缓冲区
  • @param - cnt : 要读取的数据长度
  • @param - offt : 相对于文件首地址的偏移
  • @return : 读取的字节数,如果为负值,表示读取失败 / static ssize_t led_read(struct file filp, char __user buf, size_t cnt, loff_t offt) { return 0; }

/*

  • @description : 向设备写数据
  • @param - filp : 设备文件,表示打开的文件描述符
  • @param - buf : 要写给设备写入的数据
  • @param - cnt : 要写入的数据长度
  • @param - offt : 相对于文件首地址的偏移
  • @return : 写入的字节数,如果为负值,表示写入失败 / static ssize_t led_write(struct file filp, const char __user buf, size_t cnt, loff_t offt) { int retvalue; unsigned char databuf[1]; unsigned char ledstat;

    retvalue = copy_from_user(databuf, buf, cnt); if(retvalue < 0) {

    1. printk("kernel write failed!\r\n");
    2. return -EFAULT;

    }

    ledstat = databuf[0]; / 获取状态值 /

    if(ledstat == LEDON) {

    1. led_switch(LEDON); /* 打开LED灯 */

    } else if(ledstat == LEDOFF) {

    1. led_switch(LEDOFF); /* 关闭LED灯 */

    } return 0; }

/*

  • @description : 关闭/释放设备
  • @param - filp : 要关闭的设备文件(文件描述符)
  • @return : 0 成功;其他 失败 / static int led_release(struct inode inode, struct file *filp) { return 0; }

/ 设备操作函数 / static struct file_operations dtsled_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, };

/*

  • @description : 驱动出口函数
  • @param : 无
  • @return : 无 / static int __init led_init(void) { u32 val = 0; int ret; u32 regdata[14]; const char str; struct property *proper;

    / 获取设备树中的属性数据 / / 1、获取设备节点:barretled / dtsled.nd = of_find_node_by_path(“/barretled”); if(dtsled.nd == NULL) {

    1. printk("barretled node nost find!\r\n");
    2. return -EINVAL;

    } else {

    1. printk("barretled node find!\r\n");

    }

    / 2、获取compatible属性内容 / proper = of_find_property(dtsled.nd, “compatible”, NULL); if(proper == NULL) {

    1. printk("compatible property find failed\r\n");

    } else {

    1. printk("compatible = %s\r\n", (char*)proper->value);

    }

    / 3、获取status属性内容 / ret = of_property_read_string(dtsled.nd, “status”, &str); if(ret < 0){

    1. printk("status read failed!\r\n");

    } else {

    1. printk("status = %s\r\n",str);

    }

    / 4、获取reg属性内容 / ret = of_property_read_u32_array(dtsled.nd, “reg”, regdata, 10); if(ret < 0) {

    1. printk("reg property read failed!\r\n");

    } else {

    1. u8 i = 0;
    2. printk("reg data:\r\n");
    3. for(i = 0; i < 10; i++)
    4. printk("%#X ", regdata[i]);
    5. printk("\r\n");

    }

    / 初始化LED / IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0); SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1); SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2); GPIO1_DR = of_iomap(dtsled.nd, 3); GPIO1_GDIR = of_iomap(dtsled.nd, 4);

    / 2、使能GPIO1时钟 / val = readl(IMX6U_CCM_CCGR1); val &= ~(3 << 26); / 清楚以前的设置 / val |= (3 << 26); / 设置新值 / writel(val, IMX6U_CCM_CCGR1);

    /* 3、设置GPIO1_IO03的复用功能,将其复用为

    • GPIO1_IO03,最后设置IO属性。 */ writel(5, SW_MUX_GPIO1_IO03);

      /寄存器SW_PAD_GPIO1_IO03设置IO属性 bit 16:0 HYS关闭 bit [15:14]: 00 默认下拉 bit [13]: 0 kepper功能 bit [12]: 1 pull/keeper使能 bit [11]: 0 关闭开路输出 bit [7:6]: 10 速度100Mhz bit [5:3]: 110 R0/6驱动能力 bit [0]: 0 低转换率 / writel(0x10B0, SW_PAD_GPIO1_IO03);

      / 4、设置GPIO1_IO03为输出功能 / val = readl(GPIO1_GDIR); val &= ~(1 << 3); / 清除以前的设置 / val |= (1 << 3); / 设置为输出 / writel(val, GPIO1_GDIR);

      / 5、默认关闭LED / val = readl(GPIO1_DR); val |= (1 << 3);
      writel(val, GPIO1_DR);

      / 注册字符设备驱动 / / 1、创建设备号 / if (dtsled.major) { / 定义了设备号 / dtsled.devid = MKDEV(dtsled.major, 0); register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME); } else { / 没有定义设备号 / alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME); / 申请设备号 / dtsled.major = MAJOR(dtsled.devid); / 获取分配号的主设备号 / dtsled.minor = MINOR(dtsled.devid); / 获取分配号的次设备号 / } printk(“dtsled major=%d,minor=%d\r\n”,dtsled.major, dtsled.minor);

      / 2、初始化cdev / dtsled.cdev.owner = THIS_MODULE; cdev_init(&dtsled.cdev, &dtsled_fops);

      / 3、添加一个cdev / cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);

      / 4、创建类 / dtsled.class = class_create(THIS_MODULE, DTSLED_NAME); if (IS_ERR(dtsled.class)) { return PTR_ERR(dtsled.class); }

      / 5、创建设备 / dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME); if (IS_ERR(dtsled.device)) { return PTR_ERR(dtsled.device); }

      return 0; }

/*

  • @description : 驱动出口函数
  • @param : 无
  • @return : 无 / static void __exit led_exit(void) { / 取消映射 */ iounmap(IMX6U_CCM_CCGR1); iounmap(SW_MUX_GPIO1_IO03); iounmap(SW_PAD_GPIO1_IO03); iounmap(GPIO1_DR); iounmap(GPIO1_GDIR);

    / 注销字符设备驱动 / cdev_del(&dtsled.cdev);/ 删除cdev / unregister_chrdev_region(dtsled.devid, DTSLED_CNT); / 注销设备号 /

    device_destroy(dtsled.class, dtsled.devid); class_destroy(dtsled.class); }

module_init(led_init); module_exit(led_exit); MODULE_LICENSE(“GPL”); MODULE_AUTHOR(“barretren”); ```