以下总结为均为个人查阅各种资料加上个人理解总结而成:
一.在学习关于IO内存与硬件通信方式过程之前,首先需要了解驱动中的内存分配,可以通过三种方式:
1.kmalloc 注意kmalloc函数的第二个参数的标志,可以指定内存分配的相关方式或者属性
- #include <linux/slab.h>
- void *kmalloc(size_t size,int flag)
2.get_free_page 用于分配大块的页
- get_zero_page(unsigned int flags);
- 分配一个被清空的页
- __get_free_page(unsigned int flags);
- 分配一个未清空的页
- __get_free_pages(unsigned int flags,unsigned int order);
- 分配2的order次方个未清空页
3.vmalloc 用于在虚拟地址空间分配一块连续区域,这些页在物理内存上不一定连续,其中每个页单独通过alloc_page调用来进行分配,内核将他们作为一个连续的内存区域给用户。
- #include <linux/valloc.h>
- void *vmalloc(unsigned long size);
上述三种分配方式对应的释放方式为:
1.kfree
- void kfree(void *p);
2.free_page() free_pages()
- void free_page(unsigned long addr);
- void free_pages(unsigned long addr,unsigned long order);
- order=get_order(unsigned long size);
3.vfree
- void vfree(void *addr);
除了free_pages需要指定要释放的页数参数之外,其他几个函数参数都是页的起始地址。
二、使用IO端口地址空间与硬件进行通信的内核API
注:这一块个人理解起来不是很通
我们在操作外部设备是通过读写其芯片上寄存器进行控制的,大部分一个设备的芯片对应着几个寄存器,并且这几个寄存器在地址是相邻的,比如在LED驱动程序中的GPBCON GPBDAT GPBUP这三个控制寄存器,地址分别是,0×56000010 0×56000014 0×56000018,这个空间在内存地址空间或者IO地址空间。在硬件级别上,内存地址空间区域和IO地址空间区域没有明确的区别,都是通过控制总线和地址总线进行控制存取数据的。所以我们可以直接对这些地址空间来进行操作。
值得注意的是:ARM体系结构下,没有这里所说的IO端口,但是我们在编写驱动程序过程中,并不仅仅是在ARM体系下编程,所以仍然需要了解。
操作方法如下:
1.首先要进行IO端口分配
- #include <linux/ioport.h>
- struct resource *request_region(unsigned long first, unsigned long n, const char *name);
这个函数告诉内核, 要使用n个端口,从first开始,name参数是设备的名字,它会出现在/proc/ioports中。可以通过/proc/ioports接口查看系统中I/O端口的分配情况。
可以看到返回值为一个结构体sturct resource,在linux2.6.22.6内核源代码中这个结构体定义如下:
- struct resource {
- resource_size_t start;
- resource_size_t end;
- const char *name;
- unsigned long flags;
- struct resource *parent, *sibling, *child;
- };
start为起始端口,end为终止端口,name是设备的名字,flags是一个标志,剩下的几个成员,个人感觉应该是采用数据结构中的树进行IO端口管理,所以看到父亲结点和孩子结点。
注意:在分配过程中,可能需要检查一个给定的IO端口是否可用,函数如下:
- int check_region(unsigned long start, unsigned long n);
2.IO端口分配完成之后,就可以操作这些IO端口进行存取操作。
这些函数分为单次操作端口,或者重复操作端口,即操作一个连续的端口号(组),并且各种操作包括对字节,半字,和字类型的数据进行操作函数,这里8位表示一个字节,16位表示半字,32位表示一个字。这些函数如下,根据函数名、参数、返回值即可区分其用途。
- 单次操作:
- unsigned inb(unsigned port);
- void outb(unsigned char byte, unsigned port);
- 读或写字节端口(8位宽)。port参数在某些平台定义为unsigned long以及在其他上定义为unsigned short。
- unsigned inw(unsigned port);
- void outw(unsigned short word, unsigned port);
- 这些函数存取16位端口(一个字宽)。
- unsigned inl(unsigned port);
- void outl(unsigned long word, unsigned port);
- 这些函数存取32位端口, long word声明为或者unsigned long或者unsigned int。
- 重复操作:
- void insb(unsigned port, void *addr, unsigned long count);
- void outsb(unsigned port, void *addr, unsigned long count);
- 读或写从内存地址addr开始的count字节,数据读自或者写入单个port端口。
- void insw(unsigned port, void *addr, unsigned long count);
- void outsw(unsigned port, void *addr, unsigned long count);
- 读或写16位值到一个16位端口。
- void insl(unsigned port, void *addr, unsigned long count);
- void outsl(unsigned port, void *addr, unsigned long count);
- 读或写32位值到一个32位端口。
3.和大多数函数一样,我们需要在操作完成之后,对端口进行释放,释放后系统才可对其使用,关于释放的时机,可能是在模块卸载时,也可能是在其他时候。
- void release_region(unsigned long start, unsigned long n);
三、使用IO内存地址空间与硬件通信
上面也提到IO地址空间与物理地址空间在硬件级别上并没有多少区别,也就是说IO内存实际位置是外设控制器芯片上的物理寄存器,并且其地址空间与普通内存进行统一编址,也就说IO内存地址空间和外设上的控制器芯片上的物理寄存器地址是相同的。在32位机器上,位于0-4GB的某个位置,这也就是为什么我们32位机器装4G内存条一般只能识别3G左右的空间。
在程序中对IO内存完成了对外设的驱动控制。
值得注意的是,ARM体系结构下只有IO内存,没有上面所讲的IO端口。而在x86体系下存在IO端口,所在x86机子可以访问IO端口,但是IO端口并不是与内存进行统一编址的,它是独立编址,所以在x86下提供一套区别于内存访存指令的IO端口访问指令。
关于使用方法同上一下,申请、映射、操作、释放三步骤,详细如下:
1.IO内存分配:
I/O内存区必须在使用前分配。分配内存区的接口是(在<linux/ioport.h>定义):
- struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
这个函数分配一个len字节的内存区,从start(物理地址)开始。如果一切顺利,一个非NULL指针返回;否则, 返回值是NULL。
name是申请到的区域名称, 会在/proc/iomem中列出。可以通过/proc/iomem接口查看系统中I/O内存的分配情况。
2.IO内存映射:
由于IO内存分配和释放的地址使用的是物理地址,而在linux下对内存进行合法的访问必须采用虚拟地址,因此在使用申请到的内存之前,必须对该区域进行映射,即映射到虚拟地址,才能对虚拟地址对该区域的访问,比如led驱动程序中,我们可能将GPBCON的地址0×56000010映射为一个其他的虚拟地址,以提供我们对内存进行合法的访问。
采用的函数方法如下(分为带cache和不带cache的):
- #include <asm/io.h>
- void *ioremap(unsigned long phys_addr,unsigned long size);
- //将长度为size,起始地址为phys_addr的物理内存地址映射到虚拟地址,虚拟地址的首地址作为返回值返回。其本质是在MMU的页表中新建条目。
- void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
- //同ioremap,区别在于不允许映射的内存区域的内容可在CPU的cache中缓存, 其本质是在新建MMU的页表条目时, 在该条目的相应域指定不可缓存。但由于对外设寄存器的映射都不应该允许缓存,所以内核对这两个API的实现是一样的。
3.映射到虚拟地址之后,我们便可以对该地址进行存放数据。这里可能会有疑问,因为映射之后就是一个地址,在C语言中,我们可以把这个地址当作一个指针来看待,进行操作,当然这里也可以来实现,但是《linux设备驱动程序》这本书上提到,这种方法不具备移植性,访问IO内存地址的正确方法是通过下述地址进行访问。同样区分8位,16位,32位的值和单次访问或者重复访问。函数如下:
- 单次访问:
- 从I/O内存地址addr处读取字节、半字、字:
- unsigned int ioread8(void *addr);
- unsigned int ioread16(void *addr);
- unsigned int ioread32(void *addr);
- 向I/O内存地址addr处写入字节、半字、字:
- void iowrite8(u8 value, void *addr);
- void iowrite16(u16 value, void *addr);
- void iowrite32(u32 value, void *addr);
- 重复访问:
- void ioread8_rep(void *addr, void *buf, unsigned long count);
- void ioread16_rep(void *addr, void *buf, unsigned long count);
- void ioread32_rep(void *addr, void *buf, unsigned long count);
- void iowrite8_rep(void *addr, const void *buf, unsigned long count);
- void iowrite16_rep(void *addr, const void *buf, unsigned long count);
- void iowrite32_rep(void *addr, const void *buf, unsigned long count);
在《linux设备驱动程序》这本书也提到,如果要IO内存进行直接操作,可以采用下面的函数:
- void memset_io(void *addr,v8 value,unsigned int count);
- void memcpy_fromio(void *dest,void *source,unsigned int count);
- void memcmpy_toio(void *dest,void *source,unsigned int count);
- //类似于C函数库中memset和memcpy函数的功能。
在阅读程序过程中,可能会遇到一些老的API接口,函数如下:
- unsigned readb(address);
- unsigned readw(address);
- unsigned readl(address);
- void writeb(unsigned value, address);
- void writew(unsigned value, address);
- void writel(unsigned value, address);
- void_raw_writel(unsigned value, address);
这些函数的确定显而易见,并没有对类型进行限制,即没有执行类型检查,因此安全性较差。
还有一点注意的是,以前的处理器可能采用16位作为一个字来操作导致的一些问题。
4.取消IO内存映射:
- void iounmap(void *addr);
- //取消ioremap建立的虚实地址映射。其本质是在MMU的页表中删除条目。
5.当IO内存不再使用时,我们应该释放,调用函数:
- void release_mem_region(unsigned long start, unsigned long len);