一个字符设备就是像led灯、蜂鸣器等这样的设备,驱动是要这样的设备进行工作(包括初始化)、操作的方法。
在Linux字符设备驱动程序中,完成设备如何工作的方法。在应用程序中,我们编写程序通过驱动程序中实现的方法来达到设备在应用中的使用。而应用程序对驱动实现的方法的调用提供了统一的程序接口(API),在Linux系统中“一切都是用文件”的形式表的。我们在操作设备的时候采用的是open()方式,就是像打开文件一样,对其进行读、写、控制操作。应用程序中的方法open()、close()、read()、write()等API在驱动程序中都是唯一的对应的,只有在驱动程序中实现了方法才能在应用程序中调动这些方法。
但应用程序调用方法不是直接的调用,过程为:应用程序API-->对用的库函数--->系统调用-->驱动程序调用。这样的好处是进行了分层的设计。
而驱动中的方法通过结构体管理struct file_operations;该结构体中包含了多种方法:
read()、write()、open()、release()四种是非常见的方法,一般是都需要实现的。
字符设备驱动程序一般可以直接编写到内核和内核一起编译,也可以编译成模块.ko文件动态的加载到内核中,一般选择后者。
驱动模块中必须实现的函数:
(1) 模块的加载和卸载函数:
module_init(XX_init):函数是完成驱动模块在动态记载insmod时向Linux内核注册模块。
module_exit(XX_exit):是在模块卸载时 rmmod 时向内核中注销模块。
(2)字符设备的注册和注销:
模块加载成功后,需要注册字符设备。
1)字符设备注册:static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
三个参数:major主设备号、name设备名字、fops设备操作函数(各种设备的操作方法结构体);
2)字符设备注销:static inline void unregister_chrdev(unsigned int major, const char *name);
两个参数:主设备号,设备名;
(3) LICENSE 和 AUTHOR
这个需要添加模块的license 和 author;
(4) fops的具体操作方法
static struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = XXX_open,
.read = XXX_read,
.write = XXX_write,
.release = XXX_release,
};
使用文件操作方法进行;
(1)加载模块
(2)这样的模块加载后,可以使用cat/proc/devices 查看
(3)手动创建设备节点文件:mknod /dev/XXX_dev c 主设备号 此设备号
(4)设备节点创建完成后,在dev/下面生成了节点文件,可以使用应用程序直接读写操作了。
上述确实是一个设备驱动程序的全过程,但是一般都不这样使用,为什么?别急我们慢慢总结;
(1)上述介绍的字符设备驱动开发中使用的register_chrdev和unregister_chrdev 是老版本的驱动使用的注册和注销函数。
(2)上述介绍的需要手动生成设备节点,因此在开发中很麻烦。
(3)上述介绍的设备使用的一个主设备号,一下子占用了整个这个主设备,造成设备号资源的严重浪费。
所以我们开使用新版本的设备驱动开发
1.设备号的处理
(1)静态申请:静态是使用已知的设备主设备号和次设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数:起始设备号,申请个数,设备名称;
(2)动态申请:系统自动申请设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
参数:返回设备号,申请个数,设备名称;
(3)注销设备号:
void unregister_chrdev_region(dev_t from, unsigned count)
这样就解决了设备号分配的问题。
2. 设备注册新方法
(1)使用字符设备结构体统一管理设备和字符设备操作方法:
struct cdev {
struct kobject kobj;
struct module *owner; // 一般为 THIS_MODULE
const struct file_operations *ops; // 字符设备操作方法结构体,自己定义每个函数的操作方法;
struct list_head list;
dev_t dev; // 设备号
unsigned int count;
};
(2)定义字符设备结构体后需要初始化
这个过程需要先定义字符设备结构体,之后定义file_ioerations 结构体的操作方法,最有进行方法、设备绑定;
cdev_init(&XXXcdev, &XXX_fops);
(3)向内核添加字符设备
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数:字符设备指针,设备号,添加的设备数量;
其中,有添加就有删除,使用卸载驱动的时候一定要使用 cdev_del函数从Linux内核中删除相应的字符设备:
void cdev_del(struct cdev *p);
(4)设备号的申请、字符设备的注册添加都搞定后,还有一个就是要能自动创设备节点,供应用程序访问设备;
这个在嵌入式Linux中使用的是mdev来实现设备节点文件的自动创建于删除,我们在驱动程序中只需要完成class类的创建和设备的创建就行了。
1) class类 的创建 struct class *class_create (struct module *owner, const char *name)
参数:THIS_MODULE,类名;
卸载驱动的时候需要删除创建的类:void class_destroy(struct class *cls);
2)创建设备:struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
参数:创建的类1)创建的那个类,父设备(NULL),设备号,参数值没有的话为NULL,设备的名字(会根据名字生成/dev/xxx设备)
卸载时删除设备函数:void device_destroy(struct class *class, dev_t devt);
(5) 其他处理方法
1)在实际的程序中需要自己定义一个结构体来管理设备的属性参数:
struct test_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
//还要的属性例如信号量、互斥体等需要的都可以定义在一起。
};
2) 一般在驱动的open(struct inode *inode, struct file *filp)函数中把定义的设备结构体赋值给filp->private_data,这样话在其他操作函数中可以获取到定义的设备结构体。
注意:上述就是经常使用的字符设备驱动的编写流程,关键点都已经包含了,使用应用程序可以访问操作设备工作了。但是一个很关键的缺点在这里就直接的说了,就是一个设备对应一种驱动,对于同类型的设备还有需要重新的编写驱动与之对应。然而实际中是一种驱动方法可以对应多个设备,一个一个的再编写驱动效率比较低。Linux采用了分层设计的方法实现设备和驱动分离来解决这个问题,也就是我们接下来要总结的了。
Linux考虑了驱动的可重复性,将驱动的分离与分层应用到软件的设计上。
platform 就是完成这个任务,实际是一种虚拟的总线,叫做平台设备驱动。
这三幅图来源于那个书籍上的图,但是涉及到了主机驱动和设备驱动分离,我们在这先不讲主机驱动和设备驱动分离,只是先谈一谈设备和驱动分离。设备和驱动分离主要是将设备信息从设备驱动中剥离开来,设备只需要管理设备信息,驱动只需要管理驱动信息,至于哪个设备使用哪种驱动我们让BUS总线决定,也就是platform 决定。使用总线将驱动和设备信息进行了管理和匹配。就变成了下边这幅图的形式:
platform 是一种虚拟的总线,来管理没有实际总线形式的设备和驱动(I2C、SPI、USB是总线除外)。
platform是bus_type的一个具体实例。 最关键的的两个结构体platform_driver和platform_device;
(1)platform_driver:表示的是platform驱动:
struct platform_driver {
int (*probe)(struct platform_device *); // 当驱动与设备匹配成功后执行probe
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; //是device_driver是基类,platform_driver继承了这个基类
const struct platform_device_id *id_table; //platform 总线匹配驱动和设备时的一种方法;每个元素是platform_device_id;
bool prevent_deferred_probe;
};
其中的(1)struct platform_device_id {
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data;
};
(2)struct device_driver结构体的of_match_table就是采用设备树的时候驱动使匹配表
struct of_device_id {
char name[32];
char type[32];
char compatible[128]; //设备树的compatible
const void *data;
};
(2)编写platform驱动的步骤:
1)定义一个platform_driver结构体变量,然后实现结构体变量;重点是实现匹配方法以及 probe函数。
2)向内核注册platform驱动:int platform_driver_register (struct platform_driver *driver)
卸载函数为:void platform_driver_unregister(struct platform_driver *drv)
(3)platform驱动框架:
1)还是和第二部分一样定义一个设备结构体管理字符设备和属性
struct xxx_dev{struct cdev cdev;}
struct xxx_dev xxxdev;
2) 定义字符设备驱动操作方法 file_operations xxx_fops={.wner=THIS_MODULE,.open(),.read(),]
3) 编写xxx_probe 函数,这个函数在设备和驱动匹配成功后执行,我们之前把注册字符设备驱动在模块初始化函数中进行,现在放在xxx_probe函数中完成,cdev_init(&xxxdev.cdev,&xxx_fops); add_cdev(xxxdev.cdev);
4) 编写xxx_remove函数,完成cdev_del(&xxxdev.cdev);/* 删除cdev */
5)编写匹配列表 of_device_id
6)platform平台驱动结构体定义,完成其结构体成员函数;
7)在模块加载函数中注册platform驱动platform_driver_register(&xxx_driver);
8)在模块卸载函数中卸载platform_driver_unregister(&xxx_driver);
9)license 和作者申明;
注意:只是在字符设备驱动中套用了一张platform的壳;
platform_device变量主要是描述设备信息
struct platform_device {
const char *name; // 设备的名字要和platform驱动的名字相同,否则无法匹配。
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;
}
(1) platform设备注册到内核中:int platform_device_register(struct platform_device *pdev)
卸载:void platform_device_unregister(struct platform_device *pdev)
(2)platform设备模块编写使用时,在模块初始化时进行注册,在模块卸载时进行注销卸载。
注意:上述就是平台架构的字符主设备驱动编写方法和流程。其实字符设备驱动的编写方法还是一样,只不过套用了一个platform的外壳来管理设备驱动和设备。
特点是:(1)字符设备的设备号申请、注册、字符设备的初添加字符设备、创建类、创建设备都在platform_driver 的probe函数中完成。(2)字符设备的删除(cdev_del())、设备号的注销、设备的删除、类的删除都在remove函数中进行了。
(3)其实platform_device 一般是在平台文件中进行,如果采用了设备树的话如在设备树文件中包含设备信息,不需要手动实现platform_device;