【Linux驱动】设备树引入&内核对设备树的处理

🦋设备树的引入

在上一篇文章中讲了总线驱动模型,如下:
在这里插入图片描述
在总线驱动模型中,使用platform_device结构体来描述不同类型的硬件资源:

  • 在某个开发板的board_XXX.c文件中,定义一个或多个platform_device结构体对象来给驱动程序提供硬件资源;
  • 不同开发板的硬件资源不同,所以多个开发板就会有多个board_XXX.c文件。

因此,如果要更换硬件资源信息,就需要修改board_XXX.c文件,然后重新编译驱动,重新加载驱动。此时,对Linux内核来说,存在以下影响:

  • 操作复杂:每次修改或添加硬件信息后,都需要重新编译和加载驱动。
  • 内核冗余:每个开发板都需要一个board_XXX.c文件,这就导致内核中会充斥的大量的board_XXX.c文件。

针对以上问题,Linux对总线驱动模型进行了改进——在内核中引入设备树
设备树文件是位于linux内核之外的一个文件,用以给内核中的驱动程序指定硬件信息。设备树文件起着与board_XXX.c文件相同的作用,但是设备树文件的优势在于:

不属于内核,不参与内核编译。
个人理解:修改设备树dts文件后,只需要将dts文件编译成dtb文件即可,不需要再编译驱动程序,因为在驱动程序中,不再以头文件的形式包含board_XXX.c文件,将驱动程序和描述硬件信息的设备树文件彻底分离开来。而驱动程序所需要的硬件信息,由内核读取设备树文件后,转化为platform_device结构体,提供给驱动程序。

🦋 设备树的语法

在这里插入图片描述
如上图所示,是设备树的模型,之所以叫“树”,是因为所有的设备节点都挂载在系统总线上,形成一个树状结构。其中,所有的设备节点都是“根节点”的子节点,且子节点还可以继续挂载子节点,可以无限挂载下去,所以该模型有着较强的扩展能力。

如下图所示是一个设备树的示例:
在这里插入图片描述

对应的dts文件如下:
在这里插入图片描述

🐞DTS文件布局

/{
	[property definitions]
	[child nodes]
};

如上所示,‘/’表示根节点,后面紧跟一对花括号,根节点的属性和根节点下挂载的子节点在花括号内定义。

🐞node的格式

node是设备树中的基本单元,被称为“设备节点”,其格式如下:

[label:] node-name[@unit-address] {
	[properties definitions]
	[child nodes]
};

其中,label是标号,可以省略,其作用是为了方便引用nodeunit-address是设备节点的地址,也可省略。如下,是一个串口设备在设备树中的表示:

/ {
	uart0: uart@fe001000 {			//uart0是label, uart是node-name, fe001000是unit-adress。
		compatible="ns16550";
 		reg=<0xfe001000 0x100>;
	};
};

在根节点之外,可以使用以下2种方法来修改uart@fe001000这个node:

  • 使用label引用node
&uart0 {
	status = “disabled”;			//修改status属性
};
  • 使用全路径引用node
&{/uart@fe001000} {
	status = “disabled”;			//修改status属性
};

🐞properties的格式

在设备树中,无论是根节点还是其他设备节点,都有自己的属性,而在描述其属性时,一般都遵循以下格式:

property-name = value;

其中,value有多种取值方式:

  • 元胞数组(arrays of cells)
    什么意思呢?就是用一个数组来表示value,使用尖括号包围起来
    该数组中的每一个值都是一个32位的数据,可以理解为值的单位就是32位,也就是一个cell表示一个32位的数。当value是一个64位的数时,可以使用2cell来表示,示例如下:

    interrupts = <17 0xc>;			//17和0xc都是32位的数,value可以是十进制,也可以是十六进制。
    clock-frequency = <0x00000001 0x00000000>;		//value是64位数时,使用2个cell表示。
    
  • 字符串
    value是一个字符串,使用双引号包围起来。示例如下:

    compatible = "simple-bus";
    
  • 字节序列
    value是一个字符序列,使用中括号包围起来。示例如下:

    local-mac-address = [00 01 02 03 04 05];
    
  • 组合
    value的值可以是上面三种方式的组合,用逗号隔开。示例如下:

    compatible = "ns16550", "ns8250";
    example = <0xf00f0000 19>, "a strange property format";
    

🦋 设备树常用属性

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

  • address-cellsaddress要用多少个32位数来表示;

  • sizess-cellssize要用多少个32位数来表示。

    示例如下:

    /{
    	#address-cells = <1>;			//用一个32位数表示reg的地址,即0x80000000;
    	#size-cells = <1>;				//用一个32位数表示reg的大小, 即0x20000000;
    	memory {
    		reg = <0x80000000 0x20000000>;
    	};
    };
    
  1. compatible
    "compatible"表示“兼容”。如下,对于某个LED,内核中可能有ABC三个驱动都支持它。

    led{
    	compatible = "A", "B", "C";
    };
    

    在内核启动时,就会为这个LED按这样的优先顺序为它找到驱动程序:ABC。此外,在根节点下也有"compatible"属性,用来选择哪一个"machine desc":一个内核可以支持 machine A,也支持 machine B,内核启动后会根据根节点的compatible 属性找到对应的 machine desc 结构体,执行其中的初始化函数。

  2. model
    model属性与compatible属性类似,但是有些差异。compatible属性时一个字符串列表,表示硬件可以兼容ABC等驱动;而model用来准确定义这个硬件是什么。示例如下:

    {
    	compatible = "samsung,smdk2440", "samsung,mini2440";
    	model = "jz2440_v3";
    };
    
  3. status
    dtsi文件中定义了很多设备,但是在板子上某些设备是没有的,这时可以给这些设备添加一个status属性,设置为“distabled”,示例如下:

    &uart1{
    	status = "disabled";
    };
    

    status的常用取值如下:

    value描述
    “okay”设备正常工作
    “disabled”设备不可操作,但是后面可以恢复工作
    “fail”发生了严重错误,需修复
  4. reg
    reg属性的值,是一系列"address size",用多少个32位的数来表示addresssize,由其父节点的#address-cells#size-cells 决定。示例如下:

    / {
    	#address-cells = <1>;
    	#size-cells = <1>;
    	memory {
    		reg = <0x80000000 0x20000000>;
    	};
    };
    

🦋设备树常用的节点

  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 可以支持本设备,即这个板子兼容哪些平台。
    • model:当前是哪一个板子。
  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";
    };
    

🦋编译设备树文件

我们一般情况下不会从零写dts文件,而是修改,修改之后需要将dts文件编译成二进制格式的dtb文件。如何编译呢?有两种方式:

  • 在内核中直接make
    首先,设置ARCHCROSS_COMPILEPATH这三个环境变量,这三个环境变量分别表示什么意思呢?如下:

    • ARCH:选择编译哪一种CPU architecture,也就是编译arch/目录下的哪一个子目录。
    • CROSS_COMPILE:交叉编译器的前缀,也就是选择将代码编译成目标CPU的指令的工具。
    • PATH:用于保存可以搜索的目录路径,如果待运行的程序不在当前目录,操作系统便可以去依次搜索PATH变量中记录的目录。

    这三个环境变量的值如下:

    ARCH=arm
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/book/100ask_imx6ull-	sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin
    CROSS_COMPILE=arm-buildroot-linux-gnueabihf-
    

    然后,在Linux源码根目录下,执行如下命令即可编译dtb文件:

    make dtbs V=1			//V=1选项是为了查看编译过程中的打印信息
    
  • 手动编译
    一般情况下不推荐手动使用DTC工具直接编译设备树文件。在Linux内核目录下scripts/dtc是设备树的编译工具,直接使用它的话,包含其他文件时不能使用“#include”,而必须使用“/include”。
    编译、反编译的示例命令如下,“-I”指定输入格式,“-O”指定输出格式,“-o”指定输出文件:

    ./scripts/dtc/dtc -I dts -O dtb -o tmp.dtb arch/arm/boot/dts/xxx.dts // 编译 dts 为 dtb
    ./scripts/dtc/dtc -I dtb -O dts -o tmp.dts arch/arm/boot/dts/xxx.dtb // 反编译 dtb 为dts
    

🦋内核对设备树的处理

使用设备树的目的还是为了给驱动程序提供硬件资源,那么内核是如何根据设备树文件向驱动程序提供硬件资源呢?
在这里插入图片描述
如上图所示,是从源代码文件dts文件处理设备的过程:

  • dtslinux环境下被编译为dtb文件;
  • ubootdtb文件传给内核;
  • 内核解析dtb文件,把每一个节点都转换为device_node结构体,根节点会被保存在全局变量of_root中,从of_root开始可以访问到任务节点。
  • 对于某些device_node结构体,会被转换为platform_device结构体。

🐞哪些设备会被转换为Platform_device?

  • 根节点下含有compatile属性的子节点
    在这里插入图片描述
    如上所示,在根节点‘/’下有三个子节点mytesti2cspi,这三个子节点都有compatile属性,所以都会转换为platform_device结构体。

    📓 注意,必须是根节点的直系子节点符合该条件时才会转换。

  • 含有特定compatile属性值的节点的子节点

    • compatible = “simple-bus
    • compatible = “simple-mfd
    • compatible = “isa
    • compatible = “arm,amba-bus

    上图的设备树文件中,节点mytest@0可以被转换为platform_device,因为其父节点mytestcompatible属性值为simple-bus

  • 总线I2CSPI节点下的子节点:不转换platform_device
    在上图所示的设备树文件中,i2c节点会被转换为platform_device,表示i2c控制器;而节点at24c02则不会被转换为platform_device,一般是被创建为一个i2c_client,因为其父节点是一个i2c节点。

🐞paltform_device与platform_driver配对

从设备树中转换而来的platform_device会被注册进内核中,以后每当注册一个platform_driver时,它们就会两两进行匹配,如果匹配成功,则会调用platform_driverprobe函数。匹配规则如下:
在这里插入图片描述

  1. 比较platform_device.driver_overrideplatform_driver.driver.name
    可以设置platform_devicedriver_override,强制选择某个platform_driver.

  2. 比较platform_device.dev.of_nodeplatform_driver.driver.of_match_table
    由设备树转换得来的platform_device中,有一个结构体:of_node,其类型如下:
    在这里插入图片描述
    驱动程序中的platform_driver中,有一个数组platform_driver.driver.of_match_table,如下:

    在这里插入图片描述
    此时,使用通过如下方式来判断devicedriver是否配对:
    在这里插入图片描述

    • 首先,如果of_match_table中含有compatible值,就跟devicecompatible属性比较,若一致则成功,否则返回失败。
    • 其次,如果of_match_table中含有type值,就跟devdevice_type属性比较,若一致则成功,否则返回失败。
    • 最后,如果of_match_table中含有name值,就跟devname属性比较,若一致则成功,否则返回失败。
  3. 比较platform_device.nameplatform_driver.id_table[i].name,id_table中可能有多项

  4. 比较platform_device.nameplatform_driver.driver.name
    platform_driver.id_table可能为空,这时可以根据platform_driver.driver.name来寻找同名的platform_device

🐞没有转换为platform_device的节点如何使用?

上面说到,所有的设备树节点都会被转换为device_node结构体,但不是所有的device_node都会转换为platform_device,那么没有转换为platform_device的节点怎么访问它们?
linux中提供了一些访问device_node的接口,主要分为以下3类:

  • 查找节点
/**
 * @brief 根据路径找到节点
 * @param path:节点路径
 * @return device_node
 */
static inline struct device_node *of_find_node_by_path(const char *path);

/**
 * @brief 根据名字找到节点
 * @param from: 表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
 * 		  name: 节点name属性。
 * @return device_node
 */
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);

/**
 * @brief 根据类型找到节点
 * @param from: 表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
 * 		  type: 节点device_type属性。
 * @return device_node
 */
struct device_node *of_find_node_by_type(struct device_node *from, const char *type);

/**
 * @brief 根据compatible找到节点
 * @param from: 表示从哪一个节点开始寻找,传入NULL表示从根节点开始寻找。
 * 		  compat: 用来指定compatible属性的值。
 * 		  type: 节点device_type属性。
 * @return device_node
 */
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compat);

/**
 * @brief 根据phandle找到节点
 * @param phandle : 每一个节点都有一个数字 ID,这些数字 ID 彼此不同。可以使用数字 ID 来找到 device_node。这些数字 ID 就是 phandle。
 * @return device_node
 */
struct device_node *of_find_node_by_phandle(phandle handle);

/**
 * @brief 找到device_node的父节点
 * @param node
 * @return device_node
 */
struct device_node *of_get_parent(const struct device_node *node);

/**
 * @brief 找到device_node的父节点;在调用该函数后,把node节点的引用计数减少了1,这意味着不需要调用of_node_put释放node节点。
 * @param node
 * @return device_node
 */
struct device_node *of_get_next_parent(struct device_node *node);

/**
 * @brief 取出下一个子节点;
 * @param node,表示父节点;
 * 		  pre,表示上一个子节点,设为NULL时表示想找到第一个子节点。
 * @return device_node
 */
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev);

/**
 * @brief 取出下一个可用的子节点,有些节点的status是“disabled”,那就会跳过这些节点。
 * @param node,表示父节点;
 * 		  pre,表示上一个子节点,设为NULL时表示想找到第一个子节点。
 * @return device_node
 */
struct device_node *of_get_next_available_child( const struct device_node *node, struct device_node *prev);

/**
 * @brief 根据名字取出子节点。
 * @param node,表示父节点;
 * 		  name,表示子节点的名字。
 * @return device_node
 */
struct device_node *of_get_child_by_name(const struct device_node *node, const char *name);
  • 查找属性
/**
 * @brief 找到节点的指定属性
 * @param np,表示节点;
 * 		  name,表示要查找的属性的名字;
 *        lenp,用来保存这个属性值的长度。
 * @return 
 */
struct property *of_find_property(const struct device_node *np, const char *name, int *lenp);
  • 获取属性值
/**
 * @brief 根据名字找到节点的属性,并返回它的值。
 * @param np,表示节点;
 * 		  name,表示要查找的属性的名字;
 *        lenp,用来保存这个属性值的长度。
 * @return 
 */
const void *of_get_property(const struct device_node *np, const char *name, int *lenp);

/**
 * @brief 根据名字找到节点的属性,确定它的值有多少个元素。
 * @param np,表示节点;
 * 		  propname,表示要查找的属性的名字;
 *        elem_size,元素的大小。
 * @return 
 */
int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size);

...
  • 15
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值