一、前言
字符设备是Linux 驱动中最重要的设备之一,其能够像字节流一样被访问,也就是说对它的操作以字节为单位来实现读写等操作。比如最常见的点灯、按键、IIC、SPI、LCD等都是字符设备,这些设备的驱动就叫做字符设备驱动。字符设备的驱动程序实现了open、close、read、write等系统调用,应用程序可以通过设备文件(/dev/xxx,其中xxx为该字符设备文件,例如/dev/led)来访问字符设备。在Linux中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
二、目的
本文简单的介绍Linux字符设备驱动的开发步骤与流程。如果学过裸机或者STM32都知道对某个设备模块就是要初始化相关的外设寄存器等。在Linux内核下开发字符设备驱动仍是按初始化相关外设寄存器的步骤流程或者说框架来编写驱动。重点是要掌握其驱动开发框架。
三、开发流程与介绍
Linux的设备驱动程序大致可以分为如下几个部分:驱动模块的加载与卸载、驱动程序的注册与注销、设备的打开与释放、设备的读写操作、设备的控制操作、设备的中断和轮询处理等。以字符设备驱动开发为例,其开发流程模型与工作关联分别如下图所示:


1.Linux内核中
①使用cdev结构体来描述字符设备。
②通过cdev的成员dev_t来定义设备号(主、次设备号)来确定字符设备的唯一性。
③通过cdev的成员file_operations来定义字符设备驱动提供的VFS的函数接口,如open( ),read( ),write( )等。
2.Linux字符设备驱动中
①模块加载函数通过register_chrdev_region( )或alloc_chrdev_region( )来静态或动态获取设备号。
②通过cdev_init( )建立cdev与file_opreations之间的联系,通过cdev_add( )向系统添加一个cdev以完成注册。
③模块卸载通过cdev_del( )来注销cdev,通过unregister_chrdev_region( )来释放设备号。
3.字符设备驱动加载
当字符设备驱动被添加到内核后,用户空间的应用程序通过系统调用可以间接访问到字符设备驱动的read、write、ioctl等底层驱动函数。
4.用户空间访问该设备程序
通过Linux系统调用,如open( ),read( ),write( ),来调用file_opreations来定义字符设备提供给VFS的接口函数。
5.file_operations对设备的读写操作
由于用户空间不能直接访问内核空间的内存,因此借助了函数**copy_from_user( )来完成用户空间缓冲区到内核空间的复制,以及copy_to_user( )**来完成内核空间到用户空间缓冲区的复制。
四、驱动模块的加载与卸载
Linux驱动有两种运行方式,第一种就是将驱动编译到内核中去,这样当Linux内核在启动运行过程的时候自动加载驱动程序。第二种将驱动程序编译成模块(在linux下模块的扩展名为.ko),该驱动模块需要在Linux内核启动以后使用“insmod”命令加载。一般在调试驱动的时候都选择将其编译成模块,这样的好处是当修改代码后只需要编译一下驱动代码即可,从而不需要编译整个内核代码。
模块有加载和卸载两种操作,需要编写这两个部分的代码。函数如下:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数
1.module_init函数
用来向 Linux内核注册一个模块加载函数,参数 xxx_init就是需要注册的具体函数,当使用“ insmod”命令加载驱动的时候 xxx_init这个函数就会被调 用。
2.module_exit()函数
用来向 Linux内核注册一个模块卸载函数,参数 xxx_exit就是需要注册的具体函数,当使用“ rmmod”命令卸载具体驱动的时候 xxx_exit函数就会被调用。
3.驱动模块加载和卸载模板
驱动模块加载和卸载模板实现如下所示:
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
/* 添加LICENSE和作者信息 */
MODULE_LICENSE("GPL"); //必须添加模块LICENSE信息,否则的话编译的时候会报错
MODULE_AUTHOR("Yimning"); //添加模块作者信息
五、字符设备的申请与注销设备号
当字符设备驱动模块加载的时候,往往也需要初始化或者注册字符设备号,同样卸载驱动模块的时候也需要注销字符设备号。
1.申请设备号的注册函数原型
/*
* @description : 函数用于静态申请字符设备号
* @params :
* unsigned int major : 主设备号
* const char *name : 设备名字,指向一串字符串
* const struct file_operations *fops : 结构体file_operations类型指针,指向设备的操作函数集合变量
* @return : int
*/
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
/*
* @description : 函数用于动态申请字符设备号
* @params :
* dev_t *dev : 主设备号
* unsigned baseminor : 次设备号从0开始
* unsigned count : 申请设备号个数
* const char *name :设备名字,指向一串字符串
* @return : int
*/
static inline int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
①register_chrdev_region( )函数用于已知起始设备的设备号的情况;即向系统静态申请设备号。
②alloc_chrdev_region( )函数用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放入第一个参数dev中。即向系统动态申请设备号。
③alloc_chrdev_region( )相比于register_chrdev_region( )的优点在于它会自动避开设备号重复的冲突。
2.删除设备号的注销函数原型
/*
* @description : 函数用于注销字符设备号
* @params :
* unsigned int major : 要注销的设备对应的主设备号
* const char *name : 要注销的设备对应的设备名
* @return : 无
*/
static inline void unregister_chrdev(unsigned int major, const char *name)
六、字符设备cdev注册与注销
1.cdev结构体
在 Linux中使用 cdev结构体表示一个字符设备,cdev结构体在include/linux/cdev.h文件中的定义如下:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops; //字符设备文件操作函数集合file_operations
struct list_head list;
dev_t dev; //以及设备号 dev_t
unsigned int count;
};
该函数主要对struct cdev结构体做初始化,最重要的就是建立cdev和file_operations之间的连接:
①将整个结构体清零;
②初始化list成员使其指向自身;
③初始化kobj成员;
④初始化ops成员;
2.cdev_init函数
/*
* @description : 函数用于建立cdev和file_operations之间的连接
* @params :
* struct cdev *cdev : 设备号
* const struct file_operations *fops : 字符设备文件操作函数集合
* @return : 无
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
①建立cdev和file_operations之间的连接。
3.cdev_add函数
/*
* @description : 函数用于注册cdev设备
* @params :
* struct cdev *p : 添加的字符设备(cdev结构体变量)
* dev_t dev : 设备所使用的设备号
* unsigned count : 添加的设备数量
* @return : int
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
4.cdev_del函数
卸载驱动的时候一定要使用cdev_del函数从Linux内核中删除相应的字符设备。
void cdev_del(struct cdev *p)
①参数struct cdev *p指向要删除的字符设备。
七、创建设备节点
驱动加载成功需要在/dev目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。设备节点的创建方式分有两种:手动创建设备节点和自动创建设备节点。
1.手动创建设备节点
在Linux中,可以通过 “mknod” 命令来手动创建设备节点,该命令的格式为:
mknod filename type(设备类型) major(主设备号) minor(次设备号)
例如:在/dev 目录下创建xxx设备节点
mknod /dev/xxx c 200 0
其中,/dev/xxx 是要创建的设备节点文件;“c” 表示字符设备;“200” 是设备的主设备号;“0” 是设备的次设备号。
2.自动创建设备节点
利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。udev来实现设备文件的创建与删除,udev可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用
modprobe命令成功加载驱动模块以后就自动在/dev目录下创建对应的设备节点文件,使用rmmod命令卸载驱动模块以后就删除掉/dev目录下的设备节点文件。Linux系统中的热插拔事件也由mdev管理,在/etc/init.d/rcS文件中有如下语句:
echo /sbin/mdev > /proc/sys/kernel/hotplug
具体udev相关知识这里不详细阐述。这里以第二种方式来介绍。
八、创建和摧毁类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在cdev_add函数后面添加自动创建设备节点相关代码。出口函数中需要删除掉类。
1.class_create函数
struct class *class_create (struct module *owner, const char *name)
①参数owner一般为THIS_MODULE
②参数name是类名字。
③返回值是个指向结构体class的指针,也就是创建的类。
2.class_destroy函数
void class_destroy(struct class *cls);
①参数cls就是要删除的类。
九、创建和摧毁设备
在驱动入口函数里面创建设备,在驱动出口函数里面删除设备。
1.device_create函数
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
①device_create是个可变参数函数;
②参数class就是设备要创建哪个类下面;
③参数parent是父设备,一般为 NULL,也就是没有父设备;
④参数devt是设备号;
⑤参数drvdata是设备可能会使用的一些数据,一般为 NULL;
⑥参数fmt是设备名字,如果设置 fmt=xxx的话,就会生成/dev/xxx这个设备文件;
⑦返回值就是创建好的设备。
2.device_destroy函数
void device_destroy(struct class *class, dev_t devt)
①参数class是要删除的设备所处的类;
②参数 devt是要删除的设备号。
十、字符设备驱动程序代码框架
一般字符设备的注册在驱动模块的入口函数xxx_init中进行,字符设备的注销在驱动模块的出口函数xxx_exit中进行。字符设备的注册和注销示例代码内容如下所示:
#define DEVBASE_CNT 1 /* 设备号个数 */
#define DEVBASE_NAME "chrdevbase" /* 设备名 */
struct xxx_dev
{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct xxx_dev xxx; /* led结构体 */
//static struct file_operations xxx_fops; /* 设备的操作函数集合,包含打开、关闭、读写等操作*/
static struct file_operations xxx_fops = {
... /* 需要实现函数,此处略 */
};
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
int retvalue = 0;
/* 注册字符设备驱动 */
/* 创建设备号 */
if (xxx.major) { /* 定义了设备号 */
xxx.devid = MKDEV(xxx.major, 0);
retvalue = register_chrdev_region(xxx.devid, DEVBASE_CNT, DEVBASE_NAME);
} else { /* 没有定义设备号 */
retvalue= alloc_chrdev_region(&xxx.devid, 0, DEVBASE_CNT, DEVBASE_NAME); /* 申请设备号 */
xxx.major = MAJOR(xxx.devid); /* 获取分配号的主设备号 */
xxx.minor = MINOR(xxx.devid); /* 获取分配号的次设备号 */
}
if(retvalue < 0){
/* 字符设备注册失败,自行处理 */
}
/* 初始化cdev */
xxx.cdev.owner = THIS_MODULE;
cdev_init(&xxx.cdev, &xxx_fops);
/* 添加一个cdev */
cdev_add(&xxx.cdev, xxx.devid, DEVBASE_CNT);
/* 创建类 */
xxx.class = class_create(THIS_MODULE, DEVBASE_NAME);
if (IS_ERR(xxx.class)) {
return PTR_ERR(xxx.class);
}
/* 创建设备 */
xxx.device = device_create(xxx.class, NULL, xxx.devid, NULL, DEVBASE_NAME);
if (IS_ERR(xxx.device)) {
return PTR_ERR(xxx.device);
}
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 注销字符设备驱动 */
cdev_del(&xxx.cdev);/* 删除cdev */
unregister_chrdev_region(xxx.devid, DEVBASE_CNT); /* 注销设备号 */
device_destroy(xxx.class, xxx.devid);
class_destroy(xxx.class);
}
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
/* 添加LICENSE和作者信息 */
MODULE_LICENSE("GPL"); //必须添加模块LICENSE信息,否则的话编译的时候会报错
MODULE_AUTHOR("Yimning"); //添加模块作者信息
示例代码中定义了一个file_operations结构体变量xxx_fops,是设备的操作函数集合,只是此时尚未初始化xxx_fops中的open、release等这些成员变量,所以这个操作函数集合还暂时是空的。
待续未完!!!下期将接着继续介绍有关Linux字符设备驱动开发。
感谢阅读与分享!关注我们世间万物,千奇百怪,都等待着你去发觉…
关注“嵌入式IOT杂谈”以查看更多分享,欢迎点分享、收藏、点赞、在看
微信搜一搜关注该公众号微信搜一搜关注该公众号微信搜一搜关注该公众号
本文介绍了Linux字符设备驱动的开发流程,包括设备号的申请与注销、cdev的注册与注销、创建设备节点和销毁类。通过module_init和module_exit实现模块加载与卸载,使用file_operations定义设备的读写操作。
713

被折叠的 条评论
为什么被折叠?



