Linux硬件设备访问
一、mmap设备操作
1、mmap系统调用
void *mmap(void * addr, size_t len, int prot, int flags, int fd, off_t offset)
返回值:内存映射的首地址(虚拟映射区首地址)。
功能:内存映射函数mmap,负责把文件内容映射到进程的虚拟内存空间,通过对这段内存的读取和修改(使用指针),来实现对文件的读取和修改,而不需要再调用read,write等操作。实际上内核和应用程序都是使用的虚拟地址,我们不会对物理地址直接操作,而是通过映射将物理地址转化为虚拟地址,然后对虚拟地址操作来实现对物理地址的操作。
参数:addr:指定映射的起始地址,通常设为NULL,由系统指定。
len:映射到内存的文件长度
prot:映射区的保护方式,可以是:
PROT_EXEC:映射区可被执行
PROT_READ:映射区可被读取
PROT_WRITE:映射区可被写入
flags:映射区的特性,可以是:
MAP_SHARED:写入映射区的数据会复制回文件,且允许其他映射该文件的进程共享。
MAP_PRIVATE:对映射区的写入操作会产生一个映射区的复制(copy-on-write),对此区域所做的修改不会写回原文件。
fd:由open返回的文件描述符,代表要映射的文件
offset:以文件开始处的偏移量,必须是分页大小的整数倍,通常为0,表示从文件头开始映射。
解除映射
int munmap(void *start, size_t length)
功能:取消参数start所指向的映射内存,参数length表示欲取消的内存大小。
返回值:解除成功返回0,否则返回-1,错误原因存于errno中。
实例分析:mmap系统调用
//printf输入输出函数需要的头文件 #include <stdio.h> //open系统调用需要的头文件 #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> //close需要的头文件 #include<unistd.h> //mmap/munmap需要的头文件 #include<sys/mman.h> //strcpy需要的头文件 #include<string.h> int main() { int fd; char *start; char buf[100]; /*打开文件*/ fd = open("testfile",O_RDWR); start=mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); //注意:mmap不会改变原有文件的长度,也就是说原有文件有多长,我们才能写入多长,多出来的部分,会被舍弃。 /* 读出数据 */ strcpy(buf,start); printf("buf = %s\n",buf); /* 写入数据 */ strcpy(start,"Buf Is Not Null!"); //mmap不会影响原有文件的长度;所以原来有多长,就会写入多长 munmap(start,100); /*解除映射*/ close(fd); return 0; }
2、虚拟内存区域
虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。一个进程的内存映像由下面几个部分组成:程序代码、数据、BSS和栈区域,以及内存映射的区域。
一个进程的内存区域可以通过查看/proc/pid(1)/maps
001f6000-00231000 r-xp 00000000 fd:00 263878 /lib/libsepol.so.1
00231000-00232000 rw-p 0003a000 fd:00 263878 /lib/libsepol.so.1
00232000-0023c000 rw-p 00232000 00:00 0
0023e000-00253000 r-xp 00000000 fd:00 263879 /lib/libselinux.so.1
00253000-00255000 rw-p 00015000 fd:00 263879 /lib/libselinux.so.1
00451000-00452000 r-xp 00451000 00:00 0 [vdso]
009a2000-009bb000 r-xp 00000000 fd:00 263818 /lib/ld-2.5.so
009bb000-009bc000 r--p 00019000 fd:00 263818 /lib/ld-2.5.so
009bc000-009bd000 rw-p 0001a000 fd:00 263818 /lib/ld-2.5.so
每一行的域为:
start_end perm offset major:minor inode
start:该区域起始虚拟地址
end:该区域结束虚拟地址
perm:读、写和执行权限;表示对这个区域,允许进程做什么。这个域的最后一个字符要么是p表示私有的,要么是s表示共享的。
offset:被映射部分在文件中的起始地址
major、minor:主次设备号
inode:索引结点
3、vm_area_struct
linux内核使用结构vm_area_struct(<linux/mm_types.h>)来描述虚拟内存区域,其中几个主要成员如下:
unsigned long vm_start
虚拟内存区域起始地址
unsigned long vm_end
虚拟内存区域结束地址
unsigned long vm_flags
该区域的标记。如:VM_IO和VM_RESERVED。VM_IO将该VMA标记为内存映射的IO区域,VM_IO会阻止系统将该区域包含在进程的存放转存(core dump)中,VM_RESERVED标志内存区域不能被唤出。
4、mmap设备操作
映射一个设备是指把用户空间的一段地址关联到设备内存上。当程序读写这段用户空间的地址时,它实际上是在访问设备。实际上mmap设备方法要做三部分工作,首先要找到用户空间的一段地址,这个由内核来完成,内核完全知道那段用户空间可以用;设备的物理地址我们可以从芯片手册上得到,所以也较简单。而我们的驱动只是完成一小部分那就是实现用户空间的这段地址与设备内存的映射。
mmap设备方法需要完成什么功能?
mmap方法是file_oprations结构的成员,在mmap系统调用发出时被调用。在此之前,内核已经完成了很多工作。mmap设备方法所需要做的就是建立虚拟地址到物理地址的页表。
int (*mmap)(struct file *, struct vm_area_struct *)
mmap如何完成页表的建立?
方法有二:
1、使用remap_pfn_range一次建立所有页表;
2、使用nopage VMA方法每次建立一个页表。
这里解释第一种,remap_pfn_range函数,原型如下:
int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot)
vma:虚拟内存区域指针
virt_addr:虚拟地址的起始值
pfn:要映射的物理地址所在的物理页帧号,可将物理地址>>PAGE_SHIFT得到(PAGE_SHIFT是一个宏,数字12,代表12个字节)。
size:要映射的区域的大小。
prot:VMA的保护属性
mmap设备方法的实现:
int memdev_mmap(struct file *filp, struct vm_area_struct *vma)
{
Vma->vm_flags |= VM_IO;
Vma->vm_flags |= VM_RESERVED;
if(remap_pfn_range(vma, vma->vm_start,
virt_to_phys(dev->data) >> PAGE_SHIFT,
size, vma->vm_page_prot))
return -EAGAIN;
return 0;
}
二、硬件访问
1、寄存器和内存的区别在哪里呢?
寄存器和RAM的主要不同在于寄存器操作有副作用(side effect或边际效果):读取某个地址时可能导致该地址内容发生变化,比如很多设备的中断状态寄存器只要一读取,便自动清零。
2、内存与I/O
在X86处理器中存在I/O空间的概念,I/O空间是相对内存空间而言的,他们是彼此独立的地址空间,在32位的X86系统中,I/O空间大小为64k,内存空间大小为4G。
X86:支持内存空间、IO空间
ARM:只支持内存空间
MIPS:只支持内存空间
PowerPC:只支持内存空间
3、IO端口与IO内存
IO端口:
当一个寄存器或内存位于IO空间时,称其为IO端口。
IO内存:
当一个寄存器或内存位于内存空间时,称其为IO内存。
操作IO端口
对I/O端口的操作需按如下步骤完成:
1、申请
2、访问
3、释放
申请I/O端口:内核提供了一套函数来允许驱动申请它需要的I/O端口,其中核心的函数是:
struct resource *request_region(unsigned long first, unsigned long n, const char *name)
这个函数告诉内核,你要使用从first开始的n个端口,name参数是设备的名字。如果申请成功,返回非NULL,申请失败,返回NULL。
系统中端口的分配情况记录在/proc/ioports中(展示)。如果不能分配需要的端口,可以来这里查看谁在使用。
访问I/O端口:I/O端口可分为8-位,16-位,和32-位端口。linux内核头文件(体系依赖的头文件<asm/io.h>)定义了下列内联函数来访问I/O端口:
unsigned inb(unsigned port)
读字节端口(8位宽)
void outb(unsigned char byte, unsigned port)
写字节端口(8位宽)
unsigned inw(unsigned port)
void outw(unsigned short word, unsigned port)
存取16-位端口
unsigned inl(unsigned port)
void outl(unsigned longword, unsigned port)
存取32-位端口
释放I/O端口:
当用完一组I/O端口(通常在驱动卸载时),应使用如下函数把它们返还给系统:
void release_region(unsigned long start, unsigned long n)
操作I/O内存
对I/O内存的操作需按如下步骤完成:
1、申请
2、映射
3、访问
4、释放
申请I/O内存:内核提供了一套函数来允许驱动申请它需要的I/O内存,其中核心的函数是:
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name)
这个函数申请一个从start开始,长度为len字节的内存区。如果成功,返回非NULL;否则返回NULL,所有已经在使用的I/O内存在/proc/iomem中列出。
映射I/O内存:在访问I/O内存之前,必须进行物理地址到虚拟地址的映射,ioremap函数具有此功能:
void *ioremap(unsigned long phys_addr, unsigned long size)
访问I/O内存:访问I/O内存的正确方法是通过一系列内核提供的函数:
从I/O内存读,使用下列之一:
unsigned ioread8(void *addr)
unsigned ioread16(void *addr)
unsigned ioread32(void *addr)
写I/O内存,使用下列之一:
void iowrite8(u8 value, void *addr)
void iowrite16(u16 value, void *addr)
void iowrite32(u32 value, void *addr)
老版本的I/O内存访问函数:
从I/O内存读,使用下列之一:
unsigned readb(address)
unsigned readw(address)
unsigned readl(address)
写I/O内存,使用下列之一:
unsigned writeb(unsigned value, address)
unsigned writew(unsigned value, address)
unsigned writel(unsigned value, address)
释放I/O内存
I/O内存不再需要使用时应该释放,步骤如下:
1、void iounmap(void * addr)
2、void release_mem_region(unsigned long start, unsigned long len)
三、混杂设备驱动
1、定义
在Linux系统中,存在一类字符设备,它们共享一个主设备号(10),但次设备号不同,我们称这样设备为混杂设备(miscdevice)。所有的混杂设备形成一个链表,对设备访问时内核根据次设备号查找到相应miscdevice设备
2、设备描述
Linux内核使用struct miscdevice来描述一个混杂设备。
struct miscdevice
{
int minor; /*次设备号*/
const char *name; /*设备号*/
const struct file_operations *fops; /*文件操作*/
struct list_head list;
struct device *parent;
struct device *this_device;
}
3、设备注册
Linux内核使用misc_register函数来注册
一个混杂设备驱动。
int misc_register(struct miscdevice * misc)
四、LED驱动程序设计