公众号:嵌入式攻城狮(ID:andyxi_linux)
作者:安迪西
Linux字符设备驱动开发模板中介绍了旧版本的驱动开发模板,其需要手动分配设备号后,再进行注册,驱动加载成功后还需要手动创建设备节点,比较麻烦。目前Linux内核推荐的新字符设备驱动API函数,可以自动分配设备号、创建设备节点,使得驱动的使用更加方便
1. 新字符设备驱动原理
1.1 分配和释放设备号
旧字符设备驱动开发中使用register_chrdev函数注册字符设备时,需要事先确定好主设备号,并且注册成功后,会将该设备号下的所有次设备号都使用掉而新字符设备驱动API函数很好的解决了这个问题,使用设备号时再向内核申请,需要几个就申请几个,由内核分配设备可以使用的设备号
⏩ 设备号申请函数:没有指定设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
//dev:保存申请到的设备号
//baseminor:次设备号起始地址,一般为0
//count:要申请的设备号数量
//name:设备名字
⏩ 设备号申请函数:指定了主次设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name)
//from:要申请的起始设备号
//count:要申请的设备号数量
//name:设备名字
⏩ 设备号释放函数:统一使用下面函数释放
void unregister_chrdev_region(dev_t from, unsigned count)
//from:要释放的设备号
//count:表示从from开始,要释放的设备号数量
因此新字符设备驱动中,设备号分配代码通常按如下示例编写:
int major; /* 主设备号 */
int minor; /* 次设备号 */
dev_t devid; /* 设备号 */
if (major) { /* 定义了主设备号 */
devid = MKDEV(major, 0); /*大部分驱动次设备号都选择0*/
register_chrdev_region(devid, 1, "test");
} else { /* 没有定义设备号 */
alloc_chrdev_region(&devid, 0, 1, "test"); /*申请设备号*/
major = MAJOR(devid); /* 获取分配号的主设备号 */
minor = MINOR(devid); /* 获取分配号的次设备号 */
}
1.2 注册字符设备
Linux中使用cdev表示字符设备,在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;
};
//编写字符设备驱动之前需要定义一个cdev结构体变量
⏩ cdev_init函数:定义好cdev变量后,用该函数进行初始化
⏩ cdev_add函数:向系统添加字符设备(cdev结构体变量)
struct cdev testcdev;
/* 设备操作函数 */
static struct file_operations test_fops = {
.owner = THIS_MODULE,
/* 其他具体的初始项 */
};
testcdev.owner = THIS_MODULE;
cdev_init(&testcdev, &test_fops); /* 初始化cdev结构体变量 */
cdev_add(&testcdev, devid, 1); /* 添加字符设备 */
⏩ cdev_del函数:卸载驱动时从内核中删除相应的字符设备
void cdev_del(struct cdev *p)
//p:要删除的字符设备
1.3 自动创建设备节点
旧字符设备驱动开发中,驱动程序加载成功后还需要使用mknod命令手动创建设备节点,十分麻烦。而新字符设备驱动开发中,Linux通过udev用户程序来实现设备文件的自动创建与删除。udev会检测系统中硬件设备状态,并根据硬件设备状态来创建或者删除设备文件。
使用busybox构建根文件系统时,busybox会创建一个udev的简化版本mdev。因此,在嵌入式开发中使用mdev来实现设备节点文件的自动创建与删除。Linux系统中的热插拔事件也由mdev管理,在/etc/init.d/rcS文件中有如下语句:
echo /sbin/mdev > /proc/sys/kernel/hotplug
⏩ 创建类:自动创建设备节点的工作是在驱动程序入口函数中完成的,一般在cdev_add之后添加相关代码
struct class *class_create (struct module *owner, const char *name)
//owner 一般为 THIS_MODULE
//name 是类名字
//返回值是个指向结构体class的指针,也就是创建的类
删除类:卸载驱动程序时需要删除类
void class_destroy(struct class *cls)
//cls 就是要删除的类
⏩ 创建设备:类创建好后还不能实现自动创建设备节点,还需要在该类下创建一个设备
struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
//class 设备创建在哪个类下
//parent 父设备,一般为NULL
//devt 设备号
//drvdata 设备可能会使用的数据,一般NULL
//fmt 设备名字
//返回值是创建好的设备
删除设备:卸载驱动时需要删除创建的设备
void device_destroy(struct class *class, dev_t devt)
//class 是要删除的设备所处的类
//devt 是要删除的设备号
1.4 设置文件私有数据
每个硬件设备都有一些属性,比如主设备号、类、设备、开关状态等等,在编写驱动时可将这些属性封装成一个结构体。并在编写驱动open函数时将设备结构体作为私有数据添加到设备文件中:
/*设备结构体*/
struct test_dev{
dev_t devid; /*设备号*/
struct cdev cdev; /*cdev*/
struct class *class; /*类*/
struct device *device; /*设备*/
int major; /*主设备号*/
int minor; /*次设备号*/
};
struct test_dev testdev;
/*open函数*/
static int test_open(struct inode *inode, struct file *filp)
{
filp->private_data = &testdev; /*设置私有数据*/
return 0;
}
综上所述,新字符设备驱动开发流程如下图所示:
2. 新字符设备驱动开发实验
新字符设备驱动开发实验是在Linux字符设备驱动开发模板一文的基础上进行修改,只更改了驱动的编写方式,与应用程序无关,因此只修改驱动程序即可
2.1 驱动程序编写
⏩ 添加定义:宏及字符设备定义
#define CHRDEVBASE_CNT 1 //设备号个数
#define CHRDEVBASE_NAME "chrdevbase" //名字
/*newchr设备结构体 */
struct newchr_dev{
dev_t devid; //设备号
struct cdev cdev; //cdev
struct class *class; //类
struct device *device; //设备
int major; //主设备号
int minor; //次设备号
};
struct newchr_dev chrdevbase; //自定义字符设备
⏩ 修改open函数:设置私有数据
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
printk("chrdevbase open!rn");
filp->private_data = &chrdevbase; //设置私有数据
return 0;
}
⏩ 修改init函数
static int __init chrdevbase_init(void)
{
/* 注册字符设备驱动 */
//1、创建设备号
if (chrdevbase.major) /* 定义了设备号 */
{
chrdevbase.devid = MKDEV(chrdevbase.major, 0);
register_chrdev_region(chrdevbase.devid, CHRDEVBASE_CNT, CHRDEVBASE_NAME);
}
else /* 没有定义设备号 */
{
alloc_chrdev_region(&chrdevbase.devid, 0, CHRDEVBASE_CNT, CHRDEVBASE_NAME); /* 申请设备号 */
chrdevbase.major = MAJOR(chrdevbase.devid); /* 获取分配号的主设备号 */
chrdevbase.minor = MINOR(chrdevbase.devid); /* 获取分配号的次设备号 */
}
printk("chrdevbase major=%d,minor=%drn",chrdevbase.major, chrdevbase.minor);
//2、初始化cdev
chrdevbase.cdev.owner = THIS_MODULE;
cdev_init(&chrdevbase.cdev, &chrdevbase_fops);
//3、添加一个cdev
cdev_add(&chrdevbase.cdev, chrdevbase.devid, CHRDEVBASE_CNT);
//4、创建类
chrdevbase.class = class_create(THIS_MODULE, CHRDEVBASE_NAME);
if (IS_ERR(chrdevbase.class))
{
return PTR_ERR(chrdevbase.class);
}
//5、创建设备
chrdevbase.device = device_create(chrdevbase.class, NULL, chrdevbase.devid, NULL, CHRDEVBASE_NAME);
if (IS_ERR(chrdevbase.device))
{
return PTR_ERR(chrdevbase.device);
}
printk("chrdevbase init done!rn");
return 0;
}
⏩ 修改exit函数
static void __exit chrdevbase_exit(void)
{
/* 注销字符设备驱动 */
cdev_del(&chrdevbase.cdev);/* 删除cdev */
unregister_chrdev_region(chrdevbase.devid, CHRDEVBASE_CNT); /* 注销设备号 */
device_destroy(chrdevbase.class, chrdevbase.devid);
class_destroy(chrdevbase.class);
printk("chrdevbase exit done!rn");
}
2.2 程序编译
程序编译包括驱动程序和应用程序编译两个部分:
⏩ 驱动程序编译:创建Makefile文件,使用make命令,编译驱动程序
KERNELDIR := /home/andyxi/linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_andyxi
CURRENT_PATH := $(shell pwd)
obj-m := newchrdev.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
⏩ 应用程序编译:无需内核参与,直接编译即可
arm-linux-gnueabihf-gcc newchrdevApp.c -o newchrdevApp
2.3 运行测试
为了方便,选择通过TFTP从网络启动,并使用NFS挂载网络根文件系统。确保开发板能正常启动,在Ubuntu中将驱动和测试文件复制到modules/4.1.15目录中
⏩ 在开发板中输入如下指令加载驱动模块
depmod //第一次加载驱动的时候需要运行此命令
modprobe newchrdev.ko //加载驱动
⏩ 驱动加载成功后,可以看到自动申请到的主设备号和次设备号
⏩ 使用ls /dev/chrdevbase -l命令验证该设备节点文件是否存在,而旧驱动方式需要额外使用mknod指令来手动创建该设备节点文件
⏩ 驱动加载成功后,测试APP程序,如下
⏩ 测试完使用rmmod指令卸载驱动
以上可见Linux新字符设备驱动开发方式可以自动分配设备号、创建设备节点,使得驱动的使用更加方便、便捷。