I/O资源读写接口 writel/readl/memcpy_toio

内核里面writel(readl)是如何实现的

writel和readl,这两个个函数实现在操作系统层,有内存保护的情况下,往一个寄存器或者内存地址写一个数据。先说一下writel:

在arch/alpha/kernel/io.c中有

188 void writel(u32 b, volatile void __iomem *addr)
189 {
190     __raw_writel(b, addr);
191     mb();
192 }
  • 1
  • 2
  • 3
  • 4
  • 5

这样一个writel函数的作用应该是向一个地址上写一个值,我想知道这个函数底下具体实现的细节,于是往下继续跟踪代码:__raw_writel(b, addr);(发现在同目录下)

  129 void __raw_writel(u32 b, volatile void __iomem *addr)
    130 {
    131     IO_CONCAT(__IO_PREFIX,writel)(b, addr);
    132 }
  • 1
  • 2
  • 3
  • 4

再往下跟踪 IO_CONCAT,在对应的io.h中的定义如下:

134 #define IO_CONCAT(a,b)  _IO_CONCAT(a,b)
135 #define _IO_CONCAT(a,b) a ## _ ## b
  • 1
  • 2

这段代码前几天问过了,是标示将两边的字符串连接起来的意思。

跟踪__IO_PREFIX 定义如下

  501 #undef __IO_PREFIX
    502 #define __IO_PREFIX     apecs
  • 1
  • 2

继续阅读代码,看看定义__IO_PREFIX之后紧接着包含了哪个头文件。在哪个头文
件里面寻找答案。对于你的apsec,看看以下代码段(linux-2.6.28-rc4)

arch/alpha/include/asm/core_apecs.h

#undef __IO_PREFIX
#define __IO_PREFIX             apecs
#define apecs_trivial_io_bw     0
#define apecs_trivial_io_lq     0
#define apecs_trivial_rw_bw     2
#define apecs_trivial_rw_lq     1
#define apecs_trivial_iounmap   1
#include 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

前往arch/alpha/include/asm/io_trivial.h

__EXTERN_INLINE void
IO_CONCAT(__IO_PREFIX,writel)(u32 b, volatile void __iomem *a)
{
       *(volatile u32 __force *)a = b;
} 
  • 1
  • 2
  • 3
  • 4
  • 5

就是最终通过*(volatile u32 __force *)a = b;
来写入数据的。

同样的readl读取数据也和writel类似,这里就不重复了。

(如果在没有os,没有mmu的情况下,当开发板裸跑的时候,我们只需要一句话就一切ok:

*(unsigned long *)addr = value

#ifdef SLOW_IO_BY_JUMPING

#define __SLOW_DOWN_IO "

jmp 1f

1: jmp 1f

1:"

#else

#define __SLOW_DOWN_IO "

outb %%al,$0x80"

#endif

#ifdef REALLY_SLOW_IO

#define __FULL_SLOW_DOWN_IO __SLOW_DOWN_IO

__SLOW_DOWN_IO __SLOW_DOWN_IO __SLOW_DOWN_IO

#else

#define __FULL_SLOW_DOWN_IO __SLOW_DOWN_IO

#endif

显然,__FULL_SLOW_DOWN_IO就是一个或四个__SLOW_DOWN_IO(根据是否定义了宏REALLY_SLOW_IO来决定),而宏__SLOW_DOWN_IO则被定义成毫无意义的跳转语句或写端口0x80的操作(根据是否定义了宏SLOW_IO_BY_JUMPING来决定)。

3.6 访问I/O内存资源

尽管I/O端口空间曾一度在x86平台上被广泛使用,但是由于它非常小,因此大多数现代总线的设备都以内存映射方式(Memory-mapped)来映射它的I/O端口(指I/O寄存器)和外设内存。基于内存映射方式的I/O端口(指I/O寄存器)和外设内存可以通称为“I/O内存”资源(I/O Memory)。因为这两者在硬件实现上的差异对于软件来说是完全透明的,所以驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是“I/O内存”资源。

从前几节的阐述我们知道,I/O内存资源是在CPU的单一内存物理地址空间内进行编址的,也即它和系统RAM同处在一个物理地址空间内。因此通过CPU的访内指令就可以访问I/O内存资源。

一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,这可以通过系统固件(如BIOS)在启动时分配得到,或者通过设备的硬连线(hardwired)得到。比如,PCI卡的I/O内存资源的物理地址就是在系统启动时由PCI BIOS分配并写到PCI卡的配置空间中的BAR中的。而ISA卡的I/O内存资源的物理地址则是通过设备硬连线映射到640KB-1MB范围之内的。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,因为它们是在系统启动后才已知的(某种意义上讲是动态的),所以驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。

3.6.1 映射I/O内存资源

Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中,如下:

void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);

void iounmap(void * addr);

函数用于取消ioremap()所做的映射,参数addr是指向核心虚地址的指针。这两个函数都是实现在mm/ioremap.c文件中。具体实现可参考《情景分析》一书。

3.6.2 读写I/O内存资源

在将I/O内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。但是,由于在某些平台上,对I/O内存和系统内存有不同的访问处理,因此为了确保跨平台的兼容性,Linux实现了一系列读写I/O内存资源的函数,这些函数在不同的平台上有不同的实现。但在x86平台上,读写I/O内存与读写RAM无任何差别。如下所示(include/asm-i386/io.h):

#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))

上述定义中的宏__io_virt()仅仅检查虚地址addr是否是核心空间中的虚地址。该宏在内核2.4.0中的实现是临时性的。具体的实现函数在arch/i386/lib/Iodebug.c文件。

显然,在x86平台上访问I/O内存资源与访问系统主存RAM是无差别的。但是为了保证驱动程序的跨平台的可移植性,我们应该使用上面的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。

你可能感兴趣的:(Linux内核-驱动开发,IO,Linux,读写)