内存映射-IO空间-ioremap-iounremap

内存映射-IO空间-ioremap-iounremap

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 空间所用方法的差异。

你可能感兴趣的:(linux,IO映射)