Linux驱动学习(四)字符设备
字符设备是指在 I/O 传输过程中以字符为单位进行传输的设备, 可以使用与普通文件相同的文件操作命令(打开、 关闭、 读、 写等) 对字符设备进行操作, 是 Linux 驱动中最基本的一类设备驱动, 例如最常见的 LED、 按键、 IIC、SPI, LCD 等都属于字符设备的范畴。
申请驱动设备号
在 Linux 系统中每一个设备都有相应的设备号, 通过该设备号查找对应的设备, 从而进行之后的文件操作。 设备号有主设备号与次设备号之分, 主设备号用来表示一个特定的驱动, 次设备号用来管理下面的设备。
两种方法:静态申请(手动指定)、动态申请(自动分配未使用的)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
static int major;//定义静态加载方式时的主设备号参数major
static int minor;//定义静态加载方式时的次设备号参数minor
module_param(major,int,S_IRUGO);//通过驱动模块传参的方式传递主设备号参数major
module_param(minor,int,S_IRUGO);//通过驱动模块传参的方式传递次设备号参数minor
static dev_t dev_num;//定义dev_t类型(32位大小)的变量dev_numstatic int __init dev_t_init(void)//驱动入口函数
{int ret;//定义int类型的变量ret,用来判断函数返回值/*以主设备号进行条件判断,即如果通过驱动传入了major参数则条件成立,进入以下分支*/if(major){dev_num = MKDEV(major,minor);//通过MKDEV函数将驱动传参的主设备号和次设备号转换成dev_t类型的设备号printk("major is %d\n",major);printk("minor is %d\n",minor);ret = register_chrdev_region(dev_num,1,"chrdev_name");//通过静态方式进行设备号册if(ret < 0){printk("register_chrdev_region is error\n");}printk("register_chrdev_region is ok\n");}/*如果没有通过驱动传入major参数,则条件成立,进入以下分支*/else{ret = alloc_chrdev_region(&dev_num,0,1,"chrdev_num");//通过动态方式进行设备号注册if(ret < 0){printk("alloc_chrdev_region is error\n");} printk("alloc_chrdev_region is ok\n");major=MAJOR(dev_num);//通过MAJOR()函数进行主设备号获取minor=MINOR(dev_num);//通过MINOR()函数进行次设备号获取printk("major is %d\n",major);printk("minor is %d\n",minor);}return 0;
}static void __exit dev_t_exit(void)//驱动出口函数
{unregister_chrdev_region(dev_num,1);//释放字符驱动设备号 printk("module exit \n");
}module_init(dev_t_init);//注册入口函数
module_exit(dev_t_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("topeet"); //作者信息
注册字符设备
申请到设备备号后需要将设备号是怎样与字符设备进行关联。
注册字符设备可以分为两个步骤:
1.字符设备初始化
字符设备初始化所用到的函数为cdev_init(…),在对该函数讲解之前,首先对cdev结构体进行介绍,Linux 内核中将字符设备抽象成一个具体的数据结构 (struct cdev), 我们可以理解为字符设备对象,cdev 记录了字符设备号、内核对象、文件操作file_operations结构体(设备的打开、读写、关闭等操作接口)等信息。
struct cdev { struct kobject kobj; //内嵌的内核对象.struct module *owner; //该字符设备所在的内核模块的对象指针.const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
设备初始化所用到的函数为cdev_init(),该函数同样在“内核源码/include/linux/cdev.h”文件中所引用如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{memset(cdev, 0, sizeof *cdev);//将整个结构体清零;INIT_LIST_HEAD(&cdev->list);//初始化list成员使其指向自身;kobject_init(&cdev->kobj, &ktype_cdev_default);//初始化kobj成员;cdev->ops = fops;//初始化ops成员,建立cdev 和 file_operations之间的连接
}
2.字符设备的添加
字符设备添加所用到的函数为cdev_add(),该函数在“内核源码/include/linux/cdev.h”文件中所引用:
int cdev_add(struct cdev *, dev_t, unsigned);
代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>static dev_t dev_num;//定义dev_t类型(32位大小)的变量dev_num,用来存放设备号
struct cdev cdev_test;//定义cdev结构体类型的变量cdev_test
struct file_operations cdev_test_ops{.owner=THIS_MODULE//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
};//定义file_operations结构体类型的变量cdev_test_opsstatic int __init module_cdev_init(void)//驱动入口函数
{int ret;//定义int类型变量ret,进行函数返回值判断int major,minor;//定义int类型的主设备号major和次设备号minorret = alloc_chrdev_region(&dev_num,0,1,"chrdev_name");//自动获取设备号,设备名chrdev_nameif (ret < 0){printk("alloc_chrdev_region is error\n");}printk("alloc_register_region is ok\n");major = MAJOR(dev_num);//使用MAJOR()函数获取主设备号minor = MINOR(dev_num);//使用MINOR()函数获取次设备号printk("major is %d\n",major);printk("minor is %d\n",minor);
cdev_init(&cdev_test,&cdev_test_ops);//使用cdev_init()函数初始化cdev_test结构体,并链接到cdev_test_ops结构体
cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块 ret = cdev_add(&cdev_test,dev_num,1);//使用cdev_add()函数进行字符设备的添加if(ret < 0 ){printk("cdev_add is error\n");}printk("cdev_add is ok\n");return 0;
}static void __exit module_cdev_exit(void)//驱动出口函数
{cdev_del(&cdev_test);//使用cdev_del()函数进行字符设备的删除unregister_chrdev_region(dev_num,1);//释放字符驱动设备号 printk("module exit \n");
}module_init(module_cdev_init);//注册入口函数
module_exit(module_cdev_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("topeet"); //作者信息
创建设备节点
已经成功的申请了设备号并且注册了相应的字符设备。系统通过设备号对设备进行查找,而字符设备注册到内核之后,并不能直接进行设备文件操作命令(打开、关闭、读、写等),需要相应的设备文件作为桥梁以此来进行设备的访问
在Linux操作系统中一切皆文件,设备访问也是通过文件的方式来进行的,对于用来进行设备访问的文件称之为设备节点,设备节点被创建在/dev目录下,将内核中注册的设备与用户层进行链接,这样应用程序才能对设备进行访问。
1、手动创建设备节点
使用mknod命令手动创建设备节点,mknod 命令格式为:mknod NAME TYPE MAJOR MINOR
例如使用以下命令创建一个名为device_test的字符设备节点,链接设备的主设备号和从设备号分别为236和0:mknod /dev/device_test c 236 0
2、自动创建设备节点
设备文件的自动创建是利用 udev(mdev)机制来实现, 多数情况下采用自动创建设备节点的方式。 udev(mdev)可以检测系统中硬件设备状态, 可以根据系统中硬件设备状态来创建或者删除设备文件。
在驱动中首先使用 class_create(…)函数对 class 进行创建, 这个类存放于
/sys/class/ 目录下, 之后使用 device_create(…)函数创建相应的设备, 在进行模块加载时, 用户空间中的 udev 会自动响应 device_create()函数,寻找对应的类从而创建设备节点。
class_create(…)函数
// 用于动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加进Linux内核系统。
//owner:struct module结构体类型的指针,指向函数即将创建的这个struct
class的模块。一般赋值为THIS_MODULE
//name:char类型的指针,代表即将创建的struct class变量的名字
//返回值:struct class * 类型的结构体#define class_create(owner, name) ({ static struct lock_class_key __key; \__class_create(owner, name, &__key); \ })
class_destroy(…)函数
//用于删除设备的逻辑类,即从Linux内核系统中删除设备的逻辑类
extern void class_destroy(struct class *cls);
device_create(…)函数
/*
cls:指定所要创建的设备所从属的类。parent:指定该设备的父设备,如果没有就指定为NULL。
devt:指定创建设备的设备号。
drvdata:被添加到该设备回调的数据,没有则指定为NULL。
fmt:添加到系统的设备节点名称。
返回值:struct device * 类型结构体
*/struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata,const char *fmt, ...);
device_destroy(…)函数
// extern void device_destroy(struct class *cls, dev_t devt); extern void device_destroy(struct class *cls, dev_t devt);
代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>static dev_t dev_num;//定义dev_t类型变量dev_num来表示设备号
struct cdev cdev_test;//定义struct cdev 类型结构体变量cdev_test,表示要注册的字符设备
struct file_operations cdev_fops_test = {.owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
};//定义file_operations结构体类型的变量cdev_test_ops
struct class *class_test;//定于struct class *类型结构体变量class_test,表示要创建的类static int __init chrdev_fops_init(void)//驱动入口函数
{int ret;//定义int类型的变量ret,用来对函数返回值进行判断int major,minor;//定义int类型的主设备号major和次设备号minorret = alloc_chrdev_region(&dev_num,0,1,"chrdev_name");//自动获取设备号,设备名chrdev_nameif (ret < 0){printk("alloc_chrdev_region is error \n");}printk("alloc_chrdev_region is ok \n");major = MAJOR(dev_num);//使用MAJOR()函数获取主设备号minor = MINOR(dev_num);//使用MINOR()函数获取次设备号printk("major is %d\n minor is %d \n",major,minor);cdev_init(&cdev_test,&cdev_fops_test);//使用cdev_init()函数初始化cdev_test结构体,并链接到cdev_test_ops结构>体cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块ret = cdev_add(&cdev_test,dev_num,1); //使用cdev_add()函数进行字符设备的添加if (ret < 0){printk("cdev_add is error \n");}printk("cdev_add is ok \n");class_test = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_testdevice_create(class_test,NULL,dev_num,NULL,"device_test");//使用device_create进行设备的创建,设备名称为device_testreturn 0;
}static void __exit chrdev_fops_exit(void)//驱动出口函数
{cdev_del(&cdev_test);//删除添加的字符设备cdev_testunregister_chrdev_region(dev_num,1);//释放字符设备所申请的设备号device_destroy(class_test,dev_num);//删除创建的设备class_destroy(class_test);//删除创建的类printk("module exit \n");
}module_init(chrdev_fops_init);//注册入口函数
module_exit(chrdev_fops_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("topeet");//作者信息
字符设备驱动框架
三个步骤:
首先驱动向Linux内核进行设备号申请。
字符设备注册
设备节节点来充当内核和用户层通信的桥梁
在进行注册字符设备实验章节中,使用cdev_init(…)函数对struct cdev结构体类型变量和struct file_operations结构体类型变量相链接,struct file_operations结构体就是把系统调用和驱动程序关联起来的关键数据结构。
该结构体的每一个成员都对应着一个系统调用,读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。
部分常用函数的 file_operations结构体如下:
static struct file_operations cdev_fops_test = {.owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块.open = chrdev_open,//将open字段指向chrdev_open(...)函数.read = chrdev_read,//将open字段指向chrdev_read(...)函数.write = chrdev_write,//将open字段指向chrdev_write(...)函数.release = chrdev_release,//将open字段指向chrdev_release(...)函数
};//定义file_operations结构体类型的变量cdev_test_ops
完整的字符设备驱动代码:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>static int chrdev_open(struct inode *inode, struct file *file)
{printk("This is chrdev_open \n");return 0;
}static ssize_t chrdev_read(struct file *file,char __user *buf, size_t size, loff_t *off)
{printk("This is chrdev_read \n");return 0;
}static ssize_t chrdev_write(struct file *file,const char __user *buf,size_t size,loff_t *off)
{printk("This is chrdev_write \n");return 0;
}
static int chrdev_release(struct inode *inode, struct file *file)
{return 0;
}static dev_t dev_num;//定义dev_t类型变量dev_num来表示设备号
static struct cdev cdev_test;//定义struct cdev 类型结构体变量cdev_test,表示要注册的字符设备
static struct file_operations cdev_fops_test = {.owner = THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块.open = chrdev_open,//将open字段指向chrdev_open(...)函数.read = chrdev_read,//将open字段指向chrdev_read(...)函数.write = chrdev_write,//将open字段指向chrdev_write(...)函数.release = chrdev_release,//将open字段指向chrdev_release(...)函数
};//定义file_operations结构体类型的变量cdev_test_opsstatic struct class *class_test;//定于struct class *类型结构体变量class_test,表示要创建的类static int __init chrdev_fops_init(void)//驱动入口函数
{int ret;//定义int类型的变量ret,用来对函数返回值进行判断int major,minor;//定义int类型的主设备号major和次设备号minorret = alloc_chrdev_region(&dev_num,0,1,"chrdev_name");//自动获取设备号,设备名chrdev_nameif (ret < 0){printk("alloc_chrdev_region is error \n");}printk("alloc_chrdev_region is ok \n");major = MAJOR(dev_num);//使用MAJOR()函数获取主设备号minor = MINOR(dev_num);//使用MINOR()函数获取次设备号printk("major is %d\n minor is %d \n",major,minor);cdev_init(&cdev_test,&cdev_fops_test);//使用cdev_init()函数初始化cdev_test结构体,并链接到cdev_test_ops结构体cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块ret = cdev_add(&cdev_test,dev_num,1); //使用cdev_add()函数进行字符设备的添加if (ret < 0){printk("cdev_add is error \n");}printk("cdev_add is ok \n"); class_test = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_testdevice_create(class_test,NULL,dev_num,NULL,"device_test");//使用device_create进行设备的创建,设备名称为device_testreturn 0;
}static void __exit chrdev_fops_exit(void)//驱动出口函数
{device_destroy(class_test,dev_num);//删除创建的设备
class_destroy(class_test);//删除创建的类cdev_del(&cdev_test);//删除添加的字符设备cdev_testunregister_chrdev_region(dev_num,1);//释放字符设备所申请的设备号printk("module exit \n");
}module_init(chrdev_fops_init);//注册入口函数
module_exit(chrdev_fops_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("topeet");//作者信息
app.c代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int main(int argc,char *argv[])
{int fd;//定义int类型的文件描述符char buf[32];//定义读取缓冲区buffd=open(argv[1],O_RDWR,0666);//调用open函数,打开输入的第一个参数文件,权限为可读可写if(fd<0){printf("open is error\n");return -1;}printf("open is ok\n");/*如果第二个参数为read,条件成立,调用read函数,对文件进行读取*/ if(!strcmp(argv[2], "read")){read(fd,buf,32);}/*如果第二个参数为write,条件成立,调用write函数,对文件进行写入*/ else if(!strcmp(argv[2], "write")){write(fd,"hello\n",6);}
close(fd);//调用close函数,对取消文件描述符到文件的映射return 0;
}