新字符设备驱动实验

news2024/11/25 16:23:06

目录:

  • 1. 新字符设备驱动原理
    • 1.1. 分配和释放设备号
    • 1.2. 添加字符设备
  • 2.自动创建设备节点
    • 2.1. mdev机制
    • 2.2. 创建和删除类
    • 2.3. 创建设备
    • 2.4. 新字符设备驱动框架总结
  • 3. 文件结构体和文件私有数据
  • 4. 实验程序编写
    • 4.1. 驱动程序
    • 4.2. 应用程序
  • 5. 运行测试

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卸载掉驱动,整个新字符设备驱动开发的实验就完成了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/753942.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RabbitMQ 同样的操作一次成功一次失败

RabbitMQ 是一个功能强大的消息队列系统&#xff0c;广泛应用于分布式系统中。然而&#xff0c;我遇到这样的情况&#xff1a;执行同样的操作&#xff0c;一次成功&#xff0c;一次失败。在本篇博文中&#xff0c;我将探讨这个问题的原因&#xff0c;并提供解决方法。 我是在表…

DatenLord前沿技术分享 No.30

达坦科技专注于打造新一代开源跨云存储平台DatenLord&#xff0c;通过软硬件深度融合的方式打通云云壁垒&#xff0c;致力于解决多云架构、多数据中心场景下异构存储、数据统一管理需求等问题&#xff0c;以满足不同行业客户对海量数据跨云、跨数据中心高性能访问的需求。在本周…

【雕爷学编程】Arduino动手做(161)---16路PWM舵机驱动板2

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

windows搭建git服务器 无法识别 ‘git‘ 命令:exec: “git“: executable file not found in %PATH%

无法识别 git 命令&#xff1a;exec: "git": executable file not found in %PATH% 确保已经安装git&#xff0c;如下图配置环境变量即可。

ylb-接口7注册发送短信

总览&#xff1a; 在common模块下引入短信验证码的依赖项&#xff08;生成4位随机数&#xff09;&#xff1a; 在web模块下的resources/application.yml&#xff0c;添加配置信息&#xff08;京东万象&#xff09;&#xff1a; #短信配置 jdwx:sms:url: https://way.jd.com…

在 3ds Max 中使用Mental Ray渲染 wip 图像

推荐&#xff1a; NSDT场景编辑器助你快速搭建可二次开发的3D应用场景 本教程面向初学者&#xff0c;每个步骤都详细概述和显示。如果您是 3D Studio MAX 的新手&#xff0c;您可能想先尝试我们的其他一些教程。 使用默认的 3D Studio MAX 渲染器创建粘土渲染 步骤 1 在 3D S…

Unity DOTS如何优雅地实现ECS框架下的定时器Timer系统(无对象池,零GC)

实现定时器并不复杂&#xff0c;就是写个类存放回调&#xff0c;再写个类来统一管理这些回调类。但是ECS写代码的方式变了&#xff0c;所以还是有些区别的。 实现过程中需要注意的几点&#xff1a; 1、由于IComponentData不能存放managed类型的数据&#xff0c;所以无法用常规…

微服务架构+创建微服务工程(商品/订单)

目录 1.微服务架构 1.1.单体应用架构 1.2.微服务应用 1.3 微服务架构的优势 1.4.微服务架构的缺点(挑战) 1.5. SpringCloud与微服务关系 1.6.SpringBoot和SpringCloud关系 2. 创建微服务工程 2.1.数据库 2.2.搭建父工程 2.2 创建公共模块 2.3.商品系统 2.4.订单微…

【通讯协议备忘录】stm32的CAN外设

文章目录 帧结构测试模式&#xff08;静默/换回/环回静默&#xff09;&#xff1a;环回测试配置 过滤器的使用测试参考用例过滤器的初始化发送和接收 中断 帧结构 CAN的报文结构&#xff1a; 测试模式&#xff08;静默/换回/环回静默&#xff09;&#xff1a; 静默模式&…

Apache(httpd) 搭建笔记

Apache 搭建笔记 安装Apache HTTP服务器&#xff1a;启动Apache服务并设置开机自启 配置SSL证书配置Apache的SSL虚拟主机&#xff1a;重启Apache服务以使更改生效&#xff1a; 多站点配置第一个虚拟主机配置第二个虚拟主机创建每个站点的根目录&#xff1a; 强制跳转http>&g…

mysql及事务隔离级别

目录 一 事务之间相互影响分为几种 二 mysql常见的储存引擎 三 死锁 四 查看使用的储存引擎 五 修改储存引擎 六 总结 一 事务之间相互影响分为几种 脏读:就是读取了没有提交的数据, 不可重复读 :前后多次读取内容不一致 幻读:两次读的结果不一样 丢失更新:后一个会覆…

哈夫曼编码(霍夫曼、赫夫曼)

一、发展历史 哈夫曼使用自底向上的方法构建二叉树。 哈夫曼编码的基本方法是先对图像数据扫描一遍&#xff0c;计算出各种像素出现的概率&#xff0c;按概率的大小指定不同长度的唯一码字&#xff08;这种长度不同的编码方式称为变长编码&#xff0c;对应的长度相同的编码方…

关闭Vue CLI(脚手架)中的语法检查

1.创建一个名为vue.config.js的文件&#xff0c;与package.json文件平级 参考官方文档&#xff1a;Home | Vue CLI (vuejs.org) 2.将下面代码复制进vue.config.js文件中 module.exports{lintOnSave:false;//关闭语法检查 }

c++计算贝塞尔曲线(折线平滑为曲线)坐标方法

效果可查看上一篇博文&#xff1a;js手动画平滑曲线&#xff0c;贝塞尔曲线拟合【代码】js手动画平滑曲线&#xff0c;贝塞尔曲线拟合。https://blog.csdn.net/qiufeng_xinqing/article/details/131711963?spm1001.2014.3001.5502 代码如下&#xff1a; #include <cmath&…

10.6.1 【Linux】撷取命令: cut, grep

cut cut 主要的用途在于将“同一行里面的数据进行分解&#xff01;”最常使用在分析一些数据或文字数据的时候。这是因为有时候我们会以某些字符当作分区的参数&#xff0c;然后来将数据加以切割&#xff0c;以取得我们所需要的数据。 grep 10.6.2 排序命令&#xff1a; sort,…

最火爆的大模型框架LangChain七大核心及案例剖析上(一)

最火爆的大模型框架LangChain七大核心及案例剖析上 10.1 Models解析及案例剖析 本节正式进入当前开源界最火爆的大模型开发框架LangChain的部分,会讲解整个LangChain解决的问题及它的工作机制,通过一个“LangChain GPT内容创建者”(“LangChain GPT Content Creator”)的具体…

第九章:RefineNet——多路径细化网络用于高分辨率语义分割

0.摘要 最近&#xff0c;非常深的卷积神经网络&#xff08;CNN&#xff09;在目标识别方面表现出色&#xff0c;并且也是密集分类问题&#xff08;如语义分割&#xff09;的首选。然而&#xff0c;在深度CNN中&#xff0c;重复的子采样操作&#xff08;如池化或卷积跳跃&#x…

Matplotlib figure图形对象

通过前面的学习&#xff0c;我们知道matplotlib.pyplot模块能够快速地生成图像&#xff0c;但如果使用面向对象的编程思想&#xff0c;我们就可以更好地控制和自定义图像。 在 Matplotlib 中&#xff0c;面向对象编程的核心思想是创建图形对象&#xff08;figure object&#…

第十三章——类继承

面向对象编程的主要目的之一是提供可重用的代码。&#xff08;重用经过测试的代码比重新编写代码要好的多&#xff09; C类提供了更高层次的重用性。很多厂商提供了类库&#xff0c;类组合了数据表示和类方法&#xff0c;因此提供了比函数库更加完整的程序包。通常类库是以源代…

文件IO_文件读写(附Linux-5.15.10内核源码分析)

目录 1.什么是文件偏移量&#xff1f; 1.1 文件偏移量介绍 1.2 文件偏移量重点 1.3 文件偏移量工作原理 2.文件偏移量设置 2.1 lseek函数 2.2 lseek内核源码分析 3.写文件 3.1 write函数 3.2 write内核源码分析 4.读文件 4.1 read函数 4.2 read内核源码分析 5.文…