设备通常会提供一组寄存器来用于控制设备、读写设备和获取设备状态,既控制寄存器、数据寄存器和状态寄存器、这些寄存器可能位于I/O空间,也可能位于内存空间。当位于I/O空间时,通常被称为I/O端口,位于内存空间时,对应的内存空间被称为I/O内存(现在一般都是统一编址)
1、对于I/O端口
有专门的函数提供读取端口上的数据,例如读写字节端口(8字节宽)
Unsigned inb(unsigned port);
Unsigned outb(unsigned char byte,unsignedport);
2、对于I/O内存
在内核中访问I/O内存之前,需首先使用ioremap()函数将设备所处的物理地址映射到虚拟地址,ioremap()的原型如下:
Void* ioremap(unsigned long offset,unsignedlong size);
Ioremap()与vmalloc()类似,也需要建立新的页表,但是它并不进行vmalloc()中所执行的内存分配行为。Ioremap()返回一个特殊的虚拟地址,该地址可用来存取特定的物理地址范围。通过ioremap()获得的虚拟地址应该被iounmap()函数释放,
Void iounmap(void* addr);
在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是可以使用Linux内核的如下一组函数来完成设备内存映射的虚拟地址的读写,这些函数如下:
a) 读I/O内存
Unsigned intioread8(void* addr);
Unsigned intioread16(void* addr);
Unsigned intioread32(void* addr);
b) 写I/O内存
Void iowrite8(u8value ,void * addr);
Void iowrite16(u16value ,void * addr);
Void iowrite32(u32value ,void * addr);
3、把I/O端口映射到内存空间
Void*ioport_map(unsigned long port,unsigned int count);
通过这个函数,可以把port开始的count个连续的I/O端口重映射为一段“内存空间“。然后就可以在其返回的地址上像访问I/O内存一样访问这些I/O端口。当不需要这种映射时,需要调用下面的函数来撤销
Voidioport_unmap(void* addr);
Ioport_map()映射到内存空间行为实际上是给开发人员制造一个假象,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用同一的I/O内存访问接口访问I/O端口
4、申请与释放设备I/O端口和I/O内存
I/O端口申请
Struct resource* request_region(unsignedlong first,unsigned long n,const char* name);
这个函数向内核申请n个端口,这些端口从first开始,name参数为设备的名称,如果分配成功返回值非NULL
Void relese_region(unsigned longstart,unsigned long n);
I/O内存申请
Struct resoutce* request_mem_region(unsignedlong start,unsigned long len,char* name);
向内核申请n个内存地址,这些地址从first开始,name参数为设备的名称
Void relese_mem_region(unsigned longstart,unsigned long len);
5、设备I/O端口和I/O内存访问流程
a) I/O端口访问方法:
直接使用I/O端口操作函数,在设备打开或驱动模块被加载时申请I/O端口,之后使用inb() outb()进行访问,最后再设备关闭或驱动被卸载时释放I/O端口范围
另一个途径就是将I/O端口映射为内存进行访问,在设备打开或驱动模块被加载时,申请I/O端口区域并使用ioport_map()映射到内存,之后使用I/O内存的函数进行端口访问。最后,在设备关闭或驱动被卸载时释放I/O并使用映射
c) I/O内存的访问方法
首先使用request_mem_region()申请资源,接着将寄存器地址通过ioremap()映射到内核空间虚拟地址,之后就可以通过linux设备访问编程接口访问这些设备的寄存器了,访问完成后,使用ioremap()申请的虚拟地址进行释放,并释放relese_mem_region()申请的I/O内存资源
6、将设备地址映射到用户空间
一般情况用户空间不可能也不应该直接访问设备,但是,设备驱动程序中可实现mmap()函数,这个函数可是的用户空间能直接访问设备的物理地址,mmap()实现了将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址实际上转化为对设备的访问。如果用户空间可直接通过内存映射访问显存的话,屏幕帧的各点的像素将不再需要一个从用户空间到内核空间的复制过程,mmap()必须以PAGE_SIZE为单位进行映射
从file_operatoions文件操作结构体可以看出,驱动中mmap()函数的原型:
Int (*mmap) (struct file*,structvm_area_struct*);
用户的调用为:
Caddr_t mmap(caddr_t addr,size_t len,intport, int flags,int fd,off_t offset);
当用户调用mmap()的时候,内核会进行如下处理:
在进程的虚拟空间查找一块VMA
将这块VMA进行映射
如果设备驱动程序或文件系统的file_operations定义了mmap()操作,则调用它
将这个vma插入进程的VMA链表中
File_operations中mmap()函数的第一个参数就是步骤1中找到的VMA
至于file_operations中的mmap()需要别的系统调用remap_pfn_range()
大多数设备驱动都不需要提供设备内存到用户空间的映射能力,因为,对于串口等面向流的设备而言,实现这种映射毫无意义,而对于显示、视频等设备,建立映射可减少用户空间和内核空间之间的内存拷贝
参考:linux设备驱动开发详解