在这里首先要说一下,之所以称作新写法,是针对我之前的一篇文章—如何开始编写你的第一个字符设备驱动?来说的。今天要说的新的写法,和之前比,主要变化在于:
- 分配和释放设备号的方式
- 字符设备注册的方式
- 不再需要手动创建设备节点
好了,下面就来一个一个的说。
一、分配和释放设备号之前我们使用 register_chrdev
函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来两个问题:
- 需要我们事先确定好哪些主设备号没有使用。
- 会将一个主设备号下的所有次设备号都使用掉,比如现在设置
LED
这个主设备号为200
,那么0~1048575(2^20-1)
这个区间的次设备号就全部都被LED
一个设备分走了。这样太浪费次设备号了!一个LED
设备肯定只能有一个主设备号,一个次设备号。
解决这两个问题最好的方法就是要使用设备号的时候向 Linux 内核申请,需要几个就申请几个,由 Linux 内核分配设备可以使用的设备号。
这里又分两种情况:
- 如果没有指定设备号的话就使用如下函数来申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
- 如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数 from
是要申请的起始设备号,也就是给定的设备号;参数 count
是要申请的数量,一般都是一个;参数 name
是设备名字。
注 销 字 符 设 备 之 后 要 释 放 掉 设 备 号 , 不 管 是 通 过 alloc_chrdev_region
函 数 还 是register_chrdev_region
函数申请的设备号,统一使用如下释放函数:
void unregister_chrdev_region(dev_t from, unsigned count)
下面举一个例子,来实际看一下这两个函数是怎么用的。注意,下面例子中的这种写法是推荐的,它考虑到了指定设备号和不指定设备号两种情况。
1 int major; /* 主设备号 */
2 int minor; /* 次设备号 */
3 dev_t devid; /* 设备号 */
4
5 if (major) { /*定义了主设备号*/
6 devid = MKDEV(major, 0); /* 大部分驱动次设备号都选择 0 */
7 register_chrdev_region(devid, 1, "test");
8 } else { /* 没有定义设备号*/
9 alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */
10 major = MAJOR(devid); /* 获取分配号的主设备号 */
11 minor = MINOR(devid); /* 获取分配号的次设备号 */
12 }
- 第 1~3 行,定义了主/次设备号变量
major
和minor
,以及设备号变量devid
。 - 第 5 行,判断主设备号
major
是否有效,在 Linux 驱动中一般给出主设备号的话就表示这个设备的设备号已经确定了,因为次设备号基本上都选择0
,这算个 Linux 驱动开发中约定俗成的一种规定了。 - 第 6 行,如果
major
有效的话就使用MKDEV
来构建设备号,次设备号选择0
。 - 第 7 行,使用
register_chrdev_region
函数来注册设备号。 - 第 9~11 行,如果
major
无效,那就表示没有给定设备号。此时就要用alloc_chrdev_region
函数来申请设备号。设备号申请成功以后使用MAJOR
和MINOR
来提取出主设备号和次设备号,当然了,第 10 和 11 行提取主设备号和次设备号的代码可以不要。
如果要注销设备号的话,使用如下代码即可:
1 unregister_chrdev_region(devid, 1);/* 注销设备号 */
二、新的字符设备注册方式
1、字符设备结构体
首先,我们介绍一个字符设备结构体
,每一个字符设备都可以使用一个结构体来表示,然后每一个结构体里可以包含这个字符设备的一些基本信息。在Linux 中使用cdev
结构体表示一个字符设备,cdev
结构体在内核源码的顶层路径下的include/linux/cdev.h
文件中的定义如下:
1 struct cdev {
2 struct kobject kobj;
3 struct module *owner;
4 const struct file_operations *ops;
5 struct list_head list;
6 dev_t dev;
7 unsigned int count;
8 };
在 cdev
中有两个重要的成员变量:ops
和 dev
,这两个就是字符设备文件操作函数集合file_operations
以及设备号 dev_t
。编写字符设备驱动之前需要定义一个cdev
结构体变量,这个变量就表示一个字符设备,如下所示:
struct cdev test_cdev;
定义好一个具体的结构体之后,那么此时它就代表一个字符设备。我们看到这个结构体有很多属性,我们可以直接给它们赋值,也可以使用下面将要介绍的cdev_init
函数进行初始化。但是要注意的是,这个函数只对成员变量ops
进行初始化。
定义好 cdev
变量以后就要使用 cdev_init
函数对其进行初始化,cdev_init
函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev
就是要初始化的 cdev
结构体变量,参数 fops
就是字符设备文件操作函数集合。使用 cdev_init 函数初始化 cdev 变量的示例代码如下:
1 struct cdev testcdev;
2
3 /* 设备操作函数 */
4 static struct file_operations test_fops = {
5 .owner = THIS_MODULE,
6 /* 其他具体的初始项 */
7 };
8
9 testcdev.owner = THIS_MODULE;
10 cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
3、cdev_add 函数
我们通过直接赋值和调用上面的初始化函数完成初始化之后,这个时候我们就可以向Linux系统添加字符设备,我们需要提供字符设备结构体
和设备号
才能完成添加。而这两个变量我们在上面所讲的内容中都已经得到了,分别是testcdev
和devid
。
cdev_add 函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p
指向要添加的字符设备(cdev
结构体变量),参数 dev
就是设备所使用的设备号,参数 count
是要添加的设备数量。
1 struct cdev testcdev;
2
3 /* 设备操作函数 */
4 static struct file_operations test_fops = {
5 .owner = THIS_MODULE,
6 /* 其他具体的初始项 */
7 };
8
9 testcdev.owner = THIS_MODULE;
10 cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
11 cdev_add(&testcdev, devid, 1); /* 添加字符设备 */
4、cdev_del 函数
卸载驱动的时候一定要使用cdev_del
函数从 Linux 内核中删除相应的字符设备,cdev_del
函数原型如下:
void cdev_del(struct cdev *p)
参数p
就是要删除的字符设备。如果要删除字符设备,参考如下代码:
1 cdev_del(&testcdev);/*删除 cdev */
cdev_del
和 unregister_chrdev_region
这两个函数合起来的功能相当于我们以前使用的 unregister_chrdev
函数。
在前面的 Linux 驱动实验中,当我们使用 modprobe
加载驱动程序以后还需要使用命令mknod
手动创建设备节点。本节就来讲解一下如何实现自动创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用 modprobe
加载驱动模块成功的话就会自动在/dev
目录下创建对应的设备文件。
udev
是一个用户程序,在 Linux 下通过 udev
来实现设备文件的创建与删除,udev
可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。比如使用modprobe
命令成功加载驱动模块以后就自动在/dev 目录下创建对应的设备节点文件,使用rmmod
命令卸载驱动模块以后就删除掉/dev
目录下的设备节点文件。使用 busybox
构建根文件系统的时候,busybox
会创建一个 udev
的简化版本—mdev
,所以在嵌入式 Linux 中我们使用mdev
来实现设备节点文件的自动创建与删除,Linux 系统中的热插拔事件也由 mdev
管理。
自动创建设备节点的工作是在驱动程序的入口函数
中完成的,一般在 cdev_add
函数后面添加自动创建设备节点相关代码。首先要创建一个 class
类,class
是个结构体,定义在文件include/linux/device.h
里面。class_create
是类创建函数。
- 创建类:
struct class *class_create (struct module *owner, const char *name)
class_create
一共有两个参数,参数 owner
一般为THIS_MODULE
,参数 name
是类名字。返回值是个指向结构体 class
的指针,也就是创建的类。
- 删除类
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy
,函数原型如下:
void class_destroy(struct class *cls);
参数 cls
就是要删除的类。
创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备。使用device_create
函数在类下面创建设备,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
这个设备文件。这里需要注意一下最后的设备名字是在这里确定的。 - 返回值就是创建好的设备。
同样的,卸载驱动的时候需要删除掉创建的设备,需要注意的是要先删除设备,然后才能删除类。设备删除函数为device_destroy
,函数原型如下:
void device_destroy(struct class *class, dev_t devt)
参数classs
是要删除的设备所处的类,参数 devt
是要删除的设备号。
在驱动入口函数里面创建类和设备,在驱动出口函数里面删除类和设备,参考示例如下:
1 struct class *class; /* 类 */
2 struct device *device; /* 设备 */
3 dev_t devid; /* 设备号 */
4
5 /* 驱动入口函数 */
6 static int __init xxx_init(void)
7 {
8 /* 创建类*/
9 class = class_create(THIS_MODULE, "xxx");
10 /* 创建设备 */
11 device = device_create(class, NULL, devid, NULL, "xxx");
12 return 0;
13 }
14
15 /* 驱动出口函数 */
16 static void __exit led_exit(void)
17 {
18 /* 删除设备 */
19 device_destroy(newchrled.class, newchrled.devid);
20 /* 删除类*/
21 class_destroy(newchrled.class);
22 }
23
24 module_init(led_init);
25 module_exit(led_exit);
四、最后附上完整的驱动程序
注意,这里只需要看整个流程,整个结构是啥样的就行,有些地方没必要在意,因为你要写的驱动程序和这个功能很可能不一样,所以没必要。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define NEWCHRLED_NAME "newchrled"
#define NEWCHRLED_COUNT 1
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
/* 地址映射后的虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
#define LEDOFF 0 /* 关闭 */
#define LEDON 1 /* 打开 */
/* LED设备结构体 */
struct newchrled_dev{
struct cdev cdev; /* 字符设备 */
dev_t devid; /* 设备号 */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct newchrled_dev newchrled; /* led设备 */
/* LED灯打开/关闭 */
static void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO1_DR);
val &= ~(1
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?