我们知道默认外设 I/O 资源是不在 Linux 内核空间中的(如 sram 或硬件接口寄存器等),若需要访问该外设 I/O 资源,必须先将其地址映射到内核空间中来,然后才能在内核空间中访问它。
Linux 内核访问外设 I/O 内存资源的方式有两种:动态映射 (ioremap) 和静态映射 (map_desc) 。
一、动态映射 (ioremap) 方式
动态映射方式是大家使用了比较多的,也比较简单。即直接通过内核提供的 ioremap 函数动态创建一段外设 I/O 内存资源到内核虚拟地址的映射表,从而可以在内核空间中访问这段 I/O 资源。
Ioremap 宏定义在 asm/io.h 内:
#define ioremap(cookie,size) __ioremap(cookie,size,0)
__ioremap 函数原型为 (arm/mm/ioremap.c) :
void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);
phys_addr :要映射的起始的 IO 地址
size :要映射的空间的大小
flags :要映射的 IO 空间和权限有关的标志
该函数返回映射后的内核虚拟地址 (3G-4G). 接着便可以通过读写该返回的内核虚拟地址去访问之这段 I/O 内存资源。
举一个简单的例子 : ( 取自 s3c2410 的 iis 音频驱动 )
比如我们要访问 s3c2410 平台上的 I2S 寄存器 , 查看 datasheet 知道 IIS 物理地址为 0x55000000 ,我们把它定义为宏 S3C2410_PA_IIS ,如下:
#define S3C2410_PA_IIS (0x55000000)
若要在内核空间 (iis 驱动 ) 中访问这段 I/O 寄存器 (IIS) 资源需要先建立到内核地址空间的映射 :
our_card->regs = ioremap(S3C2410_PA_IIS, 0x100);
if (our_card->regs == NULL) {
err = -ENXIO;
goto exit_err;
}
创建好了之后,我们就可以通过 readl(our_card->regs ) 或 writel(value, our_card->regs) 等 IO 接口函数去访问它。
二、静态映射 (map_desc) 方式
下面重点介绍静态映射方式即通过 map_desc 结构体静态创建 I/O 资源映射表。
内核提供了在系统启动时通过 map_desc 结构体静态创建 I/O 资源到内核地址空间的线性映射表 ( 即 page table) 的方式,这种映射表是一种一一映射的关系。程序员可以自己定义该 I/O 内存资源映射后的虚拟地址。创建好了静态映射表,在内核或驱动中访问该 I/O 资源时则无需再进行 ioreamp 动态映射,可以直接通过映射后的 I/O 虚拟地址去访问它。
下面详细分析这种机制的原理并举例说明如何通过这种静态映射的方式访问外设 I/O 内存资源。
内核提供了一个重要的结构体 struct machine_desc , 这个结构体在内核移植中起到相当重要的作用 , 内核通过 machine_desc 结构体来控制系统体系架构相关部分的初始化。
machine_desc 结构体的成员包含了体系架构相关部分的几个最重要的初始化函数,包括 map_io, init_irq, init_machine 以及 phys_io , timer 成员等。
machine_desc 结构体定义如下:
|
这里的 map_io 成员即内核提供给用户的创建外设 I/O 资源到内核虚拟地址静态映射表的接口函数。 Map_io 成员函数会在系统初始化过程中被调用 , 流程如下:
Start_kernel -> setup_arch() --> paging_init() --> devicemaps_init() 中被调用
Machine_desc 结构体通过 MACHINE_START 宏来初始化。
注: MACHINE_START 的使用及各个成员函数的调用过程请参考 :
http://blog.chinaunix.net/u2/60011/showart_1010489.html
用户可以在定义 Machine_desc 结构体时指定 Map_io 的接口函数,这里以 s3c2410 平台为例。
s3c2410 machine_desc 结构体定义如下:
|
如上 ,map_io 被初始化为 smdk2410_map_io 。 smdk2410_map_io 即我们自己定义的创建静态 I/O 映射表的函数。在 Porting 内核到新开发板时,这个函数需要我们自己实现。
( 注:这个函数通常情况下可以实现得很简单,只要直接调用 iotable_init 创建映射表就行了,我们的板子内核就是。不过 s3c2410 平台这个函数实现得稍微有点复杂,主要是因为它将要创建 IO 映射表的资源分为了三个部分 (smdk2410_iodesc, s3c_iodesc 以及 s3c2410_iodesc) 在不同阶段分别创建。这里我们取其中一个部分进行分析,不影响对整个概念的理解。 )
S3c2410 平台的 smdk2410_map_io 函数最终会调用到 s3c2410_map_io 函数。
流程如下: s3c2410_map_io -> s3c24xx_init_io -> s3c2410_map_io
下面分析一下 s3c2410_map_io 函数:
|
iotable_init 内核提供,定义如下:
|
由上知道, s3c2410_map_io 最终调用 iotable_init 建立映射表。
iotable_init 函数的参数有两个:一个是 map_desc 类型的结构体,另一个是该结构体的数量 nr 。这里最关键的就是 struct map_desc 。 map_desc 结构体定义如下:
|
create_mapping 函数就是通过 map_desc 提供的信息创建线性映射表的。
这样的话我们就知道了创建 I/O 映射表的大致流程为:只要定义相应 I/O 资源的 map_desc 结构体,并将该结构体传给 iotable_init 函数执行,就可以创建相应的 I/O 资源到内核虚拟地址空间的映射表了。
我们来看看 s3c2410 是怎么定义 map_desc 结构体的 ( 即上面 s3c2410_map_io 函数内的 s3c2410_iodesc) 。
|
IODESC_ENT 宏如下:
#define IODESC_ENT(x) { (unsigned long)S3C24XX_VA_##x, __phys_to_pfn(S3C24XX_PA_##x), S3C24XX_SZ_##x, MT_DEVICE }
展开后等价于:
|
S3C24XX_PA_ LCD 和 S3C24XX_VA_ LCD 为定义在 map.h 内的 LCD 寄存器的物理地址和虚拟地址。在这里 map_desc 结构体的 virtual 成员被初始化为 S3C24XX_VA_ LCD , pfn 成员值通过 __phys_to_pfn 内核函数计算,只需要传递给它该 I/O 资源的物理地址就行。 Length 为映射资源的大小。 MT_DEVICE 为 I/O 类型,通常定义为 MT_DEVICE 。
这里最重要的即 virtual 成员的值 S3C24XX_VA_ LCD ,这个值即该 I/O 资源映射后的内核虚拟地址,创建映射表成功后,便可以在内核或驱动中直接通过该虚拟地址访问这个 I/O 资源。
S3C24XX_VA_ LCD 以及 S3C24XX_PA_ LCD 定义如下:
/* include/asm-arm/arch-s3c2410/map.h */
/* LCD controller */
#define S3C24XX_VA_LCD S3C2410_ADDR(0x00600000) //LCD 映射后的虚拟地址
#define S3C2410_PA_LCD (0x4D000000) //LCD 寄存器物理地址
#define S3C24XX_SZ_LCD SZ_1M //LCD 寄存器大小
S3C2410_ADDR 定义如下:
#define S3C2410_ADDR(x) ((void __iomem *)0xF0000000 + (x))
这里就是一种线性偏移关系,即 s3c2410 创建的 I/O 静态映射表会被映射到 0xF0000000 之后。 ( 这个线性偏移值可以改,也可以你自己在 virtual 成员里手动定义一个值,只要不和其他 IO 资源映射地址冲突 , 但最好是在 0XF0000000 之后。 )
( 注:其实这里 S3C2410_ADDR 的线性偏移只是 s3c2410 平台的一种做法,很多其他 ARM 平台采用了通用的 IO_ADDRESS 宏来计算物理地址到虚拟地址之前的偏移。
IO_ADDRESS 宏定义如下:
/* include/asm/arch-versatile/hardware.h */
/* macro to get at IO space when running virtually */
#define IO_ADDRESS(x) (((x) & 0x0fffffff) + (((x) >> 4) & 0x0f000000) + 0xf0000000) )
s3c2410_iodesc 这个映射表建立成功后,我们在内核中便可以直接通过 S3C24XX_VA_ LCD 访问 LCD 的寄存器资源。
如: S3c2410 lcd 驱动的 probe 函数内
|
S3C2410_LCDCON1 寄存器地址为相对于 S3C24XX_VA_LCD 偏移的一个地址,定义如下:
/* include/asm/arch-s3c2410/regs-lcd.h */
#define S3C2410_LCDREG(x) ((x) + S3C24XX_VA_LCD)
/* LCD control registers */
#define S3C2410_LCDCON1 S3C2410_LCDREG(0x00)
到此,我们知道了通过 map_desc 结构体创建 I/O 内存资源静态映射表的原理了。总结一下发现其实过程很简单,一通过定义 map_desc 结构体创建静态映射表,二在内核中通过创建映射后虚拟地址访问该 IO 资源。
三、 I/O 静态映射方式应用实例
I/O 静态映射方式通常是用在寄存器资源的映射上,这样在编写内核代码或驱动时就不需要再进行 ioremap ,直接使用映射后的内核虚拟地址访问。同样的 IO 资源只需要在内核初始化过程中映射一次,以后就可以一直使用。
寄存器资源映射的例子上面讲原理时已经介绍得很清楚了,这里我举一个 SRAM 的实例介绍如何应用这种 I/O 静态映射方式。当然原理和操作过程同寄存器资源是一样的,可以把 SRAM 看成是大号的 I/O 寄存器资源。
比如我的板子在 0x30000000 位置有一块 64KB 大小的 SRAM 。我们现在需要通过静态映射的方式去访问该 SRAM 。我们要做的事内容包括修改 kernel 代码,添加 SRAM 资源相应的 map_desc 结构,创建 sram 到内核地址空间的静态映射表。写一个 Sram Module, 在 Sram Module 内直接通过静态映射后的内核虚拟地址访问该 sram 。
第一步:创建 SRAM 静态映射表
在我板子的 map_des 结构体数组 (xxx_io_desc )内添加 SRAM 资源相应的 map_desc 。如下:
|
宏 XXX_SRAM_BASE 为我板子上 SRAM 的物理地址 , 定义为 0x30000000 。我的 kernel 是通过 IO_ADDRESS 的方式计算内核虚拟地址的,这点和之前介绍的 S3c2410 有点不一样。不过原理都是相同的,为一个线性偏移 , 范围在 0xF0000000 之后。
第二步:写个 SRAM Module, 在 Module 中通过映射后的虚拟地址直接访问该 SRAM 资源
SRAM Module 代码如下:
|
在开发板上运行结果如下:
/ # insmod bin/sram.ko
Request SRAM mem region ............
Hello,sram! ß 这句即打印的 SRAM 内的字符串
/ # rmmod sram
Release SRAM mem region success!
SRAM is close
实验发现可以通过映射后的地址正常访问 SRAM 。
最后,这里举 SRAM 作为例子的还有一个原因是通过静态映射方式访问 SRAM 的话,我们可以预先知道 SRAM 映射后的内核虚拟地址(通过 IOADDRESS 计算)。这样的话就可以尝试在 SRAM 上做点文章。比如写个内存分配的 MODULE 管理 SRAM 或者其他方式,将一些 critical 的数据放在 SRAM 内运行,这样可以提高一些复杂程序的运行效率 (SRAM 速度比 SDRAM 快多了 ) ,比如音视频的编解码过程中用到的较大的 buffer 等。