字符设备的注册分为两部分:
- 注册设备号
- 注册设备本身
注册设备的基本流程是先初始化要注册的设备,然后将该设备加载到内核。
一、字符设备的类型表示
1、字符设备表示 — struct cdev
在 Linux 中使用 cdev 结构体表示一个字符设备,cdev 结构体及其相关api函数在 include/linux/cdev.h 文件中,结构体的定义如下。
struct cdev {
struct kobject kobj;
struct module *owner; /* 该设备在内核中 */
const struct file_operations *ops; /* 字符设备文件操作函数 */
struct list_head list;
dev_t dev; /* 设备号 */
unsigned int count;
};
static struct cdev chrdev;
2、操作集合表示 — struct file_operations
操作集合代表了我们可以对该字符设备进行哪些操作,比如读设备 read、写设备 write、关闭设备 release,我们需要做的就是完善这些函数,前人已经设计出了一套完整的框架,这个框架已经包含了我们所需的大部分函数声明,所以无需我们自己设计。
内核源码中有个 include/linux/fs.h 文件,这个文件定义了一个 file_operations 结构体,该结构体中就包含了今后要实现的函数声明。
下面是操作集合对象的声明与定义。等号左边是 file_operations 的结构体成员,等号右边是我们自己实现的操作函数。
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open, // chrdevbase_open: 待实现的函数,.open: open操作对应的成员变量
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
二、字符设备操作 API
1、字符设备初始化
创建好字符设备对象以后,就需要对其进行初始化,初始化使用的函数是 cdev_init。函数声明如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
cdev:字符设备指针(对象)
fops:当前字符设备文件操作集合
/* 字符设备 */
static struct cdev dev;
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
// .open = chrdevbase_open,
// .read = chrdevbase_read,
// .write = chrdevbase_write,
// .release = chrdevbase_release,
};
/* 初始化字符设备 */
chrdev_led.dev.owner = THIS_MODULE;
cdev_init(&dev, &devfops);
2、设备加载函数
创建好字符设备以后,我们需要连同设备号,将字符设备加载到内核。字符设备加载函数声明如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
p:要添加的字符设备指针
dev:为当前字符设备注册的设备号
count:加载的字符设备的数量
返回值:0 代表成功;其他代表失败
static dev_t devid; /* 分配好的设备号 */
static struct cdev dev; /* 初始化以后的字符设备 */
ret = cdev_add(&dev, devid, 1); /* 将字符设备添加到内核 */
if (ret != 0)
{
return -1;
}
3、设备卸载函数
卸载驱动时,一定要从内核中删除对应的字符设备。使用的函数是 cdev_del,函数原型如下:
void cdev_del(struct cdev *p);
参数 p 就是要删除的字符设备指针。
三、字符设备加载测试
1、手动创建节点
之后会采取自动创建节点的方式,这里为了测试方便,临时采用手动创建节点的方式,需要将驱动模块代码编译加载到内核以后,使用 cat /proc/devices 查看主设备号。(驱动模块代码在最后)
insmod chrdevbase.ko # 加载驱动模块
cat /proc/devices # 查看主设备号
设备名为 chrdevbase,主设备号为 248,次设备号为0(因为只注册了一个设备,次设备号默认为0)。手动创建节点使用 mknod 命令
# 命令格式:mknod /dev/设备名 c 主设备号 次设备号
mknod /dev/chrdevbase c 200 0
2、应用测试代码
将应用程序代码交叉编译以后得到的执行文件是 chrdevbaseApp,此时在开发板的命令行中输入
./chrdevbaseApp /dev/chrdevbase
应用程序测试代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
char* driver_path = argv[1]; // 位置0 保存的是 ./chrdevbaseApp
int fd = open(driver_path, O_WRONLY); // 对应 open 操作
if (fd < 0)
{
perror("open file failed");
return -2;
}
write(fd, (char*)'a', 1); // 对应 write 操作
close(fd); // 对应 close 操作
return 0;
}
3、驱动模板代码
#include <linux/module.h> // MODULE_LICENSE、MODULE_AUTHOR
#include <linux/init.h> // module_init、module_exit
#include <linux/printk.h> // printk
#include <linux/kdev_t.h> // MKDEV
#include <linux/fs.h> // register_chrdev_region
#include <linux/cdev.h> // struct cdev
#define CHRDEVBASE_NAME "chrdevbase" /* 设备名 */
static struct chrdev_t{
dev_t devid; /* 设备号(由主设备号和次设备号组成) */
int major; /* 主设备号 */
int minor; /* 次设备号 */
struct cdev dev; /* 字符设备 */
} chrdev;
/*
* @description : 打开设备
* @param – pinode : 传递给驱动的 inode
* @param - pfile : 设备文件,file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrdevbase_open(struct inode *pinode, struct file *pfile)
{
/* 用户实现具体功能 */
printk("open chrdevbase\n");
return 0;
}
/*
* @description : 从设备读取数据
* @param - pfile : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 缓冲区长度
* @param - offset : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *pfile, char __user *buf, size_t cnt, loff_t *offset)
{
printk("read chrdevbase\n");
return 0;
}
/*
* @description : 向设备写数据
* @param - pfile : 要打开的设备文件(文件描述符)
* @param - buf : 要给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offset : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdevbase_write(struct file *pfile, const char __user *buf, size_t cnt, loff_t *offset)
{
printk("write chrdevbase\n");
return cnt;
}
/*
* @description : 关闭/释放设备
* @param - pfile : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int chrdevbase_release (struct inode *pinode, struct file * pfile)
{
/* 用户实现具体功能 */
printk("close chrdevbase\n");
return 0;
}
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevfops = {
.owner = THIS_MODULE,
.open = chrdevbase_open, // 将chrdevbase_open的函数地址传递给 .open 成员
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 0 成功;其他 失败
*/
static int __init chrdevbase_init(void)
{
int ret = 0;
printk("chrdevbase init!\n");
/* 1、注册设备号 */
if (!chrdev.major)
{
ret = alloc_chrdev_region(&chrdev.devid, 0, 1, CHRDEVBASE_NAME);
chrdev.major = MAJOR(chrdev.devid);
chrdev.minor = MINOR(chrdev.devid);
}
else
{
chrdev.devid = MKDEV(chrdev.major, 0);
ret = register_chrdev_region(chrdev.devid, 1, CHRDEVBASE_NAME);
}
/* 2、初始化字符设备 */
chrdev.dev.owner = THIS_MODULE;
cdev_init(&chrdev.dev, &chrdevfops);
/* 3、加载字符设备 */
ret = cdev_add(&chrdev.dev, chrdev.devid, 1);
if (ret != 0)
{
return -1;
}
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit chrdevbase_exit(void)
{
printk("chrdevbase exit!\n");
/* 释放设备号 */
unregister_chrdev_region(chrdev.devid, 1);
/* 卸载设备号 */
cdev_del(&chrdev.dev);
}
/*
* 将上面两个函数指定为驱动的入口和出口函数
*/
module_init(chrdevbase_init); // 注册 ko模块被加载到内核,系统会调用的函数
module_exit(chrdevbase_exit); // 注册 ko模块从内核卸载,系统会调用的函数
/*
* LICENSE和作者信息
*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("author_name");