除了执行面向数据RAM的操作外,您还可以执行I/O内存事务来与硬件进行通信。当涉及到访问设备的寄存器时,内核根据系统架构提供了两种可能性:
- 通过I/O端口:这也称为端口输入输出(PIO)。寄存器可以通过专用总线访问,并且需要特定的指令(通常在汇编程序中)来访问这些寄存器。这是x86体系结构上的情况。
- 内存映射输入输出(MMIO):这是最常见和最常用的方法。设备的寄存器被映射到内存。简单地读和写一个特定的地址等价于写入设备的寄存器。这就是ARM架构上的情况。
访问PIO设备
在使用PIO的系统上,有两个不同的地址空间,一个用于内存,另一个用于I/O端口,称为端口地址空间,仅限制为65,536个端口。这是一种古老的方式,现在非常少见。
内核导出一些函数(符号)来处理I/O端口。在访问任何端口区域之前,我们必须首先通知内核,我们正在使用request_region()函数使用一系列端口,该函数将在出错时返回NULL。一旦处理完 region,就必须调用 release_region()。它们都是在 linux/iport .h 中声明的。他们的原型为:
struct resource *request_region(unsigned long start, unsigned long len, char *name); void release_region(unsigned long start, unsigned long len);
这些函数从一开始就通知内核你打算使用/释放一个 len 端口区域。name参数应该与你的设备名称一起设置。它们的使用不是强制性的。这是一种习惯,可以防止两个或多个驱动程序引用同一范围的端口。您可以通过读取/proc/ioports文件的内容来显示关于系统上实际使用的端口的信息。完成 region 申请后,可以使用以下函数访问端口:
1 u8 inb(unsigned long addr) 2 u16 inw(unsigned long addr) 3 u32 inl(unsigned long addr)
以上函数它们分别访问(读)8位、16位或32位(宽)端口。
1 void outb(u8 b, unsigned long addr) 2 void outw(u16 b, unsigned long addr) 3 void outl(u32 b, unsigned long addr)
以上函数它们将b数据(8、16或32位大小)写入addr端口。
PIO使用不同的指令集来访问I/O端口与MMIO相比是一个缺点,因为PIO需要比正常内存更多的指令来完成相同的任务。例如,1-bit 测试在MMIO中只有一条指令,而PIO需要在测试位之前将数据读入寄存器,这不止一条指令。
访问MMIO设备
内存映射的I/O存在于与内存相同的地址空间中。内核使用RAM(实际上是HIGH_MEM)通常使用的部分地址空间来映射设备寄存器,这样I/O设备就取代了该地址上的实际内存(即RAM)。因此,与I/O设备通信就像对专用于该I/O设备的内存地址进行读写一样。
换句话说,如果我需要访问分配给i.MX6的IPU-2的4MB内存映射空间(从0x02A00000到0x02DFFFFF), CPU(通过MMU)可能给我分配的虚拟地址范围是0x10000000到0x10400000。这并不消耗物理RAM(构建和存储页表项除外),而只是地址空间,这意味着内核将不再使用这个虚拟内存范围进行映射RAM。现在,这个地址范围内的任何写/读操作(例如0x10000004)都将重定向到IPU-2设备。
像PIO一样,也有MMIO函数来通知内核我们使用内存区域的意图。它们是 request_mem_region() and release_mem_region():
struct resource* request_mem_region(unsigned long start, unsigned long len, char *name); void release_mem_region(unsigned long start, unsigned long len);
我们可以通过读取/proc/iomem文件的内容来显示系统上实际使用的内存区域。
在访问一个内存区域之前(以及在成功请求它之后),必须通过调用特殊的依赖于体系结构的函数将该区域映射到内核地址空间 (它使用MMU来构建页表,因此不能从中断处理程序调用)。它们是ioremap()和iounmap(),它们也处理缓存一致性:
void __iomem *ioremap(unsigned long phys_add, unsigned long size); void iounmap(void __iomem *addr);
ioremap()返回一个__iomem void指针,指向映射区域的开始。不要试图解引用(通过读/写指针来获取/设置值)这种指针。内核提供访问 ioremap 内存的函数。如下所示:
unsigned int ioread8(void __iomem *addr); unsigned int ioread16(void __iomem *addr); unsigned int ioread32(void __iomem *addr); void iowrite8(u8 value, void __iomem *addr); void iowrite16(u16 value, void __iomem *addr); void iowrite32(u32 value, void __iomem *addr);
与vmalloc一样,ioremap构建新的页表。但是,它实际上并没有分配任何内存,而是返回一个特殊的虚拟地址,您可以使用它来访问指定的物理地址范围。
在32位系统上,MMIO窃取物理内存地址空间来为内存映射的I/O设备创建映射是一个缺点,因为它阻止系统将窃取的内存用于一般RAM用途。
__iomem cookie
__iomem是Sparse使用的内核cookie,它是内核用来查找可能的代码编写错误的语义检查器。为了利用Sparse提供的特性,应该在内核编译时启用它;如果不启用,__iomem cookie 将被忽略。
命令行中的C=1将为你启用Sparse,但是Sparse应该首先安装在你的系统上:
sudo apt-get install sparse
例如,当构建一个模块时,使用:
make -C $KPATH M=$PWD C=1 modules
下面展示了__iomem是如何在内核中定义的:
#define __iomem __attribute__((noderef, address_space(2)))
它保护我们不受执行I/O内存访问的错误驱动程序的影响。为所有I/O访问添加__iomem也是一种更严格的方法。因为即使是I/O访问也是通过虚拟内存完成的(在使用MMU的系统上),这个cookie阻止我们使用绝对物理地址,并要求我们使用ioremap(),它将返回一个带有__iomem cookie标记的虚拟地址:
void __iomem *ioremap(phys_addr_t offset, unsigned long size);
因此,我们可以使用专用函数,如 ioread32() 和 iowrite32()。你可能想知道为什么不使用 readl()/writel() 函数。不建议使用它们,因为它们不进行完整性检查,并且与只接受 __iomem 地址的 ioreadX()/iowriteX() 函数族相比安全性要差一些。
此外,noderef 是 Sparse 用来确保程序员不会解引用 __iomem 属性的指针。即使它可以在某些架构上工作,也不鼓励你这样做。请使用特殊的 ioreadX()/iowriteX() 函数代替。它是可移植的,适用于所有架构。现在,让我们看看当解引用__iomem指针时,Sparse是如何提示我们的:
#define BASE_ADDR 0x20E01F8 void * _addrTX = ioremap(BASE_ADDR, 8);
首先,因为错误的类型初始化器,报如下错误:
warning: incorrect type in initializer (different address spaces) expected void *_addrTX got void [noderef] <asn:2>*
或者使用如下:
u32 __iomem* _addrTX = ioremap(BASE_ADDR, 8); *_addrTX = 0xAABBCCDD; /* bad. No dereference */ pr_info("%x ", *_addrTX); /* bad. No dereference */
报如下错误:
Warning: dereference of noderef expression
下面是__iomem的正确用法:
void __iomem* _addrTX = ioremap(BASE_ADDR, 8); iowrite32(0xAABBCCDD, _addrTX); pr_info("%x ", ioread32(_addrTX));
必须记住两条规则:
- 在需要的地方总是使用__iomem,无论是作为返回类型还是作为参数类型,并使用Sparse确保你会这样做。
- 不要解引用 __iomem 指针,使用专用函数代替。