在驱动程序编写过程中,很少会注意到 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