分类: LINUX
2013-05-18 20:16:41
2410下寄存器地址虚实映射的实现
我们知道在我们的驱动里面一般操作的寄存器的地址都是虚拟地址, 然而一般在cpu的datasheet里描述的寄存器的地址都是物理地址, 那linux内核是如何把我们驱动中指定操作的虚拟地址转换成正真可寻址并操作的物理地址的呢? 这篇文档以s3c2410为例, 将详细的描述这么一个实现流程。
S3c2410使用的是arm920T的核,它支持MMU,正因为这样它才可以进行虚拟地址到物理地址的转换。而诸如使用arm7核的CPU一般都无法进行类似的转换, 就在于它没有MMU,所以它上面能跑的操作系统也是去掉了MMU功能后的linux如uClinux, 关于MMU的原理可参考相关文档。
我们先来看文件Map.h
include/asm-arm/arch-s3c2410/Map.h:
#ifndef __ASSEMBLY__
#define S3C2410_ADDR(x) ((void __iomem __force *)0xF0000000 + (x))
#else
#define S3C2410_ADDR(x) (0xF0000000 + (x))
#endif
#define S3C2400_ADDR(x) S3C2410_ADDR(x)
/* interrupt controller is the first thing we put in, to make
* the assembly code for the irq detection easier
*/
#define S3C24XX_VA_IRQ S3C2410_ADDR(0x00000000)
#define S3C2400_PA_IRQ (0x14400000)
#define S3C2410_PA_IRQ (0x4A000000)
#define S3C24XX_SZ_IRQ SZ_1M
/* memory controller registers */
#define S3C24XX_VA_MEMCTRL S3C2410_ADDR(0x00100000)
#define S3C2400_PA_MEMCTRL (0x14000000)
#define S3C2410_PA_MEMCTRL (0x48000000)
#define S3C24XX_SZ_MEMCTRL SZ_1M
……
我们可以看到IRQ的寄存器虚拟地址定义为0xF0000000, 而物理地址为0x4A000000(这可从2410的datasheet上查到), memory控制器的寄存器虚拟地址定义为0xF0000000 + 0x00100000的地址处,物理地址为0x48000000, 其他如lcd 等寄存器都在这里定义了虚拟地址,当然这里仅仅是定义而已, 还没有和物理地址达成映射的联系。 我们接着看。
我们来看2410的machine_desc结构:
arch/arm/mach-s3c2410/Mach-smdk2410.c:
MACHINE_START(SMDK2410, "SMDK2410") /* @TODO: request a new identifier and switch
* to SMDK2410 */
/* Maintainer: Jonas Dietsche */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = smdk2410_map_io,
.init_irq = s3c24xx_init_irq,
.init_machine = smdk2410_init,
.timer = &s3c24xx_timer,
MACHINE_END
这里定义了一个描述2410开发板的结构, 其中的map_io, init_irq, init_machine都会在系统跑起来的时候被调用, 我们这里要看的是smdk2410_map_io, 这个函数完成后我们的虚拟地址和物理地址的映射关系就完成了。
arch/arm/mach-s3c2410/Mach-smdk2410.c:
static void __init smdk2410_map_io(void)
{
s3c24xx_init_io(smdk2410_iodesc, ARRAY_SIZE(smdk2410_iodesc)); //重点在这行
s3c24xx_init_clocks(0);
s3c24xx_init_uarts(smdk2410_uartcfgs, ARRAY_SIZE(smdk2410_uartcfgs));
}
该函数调用s3c24xx_init_io 来完成实质的东西。smdk2410_iodesc的定义如下:
arch/arm/mach-s3c2410/Mach-smdk2410.c:
static struct map_desc smdk2410_iodesc[] __initdata = {
/* nothing here yet */
};
这是一个保存虚拟地址和物理地址的映射关系表,内核通过这个参数的指导来完成映射关系。当然我们可以在这里添加我们需要映射的东东了。不过这里并没有条目, 很简单因为在后面还会有这样的映射表。
arch/arm/plat-s3c24xx/cpu.c:
void __init s3c24xx_init_io(struct map_desc *mach_desc, int size)
{
unsigned long idcode = 0x0;
/* initialise the io descriptors we need for initialisation */
iotable_init(s3c_iodesc, ARRAY_SIZE(s3c_iodesc)); //完成映射
//获取当前系统的CPU
if (cpu_architecture() >= CPU_ARCH_ARMv5) {
idcode = s3c24xx_read_idcode_v5();
} else {
idcode = s3c24xx_read_idcode_v4();
}
cpu = s3c_lookup_cpu(idcode);
if (cpu == NULL) {
printk(KERN_ERR "Unknown CPU type 0x%08lx/n", idcode);
panic("Unknown S3C24XX CPU");
}
printk("CPU %s (id 0x%08lx)/n", cpu->name, idcode);
if (cpu->map_io == NULL || cpu->init == NULL) {
printk(KERN_ERR "CPU %s support not enabled/n", cpu->name);
panic("Unsupported S3C24XX CPU");
}
(cpu->map_io)(mach_desc, size); //调用CPU相关的映射函数
}
完成映射关系的核心就在这个函数里了, 对iotable_init()的调用完成了s3c_iodesc 里的映射条目的映射。而这个函数也是最最核心的东东了。我们先来看要完成映射的条目有那些
arch/arm/plat-s3c24xx/cpu.c:
static struct map_desc s3c_iodesc[] __initdata = {
IODESC_ENT(GPIO), //GPIO寄存器虚实地址映射
IODESC_ENT(IRQ), //中断寄存器虚实地址映射
IODESC_ENT(MEMCTRL), /…
IODESC_ENT(UART) //….
};
这个就是要进行虚实地址映射的映射表了, 里面的每个条目都对应一个映射关系, 用map_desc来描述, 我们来看map_desc
include/asm-arm/mach/Map.h:
struct map_desc {
unsigned long virtual; //虚拟地址
unsigned long pfn; //对应的物理地址
unsigned long length; //映射长度
unsigned int type; //类型。
};
我们以IRQ的映射关系为例来看一下:
先看IODESC_ENT的定义:
#define IODESC_ENT(x) { (unsigned long)S3C24XX_VA_##x, __phys_to_pfn(S3C24XX_PA_##x), S3C24XX_SZ_##x, MT_DEVICE }
则 IODESC_ENT(IRQ) 就是:
{
(unsigned long)S3C24XX_VA_IRQ,
_phys_to_pfn(S3C24XX_PA_IRQ),
S3C24XX_SZ_IRQ,
MT_DEVICE
}
而S3C24XX_VA_IRQ 等都在include/asm-arm/arch-s3c2410/Map.h下定义过了。因此这个条目解释就是虚拟地址为0xF0000000开始长度为1M的虚拟地址空间由MMU转换成物理地址为0x4A000000开始的1M地址空间, 这正是IRQ寄存器物理地址所在位置。 条目定义好了, 剩下的就是要让系统MMU知道并在遇到这样一个虚拟地址时能正确映射到具体的物理地址。而这个工作就是由iotable_init完成的。
Arch/arm/mm/Mmu.c:
void __init iotable_init(struct map_desc *io_desc, int nr)
{
int i;
for (i = 0; i < nr; i++)
create_mapping(io_desc + i); //为每个条目创建一个映射表
}
我们接着看核心函数:create_mapping(), 它是如何让MMU部件知道映射关系的
void __init create_mapping(struct map_desc *md)
{
unsigned long phys, addr, length, end;
const struct mem_type *type;
pgd_t *pgd;
//参数检查, 是否可以进行映射
if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) {
printk(KERN_WARNING "BUG: not creating mapping for "
"0x%08llx at 0x%08lx in user region/n",
__pfn_to_phys((u64)md->pfn), md->virtual);
return;
}
if ((md->type == MT_DEVICE || md->type == MT_ROM) &&
md->virtual >= PAGE_OFFSET && md->virtual < VMALLOC_END) {
printk(KERN_WARNING "BUG: mapping for 0x%08llx at 0x%08lx "
"overlaps vmalloc space/n",
__pfn_to_phys((u64)md->pfn), md->virtual);
}
type = &mem_types[md->type]; //获取memory类型
/*
* Catch 36-bit addresses
*/
if (md->pfn >= 0x100000) {
create_36bit_mapping(md, type);
return;
}
addr = md->virtual & PAGE_MASK; //得到虚拟地址
phys = (unsigned long)__pfn_to_phys(md->pfn); //得到物理地址
length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK)); //映射长度
if (type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK)) {
printk(KERN_WARNING "BUG: map for 0x%08lx at 0x%08lx can not "
"be mapped using pages, ignoring./n",
__pfn_to_phys(md->pfn), addr);
return;
}
pgd = pgd_offset_k(addr);
end = addr + length;
do {
unsigned long next = pgd_addr_end(addr, end);
alloc_init_section(pgd, addr, next, phys, type);
phys += next - addr;
addr = next;
} while (pgd++, addr != end);
}
这个函数涉及到了相当多的MMU方面的知识, 一般系统中会有一个页表,页表中的每个条目都是一个映射关系,包括对应的物理地址,及该块物理地址区域访问的各种属性等, 而该页表的地址被保存在了MMU中的一个寄存器中。当我们在驱动中要访问某个寄存器时, 一般都使用的是虚拟地址, 这个虚拟地址被分成了两个区域, 高地址的若干位其实是个index, 用于指定需要页表中的哪个条目来映射该虚拟地址, 而剩下的若干位则是偏移地址, 即具体要操作的物理地址处。
当MMU需要进行虚拟地址到物理地址的映射时会先通过该寄存器找到这张页表,然后从虚拟地址的高若干位来得到一个具体的映射条目, 从这个条目我们可以知道该虚拟地址所在区间对应的物理地址区间, 然后在通过虚拟地址的低若干位来精确定位到具体的物理地址。
而这个函数实际上就是完成这个页表下面的相关映射条目项的填写工作, 以后当访问到这个虚拟地址时, 就会使用这里的条目进行虚实转换, 从而最终找到我们需要的物理地址并进行读写操作。