三.字符设备驱动高级

目录

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

1.1、新接口与老接口

1.2、register_chrdev_region与alloc_chrdev_region函数简介

1.3、cdev介绍

1.4、设备号

二.注册字符设备驱动新接口2

2.1、实践编程(在第二节最后一个实验源码的基础上,注释老接口,添加新接口,方法还是参考kernel_210的源码,直接移植)

2.2、测试

三.注册字符设备驱动新接口3

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

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

3.3、中途出错的倒影式错误处理方法(内核中的源码多是采用这种方法)

四.注册字符设备驱动新接口4

4.1、使用cdev_alloc

4.2、cdev_init的替代

五.字符设备驱动注册代码分析

5.1、老接口分析

5.2、新接口分析

5.3、注销

六.自动创建字符设备驱动的设备文件

6.1、问题描述:

6.2、解决方案:udev(嵌入式中用的是mdev)

6.3、内核驱动设备类相关函数

6.4、编程

七.设备类相关代码分析

7.1、sys文件系统简介

7.2、代码分析

八.静态映射表建立过程分析

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

九.动态映射结构体方式操作寄存器

9.1、问题描述

9.2、实践编码

十.内核提供的读写寄存器接口

10.1、前面访问寄存器的方式

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

10.3、编程实验


 

 

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

1.1、新接口与老接口

  1. (1)老接口:register_chrdev   (上节有说明)

     作用:驱动向内核注册自己的file_operations结构体,用于挂钩结构体里面的成员

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

    作用:内核提供了三个函数来注册一组字符设备编号

  1. (3)新旧接口的区别?为什么需要新接口?

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

1.2、register_chrdev_region与alloc_chrdev_region函数简介

  1. register_chrdev_region(dev_t first,unsigned int count,char *name)

作用:用于分配指定的设备编号范围,设备的主次设备号的设备分配设备编号

 

参数:

First :要分配的设备编号范围的初始值, 这组连续设备号的起始设备号, 相当于register_chrdev()中主设备号

Count:连续编号范围.   是这组设备号的大小(也是次设备号的个数)

Name:编号相关联的设备名称. (/proc/devices); 本组设备的驱动名称

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

作用:用于动态申请设备编号范围,自动分配设备号,不用提前查看是否是没有被占用的设备号

 

参数:

dev:              输出型参数,获得一个分配到的设备号。可以用MAJOR宏和MINOR宏,将主设备号和次设备号,提取打印出来,看是自动分配的是多少,方便我们在mknod创建设备文件时用到主设备号和次设备号。 mknod /dev/xxx c 主设备号 次设备号

baseminor:  次设备号的基准,从第几个次设备号开始分配。

count:          次设备号的个数

name:          驱动的名字

返回值:       小于0,则错误,自动分配设备号错误。否则分配得到的设备号就被第一个参数带出来

1.3、cdev介绍

  1. (1)结构体

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;//次设备号个数
};

  1. (2)相关函数:

cdev_alloc:让内核为这个结构体分配内存。

cdev_init:将struct cdev类型的结构体变量和file_operations结构体进行绑定的。

cdev_add:向内核里面添加一个驱动,注册驱动。

cdev_del:从内核中注销掉一个驱动。注销驱动。

一个 cdev 一般它有两种定义初始化方式:静态的和动态的。

静态内存定义初始化:

struct cdev my_cdev;
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;

动态内存定义初始化:

struct cdev *my_cdev = cdev_alloc();//给my_cdev分配内存,指针实例化
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULE;

两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的数据结构需求而定。

1.4、设备号

  1. (1)主设备号和次设备号

(相当于基地址+偏移地址的概念,在有限的主设备号的基础上,加次设备号来扩展设备号,主设备号还是和原来一样,次设备号决定你的设备在主设备号中的排号)

  1. (2)dev_t类型

(包括了主设备号和次设备号  不同的内核中定义不一样。有的是16位次设备号和16位主设备号构成  有的是20为次设备号和12位主设备号 )

  1. (3)MKDEV、MAJOR、MINOR三个宏

功能:在主、次设备号之间换算。

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

1.4、编程实践目的

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

(2)使用 cdev_del + unregister_chrdev_region 进行字符设备的注销

二.注册字符设备驱动新接口2

E:\Linux\4.LinuxDriver\3.CharDevSenior\4.1

2.1、实践编程(在第二节最后一个实验源码的基础上,注释老接口,添加新接口,方法还是参考kernel_210的源码,直接移植)

修改module_test.c,其他不变如下:

......

#define MYMAJOR  200

#define MYNAME  "testchar"

int MYmajor;//用来返回内核自动分配的主设备号

static dev_t mydev;

static struct cdev test_cdev;

int mymajor;

char kbuf[100];   // 内核空间的buf

.........

..........

// 修改模块安装函数

static int __init chrdev_init(void)

{

       int retval;

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

 

/*

注释老接口

// 在module_init宏调用的函数中去注册字符设备驱动 //major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号

// 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数

mymajor = register_chrdev(0, MYNAME, &test_fops);

if (mymajor < 0)

{

printk(KERN_ERR "register_chrdev fail\n");

return -EINVAL;

}

printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);

*/

 

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

 

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

mydev = MKDEV(MAMAJR, 0);//用来将主设备号和次设备号,转换成一个主次设备号的

retval = register_chrdev_region(mydev,MYCNT,MANAME);//使用此函数事先要知道设备号

if (retval) {

printk(KERN_ERR "Unable to register minors for %s\n", MANAME);

return -EINVAL;

}

printk(KERN_ERR "register_chrdev_region success for ok\n");

printk(KERN_ERR "major = %d.\n minor = %d.\n ", MAJOR(mydev), MINOR(mydev)); //打印主次设备号

 

//第二步: 静态初始化字符设备

cdev_init(&test_cdev, &test_fops);

retval = cdev_add(&test_cdev, mydev, MYCNT); //把字符设备添加到系统中去

if (retval)

{

printk(KERN_ERR "Unable to cdev_add \n");

return -EINVAL;

       }

printk(KERN_ERR "cdev_add success for ok\n");

 

 

// 使用动态映射的方式来操作寄存器

if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))

return -EINVAL;

if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))

return -EINVAL;

 

pGPJ0CON = ioremap(GPJ0CON_PA, 4);

pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);

 

*pGPJ0CON = 0x11111111;

*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  // led亮

 

 

/*

// insmod时执行的硬件操作

rGPJ0CON = 0x11111111;

rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  // 亮

printk(KERN_INFO "GPJ0CON = %p.\n", GPJ0CON);

printk(KERN_INFO "GPJ0DAT = %p.\n", GPJ0DAT);

*/

return 0;

}

 

// 修改模块下载函数

static void __exit chrdev_exit(void)

{

printk(KERN_INFO "chrdev_exit helloworld exit\n");

 

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

 

// 解除映射

iounmap(pGPJ0CON);

iounmap(pGPJ0DAT);

release_mem_region(GPJ0CON_PA, 4);

release_mem_region(GPJ0DAT_PA, 4);

 

// 在module_exit宏调用的函数中去注销字符设备驱动

 注释老接口

//unregister_chrdev(mymajor, MYNAME);

 

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

//注销也是分2步

//第一步: 真正的去注销字符设备驱动用 cdev_del

cdev_del(&test_cdev);

//第二步去注销申请的主次设备号

unregister_chrdev_region(mydev, MYCNT);

printk(KERN_ERR "unregister_chrdev success. MYmajor = %d.\n",MYmajor);

}

2.2、测试

按照前面的测试,先make&make cp,下载到开发板后,启动终端如下

cat /proc/devices 查看设备号是 200

mknod /dev/test c 200 0

三.注册字符设备驱动新接口3

E:\Linux\4.LinuxDriver\3.CharDevSenior\4.2

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

直接在上面的实验源码基础上,修改。

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

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

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

作用:用于动态申请设备编号范围,自动分配设备号,不用提前查看是否是没有被占用的设备号

 

参数:

dev:              输出型参数,获得一个分配到的设备号。可以用MAJOR宏和MINOR宏,将主设备号和次设备号,提取打印出来,看是自动分配的是多少,方便我们在mknod创建设备文件时用到主设备号和次设备号。 mknod /dev/xxx c 主设备号 次设备号

baseminor:  次设备号的基准,从第几个次设备号开始分配。

count:          次设备号的个数

name:          驱动的名字

返回值:       小于0,则错误,自动分配设备号错误。否则分配得到的设备号就被第一个参数带出来

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

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

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

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

(3)使用这些宏的代码具有可移植性(alloc_chrdev_region函数的使用直接移植内核源码即可

......

// 模块安装函数

static int __init chrdev_init(void)

{

int retval;

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

 

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

 

//第一步:分配主次设备号(其他都不用修改,只需要把这里修改)

retval=alloc_chrdev_region(&mydev, 0, MYCNT, MANAME);

if (retval) {

printk(KERN_ERR "Unable to alloc minors for %s\n", MANAME);

return -EINVAL;

}

printk(KERN_ERR "alloc_chrdev_region success for ok\n");

printk(KERN_ERR "major = %d.\n minor = %d.\n ", MAJOR(mydev), MINOR(mydev)); //打印主次设备号的3个宏

 

 

//第二步: 静态初始化字符设备

cdev_init(&test_cdev, &test_fops);

retval = cdev_add(&test_cdev, mydev, MYCNT); //把字符设备添加到系统中去

if (retval)

{

printk(KERN_ERR "Unable to cdev_add \n");

return -EINVAL;

       }

printk(KERN_ERR "cdev_add success for ok\n");

 

// 使用动态映射的方式来操作寄存器

if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))

return -EINVAL;

if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))

return -EINVAL;

 

pGPJ0CON = ioremap(GPJ0CON_PA, 4);

pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);

 

*pGPJ0CON = 0x11111111;

*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  // led亮

 

 

/*

// insmod时执行的硬件操作

rGPJ0CON = 0x11111111;

rGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  // 亮

printk(KERN_INFO "GPJ0CON = %p.\n", GPJ0CON);

printk(KERN_INFO "GPJ0DAT = %p.\n", GPJ0DAT);

*/

return 0;

}

 

// 模块下载函数

static void __exit chrdev_exit(void)

.............

程序执行结果如下:

3.3、中途出错的倒影式错误处理方法(内核中的源码多是采用这种方法)

(1)内核中很多函数中包含了很多个操作,这些操作每一步都有可能出错,而且出错后后面的步骤就没有进行下去的必要性了。

比如我们的 module_test.c 中

// 模块安装函数

static int __init chrdev_init(void)

{

..........

 

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

retval=alloc_chrdev_region(&mydev, 0, MYCNT, MANAME);

if (retval<0) {

printk(KERN_ERR "Unable to alloc minors for %s\n", MANAME);

return -EINVAL;

}

.............

//第二步: 静态初始化字符设备

......

if (retval)

{

printk(KERN_ERR "Unable to cdev_add \n");

        return -EINVAL;

       }

.......

如果全部用return -EINVAL===当函数或事件没有成功执行,则错误返回,思考这样一个场景,例如当第一步分配主次设备号成功,但执行第二步(静态初始字符设备)时没成功,则直接就错误返回了,但我们第一步已经分配好了主次设备号,却没有什么作用,相反还占用了一个设备号,俗称“占到茅坑不拉屎

”,而我们的倒影式错误处理方法就===就相当于把前面执行成功了的(占用)事件,后续却排不上用场的“垃圾”,处理干净。俗称“把屁股开干净”

修改 module_test.c 文件中的处理错误的方式为倒影式处理方法如下:

// 模块安装函数

static int __init chrdev_init(void)

{

......

 

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

retval=alloc_chrdev_region(&mydev, 0, MYCNT, MANAME);

if (retval<0) {

printk(KERN_ERR "Unable to alloc minors for %s\n", MANAME);

//return -EINVAL;

goto flag1;

}

....

 

//第二步: 静态初始化字符设备

cdev_init(&test_cdev, &test_fops);

retval = cdev_add(&test_cdev, mydev, MYCNT); //把字符设备添加到系统中去

if (retval)

{

printk(KERN_ERR "Unable to cdev_add \n");

//return -EINVAL;

goto flag2;

       }

printk(KERN_ERR "cdev_add success for ok\n");

 

// 第三步:使用动态映射的方式来操作寄存器

if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))

//return -EINVAL;

goto flag3;

if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))

//return -EINVAL;

goto flag4; //这两个 goto 的地方应该不一样, 这里只是为了表达清楚意思

 

........

 

flag4:   //如果第 4 步出错, 则跳转到这里来

release_mem_region(GPJ0CON_PA, 4); //参数为物理地址

 

flag3:   //如果第 3 步出错, 则跳转到这里来

cdev_del(&test_cdev);

 

flag2:   //如果第 2 步出错, 则跳转到这里来

unregister_chrdev_region(mydev, MYCNT); //这里把第一步做成功的东西注销掉

 

flag1:   //如果第 1 步出错, 则跳转到这里来

return -EINVAL;

 

}

四.注册字符设备驱动新接口4

E:\Linux\4.LinuxDriver\3.CharDevSenior\4.3

4.1、使用cdev_alloc

(1)cdev_alloc的编程实践

一个 cdev 一般它有两种定义初始化方式:静态的和动态的。

静态内存定义初始化:

struct cdev my_cdev;
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;

动态内存定义初始化:

struct cdev *my_cdev = cdev_alloc();//给my_cdev分配内存,指针实例化
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULE;

两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的数据结构需求而定。

在上节实验基础上,修改 module_test.c 代码如下:

 

static struct cdev *pcdev;

... ...

// 模块安装函数

static int __init chrdev_init(void)

{

......

}

printk(KERN_ERR "alloc_chrdev_region success for ok\n");

printk(KERN_ERR "major = %d.\n minor = %d.\n ", MAJOR(mydev), MINOR(mydev)); //打印主次设备号的3个宏

 

//第二步: 静态初始化字符设备

pcdev = cdev_alloc();   // 给pcdev分配内存,指针实例化

cdev_init(pcdev, &test_fops);

//pcdev->owner = THIS_MODULE;

//pcdev->ops = &test_fops;//这一句等价于 cdev_init

 

retval = cdev_add(pcdev, mydev, MYCNT); //把字符设备添加到系统中去

.......

 

//goto语句是从上到下执行的,故如果执行了flag4,则会自动执行flag3 ...

flag4:   //如果第 4 步出错, 则跳转到这里来

release_mem_region(GPJ0CON_PA, 4); //参数为物理地址

 

flag3:   //如果第 3 步出错, 则跳转到这里来

cdev_del(pcdev);//此函数内部对cdev_alloc申请的空间进行释放

 

........

 

}

 

// 模块下载函数

static void __exit chrdev_exit(void)

{

.....

 

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

//注销也是分2步

//第一步: 真正的去注销字符设备驱动用 cdev_del

cdev_del(pcdev);

//第二步去注销申请的主次设备号

unregister_chrdev_region(mydev, MYCNT);

printk(KERN_ERR "unregister_chrdev success. MYmajor = %d.\n",MYmajor);

 

}

.. ....

4.2、cdev_init的替代

  1. (1)cdev_init源码分析

分析参考kernel_x210的内核源码 ,里面很多这种写法,以管窥豹,包括上面cdev_alloc函数的调用,都是参考内核源码的写法。

  1. (2)不使用cdev_init时的编程

//第二步: 静态初始化字符设备  pcdev = cdev_alloc();  // 给pcdev分配内存,指针实例化  cdev_init(pcdev, &test_fops);  //pcdev->owner = THIS_MODULE;  //pcdev->ops = &test_fops;  //这一句等价于 cdev_init  retval = cdev_add(pcdev, mydev, MYCNT);

  1. (3)为什么讲这个

知道 cdev的方法: 一般它有两种定义初始化方式:静态的和动态的。

五.字符设备驱动注册代码分析

5.1、老接口分析

  1. register_chrdev注册函数

=》 __register_chrdev==内核级函数

==》  __register_chrdev_region==内核级函数

 ==》cdev_alloc==让内核为这个结构体分配内存的。

===》cdev_add==向内核里面添加一个驱动,注册驱动。

函数 __register_chrdev_region() 主要执行以下步骤:

1. 分配一个新的 char_device_struct 结构,并用 0 填充。

2. 如果申请的设备编号范围的主设备号为 0,那么表示设备驱动程序请求动态分配一个主设备号。动态分配主设备号的原则是从散列表的最后一个桶向前寻找,那个桶是空的,主设备号就是相应散列桶的序号。所以动态分配的主设备号总是小于 256,如果每个桶都有字符设备编号了,那动态分配就会失败。

3. 根据参数设置 char_device_struct 结构中的初始设备号,范围大小及设备驱动名称。

4. 计算出主设备号所对应的散列桶,为新的 char_device_struct 结构寻找正确的位置。同时,如果设备编号范围有重复的话,则出错返回。

5. 将新的 char_device_struct 结构插入散列表中,并返回 char_device_struct 结构的地址。

5.2、新接口分析

register_chrdev_region== 动态分配主次设备号

=》__register_chrdev_region

register_chrdev_region() 函数用于分配指定的设备编号范围。如果申请的设备编号范围跨越了主设备号,它会把分配范围内的编号按主设备号分割成较小的子范围,并在每个子范围上调用 __register_chrdev_region() 。如果其中有一次分配失败的话,那会把之前成功分配的都全部退回。

alloc_chrdev_region==让内核自动给我们分配设备号

=》__register_chrdev_region

alloc_chrdev_region() 函数用于动态申请设备编号范围,通过指针参数返回实际获得的起始设备编号。

5.3、注销

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

六.自动创建字符设备驱动的设备文件

6.1、问题描述:

(1)整体流程回顾

(2)使用mknod创建设备文件的缺点

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

6.2、解决方案:udev(嵌入式中用的是mdev)

( 1) 什么是 udev? 应用层的一个应用程序, 是 BusyBox 中的一个命令, 内核源码中是找不

到的。

( 2) 内核驱动和应用层 udev 之间有一套信息传输机制( netlink 协议)

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

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

6.3、内核驱动设备类相关函数

  1. (1)class_create 创建一个类
  2. (2)device_create

这两个函数目的就是发信息给 udev, 让 udev 赶紧创建设备文件。用来在模块加载的时候自动在/dev目录下创建相应设备节点,并在卸载模块时删除该节点,当然前提条件是用户空间移植了udev。

  1. device_destroy(test_class, mydev); 

函数功能:函数device_destroy()用于从linux内核系统设备驱动程序模型中移除一个设备,并删除/sys/devices/virtual目录下对应的设备目录及/dev/目录下对应的设备文件

  1. class_destroy(test_class);删除一个类

6.4、编程

E:\Linux\4.LinuxDriver\3.CharDevSenior\4.4

复制上节源码

修改代码如下,其他不动:

static struct class *test_class;

...// 模块安装函数

static int __init chrdev_init(void)

{

int retval;

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

 

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

 

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

retval=alloc_chrdev_region(&mydev, 12, MYCNT, MANAME);

if (retval<0) {

printk(KERN_ERR "Unable to alloc minors for %s\n", MANAME);

//return -EINVAL;

goto flag1;

}

printk(KERN_ERR "alloc_chrdev_region success for ok\n");

printk(KERN_ERR "major = %d.\n minor = %d.\n ", MAJOR(mydev), MINOR(mydev)); //打印主次设备号的3个宏

 

 

//第二步: 静态初始化字符设备

.....

 

 

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

test_class = class_create(THIS_MODULE, "aliya_class"); //参数2是类名,在/sys/class/下创建的类,就是这里命名的。

if (IS_ERR(test_class))

return -EINVAL;

//最后一个参数就是希望在应用中app.c创建的设备文件的名字, 是/dev/test

device_create(test_class, NULL, mydev, NULL, "test");

 

// 第三步:使用动态映射的方式来操作寄存器

......

}

app.c

.......... #define FILE "/dev/test" //和我们使用mknod创建的字符设备名一致

然后 make 编译 make cp, insmod module_test.ko 装载模块, 效果如下所示, 自动创建

了 aston_test 设备文件。

注意:使用 rmmod module_test.ko 删除驱动后, 驱动文件/dev/aston_test 驱动文件会自动被删除。

七.设备类相关代码分析

7.1、sys文件系统简介

  1. (1)sys文件系统的设计思想
  2. (2)设备类的概念

aliya_class 就是我们在 module_test.c 文件中创建的类对象

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

class 文件夹中的每个文件都是一个类, uevent 及 class 就是内核提供给我们的一个和内

核进行交互的一个窗口, 通过这个 class, 我们就可以了解驱动程序安装卸载等过程时内核

所发生的事情。

7.2、代码分析

(1)class_create

=》__class_create //填充设备文件的相关信息

==》__class_register //

===》kset_register

====》kobject_uevent 实 现 从 内 核 里 面 向 应 用 层 里 面 的 udev 去 发 送 一 个uevent(udev event)事件, 这个事件要么就是装了个驱动, 要么就是卸载了个驱动, udev 会根据这个情况去创建或删除一个设备文件。

(2)device_create

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

上图中函数参数”...”就是可变参数, 更多的时候表示了次设备号。

例如:此图中 tty 就表示主设备号, 后面的14.. 15等表示次设备号

  =》device_create_vargs

==》kobject_set_name_vargs

==》device_register //向内核中相关部分做初始化

===》device_add

====》kobject_add //真正的把设备添加进去了

      ======》device_create_file //下面这些函数都是操作 sysfs 的函数

 ======》device_create_sys_dev_entry

 =======》devtmpfs_create_node

 =======》device_add_class_symlinks

        =======》  device_add_attrs

 =======》   device_pm_add

        =======》  kobject_uevent

上图中 test 目录中的文件就是上面操作 sysfs 的函数弄出来的。 比如:device_create_file函数

创建的就是 dev 目录, 参数 uevent_attr

static struct device_attribute uevent_attr =
	__ATTR(uevent, S_IRUGO | S_IWUSR, show_uevent, store_uevent);

就是给出的属性, 参数中 show_uevent、 store_uevent 表示读和存储。

所以我们在执行cat dev

读取 dev 这个文件时, 内核调用的就是 show_uevent 函数。

八.静态映射表建立过程分析

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

  1. (1)映射表具体物理地址和虚拟地址的值相关的宏定义

主映射表

              位于:arch/arm/plat-s5p/include/plat/map-s5p.h

  1. (2)映射表建立函数。该函数负责由(1)中的映射表来建立linux内核的页表映射关系。

在kernel/arch/arm/mach-s5pv210/mach-smdkc110.c中的smdkc110_map_io函数

调用关系如下:

smdkc110_map_io

s5p_init_io

iotable_init

结论:经过分析,真正的内核移植时给定的静态映射表在arch/arm/plat-s5p/cpu.c中的s5p_iodesc

iotable_init 函数调用s5p_iodesc 表如下

s5p_iodesc本质是一个结构体数组,数组中每一个元素就是一个映射,这个映射描述了一段物理地址到虚拟地址之间的映射。这个结构体数组所记录的几个映射关系被iotable_init所使用,该函数负责将这个结构体数组格式的表建立成MMU所能识别的页表映射关系,这样在开机后可以直接使用相对应的虚拟地址来访问对应的物理地址。

建立映射表的调用函数关系如下:

static void __init create_mapping(struct map_desc *md)
{
... ...
if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) {
printk(KERN_WARNING "BUG: not creating mapping for "
"0x%08llx at 0x%08lx in user region\n",__pfn_to_phys((u64)md->pfn), md->virtual);
return;
} /
* *
Catch 36-bit addresses
*/
if (md->pfn >= 0x100000) {
create_36bit_mapping(md, type);
return;
} .
.. ...
do {
unsigned long next = pgd_addr_end(addr, end);
alloc_init_section(pgd, addr, next, phys, type);
... ...
} while (pgd++, addr != end);
}

我们暂时不需要要研究太深, 现在只需知道内核调用了 iotable_init 函数来建立页表即可。

将来我们如果自己有需要再添加内存映射的代码, 就需要在 s5p_iodesc 数组中添加元素, 然后添加地址等相应的宏即可。

  1. (3)开机时调用映射表建立函数

问题:开机时(kernel启动时)smdkc110_map_io怎么被调用的?

在\kernel\kernel\arch\arm\kernel\head.S 中 ,从 ENTRY(stext) 函 数 中 的__switch_data 函数中进去

然后跳到__mmap_switched 中, 在__mmap_switched 段的最后有一句 b start_kernel

调用关系如下:

start_kernel

setup_arch //和硬件架构相关初始化

paging_init

devicemaps_init

在 devicemaps_init 函数中有下面这句话

if (mdesc->map_io) // machine descriputer,机器描述符, 如果此函数指针不为 NULL

mdesc->map_io(); // 在这里调用后面的 smdkc110_map_io 等函数来建立内存映射

跟踪发现map_io 指针的赋值如下:

\kernel\kernel\arch\arm\mach-s5pv210\mach-smdkc110.c ,即调用了smdkc110_map_io函数。

以上就是kernel启动时调用smdkc110_map_io过程 !!

九.动态映射结构体方式操作寄存器

9.1、问题描述

(1)仿效真实驱动中,用结构体封装的方式来进行单次多寄存器的地址映射。来代替我们5.2.17节中讲的多次映射。

从 E:\Linux\4.LinuxDriver\2.CharDevBasic\4.8 复制文件到 ...\3.CharDevSenior\4.5

9.2、实践编码

修改 module_test.c 文件中代码如下:

.........

typedef struct GPJ0REG

{

volatile unsigned int gpjocon;

volatile unsigned int gpjodat;

}gpj0_reg_t;

 

//#define GPJ0CON_PA 0xe0200240//物理地址

//#define GPJ0DAT_PA  0xe0200244

#define GPJ0_REGBASE 0xe0200240

 

// unsigned int *pGPJ0CON;

// unsigned int *pGPJ0DAT;

gpj0_reg_t *pGPJ0REG;

 

// 模块安装函数

static int __init chrdev_init(void)

{

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

.....

/*

// 使用动态映射的方式来操作寄存器

if (!request_mem_region(GPJ0CON_PA, 4, "GPJ0CON"))

return -EINVAL;

if (!request_mem_region(GPJ0DAT_PA, 4, "GPJ0CON"))

return -EINVAL;

 

pGPJ0CON = ioremap(GPJ0CON_PA, 4);//返回的虚拟地址

pGPJ0DAT = ioremap(GPJ0DAT_PA, 4);

 

*pGPJ0CON = 0x11111111;

*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5));  // led

*/

//两步完成整个结构体方式的映射

if (!request_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t), "GPJ0REG"))//参数为物理地址

return -EINVAL;

pGPJ0REG = ioremap(GPJ0_REGBASE, sizeof(gpj0_reg_t));//返回的虚拟地址

 

//映射之后用指向结构体指针的方式操作

//指针使用->结构体元素的方式来操作各个寄存器

 pGPJ0REG -> gpjocon = 0x11111111;

pGPJ0REG -> gpjodat = ((0<<3) | (0<<4) | (0<<5));// led

 

return 0;

}

 

// 模块下载函数

static void __exit chrdev_exit(void)

{

printk(KERN_INFO "chrdev_exit helloworld exit\n");

 

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

/*

// 解除映射

iounmap(pGPJ0CON);

iounmap(pGPJ0DAT);

release_mem_region(GPJ0CON_PA, 4);

release_mem_region(GPJ0DAT_PA, 4);

*/

//解除整个结构体的方式映射

iounmap(pGPJ0REG);

release_mem_region(GPJ0_REGBASE, sizeof(gpj0_reg_t));

 

// 在module_exit宏调用的函数中去注销字符设备驱动

unregister_chrdev(mymajor, MYNAME);

}

make 编译完成后 make cp, 然后在开发板中的执行结果如下:

工作正常, 代码和原来的效果是一样的, 但这次的代码具有扩展性, 更具有实际意义

十.内核提供的读写寄存器接口

10.1、前面访问寄存器的方式

(1)行不行

(2)好不好

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

函数路径: kernel\arch\arm\include\asm\io.h

(1)writel和readl

//往内存映射的 I/O 空间上写数据,wirtel()   I/O 上写入 32 位数据 (4字节)。 void writel (unsigned char data , unsigned short addr ) addr  是 I/O 地址。

// 从内存映射的 I/O 空间读取数据,readl 从 I/O 读取 32 位数据 ( 4 字节 )。 unsigned char readl (unsigned int addr ) addr  是 I/O 地址。

(2)iowrite32和ioread32

10.3、编程实验

从E:\Linux\4.LinuxDriver\2.CharDevBasic\4.8 复制文件到 ...\3.CharDevSenior\4.6

修改 module_test.c 文件中代码如下:

直接将模块下载函数和模块卸载函数中操作硬件led亮灭的函数注释,直接用内核读写接口测试。

  1. 测试 1. 用 2 次 ioremap 得到的动态映射虚拟地址来操作

unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;
// 模块安装函数
static int __init chrdev_init(void)
{
... ...
//*pGPJ0CON = 0x11111111;
//*pGPJ0DAT = ((0<<3) | (0<<4) | (0<<5)); //亮
//测试1相关代码:用2次ioremap得到的动态映射虚拟地址来操作,测试成功
writel(0x11111111, pGPJ0CON);
writel((0<<3) | (0<<4) | (0<<5), pGPJ0DAT);
... ...
} /
/ 模块卸载函数
static void __exit chrdev_exit(void)
{
... ...
//*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); //灭
//测试1相关代码:
writel((1<<3) | (1<<4) | (1<<5), pGPJ0DAT);
... ...
}

 然后编译执行, 程序能正常运行。

  1. 测试 2: 用静态虚拟地址映射

#define GPJ0CON		S5PV210_GPJ0CON
#define GPJ0DAT		S5PV210_GPJ0DAT
// 模块安装函数
static int __init chrdev_init(void)
{
//测试 1. 用 2 次 ioremap 得到的动态映射虚拟地址来操作, 测试成功。
//writel(0x11111111, pGPJ0CON);
//writel((0<<3) | (0<<4) | (0<<5), pGPJ0DAT);

//测试 2: 用静态虚拟地址映射。
writel(0x11111111, GPJ0CON);
writel((0<<3) | (0<<4) | (0<<5), GPJ0DAT);
... ...
}
/ 模块卸载函数
static void __exit chrdev_exit(void)
{......
//*pGPJ0DAT = ((1<<3) | (1<<4) | (1<<5)); //灭
//writel((1<<3) | (1<<4) | (1<<5), pGPJ0DAT);
writel((1<<3) | (1<<4) | (1<<5), GPJ0DAT);//测试2&3相关代码
...
}

  1. 测试 3: 用 1 次 ioremap 映射多个寄存器得到虚拟地址。 这里我们映射 2 个寄存器

#define GPJ0CON_PA	0xe0200240
#define GPJ0DAT_PA 	0xe0200244
static void __iomem *baseaddr; //寄存器的虚拟地址的基地址
#define S5P_GPIJ0REG(x) (x)
... ...
#define S5P_GPJ0CON S5P_GPIJ0REG(0)
#define S5P_GPJ0DAT S5P_GPIJ0REG(4)
static int __init chrdev_init(void)
{
... ...
    测试3相关代码:用1次ioremap映射多个寄存器得到虚拟地址。这里我们映射2个寄存器
	if (!request_mem_region(GPJ0CON_PA, 8, "GPJ0BASE"))
	return -EINVAL;
	baseaddr = ioremap(GPJ0CON_PA, 8);
	writel(0x11111111, baseaddr + S5P_GPJ0CON);
	writel((0<<3) | (0<<4) | (0<<5), baseaddr + S5P_GPJ0DAT);	
} 
/ 模块卸载函数
static void __exit chrdev_exit(void)
{
... ...
	//测试2&3相关代码
	writel((1<<3) | (1<<4) | (1<<5), GPJ0DAT);//测试2&3相关代码
	
	// 解除映射
	// iounmap(pGPJ0CON);
	// iounmap(pGPJ0DAT);
	// release_mem_region(GPJ0CON_PA, 4);
	// release_mem_region(GPJ0DAT_PA, 4);
		
	//测试3相关代码
	iounmap(baseaddr); //参数为虚拟地址
	release_mem_region(baseaddr, 8); //参数为物理地址
 
	// 在module_exit宏调用的函数中去注销字符设备驱动
	unregister_chrdev(mymajor, MYNAME);
... ...
}

注意: 如果在调试过程中内核奔溃过, 则最好重新启动内核。

测试3时,insmod 和rmmod都能控制led亮灭,但最终卸载模块时有一个错误提示如下:

不知道什么原因, 留到以后解决。

总结:使用IO内存的步骤(参考上面做的n个程序源码)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值