5.3.字符设备驱动高级
5.3.1.注册字符设备驱动新接口1
5.3.1.1、新接口与老接口
(1)老接口:register_chrdev
(2)新接口:register_chrdev_region(指定一个设备号,向内核注册这个设备号)/alloc_chrdev_region(不指定设备号,让内核给我分配一个设备号) + cdev
(3)为什么需要新接口
5.3.1.2、cdev介绍(怎么找到这个结构体呢,就是凭借经验,用SI搜索cdev.h)
(1)结构体
(2)相关函数:cdev_alloc、cdev_init、cdev_add、cdev_del
5.3.1.3、设备号
(1)主设备号和次设备号
(2)dev_t类型
(3)MKDEV、MAJOR、MINOR三个宏
5.3.1.4、编程实践
(1)使用register_chrdev_region + cdev_init + cdev_add进行字符设备驱动注册
5.3.2.注册字符设备驱动新接口2
5.3.2.1、实践编程
5.3.2.2、测试
5.3.3.注册字符设备驱动新接口3
5.3.2.1、使用alloc_chrdev_region自动分配设备号
(1)register_chrdev_region是在事先知道要使用的主、次设备号时使用的;要先查看cat /proc/devices去查看没有使用的。
(2)更简便、更智能的方法是让内核给我们自动分配一个主设备号,使用alloc_chrdev_region就可以自动分配了。
(3)自动分配的设备号,我们必须去知道他的主次设备号,否则后面没法去mknod创建他对应的设备文件。
5.3.2.2、得到分配的主设备号和次设备号
(1)使用MAJOR宏和MINOR宏从dev_t得到major和minor
(2)反过来使用MKDEV宏从major和minor得到dev_t。
(3)使用这些宏的代码具有可移植性
5.3.2.3、中途出错的倒影式错误处理方法
(1)内核中很多函数中包含了很多个操作,这些操作每一步都有可能出错,而且出错后后面的步骤就没有进行下去的必要性了。
5.3.4.注册字符设备驱动新接口4
5.3.4.1、使用cdev_alloc
(1)cdev_alloc的编程实践
//static struct cdev test_cdev;
static struct cdev *pcdev;
最开始我们是采用的cdev_init去初始化在全部变量定义的static struct cdev test_cdev这样一个结构体,这样全局变量不够灵活,占用的内存会因为结构是多大就多大,而且全局变量是一直存在,直到程序终止。
我们在这里采用的cdev_alloc函数,全部只需要定义一个结构体类型的指针,始终只占4个字节,并且是在注册字符设备驱动的时候再去分配内存,这样更加灵活。
(2)从内存角度体会cdev_alloc用与不用的差别
(3)这就是非面向对象的语言和面向对象的代码
(4)再次感叹C语言的博大精深,好好去看《4.C语言高级专题》
这里就可以看出在cdev_alloc之后我们应该需要做的一些事情,我们也可以分析原本的cdev_init干的活儿也可以反映出我们使用cdev_alloc之后还需要做的一些事情。
5.3.4.2、cdev_init的替代
(1)cdev_init源码分析
(2)不使用cdev_init时的编程
(3)为什么讲这个
5.3.5.字符设备驱动注册代码分析1
5.3.5.1、老接口分析
register_chrdev
__register_chrdev
__register_chrdev_region 用来注册设备号的
5.3.5.2、新接口分析
register_chrdev_region
__register_chrdev_region
alloc_chrdev_region
__register_chrdev_region
上图中注意__register_chrdev_region这个函数的 参数1是否为0,为0代表自动分配设备号;
在下图中就相当于进入了上图中的函数。我们也可以看出不是采用定义全局变量结构的形式,而是采用先定义一个结构体指针cd,然后再用kzalloc来申请内存这样更加灵活。
上图中也写了major=0的情况,然后进入一个for循环,呈现递减的趋势,这也就解释了我们在自动分配设备号的时候,都是从大开始,从我们的实验来看基本都是250开始的。
从上图中可以看到if (chrdevs[i] == NULL)就是在不断的循环下图中的这个结构体类型的数组,找出为空的,然后最终赋值给major然后返回。
然后往下看就可看出在实例化那个申请的结构体cd,然后再判断当超过255的时候要取余。
cdev_alloc
cdev_add
5.3.6.字符设备驱动注册代码分析2
5.3.7.自动创建字符设备驱动的设备文件
5.3.7.1、问题描述:
(1)整体流程回顾
(2)使用mknod创建设备文件的缺点------------是一个应用层结构函数,为啥不是内核层,因为设备文件本身就在应用层。
(3)能否自动生成和删除设备文件
5.3.7.2、解决方案:udev(嵌入式中用的是mdev)
(1)什么是udev?应用层的一个应用程序
(2)内核驱动和应用层udev之间有一套信息传输机制(netlink协议)
(3)应用层启用udev,内核驱动中使用相应接口
(4)驱动注册和注销时信息会被传给udev,由udev在应用层进行设备文件的创建和删除
5.3.7.3、内核驱动设备类相关函数
(1)class_create
(2)device_create -----------在内核代码中找出这两个函数的定义格式,抄写过来就可
5.3.7.4、编程实践
5.3.8.设备类相关代码分析1
5.3.8.1、sys文件系统简介
(1)sys文件系统的设计思想
(2)设备类的概念
(3)/sys/class/xxx/中的文件的作用
Class目录下就是各种设备驱动的类别的,我们的函数class_create(THIS_MODULE, “aston_class”);也就是在创建这样一个设备类文件。
由上图可知实际当我们用class_create函数的时候就会创建一个在class下面的设备类文件(aston_class),再进入这个设备类文件中,我们又会看到test文件,就是我们对应的设备文件了,再进入这个设备文件里面又有几个文件(dev,power,subsystem,uevent),我们再cat查看uevent可以看到里面的内容,实际上就是我们mknod传递的参数等等,这些都是传递给内核的,内核通过相应的数据结构传递给我们生成的uvent文件夹中,内核也帮我们产生类似test下面的所有的目录。
在我们代码中的模块卸载函数中,我们又看到了上图所示的两个函数,当执行第一个,那我们的设备文件就没了,当执行第二个我们的设备类文件也就没有了。所有当我们通过、sys这个目录就可以看到内核所发生的所有的改变,这就是我们应用层和驱动层的一扇窗户。
5.3.8.2
、
(1)
class_create
__class_create
__class_register
kset_register
kobject_uevent------这里可以看出向应用层发出的事件,因此应用层可以看到uevent这个文件和其中的内容。
从上图可以看到从class_create函数进入到了__class_create函数,这个函数做的工作,我们也能真正实际可以看得到的函数做的工作,,239行定义了一个class类,然后242申请内存,248-250行在填充这个类,252行在向内核注册这个类,258行就是在错误的情况下要释放申请到的内存,从这个函数我们就可以看出为什么会在、sys/class目录下生成这样的设备类文件,实际就是这个函数做的工作。
(2)
device_create
device_create_vargs
kobject_set_name_vargs
device_register----向内核相关文件注册这个设备
device_add
kobject_add------起真正添加设备的函数
下面的函数都是为了实现我们创建的设备文件里面的这些文件,及其内容的填充。
device_create_file
device_create_sys_dev_entry
devtmpfs_create_node
device_add_class_symlinks
device_add_attrs
device_pm_add
kobject_uevent
从上图中我们又看出定义指针,申请内存,然后再填充这个类,有点面向对象的意思,另外值得注意的1509行,自己给自己填充的自动释放自己的函数,很多函数里面都有这样的机制。1512行这个函数就是为设备文件设计名字。
5.3.9.设备类相关代码分析2
5.3.10.静态映射表建立过程分析
5.3.10.1、建立映射表的三个关键部分
(1)映射表具体物理地址和虚拟地址的值相关的宏定义
(2)映射表建立函数。该函数负责由(1)中的映射表来建立linux内核的页表映射关系。
在kernel/arch/arm/mach-s5pv210/mach-smdkc110.c中的smdkc110_map_io函数(2303行)
smdkc110_map_io
s5p_init_io
iotable_init
结论:经过分析,真正的内核移植时给定的静态映射表在arch/arm/plat-s5p/cpu.c中的s5p_iodesc,本质是一个结构体数组,数组中每一个元素就是一个映射,这个映射描述了一段物理地址到虚拟地址之间的映射。这个结构体数组所记录的几个映射关系被iotable_init所使用,该函数负责将这个结构体数组格式的表建立成MMU所能识别的页表映射关系,这样在开机后可以直接使用相对应的虚拟地址来访问对应的物理地址。
通过进入这个函数,可以看到里面又几个函数组成,都是与IO,时钟,串口,内存管理相关的,我们需要的是IO相关的。
(3)开机时调用映射表建立函数
问题:开机时(kernel启动时)smdkc110_map_io怎么被调用的?
start_kernel
setup_arch-----------------完成了和硬件结构相关的所有的初始化
paging_init
devicemaps_init
if (mdesc->map_io)
mdesc->map_io();
5.3.11.动态映射结构体方式操作寄存器
5.3.11.1、问题描述
(1)仿效真实驱动中,用结构体封装的方式来进行单次多寄存器的地址映射。来代替我们5.2.17节中讲的多次映射。
5.3.11.2、实践编码
5.3.11.3、分析和总结
5.3.12.内核提供的读写寄存器接口
5.3.12.1、前面访问寄存器的方式---------之前我们都是通过定义一个指向寄存器的指针,通过指针解引用的方式进行寄存器的操作。
(1)行不行
(2)好不好
5.3.12.2、内核提供的寄存器读写接口-----此时这些函数具有可移植性
(1)writel和readl
(2)iowrite32和ioread32---------同样在下图中往下滑动中,在这里我们可以看到wtitel调用的是writeb_relaxed函数中使用的__raw_writeb函数,和iowrite32中是一样的,所以上面这两组函数其实是没有本质区别的。
5.3.12.3、代码实践
5.3.12.4、分析和总结
(1)我们通过adc的简单例子先分析一下使用上面5上面两个函数的方法—通过下图1中可以看出定义了一个base_addr这样的一个变量,通过图2可以看出是定义的是一个指针,指针指向的类型是不知道的一个指针,然后在图1中292行可以看出是这个指针是用来结构ioremap函数的返回值;其实就是映射成功那几个连续寄存器的基地址。
(2)ioremap就上面一小节中把所有寄存器放在一个结构体一起进行虚拟地址映射的函数,从图1我们可以看出这个函数的第一个参数就是要映射的寄存器的起始地址,size就是要映射的寄存器的总大小.
(3)下图3就是使用方法,第一个就是写入寄存器的值,第二个参数就是对应的寄存器。
图1
图2
图3