新闻中心

EEPW首页 > 嵌入式系统 > 设计应用 > 一文读懂Linux下如何访问I/O端口和I/O内存

一文读懂Linux下如何访问I/O端口和I/O内存

作者:时间:2017-12-27来源:网络收藏

  虽然访问端口非常简单,但是检测哪些端口已经分配给设备可能就不这么简单了,对基于ISA总线的系统来说更是如此。通常,I/O设备驱动程序为了探测硬件设备,需要盲目地向某一I/O端口写入数据;但是,如果其他硬件设备已经使用这个端口,那么系统就会崩溃。为了防止这种情况的发生,内核必须使用“资源”来记录分配给每个硬件设备的I/O端口。资源表示某个实体的一部分,这部分被互斥地分配给设备驱动程序。在这里,资源表示I/O端口地址的一个范围。每个资源对应的信息存放在resource数据结构中:

本文引用地址:http://www.eepw.com.cn/article/201712/373670.htm

  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;// 指向资源树中父亲,兄弟和孩子的指针

  };

  所有的同种资源都插入到一个树型数据结构(父亲、兄弟和孩子)中;例如,表示I/O端口地址范围的所有资源都包括在一个根节点为ioport_resource的树中。节点的孩子被收集在一个链表中,其第一个元素由child指向。sibling字段指向链表中的下一个节点。

  为什么使用树?例如,考虑一下IDE硬盘接口所使用的I/O端口地址-比如说从0xf000 到 0xf00f。那么,start字段为0xf000 且end 字段为0xf00f的这样一个资源包含在树中,控制器的常规名字存放在name字段中。但是,IDE设备驱动程序需要记住另外的信息,也就是IDE链主盘使用0xf000 到0xf007的子范围,从盘使用0xf008 到0xf00f的子范围。为了做到这点,设备驱动程序把两个子范围对应的孩子插入到从0xf000 到0xf00f的整个范围对应的资源下。

  一般来说,树中的每个节点肯定相当于父节点对应范围的一个子范围。I/O端口资源树(ioport_resource)的根节点跨越了整个I/O地址空间(从端口0到65535)。

  任何设备驱动程序都可以使用下面三个函数,传递给它们的参数为资源树的根节点和要插入的新资源数据结构的地址:

  request_resource( ) //把一个给定范围分配给一个I/O设备。

  allocate_resource( ) //在资源树中寻找一个给定大小和排列方式的可用范围;若存在,将这个范围分配给一个I/O设备(主要由PCI设备驱动程序使用,可以使用任意的端口号和主板上的内存地址对其进行配置)。

  release_resource( ) //释放以前分配给I/O设备的给定范围。

  内核也为以上函数定义了一些应用于I/O端口的快捷函数:request_region( )分配I/O端口的给定范围,release_region( )释放以前分配给I/O端口的范围。当前分配给I/O设备的所有I/O地址的树都可以从/proc/ioports文件中获得。

  2、内存映射方式

  将IO端口映射为内存进行访问,在设备打开或驱动模块被加载时,申请IO端口区域并使用ioport_map()映射到内存,之后使用IO内存的函数进行端口访问,最后,在设备关闭或驱动模块被卸载时释放IO端口并释放映射。

  映射函数的原型为:

  void *ioport_map(unsigned long port, unsigned int count);

  通过这个函数,可以把port开始的count个连续的I/O端口重映射为一段“内存空间”。然后就可以在其返回的地址上像访问I/O内存一样访问这些I/O端口。但请注意,在进行映射前,还必须通过request_region( )分配I/O端口。

  当不再需要这种映射时,需要调用下面的函数来撤消:

  void ioport_unmap(void *addr);

  在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是宜使用内核的如下一组函数来完成访问I/O内存:

  读I/O内存

  unsigned int ioread8(void *addr);

  unsigned int ioread16(void *addr);

  unsigned int ioread32(void *addr);

  与上述函数对应的较早版本的函数为(这些函数在 2.6中仍然被支持):

  unsigned readb(address);

  unsigned readw(address);

  unsigned readl(address);

  写I/O内存

  void iowrite8(u8 value, void *addr);

  void iowrite16(u16 value, void *addr);

  void iowrite32(u32 value, void *addr);

  与上述函数对应的较早版本的函数为(这些函数在 2.6中仍然被支持):

  void writeb(unsigned value, address);

  void writew(unsigned value, address);

  void writel(unsigned value, address);

  流程如下:

    

 

  六、Linux下访问IO内存

  IO内存的访问方法是:首先调用request_mem_region()申请资源,接着将寄存器地址通过ioremap()映射到内核空间的虚拟地址,之后就可以Linux设备访问编程接口访问这些寄存器了,访问完成后,使用ioremap()对申请的虚拟地址进行释放,并释放release_mem_region()申请的IO内存资源。

  struct resource *requset_mem_region(unsigned long start, unsigned long len,char *name);

  这个函数从内核申请len个内存地址(在3G~4G之间的虚地址),而这里的start为I/O物理地址,name为设备的名称。注意,。如果分配成功,则返回非NULL,否则,返回NULL。

  另外,可以通过/proc/iomem查看系统给各种设备的内存范围。

  要释放所申请的I/O内存,应当使用release_mem_region()函数:

  void release_mem_region(unsigned long start, unsigned long len)

  申请一组I/O内存后, 调用ioremap()函数:

  void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);

  其中三个参数的含义为:

  phys_addr:与requset_mem_region函数中参数start相同的I/O物理地址;

  size:要映射的空间的大小;

  flags:要映射的IO空间的和权限有关的标志;

  功能:将一个I/O地址空间映射到内核的虚拟地址空间上(通过release_mem_region()申请到的)

  流程如下:

    

 

  七、ioremap和ioport_map

  下面具体看一下ioport_map和ioport_umap的源码:

  void __iomem *ioport_map(unsigned long port, unsigned int nr)

  {

  1.

  if (port > PIO_MASK)

  2.

  return NULL;

  3.

  return (void __iomem *) (unsigned long) (port + PIO_OFFSET);

  4.

  }

  5.

  6.

  void ioport_unmap(void __iomem *addr)

  7.

  {

  8.

  /* Nothing to do */

  ioport_map仅仅是将port加上PIO_OFFSET(64k),而ioport_unmap则什么都不做。这样portio的64k空间就被映射到虚拟地址的64k~128k之间,而ioremap返回的虚拟地址则肯定在3G之上。ioport_map函数的目的是试图提供与ioremap一致的虚拟地址空间。分析ioport_map()的源代码可发现,所谓的映射到内存空间行为实际上是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用统一的I/O内存访问接口ioread8/iowrite8(......)访问I/O端口。

  最后来看一下ioread8的源码,其实现也就是对虚拟地址进行了判断,以区分IO端口和IO内存,然后分别使用inb/outb和readb/writeb来读写。

  unsigned int fastcall ioread8(void __iomem *addr)

  {

  IO_COND(addr, return inb(port), return readb(addr));

  }

  #define VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET)

  #define IO_COND(addr, is_pio, is_mmio) do { 

  unsigned long port = (unsigned long __force)addr; 

  if (port < PIO_RESERVED) { 

  VERIFY_PIO(port); 

  port &= PIO_MASK; 

  is_pio; 

  } else { 

  is_mmio; 

  } 

  } while (0)

  展开:

  unsigned int fastcall ioread8(void __iomem *addr)

  {

  unsigned long port = (unsigned long __force)addr;

  if( port < 0x40000UL ) {

  BUG_ON( (port & ~PIO_MASK) != PIO_OFFSET );

  port &= PIO_MASK;

  return inb(port);

  }else{

  return readb(addr);

  }

  }

  八、总结

  外设IO寄存器地址独立编址的CPU,这时应该称外设IO寄存器为IO端口,访问IO寄存器可通过ioport_map将其映射到虚拟地址空间,但实际上这是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了可以使用和IO内存一样的接口访问IO寄存器;也可以直接使用in/out指令访问IO寄存器。

  例如:Intel x86平台普通使用了名为内存映射(MMIO)的技术,该技术是PCI规范的一部分,IO设备端口被映射到内存空间,映射后,CPU访问IO端口就如同访 问内存一样。

  外设IO寄存器地址统一编址的CPU,这时应该称外设IO寄存器为IO内存,访问IO寄存器可通过ioremap将其映射到虚拟地址空间,然后再使用read/write接口访问。


上一页 1 2 下一页

关键词: Linux I/O

评论


相关推荐

技术专区

关闭