字符设备驱动高级

一、注册字符设备驱动新接口

https://blog.csdn.net/huhuandk/article/details/98845705

1、新接口与老接口

(1)老接口:register_chrdev

  注册的关键是主次设备号

(2)新接口:register_chrdev_region/alloc_chrdev_region + cdev

int register_chrdev_region(dev_t from, unsigned count, const char *name)
	First:要分配的设备编号范围的初始值(次设备号常设为0);
	Count:连续编号范围.
	Name:编号相关联的设备名称. (/proc/devices);

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, 
const char *name)
	baseminor : 通常为0;
	*dev:存放返回的设备号;

(3)为什么需要新接口
  register_chrdev()函数是老版本里面的设备号注册函数,可以实现静态和动态注册两种方法,主要是通过给定的主设备号是否为0来进行区别,为0的时候为动态注册。register_chrdev_region以及alloc_chrdev_region就是将上述函数的静态和动态注册设备号进行了拆分的强化。

2、cdev介绍

(1)cdev是一个结构体,里面的成员来共同帮助我们注册驱动到内核中,表达字符设备的,将这个struct cdev结构体进行填充,主要填充的内容就是

struct cdev {
 
	struct kobject kobj;

	struct module *owner;//填充时,值要为 THIS_MODULE,表示模块
 
	const struct file_operations *ops;//这个file_operations结构体,注册驱动的关键,要填充成这个结构体变量
 
	struct list_head list;
 
	dev_t dev;//设备号,主设备号+次设备号
 
	unsigned int count;//次设备号个数 
};

file_operationsile_operations这个结构体变量,让cdev中的ops成员的值为file_operations结构体变量的值。这个结构体会被cdev_add函数想内核注册。

(2)相关函数:

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

struct cdev *cdev_alloc(void);

void cdev_put(struct cdev *p);

int cdev_add(struct cdev *, dev_t, unsigned);

void cdev_del(struct cdev *);

int cdev_index(struct inode *inode);
(这些内容自己可通过查找内核源码分析学习)

cdev结构体,可以用很多函数来操作他。如:
cdev_alloc:让内核为这个结构体分配内存的
cdev_init:将struct cdev类型的结构体变量和file_operations结构体进行绑定的
cdev_add:向内核里面添加一个驱动,注册驱动
cdev_del:从内核中注销掉一个驱动。注销驱动

3、设备号

(1)由主设备号 和 次设备号组成

(2)dev_t类型
  设备文件的关键,用于搜索设备文件,不同的内核中定义是不一样,有的是16位次设备号和16位主设备号构成 有的是20位次设备号12位主设备号

(3)MKDEV、MAJOR、MINOR三个宏

MKDEV:是用来将主设备号和次设备号,转换成一个主次设备号的。(设备号)
MAJOR:从设备号里面提取出来主设备号的。
MINOR:从设备号中提取出来次设备号的。

4、编程实践

(1)使用register_chrdev_region + cdev_init + cdev_add进行字符设备驱动注册

(2)和注册分配字符设备编号范围类似,内核提供了两个注销字符设备编号范围的函数,分别是 unregister_chrdev_region() 和 unregister_chrdev() 。它们都调用了 __unregister_chrdev_region() 函数。

static struct char_device_struct * __unregister_chrdev_region(unsigned major, unsigned baseminor, int minorct)

//使用新的接口来注销字符设备驱动

//注销分2步:
	
//第一步:真正注销字符设备驱动用cdev_del
cdev_del(&test_cdev);

//第二步:注销申请的主次设备号
unregister_chrdev_region(mydev, MYCNT);

5、使用alloc_chrdev_region自动分配设备号

(1)register_chrdev_region是在事先知道要使用的主、次设备号时使用的;要先查看cat /proc/devices去查看没有使用的。

(2)更简便、更智能的方法是让内核给我们自动分配一个主设备号,使用alloc_chrdev_region就可以自动分配了。

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)
{
	struct char_device_struct *cd;
	cd = __register_chrdev_region(0, baseminor, count, name);
	if (IS_ERR(cd))
		return PTR_ERR(cd);
	*dev = MKDEV(cd->major, cd->baseminor);
	return 0;
}

dev :alloc_chrdev_region函数向内核申请下来的设备号

baseminor :次设备号的起始

count: 申请次设备号的个数

name :执行 cat /proc/devices显示的名称

1.执行cdev_init函数,将cdev和file_operations关联起来

2.使用cdev_add函数,将cdev和设备号关联起来

(3)自动分配的设备号,我们必须去知道他的主次设备号,否则后面没法去mknod创建他对应的设备文件。

6、得到分配的主设备号和次设备号

(1)使用MAJOR宏和MINOR宏从dev_t得到major和minor

(2)反过来使用MKDEV宏从major和minor得到dev_t。

(3)使用这些宏的代码具有可移植性

7、中途出错的倒影式错误处理方法

  C语言没有错误处理机制,所以一旦程序某个部分出错,需要退出的时候,就需要先把之前分配的资源先释放掉再退出。

  内核中很多函数中包含了很多个操作,这些操作每一步都有可能出错,而且出错后后面的步骤就没有进行下去的必要性了。而且可以释放前面步骤所申请的资源,使用goto语句

8、使用cdev_alloc

(1)cdev_alloc的编程实践

struct cdev *cdev_alloc(void)
{
	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if (p) {
		INIT_LIST_HEAD(&p->list);
		kobject_init(&p->kobj, &ktype_cdev_dynamic);
	}
	return p;
}

(2)从内存角度体会cdev_alloc用与不用的差别

.data段
栈
堆内存

static struct cdev *pcdev;
pcdev = cdev_alloc();			// 给pcdev分配内存,指针实例化
否则就会成为野指针,此外这样使用内存空间比较方便,堆内存可以按需申请分配,且不用占用
数据段或者堆的大量内存,结构体的大小一般为多个字节,而是用指针则只占用四字节(在32位
系统上)。

cdev_del内部会调用相关函数kfree(found)对cdev_alloc所申请的内存进行释放。

cdev_alloc内部会对申请的内存进行记录,cdev_del内部调用的kfree(found)会去寻找标记
释放申请的内存,若是使用

static struct cdev test_cdev;则使用的是数据段,cdev_del函数中释放则无意义,找不
到申请的内存。
 
pcdev = cdev_alloc();			// 给pcdev分配内存,指针实例化
//cdev_init(pcdev, &test_fops);

pcdev->owner = THIS_MODULE;//在使用cdev_alloc时,此句代码和下一句代码可代替cdev_init,该函数所做的
pcdev->ops =&test_fops;//工作,在cdev_alloc进行了一部分,在加上这两条语句即可实现该函数的所有功能
如果使用了cdev_alloc函数申请的内存,可以在下面直接对ops变量赋值,而不需要调用cdev_init再一次初始化,进而提高效率。

(3)这就是非面向对象的语言和面向对象的代码

#include <linux/module.h>               // module_init  module_exit
#include <linux/init.h>                 // __init   __exit
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h>             // arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include <linux/string.h>
#include <linux/io.h>
#include <linux/ioport.h>
#include <linux/cdev.h>

//#define MYMAJOR               200
#define MYCNT           1
#define MYNAME          "testchar"

#define GPJ0CON_PA      0xe0200240
#define GPJ0DAT_PA      0xe0200244

//int mymajor;
static dev_t mydev;   //设备号
//static struct cdev test_cdev;//其内成员来帮助我们注册驱动到内核中,用于表达字符设备
static struct cdev *pcdev;//使用指针的方式结合struct cdev *cdev_alloc(void)函数

unsigned int *rGPJ0CON;
unsigned int *rGPJ0DAT;

char kbuf[100] = {0};//内核空间的buf

ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
        int ret = -1;

        printk(KERN_INFO "this is test_chrdev_read.\n");

        ret = copy_to_user(ubuf, kbuf, count);
        if (ret)
        {
                printk(KERN_ERR "copy_to_user fail.\n");

                return -EINVAL;
        }
        else
        {
                printk(KERN_INFO "copy_to_user successfully.\n");

        }

        return 0;
}

//写函数的本质就是,将应用层传递过来的数据先复制到内核中,然后以正确的
//方式写入硬件完成操作

ssize_t test_chrdev_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
{
        int ret = -1;

        printk(KERN_INFO "this is test_chrdev_write.\n");

        //使用该函数将应用层传过来的ubuf的内容拷贝到驱动空间中的一个buf中
        //memcpy(kbuf, ubuf);  //错误,这两个不在一个地址空间中,一个属于内核,一个属于应用
        memset(kbuf, 0, sizeof(kbuf));
        ret = copy_from_user(kbuf, ubuf, count);
        if (ret)
        {
                printk(KERN_ERR "copy_from_user fail.\n");
                return -EINVAL;
        }
        else
        {
                printk(KERN_INFO "copy_from_user successfully.\n");

        }

        //真正驱动中,数据从应用层复制到驱动中后, 我们要根据这个数据
        //去操作硬件,所以下面应该就是操作硬件的代码
#if 0
        //方式1:
        if (kbuf[0] == '0')//灯灭
        {
                *rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
        }
        else if (kbuf[0] == '1')//灯亮
        {
                *rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
        }
#endif

        //方式2:

        if (!strcmp(kbuf, "on"))
        {
                *rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));

        }
        else if (!strcmp(kbuf, "off"))
        {
                *rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));

        }

        return 0;
}


int test_chrdev_open(struct inode *inode, struct file *file)
{
        //该函数实现打开这个设备的硬件操作代码
        printk(KERN_INFO "this is test_chrdev_open.\n");
        *rGPJ0CON = 0x11111111;
        *rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));         // 亮

        return 0;
}


int test_chrdev_release(struct inode *node, struct file *file)//对应close函数
{
        printk(KERN_INFO "this is test_chrdev_release.\n");
        *rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));         // 灭
        return 0;
}

// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
        .owner          = THIS_MODULE,// 惯例,直接写即可

        .open           = test_chrdev_open,// 将来应用open打开这个设备时实际调用的
        .release        = test_chrdev_release,// 就是这个.open对应的函数,,关闭释放
        .write          = test_chrdev_write,
        .read           = test_chrdev_read,
};

//模块安装函数
static int __init chrdev_init(void)
{
        int retval;

        printk(KERN_INFO "chrdev_init helloworld init\n");

        //使用新的cdev接口来注册字符设备驱动
        //新的接口注册字符设备驱动需要两步

        //第一步:注册/分配主次设备号

        //方式1:指定主次设备号
        //mydev = MKDEV(MYMAJOR, 0);//是用来将主设备号和次设备号,转换成一个主次设备号的
        //retval = register_chrdev_region(mydev, MYCNT, MYNAME);

        //方式2:自动分配设备号
        retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
        if (retval)
        {
                printk(KERN_ERR "Unable to alloc_chrdev_region minors for %s\n", MYNAME);
                //return -EINVAL;
                goto flag1;
        }
        else
        {
                printk(KERN_INFO "alloc_chrdev_region success\n");
                printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev));
                retval = 0;
        }


        //第二步:注册字符设备驱动

        //cdev_init(&test_cdev, &test_fops);

        pcdev = cdev_alloc();
        pcdev->owner = THIS_MODULE;
        pcdev->ops = &test_fops;

        //retval = cdev_add(&test_cdev, mydev, MYCNT);
        retval = cdev_add(pcdev, mydev, MYCNT);
        if (retval)
        {
                printk(KERN_ERR "Unable to cdev_add\n");
                //return -EINVAL;
                goto flag2;
        }
        else
        {
                printk(KERN_INFO "cdev_add successfully\n");

        }


        //真正驱动中,数据从应用层复制到驱动中后, 我们要根据这个数据
        //去操作硬件,所以下面应该就是操作硬件的代码
        //使用动态映射的方式来操作寄存器

        //使用动态映射的方式来操纵寄存器
#if 1
        //方式1:

        //申请
        if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
                return -EINVAL;
        if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0DAT"))
                return -EINVAL;
        //建立连接,真正实现映射
                rGPJ0CON = ioremap(GPJ0CON_PA, 4);
                rGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
#endif

#if 0
        //方式2:
        //申请
        if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
                //return -EINVAL;
                goto flag3;
        //建立连接,真正实现映射
                rGPJ0CON = ioremap(GPJ0CON_PA, 4);
                rGPJ0DAT = rGPJ0CON + 1;//使用这种方式在卸载模块时在该文件出现了段错误
#endif
        return 0;


// 如果第4步才出错跳转到这里来
#if 0
flag4:
        release_mem_region(GPJ0CON_PA, 4);
        release_mem_region(GPJ0DAT_PA, 4);
#endif

//如果第三步出错跳转到这里来
flag3:
        cdev_del(pcdev);

//将第一步 成功做出的东西释放掉
flag2:
        unregister_chrdev_region(mydev, MYCNT);

//如果第一步出错跳转到这里来
flag1:
        return -EINVAL;

}


//模块卸载函数
static void __exit chrdev_exit(void)
{
        printk(KERN_INFO "chrdev_exit hellowworld exit\n");

        //解除动态映射,先解除映射在释放申请

        iounmap(rGPJ0CON);
        iounmap(rGPJ0DAT);

        release_mem_region(GPJ0CON_PA, 4);
        release_mem_region(GPJ0DAT_PA, 4);


        //在module_exit调用的函数中去注销字符设备驱动
        //使用新的接口来注销字符设备驱动
        //注销分2步:

        //第一步:真正注销字符设备驱动用cdev_del
        //cdev_del(&test_cdev);
        cdev_del(pcdev);

        //第二步:注销申请的主次设备号
        unregister_chrdev_region(mydev, MYCNT);

}

module_init(chrdev_init);
module_exit(chrdev_exit);

//MODULE_xxx这种宏的作用是用来添加模块描述信息的
MODULE_LICENSE("GPL");                          // 描述模块的许可证
MODULE_AUTHOR("Mr.zhang");                              // 描述模块的作者
MODULE_DESCRIPTION("module test");      // 描述模块的介绍信息
MODULE_ALIAS("alias xxx");                      // 描述模块的别名信息

9、cdev_init的替代
(1)cdev_init源码分析

void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
	memset(cdev, 0, sizeof *cdev);
	INIT_LIST_HEAD(&cdev->list);
	kobject_init(&cdev->kobj, &ktype_cdev_default);
	cdev->ops = fops;
}

二、字符设备驱动注册代码分析

1、老接口分析

函数调用关系:
register_chrdev
	__register_chrdev
		__register_chrdev_region
		cdev_alloc
		cdev_add

2、新接口分析

函数调用关系:
register_chrdev_region
	__register_chrdev_region

alloc_chrdev_region
	__register_chrdev_region

三、自动创建字符设备驱动的设备文件

1、问题描述:
(1)使用mknod命令创建设备文件的缺点
需要手动删除

(2)能否自动生成和删除设备文件

2、解决方案:udev(嵌入式中用的是mdev)
详解:https://blog.csdn.net/lifengxun20121019/article/details/17403527

(1)什么是udev?应用层的一个应用程序
  udev 是基于netlink 机制的,它在系统启动时运行了一个deamon 程序udevd,通过监听内核发送的uevent 来执行相应的热拔插动作,包括创建/删除设备节点,加载/卸载驱动模块等等。

  mdev 是基于uevent_helper 机制的,它在系统启动时修改了内核中的uevnet_helper 变量(通过写/proc/sys/kernel/hotplug),值为“/sbin/mdev”。这样内核产生uevent 时会调用uevent_helper 所指的用户级程序,也就是mdev,来执行相应的热拔插动作。

  udev 使用的netlink 机制在有大量uevent 的场合效率高,适合用在PC 机上;而mdev 使用的uevent_helper 机制实现简单,适合用在嵌入式系统中。另外要说明的一点是,uevent_helper 的初始值在内核编译时时可配置的,默认值为/sbin/hotplug。如果想修改它的值,写/proc/sys/kernel/hotplug 文件就可以了,例如:

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

(2)内核驱动和应用层udev之间有一套信息传输机制(netlink协议)
Netlink 是一种特殊的 socket,它是Linux所特有的,由于传送的消息是暂存socket接收缓存中,并不被接收者立即处理,所以netlink是一种异步通信机制.系统调用和 ioctl 则是同步通信机制。

(3)应用层启用udev,内核驱动中使用相应接口

(4)驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除

3、内核驱动设备类相关函数
(1)class_create为设备创建一个class

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

(2)device_create
再为每个设备调用device_create创建对应的设备

struct device *device_create(struct class *class, struct device *parent,
			     dev_t devt, void *drvdata, const char *fmt, ...)//...表示支持变参

@class: pointer to the struct class that this device should be registered to.
@parent: pointer to the parent struct class_device of this new device, if any.
@devt: the dev_t for the char device to be added.
@fmt: string for the class device's name

(3)void device_destroy(struct class *class, dev_t devt)

内核2.6.15中的函数: 
class_device_create(); 
class_device_destroy();
在2.6.27中变为: 
device_create() 
device_destroy() 

(4)void class_destroy(struct class *cls)

4、编程实践
  类和对象的关系 类是对象的抽象,而对象是类的具体实例

  IS_ERR(),首先要理解内核空间,所有的驱动程序都运行在内核空间。内核空间虽然很大,但总是有限的。在这有限的空间中,其最后一个page是专门为错误码保留的,即内核用最后一页捕捉错误,一般人不可能用到内核空间最后一个page的指针。因此,在写设备驱动程序的过程中,涉及到的指针,必然有以下三种情况:有效指针;NULL,即空指针;错误指针,或者说无效指针。

  IS_ERR()就是用来判断指针是否有错,如果指针并不是指向最后一个page,那么没有问题;如果指针指向了最后一个page,那么说明实际上这不是一个有效的指针,这个指针里保存的实际上是一种错误代码。而通常很常用的方法就是先用IS_ERR()来判断是否是错误,然后如果是,那么就调用PTR_ERR()来返回这个错误代码。

#include <linux/module.h>               // module_init  module_exit
#include <linux/init.h>                 // __init   __exit
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h>             // arch/arm/mach-s5pv210/include/mach/gpio-bank.h
#include <linux/string.h>
#include <linux/io.h>
#include <linux/ioport.h>
#include <linux/cdev.h>
#include <linux/device.h>

#define MYCNT           1
#define MYNAME          "testchar"

#define GPJ0CON_PA      0xe0200240
#define GPJ0DAT_PA      0xe0200244

static dev_t mydev;   //设备号
static struct cdev *pcdev;//使用指针的方式结合struct cdev *cdev_alloc(void)函数
static struct class *test_class;


unsigned int *rGPJ0CON;
unsigned int *rGPJ0DAT;

char kbuf[100] = {0};//内核空间的buf

ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
        int ret = -1;

        printk(KERN_INFO "this is test_chrdev_read.\n");

        ret = copy_to_user(ubuf, kbuf, count);
        if (ret)
        {
                printk(KERN_ERR "copy_to_user fail.\n");

                return -EINVAL;
        }
        else
        {
                printk(KERN_INFO "copy_to_user successfully.\n");

        }

        return 0;
}

//写函数的本质就是,将应用层传递过来的数据先复制到内核中,然后以正确的
//方式写入硬件完成操作

ssize_t test_chrdev_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
{
        int ret = -1;

        printk(KERN_INFO "this is test_chrdev_write.\n");

        //使用该函数将应用层传过来的ubuf的内容拷贝到驱动空间中的一个buf中
        //memcpy(kbuf, ubuf);  //错误,这两个不在一个地址空间中,一个属于内核,一个属于应用
        memset(kbuf, 0, sizeof(kbuf));
        ret = copy_from_user(kbuf, ubuf, count);
        if (ret)
        {
                printk(KERN_ERR "copy_from_user fail.\n");
                return -EINVAL;
        }
        else
        {
                printk(KERN_INFO "copy_from_user successfully.\n");

        }

        //真正驱动中,数据从应用层复制到驱动中后, 我们要根据这个数据
        //去操作硬件,所以下面应该就是操作硬件的代码
#if 0
        //方式1:
        if (kbuf[0] == '0')//灯灭
        {
                *rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));
        }
        else if (kbuf[0] == '1')//灯亮
        {
                *rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));
        }
#endif

        //方式2:

        if (!strcmp(kbuf, "on"))
        {
                *rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));

        }
        else if (!strcmp(kbuf, "off"))
        {
                *rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));

        }

        return 0;
}


int test_chrdev_open(struct inode *inode, struct file *file)
{
        //该函数实现打开这个设备的硬件操作代码
        printk(KERN_INFO "this is test_chrdev_open.\n");
        *rGPJ0CON = 0x11111111;
        *rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));         // 亮

        return 0;
}


int test_chrdev_release(struct inode *node, struct file *file)//对应close函数
{
        printk(KERN_INFO "this is test_chrdev_release.\n");

        *rGPJ0DAT = ((1<<3) | (1<<4) | (1<<5));         // 灭

        return 0;
}



// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
        .owner          = THIS_MODULE,                          // 惯例,直接写即可

        .open           = test_chrdev_open,                     // 将来应用open打开这个设备时实际调用的
        .release        = test_chrdev_release,          // 就是这个.open对应的函数
        .write          = test_chrdev_write,
        .read           = test_chrdev_read,
};



//模块安装函数
static int __init chrdev_init(void)
{
        int retval;

        printk(KERN_INFO "chrdev_init helloworld init\n");

        //使用新的cdev接口来注册字符设备驱动
        //新的接口注册字符设备驱动需要两步

        //第一步:注册/分配主次设备号
        //自动分配设备号
        retval = alloc_chrdev_region(&mydev, 12, MYCNT, MYNAME);
        if (retval)
        {
                printk(KERN_ERR "Unable to alloc_chrdev_region minors for %s\n", MYNAME);
                //return -EINVAL;
                goto flag1;
        }
        else
        {
                printk(KERN_INFO "alloc_chrdev_region success\n");
                printk(KERN_INFO "major = %d, minor = %d.\n", MAJOR(mydev), MINOR(mydev));
                retval = 0;
        }


        //第二步:注册字符设备驱动

        //cdev_init(&test_cdev, &test_fops);

        pcdev = cdev_alloc();
        pcdev->owner = THIS_MODULE;
        pcdev->ops = &test_fops;

        //retval = cdev_add(&test_cdev, mydev, MYCNT);
        retval = cdev_add(pcdev, mydev, MYCNT);
        if (retval)
        {
                printk(KERN_ERR "Unable to cdev_add\n");
                //return -EINVAL;
                goto flag2;
        }
        else
        {
                printk(KERN_INFO "cdev_add successfully\n");

        }

        //注册字符设备驱动完成后,添加设备类文件,以让内核帮我们发信息
        //给udev,让udev自动创建和删除设备文件

        test_class = class_create(THIS_MODULE, "zhang_class");
        if (IS_ERR(test_class))
                return -EINVAL;
        //最后一个参数字符串,就是将来我们要在/dev目录下创建的设备文件的名字
        device_create(test_class, NULL, mydev, NULL, "test111");


        //真正驱动中,数据从应用层复制到驱动中后, 我们要根据这个数据
        //去操作硬件,所以下面应该就是操作硬件的代码
        //使用动态映射的方式来操作寄存器

        //使用动态映射的方式来操纵寄存器
#if 1
        //方式1:

        //申请
        if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
                return -EINVAL;
        if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0DAT"))
                return -EINVAL;
        //建立连接,真正实现映射
                rGPJ0CON = ioremap(GPJ0CON_PA, 4);
                rGPJ0DAT = ioremap(GPJ0DAT_PA, 4);
#endif

#if 0
        //方式2:
        //申请
        if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))
                //return -EINVAL;
                goto flag3;
        //建立连接,真正实现映射
                rGPJ0CON = ioremap(GPJ0CON_PA, 4);
                rGPJ0DAT = rGPJ0CON + 1;//使用这种方式在卸载模块时在该文件出现了段错误
#endif
        return 0;

//如果第三步出错跳转到这里来
flag3:
        cdev_del(pcdev);

//将第一步 成功做出的东西释放掉
flag2:
        unregister_chrdev_region(mydev, MYCNT);

//如果第一步出错跳转到这里来
flag1:
        return -EINVAL;

}


//模块卸载函数
static void __exit chrdev_exit(void)
{
        printk(KERN_INFO "chrdev_exit hellowworld exit\n");

        //解除动态映射,先解除映射在释放申请

        iounmap(rGPJ0CON);
        iounmap(rGPJ0DAT);

        release_mem_region(GPJ0CON_PA, 4);
        release_mem_region(GPJ0DAT_PA, 4);

        //删除创建的设备文件
        device_destroy(test_class, mydev);
        class_destroy(test_class);

        //在module_exit调用的函数中去注销字符设备驱动
        //使用新的接口来注销字符设备驱动
        //注销分2步:

        //第一步:真正注销字符设备驱动用cdev_del
        //cdev_del(&test_cdev);
        cdev_del(pcdev);

        //第二步:注销申请的主次设备号
        unregister_chrdev_region(mydev, MYCNT);

}

module_init(chrdev_init);
module_exit(chrdev_exit);

//MODULE_xxx这种宏的作用是用来添加模块描述信息的
MODULE_LICENSE("GPL");                          // 描述模块的许可证
MODULE_AUTHOR("Mr.zhang");                              // 描述模块的作者
MODULE_DESCRIPTION("module test");      // 描述模块的介绍信息
MODULE_ALIAS("alias xxx");                      // 描述模块的别名信息

四、设备类相关代码分析

详解:https://blog.csdn.net/cjsycyl/article/details/46309451
1、sys文件系统简介
  在 sysfs 下的很多 kobject 下都有 uevent 属性,它主要用于内核与 udev (自动设备发现程序)之间的一个通信接口;从 udev 本身与内核的通信接口 netlink 协议套接字来说,它并不需要知道设备的 uevent 属性文件,但多了 uevent 这样一个接口,可用于 udevmonitor 通过内核向 udevd (udev 后台程序)发送消息,也可用于检查设备本身所支持的 netlink 消息上的环境变量,这个特性一般用于开发人员调试 udev 规则文件, udevtrigger 这个调试工具本身就是以写各设备的 uevent 属性文件实现的。

(1)sys文件系统的设计思想
  Sysfs文件系统是一个类似于proc文件系统的特殊文件系统,用于将系统中的设备组织成层次结构,并向用户模式程序提供详细的内核数据结构信息。

  其实,就是 在用户态可以通过对sys文件系统的访问,来看内核态的一些驱动或者设备等。

(2)设备类的概念

  在Linux设备模型中,Class的概念非常类似面向对象程序设计中的Class(类),它主要是集合具有相似功能或属性的设备,这样就可以抽象出一套可以在多个设备之间共用的数据结构和接口函数。因而从属于相同Class的设备的驱动程序,就不再需要重复定义这些公共资源,直接从Class中继承即可。

(3)/sys/class/xxx/中的文件的作用

2、此部分内容:自己先去浏览分析内核源代码,尝试去理解分析,再去看视频,这样效果可能更佳。

从内核摘录出的一段代码:

adbdev_init(void)
{
	if (register_chrdev(ADB_MAJOR, "adb", &adb_fops)) {
		printk(KERN_ERR "adb: unable to get major %d\n", ADB_MAJOR);
		return;
	}

	adb_dev_class = class_create(THIS_MODULE, "adb");                    //class_create
	if (IS_ERR(adb_dev_class))
		return;
	device_create(adb_dev_class, NULL, MKDEV(ADB_MAJOR, 0), NULL, "adb");//device_create

	platform_device_register(&adb_pfdev);
	platform_driver_probe(&adb_pfdrv, adb_dummy_probe);
}
(1)
class_create
	__class_create
		__class_register
			kset_register
				kobject_uevent
	
(2)
device_create
	device_create_vargs
		kobject_set_name_vargs
		device_register
			device_add
				kobject_add
				device_create_file//以下都是用来操作sys的 
				device_create_sys_dev_entry
				devtmpfs_create_node
				device_add_class_symlinks
				device_add_attrs
				device_pm_add
				kobject_uevent

可变参数(解决不定数目函数参数问题):函数参数列表最后为"..."

详解:https://blog.csdn.net/wutao_water/article/details/9531151
参数在堆栈中的分布、位置:在进程中,堆栈地址是从高到低分配的,当执行一个函数的时候,将参数列表压入堆栈的高地址部分,然后入栈函数的返回地址,接着入栈函数的执行代码,这个入栈的过程,堆栈地址在不断递减,一些黑客就是通过在堆栈中修改函数的返回地址,执行自己的代码来达到执行自己插入的代码段的目的。

总之,函数在堆栈中的分布情况是:地址从高到低,依次是,函数参数列表,函数返回地址,函数执行代码段。最后一个参数在列表中地址最高部分,第一个参数在列表地址最低部分。
	参数在堆栈中的分布情况如下:
	最后一个参数
	倒数第二个参数
	...
	第一个参数
	函数返回地址
	函数代码段
typedef char *  va_list;
#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap)      ( ap = (va_list)0 )

1#define _INTSIZEOF(n) ? ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
目的是把sizeof(n)的结果变成至少是sizeof(int)的整倍数,一般用来在结构中实现按int的倍数对齐。

2#define va_start(ap,v) ?( ap = (va_list)&v + _INTSIZEOF(v) )
将ap指针指向参数v的下一参数地址处。

3?#define va_arg(ap,t) ? ?( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
这条代码目的是将指针移动到下一参数后,再返回上一地址将上一地址的值取出。原理:根据运算顺序会先执行(ap += _INTSIZEOF(t))相当于ap=ap+_INTSIZEOF(t),也就是将指针ap指向下一地址;然后再执行- _INTSIZEOF(t)回到原地址,并转化为类型*的指针,再取指针所指地址的值。

4#define va_end(ap)  ( ap = (va_list)0 )
结束指针ap的使用,避免出现野指针。
void arg_test(int i, ...)
{
	int j=0;
	va_list arg_ptr;
	
	va_start(arg_ptr, i);
	printf("&i = %p\n", &i);//打印参数i在堆栈中的地址
	printf("arg_ptr = %p\n", arg_ptr);
	//打印va_start之后arg_ptr地址,
	//应该比参数i的地址高sizeof(int)个字节
	//这时arg_ptr指向下一个参数的地址
	
	j=*((int *)arg_ptr); //强制转换成int型指针,并将该地址中的数赋值给j
	printf("%d %d\n", i, j);
	j=va_arg(arg_ptr, int);//第一个可变参数改为int型,调用该参得到第二个可变参数的地址
	printf("arg_ptr = %p\n", arg_ptr);
	//打印va_arg后arg_ptr的地址
	//应该比调用va_arg前高sizeof(int)个字节
	//这时arg_ptr指向下一个参数的地址
	va_end(arg_ptr);
	printf("%d %d\n", i, j);
}
int int_size = _INTSIZEOF(int);得到int类型所占字节数
va_start(arg_ptr, i); 得到第一个可变参数地址,

根据定义(va_list)&v得到起始参数的地址, 再加上_INTSIZEOF(v) ,就是其实参数下一个参数的地址,即第一个可变参数地址.
j=va_arg(arg_ptr, int); 得到第一个参参数的值,并且arg_ptr指针上移一个_INTSIZEOF(int),即指向下一个可变参数的地址.

va_end(arg_ptr);置空arg_ptr,即arg_ptr=0;

  读取可变参数的过程其实就是堆栈中,使用指针,遍历堆栈段中的参数列表,从低地址到高地址一个一个地把参数内容读出来的过程.

在编程中应该注意的问题和解决办法:
  虽然可以通过在堆栈中遍历参数列表来读出所有的可变参数,但是由于不知道可变参数有多少个,什么时候应该结束遍历,如果在堆栈中遍历太多,那么很可能读取一些无效的数据.

解决办法:
a. 可以在第一个起始参数中指定参数个数,那么就可以在循环还中读取所有的可变参数;

b. 定义一个结束标记,在调用函数的时候,在最后一个参数中传递这个标记,这样在遍历可变参数的时候,可以根据这个标记结束可变参数的遍历;

从内核中摘录的代码:内核可变参数的实现与上述相似

#define va_arg(ap, T)           (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap)              (void) 0
#define va_start(ap, A)         (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

struct device *device_create(struct class *class, struct device *parent,
			     dev_t devt, void *drvdata, const char *fmt, ...)
{
	va_list vargs;
	struct device *dev;

	va_start(vargs, fmt);
	dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
	va_end(vargs);
	return dev;
}

五、静态映射表建立过程分析

1、使用静态映射表中的GPIO映射

kernel\arch\arm\plat-samsung\include\plat\map-base.h
#define S3C_ADDR_BASE	(0xFD000000)

#define S3C_ADDR(x)	((void __iomem __force *)S3C_ADDR_BASE + (x))

kernel\arch\arm\plat-s5p\include\plat\map-s5p.h
#define S5P_VA_GPIO		S3C_ADDR(0x00500000)

kernel\arch\arm\mach-s5pv210\include\mach\regs-gpio.h
#define S5PV210_GPA0_BASE		(S5P_VA_GPIO + 0x000)

kernel\arch\arm\mach-s5pv210\include\mach\gpio-bank.h
#define S5PV210_GPA0CON			(S5PV210_GPA0_BASE + 0x00)

2、建立映射表的三个关键部分

(1)映射表具体物理地址和虚拟地址的值相关的宏定义
kernel\arch\arm\plat-s5p\include\plat\map-s5p.h

(2)映射表建立函数。该函数负责由(1)中的映射表来建立linux内核的页表映射关系。
  在kernel/arch/arm/mach-s5pv210/mach-smdkc110.c中的smdkc110_map_io函数

smdkc110_map_io
	s5p_init_io
		iotable_init

结论:经过分析,真正的内核移植时给定的静态映射表在 arch/ /plat-s5p/cpu.c中的s5p_iodesc,本质是一个结构体数组,数组中每一个元素就是一个映射,这个映射描述了一段物理地址到虚拟地址之间的映射。这个结构体数组所记录的几个映射关系被iotable_init所使用,该函数负责将这个结构体数组格式的表建立成MMU所能识别的页表映射关系,这样在开机后可以直接使用相对应的虚拟地址来访问对应的物理地址。

static struct map_desc s5p_iodesc[] __initdata = {
	{
		.virtual	= (unsigned long)S5P_VA_CHIPID,//虚拟地址起始值
		.pfn		= __phys_to_pfn(S5P_PA_CHIPID),//物理地址起始值
		.length		= SZ_4K,                       //长度
		.type		= MT_DEVICE,				   //映射类型
	}, {
		.virtual	= (unsigned long)S3C_VA_SYS,
		.pfn		= __phys_to_pfn(S5P_PA_SYSCON),
		.length		= SZ_64K,
		.type		= MT_DEVICE,
	}, {
		.virtual	= (unsigned long)S3C_VA_UART,
		.pfn		= __phys_to_pfn(S3C_PA_UART),
 		.length		= SZ_4K,
		.type		= MT_DEVICE,
	}, {
		.virtual	= (unsigned long)VA_VIC0,
		.pfn		= __phys_to_pfn(S5P_PA_VIC0),
		.length		= SZ_16K,
		.type		= MT_DEVICE,
	}, {
		.virtual	= (unsigned long)VA_VIC1,
		.pfn		= __phys_to_pfn(S5P_PA_VIC1),
		.length		= SZ_16K,
		.type		= MT_DEVICE,
	}, {
		.virtual	= (unsigned long)S3C_VA_TIMER,
		.pfn		= __phys_to_pfn(S5P_PA_TIMER),
		.length		= SZ_16K,
		.type		= MT_DEVICE,
	}, {
		.virtual	= (unsigned long)S5P_VA_GPIO,
		.pfn		= __phys_to_pfn(S5P_PA_GPIO),
		.length		= SZ_4K,
		.type		= MT_DEVICE,
	},
};

/* read cpu identification code */  

(3)开机时调用映射表建立函数
问题:开机时(kernel启动时)smdkc110_map_io怎么被调用的?

start_kernel
	setup_arch
		paging_init
			devicemaps_init
			
if (mdesc->map_io)
		mdesc->map_io();

六、动态映射结构体方式操作寄存器

1、问题描述
(1)仿效真实驱动中,用结构体封装的方式来进行单次多寄存器的地址映射。

2、实践编码

#include <linux/module.h>               // module_init  module_exit
#include <linux/init.h>                 // __init   __exit

#include <linux/fs.h>           //与file_operations结构体有关
#include <asm/uaccess.h>

#include <mach/regs-gpio.h>   //与静态映射表相关
#include <mach/gpio-bank.h>             // arch/arm/mach-s5pv210/include/mach/gpio-bank.h

#include <linux/string.h>       //使用内核中的memset等函数

#include <linux/io.h>                   //与动态映射表相关
#include <linux/ioport.h>

#include <linux/cdev.h>
#include <linux/device.h>

//宏定义驱动模块的设备号以及名称
#define MYMAJOR  200
#define MYNAME   "test_char"

//本程序中使用动态映射并通过结构体的方式访问寄存器
#define GPJ0_REGBASE 0xe0200240

//定义需要使用的全局变量
int mymajor = 0;
char kbuf[100] = {0};/*内核空间的buf*/

static dev_t mydev;   //设备号
static struct class *test_class;

typedef struct GPJ0_REG
{
        volatile unsigned int gpj0con;
        volatile unsigned int gpj0dat;

}gpj0_reg_t;

gpj0_reg_t *pGPJ0REG;

ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
        int ret = -1;

        printk(KERN_INFO "this is test_chrdev_read.\n");

        ret = copy_to_user(ubuf, kbuf, count);
        if (ret)
        {
                printk(KERN_ERR "copy_to_user fail.\n");

                return -EINVAL;
        }
        else
        {
                printk(KERN_INFO "copy_to_user successfully.\n");

        }

        return 0;
}

//写函数的本质就是,将应用层传递过来的数据先复制到内核中,然后以正确的
//方式写入硬件完成操作

ssize_t test_chrdev_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
{
        int ret = -1;

        printk(KERN_INFO "this is test_chrdev_write.\n");

        //使用该函数将应用层传过来的ubuf的内容拷贝到驱动空间中的一个buf中
        memset(kbuf, 0, sizeof(kbuf));

        ret = copy_from_user(kbuf, ubuf, count);
        if (ret)
        {
                printk(KERN_ERR "copy_from_user fail.\n");
                return -EINVAL;
        }
        else
        {
                printk(KERN_INFO "copy_from_user successfully.\n");

        }


        //方式1:
        if (kbuf[0] == '0')//灯灭
        {
                pGPJ0REG->gpj0dat = ((1<<3) | (1<<4) | (1<<5));
        }
        else if (kbuf[0] == '1')//灯亮
        {
                pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));
        }

        //方式2:

        if (!strcmp(kbuf, "on"))
        {
                pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));

        }
        else if (!strcmp(kbuf, "off"))
        {
                pGPJ0REG->gpj0dat = ((1<<3) | (1<<4) | (1<<5));

        }

        return 0;
}


int test_chrdev_open(struct inode *inode, struct file *file)
{
        //该函数实现打开这个设备的硬件操作代码
        printk(KERN_INFO "this is test_chrdev_open.\n");

        pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));         // 亮

        return 0;
}


int test_chrdev_release(struct inode *node, struct file *file)//对应close函数
{
        printk(KERN_INFO "this is test_chrdev_release.\n");

        pGPJ0REG->gpj0dat = ((1<<3) | (1<<4) | (1<<5));         // 灭

        return 0;
}


// 自定义一个file_operations结构体变量(一个对设备进行操作的抽象结构体),
//并且去填充
static const struct file_operations test_fops = {
        .owner          = THIS_MODULE,                          // 惯例,直接写即可

        .open           = test_chrdev_open,                     // 将来应用open打开这个设备时实际调用的
        .release        = test_chrdev_release,          // 就是这个.open对应的函数
        .write          = test_chrdev_write,
        .read           = test_chrdev_read,
};


//模块安装函数
static int __init chrdev_init(void)
{
        printk(KERN_INFO "chrdev_init helloworld init\n");

        //1.在module_init宏调用的函数中去注册字符设备驱动,
        //使用旧接口去申请设备号
        mymajor = register_chrdev(0, MYNAME, &test_fops);
        if (mymajor < 0)
        {
                printk(KERN_ERR "register_chrdev fail\n");
                goto flag1;

        }
        else
        {
                printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
                mydev = MKDEV(mymajor, 0);
        }

        //2.注册字符设备驱动完成后,添加设备类文件,让内核
        //帮我们发信息给udev,让应用层的udev帮我们自动创建
        //和删除设备文件
#if 1
        test_class = class_create(THIS_MODULE, "zhang_class");
        if (IS_ERR(test_class))
        {
                printk(KERN_INFO "class_create() fail.\n");
                goto flag2;
        }
        device_create(test_class, NULL, mydev, NULL, "test111");

#endif
        //3.进行动态映射
        if (!request_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t), "GPJ0REG"))/*申请映射*/
        {
                printk(KERN_INFO "request_mem_region() fail.\n");
                goto flag3;
        }
        pGPJ0REG = ioremap(GPJ0_REGBASE, sizeof(gpj0_reg_t));/*建立映射*/

        //映射之后使用结构体的方式操作指针
        pGPJ0REG->gpj0con = 0x11111111;
        pGPJ0REG->gpj0dat = ((0<<3) | (0<<4) | (0<<5));         // 亮

        return 0;

//使用goto语句进行倒影式错误处理


//将第二步成功做出的东西释放掉
flag3:
        unregister_chrdev_region(mydev, 1);
        device_destroy(test_class, mydev);
        class_destroy(test_class);

//将第一步 成功做出的东西释放掉
flag2:
        unregister_chrdev_region(mydev, 1);

//如果第一步出错跳转到这里来
flag1:
        return -EINVAL;

}


//模块卸载函数
static void __exit chrdev_exit(void)
{
        printk(KERN_INFO "chrdev_exit helloworld exit\n");

        pGPJ0REG->gpj0dat = ((1<<3) | (1<<4) | (1<<5));         //灭

        //1.解除动态映射
        iounmap(pGPJ0REG);   /*断开映射*/
        release_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t));/*释放申请*/

        //2.删除创建的设备文件
        device_destroy(test_class, mydev);
        class_destroy(test_class);


        //3.在module_exit宏调用的函数中去注销字符设备驱动
        unregister_chrdev(mymajor, MYNAME);

}

module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");                          // 描述模块的许可证
MODULE_AUTHOR("Mr.Zhang");                              // 描述模块的作者
MODULE_DESCRIPTION("module test");      // 描述模块的介绍信息
MODULE_ALIAS("alias xxx");                      // 描述模块的别名信息

七、内核提供的读写寄存器接口

1、内核提供的寄存器读写接口
kernel\arch\arm\include\asm\io.h

(1)writel和readl

#define readb(c)		({ u8  __v = readb_relaxed(c); __iormb(); __v; })
#define readw(c)		({ u16 __v = readw_relaxed(c); __iormb(); __v; })
#define readl(c)		({ u32 __v = readl_relaxed(c); __iormb(); __v; })

#define writeb(v,c)		({ __iowmb(); writeb_relaxed(v,c); })
#define writew(v,c)		({ __iowmb(); writew_relaxed(v,c); })
#define writel(v,c)		({ __iowmb(); writel_relaxed(v,c); })

b\w\l代表读写1\2\4个字节的寄存器
writel() 往内存映射的 I/O 空间上写数据,wirtel()   I/O 上写入 32 位数据 (4字节)。
 原型:
#include <asm/io.h> 
void writel (unsigned char data , unsigned short addr )
readl() 从内存映射的 I/O 空间读取数据,readl 从 I/O 读取 32 位数据 ( 4 字节 )。
原型:
#include <asm/io.h> 
unsigned char readl (unsigned int addr )
注:变量    addr  是 I/O 地址。
返回值 : 从 I/O 空间读取的数值。

(2)iowrite32和ioread32

#define iowrite8(v,p)	({ __iowmb(); (void)__raw_writeb(v, p); })
#define iowrite16(v,p)	({ __iowmb(); (void)__raw_writew((__force __u16)cpu_to_le16(v), p); })
#define iowrite32(v,p)	({ __iowmb(); (void)__raw_writel((__force __u32)cpu_to_le32(v), p); })

#define ioread8_rep(p,d,c)	__raw_readsb(p,d,c)
#define ioread16_rep(p,d,c)	__raw_readsw(p,d,c)
#define ioread32_rep(p,d,c)	__raw_readsl(p,d,c)

summary:这两组函数没有本质区别,只不过是为了考虑版本兼容性问题(1)

(3)两个接口本质上没有什么区别,最终调用的函数都是一样的 。

2、代码实践

#include <linux/module.h>               // module_init  module_exit
#include <linux/init.h>                 // __init   __exit

#include <linux/fs.h>           //与file_operations结构体有关
#include <asm/uaccess.h>

#include <mach/regs-gpio.h>   //与静态映射表相关
#include <mach/gpio-bank.h>             // arch/arm/mach-s5pv210/include/mach/gpio-bank.h

#include <linux/string.h>       //使用内核中的memset等函数

#include <linux/io.h>                   //与动态映射表相关
#include <linux/ioport.h>

#include <linux/cdev.h>
#include <linux/device.h>

//宏定义驱动模块的设备号以及名称
#define MYMAJOR  200
#define MYNAME   "test_char"

//本程序中使用动态映射并通过结构体的方式访问寄存器
#define GPJ0_REGBASE 0xe0200240

#define S5P_GPJ0REG(x)          (x)
#define S5P_GPJ0CON                     S5P_GPJ0REG(0)
#define S5P_GPJ0DAT                     S5P_GPJ0REG(4)


//定义需要使用的全局变量
int mymajor = 0;
char kbuf[100] = {0};/*内核空间的buf*/

static dev_t mydev;   //设备号
static struct class *test_class;
static void __iomem *baseaddr; //寄存器的虚拟地址的基地址

ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
        int ret = -1;

        printk(KERN_INFO "this is test_chrdev_read.\n");

        ret = copy_to_user(ubuf, kbuf, count);
        if (ret)
        {
                printk(KERN_ERR "copy_to_user fail.\n");

                return -EINVAL;
        }
        else
        {
                printk(KERN_INFO "copy_to_user successfully.\n");

        }

        return 0;
}

//写函数的本质就是,将应用层传递过来的数据先复制到内核中,然后以正确的
//方式写入硬件完成操作

ssize_t test_chrdev_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos)
{
        int ret = -1;

        printk(KERN_INFO "this is test_chrdev_write.\n");

        //使用该函数将应用层传过来的ubuf的内容拷贝到驱动空间中的一个buf中
        memset(kbuf, 0, sizeof(kbuf));

        ret = copy_from_user(kbuf, ubuf, count);
        if (ret)
        {
                printk(KERN_ERR "copy_from_user fail.\n");
                return -EINVAL;
        }
        else
        {
                printk(KERN_INFO "copy_from_user successfully.\n");

        }


        //方式1:
        if (kbuf[0] == '0')//灯灭
        {
                writel(((1<<3) | (1<<4) | (1<<5)), baseaddr + S5P_GPJ0DAT);     // 灭
        }
        else if (kbuf[0] == '1')//灯亮
        {
                writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);;
        }

        //方式2:

        if (!strcmp(kbuf, "on"))
        {
                writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);

        }
        else if (!strcmp(kbuf, "off"))
        {
                writel(((1<<3) | (1<<4) | (1<<5)), baseaddr + S5P_GPJ0DAT);     // 灭

        }

        return 0;
}


int test_chrdev_open(struct inode *inode, struct file *file)
{
        //该函数实现打开这个设备的硬件操作代码
        printk(KERN_INFO "this is test_chrdev_open.\n");

        writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);             // 亮

        return 0;
}


int test_chrdev_release(struct inode *node, struct file *file)//对应close函数
{
        printk(KERN_INFO "this is test_chrdev_release.\n");

        writel(((1<<3) | (1<<4) | (1<<5)), baseaddr + S5P_GPJ0DAT);     // 灭

        return 0;
}


// 自定义一个file_operations结构体变量(一个对设备进行操作的抽象结构体),
//并且去填充
static const struct file_operations test_fops = {
        .owner          = THIS_MODULE,                          // 惯例,直接写即可

        .open           = test_chrdev_open,                     // 将来应用open打开这个设备时实际调用的
        .release        = test_chrdev_release,          // 就是这个.open对应的函数
        .write          = test_chrdev_write,
        .read           = test_chrdev_read,
};


//模块安装函数
static int __init chrdev_init(void)
{
        printk(KERN_INFO "chrdev_init helloworld init\n");

        //1.在module_init宏调用的函数中去注册字符设备驱动,
        //使用旧接口去申请设备号
        mymajor = register_chrdev(0, MYNAME, &test_fops);
        if (mymajor < 0)
        {
                printk(KERN_ERR "register_chrdev fail\n");
                goto flag1;

        }
        else
        {
                printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
                mydev = MKDEV(mymajor, 0);
        }

        //2.注册字符设备驱动完成后,添加设备类文件,让内核
        //帮我们发信息给udev,让应用层的udev帮我们自动创建
        //和删除设备文件
#if 1
        test_class = class_create(THIS_MODULE, "zhang_class");
        if (IS_ERR(test_class))
        {
                printk(KERN_INFO "class_create() fail.\n");
                goto flag2;
        }
        device_create(test_class, NULL, mydev, NULL, "test111");

#endif

        //3.进行动态映射
        if (!request_mem_region(GPJ0_REGBASE, 8, "GPJ0REG"))/*申请映射*/
        {
                printk(KERN_INFO "request_mem_region() fail.\n");
                goto flag3;
        }
        baseaddr = ioremap(GPJ0_REGBASE, 8);/*建立映射*/

        //映射之后使用内核提供的接口函数读写寄存器
        writel(0x11111111, baseaddr + S5P_GPJ0CON);
        writel(((0<<3) | (0<<4) | (0<<5)), baseaddr + S5P_GPJ0DAT);

        return 0;

//使用goto语句进行倒影式错误处理


//将第二步成功做出的东西释放掉
flag3:
        device_destroy(test_class, mydev);
        class_destroy(test_class);

//将第一步 成功做出的东西释放掉
flag2:
        unregister_chrdev(mymajor, MYNAME);

//如果第一步出错跳转到这里来
flag1:
        return -EINVAL;

}


//模块卸载函数
static void __exit chrdev_exit(void)
{
        printk(KERN_INFO "chrdev_exit helloworld exit\n");

        writel(((1<<3) | (1<<4) | (1<<5)), baseaddr + S5P_GPJ0DAT);     // 灭

        //1.解除动态映射
        iounmap(baseaddr);   /*断开映射*/
        release_mem_region(baseaddr, 8);/*释放申请*/

        //2.删除创建的设备文件
        device_destroy(test_class, mydev);
        class_destroy(test_class);
        
        //3.在module_exit宏调用的函数中去注销字符设备驱动
        unregister_chrdev(mymajor, MYNAME);
}

module_init(chrdev_init);
module_exit(chrdev_exit);

// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL");                          // 描述模块的许可证
MODULE_AUTHOR("Mr.Zhang");                              // 描述模块的作者
MODULE_DESCRIPTION("module test");      // 描述模块的介绍信息
MODULE_ALIAS("alias xxx");                      // 描述模块的别名信息

注:本资料大部分由朱老师物联网大讲堂课程笔记整理而来并且引用了部分他人博客的内容,如有侵权,联系删除!水平有限,如有错误,欢迎各位在评论区交流。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小嵌同学

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

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

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

打赏作者

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

抵扣说明:

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

余额充值