下文中除了具体使用上的总结以外,还有一些个人认为帮助我自己理解的一些认识,不知道比喻的或者总结的对不对,如有错误,请斧正。
对于编程者来说,编写一个设备驱动来驱动一个设备要有两个方面:一是设备实体,二是设备抽象,这些内容要在驱动框架
中体现。Linux操作系统将字符设备的这两个方面定义为: cdev
和file两个结构,设备相关的内容以及操作方法(file_operations)都存在cdev结构中,对于上层操作系统来说,将所有的设备都抽象为
文件(file),打开设备即得到一个代表设备的file(设备描述符),其中包含了操作设备的方法file_operations,以及设备实体的描述
cdev(“潜规则”的将其存在private_data字段)。cdev和file通过绑定和操作函数产生关系。
每打开一个文件或者设备文件都会有一个对应的file,但是相同的file只有一个inode节点。
platform设备驱动也一样有两个方面:platform devices 和platform driver。不一样的是,多出一个platform bus 的虚拟总线。
一、字符设备驱动的使用
字符设备重要结构:
cdev 、 file 和 file_operations
cdev结构:
-
struct cdev {
-
struct kobject kobj;
-
struct module *owner;
-
const struct file_operations *ops;
-
struct list_head list;
-
dev_t dev;
-
unsigned int count;
-
}
file_oprations结构:
-
struct file_operations {
-
struct module *owner;
-
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 (*iterate) (struct file *, struct dir_context *);
-
unsigned int (*poll) (struct file *, struct poll_table_struct *);
-
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
-
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
-
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 *);
-
……
-
}
file结构:
-
struct file {
-
union {
-
struct llist_node fu_llist;
-
struct rcu_head fu_rcuhead;
-
} f_u;
-
struct path f_path;
-
#define f_dentry f_path.dentry
-
struct inode *f_inode; /* cached value */
-
const struct file_operations *f_op;
-
……
-
/* needed for tty driver, and maybe others */
-
void *private_data; /*<<<<<<<------------这个指针*/
-
……
-
}
字符设备的具体使用过程如下:
1、设备首先要有设备号,自定义设备号:
设备号类型:dev_t ,宏定义的u32类型
生成设备号函数:dev_t devno = MKDEV(unsigned int major, unsigned int minor);
从设备号中获取主次设备号: unsigned int dev_major = MAJOR(dev_t devno);
unsigned int dev_minor = MINOR(dev_t devno);
2、设备号有了以后,需要看看自己定义的设备号合不合法,需要调用:
register_chrdev_region(dev_t first, unsigned int count, char *name);
定义的设备号如果注册成功,说明设备号可用,否则返回错误。
first为自定义的设备号,count为需要申请的设备个数,可以为1,可以为12,等。
有时驱动作者并不知道系统中哪些设备号可以用,如果自定义的被占用则会注册失败,所以内核提供了一种动态申请设备号的方法:
alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
*dev 是接收动态分配的设备号的变量;firstminor通常为0,表示动态分配;count、name 同上。
动态分配每次的设备号可能是不同的,在cat /proc/devices文件中可以得到。
无论那种方法,注销设备号都调用:unregister_chrdev_region(dev_t first, unsigned int count);
另外,从源码看,静态和动态申请设备号的函数,内部都是调用了 __register_chrdev_region,如果主设备号为0,表示动态分配。
3、构造设备实体相关的结构,并为结构体分配空间(cdev_alloc)
每个设备各不相同,所以要为不同设备定义自己特有的设备实体的描述,这里面会包含cdev结构,以及其他的特殊信息。比如一个用内存来模拟的虚拟设备可以有如下定义:
-
struct mydev{
-
struct cdev cdev;
-
unsigned int dev_mem[ 2048 ];
-
/*设备版本、一些全局变量等内容,我想也可以放在这里统一管理*/
-
}
cdev相关的操作函数有:
-
struct cdev * cdev_alloc(void); /*为cdev分配空间,调用了kzalloc函数,如果自定义了设备的结构,如上面的mydev,可以之直接用kmalloc给整个mydev分配空间,在memset为0,效果相同*/
-
void cdev_init(struct cdev *dev, struct file_operations *fops); /*初始化cdev,即将cdev->ops和自定义的file_operation方法想关联*/
-
int cdev_add(struct cdev *dev, dev_t num, unsigned int count); /*向系统添加一个cdev*/
-
void cdev_del(staruc cdev *dev); /*从系统删除一个cdev*/
4、初始化该设备结构,并向系统添加
分配好空间后,调用cdev_init(); cdev.owner = THIS_MODULE;再调用cdev_add(); 将cdev添加到系统即可完成对字符设备的注册
5、字符设备的操作函数
如前所说的file_operations结构,需要实现open,release,read, write, ioctl等操作方法,然后在cdev初始化时,将file_operations结构作为第二个参数传入。
模块编译到内核,开机之后cat /proc/devices 可以看到mydev设备节点,但是在/dev下面是没有的。
250 ttyGS
251 ttyV
252 mydev
253 iio
也就是说,单纯cdev_add之后没有设备节点,不能供上层应用程序使用。即,不能调用open close read write等函数来操作设备。需要手动创建节点:mknod /dev/mydev c 252 0,才会出现。
注意: 注册设备的旧接口,已经不再使用:
register_chrdev();和unregister_chrdev();在2.6之后不再提倡使用。
字符设备每次在系统启动后都要手动创建节点,肯定是不现实的,所以从2.6开始,udev代替了之前devfs,作为新的设备管理器(udev是应用层程序),内核提供了接口来自动创建设备节点:
#define class_create(owner, name)
和
device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
相应的注销函数:
class_destory() class_devices_destory()
并且需要声明 #include
头文件,然后在/dev目录就会产生设备节点。
使用示例:
struct class *myclass = class_create(THIS_MODULE, “char_dev”);
device_create(myclass, NULL, MKDEV(major_num, 0), NULL, “char_dev”);
注:class_device_create函数已经在2.6.29以后改为device_create
几个概念:设备节点(在/dev下) 和 设备属性文件(/sys/devices 等目录下)
二、混杂设备misc的使用
从源码可以看出混杂设备是具有固定主设备号的字符设备,主设备号为10,那么它的作用和字符设备就类似了。只是misc设备的实现接口更加简单。
并且更重要的一点是:可以自动在/dev目录下创建设备节点,而不需要跟字符设备一样,手动创建,或者调用clase_create、devices_create函数 。
混杂设备的数据结构为:
-
struct miscdevice {
-
int minor; //次设备号,若为 MISC_DYNAMIC_MINOR 自动分配
-
const char *name; //设备名
-
const struct file_operations *fops; //设备文件操作结构体
-
struct list_head list; //misc_list链表头
-
struct device *parent;
-
struct device *this_device;
-
const char *nodename;
-
mode_t mode;
-
}
可以看出,由于主设备号为10,在结构中只有次设备号需要自己定义。另外,跟字符设备类似,需要自己定义文件操作函数,填充到file_operations 字段;设备名字,同时也会以此名称,在/dev下创建相应的设备节点。示例定义如下:
-
static struct miscdevice misc = {
-
.minor = MISC_DYNAMIC_MINOR,
-
.name = DEVICE_NAME,
-
.fops = &dev_fops,
-
}
定义好之后,注册和注销misc设备只需要调用如下函数即可:
int misc_register(struct miscdevice * misc); //在加载模块时会自动创建设备文件
int misc_deregister(struct miscdevice *misc); //在卸载模块时会自动删除设备文件
下面从misc设备的初始化和注册函数源码中看看,如何自动创建设备节点的:
初始化函数中创建了misc的class
-
static int __init misc_init(void)
-
{
-
……
-
/*在/sys/class/目录下创建一个名为misc的类*/
-
misc_class = class_create(THIS_MODULE, "misc");
-
……
-
/*调用字符驱动的注册函数, 设备的主设备号为MISC_MAJOR,为10*/
-
if (register_chrdev(MISC_MAJOR,"misc",&misc_fops))
-
goto fail_printk;
-
misc_class->devnode = misc_devnode;
-
return 0;
-
……
-
}
-
/*向内核注册misc子系统*/
-
subsys_initcall(misc_init)
注册函数中创建了设备节点:
-
int misc_register(struct miscdevice * misc)
-
{
-
……
-
/*动态分配设备的次设备号*/
-
if (misc->minor == MISC_DYNAMIC_MINOR) {
-
int i = find_first_zero_bit(misc_minors, DYNAMIC_MINORS);
-
if (i >= DYNAMIC_MINORS) {
-
mutex_unlock(&misc_mtx);
-
return -EBUSY;
-
}
-
misc->minor = DYNAMIC_MINORS - i - 1;
-
set_bit(i, misc_minors);
-
}
-
-
/*使用固定的主设备号,动态分配的次设备号构造设备号*/
-
dev = MKDEV(MISC_MAJOR, misc->minor);
-
-
/*创建设备文件,使用了miscdevice结构的name和this_device字段*/
-
misc->this_device = device_create(misc_class, misc->parent, dev,
-
misc, "%s", misc->name);
-
……
-
/*到这一步也就注册成功了,将新注册的misc设备加入到内核维护的misc_list链表中*/
-
list_add(&misc->list, &misc_list);
-
……
-
}
由此可见,misc设备的初始化和注册过程中只是集成了class_create和device_create函数而已,流程其实和字符设备大同小异,具体使用就不再赘述。
三、platform驱动模型的使用
platform驱动使用同样比较简单,包括了两个结构(platform_
device/platform_
driver)和两个函数(platform_
device_register/platform_
driver_register)。
使用过程可以简单概括为两步:1、实现platform_device以及其中的resource字段,调用
platform_device_register注册设备;2、实现platform_driver结构,调用
platform_driver_register注册驱动。
platform_device/platform_driver结构:
在2.6内核中platform设备用结构体platform_device来描述,该结构体定义在kernel\include\linux\platform_device.h中
-
struct platform_device {
-
const char * name; //设备名
-
u32 id;
-
struct device dev;
-
u32 num_resources; //resources个数,可以不赋值
-
struct resource * resource;
-
};
该结构一个重要的元素是resource,该元素存入了最为重要的设备资源信息,定义在kernel\include\linux\ioport.h中,
-
struct resource {
-
const char *name;
-
unsigned long start, end;
-
unsigned long flags;
-
struct resource *parent, *sibling, *child;
-
}
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; //driver字段下有需要实现的部分
-
const struct platform_device_id *id_table;
-
};
-
//上面结构中的device_driver结构
-
struct device_driver {
-
const char *name; //必须要与device中的相同,不然会注册失败
-
struct bus_type *bus;
-
struct module *owner;
-
const char *mod_name; /* used for built-in modules */
-
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
-
const struct of_device_id *of_match_table;
-
const struct acpi_device_id *acpi_match_table;
-
int (*probe) (struct device *dev);
-
int (*remove) (struct device *dev);
-
void (*shutdown) (struct device *dev);
-
int (*suspend) (struct device *dev, pm_message_t state);
-
int (*resume) (struct device *dev);
-
const struct attribute_group **groups;
-
const struct dev_pm_ops *pm;
-
struct driver_private *p;
-
}
第一步,实现device相关内容。
-
static struct resource s3c_i2c_resource[] = {
-
[0] = {
-
.start = S3C24XX_PA_IIC,
-
.end = S3C24XX_PA_IIC + S3C24XX_SZ_IIC - 1,
-
.flags = IORESOURCE_MEM,
-
},
-
[1] = {
-
.start = IRQ_IIC, //S3C2410_IRQ(27)
-
.end = IRQ_IIC,
-
.flags = IORESOURCE_IRQ,
-
}
-
}
这里定义了两组resource,它描述了一个I2C设备的资源,第1组描述了这个I2C设备所占用的总线地址范
围,IORESOURCE_MEM表示第
1组描述的是内存类型的资源信息,第2组描述了这个I2C设备的中断号,IORESOURCE_IRQ表示第2组描述的是中断资源信息。
设备驱动会根据flags来获取相应的资源信息。
有了resource信息,就可以定义platform_device了:
-
struct platform_device s3c_device_i2c = {
-
.name = "s3c2410-i2c",
-
.id = -1,
-
.num_resources = ARRAY_SIZE(s3c_i2c_resource),
-
.resource = s3c_i2c_resource,
-
}
驱动程序需要实现结构体struct platform_driver
-
static struct platform_driver s3c2410_i2c_driver = {
-
.probe = s3c24xx_i2c_probe,
-
.remove = s3c24xx_i2c_remove,
-
.resume = s3c24xx_i2c_resume,
-
.driver = {
-
.owner = THIS_MODULE,
-
.name = "s3c2410-i2c",
-
},
-
}
如果device和driver的name一致的话,在执行platform的match函数时,就会将驱动与设备匹配,并且调用probe函数,将设备初始化,也就完成了设备和驱动程序的注册。
在resource中定义了设备的硬件资源,寄存器地址或者memory空间或者中断号,在驱动中想要获取resource中定义的资源,则调用platform_get_resource函数即可得到。
-
/**
-
* platform_get_resource - get a resource for a device
-
* @dev: platform device
-
* @type: resource type
-
* @num: resource index
-
*/
-
struct resource *platform_get_resource(struct platform_device *dev,
-
unsigned int type, unsigned int num)
struct resource *res = NULL;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
获取IRQ可以这样:res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
以
上是platform驱动的一般实现方式,这里的resource结构也可统一的在其他地方定义,比如一个大的系统,各种设备的资源、名称、寄存器等,可
以统一定义,在模块驱动文件中直接使用。但是不论统一定义,还是模块内定义,都会给内核带来大量的垃圾代码,并且可维护性降低。引用一段话,说明对于上述
情况的解决办法:
Linus Torvalds 在 2011 年 3 月 17 日的 ARM Linux 邮件列表宣称“this whole ARM thing is a f*cking pain in the ass”,引发 ARM Linux 社区的地震,随后 ARM 社区进行了一系列 的重大修正。在过去的 ARM Linux 中,arch/arm/plat-xxx 和 arch/arm/mach-xxx 中充斥着大 量的垃圾代码,相当多数的代码只是在描述板级细节,而这些板级细节对于内核来讲,不 过是垃圾,如板上的 platform 设备、resource、i2c_board_info、spi_board_info 以及各种硬 件的 platform_data。读者有兴趣可以统计下常见的 s3c2410、s3c6410 等板级目录,代码量 在数万行。
社区必须改变这种局面,于是 PowerPC 等其他体系架构下已经使用的 Flattened Device Tree(FDT)进入 ARM 社区的视野。Device Tree 是一种描述硬件的数据结构,它起源于 OpenFirmware (OF)。在 Linux 2.6 中,ARM 架构的板极硬件细节过多地被硬编码在 arch/arm/plat-xxx 和 arch/arm/mach-xxx,采用 Device Tree 后,许多硬件的细节可以直接透 过它传递给 Linux,而不再需要在 kernel 中进行大量的冗余编码。
所以现在有节操的代码中,可能没有了resource的定义,设备相关的描述统一的放在了arch/arm(arm64)/boot/dts路径下的dts文件中,dt的内容本文不做讨论。
在加入dt以后,驱动程序的变化是,不再需要对设备资源有描述,没有了platfrom_device_register函数来注册设备,这些工作都由内核来替我们完成(解析dt的代码为of_*开头的源文件), 驱动只要做driver相关的定义和注册即可。相应的如果想要使用本设备的资源,依然是调用platform_get_resource函数获取;如果想要使用其他设备的资源,则需要获取该设备的节点,再获取其resource内容。
-
device01{
-
compatible = "compa ny,device01";
-
reg = <0 0x30040000 0 0xA0000 //设备资源的描述
-
0 0x20F00000 0 0x300000>;
-
interrupts = <0 86 0x0>;
-
}
如果使用dt,驱动中还要给platform_driver中.driver的.of_match_table填充一个of_device_id结构,比如:
-
static const struct of_device_id of_match[] = {
-
{ .compatible = "company,device01", },
-
{ }
-
};
-
static struct platform_driver xx_driver = {
-
.probe = xx_probe,
-
.remove = xx_remove,
-
.suspend = xx_suspend,
-
.resume = xx_resume,
-
.driver = {
-
.owner = THIS_MODULE,
-
.name = "device01",
-
.of_match_table = of_match
-
}
-
}
如果驱动中想要使用其他节点(设备)定义的资源,由于没有相应platform device在本驱动中,不能简单的通过platform_get_resource来获取,platform_get_resource第一个参数就是paltform device,即本设备。则需要通过一系列的of_*开头的函数来获取。在driver/of目录下有相关的源文件,比如下面广两个函数可以满足简单需求:
获取dt节点:
struct device_node *of_find_compatible_node (struct device_node *from, const char *type, const char *compatible)
struct device_node *of_find_node_by_name(struct device_node *from, const char *name)
将节点中的指定资源,转到resource变量中:
int of_address_to_resource (struct device_node *dev, int index, struct resource *r)
最后:在项目过程中,感觉简单的字符设备应用并不常见,有的完全可以用misc设备代替,并且platform引入以后,linux下大部分的驱动都可以以此框架实现,所以platfrom更为常见,并可配合misc设备向上提供节点,供引用层使用。也可以配合sys节点,创建设备的属性文件,供上层使用,方便并且容易实现。
阅读(6175) | 评论(0) | 转发(0) |