=========================================================================
以下是自我的理解
=========================================================================
要理解设备驱动开发,首先要理清几个大思路:
- 设备驱动是内核的一部分,而内核的套路就是先注册才能使用,因此就有注册接口。
- 向内核注册之后,也要能被用户所使用。而要被使用,就得提供接口出来给用户。
- 同样也要提供使用方式。
- 内核就作为一个中间层,即接收设备驱动的注册,又提供公共接口给用户。完美的中间层。
- 使用接口一般就是/dev目录下的设备节点文件。
- 使用方式就是file_operations结构体中的操作函数(即ops操作函数)。
=========================================================================
在理解了这几个思路之后,剩下的就是满足内核的具体套路(以字符型设备驱动为例):
(1)首先是注册进内核。
- 既然是字符型设备,那必然有对应的结构体描述:“struct cdev”。
- 驱动的所有内容,都是围绕着这个结构体在进行。
- 既然是结构体,那就有相应的初始化。
- 调用cdev_init()函数可以初始化cdev结构体
- cdev 结构体的初始化主要是包括 ops 操作函数 和 owner 拥有者 的初始化。
===============================
- 同样,既然是设备,想要编写驱动,按内核套路来说,就需要有一个设备号去供内核识别。
- 而获取这个设备号,有两种方式:动态分配获取(调用alloc_chrdev_region() 函数) 和 静态分配获取(调用register_chrdev_region() 函数,需要自定义主设备号)。
===============================
- 有了 设备号 和 描述结构体 后,就要将这两者关联起来,并注册进内核成为内核一部分。
- 可以调用 cdev_add()函数,将设备号 和 描述结构体 进行关联并注册。
(2)然后提供出用户接口 及 使用方式 给用户使用。
- 使用方式就是上面所提到的 file_operations 结构体成员。
- 而使用接口这一部分其实与文件系统强关联。
- 有提供两种方式可以满足目的,mknod手动创建 和 udev的自动创建。
- 对于udev自动创建,内核的套路就是以下描述。
- 调用 class_create() 函数创建一个设备类,在 /sys/class下生成设备目录。
- 调用 device_create() 函数在 /dev/ 目录和 /sys/class/xxx 目录下生成设备文件接口 xxx。
(3)至此,驱动的两大要求基本完成。
=========================================================================
以下是一些具体的描述
=========================================================================
驱动中对 udev (自动创建设备节点)的主要做法是(以字符驱动为例)在驱动初始化代码里:
- 调用 register_chrdev_region() 或 alloc_chrdev_region() 来静态或者动态获取设备号 ->
- 调用 cdev_init() 初始化设备号为cdev结构,并建立cdev与file_operations之间的连接 ->
- 调用 cdev_add() 向系统添加一个cdev,以完成字符设备注册 ->
- 调用 class_create() 为该设备创建一个 class,在/sys/class/下生成设备信息 ->
- 调用 device_create() 创建对应设备
当然,也可以利用mknod命令手动创建设备节点。
=========================================================================
- 内核中定义了 struct class 结构体,一个 struct class 结构体类型的变量对应一个驱动类。
- 内核同时提供了 class_create() 函数,可以用它来创建一个类(class)。
- 这个类(class)存放在 sysfs 下面,一旦调用class_create() 函数创建好了这个类,再调用 device_create() 函数,就会在 /sys/class/ 下生成设备信息目录。
- 这样在启动加载模块的时候,用户空间中的 udev 会自动响应 device_create() 函数,去 /sysfs 下寻找对应的类,从而创建设备节点。
=========================================================================
- 内核中也提供了另一个字符型设备的注册函数
-
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops)
- 第一个参数 major 为主设备号,0代表动态分配;第二个参数 name 是设备的名字;第三个参数 fops 为文件操作指针。
- 此函数 返回0 表示成功。返回-EINVAL 表示申请的主设备号非法,一般来说是主设备号大于系统所允许的最大设备号。返回-EBUSY 表示所申请的主设备号正在被其它设备驱动程序使用。
- 如果是动态分配主设备号 成功,此函数将 返回所分配的主设备号。
- 如果register_chrdev操作成功,设备名就会出现在 /proc/devices文件里。
- 在register_chrdev函数中,不只是注册了设备号,还做了 cdev 的初始化以及 cdev 的注册。(简单来说:register_chrdev() 中包含了cdev_init() 和 cdev_add() 所要做的事情)
- module在被加载时,udev 就会自动在/dev下创建设备节点文件。
- 例如:register_chrdev(MEM_MAJOR,"mem",&memory_fops) 向内核注册一个字符设备,在完成注册后,/proc/devices 中就能看到:/mem。
=========================================================================
注意
- 从linux内核2.6的某个版本之后,devfs不复存在,udev成为devfs的替代。相比devfs,udev有很多优势。udev属于应用层,不要试图在内核的配置选项里找到它。
- 加入对udev的支持很简单,以作者所写的一个字符设备驱动为例,在驱动初始化的代码里调用class_create为该设备创建一个class,再为每个设备调用 class_device_create创建对应的设备。
-
注意,在2.6较早内核版本中,device_create(…)函数名称不同,叫class_device_create(…),所以在新的内核中编译以前的模块程序有时会报错,就是因为函数名称不同,而且里面的参数设置也有一些变化。
-
struct class和device_create(…) 以及device_create(…)都定义在/include/linux/device.h中,使用的时候一定要包含这个头文件,否则编译器会报错。
-
在2.6.26.6内核版本中,struct class定义在头文件include/linux/device.h中。
-
(手动创建设备节点)在成功的向系统注册了设备驱动程序后(调用register_chrdev()成功后),就可以用mknod命令来把设备在/dev目录下映射为一个设备节点文件。其它程序使用这个设备的时候,只要对在/dev目录下的此设备节点文件进行操作就行了。
-
register_chrdev()是向系统注册设备。注册设备时包括分配设备号,为设备分配资源等,添加设备等;cdev_add()主要是向系统添加设备,在register_chrdev()的时候会调用到该函数。
=========================================================================
举例
- class_create() 和 class_destroy() 使用示例
(1)通过class_create()、class_destroy()去注册和注销 /sys/class/test_class
(2)class_create动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加到内核中。
(3)创建的逻辑类位于/sys/class/,文件夹里面的内容仍然为空。
/*
* test-debug-scr.c
*
* Copyright (C) 2012 - 2021 Reuuimlla Limited
*
* Adapt to support xxx
*/
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/vmalloc.h>
#include <linux/device.h>
/*-------------------------------------------------------------------------------*/
/* DEFINITION */
/*-------------------------------------------------------------------------------*/
struct class *mem_class;
/
/*
* @description : 驱动入口函数
* @param : 无
* @return : 无
*/
// 参数:
// owner, 拥有者。一般赋值为THIS_MODULE。12
// name, 创建的逻辑类的名称。13
static int __init class_create_init(void)
{
mem_class = class_create(THIS_MODULE, "test_class");
if (mem_class==NULL)
{
printk("--create class failed!--\n");
return -1;
}
printk("--create class ok!--\n");
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit class_create_destroy(void)
{
if (mem_class != NULL)
{
class_destroy(mem_class);
mem_class = NULL;
}
return;
}
module_init(class_create_init);
module_exit(class_create_destroy);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("test-debug-scr generic test driver");
MODULE_AUTHOR("xxxx");
编译并insmod后,在 /sys/clas/ 目录下就会生成 “ test_class ”目录,但因为只是创建了设备类,并没有注册设备且分配设备设备好,因此该目录仍然为空。
- device_create() 使用示例
(1)通过alloc_chrdev_region() 动态分配主设备和从设备号
(2)通过 MAJOR(),MINOR()分配设备号,通过MKDEV()将设备号转换为dev_t类型。
(3)通过 cdev_init(),cdev_add()注册字符设备。
(4)通过class_create() 和 class_destroy()去注册和注销 /sys/class/test_class
(5)class_create动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加到内核中。
(6)通过 device_create()在/dev/目录和/sys/class/test目录下分别创建设备文件 test_class。
(7)创建的逻辑类位于/sys/class/,文件夹里面已经有分配好的设备号和设备名。
/*
* test-debug-scr.c
*
* Copyright (C) 2012 - 2021 Reuuimlla Limited
*
* Adapt to support xxx
*/
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/ioctl.h>
#include <linux/uaccess.h>
#include <linux/string.h>
#include <linux/wait.h>
#include <linux/types.h>
#include <linux/proc_fs.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
/*-------------------------------------------------------------------------------*/
/* DEFINITION */
/*-------------------------------------------------------------------------------*/
/*主设备和从设备号变量*/
static int test_major = 0;
static int test_minor = 0;
/*设备类别和设备变量*/
static struct cdev cdev;
static struct class *test_class = NULL;
static struct device *test_class_dev = NULL;
#define TEST_DEVICE_CNT 1
#define TEST_DEVICE_NAME "test_class"
dev_t dev = 0;
/* 打开设备方法,空实现*/
static int test_open(struct inode *inode, struct file *filp) {
return 0;
}
/* 关闭设备方法,空实现*/
static int test_release(struct inode *inode, struct file *filp) {
return 0;
}
/* 读取设备数据,空实现*/
static ssize_t test_read(struct file *file, char __user *buffer,size_t count, loff_t *f_pos)
{
return 0;
}
/* 写入设备数据,空实现*/
static ssize_t test_write(struct file *file, const char __user *buffer,size_t count, loff_t *f_pos)
{
return 0;
}
/* 设备文件操作列表 */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = test_open,
.release = test_release,
.read = test_read,
.write = test_write,
};
/
/*
* @description : 驱动入口函数
* @param : 无
* @return : 无
*/
static int __init class_create_init(void)
{
int err = -1;
dev_t devno = 0;
/* 通过register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号*/
if (test_major)
{
devno = MKDEV(test_major, test_minor);/* 将主设备号和次设备号转换成dev_t类型 */
register_chrdev_region(devno, TEST_DEVICE_CNT, TEST_DEVICE_NAME);
}
else
{
err = alloc_chrdev_region(&devno, 0, TEST_DEVICE_CNT, TEST_DEVICE_NAME);
if(err < 0) {
printk("Failed to alloc char dev region.\n");
return err;
}
// 保存并打印一下主设备号与从设备号
test_major = MAJOR(devno);
test_minor = MINOR(devno);
printk("--test_major: %d ; test_minor:%d--\n", test_major, test_minor);
}
/* 注册字符设备 */
cdev_init (&cdev, &test_fops);
cdev.owner = THIS_MODULE;
cdev.ops = &test_fops;
int error = cdev_add (&cdev, devno, 1);
if(error)
printk("Err %d adding test_setup_cdev", error);
/* 在/sys/class/目录下创建设备类 test_class 目录 */
test_class = class_create(THIS_MODULE, "test_class");
if(IS_ERR(test_class))
{
printk("Err: failed in creating class.\n");
return -1;
}
/* 在/dev/目录和/sys/class/test目录下分别创建设备文件test_class */
test_class_dev = device_create(test_class, NULL, devno, NULL, "test_class");
if(IS_ERR(test_class_dev))
{
printk("Err: failed in creating device---\n");
return -1;
}
printk("---Succedded to initialize test device---\n");
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit class_create_destroy(void)
{
dev_t devno = MKDEV (test_major, test_minor);
cdev_del(&cdev);
device_destroy(test_class, devno);
class_destroy(test_class);
unregister_chrdev_region (devno, TEST_DEVICE_NAME);
printk ("Char driver test_exit.\n");
}
module_init(class_create_init);
module_exit(class_create_destroy);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("test-debug-scr generic test driver");
MODULE_AUTHOR("xxxx");
编译并insmod后:
在 /sys/clas/ 目录下就会生成 test_class 目录
并在/dev目录下生成 test_class 设备节点
如图所示:
lsmod后可以看到 “ test_debug ” 模块已经加载进内核。
在 /sys/class 目录下生成了 test_class 目录,且test_class目录下有关于设备的各类文件。
查看 /sys/class/test_class/目录下的uevent 文件内容,有主设备号,次设备号 ,及设备节点名。与代码中注册的基本一致。