【Linux】【设备树】1-Linux设备树的引入与体验
1 字符设备驱动程序的三种写法
1.1 驱动开发流程
我们以下图所示LED控制电路为例,说明驱动的开发流程。芯片引脚输出低电平时,LED灯点亮。芯片引脚输出高电平时,LED灯熄灭。
驱动的开发流程主要分为三部分:看原理图、写驱动程序和写测试程序。
1.1.1 看原理图
开发驱动程序之前,首先要查看原理图,确定硬件控制方式。譬如使用哪些GPIO等。
其次,需要查阅芯片手册,确定如何使用这些GPIO。譬如查看寄存器等。
1.1.2 写驱动程序
驱动程序起封装作用,它屏蔽了复杂的硬件操作,为应用层提供统一接口,让应用程序访问硬件变得简单。
在Linux的应用开发者看来,操作硬件都是统一的接口。比如操作LED灯,需要先open,如果要读取LED状态就调用read,如果要操作LED就调用write函数,也可以通过ioctl去实现。
在驱动里,针对上述应用的每个调用函数,都写一个对应的函数,实现对硬件的操作。
1.1.3 写测试程序
驱动程序开发完成后,要编写用于测试的sample程序,以验证驱动程序的功能。
1.2 驱动程序的组成部分
写驱动程序,分为五个部分:分配file_operations结构体、设置file_operations结构体、注册、入口函数和出口函数。接下来逐一介绍。
1.2.1 分配file_operations结构体
结构体file_operations在头文件linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数的地址。
struct file_operations {
struct module *owner; // 拥有该结构的模块的指针,一般为THIS_MODULES
loff_t (*llseek) (struct file *, loff_t, int); // 用来修改文件当前的读写位置
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); // 从设备中同步读取数据
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // 向设备发送数据
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); // 初始化一个异步的读取操作
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); // 初始化一个异步的写入操作
int (*readdir) (struct file *, void *, filldir_t); // 仅用于读取目录,对于设备文件,该字段为NULL
unsigned int (*poll) (struct file *, struct poll_table_struct *); // 轮询函数,判断目前是否可以进行非阻塞的读写或写入
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); // 执行设备I/O控制命令
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); // 不使用BLK文件系统,将使用此种函数指针代替ioctl
long (*compat_ioctl) (struct file *, unsigned int, unsigned long); // 在64位系统上,32位的ioctl调用将使用此函数指针代替
int (*mmap) (struct file *, struct vm_area_struct *); // 用于请求将设备内存映射到进程地址空间
int (*open) (struct inode *, struct file *); // 打开
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *); // 关闭
int (*fsync) (struct file *, struct dentry *, int datasync); // 刷新待处理的数据
int (*aio_fsync) (struct kiocb *, int datasync); // 异步刷新待处理的数据
int (*fasync) (int, struct file *, int); // 通知设备FASYNC标志发生变化
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};
1.2.2 设置file_operations结构体
驱动内核模块不需要实现file_operations结构体中的所有函数,未用到的项会被初始化为NULL。
譬如上述LED控制电路,只需要打开open和write功能。因此只要对file_operations中的这两个成员赋值:
.open = led_open;
.write = led_write;
led_open函数中,需要将控制LED的GPIO设置为输出模式。
led_write函数中,需要根据APP传入的值将GPIO拉高或拉低。
1.2.3 注册
我们编写的驱动程序需要调用register_chrdev函数注册到内核才能使用。
/*
* major :动态申请字符设备的主设备号
* name :申请设备的设备名
* fops :申请设备的操作函数,通过此结构体包含的函数完成对设备的访问及控制操作
*/
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
关于register_chrdev函数的更多信息,参见《Linux内核API register_chrdev》。
我们可以这样简单理解注册流程——内核中有一个数组,这个数组中存放着驱动程序。register_chrdev中的第一个参数major就用于指定,将驱动放到数组的哪个位置上。如果将major传入0,则会自动从头向后遍历,放到查询到的空位中。
1.2.4 入口函数
入口函数调用register_chrdev,向内核注册驱动。
1.2.5 出口函数
出口函数调用unregister_chrdev,卸载驱动。
1.3 字符设备驱动程序的三种写法
在上一小节中,我们粗略介绍了编写驱动程序的几个部分。其中led_open、led_write这两个函数就是驱动代码的具体实现。本节我们就来看看,LED的驱动代码具体是如何实现的。
1.3.1 字符设备驱动程序实现方法简介
在驱动中指定LED GPIO有三种方法:
- 传统方法
- led_drv.c
- 分配file_operations
- 初始化file_operations
- 注册
- 入口函数
- 出口函数
- 将GPIO写为字面值常量。
- led_drv.c
- 总线设备驱动模型
- led_drv.c
- 分配file_operations
- 初始化file_operations
- 注册
- 入口函数
- 出口函数
- led_dev.c
- 指定GPIO
- led_drv.c
- 设备树
- led_drv.c
- 分配file_operations
- 初始化file_operations
- 注册
- 入口函数
- 出口函数
- jz2440.dts
- 指定GPIO
- led_drv.c
蓝色部分内容相同。显而易见,不论采用哪种方法,驱动代码的核心都是一样的。区别在于指定硬件资源的方式。
1.3.2 字符设备驱动程序实现方法对比
假设某公司用同一芯片开发两款产品。TV产品使用Pin1作为LED控制引脚,Camera使用Pin2作为LED控制引脚。以此为背景,对比传统方法、总线设备驱动模型、设备树这三种方法的优缺点。
2 字符设备驱动的传统写法
本节使用传统方法编写字符驱动程序,控制LED。
2.1 分配、初始化file_operations结构体
分配file_operations、设置file_operations这两步通常合并执行,代码如下:
static struct file_operations myled_oprs = {
.owner = THIS_MODULE, //表示这个模块本身
.open = led_open,
.write = led_write,
.release = led_release,
};
2.2 注册
定义、设置file_operations结构体之后,就要调用register_chrdev函数注册驱动。前文提到过,register_chrdev是在入口函数中调用的。因此我们直接编写入口函数:
static int myled_init(void)
{
/*
* major :传入0,由系统分配
* name :没有特殊要求
* fops :传入上一步的file_operations结构体
*/
major = register_chrdev(0, "myled", &myled_oprs);
return 0;
}
出口函数需要卸载驱动,代码如下:
static void myled_exit(void)
{
unregister_chrdev(major, "myled");
}
2.3 指定入口函数
使用宏module_init指定入口函数,代码如下:
module_init(myled_init);
module_init(myled_init)实际就是int init_module(void) attribute((alias(“myled_init”))),表示myled_init的别名是init_module,以后就可以使用init_module来引用myled_init。也就是说,入口函数的名字被恒定设置为init_module。
2.4 指定出口函数
使用宏module_init指定入口函数,代码如下:
module_exit(myled_exit);
2.5 GPL协议
此外,还要加上GPL协议:
MODULE_LICENSE("GPL");
2.6 具体的硬件操作函数
经过以上几个小节,驱动程序的框架已经搭建起来了。接下来实现具体的硬件操作函数:led_open()和led_write()。
在led_open()里把对应的引脚配置为输出引脚,在led_write()根据应用程序传入的数据点灯,让其输出高电平或低电平。
为了让程序更具有扩展性,把GPIO的寄存器放在一个数组里:
static unsigned int gpio_base[] = {
0x56000000, /* GPACON */
0x56000010, /* GPBCON */
0x56000020, /* GPCCON */
0x56000030, /* GPDCON */
0x56000040, /* GPECON */
0x56000050, /* GPFCON */
0x56000060, /* GPGCON */
0x56000070, /* GPHCON */
0, /* GPICON */
0x560000D0, /* GPJCON */
};
定义好了引脚的组,还得确定使用该组的哪个引脚,使用宏来确定哪个引脚:
#define S3C2440_GPA(n) (0<<16 | n)
#define S3C2440_GPB(n) (1<<16 | n)
#define S3C2440_GPC(n) (2<<16 | n)
#define S3C2440_GPD(n) (3<<16 | n)
#define S3C2440_GPE(n) (4<<16 | n)
#define S3C2440_GPF(n) (5<<16 | n)
#define S3C2440_GPG(n) (6<<16 | n)
#define S3C2440_GPH(n) (7<<16 | n)
#define S3C2440_GPI(n) (8<<16 | n)
#define S3C2440_GPJ(n) (9<<16 | n)
后面就可以向对应宏传入对应位,得到对应组的对应引脚。查看原理图,知道我们要使用的引脚是GPF5,因此定义:
static int led_pin = s3c2440_GPF(5);
static int led_open (struct inode *node, struct file *filp)
{
/* 把LED引脚配置为输出引脚 */
/* GPF5 - 0x56000050 */
int bank = led_pin >> 16;
int base = gpio_base[bank];
int pin = led_pin & 0xffff;
gpio_con = ioremap(base, 8);
if (gpio_con) {
printk("ioremap(0x%x) = 0x%x\n", base, gpio_con);
}
else {
return -EINVAL;
}
gpio_dat = gpio_con + 1;
*gpio_con &= ~(3<<(pin * 2));
*gpio_con |= (1<<(pin * 2));
return 0;
}
在Linux中,不能直接操作基地址,需要使用ioremap()映射。
对于基地址,定义全局指针来表示,gpio_con表示控制寄存器,gpio_dat表示数据寄存器。
这里将GPF5的第二个引脚先清空,再设置为1,表示输出引脚。
接下来是写函数:
static ssize_t led_write (struct file *filp, const char __user *buf, size_t size, loff_t *off)
{
/* 根据APP传入的值来设置LED引脚 */
unsigned char val;
int pin = led_pin & 0xffff;
copy_from_user(&val, buf, 1);
if (val)
{
/* 点灯 */
*gpio_dat &= ~(1<<pin);
}
else
{
/* 灭灯 */
*gpio_dat |= (1<<pin);
}
return 1; /* 已写入1个数据 */
}
注意这里的__user宏起强调作用,告诉你buf来自应用空间,在内核里不能直接使用。使用copy_from_user()将用户空间的数据拷贝到内核空间。再根据传入的值,设置gpio_dat的值,来点亮或者熄灭pin所对应的灯。
至此,这个驱动程序已经具备操作硬件的功能,但我们还要增加一些内容,比如我们先注册驱动后,自动创建节点信息。
在入口函数里,使用class_create()创建class,并且使用device_create()创建设备。有了这些信息,根文件系统中的mdev或udev会帮我们创建设备节点,就可以对/dev/led进行open、write等操作。
static int myled_init(void)
{
major = register_chrdev(0, "myled", &myled_oprs);
led_class = class_create(THIS_MODULE, "myled");
device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
return 0;
}
出口函数需要进行相反操作:
static void myled_exit(void)
{
unregister_chrdev(major, "myled");
device_destroy(led_class, MKDEV(major, 0));
class_destroy(led_class);
}
还有在release函数里,释放前面的iormap()的资源
static int led_release (struct inode *node, struct file *filp)
{
printk("iounmap(0x%x)\n", gpio_con);
iounmap(gpio_con);
return 0;
}
完整代码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/of_platform.h>
#define S3C2440_GPA(n) (0<<16 | n)
#define S3C2440_GPB(n) (1<<16 | n)
#define S3C2440_GPC(n) (2<<16 | n)
#define S3C2440_GPD(n) (3<<16 | n)
#define S3C2440_GPE(n) (4<<16 | n)
#define S3C2440_GPF(n) (5<<16 | n)
#define S3C2440_GPG(n) (6<<16 | n)
#define S3C2440_GPH(n) (7<<16 | n)
#define S3C2440_GPI(n) (8<<16 | n)
#define S3C2440_GPJ(n) (9<<16 | n)
static int major;
static struct class *led_class;
static unsigned int gpio_base[] = {
0x56000000, /* GPACON */
0x56000010, /* GPBCON */
0x56000020, /* GPCCON */
0x56000030, /* GPDCON */
0x56000040, /* GPECON */
0x56000050, /* GPFCON */
0x56000060, /* GPGCON */
0x56000070, /* GPHCON */
0, /* GPICON */
0x560000D0, /* GPJCON */
};
static int led_pin = S3C2440_GPF(5);
static volatile unsigned int *gpio_con;
static volatile unsigned int *gpio_dat;
static int led_open(struct inode *node, struct file *filp)
{
/* 将LED引脚 GPF5 配置为输出模式 */
int bank = led_pin >> 16;
int base = gpio_base[bank];
int pin = led_pin & 0xffff;
gpio_con = ioremap(base, 8); // GPFCON为0x56000050
if (gpio_con) {
printk("ioremap(0x%x) = 0x%x\n", base, gpio_con);
} else {
return -EINVAL;
}
gpio_dat = gpio_con + 1; // GPFDAT为0x56000054,这里是指针,所以+1
*gpio_con &= ~(3 << (pin * 2)); // 将GPFCON Bit[11:10]清零
*gpio_con |= (1 << (pin * 2)); // 将GPFCON Bit[11:10]设置为01
return 0;
}
// __user是一个空的宏。用途是提示这个buf来自应用,不能在内核中直接使用
static ssize_t *led_write (struct file *filp, const char __user *buf, size_t size, loff_t *off)
{
// 根据应用程序传入的值设置LED GPIO电平
unsigned char val;
int pin = led_pin & 0xffff;
copy_from_user(&val, buf, 1);
if (val) {
// 拉低GPIO,点亮LED
*gpio_dat &= ~(1 << pin);
} else {
// 拉高GPIO,熄灭LED
*gpio_dat |= (1 << pin);
}
return 1; // 表明已写入1个数据
}
static int led_release (struct inode *node, struct file *filp)
{
printk("iounmap(0x%x)\n", gpio_con);
iounmap(gpio_con);
return 0;
}
static struct file_operations myled_oprs = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
.release = led_release
};
static int myled_init(void)
{
major = register_chrdev(0, "myled", &myled_oprs);
led_class = class_create(THIS_MODULE, "myled");
device_create(led_class, NULL, MKDEV(major, 0), NULL, "led");
return 0;
}
static void myled_exit(void)
{
unregister_chrdev(major, "myled");
device_destroy(led_class, MKDEV(major, 0));
class_destroy(led_class);
}
module_init(myled_init);
module_exit(myled_exit);
MODULE_LICENSE("GPL");
可以看出,这种传统写驱动程序的方法把硬件资源写在了代码里,换个LED,换个引脚,就得去修改 led_pin = s3c2440_GPF(5),然后重新编译,加载。
测试程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(int argc, char **argv)
{
unsigned char val = 1;
int fd = open("/dev/led", O_RDWR);
if (fd < 0) {
printf("Open error\n");
}
if (argc != 2) {
printf("Usage: \n");
printf("%s <on|off>\n", argv[0]);
return 0;
}
if (strcmp(argv[1], "on") == 0) {
val = 1;
} else {
val = 0;
}
write(fd, &val, 1);
return 0;
}
3 字符设备驱动的编译测试
驱动程序的编译依赖于内核,在驱动程序里的一堆头文件,是来自于内核的,因此我们需要先编译内核。
接下来我们要编译驱动程序,编译测试程序,并在单板上测试一样。
首先从网盘下载:
doc_and_sources_for_device_tree/source_and_images/source_and_images下的内核源码和补丁;
doc_and_sources_for_device_tree/source_and_images/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabi.tar.xz编译内核和驱动的交叉编译工具链;
doc_and_sources_for_device_tree/source_and_images/arm-linux-gcc-4.3.2.tar.bz2编译测试程序的交叉编译工具链;
doc_and_sources_for_device_tree/source_and_images/readme.txt介绍了一些编译器、工具的使用、uboot等笔记,需要时可以看一看;
1.编译内核
将内核源码、补丁、编译内核的交叉工具链上传到Ubuntu,然后解压、打补丁。
再解压工具链,设置工具链环境,最后编译。
编译中遇到错误提示,尝试百度搜索,一般都能找到解决方法。
2.编译驱动
待内核编译完后,修改Makefile,编译驱动。
3.编译应用程序
解压编译应用程序的交叉编译工具链,修改环境变量,编译应用程序。
4.加载驱动和运行测试程序
使用nfs挂载该目录,加载驱动,运行测试程序。
4 总线设备驱动模型
4.1 总线设备驱动模型是为了解决什么问题
前面我们使用传统方法写了LED驱动。由于GPIO是写死在代码里的,如果需要修改GPIO脚,就得重新编译驱动文件。如果驱动放在内核中,就得重新编译内核。这种做法非常不方便。
改进的方法是,将驱动程序抽离出来,这部分是不需要改动的。我们另找一个方法将GPIO传进去,就可以避免这种问题。
bus总线是虚拟的软件概念,并不对应具体的硬件。
4.2 Dev是如何指定硬件资源的
dev注册设置某个结构体,这个结构体也就是平台设备。
struct platform_device {
const char *name;
int id;
bool id_auto;
struct device dev;
u32 num_resources;
struct resource *resource; /* resource里面确定使用哪些资源 */
const struct platform_device_id *id_entry;
char *driver_override; /* Driver name to force a match */
struct mfd_cell *mfd_cell; /* MFD cell pointer */
struct pdev_archdata archdata; /* arch specific additions */
};
drv那面定义platform_driver去注册
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;
};
4.2 设备和驱动如何进行通信
在Linux内核中,有多个Dev和Drv,如何确定对应关系?
通常平台bus进行匹配。调用platform_match函数匹配Dev和Drv。若匹配,则调用drv中的probe函数。
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
这种模型只是一种编程技巧一种机制,并不是驱动程序的核心。
4.3 platform_match是如何判断dev drv是匹配的
platform_match函数通过比较Dev和Drv的name来进行匹配
- 平台设备platform_device这面有name
- platform_driver这面有 driver (里面含有name) 还有id_table(包含 name driver_data)
- id_table里面的内容表示所支持一个或多个的设备名
static int platform_match(struct device *dev, struct device_driver *drv)
{
/*省略部分无用代码*/
/* 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);
}
也就是优先比较 id_table中名字,如果没有则对比driver中名字
根据二期视频led代码进行修改
/* 分配/设置/注册一个platform_device */
/*设置资源*/
static struct resource led_resource[] = {
[0] = {
/*指明了使用那个引脚*/
.start = S3C2440_GPF(5),
/*end并不重要,可以随意指定*/
.end = S3C2440_GPF(5),
.flags = IORESOURCE_MEM,
},
};
static void led_release(struct device * dev)
{
}
static struct platform_device led_dev = {
.name = "myled",
.id = -1,
.num_resources = ARRAY_SIZE(led_resource),
.resource = led_resource,
.dev = {
.release = led_release,
},
};
/*入口函数去注册平台设备*/
static int led_dev_init(void)
{
platform_device_register(&led_dev);
return 0;
}
/*出口函数去释放这个平台设备*/
static void led_dev_exit(void)
{
platform_device_unregister(&led_dev);
}
module_init(led_dev_init);
module_exit(led_dev_exit);
led_drv驱动文件
static int led_probe(struct platform_device *pdev)
{
struct resource *res;
/* 根据platform_device的资源进行ioremap */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
led_pin = res->start;
major = register_chrdev(0, "myled", &myled_oprs);
led_class = class_create(THIS_MODULE, "myled");
device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
return 0;
}
struct platform_driver led_drv = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = "myled",
}
};
static int myled_init(void)
{
platform_driver_register(&led_drv);
return 0;
}
static void myled_exit(void)
{
platform_driver_unregister(&led_drv);
}
Makefile文件
KERN_DIR = /work/system/linux-4.19-rc3
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += led_drv.o
obj-m += led_dev.o
执行测试程序
如果我需要更换一个led
则只需要修改 led_dev led_resource结构体中的引脚即可
static struct resource led_resource[] = {
[0] = {
.start = S3C2440_GPF(6),
.end = S3C2440_GPF(6),
.flags = IORESOURCE_MEM,
},
};
设备和驱动的匹配是如何完成的?
- dev这面有设备链表
- drv这面也有驱动的结构体链表
- 通过match函数进行对比,如果相同,则调用drv中的probe函数
5 使用设备树时对应的驱动编程
本节介绍怎么使用设备树怎么编写对应的驱动程序
只是平台设备的构建区别,以前构造平台设备是在.c文件中,使用设备树构造设备节点原本不存在,需要在dts文件中构造节点,节点中含有资源
dts被编译成dtb文件传给内核,内核会处理解析dtb文件得到device_node结构体,之后变成platform_device结构体,里面含有资源(资源来自dts文件)
我们定义的led设备节点
led {
compatible = "jz2440_led";
reg = <S3C2410_GPF(5) 1>;
};
以后就使用compatible找到内核支持这个设备节点的平台driver reg = <S3C2410_GPF(5) 1>; 就是寄存器地址的映射
修改好后编译 设备树文件 make dtb
拷贝到tftp文件夹,开发板启动
进入 /sys/devices/platform 目录查看是否有5005.led平台设备文件夹
查看 reg 的地址,这里面是以大字节须来描述这些值的
这个属性有8个字节,对应两个数值
第一个值S3C2410_GPF(5)是我们的起始地址,对应 #define S3C2410_GPF(_nr) ((5<<16) + (_nr))
第二个值1 本意是指寄存器的大小
如何去写平台驱动?
通过bus总线去匹配设备驱动
在 platform_match函数中,通过
/* Attempt an OF style match first */
if (of_driver_match_device(dev, drv))
return 1;
进入 of_device.h中
/**
- of_driver_match_device - Tell if a driver’s of_match_table matches a device.
- @drv: the device_driver structure to test
- @dev: the device structure to match against
*/
static inline int of_driver_match_device(struct device *dev,
const struct device_driver *drv)
{
return of_match_device(drv->of_match_table, dev) != NULL;
}
of_match_table结构体
include\linux\mod_devicetable.h
/*
- Struct used for matching a device
*/
struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};
compatible 也就是从dts得到的platform_device里有compatible 属性,两者进行对比,一样就表示匹配
写led驱动,修改led_drv.c
添加
static const struct of_device_id of_match_leds[] = {
{ .compatible = “jz2440_led”, .data = NULL },
{ /* sentinel */ }
};
*修改
struct platform_driver led_drv = {
.probe = led_probe,
.remove = led_remove,
.driver = {
.name = “myled”,
.of_match_table = of_match_leds, /* 能支持哪些来自于dts的platform_device */
}
};
*修改Makefile并编译
如果修改灯怎么办?
直接修改设备树中的led设备节点
led {
compatible = “jz2440_led”;
reg = <S3C2410_GPF(6) 1>;
};
上传编译,直接使用新的dtb文件
我们使用另外一种方法指定引脚
led {
compatible = "jz2440_led";
pin = <S3C2410_GPF(5)>;
};
修改led_drv中的probe函数
在of.h中找到获取of属性的函数 of_property_read_s32
static int led_probe(struct platform_device *pdev)
{
struct resource *res;
/* 根据platform_device的资源进行ioremap */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res) {
led_pin = res->start;
}
else {
/* 获得pin属性 */
of_property_read_s32(pdev->dev.of_node, "pin", &led_pin);
}
if (!led_pin)
{
printk("can not get pin for led\n");
return -EINVAL;
}
major = register_chrdev(0, "myled", &myled_oprs);
led_class = class_create(THIS_MODULE, "myled");
device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
return 0;
}
从新编译设备树 和led驱动文件
在platform_device结构体中的struct device dev;中对于dts生成的platform_device这里含有of_node
of_node中含有属性,这取决于设备树,比如compatible属性
让后注册/配置/file_operation
第06节_只想使用设备树不想深入研究怎么办
寄希望于写驱动程序的人,提供了文档/示例/程序写得好适配性强
根据之前写的设备树
led {undefined
compatible = “jz2440_led”;
reg = <S3C2410_GPF(6) 1>;
};
led {undefined
compatible = “jz2440_led”;
pin = <S3C2410_GPF(5)>;
};
可以通过reg指定引脚也可以通过pin指定引脚,我们在设备树中如何指定引脚完全取决于驱动程序
既可以获取pin属性值也可以获取reg属性值
/* 根据platform_device的资源进行ioremap */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res) {
led_pin = res->start;
}
else {
/* 获得pin属性 */
of_property_read_s32(pdev->dev.of_node, "pin", &led_pin);
}
if (!led_pin)
{
printk("can not get pin for led\n");
return -EINVAL;
}
我们通过驱动程序再次验证了设备树的属性完全却决于写驱动程序的人
commpatible属性必须是 jz2440_led 才可以和驱动匹配成功
我们写驱动的人应该写一个文档,告诉写应用程序的人设备树的节点应该怎么编写
对于内核自带的驱动文件,对应的设备树的文档一般放在Documentation\devicetree\bindings目录中,里面有各种架构的说明文档以及各种协议的说明文档,
这些驱动都能在 drivers 目录下找到对应的驱动程序.
比如查看Documentation\devicetree\bindings\arm\samsung\exynos-chipid.txt里面的内容如下
SAMSUNG Exynos SoCs Chipid driver.
Required properties:(必须填写的内容)
- compatible : Should at least contain “samsung,exynos4210-chipid”.
- reg: offset and length of the register set
Example:
chipid@10000000 {
compatible = “samsung,exynos4210-chipid”;
reg = <0x10000000 0x100>;
};
我们自己写的驱动说明文档自然没有适配到内核中去,所以只能期盼商家给你提供相应的说明文档
参考同类型单板的设备树文件
进入 arch\arm\boot\dts 目录下里面是各种单板的设备树文件
比如
am335x-boneblack.dts和am335x-boneblack-wireless.dts
发现多了wifi的信息,通过对比设备树文件,我们可以看出怎么写wifi设备节点,就知道如何添加设备节点.
网上搜索
实在不行就研究驱动源码
一个好的驱动程序,它会尽量确定所用资源,只把不能确定的资源留给设备树,让设备树来指定。