( 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 位宽)
unsignedinb(unsignedport);
voidoutb(unsignedcharbyte,unsignedport);
· 读写字端口( 16 位宽)
unsignedinw(unsignedport);
voidoutw(unsignedshortword,unsignedport);
· 读写长字端口( 32 位宽)
unsignedinl(unsignedport);
voidoutl(unsignedlongword,unsignedport);
· 读写一串字节
voidinsb(unsignedport,void*addr,unsignedlongcount);
voidoutsb(unsignedport,void*addr,unsignedlongcount);
·insb() 从端口 port 开始读 count 个字节端口,并将读取结果写入 addr 指向的内存; outsb() 将 addr 指向的内存的 count 个字节连续地写入 port 开始的端口。
· 读写一串字
voidinsw(unsignedport,void*addr,unsignedlongcount);
voidoutsw(unsignedport,void*addr,unsignedlongcount);
· 读写一串长字
voidinsl(unsignedport,void*addr,unsignedlongcount);
voidoutsl(unsignedport,void*addr,unsignedlongcount);
上述各函数中 I/O 端口号 port 的类型高度依赖于具体的硬件平台,因此,只是写出了 unsigned 。
( 3 ) readb 和 writeb:
在设备的物理地址被映射到虚拟地址之后,尽管可以直接通过指针访问这些地址,但是工程师宜使用 Linux 内核的如下一组函数来完成设备内存映射的虚拟地址的读写,这些函数包括:
· 读 I/O 内存
unsignedintioread8(void*addr);
unsignedintioread16(void*addr);
unsignedintioread32(void*addr);
与上述函数对应的较早版本的函数为(这些函数在 Linux2.6 中仍然被支持):
unsignedreadb(address);
unsignedreadw(address);
unsignedreadl(address);
· 写 I/O 内存
voidiowrite8(u8value,void*addr);
voidiowrite16(u16value,void*addr);
voidiowrite32(u32value,void*addr);
与上述函数对应的较早版本的函数为(这些函数在 Linux2.6 中仍然被支持):
voidwriteb(unsignedvalue,address);
voidwritew(unsignedvalue,address);
voidwritel(unsignedvalue,address);
( 4 )把 I/O 端口映射到 “ 内存空间 ”:
void*ioport_map(unsignedlongport,unsignedintcount);
通过这个函数,可以把 port 开始的 count 个连续的 I/O 端口重映射为一段 “ 内存空间 ” 。然后就可以在其返回的地址上像访问 I/O 内存一样访问这些 I/O 端口。当不再需要这种映射时,需要调用下面的函数来撤消:
voidioport_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=*((unsignedchar*)(0xc00b0fe4));
t2=*((unsignedchar*)(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=*((unsignedchar*)(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 空间区域
对于 0xfc000000I/O 单元的访问推荐使用这样的方法:
io_mem=ioremap(0xfb000000,0x200000);
t2=readb(io_mem+0x100000);
使用这些宏,就可以隐藏不同平台访问 I/O 空间所用方法的差异。