新字符设备驱动实验

1. 新字符设备驱动原理

1.1. 分配和释放设备号

我们在原来使用register_chrdev来注册字符设备的时候,我们只需要一个主设备号就能够注册字符设备,我们前面也说过,如果只使用一个主设备号的话,那么这个主设备号下的所有次设备号都会被占用,这样就会造成资源的浪费。

解决上述问题的方法也很简单,就是我们需要一个函数,在注册字符设备的时候可以自动向内核申请设备号,需要几个设备号就申请几个设备号,这样上述的问题就迎刃而解了。
当然,Linux内核中也提供了这样的函数,而且我们可以使用两个函数来注册字符设备并进行设备号的申请,这里申请设备号的方式也分为静态申请方式和动态申请方式,我们首先来看一下静态申请方式。

静态申请方式是通过函数register_chrdev_region这个函数来注册设备并申请设备号的,我们可以指定主设备号和次设备号,并且通过MKDEV函数来生成设备号(前面我们说过,设备号是由高12位的主设备号和低20位的次设备号来构成的),然后这个函数就可以通过我们生成的这个设备号来自动申请指定数量的设备号,这样就避免了次设备号的浪费。
register_chrdev_region函数的函数原型如下:

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

这个函数中有三个参数:
from:起始设备号,将我们指定的主设备号和次设备号生成的设备号填入,然后就可以从这个设备号来进行申请。
count:要申请的设备号的数量,这个就是指定要申请的设备号的数量。
name:要申请的设备的名字。

返回值:如果设备号申请成功就返回0,如果失败则返回负值。

我们接着来看动态申请方式

动态注册并申请的方式是通过函数alloc_chrdev_region函数来申请设备号的,动态申请方式是我们不需要指定设备号,如果要申请设备号的时候,系统会根据当前设备号的使用情况来自动申请一个设备号,并通过取址的方式保存在设备号变量中。
alloc_chrdev_region的函数原型如下:

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

这个函数有四个参数:
dev:将申请的设备号的变量保存在dev中,所以我们需要提前定义一个dev_t的变量来保存申请到的设备号。
baseminor:起始的次设备号,也就是我们需要指定从哪个次设备号开始,一般都给0。
count:需要申请的数量,和静态申请的参数是一样的。
name:要申请的设备的名字。

返回值:如果设备号申请成功就返回0,如果申请失败就返回负值。
当我们用自动申请的方式申请完设备号以后,我们并不知道主设备号的值和次设备号的值,那我们如何知道这两个值是多少呢,我们可以是使用两个宏MAJOR和MINOR来从设备号dev中得到主设备号的值和次设备号的值。

注销字符设备以后需要释放掉申请的设备号,不管是通过register_chrdev_region申请的设备号还是通过alloc_chrdev_region申请的设备号,都是通过unregister_chrdev_region来释放设备号。unregister_chrdev_region的函数原型如下:

void unregister_chrdev_region(dev_t from, unsigned count)

其中,dev_t是要释放的设备号,count是要释放掉几个设备号,我们通过这个函数就可以释放掉申请的设备号了。

下面来演示一下如何使用这两个函数来申请设备号的,此次新字符设备驱动实验也是操作led灯的实验,我们的设备名字就命名为newchrled。

#define NEWCHRLED_NAME "newchrled" //宏定义设备名字


int major;	//定义主设备号变量
int minor;	//定义次设备号变量
int ret;	//设置注册函数的返回值
dev_t devid;//定义设备号变量


if(major) //如果我们定义了主设备号,即通过静态方式来注册并申请设备号,但是我们一般不使用这种方式
{
	devid = MKDEV(major,minor);	//使用宏MKDEV来生成设备号
	ret = register_chrdev_region(devid,1,NEWCHRLED_NAME); //注册字符设备并将相关参数填入
	if(ret < 0)	//注册失败处理函数
	{
		printk("register failed!!\r\n");
	}
}

else	//如果没有定义主设备号,就使用动态方式来注册字符设备并申请设备号,我们大部分都使用这种方式
{
	ret = alloc_chrdev_region(&devid,0,1,NEWCHRLED_NAME); //注册字符设备并将相关参数填入
	if(ret < 0) //注册失败处理函数
	{
		printk("alloc register failed!!\r\n");
	}
}

1.2. 添加字符设备

  • 字符设备的结构
    在Linux中使用cdev结构体来表示一个字符设备,cdev结构体的定义如下;
struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};

在cdev中有很多的成员变量,其中最主要的是ops和dev这两个成员变量,ops是字符操作集合file_operations和设备号dev_t定义的变量。在编写字符设备驱动之前需要定义一个cdev结构体变量,如下所示:

struct cdev test_cdev;

这个变量就表示一个字符设备。

  • cdev_init函数
    定义好cdev变量以后就要使用cdev_init函数对其进行初始化,cdev_init函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)

cdev_init的初始化的参数包括两个,一个就是原来定义好的cdev字符设备和字符设备操作集fops,所以我们在使用cdev_init函数之前不仅要定义一个cdev结构体变量,还要定义一个file_operations操作集合,这样才能初始化字符设备。

  • cdev_add函数
    我们在初始化完成字符设备以后,需要向内核添加字符设备(cdev结构体变量),首先使用cdev_init函数完成对cdev结构体变量的初始化,然后使用cdev_add函数向Linux系统中添加这个字符设备。
    这里就有一个疑问,我们前面已经通过register_chrdev_region或者alloc_chrdev_region函数向内核注册过了字符设备了,那么我们为什么还有通过cdev_add函数向内核添加字符设备呢?

向内核注册字符设备和向内核添加字符设备是两个不同的概念。

向内核注册字符设备是指将一个字符设备与其对应的驱动程序关联起来,并向内核注册该字符设备。这个过程需要调用Linux内核中的register_chrdev_region或者alloc_chrdev_region函数,该函数会在系统中为该设备分配一个唯一的设备号,并将设备驱动程序的信息与设备号关联起来。注册成功后,应用程序就可以通过设备文件来访问该字符设备了。

向内核添加字符设备是指在Linux内核中创建一个新的字符设备,并将其添加到系统中。这个过程需要调用Linux内核中的cdev_add函数,该函数会为该设备分配一个唯一的设备号,并将设备驱动程序的信息与设备号关联起来。添加成功后,应用程序就可以通过设备文件来访问该字符设备了。

因此,向内核注册字符设备是将一个已经存在的字符设备与其对应的驱动程序关联起来,而向内核添加字符设备是在系统中创建一个新的字符设备并将其添加到系统中。

register_chrdev_region或者alloc_chrdev_region函数+cdev_add函数就相当于我们前面最初字符设备驱动开发中的register_chrdev函数。

  • cdev_del函数
    在卸载驱动的时候一定要使用cdev_del函数从Linux内核中删除相应的字符设备,cdev_del的函数原型如下:
void cdev_del(struct cdev *p)

这个函数只有一个参数,就是要删除的字符设备结构体的地址。

unregister_chrdev_region函数和cdev_del函数的结合就相当于最初字符设备驱动开发中的unregister_chrdev函数。

2.自动创建设备节点

在前面的Linux驱动实验中,当我们用modprobe加载完驱动程序以后,还是使用命令mknod来手动创建设备节点。这个方法虽然不是很复杂,但是也比较烦人,那有没有什么办法可以在驱动程序中来自动创建节点呢,答案是肯定的,下面就来介绍一下自动创建设备节点的方法。

2.1. mdev机制

udev是一个用户程序,可以在用户空间使用,在Linux下通过udev可以实现设备文件的创建与删除,udev可以检测系统中硬件设备的状态,可以根据系统中硬件设备的状态来创建或者删除设备文件。在Linux文件中,主要是使用udev来实现自动创建设备节点文件的作用,udev主要使用来实现设备节点文件的创建与删除。它是Linux内核的一个子系统,负责管理设备文件和设备节点。通过udev,Linux系统可以在设备插入或移除时自动创建或删除相应的设备节点文件,从而方便用户对设备的管理和使用。
我们在使用busybox创建根文件系统以后,busybox会创建一个udev的简化版本—mdev,所以在嵌入式Linux中我们使用mdev来实现设备节点文件的自动创建与删除,下面我们就来重点学一下如何通过mdev来实现设备文件节点的自动创建与删除。

2.2. 创建和删除类

自动创建设备节点的工作是在入口函数中完成的,一般cdev_add函数后面添加自动创建设备节点的相关代码。首先是需要创建一个class类的,class类是个结构体,通过class_create来创建类,class_create是个宏定义,内容如下:

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

struct class *__class_create(struct module *owner, const char *name,struct lock_class_key *key)

根据上述代码,将宏class_create展开以后就得到如下内容:

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

class_create一共有两个参数,参数owner一般设置为THIS_MODULE,参数name是类的名字,返回值是创建的类,是指向结构体class的指针。

卸载驱动程序的时候需要删除掉类,类的删除函数为class_destory,函数原型如下:

void class_destroy(struct class *cls)

其中的cls就是要删除的类。

那么写到这里就有一些疑问,为什么在自动创建设备节点之前需要创建类呢?
答案如下:创建类是为了将驱动程序所支持的设备归类,方便系统管理。在Linux内核中,每个驱动程序都需要属于一个类。类通常表示一类设备,例如USB设备、网络设备等。通过将驱动程序所支持的设备归类到相应的类中,系统可以更好地管理和识别设备。

那么创建的类是如何进行区分的呢?
答案如下:在Linux内核中,每个类都有一个唯一的标识符,称为class ID。class ID是一个整数值,由内核自动生成。因此,我们可以通过class ID来区分不同的类。

2.3. 创建设备

在上面创建好类以后还不能实现自动创建设备节点,我们还需要在这个类下创建一个设备,使用device_create函数在类下面创建设备,device_create函数原型如下:

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

device_create是个可变参数的函数,但通常情况下有5个参数,其中阐述cls就是这个设备要创建在哪个类下面;参数parent是父设备,一般设置为NULL,也就是没有父设备;参数devt是设备号,就是我们前面申请的设备号,如果要是使用静态的申请方法,需要使用MKDEV来生成设备号;参数drvdata是设备可能会使用的一些数据,一般也设置为NULL;参数fmt是设备的名字,如果要设置fmt=xxx的话,就会生成/dev/xxx这个设备。
device_create函数的返回值就是创建好的设备。
同样,在卸载驱动的时候需要删除掉创建的设备,设备删除函数为device_destroy,函数原型如下:

void device_destroy(struct class *class, dev_t devt)

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

同样,我们也会问,在自动创建设备节点之前为什么要创建设备呢?
答案如下:创建设备则是为了表示驱动程序所支持的设备。在Linux内核中,每个设备都对应一个struct device结构体,该结构体包含了设备的各种属性。通过创建设备,可以将驱动程序所支持的设备与相应的struct device结构体关联起来,从而方便系统管理和使用。

那么创建的设备是如何来区分的呢?
答案如下:每个设备节点都有一个唯一的标识符,称为device ID。device ID由内核自动生成,用于区分不同的设备。因此,在Linux系统中,通过class ID和device ID两个标识符,可以准确地标识每个设备节点,从而方便系统管理和使用。

那么我们需要手动创建class ID或者device ID吗?
答案如下:在Linux系统中,class ID和device ID是由内核自动生成的,通常无需手动设置。当系统加载驱动程序并创建设备节点时,内核会自动为每个设备节点分配一个唯一的device ID,并将其与相应的class ID关联起来。

2.4. 新字符设备驱动框架总结

进行到这个地方我们就可以总结出新字符设备驱动的框架出来了:

  • 首先我们需要使用register_chrdev_region(静态方法)或者alloc_chrdev_region(动态方法)来注册字符设备并申请设备号。
  • 然后我们使用cdev_init来初始化一个字符设备,并使用函数cdev_add来向内核中添加一个字符设备。
  • 最后需要设置自动申请设备节点,先使用class_create来创建一个类,然后使用device_create来创建一个设备。

通过上述的步骤就可以搭建一个新的字符设备驱动框架,并且在模块加载以后能够实现自动创建设备节点了。

下面就通过代码来实现上述的过程:

首先是注册字符设备并申请设备号

/*1.注册字符设备并申请设备号*/
    if(major)//如果有指定的主设备号的话,就使用指定的设备号
    {
        devid = MKDEV(major,minor); //通过主设备号和次设备号生成设备号
        ret = register_chrdev_region(devid,NEWCHRLED_CNT,NEWCHRLED_NAME); //向内核注册字符设备驱动
        if(ret < 0) //错误处理
        {
            printk("register failed!!\r\n");
            return -1;
        }
    }
    else //如果没有制定的主设备号的花,就用动态分配的方式来申请设备号
    {
        ret = alloc_chrdev_region(&devid,0,NEWCHRLED_CNT,NEWCHRLED_NAME); //动态申请并注册字符设备驱动
        if(ret < 0)//错误处理
        {
            printk("register failed!!\r\n");
            return -1;
        }
        major = MAJOR(devid); //使用宏MAJOR来得到申请的设备的主设备号
        minor = MINOR(devid); //使用宏MINOR来得到申请的设备的次设备号
    }

接着是初始化字符设备并向内核添加字符设备

/*2.初始化字符设备cdev并向内核添加字符设备*/
    cdev.owner = THIS_MODULE;
    cdev_init(&cdev,&newchrled_fops); //参数newchrled_fops是字符设备操作集合,需要在前面定义

    cdev_add(&cdev,devid,NEWCHRLED_CNT);

最后是创建类和创建设备

/*3.创建类和设备*/
    class = class_create(THIS_MODULE,NEWCHRLED_NAME)//使用class_create来创建类
    if(IS_ERR(class))//使用宏IS_ERR来判断创建类是否失败
    {
        unregister_chrdev_region(devid,NEWCHRLED_CNT); //如果失败,首先释放掉申请的设备号
        cdev_del(&cdev);                               //删除字符设备
        return PTR_ERR(class);               //返回失败值
    }

    device = device_create(class,NULL,devid,NULL,NEWCHRLED_NAME);//使用device_create来创建设备
    if(IS_ERR(device))  //来判断创建设备是否失败
    {
        unregister_chrdev_region(devid,NEWCHRLED_CNT); //如果失败,释放设备号
        cdev_del(&cdev);                               //删除字符设备
        class_destroy(class);                          //摧毁类
        return PTR_ERR(device)
    }  

3. 文件结构体和文件私有数据

我们在搭建字符设备框架的时候需要用到很多的变量,这些变量如下:

dev_t devid;            //定义一个设备号的变量
struct cdev cdev;       //定义一个字符设备
struct class* class;    //定义一个类
struct device* device;  //定义一个设备
int major;              //定义主设备号
int minor;              //定义次设备号

如果我们将这些变量单独定义,这样不仅很杂乱,也不利于模块化编程,并且随着后面编程难度的提升,代码量和变量的数量都会变多,所以我们需要将这些变量归属到一个结构体里面,这样不仅美观很多,而且能够提升代码的可读性。具体操作如下:

struct newchrled_dev
{
    dev_t devid;            //定义一个设备号的变量
    struct cdev cdev;       //定义一个字符设备
    struct class* class;    //定义一个类
    struct device* device;  //定义一个设备
    int major;              //定义主设备号
    int minor;              //定义次设备号
};

struct newchrled_dev newchrled;

并且我们编写驱动函数的时候,也可以将整个设备的结构体作为私有数据添加到设备文件中,这样我们在后面write,read,close等函数中可以直接读取私有数据就可以得到设备的结构体。
具体代码如下:

static int test_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &newchrled; /* 设置私有数据 */
	return 0;
}

4. 实验程序编写

我们这次的实验和上篇文章中嵌入式Linux LED驱动开发实验的实验效果相同,在应用程序向驱动程序写1的时候开灯;在应用程序向驱动程序写0的时候关灯。

4.1. 驱动程序

我们驱动程序需要结合上面的新字符设备驱动原理和自动创建设备节点以及上篇文章中嵌入式Linux LED驱动开发实验来写。
首先我们需要搭建一个设备驱动框架。代码如下:

#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>

#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define NEWCHRLED_CNT   1               //定义申请的字符设备数量
#define NEWCHRLED_NAME  "newchrled"     //定义字符设备的名字



struct newchrled_dev //定义一个结构体
{
    dev_t devid;            //定义一个设备号的变量
    struct cdev cdev;       //定义一个字符设备
    struct class* class;    //定义一个类
    struct device* device;  //定义一个设备
    int major;              //定义主设备号
    int minor;              //定义次设备号
};

struct newchrled_dev newchrled; //定义一个结构体变量

static int newchrled_open(struct inode *inode,struct file* filp) //open具体实现函数
{
    filp->private_data = &newchrled; //设置私有数据
    return 0;
}

static ssize_t newchrled_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt) //read具体实现函数
{
    return 0;
}

static ssize_t newchrled_write(struct file *filp,const char __user *buf,size_t cnt,loff_t *offt)//write具体实现函数
{
    return 0;
}

static int newchrled_release(struct inode *inode,struct file* filp) //release具体实现函数
{
    return 0;
}


static struct file_operations newchrled_fops = {  //字符设备操作集合
    .owner = THIS_MODULE,  //.owner成员
    .open = newchrled_open,//.open成员
    .write = newchrled_write,//.write成员
    .read = newchrled_read,//.read成员
    .release = newchrled_release, //.release成员
};


//驱动加载入口函数
static int __init newchrled_init(void)
{
    int ret;

    /*1.注册字符设备并申请设备号*/
    if(newchrled.major)//如果有指定的主设备号的话,就使用指定的设备号
    {
        newchrled.devid = MKDEV(newchrled.major,newchrled.minor); //通过主设备号和次设备号生成设备号
        ret = register_chrdev_region(newchrled.devid,NEWCHRLED_CNT,NEWCHRLED_NAME); //向内核注册字符设备驱动
        if(ret < 0) //错误处理
        {
            printk("register failed!!\r\n");
            return -1;
        }
    }
    else //如果没有制定的主设备号的花,就用动态分配的方式来申请设备号
    {
        ret = alloc_chrdev_region(&newchrled.devid,0,NEWCHRLED_CNT,NEWCHRLED_NAME); //动态申请并注册字符设备驱动
        if(ret < 0)//错误处理
        {
            printk("register failed!!\r\n");
            return -1;
        }
        newchrled.major = MAJOR(newchrled.devid); //使用宏MAJOR来得到申请的设备的主设备号
        newchrled.minor = MINOR(newchrled.devid); //使用宏MINOR来得到申请的设备的次设备号
    }

    /*2.初始化字符设备cdev并向内核添加字符设备*/
    newchrled.cdev.owner = THIS_MODULE;
    cdev_init(&newchrled.cdev,&newchrled_fops); //参数newchrled_fops是字符设备操作集合,需要在前面定义

    cdev_add(&newchrled.cdev,newchrled.devid,NEWCHRLED_CNT);

    /*3.创建类和设备*/
    newchrled.class = class_create(THIS_MODULE,NEWCHRLED_NAME);//使用class_create来创建类
    if(IS_ERR(newchrled.class))//使用宏IS_ERR来判断创建类是否失败
    {
        unregister_chrdev_region(newchrled.devid,NEWCHRLED_CNT); //如果失败,首先释放掉申请的设备号
        cdev_del(&newchrled.cdev);                               //删除字符设备
        return PTR_ERR(newchrled.class);               //返回失败值
    }

    newchrled.device = device_create(newchrled.class,NULL,newchrled.devid,NULL,NEWCHRLED_NAME);//使用device_create来创建设备
    if(IS_ERR(newchrled.device))  //来判断创建设备是否失败
    {
        unregister_chrdev_region(newchrled.devid,NEWCHRLED_CNT); //如果失败,释放设备号
        cdev_del(&newchrled.cdev);                               //删除字符设备
        class_destroy(newchrled.class);                          //摧毁类
        return PTR_ERR(newchrled.device);
    }    

    return 0;
}


//驱动卸载出口函数
static void __exit newchrled_exit(void)
{
    unregister_chrdev_region(newchrled.devid,NEWCHRLED_CNT); //如果失败,释放设备号
    cdev_del(&newchrled.cdev);                               //删除字符设备

    device_destroy(newchrled.class,newchrled.devid);         //摧毁设备,在出口函数中一定要线摧毁设备再摧毁类,因为一旦先摧毁类,那么就无法通过类找到设备了
    class_destroy(newchrled.class);                          //摧毁类

}


module_init(newchrled_init);  //注册模块加载函数 
module_exit(newchrled_exit);  //注册模块卸载函数
MODULE_LICENSE("GPL");
MODULE_AUTHOR("kk");

搭建完框架以后,我们就要实现具体的控制LED灯的寄存器配置了,我们就借鉴上一篇文章中的LED灯的寄存器配置的方法,完善我们的驱动程序,具体完成以后的效果如下:

#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>

#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define NEWCHRLED_CNT   1               //定义申请的字符设备数量
#define NEWCHRLED_NAME  "newchrled"     //定义字符设备的名字
#define LEDOFF 					0			/* 关灯 */
#define LEDON 					1			/* 开灯 */


/* 寄存器物理地址 */
#define CCM_CCGR1_BASE				(0X020C406C)	
#define SW_MUX_GPIO1_IO03_BASE		(0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE		(0X020E02F4)
#define GPIO1_DR_BASE				(0X0209C000)
#define GPIO1_GDIR_BASE				(0X0209C004)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;


struct newchrled_dev //定义一个结构体
{
    dev_t devid;            //定义一个设备号的变量
    struct cdev cdev;       //定义一个字符设备
    struct class* class;    //定义一个类
    struct device* device;  //定义一个设备
    int major;              //定义主设备号
    int minor;              //定义次设备号
};

struct newchrled_dev newchrled; //定义一个结构体变量


/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led_switch(u8 sta)
{
	u32 val = 0;
	if(sta == LEDON) {
		val = readl(GPIO1_DR);
		val &= ~(1 << 3);	
		writel(val, GPIO1_DR);
	}else if(sta == LEDOFF) {
		val = readl(GPIO1_DR);
		val|= (1 << 3);	
		writel(val, GPIO1_DR);
	}	
}

static int newchrled_open(struct inode *inode,struct file* filp) //open具体实现函数
{
    filp->private_data = &newchrled; //设置私有数据
    return 0;
}

static ssize_t newchrled_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt) //read具体实现函数
{
    return 0;
}

static ssize_t newchrled_write(struct file *filp,const char __user *buf,size_t cnt,loff_t *offt)//write具体实现函数
{

	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) {	
		led_switch(LEDON);		/* 打开LED灯 */
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	/* 关闭LED灯 */
	}

    return 0;
}

static int newchrled_release(struct inode *inode,struct file* filp) //release具体实现函数
{
    return 0;
}


static struct file_operations newchrled_fops = {  //字符设备操作集合
    .owner = THIS_MODULE,  //.owner成员
    .open = newchrled_open,//.open成员
    .write = newchrled_write,//.write成员
    .read = newchrled_read,//.read成员
    .release = newchrled_release, //.release成员
};


//驱动加载入口函数
static int __init newchrled_init(void)
{
    int ret;
	u32 val = 0;

	/* 初始化LED */
	/* 1、寄存器地址映射 */
  	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
  	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

	/* 2、使能GPIO1时钟 */
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);	/* 清楚以前的设置 */
	val |= (3 << 26);	/* 设置新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 3、设置GPIO1_IO03的复用功能,将其复用为
	 *    GPIO1_IO03,最后设置IO属性。
	 */
	writel(5, SW_MUX_GPIO1_IO03);
	
	/*寄存器SW_PAD_GPIO1_IO03设置IO属性
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
	 */
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 4、设置GPIO1_IO03为输出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	/* 清除以前的设置 */
	val |= (1 << 3);	/* 设置为输出 */
	writel(val, GPIO1_GDIR);

	/* 5、默认关闭LED */
	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

    /*1.注册字符设备并申请设备号*/
    if(newchrled.major)//如果有指定的主设备号的话,就使用指定的设备号
    {
        newchrled.devid = MKDEV(newchrled.major,newchrled.minor); //通过主设备号和次设备号生成设备号
        ret = register_chrdev_region(newchrled.devid,NEWCHRLED_CNT,NEWCHRLED_NAME); //向内核注册字符设备驱动
        if(ret < 0) //错误处理
        {
            printk("register failed!!\r\n");
            return -1;
        }
    }
    else //如果没有制定的主设备号的花,就用动态分配的方式来申请设备号
    {
        ret = alloc_chrdev_region(&newchrled.devid,0,NEWCHRLED_CNT,NEWCHRLED_NAME); //动态申请并注册字符设备驱动
        if(ret < 0)//错误处理
        {
            printk("register failed!!\r\n");
            return -1;
        }
        newchrled.major = MAJOR(newchrled.devid); //使用宏MAJOR来得到申请的设备的主设备号
        newchrled.minor = MINOR(newchrled.devid); //使用宏MINOR来得到申请的设备的次设备号
    }

    /*2.初始化字符设备cdev并向内核添加字符设备*/
    newchrled.cdev.owner = THIS_MODULE;
    cdev_init(&newchrled.cdev,&newchrled_fops); //参数newchrled_fops是字符设备操作集合,需要在前面定义

    cdev_add(&newchrled.cdev,newchrled.devid,NEWCHRLED_CNT);

    /*3.创建类和设备*/
    newchrled.class = class_create(THIS_MODULE,NEWCHRLED_NAME);//使用class_create来创建类
    if(IS_ERR(newchrled.class))//使用宏IS_ERR来判断创建类是否失败
    {
        unregister_chrdev_region(newchrled.devid,NEWCHRLED_CNT); //如果失败,首先释放掉申请的设备号
        cdev_del(&newchrled.cdev);                               //删除字符设备
        return PTR_ERR(newchrled.class);               //返回失败值
    }

    newchrled.device = device_create(newchrled.class,NULL,newchrled.devid,NULL,NEWCHRLED_NAME);//使用device_create来创建设备
    if(IS_ERR(newchrled.device))  //来判断创建设备是否失败
    {
        unregister_chrdev_region(newchrled.devid,NEWCHRLED_CNT); //如果失败,释放设备号
        cdev_del(&newchrled.cdev);                               //删除字符设备
        class_destroy(newchrled.class);                          //摧毁类
        return PTR_ERR(newchrled.device);
    }    

    return 0;
}


//驱动卸载出口函数
static void __exit newchrled_exit(void)
{
	u32 val;
	
	/*在卸载驱动的时候默认关闭LED灯*/
	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

	/* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);

    unregister_chrdev_region(newchrled.devid,NEWCHRLED_CNT); //如果失败,释放设备号
    cdev_del(&newchrled.cdev);                               //删除字符设备

    device_destroy(newchrled.class,newchrled.devid);         //摧毁设备,在出口函数中一定要线摧毁设备再摧毁类,因为一旦先摧毁类,那么就无法通过类找到设备了
    class_destroy(newchrled.class);                          //摧毁类

}


module_init(newchrled_init);  //注册模块加载函数 
module_exit(newchrled_exit);  //注册模块卸载函数
MODULE_LICENSE("GPL");
MODULE_AUTHOR("kk");

4.2. 应用程序

由于本次实验和上一篇文章实现的效果是相同的,所以我们就直接用上一篇文章中的应用程序即可。

当编写完成驱动代码和应用程序代码以后,需要将Makefile文件中生成的目标文件名进行修改
然后在终端中分别通过make命令和arm-linux-gnueabihf-gcc ledAPP.c -o ledAPP来生成led.ko模块和ledAPP应用程序,拷贝到rootfs下进行测试。

5. 运行测试

在开发板中打开/rootfs/lib/modules/4.1.15然后可以看到有newchrled.ko和newchrledAPP两个文件,然后使用命令depmod(第一次加载驱动的时候需要执行此命令)和modprobe newchrled.ko来加载驱动。
在这里插入图片描述
我们可以通过命令来查看自动申请的设备号,通过cat /proc/devices命令来查看
在这里插入图片描述
我们可以看到,第249号就是内核自动帮我们申请的设备号

然后我们通过ls /dev/newchrled -l命令来查看内核是否自动帮我们申请好了设备节点
在Linux系统中,设备节点通常存储在/dev目录下。我们可以使用ls命令或ls -l /dev命令来列出该目录下的所有设备节点。如果设备节点已经存在,则说明内核已经为该设备创建了相应的设备节点。
在这里插入图片描述
我们看到,节点已经存在,说明我们使用驱动程序来自动创建设备节点的工作就已经可以成功了。
接下来我们就是操作应用程序来实现点灯和关灯的操作。
使用命令./ledAPP /dev/newchrled 1命令,可以看到LED灯打开
在这里插入图片描述
可以看到灯已经亮了;

然后使用命令./ledAPP /dev/newchrled 0命令,可以看到LED灯关闭
在这里插入图片描述
可以看到灯已经灭了。

最后使用rmmod newchrled.ko卸载掉驱动,整个新字符设备驱动开发的实验就完成了。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
在CentOS中,字符设备驱动是用于与字符设备进行通信的驱动程序。字符设备驱动的作用是将用户空间的数据传输到设备或将设备的数据传输到用户空间。它通过文件系统中的特殊文件(例如/dev/null或/dev/tty)来实现对字符设备的访问。 在Linux内核中,字符设备驱动通常由两个主要组件组成:设备号和字符设备文件。设备号是一个唯一的标识符,用于标识设备对应的驱动程序。字符设备文件则是用户访问设备的接口,通过读写这些文件可以与字符设备进行交互。 字符设备驱动的框架通常包括以下步骤: 1. 在初始化时申请设备号,可以通过静态申请或动态申请来获取设备号。 2. 创建字符设备,包括分配内存和初始化设备数据结构。 3. 将设备号和字符设备关联,可以通过调用register_chrdev函数将设备号注册到内核中。 4. 在设备被释放时,需要销毁字符设备并删除申请的设备号。 在CentOS中,字符设备驱动的编写和管理过程可能会有所不同,具体的实现方法可以参考Linux内核文档和相关的教程和示例代码。 引用 提供了字符设备驱动设备号的定义和作用。 引用 提供了字符设备的定义和一些常见的字符设备的例子。 引用 可以提供更详细的关于字符设备驱动的信息和教程。 请注意,字符设备驱动的具体实现和管理可能因不同的操作系统版本和配置而有所差异。建议在编写和管理字符设备驱动时参考官方文档和相关的资料。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Linux驱动开发——字符设备驱动](https://blog.csdn.net/weixin_43920383/article/details/126487907)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [linux字符设备驱动](https://blog.csdn.net/lsyrhz/article/details/123994257)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

嵌入式进阶之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值