前言:驱动程序和应用程序一样,在系统启动之后都是运行在虚拟地址之中,每一个进程单独的享用4G的地址空间,那么虚拟地址到底是怎么建立的呢,在使用它进行硬件驱动的操作前有必要对其进行了解,下面将对静态、动态虚拟地址的映射原理和使用方法做一个分析和介绍。
静态映射相关:
1.静态映射方法的特点:
(1)内核移植时以代码的形式硬编码,如果要更改必须改源代码后重新编译内核,内核移植完成之后映射方法一直存在;
(2)在内核启动时建立静态映射表,到内核关机时销毁,中间一直有效。
2.CPU的主映射表:
以三星为s5pv210移植的内核为例,其主映射表位于:arch/arm/plat-s5p/include/plat/map-s5p.h,此文件按照模块有序的将内部众多的寄存器宏定义为规定的虚拟地址。
#define S5P_VA_CHIPID S3C_ADDR(0x00700000)
#define S5P_VA_GPIO S3C_ADDR(0x00500000)
#define S5P_VA_SYSTIMER S3C_ADDR(0x01200000)
#define S5P_VA_SROMC S3C_ADDR(0x01100000)
#define S5P_VA_AUDSS S3C_ADDR(0X01600000)
#define S5P_VA_UART0 (S3C_VA_UART + 0x0)
#define S5P_VA_UART1 (S3C_VA_UART + 0x400)
#define S5P_VA_UART2 (S3C_VA_UART + 0x800)
#define S5P_VA_UART3 (S3C_VA_UART + 0xC00)
#define S3C_UART_OFFSET (0x400)
可以发现这些虚拟地址都是基地址+偏移地址构成的。
而基地址追踪得到为:0xFD000000
#define S3C_ADDR_BASE (0xFD000000)
#ifndef __ASSEMBLY__
#define S3C_ADDR(x) ((void __iomem __force *)S3C_ADDR_BASE + (x))
#else
#define S3C_ADDR(x) (S3C_ADDR_BASE + (x))
#endif
3.以gpio为例,继续分析:
(1)GPIO相关的主映射表位于:arch/arm/mach-s5pv210/include/mach/regs-gpio.h,表中是GPIO的各个端口的基地址的定义。
#define S5PV210_GPA0_BASE (S5P_VA_GPIO + 0x000)
#define S5PV210_GPA1_BASE (S5P_VA_GPIO + 0x020)
#define S5PV210_GPB_BASE (S5P_VA_GPIO + 0x040)
#define S5PV210_GPC0_BASE (S5P_VA_GPIO + 0x060)
#define S5PV210_GPC1_BASE (S5P_VA_GPIO + 0x080)
#define S5PV210_GPD0_BASE (S5P_VA_GPIO + 0x0A0)
...............
每一个gpio口的虚拟地址都是以这个地址+偏移地址得到,而这个基地址就是上一步中得到的S5P_VA_GPIO,也就是S5P_VA_GPIO=0XFD000000+0X005000000=0XFD500000。
(2)每个gpio口具体功能寄存器的定义位于:arch/arm/mach-s5pv210/include/mach/gpio-bank.h。同上同上基地址+偏移地址得到。这里的基地址换成了每一个gpio口的起始地址。
4.虚拟地址-->物理地址的映射
规定了虚拟地址,下面将虚拟地址和物理地址进行实际的映射。而映射的实现实际是通过/arch/arm/plat-s5p/cpu.c中一个结构体数组实现的:
static struct map_desc s5p_iodesc[] __initdata = {
{
.virtual = (unsigned long)S5P_VA_CHIPID,
.pfn = __phys_to_pfn(S5P_PA_CHIPID),
.length = SZ_4K,
.type = MT_DEVICE,
}, {
.virtual = (unsigned long)S3C_VA_SYS,
.pfn = __phys_to_pfn(S5P_PA_SYSCON),
.length = SZ_64K,
.type = MT_DEVICE,
}, {
.virtual = (unsigned long)S3C_VA_UART,
.pfn = __phys_to_pfn(S3C_PA_UART),
.length = SZ_4K,
.type = MT_DEVICE,
}, {
.virtual = (unsigned long)VA_VIC0,
.pfn = __phys_to_pfn(S5P_PA_VIC0),
.length = SZ_16K,
.type = MT_DEVICE,
}, {
.virtual = (unsigned long)VA_VIC1,
.pfn = __phys_to_pfn(S5P_PA_VIC1),
.length = SZ_16K,
.type = MT_DEVICE,
}, {
.virtual = (unsigned long)S3C_VA_TIMER,
.pfn = __phys_to_pfn(S5P_PA_TIMER),
.length = SZ_16K,
.type = MT_DEVICE,
}, {
.virtual = (unsigned long)S5P_VA_GPIO,
.pfn = __phys_to_pfn(S5P_PA_GPIO),
.length = SZ_4K,
.type = MT_DEVICE,
},
};
再次以gpio为例:
.virtual= (unsigned long)S5P_VA_GPIO 代表 将转换成的虚拟地址,也就是上面映射表中宏定义的地址;
.pfn= __phys_to_pfn(S5P_PA_GPIO) 代表实际的物理地址;
.length = SZ_4K 代表映射的内存长度;
.type = MT_DEVICE 代表内存的类型。
5.开机时进行映射表的建立
之前在分析内核的启动时有分析过start_kernel()函数中先后两次完成了虚拟地址的映射,的确,映射表的建立就是通过执行start_kernel()中多层包含的函数devicemaps_init()完成的。
start_kernel()
setup_arch(&command_line);
paging_init(mdesc);
devicemaps_init(mdesc);
/* Ask the machine support to map in the statically mapped devices.*/
if (mdesc->map_io)
mdesc->map_io();
至此开机后就可以使用虚拟地址。
6.静态虚拟地址的使用:直接操作映射表中宏对应的虚拟地址即可。
动态映射相关:
1.动态映射方法的特点:
(1)驱动程序根据需要随时动态的建立映射、使用、销毁映射;
(2)映射是短期临时的。
2.建立映射:
request_mem_region:先向内核申请需要申请的内存资源;
ioremap:再实现映射,传给它物理地址,会返回映射返回的虚拟地址。
2.销毁映射:
iounmap:先解除映射;
release_mem_region:再释放申请。
注:建立映射和销毁的步骤是倒影,顺序不能错。
总结:
(1)2种映射并不排他,可以同时使用(多个虚拟地址可以指向同一个物理地址);
(2)静态映射类似于C语言中全局变量,在系统运行期间一直有效,动态方式类似于C语言中malloc堆内存,使用前需要进行申请;
(3)静态映射的好处是执行效率高,坏处是始终占用虚拟地址空间;动态映射的好处是按需使用虚拟地址空间,坏处是每次使用前后都需要代码去建立映射&销毁映射(还得学会使用那些内核函数的使用)。
End。。。。。。