回顾:
1.linux内核分离思想
明确:一个完整的硬件设备驱动必须包含硬件信息和软件信息;
分离:就是将硬件信息和软件信息进行分离,让以后驱动的开发重心放在硬件信息上,让驱动开发者从软件的实现上解脱出来!
linux内核分离思想的实现:
定义一个平台总线platform_bus_type(struct bus_type虚拟总线类型)来维护两个链表。
特点:
内核已经实现;
维护dev链表和drv链表;
前者保存硬件信息,后者保存软件信息;
match函数:用于匹配;
驱动只需关注一下两个结构体:
平台设备:struct platform_device
描述装载硬件信息
驱动实现;
内核一般实现,会将platform_device的硬件信息在平台代码中实现!arch/arm/mach-s5pv210/mach-cw210.c
使用方法:
1.分配初始化struct platform_device
struct platform_devicexxx_dev = {
.name = 必须要指定,用于匹配
.id = 设备编号
.resource = 装载硬件资源//一般是建立一个struct resource 类型的数组,将数组名赋给resource
.num_resources = resource类型的硬件资源的个数,也就是数组的元素的个数
.dev = {
.platform_data = 装载自己定义描述硬件的结构体(led_resource,btn_resource)
.release=led_dev,//必须写,否则会报错!void (*release)(struct device*)
}
};
platform_device_register//向内核注册硬件信息
1.添加硬件节点到dev链表中
2.遍历drv链表,进行匹配,如果匹配成功,调用匹配成功软件节点的probe函数,然后将硬件节点的首地址传递给probe函数
platform_device_unregister //卸载硬件信息
平台驱动:struct platform_driver:
描述装载软件信息;
在驱动中完成,一般不会在平台代码中完成;
1.分配初始化platform_driver软件节点
struct platform_driverxxx_drv = {
.driver = {
.name = 必须指定,用于匹配
},
.probe = 硬件和软件匹配成功内核调用
.remove = 卸载硬件或者卸载软件内核调用
};
2.注册
platform_driver_register
卸载
platform_driver_unregister
注意:
platform_device和platform_driver之间的连线者是name字段!
probe函数是否被执行代表着软件和硬件是否匹配成功,也代表着一个驱动的完整性,标志驱动生命周期的开始!
platform_device装载硬件信息的方法:
方法1:struct resource来装载硬件信息;给resource字段使用
方法2:自己定义硬件的数据结构体struct led_resource,给dev字段的platform_data成员使用
*****************************************************************************************
Day13
案例:分析ioremap实现LED开关驱动
分析:
1.明确:不管是在用户空间还是在内核空间,软件一律不能去直接访问设备的物理地址;
在内核空间通过iomap函数将物理地址映射成内核的虚拟地址(动态内存映射区),在用户空间将通过系统调用,通过内核来访问硬件!
2.在内核驱动中如果要访问设备的物理地址,需要利用ioremap将设备的物理地址映射到内核虚拟地址上(动态内存映射区),以后驱动程序访问这个内核虚拟地址就是在间接得访问设备的物理地址(MMU内存管理单元,TLB(cache的一个硬件单元,cache从TLB中取页表,没有则TLB从内存中取出,再给cache),TTW)
3.如果用户要访问硬件设备,不能直接访问,也不能在用户空间访问,只能通过系统调用(open,close,read,write,ioctl)来访问映射好的内核虚拟地址,通过这种间接的访问来访问硬件设备,但是如果涉及到数据的拷贝,还需要借助4个内存拷贝函数!Cpoy_from_user/copy_to_user/get_user/put_user
结论:
1.通过以上的分析,发现应用程序通过read,write,ioctl来访问硬件设备,它们都要经过两次的数据拷贝,一次是用户空间和内核空间的数据拷贝,另外一次是内核空间和硬件之间的数据拷贝,如果设备拷贝的数据量比较小,那么read,write,ioctl的两次数据拷贝的过程对系统的影响几乎可以忽略不计,如果设备的数据量非常大,例如显卡(独立),LCD屏幕(显存共享主存)(独立显卡有自己的显存,集成显卡没有自己的显存,共享主存,因此速度慢,效率低),摄像头,声卡这类设备涉及的数据量比较庞大,如果还是用read,write,ioctl进行访问设备数据,无形对系统的性能影响非常大。
类似于搬家:一个人的搬家,东西少,很轻松;一个家庭的搬家东西很多,很麻烦!
2.用户访问设备,最终其实涉及的用户和硬件,而read,write,ioctl本身会牵扯到内核,所以这些函数涉及2次的数据拷贝,用户要直接去访问硬件设备,只需要将硬件设备的物理地址信息映射到用户的虚拟地址空间即可,一旦完毕,不会在牵扯到内核空间,以后用户直接访问用户的虚拟地址就是在访问设备硬件,由2次的数据拷贝的转换为一次的数据拷贝。
目的:将硬件物理地址映射到用户虚拟地址空间,由2次数据拷贝变成1次数据拷贝!
3.如何实现将硬件设备的物理地址映射到用户空间的虚拟内存上呢?
用户空间3G虚拟内存区域的划分:
高地址开始:
栈区
MMAP内存映射区
堆区
BSS段区
DATA段区
TEXT段区
MMAP内存映射区作用:
应用程序使用的动态库映射到这个区域;
应用程序调用mmap,将设备物理地址和这个区域的虚拟内存进行映射;
结论:linux系统通过mmap来实现将物理地址映射到用户3G的MMAP内存映射区上的虚拟内存上!
mmap系统调用的过程:
void *addr;
addr = mmap(0,0x1000,PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
1.应用程序调用mmap首先调用C库的mmap
2.C库的mmap保存mmap的系统调用号到R7中,然后调用svc触发软中断异常(陷入内核空间)
3.内核启动时,已经初始化好了异常向量表,触发软中断,跳转到软中断的异常向量表的入口地址vector_swi.
4.根据R7保存的系统调用号,以它索引,在内核的系统调用表找到对应的函数sys_mmap,然后调用内核实现的sys_mmap
5.sys_mmap内核会做两件事:
1.首先在当前进程的MMAP内存映射区中找一块空闲的虚拟内存区域;
2.一旦找到以后,利用struct vm_area_strcut结构创建一个对象来描述这块空闲的虚拟内存区域;
6.sys_mmap最终调用底层驱动的mmap,然后将描述空闲虚拟内存区域的对象指针传递给底层驱动的mmap函数使用;
7.底层驱动的mmap根据传递过来的虚拟内存区域的信息获取用户要映射的虚拟地址,再根据某些函数建立用户虚拟地址和物理地址的映射关系
8.一旦建立映射,mmap函数返回,返回值保存着这块空闲内存区域的起始地址,以后用户在用户空间就可以为所欲为了!
struct vm_area_struct {
unsigned long vm_start;//空闲虚拟内存的起始地址
unsigned long vm_end;//结束地址
pgprot_t vm_page_prot; //访问权限
unsigned long vm_pgoff;//偏移量
};
9.驱动mmap利用一下函数建立映射(用户虚拟地址和物理地址)
intremap_pfn_range(struct vm_area_struct *vma,unsigned long addr, unsigned longpfn, unsigned size,pgprot_t prot);
vma: 用户虚拟内存区域指针
addr: 用户虚拟内存起始地址->vma->vm_start
pfn: 要映射的物理地址所在页帧号,可以通过物理地址>>12得到
size: 待映射的内存区域的大小
prot: vma的保护属性vma->vm_page_prot
功能:建立已知的用户虚拟内存和已知的物理地址之间的映射关系;
注意:利用这个函数进行地址映射的时候,不管是物理地址还是用户虚拟地址都要求是页的整数倍!
1页=4K=0x1000
0xe0200080这个GPIO寄存器地址不是页的整数倍!
通过芯片手册可知GPIO使用的地址空间范围:
0xE0200000 ~ 0xE02FFFFF
映射时指定的物理地址应该是:0xE0200000(页的整数倍)
访问0xe0200080:用户虚拟地址 + 0x80
访问0xe0200084:用户虚拟地址+ 0x84
注意:一个物理地址同时可以映射到内核的虚拟地址上,还可以映射到用户的虚拟地址上!
案例:利用mmap来实现点灯。采用分离思想来实现
C,B位:NCNB,NCB,CNB,CB
**********************************************************
案例:一个应用程序如何去处理多个设备,例如应用程序读取网路数据,按键,串口
分析:
明确:对设备访问永远先open
int fdx = open
读取设备的数据:
方法1:
串行+阻塞的方式读取:
while(1) {
read(标准输入);
read(网络);
}
缺点:每当阻塞读取标准输入时,如果用户不进行标准输入的操作,而此时客户端给服务器发送数据,导致服务器无法读取客户端发送来的数据!
方法2:
采用多线程或者多进程机制来实现读取:
开辟多个线程,每一个线程处理一个设备,不会导致的数据的无法读取,但是系统的开销相比方法1要大!
方案3:采用linux系统提供的高级IO的处理机制
select/poll:两者一样,主进程能够利用select或者poll能够对多个设备进行监听!
linux关于select的应用编程方法:man select
select函数原型:
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
函数功能:
主进程利用此函数能够对多个设备进行监听,一旦发现监听的设备都不可用(不可读也不可写也没有异常),那么主进程进入休眠状态,一旦监听的设备中,只要有一个设备可用(可读或者可写或者有异常)都会唤醒休眠的主进程,select也就会返回。
注意这个函数仅仅起到一个监听的功能,数据的后续处理,例如读,写都是通过read,write,ioctl来进行!
参数说明:
nfds:
对设备的访问永远先open获取fd;
监听的设备中,最大的文件描述符fd+1;
数据类型fd_set:文件描述符集合,用来保存描述监听的设备,里面存放是被监听设备的文件描述符;如果select要监听某一个设备,必须把这个设备的fd添加到对应的文件描述符集合中!
readfds:读文件描述符集合指针,如果select要监听设备是否可读,需将设备的fd添加到这个集合中!
writefds:写文件描述符集合指针,如果select要监听设备是否可写,需将设备的fd添加到这个集合中!
exceptfds:异常文件描述符集合指针,如果select要监听设备是否有异常,需将设备的fd添加到这个集合中!
注意:一个设备的fd可以同时添加到三个集合中!
timeout:指定监听的超时时间,如果此参数指定了一个时间,例如5秒钟,select发现设备不可用,主进程进入休眠状态,如果5秒之内设备还不可用,5秒到期,主进程主动唤醒;如果此参数指定为NULL,休眠为永久休眠!
返回值:有三种
如果等于0:表明是超时;
如果小于0:表明系统出错;
如果大于0:表明设备可用(至少是一个设备,或者全部);
文件描述符集合操作的方法:
fd_set rfds; //定义读文件描述符集合
//从集合中解除对fd设备的监听
void FD_CLR(int fd, fd_set *set);
//判断是否是设备fd引起的主进程的唤醒,如果是返回true,否则返回false
int FD_ISSET(int fd, fd_set *set);
//添加一个新的被监听的设备
void FD_SET(int fd, fd_set *set);
//清空文件描述符集合
void FD_ZERO(fd_set *set);
注意:如果要重复监听,需要再次清空集合和添加监听设备!
案例:采用select监听网络和标准输入
下载5.0代码
实验步骤:
PC机:
1.make
2.cp udplib_test/opt/rootfs/home/apptest/
3.cp libudp.so /opt/rootfs/home/applib/
注意修改/opt/rootfs/etc/profile添加库的路径:
exportLD_LIBRARY_PATH=/home/applib:$LD_LIBRARY_PATH
ARM:
运行服务器端:
/home/apptest/udplib_test
PC:
运行客户端
. env.sh //注意env前加空格
./client