概述
接着上一篇文章的内容,继续编写我们的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_deinitrn");
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_initrn");
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:%drn",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_deinitrn");
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
最后
以上就是超级仙人掌为你收集整理的[学习分享]嵌入式linux字符驱动详解(二)的全部内容,希望文章能够帮你解决[学习分享]嵌入式linux字符驱动详解(二)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复