字符设备驱动是 Linux 驱动开发中最重要,也是最简单的设备驱动程序,学习字符设备驱动的编写是入门驱动开发的最简单方式。字符设备驱动并不是天马行空,肆意妄为的,恰恰相反,字符设备驱动程序的编写甚至有些地方非常套路化,这主要归功于内核开发者们的努力,让驱动开发只需要调用接口即可。这篇文章,主要关注字符设备驱动中的”套路”,形成一套字符设备驱动的万能框架。
老的方法
模块的入口和出口
1 | /* 驱动入口函数 */ |
注册字符设备驱动
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备。卸载驱动模块的时也需要注销掉字符设备。 字符设备的注册和注销函数原型:
1 | static inline int register_chrdev(unsigned int major, |
这种注册函数会将后面所有的次设备号全部占用,而且主设备号需要我们自己去设置,现在不推荐这样使用。 一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。
内存映射
- 内存映射 在Linux中不能直接访问寄存器,要想要操作寄存器需要完成物理地址到虚拟空间的映射。
1 |
|
返回值: __iomem 是编辑器标记,指向映射后的虚拟空间首地址。 建立映射:**映射的虚拟地址 = ioremap(IO内存起始地址,映射长度)**; 一旦映射成功,访问对应的虚拟地址就相当于访问对应的IO内存 。
- 解除映射
1 | void iounmap (volatile void __iomem *addr) |
应用层和内核层传递数据
应用层和内核层是不能直接进行数据传输的。 要想进行数据传输, 要借助下面的这两个函数
1 | static inline long copy_from_user(void *to, const void __user * from, unsigned long n) |
to:目标地址 from:源地址 n:将要拷贝数据的字节数 返回值:成功返回 0, 失败返回没有拷贝成功的数据字节数
字符设备最基本框架
1 |
|
创建驱动节点文件
加载驱动模块后,需手动创建驱动节点文件
1 | mknod /dev/chrdevbase c 200 0 |
- 其中“mknod”是创建节点命令,
- “/dev/chrdevbase”是要创建的节点文件,
- “c”表示这是个字符设备,
- “200”是设备的主设备号,
- “0”是设备的次设备号。
- 创建完成以后就会存在/dev/chrdevbase 这个文件,可以使用“ls /dev/chrdevbase -l”命令查看
新的方法
上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。
在 Linux 下通过 udev(用户空间程序)来实现设备文件的创建与删除,但是在嵌入式 Linux 中使用mdev 来实现设备节点文件的自动创建与删除, Linux 系统中的热插拔事件也由 mdev 管理。
设备文件系统
设备文件系统有devfs,mdev,udev这三种
- devfs, 一个基于内核的动态设备文件系统
- devfs 缺点(过时原因)
- 不确定的设备映射
- 没有足够的主/辅设备号
- /dev目录下文件太多
- 内核内存使用
- udev,采用用户空间(user-space)工具来管理/dev/目录树,udev和文件系统分开
- udev 和 devfs 的区别
- 采用 devfs,当一个并不存在的 /dev 节点被打开的时候,devfs 能自动加载对应的驱动
- udev 的 Linux 应该在设备被发现的时候加载驱动模块,而不是当它被访问的时候
- 系统中所有的设备都应该产生热拔插事件并加载恰当的驱动,而udev能注意到这点并且为它创建对应的设备节点。
- mdev,是 udev 的简化版本,是 busybox 中所带的程序, 适合用在嵌入式系统
申请设备号
上述设备号为开发者挑选一个未使用的进行注册。Linux驱动开发推荐使用动态分配设备号。
动态申请设备号
1
2
3
4int alloc_chrdev_region(dev_t *dev,
unsigned baseminor,
unsigned count,
const char *name)dev:保存申请到的设备号。 baseminor: 次设备号起始地址,该函数可以申请一段连续的多个设备号,初始值一般为0 count: 要申请的设备号数量。 name:设备名字。
静态申请设备号
1
int register_chrdev_region(dev_t from, unsigned count, const char *name);
from - 要申请的起始设备号
count - 设备号个数
name - 设备号在内核中的名称 返回0申请成功,否则失败释放设备号
1
void unregister_chrdev_region(dev_t from, unsigned count)
from:要释放的设备号。 count: 表示从 from 开始,要释放的设备号数量。
申请设备号模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//创建设备号
if (newchrled.major) //定义了设备号就静态申请
{
newchrled.devid = MKDEV(newchrled.major, 0);
register_chrdev_region(newchrled.devid,
NEWCHRLED_CNT,
NEWCHRLED_NAME);
}
else //没有定义设备号就动态申请
{
alloc_chrdev_region(&newchrled.devid,
0,
NEWCHRLED_CNT,
NEWCHRLED_NAME);//申请设备号
newchrled.major = MAJOR(newchrled.devid); //获取分配号的主设备号
newchrled.minor = MINOR(newchrled.devid); // 获取分配号的次设备号
}
注册字符设备
在 Linux 中使用 cdev 结构体表示一个字符设备
1 | struct cdev { |
在 cdev 中有两个重要的成员变量:ops 和 dev,字符设备文件操作函数集合file_operations 以及设备号 dev_t。
初始化cdev结构体变量
1
2void cdev_init(struct cdev *cdev,
const struct file_operations *fops);例
1
2
3
4
5
6
7
8
9struct cdev testcdev;
//设备操作函数
static struct file_operations test_fops = {
.owner = THIS_MODULE,
//其他具体的初始项
};
testcdev.owner = THIS_MODULE;
//初始化 cdev 结构体变量
cdev_init(&testcdev, &test_fops);将设备添加到内核
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。
将cdev添加到内核同时绑定设备号。 其实这里申请设备号和注册设备在第一中驱动中直接使用register_chrdev 函数完成者两步操作
1
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
p - 要添加的cdev结构
dev - 绑定的起始设备号
count - 设备号个数 例1
cdev_add(&testcdev, devid, 1); //添加字符设备
将设备从内核注销 卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备
1
void cdev_del(struct cdev *p);
p - 要添加的cdev结构
例1
cdev_del(&testcdev); //删除 cdev
自动创建设备节点
上面的驱动框架,当使用 modprobe 加载驱动程序以后还需要使用命令mknod手动创建设备节点。
在驱动中实现自动创建设备节点的功能以后,使用 modprobe 加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。
创建一个class类
1
struct class *class_create(struct module *owner, const char *name);
- class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
- 设备类名对应 /sys/class 目录的子目录名。
- 返回值是个指向结构体 class 的指针,也就是创建的类。
删除一个class类
1
void class_destroy(struct class *cls); // cls要删除的类
创建设备 还需要在类下创建一个设备,使用 device_create 函数在类下面创建设备。 成功会在 /dev 目录下生成设备文件。
1
2
3
4
5struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)*class——设备类指针, *parent——父设备指针, devt——设备号, *drvdata——额外数据, *fmt——设备文件名
删除设备 卸载驱动的时候需要删除掉创建的设备
1
void device_destroy(struct class *class, dev_t devt);
class——设备所处的类 devt——设备号
文件私有数据
- 每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device),
- 一个设备的所有属性信息将其做成一个结构体,
- 编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中。
- 在 write、 read、 close 函数中直接读取 private_data即可得到设备结构体
1 | /* newchrled设备结构体 */ |
新字符设备驱动程序框架
1 |
|