正点原子嵌入式linux驱动开发——新字符设备驱动实验

经过之前两篇笔记的实战操作,已经掌握了Linux字符设备驱动开发的基本步骤,字符设备驱动开发重点是使用register_chrdev函数注册字符设备,当不再使用设备的时候就使用unregister_chrdev函数注销字符设备,驱动模块加载成功以后还需要手动使用mknod命令创建设备节点。register_chrdev和unregister_chrdev这两个函数是老版本驱动使用的函数,现在新的字符设备驱动已经不再使用这两个函数,而是使用Linux内核推荐的新字符设备驱动 API函数。本节就学习一下如何编写新字符设备驱动,并且在驱动模块加载的时候自动创建设备节点文件

新字符设备驱动原理

使用register_chrdev函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来问题:需要实现确定主设备号的使用情况,且会将该主设备号下的所有此设备号都使用掉。

解决这两个问题最好的方法就是在使用设备号的时候向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即设备名。

注销字符设备后要释放设备号,可统一使用如下函数:

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 }
13 
14 unregister_chrdev_region(devid, 1); /* 注销设备号 */

新的字符设备注册方法

字符设备结构

可以在Linux中使用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 } __randomize_layout

这其中,重要的是ops和dev,即字符设备文件操作函数集合file_operations以及设备号dev_t。使用如下:

struct cdev test_cdev

cdev_init函数

定义好cdev变量之后要调用cdev_init函数来初始化:

void cdev_init(struct cdev *cdev, const struct file_operations *fops)

cdev就是要初始化的cdev结构体变量,fops就是字符设备文件操作函数集合。

cdev_add函数

用于像Linux系统添加字符设备。首先cdev初始化,之后使用cdev_add来向Linux系统添加,原型如下:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)

p指向要添加的字符设备(cdev结构体变量),dev就是设备号,count是设备数量。

cdev_del函数

卸载驱动就是调用这个函数从Linux内核删除相应字符设备,原型如下:

void cdev_del(struct cdev *p)

p就是要删除的设备。

cdev_del和unregister_chedev_region合起来的功能相当于unregister_chedev函数。

自动创建设备节点

在前面的Linux驱动实验中,使用modprobe加载驱动程序以后还需要使用命令“mknod”手动创建设备节点。本节讲解一下如何实现自动创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用modprobe加载驱动模块成功的话就会自动在/dev目录下创建对应的设备文件

mdev机制

udev是一个用户程序,在Linux下通过udev来实现设备文件的创建与删除,udev可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。在使用buildroot构建根文件系统的时候选择了udev的简化版本mdev,所以在嵌入式Linux中用mdev来实现设备节点文件的自动创建与删除,Linux系统中的热插拔事件也由mdev管理,如果使用busybox构建根文件系统,会在/etc/init.d/rcS文件中如下语句:

echo /sbin/mdev > /proc/sys/kernel/hotplug

上述命令设置热插拔事件由mdev来管理。 buildroot构建的根文件系统已经全部处理好mdev了,不需要在修改什么文件。

创建和删除类

自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在cdev_add函数后面添加自动创建设备节点相关代码。首先要创建一个 class类,class是个结构体,定义在文件include/linux/device.h里面。class_create是类创建函数,class_create是个宏定义。最后将宏class_create展开后如下:

struct class *class_create (struct module *owner, const char *name)

owner一般为THIS_MODULE,name是类名字,返回值是指向结构体class的指针。

卸载驱动程序也需要删除类,函数为class_destroy,原型如下:

void class_destroy(struct class *cls);

cls就是要删除的类。

创建设备

创建好类之后,还需要在这个类下创建一个设备,使用函数device_create,原型如下:

struct device *device_create(struct class *cls,
                             struct device *parent, 
                             dev_t devt, 
                             void *drvdata, 
                             const char *fmt, ...)

device_create是个可变参数函数,cls就是设备要创建哪个类下面;parent是父设备,一般为NULL,也就是没有父设备;devt是设备号;drvdata是设备可能会使用的一些数据,一般为NULL;fmt是设备名字,如果设置fmt=xxx的话,就会生成 /dev/xxx这个设备文件,返回值就是创建好的设备。

同样,卸载驱动的时候需要删除创建的设备,函数为device_destroy,原型如下:

void device_destroy(struct class *cls, dev_t devt)

class是要删除设备所处类,devt是删除的设备号。

参考示例

struct class *class; /* 类 */ 
struct device *device; /* 设备 */ 
dev_t devid; /* 设备号 */ 

/* 驱动入口函数 */ 
static int __init xxx_init(void) 
{ 
	/* 创建类 */ 
	class = class_create(THIS_MODULE, "xxx"); 
	/* 创建设备 */ 
	device = device_create(class, NULL, devid, NULL, "xxx"); 
	return 0; 1
} 

/* 驱动出口函数 */ 
static void __exit led_exit(void) 
{
	/* 删除设备 */
	device_destroy(newchrled.class, newchrled.devid); 
	/* 删除类 */ 
	class_destroy(newchrled.class); 
} 
	
module_init(led_init); 
module_exit(led_exit);

设备私有数据

每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,但最好的做法试讲所有属性信息做成结构体。编写驱动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; 
}

设置好后,在write、read、close函数中直接读取private_data即可获得设备结构体。

实验程序编写

与之前LED的实验相比,重点就是使用了新的字符设备驱动,设置了文件四有数据,添加了自动创建设备节点相关内容。

LED灯驱动程序编写

区别点在于,申请好__iomem*的映射后虚拟地址指针,就申请一个newchrled_dev的设备结构体newchrdev;然后在led_open中设置私有数据private_data指向newchrdev;最后在led_init中申请设备号、添加字符设备、创建类和设备,并在led_exit中注销字符新设备、删除类和设备。

编写测试APP

直接使用LED实验的APP即可。

运行测试

编译驱动程序和测试APP

把Makefile中obj-m的值改为newchrled.o即可,“make”之后就会有“newchrled.ko”驱动模块文件。

ledAPP.c则通过下述命令编译:

arm-none-linux-gnueabihf-gcc ledApp.c -o ledApp

运行测试

将两个程序拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板并进入到目录lib/modules/5.4.31中,输入如下命令加载newchrled.ko:

depmod //第一次加载驱动的时候需要运行此命令
modprobe newchrled //加载驱动

加载成功后会输出申请到的主设备号和次设备号,如下图所示:
申请到的设备号
驱动加载成功后会自动在/dev目录下创建设备节点文件/dev/newchrdev,输入如下命令查看:

ls /dev/newchrled -l

驱动节点创建成功后就可以使用ledApp软件来测试,测试命令是一样的:

./ledApp /dev/newchrled 1 //打开 LED灯
./ledApp /dev/newchrled 0 //关闭 LED灯

卸载也是一样:

rmmod newchrled

总结

本篇还是在LED驱动的基础上,完成了自动创建驱动节点的代码编写,在led_open将私有数据private_data指向事先声明的结构体来管理设备文件;驱动open函数中添加类和设备。如此在加载好驱动之后,可以直接通过测试APP来测试,不需要自己设定驱动节点了

你可能感兴趣的:(linux学习,linux,驱动开发,学习,笔记,stm32)