Linux驱动学习笔记之设备树(b站讯为linux驱动教学)

一、设备树的由来以及基本概念

1、设备树的由来

(1)设备树是一种描述硬件资源的数据结构它通过bootloader将硬件资源传给内核。

(2)设备树的由来:在平台总线模型里面,将硬件资源相关的内容放在device.c(与具体开发板相关的、需要改动的),将驱动的部分放在driver.c(共性的、不需要特别改动的)。当我们每换一个平台(开发板),我们都要修改device.c文件(硬件描述的部分),并且需要重新编译,这导致linux会留下许多“垃圾代码”。

(3)引入设备树的好处:设备树实际就是平台总线模型里面的device.c部分,它使得开发板相关的硬件资源信息本身不在linux内核里面,只是在开发板启动的时候通过bootloader传给内核,不用编译到内核了,这使得剔除了linux内核与硬件平台有关的冗余代码。

总结:设备树相当于把原来的device.c换成设备树文本文件,这个文件不需要编译到内核里面,需要用到的时候通过bootloader传递给内核。

2、设备树的基本概念

of开头的函数基本上是和设备树相关的函数

.dts后缀是设备树源码,描述了硬件平台

.dtsi后缀是更为通用的设备树文件,由于一个 SOC(芯片) 可能对应多个 ARM 设备,这些 dts 文件势必包含许多共同的部分,Linux 内核为了简化,把 SOC 公用的部分或者多个设备共同的部分提炼为.dtsi 文件

dtc是指设备树编译器

.dtb后缀由设备树源文件编译得到的文件

二、设备树的基本语法

设备树(Device Tree,DT)语法结构像树一样

1、设备树基本框架

(1)设备树从根节点开始,每个设备都是一个节点

(2)节点和节点之间可以嵌套,形成了父子关系。

(3)设备的属性(就是每一个节点)用key-value的键值对来描述,每个属性用分号结束。

2、设备树的语法

(1)根节点/{};和其子节点

(2)节点名称

格式:<名称>[@<设备地址>],其中名称是不可以随意写成123、abd的,要与具体的设备相关,例如serial@02288000。此外,我们也可以给设备节点起别名,例如uart8: serial@02288000 ,其中uart8 就是这个节点名称的别名,serial@02288000 就是节点名称。

往设备节点里面写入信息:一般我往一个节点里面添加内容的时候,不会直接把添加的内容写到节点里面,而是通过节点的引用来添加,&uart8表示节点的引用

(3)节点的属性

不同的平台,不同的总线,地址位长度可能不同,有 32 位地址,有 64 位地址,为了适应这个,规范规定一个 32 位的长度为一个 cell

#address-cells属性和#size-cells属性:#address-cells"属性用来表示设备节点的地址(起始地址)需要几个 cell 表示,该属性本身是 u32 类型的,所以如果总线地址是32位的,这里#address_cells设置为1(1个字节32位);#size-cells"属性用来表示设备节点的地址空间的长度需要几个 cell 表示。

reg属性:"reg"属性用来表示节点地址资源的,比如常见的就是寄存器的起始地址及大小。要想表示一块连续地址,必须包含起始地址和空间大小两个参数,如果有多块地址,那么就需要多组这样的值表示。对于'reg' 属性,每个元素是一个二元组,包含起始地址和大小。

其地址和大小用几个 u32 表示由父节点的"#address-cells","#size-cells"属性确定。

例如:子节点 dcp: dcp@02280000 的 reg 属性值为<0x02280000 0x4000>,相当于设置了起始地址为 0x02280000,地址长度为 0x40000。这由它的父节点aips3的"#address-cells","#size-cells"属性确定。

compatible属性:用于匹配driver.c,去寻找驱动,类似平台总线模型device.c中platform_device结构体里面的.name

status属性:例如,ok表示硬件正常工作,disable表示当前硬件不可用

三、在设备树中添加自定义节点

1、内核源码中的设备树文件

设备树文件在内核源码路径下的/arch/arm/boot/dts,根据自己实际的开发板的型号选取对应的设备树文件,这里以讯为的topeet_emmc_4_3.dts为例,如图为对应的dts文件

可以根据头文件的包含关系和父子节点关系,找到根节点所在的地方,这里的根节点在imx6ull-14x14-evk.dts里面

2、添加设备树节点

我们在根节点文件imx6ull-14x14-evk.dts的如下图位置添加自定义的节点

(1)如何查看是否添加成功

在开发板上cd /proc/device-tree,ls或者cd /sys/firmware/devicetree/base/

(2)添加自定义的test节点

在根节点下面添加test(别名test1)的设备节点

(3)安装设备树编译dtc工具

sudo apt install device-tree-compiler -y

设置交叉编译器环境变量export PATH=/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin:$PATH ,export CROSS_COMPILE=/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf

在内核源码目录下编译make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs或 make ARCH=arm CROSS_COMPILE=/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- dtbs

这样会编译所有的设备树文件,编译的时候要退到内核原目录下

如果编译单独的设备树文件,只需要

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- ***.dtb

例如make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-imx6ull-14x14-evk.dtb

编译成功后在内核源码目录的arch/arm/boot/dts下面生成对应的dtb文件,例如

(4)烧写到开发板

我们的板子没有屏幕uboot下面默认lcdtype=hdmi,所以我们的设备树操作要找到topeet_emmc_hdmi.dtb这个文件
例如make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- topeet_emmc_hdmi.dtb

然后将这个topeet_emmc_hdmi.dtb烧录到开发板下,在/proc/device-tree查看节点

或者直接利用讯为提供的脚本./creatsh就行了,里面本身就编译了内核和设备树

(5)修改节点的信息

如果需要修改节点信息或者补充节点信息,用引用

&test1{

               compatible = "123";

};

这样会覆盖掉之前的信息

四、设备树中常用的of函数

1、设备树中常用的of函数

设备树描述了设备的硬件信息,我们需要获得硬件信息,这就需要在驱动里面使用of函数来访问设备树的硬件信息

Linux内核使用device_node结构体来描述节点,用property结构体来描述节点的属性

(1)查找节点的常用of函数

of_find_node_by_path(const char * path)函数,成功返回节点struct device_node的指针,失败返回NULL,这里要输入节点的完整路径,从根节点/开始

of_get_parent(struct device_node*node),用于查找节点node的父节点

of_get_next_child(const struct device_node*node,struct device_node* prev)用于查找节点的子节点,node参数为父节点,prev为前一个子节点,表示从哪一个子节点开始迭代查找下一个子节点

(2)获得属性的of函数

of_find_property(设备节点device_node,要读取的属性名称,属性值的字节数)用于获得节点的指定属性

of_property_read_u8 函数

of_property_read_u16 函数

of_property_read_u32 函数

of_property_read_u64 函数

读取只有一个整型值的属性,读取整型数据的属性

of_property_read_u8_array 函数

of_property_read_u16_array 函数

of_property_read_u32_array 函数

of_property_read_u64_array 函数

读取数组类型的属性

of_property_read_string读取字符类型的属性

(3)of_iomap用于内存映射,和驱动里的ioremap功能一样

2、示例

利用of函数对前文添加的test节点进行操作

driver.c的完整代码如下

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
int size;
struct device_node * test_device_node;//定义结构体表示我们的节点
struct property *test_node_property;//定义结构体表示我们的节点属性
u32 out_values[2]={0};
const char *str;

static int hello_init(void){
	int ret;
    printk("hello_world\n");
	/*of函数查找设备节点*/
	test_device_node = of_find_node_by_path("/test");
	if(test_device_node == NULL){
		printk("of_find_node_by_path is error\n");
		return -1;
	}
	printk("test_device_node is %s\n",test_device_node->name);

	
	/*of函数查找设备节点属性*/
	//1、查找compatible属性
	test_node_property = of_find_property(test_device_node,"compatible",&size);
	if(test_node_property == NULL){
		printk("of_find_property is error\n");
		return -1;
	}
	printk("test_node_property name is %s\n",test_node_property->name);
	printk("test_node_property value %s\n",(char *)test_node_property->value);

	//2、查找reg属性
	ret = of_property_read_u32_array(test_device_node,"reg",out_values,2);
	if(ret < 0){
		printk("of_property_read_u32_array is error\n");
		return -1;
	}
	printk("out_values[0] is 0x%08x\n",out_values[0]);
	printk("out_values[1] is 0x%08x\n",out_values[1]);

	//3、获取status属性
	ret = of_property_read_string(test_device_node, "status", &str);
	if (ret < 0)
	{
		printk("of_property_read_string is error \n");
		return -1;
	}
	printk("status is %s \n", str);
	
    return 0;
}
static int hello_exit(void){
    printk("bye_bye\n");
    return 0;
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

Makefile的完整代码如下


obj-m += driver.o  #先写生成的中间文件的名字是什么,-m的意思是把我们的驱动编译成模块
KDIR:=/home/zyd/topeet/driver/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga
PWD?=$(shell pwd)   #获取当前目录的变量
all:
	make -C $(KDIR) M=$(PWD) modules ARCH=arm CROSS_COMPILE=/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- #make会进入内核源码的路径,然后把当前路径下的代码编译成模块
clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.symvers *.order




五、设备树下的平台总线模型

设备树就充当了device.c的部分,我们就要考虑driver.c怎么写

1、driver.c如何与设备树节点进行匹配?

driver.c里面使用of_device_id的结构体

2、probe函数

driver.c与设备树匹配之后会进入probe函数,如何从设备树中拿资源,同样两种直接利用probe的参数指针和利用of函数(见上文)

完整的probe函数如下:

//platform_driver结构体对应的probe函数
int beep_probe(struct platform_device* pdev){
	int ret;
	printk("beep_probe\n");
	/*********************方法一:直接获取节点**************************/
	//printk("node name is %s\n",pdev->dev.of_node->name);

	/*********************方法二:通过函数获取硬件资源**************************/
	/*of函数查找设备节点*/
	test_device_node = of_find_node_by_path("/test");
	if(test_device_node == NULL){
		printk("of_find_node_by_path is error\n");
		return -1;
    }
    printk("test_device_node is %s\n",test_device_node->name);

	/*of函数查找设备节点属性*/
	//1、查找compatible属性
	test_node_property = of_find_property(test_device_node,"compatible",&size);
	if(test_node_property == NULL){
		printk("of_find_property is error\n");
		return -1;
	}
	printk("test_node_property name is %s\n",test_node_property->name);
	printk("test_node_property value %s\n",(char *)test_node_property->value);

	//2、查找reg属性
	ret = of_property_read_u32_array(test_device_node,"reg",out_values,2);
	if(ret < 0){
		printk("of_property_read_u32_array is error\n");
		return -1;
	}
	printk("out_values[0] is 0x%08x\n",out_values[0]);
	printk("out_values[1] is 0x%08x\n",out_values[1]);

	//3、获取status属性
	ret = of_property_read_string(test_device_node, "status", &str);
	if (ret < 0)
	{
		printk("of_property_read_string is error \n");
		return -1;
	}
	printk("status is %s \n", str);

	/*********************注册一个杂项设备**************************/
    ret = misc_register(&beep_dev); 
    if(ret<0) 
    { 
    	printk("misc registe is error\n"); 
    } 
    printk("misc register is succeed\n"); 

	/*********************映射物理地址**************************/
	//vir_gpio_dr = of_iomap(pdev->dev.of_node, 0);
	vir_gpio_dr = of_iomap(test_device_node, 0);
	//第二项用于标识设备树节点中 reg 属性的某一项,reg都是二元组
	if (vir_gpio_dr == NULL)
	{
		printk("of_iomap is error \n");
		return -1;
	}
	
	return 0;
}

3、完整的driver.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/fs.h>              // 包含file_operations
#include <linux/miscdevice.h>      // 包含miscdevice相关结构
#include <linux/uaccess.h>  // 确保包含copy_from_user函数
#include <linux/of_address.h>
#include <linux/io.h>  // 包含 iounmap 函数

int size;
struct device_node * test_device_node;//定义结构体表示我们的节点
struct property *test_node_property;//定义结构体表示我们的节点属性
u32 out_values[2]={0};
const char *str;
unsigned int * vir_gpio_dr;

int beep_open(struct inode* inode,struct file* file){
     printk("beep_open\n");
     return 0;
}
int beep_release(struct inode* inode,struct file* file){
     printk("beep_release\n");
     return 0;
}
ssize_t beep_read(struct file *file, char __user * ubuf, size_t size, loff_t * loff_t){
     printk("beep_read\n ");
     return 0;
}
ssize_t beep_write(struct file *file, const char __user * ubuf, size_t size, loff_t * loff_t){
	 //上层应用传入参数1为打开蜂鸣器,传入参数0为关闭蜂鸣器
	 char kbuf[64]={0};
     if(copy_from_user(kbuf,ubuf,size)!=0)
     {
        printk("copy_from_user error\n ");
        return -1;
     }
	 if(kbuf[0]==1){
		//打开蜂鸣器,第二位数据置为1(1左移1位为10,与虚拟地址或第二位置为1)
		*vir_gpio_dr|=(1<<1);

	 }else if(kbuf[0]==0){
		//关闭蜂鸣器,第二位数据置为0
		*vir_gpio_dr&=~(1<<1);
	 }
     printk("beep_write\n ");
     return 0;
}



struct file_operations misc_fops={
    .owner = THIS_MODULE, //宏,指向当前模块
    .open = beep_open,
    .release = beep_release,
    .read = beep_read,
    .write = beep_write,
   
};
struct miscdevice beep_dev = { 
    .minor = MISC_DYNAMIC_MINOR, //宏,自动分配空闲的次设备号
    .name = "beep", 
    .fops = &misc_fops, 
};


//platform_driver结构体对应的probe函数
int beep_probe(struct platform_device* pdev){
	int ret;
	printk("beep_probe\n");
	/*********************方法一:直接获取节点**************************/
	//printk("node name is %s\n",pdev->dev.of_node->name);

	/*********************方法二:通过函数获取硬件资源**************************/
	/*of函数查找设备节点*/
	test_device_node = of_find_node_by_path("/test");
	if(test_device_node == NULL){
		printk("of_find_node_by_path is error\n");
		return -1;
    }
    printk("test_device_node is %s\n",test_device_node->name);

	/*of函数查找设备节点属性*/
	//1、查找compatible属性
	test_node_property = of_find_property(test_device_node,"compatible",&size);
	if(test_node_property == NULL){
		printk("of_find_property is error\n");
		return -1;
	}
	printk("test_node_property name is %s\n",test_node_property->name);
	printk("test_node_property value %s\n",(char *)test_node_property->value);

	//2、查找reg属性
	ret = of_property_read_u32_array(test_device_node,"reg",out_values,2);
	if(ret < 0){
		printk("of_property_read_u32_array is error\n");
		return -1;
	}
	printk("out_values[0] is 0x%08x\n",out_values[0]);
	printk("out_values[1] is 0x%08x\n",out_values[1]);

	//3、获取status属性
	ret = of_property_read_string(test_device_node, "status", &str);
	if (ret < 0)
	{
		printk("of_property_read_string is error \n");
		return -1;
	}
	printk("status is %s \n", str);

	/*********************注册一个杂项设备**************************/
    ret = misc_register(&beep_dev); 
    if(ret<0) 
    { 
    	printk("misc registe is error\n"); 
    } 
    printk("misc register is succeed\n"); 

	/*********************映射物理地址**************************/
	//vir_gpio_dr = of_iomap(pdev->dev.of_node, 0);
	vir_gpio_dr = of_iomap(test_device_node, 0);
	//第二项用于标识设备树节点中 reg 属性的某一项,reg都是二元组
	if (vir_gpio_dr == NULL)
	{
		printk("of_iomap is error \n");
		return -1;
	}
	
	return 0;
}
//platform_driver结构体对应的remove函数
int beep_remove(struct platform_device* pdev ){
	printk("beep_remove\n");
	return 0;
}
//platform_driver结构体对应的id_table
const struct platform_device_id beep_idtable = {
	.name="test123"
};
const struct of_device_id of_match_table_test[] = {
	{.compatible = "123"},
	{}
};


//描述平台总线driver的结构体platform_driver
struct platform_driver beep_driver ={
	.probe=beep_probe,//当driver和device匹配的时候会执行probe函数
	.remove=beep_remove,//当device和driver任意一个remove时(卸载模块rmmod device或driver,执行remove函数)
	.driver={
		.owner=THIS_MODULE,
		.name="beep_test",//与device的name匹配
		//of_match_table 优先级高于id_table,id_table的优先级高于.name
		//设置 platform 驱动匹配表
		.of_match_table = of_match_table_test,
	},
	.id_table=&beep_idtable,//id_table表保存了很多id的信息
};

static int beep_driver_init(void){ 
    int ret = 0;
	//注册driver.c到内核
	ret = platform_driver_register(&beep_driver);
	if(ret<0){
		printk("platform_driver_register error\n");
	}
	printk("platform_driver_register ok\n");
    return 0;
}
static int beep_driver_exit(void){
	// 释放映射的内存
    if (vir_gpio_dr){
		iounmap(vir_gpio_dr);
	}
	 misc_deregister(&beep_dev); 
	//注销driver.c
	platform_driver_unregister(&beep_driver);
    printk("bye_bye\n");
    return 0;
}
module_init(beep_driver_init);
module_exit(beep_driver_exit);
MODULE_LICENSE("GPL");

4、测试app.c

#include <stdio.h>
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
#include <unistd.h> 
int main(int argc,char const* argv[]){
	int fd;//定义一个句柄 
	char buf[64] = {0}; 
	fd = open("/dev/beep",O_RDWR);//打开设备节点 
	if(fd < 0) 
	{ 
		perror("open error \n"); 
		return fd; 
	} 
	buf[0]=atoi(argv[1]);
	write(fd,buf,sizeof(buf)); 
	close(fd); 
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值