[学习分享]嵌入式linux字符驱动详解(二)

接着上一篇文章的内容,继续编写我们的led驱动程序。

通过上一篇文章的实验,我们知道内核在加载模块驱动时,会进入的函数是module_init()里面指定的函数,我们称之为入口函数,因此,要向内核注册设备,肯定要在入口函数处完成。

注册字符设备的函数是:

int register_chrdev_region(dev_t from, unsigned count, const char *name);

参数分别是:dev_t 设备号,设备数量,设备名字;

这里有个参数是设备号,Linux系统里面是通过设备号对设备进行访问的,可以看到这里的设备号是一个dev_t类型,查看这个类型的定义,是一个u32类型。找有关于设备号描述的资料,有关于设备号操作定义在include/linux/kdev_t.h文件中。重点关注这几行代码:

#define MINORBITS	20
#define MINORMASK	((1U << MINORBITS) - 1)

#define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))

设备号分为主设备号和次设备号。主设备号使用的是高12位,次设备号使用的是低32位。在某些场景中,一个驱动程序可能对应多个设备,那么这些设备就可以使用同一个主设备号,不同的设备用次设备号进行区分。比如IIC设备,IIC总线中挂载了多个设备,那么主设备号对应的是IIC驱动,次设备号对应的是具体的设备。这里我们以led为例,只用到了一个设备,所以次设备号设置为0.

那么,该如何确定我们要注册的设备号是什么呢?两种方法:一、手动查询当前系统中未使用的设备号。二、由系统自动分配。

手动查询的方法:到开发板上 查看 /proc下的devices文件: cat /proc/devices. 然后找一个未被使用的设备号,范围(0-4096).

使用第一种方法的话,直接使用register_chrdev_region函数注册一个字符设备,使用第二种方法,需要用alloc_chrdev_region函数让系统给我们分配设备号,函数原型是:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)

参数分别是:设备号输出,次设备号从多少开始分配,设备数量,设备名字。

到这里,完成了设备的注册和设备号的绑定,接下来需要把设备的操作函数(open、read、write、close等)与设备关联起来。我们用cdev的方式。

先查看cdev的数据结构,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里面。 还有一个需要重点关注的成员是file_operations结构体,查看它的定义,可以看到这里定义了很多函数指针:

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ......
};

这些指针,需要指向我们自己编写的操作函数里面。我们自己编写了一些程序,比如写了一个open_xxx函数,应用程序在使用该设备的时候,一般都过标准的系统调用open函数打开这个设备文件,那么它是如何进入到我们自己编写的函数中执行的呢,就是通过cdev里的file_operations里的成员指向我们编写的函数实现。

这里我们先定义两个结构体实体(变量),分别是cdev类型和file_operations类型;

static struct cdev led_cdev;

static struct file_operations led_ops={
	.owner = THIS_MODULE,
	.open  = led_open,
	.release = led_release,
	.write	= led_write,
};

在定义变量的时候,可以直接给成员赋值,这里我直接让open等指针指向了我们自己编写的操作函数led_open。这里需要注意,自己编写的操作函数需要与file_operations里面的成员定义的参数一致。如fops中write的原型是

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

则我们编写的led_write函数的定义应为:

static ssize_t led_write(struct file *filp,const char __user *buf,size_t cnt,loff_t *offt)

到这里我们可以先把用到的几个函数先实现一下,内容暂时为空。如下:

static int led_open(struct inode *inode, struct file *filp)
{
	
	return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
	
	return 0;
}

static ssize_t led_write(struct file *filp,const char __user *buf,size_t cnt,loff_t *offt)
{
	return 0;
}

刚刚我们定义了两个结构体变量,接下来要通过这两个变量实现一个完整驱动的编写。

我们是通过cdev的方式实现设备文件与驱动的关联,所以接下来是cdev的操作步骤。

一:初始化cdev。

二:向内核添加一个cdev。

内核提供cdev初始化的函数是:

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

通过参数可以大概知道,这里的初始化操作其实就是把file_opeartions的内容赋给cdev的ops成员。前面这两个结构体变量已经定义好了,所以我们可以这样调用。

cdev_init(&led_cdev, &led_ops);

内核提供添加cdev的函数是:

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

这里的参数有一个dev_t类型,前面已经提到,这个是关于设备号的数据类型,前面我们已经申请/定义了设备号,但是并没有传入到cdev里面,这一步会把前面的设备号写入cdev里面。

到这里,一个字符设备驱动初始化的流程(框架)就完成了。注册Linux字符设备会占用系统的一些资源,对于可加载卸载的设备,在卸载时(出口函数)必须释放注册时占用的资源

前面在加载(注册)的时候,我们使用了一个设备号,还有向内核添加了一个cdev。所以在卸载的时候需要释放这些资源,编写出口函数如下:

static void __exit led_exit(void)
{
	printk("led_driver_deinit\r\n");
	cdev_del(&led_dev.led_cdev);
	unregister_chrdev_region(led_dev.devid, LED_COUNT);
}

编写完这些,一个可用的字符设备驱动框架已经搭建完成了,但在实际运行中,加载模块后,开发板/dev目录下并没有对应的设备文件,那么我们的应用程序也就无法调用操作我们的设备驱动,这时候需要手动创建一个设备节点,名字和驱动程序名字相同,比如这里的驱动名字为led_driver。 那么需要在开发板上执行命令:mknod /dev/led_driver c xxx xxx 。 其中“/dev/led_driver”是设备节点的路径名字,c表示这是一个字符设备(char) xxx 表示设备的主设备号和次设备号。在加载驱动的时候,我们可以使用printk把主次设备号都打印出来。

事实上,创建设备节点这个步骤可以让驱动程序自动完成,但目前还是有比较多的驱动程序没有实现自动创建设备节点,因此有必要了解一下如何创建与驱动匹配的设备节点。

自动创建设备节点的方法如下:

一、创建一个类。

二、在这个类下创建一个设备。

函数分别是:

#define class_create(owner, name)		\
({						\
	static struct lock_class_key __key;	\
	__class_create(owner, name, &__key);	\
})

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

创建类的参数:所有者(一般设置为THIS_MODULE),名字。

创建设备的参数:类,父设备(没有的话就是NULL),设备号,驱动数据(没有可以为NULL),名字。

需要注意这里执行的是创建操作,可以想象得到它一定会从系统中申请一些资源,因此在卸载模块驱动的时候也要释放这些资源。

附上本例的全部代码:

#include <linux/types.h> 
#include <linux/kernel.h> 
#include <linux/delay.h> 
#include <linux/ide.h> 
#include <linux/init.h>
#include <linux/module.h> 
#include <linux/errno.h> 
#include <linux/gpio.h> 
#include <linux/cdev.h> 
#include <linux/device.h> 

#define MODULE_NAME "led_driver"
#define LED_COUNT 1

struct led_dev_t{
	int major;
	int minor;
	dev_t devid;
	struct cdev led_cdev;
	
	struct class *class;
	struct device *device;
};

struct led_dev_t led_dev;

static int led_open(struct inode *inode, struct file *filp)
{
	
	return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
	
	return 0;
}

static ssize_t led_write(struct file *filp,const char __user *buf,size_t cnt,loff_t *offt)
{
	return 0;
}


static struct file_operations led={
	.owner = THIS_MODULE,
	.open  = led_open,
	.release = led_release,
	.write	= led_write,
};

static int __init led_init(void)
{
	u32 val;	
	printk("led_driver_init\r\n");
	if(led_dev.major){
		led_dev.devid = MKDEV(led_dev.major,led_dev.minor);
		register_chrdev_region(led_dev.devid,LED_COUNT,MODULE_NAME);
	}else{
		alloc_chrdev_region(&led_dev.devid,0,LED_COUNT,MODULE_NAME);
		led_dev.major = MAJOR(led_dev.devid);
		led_dev.minor = MINOR(led_dev.devid);
	}
	printk("led dev: major:%d,minor:%d\r\n",led_dev.major,led_dev.minor);

	led_dev.led_cdev.owner = THIS_MODULE;
	cdev_init(&led_dev.led_cdev, &led);

	
	cdev_add(&led_dev.led_cdev, led_dev.devid, LED_COUNT);

	led_dev.class = class_create(THIS_MODULE, MODULE_NAME);

	led_dev.device = device_create(led_dev.class,NULL,led_dev.devid,NULL,MODULE_NAME);
	return 0;
}


static void __exit led_exit(void)
{
	printk("led_driver_deinit\r\n");
	cdev_del(&led_dev.led_cdev);
	unregister_chrdev_region(led_dev.devid, LED_COUNT);

	device_destroy(led_dev.class,led_dev.devid);
	class_destroy(led_dev.class);
}


module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("weymin");

此时我们还没有对GPIO进行任何操作,也就是说我们的led驱动程序其实还没有完成,目前只是完成一个基础框架,下一篇文章将加入对硬件处理器的操作,完善led驱动程序。

(完)2020.05.03

原创文章 57 获赞 71 访问量 11万+

猜你喜欢

转载自blog.csdn.net/u013053268/article/details/105907100
今日推荐