以下总结为均为个人查阅各种资料加上个人理解总结而成:

一.在学习关于IO内存与硬件通信方式过程之前,首先需要了解驱动中的内存分配,可以通过三种方式:

1.kmalloc 注意kmalloc函数的第二个参数的标志,可以指定内存分配的相关方式或者属性
  1. #include <linux/slab.h>   
  2. void *kmalloc(size_t size,int flag)  
2.get_free_page 用于分配大块的页
  1. get_zero_page(unsigned int flags);   
  2.     分配一个被清空的页   
  3. __get_free_page(unsigned int flags);   
  4.     分配一个未清空的页   
  5. __get_free_pages(unsigned int flags,unsigned int order);   
  6.     分配2的order次方个未清空页  
3.vmalloc 用于在虚拟地址空间分配一块连续区域,这些页在物理内存上不一定连续,其中每个页单独通过alloc_page调用来进行分配,内核将他们作为一个连续的内存区域给用户。
  1. #include <linux/valloc.h>   
  2. void *vmalloc(unsigned long size);  

上述三种分配方式对应的释放方式为:

1.kfree
  1. void kfree(void *p);  
2.free_page() free_pages()
  1. void free_page(unsigned long addr);   
  2. void free_pages(unsigned long addr,unsigned long order);   
  3. order=get_order(unsigned long size);  
3.vfree
  1. 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内核源代码中这个结构体定义如下:

  1. struct resource {   
  2.     resource_size_t start;   
  3.     resource_size_t end;   
  4.     const char *name;   
  5.     unsigned long flags;   
  6.     struct resource *parent, *sibling, *child;   
  7. };  

start为起始端口,end为终止端口,name是设备的名字,flags是一个标志,剩下的几个成员,个人感觉应该是采用数据结构中的树进行IO端口管理,所以看到父亲结点和孩子结点。

注意:在分配过程中,可能需要检查一个给定的IO端口是否可用,函数如下:

  1. int check_region(unsigned long start, unsigned long n);  
2.IO端口分配完成之后,就可以操作这些IO端口进行存取操作。

这些函数分为单次操作端口,或者重复操作端口,即操作一个连续的端口号(组),并且各种操作包括对字节,半字,和字类型的数据进行操作函数,这里8位表示一个字节,16位表示半字,32位表示一个字。这些函数如下,根据函数名、参数、返回值即可区分其用途。

  1. 单次操作:   
  2.     unsigned inb(unsigned port);   
  3.     void outb(unsigned char byte, unsigned port);   
  4.         读或写字节端口(8位宽)。port参数在某些平台定义为unsigned long以及在其他上定义为unsigned short。   
  5.     unsigned inw(unsigned port);   
  6.     void outw(unsigned short word, unsigned port);   
  7.         这些函数存取16位端口(一个字宽)。   
  8.     unsigned inl(unsigned port);   
  9.     void outl(unsigned long word, unsigned port);   
  10.         这些函数存取32位端口, long word声明为或者unsigned long或者unsigned int。   
  11. 重复操作:   
  12.     void insb(unsigned port, void *addr, unsigned long count);   
  13.     void outsb(unsigned port, void *addr, unsigned long count);   
  14.     读或写从内存地址addr开始的count字节,数据读自或者写入单个port端口。   
  15.   
  16.     void insw(unsigned port, void *addr, unsigned long count);   
  17.     void outsw(unsigned port, void *addr, unsigned long count);   
  18.     读或写16位值到一个16位端口。   
  19.            
  20.     void insl(unsigned port, void *addr, unsigned long count);   
  21.     void outsl(unsigned port, void *addr, unsigned long count);   
  22.     读或写32位值到一个32位端口。   
3.和大多数函数一样,我们需要在操作完成之后,对端口进行释放,释放后系统才可对其使用,关于释放的时机,可能是在模块卸载时,也可能是在其他时候。
  1. 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>定义):

  1. 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的):

  1. #include <asm/io.h>   
  2. void *ioremap(unsigned long   phys_addr,unsigned long   size);   
  3. //将长度为size,起始地址为phys_addr的物理内存地址映射到虚拟地址,虚拟地址的首地址作为返回值返回。其本质是在MMU的页表中新建条目。   
  4.   
  5. void *ioremap_nocache(unsigned long   phys_addr, unsigned long   size);   
  6. //同ioremap,区别在于不允许映射的内存区域的内容可在CPU的cache中缓存, 其本质是在新建MMU的页表条目时, 在该条目的相应域指定不可缓存。但由于对外设寄存器的映射都不应该允许缓存,所以内核对这两个API的实现是一样的。  
3.映射到虚拟地址之后,我们便可以对该地址进行存放数据。这里可能会有疑问,因为映射之后就是一个地址,在C语言中,我们可以把这个地址当作一个指针来看待,进行操作,当然这里也可以来实现,但是《linux设备驱动程序》这本书上提到,这种方法不具备移植性,访问IO内存地址的正确方法是通过下述地址进行访问。同样区分8位,16位,32位的值和单次访问或者重复访问。函数如下:
  1. 单次访问:   
  2. 从I/O内存地址addr处读取字节、半字、字:   
  3. unsigned int ioread8(void *addr);   
  4. unsigned int ioread16(void *addr);   
  5. unsigned int ioread32(void *addr);   
  6.   
  7. 向I/O内存地址addr处写入字节、半字、字:   
  8. void iowrite8(u8 value, void *addr);   
  9. void iowrite16(u16 value, void *addr);   
  10. void iowrite32(u32 value, void *addr);   
  11.   
  12. 重复访问:   
  13. void ioread8_rep(void *addr, void *buf, unsigned long count);   
  14. void ioread16_rep(void *addr, void *buf, unsigned long count);   
  15. void ioread32_rep(void *addr, void *buf, unsigned long count);   
  16.   
  17. void iowrite8_rep(void *addr, const  void *buf, unsigned long count);   
  18. void iowrite16_rep(void *addr, const void *buf, unsigned long count);   
  19. void iowrite32_rep(void *addr, const void *buf, unsigned long count);   

在《linux设备驱动程序》这本书也提到,如果要IO内存进行直接操作,可以采用下面的函数:

  1. void memset_io(void *addr,v8 value,unsigned int count);   
  2. void memcpy_fromio(void *dest,void *source,unsigned int count);   
  3. void memcmpy_toio(void *dest,void *source,unsigned int count);     
  4. //类似于C函数库中memset和memcpy函数的功能。   

在阅读程序过程中,可能会遇到一些老的API接口,函数如下:

  1. unsigned  readb(address);   
  2. unsigned  readw(address);   
  3. unsigned  readl(address);   
  4.   
  5. void writeb(unsigned value, address);   
  6. void writew(unsigned value, address);   
  7. void writel(unsigned value, address);   
  8.   
  9. void_raw_writel(unsigned value, address);  

这些函数的确定显而易见,并没有对类型进行限制,即没有执行类型检查,因此安全性较差。

还有一点注意的是,以前的处理器可能采用16位作为一个字来操作导致的一些问题。

4.取消IO内存映射:
  1. void iounmap(void *addr);   
  2. //取消ioremap建立的虚实地址映射。其本质是在MMU的页表中删除条目。  
5.当IO内存不再使用时,我们应该释放,调用函数:
  1. void release_mem_region(unsigned long start, unsigned long len);  

 

linux设备驱动程序中关于IO内存与硬件通信方式总结

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据