MIPS、ARM和PowerPC等平台的I/O和主存采用的是统一编址的方式,它们将I/O空间映射到内存,采用与内存相同的汇编指令(load和store)来读写设备,这种方式称为I/O内存;x86平台的I/O和主存采用的是独立编址的方式,CPU有专门的线路来访问I/O,而且有专门的汇编指令(in和out)来读写设备,这种方式称为I/O端口。

    Linux系统在各种平台(不管是MIPS、ARM、PowerPC,还是x86)上,都提供了文件/proc/ioports和/proc/iomem。可以cat这两个文件来查看I/O端口的分配情况和I/O映射到内存的空间范围。另外Linux还提供了相应的两个设备文件/dev/port和/dev/mem,可以读写这两个文件来访问相应的端口和内存。尽管MIPS等平台采用的是I/O内存的方式,但是也提供了ioports等文件,即可以通过函数inb()、outb()等同类函数来读写I/O端口,其实这些函数仅仅是个外壳,通过查看Linux内核源代码可以发现它们最终也是转换成readb()和writeb()来读写内存;对于x86平台,其inb()、outb()等同类函数是通过汇编指令in、out的形式来完成的。

      这部分内容可以查阅《Linux设备驱动程序 第三版》第9章的相关内容来深入学习。

      以下这篇文章主要讲解了通过函数ioport_map()来屏蔽I/O端口和I/O内存编程接口的区别。

      来源:http://blog.chinaunix.net/u3/90876/showart_1914461.html

     

      每种外设都通过读写寄存器进行控制。大部分外设都有几个寄存器,不管是在内存地址空间还是在I/O地址空间,这些寄存器的访问地址都是连续的。x86的处理器为I/O端口的读和写提供了独立的线路,并且使用特殊的CPU指令访问端口。但即使这样,也不是所有的设备都会把寄存器映射到I/O端口中。ISA设备普遍使用I/O端口,而大多数PCI设备则把寄存器映射到某个内存地址区段。
      I/O端口是驱动程序与许多设备之间的通信方式,Linux的内核为我们提供了I/O端口分配的操作接口,但对PCI设备来讲,它的配置地址空间已经为其指定了I/O端口范围,不需要额外的分配操作。Linux内核提供了如下一些访问I/O端口的内联函数:
    unsigned inb(unsigned port);
    void outb(unsigned char byte, unsigned port);
    unsigned inw(unsigned port);
    void outw(unsigned short word, unsigned port);
    unsigned inl(unsigned port);
    void outl(unsigned longword, unsigned port);
    下面我们重点来看一下2.6内核引入的ioport_map函数:
    void *ioport_map( unsigned long port, unsigned int count );
    通过这个函数,可以把port开始的count个连续端口重映射为一段“内存空间”。然后就可以在其返回的地址上象访问I/O内存一样访问这几个I/O端口。当不需要这种映射时,需要调用下面的函数来撤消:
    void ioport_unmap(void *addr);
    浏览2.6内核的源代码,我们不难发现,这种所谓的映射其实是“假”的。下面是这两个函数的实现,及一些相关数据:
    #define PIO_OFFSET  0x10000UL
    #define PIO_MASK    0x0ffffUL
    #define PIO_RESERVED    0x40000UL

    void __iomem *ioport_map(unsigned long port, unsigned int nr)
    {
        if (port > PIO_MASK)
            return NULL;
        return (void __iomem *) (unsigned long) (port + PIO_OFFSET);
    }

    void ioport_unmap(void __iomem *addr)
    {
        /* Nothing to do */
    }
    它只是简单地把I/O端口号加上PIO_OFFSET(64K),作为一个“假”的内存地址返回,而unmap则什么也不做。之所以这样做,是基于这样一个事实:真正的I/O内存地址经过映射成为虚拟地址后,由于是在内核空间,其值肯定大于3G。而port+PIO_OFFSET不会大于128K。所以,内核不会把这两种地址搞混。可以分别进行处理,下面看看ioread8函数的实现:
    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);
        }
    }
    所以,除了提供一个统一的接口,它并没有在本质上改变什么。
    除了I/O端口之外,和设备通信的另一种主要机制是通过使用映射到内存的寄存器或设备内存。这两种都称为I/O内存,因为寄存器和内存的差别对软件是透明的。
    对于分配好的I/O内存,一般不鼓励直接使用指向I/O内存的指针进行访问,最好通过页表,用包装函数访问。要通过页表访问,那么需要对分配好的I/O内存进行映射,确保该I/O内存对内核而言是可访问的。完成I/O内存映射的函数是ioremap:
    #include <asm/io.h>
    void *ioremap(unsigned long phys_addr, unsigned long size);
    它把一个I/O空间的物理地址映射到内核空间的虚拟地址。

Logo

更多推荐