IO端口和IO内存

在驱动程序编写过程中,很少会注意到 IO Port 和 IO Mem 的区别。虽然使用一些不符合规范的代码可以达到最终目的,这是极其不推荐使用的。

结合下图,我们彻底讲述 IO 端口和 IO 内存以及内存之间的关系。主存 16M 字节的 SDRAM ,外设是个视频采集卡,上面有 16M 字节的 SDRAM 作为缓冲区。

1.       CPU 是 i386 架构的情况在 i386 系列的处理中,内存和外部 IO 是独立编址,也是独立寻址的。 MEM的内存空间是 32 位可以寻址到 4G  IO 空间是 16 位可以寻址到 64K 

2.       在 Linux 内核中,访问外设上的 IO Port 必须通过 IO Port 的寻址方式。而访问 IO Mem 就比较罗嗦,外部 MEM 不能和主存一样访问,虽然大小上不相上下,可是外部 MEM 是没有在系统中注册的。访问外部 IO MEM 必须通过 remap 映射到内核的 MEM 空间后才能访问。为了达到接口的同一性,内核提供了 IO Port  IO Mem 的映射函数。映射后 IO Port 就可以看作是 IO Mem ,按照 IO Mem 的访问方式即可。

3.       CPU 是 ARM 或 PPC 架构的情况

在这一类的嵌入式处理器中, IO Port 的寻址方式是采用内存映射,也就是 IO bus 就是 Mem bus 。系统的寻址能力如果是 32 位, IO Port + Mem (包括 IO Mem )可以达到 4G 。

访问这类 IO Port 时,我们也可以用 IO Port 专用寻址方式。至于在对 IO Port 寻址时,内核是具体如何完成的,这个在内核移植时就已经完成。在这种架构的处理器中,仍然保持对 IO Port 的支持,完全是i386 架构遗留下来的问题,在此不多讨论。而访问 IO Mem 的方式和 i386 一致。

注意: linux 内核给我提供了完全对 IO Port 和 IO Mem 的支持,然而具体去看看 driver 目录下的驱动程序,很少按照这个规范去组织 IO Port 和 IO Mem 资源。对这二者访问最关键问题就是地址的定位 , C语言中,使用 volatile 就可以实现。很多的代码访问 IO Port 中的寄存器时,就使用 volatile 关键字 ,虽然功能可以实现,我们还是不推荐使用。就像最简单的延时莫过于 while ,可是在多任务的系统中是坚决避免的!

 

 RISC 指令系统的 CPU (如 ARM  PowerPC 等)通常只实现一个物理地址空间,外设 I/O 端口成为内存的一部分。此时, CPU 可以象访问一个内存单元那样访问外设 I/O 端口,而不需要设立专门的外设 I/O 指令。 

但是,这两者在硬件实现上的差异对于软件来说是完全透明的,驱动程序开发人员可以将内存映射方式的 I/O 端口和外设内存统一看作是"I/O 内存 " 资源。 

一般来说,在系统运行时,外设的 I/O 内存资源的物理地址是已知的,由硬件的设计决定。 但是 CPU 通常并没有为这些已知的外设 I/O 内存资源的物理地址 预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问 I/O 内存资源,而必须将它们映射到核心虚地址空间内(通过页表), 然后才能根据映射所得到的核 心虚地址范围,通过访内指令访问这些 I/O 内存资源。 Linux 在 io.h 头文件中声明了函数ioremap (),用来将 I/O 内存资源的物理地址映射到 核心虚地址空间( 3GB - 4GB )中,原型如下: 

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

iounmap 函数用于取消 ioremap ()所做的映射,原型如下: 

void iounmap(void * addr); 

这两个函数都是实现在 mm/ioremap.c 文件中。 

在将 I/O 内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写 RAM 那样直接读写 I/O 内存资源了。为了保证驱动程序的跨平台的可移植 性,我们应该使用 Linux 中特定的函数来访问 I/O 内存资源,而不应该通过指向核心虚地址的指针来访问。如在 x86 平台上,读写 I/O 的函数如下所示: 

#define readb(addr)   (*(volatile unsigned char *) __io_virt(addr))
#define readw(addr)   (*(volatile unsigned short *) __io_virt(addr))
#define readl(addr)    (*(volatile unsigned int *) __io_virt(addr))

#define writeb(b,addr)   (*(volatile unsigned char *) __io_virt(addr) = (b))
#define writew(b,addr)    (*(volatile unsigned short *) __io_virt(addr) = (b))
#define writel(b,addr)    (*(volatile unsigned int *) __io_virt(addr) = (b))

#define memset_io(a,b,c)   memset(__io_virt(a),(b),(c))
#define memcpy_fromio(a,b,c)   memcpy((a),__io_virt(b),(c))
#define memcpy_toio(a,b,c)    memcpy(__io_virt(a),(b),(c)) 

最后,我们要特别强调驱动程序中 mmap 函数的实现方法。用 mmap 映射一个设备,意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问。 

笔者在 Linux 源代码中进行包含 "ioremap" 文本的搜索,发现真正出现的 ioremap 的地方相当少。所以笔者追根索源地寻找 I/O 操作的物理地址转换到虚拟地址的真实所在,发现 Linux 有替代 ioremap 的语句,但是这个转换过程却是不可或缺的。

 

 

 

CPU 对外设端口 物理地址的编址方式有两种:

一种是 IO 映射方式,另一种是内存映射方式。

 

   Linux 将基于 IO 映射方式的和内存映射方式的 IO 端口统称为 IO 区域( IO region )。

 

   IO region 仍然是一种 IO 资源,因此它仍然可以用 resource 结构类型来描述。

 

   Linux 管理 IO region :

 

   1) request_region()

 

  把一个给定区间的 IO 端口分配给一个 IO 设备。

 

   2) check_region()

 

  检查一个给定区间的 IO 端口是否空闲,或者其中一些是否已经分配给某个 IO 设备。

 

   3) release_region()

 

  释放以前分配给一个 IO 设备的给定区间的 IO 端口。

 

   Linux 中可以通过以下辅助函数来访问 IO 端口:

 

   inb(),inw(),inl(),outb(),outw(),outl()

 

  “ b ”“ w ”“ l ”分别代表 8 位, 16 位, 32 位。

 

对 IO 内存资源的访问

 

   1) request_mem_region()

 

  请求分配指定的 IO 内存资源。

 

   2) check_mem_region()

 

  检查指定的 IO 内存资源是否已被占用。

 

   3) release_mem_region()

 

  释放指定的 IO 内存资源。

 

  其中传给函数的 start address 参数是内存区的物理地址(以上函数参数表已省略)。

 

  驱动开发人员可以将内存映射方式的 IO 端口和外设内存统一看作是 IO 内存资源。

 

 ioremap() 用来将 IO 资源的物理地址映射到内核虚地址空间( 3GB - 4GB )中,参数 addr 是指向内核虚地址的指针。

 

   Linux 中可以通过以下辅助函数来访问 IO 内存资源:

 

   readb(),readw(),readl(),writeb(),writew(),writel() 。

 

   Linux 在 kernel/resource.c 文件中定义了全局变量 ioport_resource 和 iomem_resource ,来分别描述基于 IO 映射方式的整个 IO 端口空间和基于内存映射方式的 IO 内存资源空间(包括 IO 端口和外设内存)。

 

 

内存映射( IO 地址和内存地址)

ARM 体系结构下 面内存和 i/o 映射区别 
( 1 )关于 IO 与内存空间: 
    在 X86 处理器中存在着 I/O 空间的概念, I/O 空间是相对于内存空间而言的,它通过特定的指令 in 、 out 来访问。端口号标识了外设的寄存器地址 。 Intel 语法的 in 、 out 指令格式为: 
    IN 累加器 , { 端口号│ DX}
    OUT { 端口号│ DX}, 累加器 
    目前,大多数嵌入式微控制器如 ARM  PowerPC 等中并不提供 I/O 空间,而仅存在内存空间。 内存空间可以直接通过地址、指针来访问,程序和程序运行中使用的变量和其他数据都存在于内存空间中。 
    即便是在 X86 处理器中,虽然提供了 I/O 空间,如果由我们自己设计电路板,外设仍然可以只挂接在内存空间。 此时, CPU 可以像访问一个内存单元那样访问外设 I/O 端口,而不需要设立专门的 I/O 指令。因此,内存空间是必须的,而 I/O 空间是可选的。 
( 2 ) inb 和 outb : 
在 Linux 设备驱动中,宜使用 Linux 内核提供的函数来访问定位于 I/O 空间的端口,这些函数包括: 
·   读写字节端口( 8 位宽) 
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
·   读写字端口( 16 位宽) 
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
·   读写长字端口( 32 位宽) 
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
·   读写一串字节 
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
·   insb() 从端口 port 开始读 count 个字节端口,并将读取结果写入 addr 指向的内存; outsb() 将 addr 指向的内存的 count 个字节连续地写入port 开始的端口。 
·   读写一串字 
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
·   读写一串长字 
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
上述各函数中 I/O 端口号 port 的类型高度依赖于具体的硬件平台,因此,只是写出了 unsigned  
( 3 ) readb 和 writeb:
在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是工程师宜使用 Linux 内核的如下一组函数来完成设备内存映射的虚拟地址的读写, 这些函数包括: 
·   读 I/O 内存 
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
与上述函数对应的较早版本的函数为(这些函数在 Linux 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);
与上述函数对应的较早版本的函数为(这些函数在 Linux 2.6 中仍然被支持): 
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);
 4 )把 I/O 端口映射到“内存空间” : 
void *ioport_map(unsigned long port, unsigned int count);
通过这个函数,可以把 port 开始的 count 个连续的 I/O 端口重映射为一段“内存空间”。然后就可以在其返回的地址上像访问 I/O 内存一样访问这些 I/O 端口。当不再需要这种映射时,需要调用下面的函数来撤消: 
void ioport_unmap(void *addr);
实际上,分析 ioport_map() 的源代码可发现,所谓的映射到内存空间行为实际上是给开发人员制造的一个“假象”,并没有映射到内核虚拟地址,仅仅是为了让工程师可使用统一的 I/O 内存访问接口访问 I/O 端口。 

11.2.7 I/O 空间的映射 
很多硬件设备都有自己的内存,通常称之为 I/O 空间。 例如,所有比较新的图形卡都有几 MB 的 RAM ,称为显存,用它来存放要在屏幕上显示的屏幕影像。 
1 .地址映射 
    根据设备和总线类型的不同, PC 体系结构中的 I/O 空间可以在三个不同的物理地址范围之间进行映射: 
( 1 )对于连接到 ISA 总线上的大多数设备 
I/O 空间通常被映射到从 0xa0000 到 0xfffff 的物理地址范围,这就在 640K 和 1MB 之间留出了一段空间,这就是所谓的“洞”。 
( 2 )对于使用 VESA 本地总线( VLB )的一些老设备 
    这是主要由图形卡使用的一条专用总线: I/O 空间被映射到从 0xe00000 到 0xffffff 的地址范围中,也就是 14MB 到 16MB 之间。因为这些设备使页表的初始化更加复杂,因此已经不生产这种设备。 
( 3 )对于连接到 PCI 总线的设备 
    I/O 空间被映射到很大的物理地址区间,位于 RAM 物理地址的顶端。这种设备的处理比较简单。 
2 .访问 I/O 空间 
    内核如何访问一个 I/O 空间单元? 让我们从 PC 体系结构开始入手,这个问题很容易就可以解决,之后我们再进一步讨论其他体系结构。 
    不要忘了内核程序作用于虚拟地址,因此 I/O 空间单元必须表示成大于 PAGE_OFFSET 的地址。在后面的讨论中,我们假设PAGE_OFFSET 等于 0xc0000000 ,也就是说,内核虚拟地址是在第 4G 。 
    内核驱动程序必须把 I/O 空间单元的物理地址转换成内核空间的虚拟地址。 在 PC 体系结构中,这可以简单地把 32 位的物理地址和0xc0000000 常量进行或运算得到。例如, 假设内核需要把物理地址为 0x000b0fe4 的 I/O 单元的值存放在 t1 中,把物理地址为 0xfc000000 的I/O 单元的值存放在 t2 中,就可以使用下面的表达式来完成这项功能: 

    t1 = *((unsigned char *)(0xc00b0fe4 ));
    t2 = *((unsigned char *)(0xfc000000));

    在第六章我们已经介绍过 , 在初始化阶段 , 内核已经把可用的 RAM 物理地址映射到虚拟地址空间第 4G 的最初部分。因此,分页机制把出现在第一个语句中的虚拟地址 0xc00b0fe4 映射回到原来的 I/O 物理地址 0x000b0fe4 ,这正好落在从 640K  1MB 的这段“ ISA 洞”中。这正是我们所期望的。 
    但是,对于第二个语句来说,这里有一个问题,因为其 I/O 物理地址超过了系统 RAM 的最大物理地址。 因此,虚拟地址 0xfc000000 就不需要与物理地址 0xfc000000 相对应。在这种情况下,为了在内核页表中包括对这个 I/O 物理地址进行映射的虚拟地址,必须对页表进行修改:这可以通过调用 ioremap( ) 函数来实现。 ioremap( ) 和 vmalloc( ) 函数类似,都调用 get_vm_area( ) 建立一个新的 vm_struct 描述符,其描述的虚拟地址区间为所请求 I/O 空间区的大小。然后, ioremap( ) 函数适当地更新所有进程的对应页表项。 
因此,第二个语句的正确形式应该为: 

    io_mem = ioremap(0xfb000000, 0x200000);
    t2 = *((unsigned char *)(io_mem + 0x100000));

    第一条语句建立一个 2MB 的虚拟地址区间,从 0xfb000000 开始;第二条语句读取地址 0xfc000000 的内存单元。驱动程序以后要取消这种映射,就必须使用 iounmap( ) 函数。 

    现在让我们考虑一下除 PC 之外的体系结构。在这种情况下,把 I/O 物理地址加上 0xc0000000 常量所得到的相应虚拟地址并不总是正确的。 为了提高内核的可移植性, Linux 特意包含了下面这些宏来访问 I/O 空间: 
    readb, readw, readl
    分别从一个 I/O 空间单元读取 1 、 2 或者 4 个字节 
    writeb, writew, writel
    分别向一个 I/O 空间单元写入 1 、 2 或者 4 个字节 
    memcpy_fromio, memcpy_toio
    把一个数据块从一个 I/O 空间单元拷贝到动态内存中,另一个函数正好相反,把一个数据块从动态内存中拷贝到一个 I/O 空间单元 
    memset_io
    用一个固定的值填充一个 I/O 空间区域 
对于 0xfc000000 I/O 单元的访问推荐使用这样的方法: 
    io_mem = ioremap(0xfb000000, 0x200000);
    t2 = readb(io_mem + 0x100000);
    使用这些宏,就可以隐藏不同平台访问 I/O 空间所用方法的差异。

 

 从本质上来说是一样的,IO端口在Linux驱动中是指IO端口的寄存器,通过操作寄存器来控制IO端口。而IO内存是指一些设备把IO寄存器映射到某个内存区域,因为访问内存就不要特殊的指令。

 

转载自:http://blogold.chinaunix.net/u3/94284/showart_2030412.html

你可能感兴趣的:(IO端口和IO内存)