( 1 )关于 IO 与内存空间: 函数
在 X86 处理器中存在着 I/O 空间的概念, I/O 空间是相对于内存空间而言的,它经过特定的指令 in 、 out 来访问。端口号标识了外设的寄存器地址。 Intel 语法的 in 、 out 指令格式为: spa
IN 累加器 ,{ 端口号 │DX} .net
OUT{ 端口号 │DX}, 累加器 设计
目前,大多数嵌入式微控制器如 ARM 、 PowerPC 等中并不提供 I/O 空间,而仅存在内存空间。内存空间能够直接经过地址、指针来访问,程序和程序运行中使用的变量和其余数据都存在于内存空间中。 指针
即使是在 X86 处理器中,虽然提供了 I/O 空间,若是由咱们本身设计电路板,外设仍然能够只挂接在内存空间。此时, CPU 能够像访问一个内存单元那样访问外设 I/O 端口,而不须要设立专门的 I/O 指令。所以,内存空间是必须的,而 I/O 空间是可选的。 blog
( 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 空间所用方法的差别。