1. 程式人生 > >嵌入式Linux移植之記憶體初始化和地址對映

嵌入式Linux移植之記憶體初始化和地址對映

Linux核心有兩個重要的巨集:PHYS_OFFSET和PAGE_OFFSET。PHYS_OFFSET是實體記憶體的起始地址,PAGE_OFFSET是Linux核心空間的虛擬起始地址(預設為0xC0000000,可通過menuconfig配置,CONFIG_PAGE_OFFSET)。PHYS_OFFSET可通過menuconfig配置(CONFIG_PHYS_OFFSET),但一般不直接配置。如果定義了CONFIG_ARM_PATCH_PHYS_VIRT,則核心會根據實際所在的地址調整PHYS_OFFSET的值。

核心映象生成的時候需要PHYS_OFFSET和PAGE_OFFSET對應的實體地址,在Makefile.boot中指定。對於SMDK2440開發板,則為arch/arm/mach-s3c24xx/Makefile.boot,如下,zreladdr-y即為__pa(PAGE_OFFSET+TEXT_OFFSET)(TEXT_OFSSET在makefile中寫死了為0x8000)。params_phys-y即為PHYS_OFFSET

ifeq ($(CONFIG_PM_H1940),y)
	zreladdr-y	+= 0x30108000
	params_phys-y	:= 0x30100100
else
	zreladdr-y	+= 0x30008000
	params_phys-y	:= 0x30000100
endif
Linux核心中記憶體初始化主要是初始化記憶體的地址範圍和佈局,以及建立頁表(MMU),不會再初始化記憶體控制器。記憶體控制器在u-boot中已經初始化好並且已經把程式碼拷貝到記憶體執行,初始化記憶體控制器也可能導致記憶體中的資料異常。uboot中可以通過兩種方式傳遞記憶體資訊:在啟動引數中使用mem=xxx或者使用atag_mem。

        對於mem=xxx引數,在early_param中初始化memblock,這裡實際上添加了一個起始地址為PHYS_OFFSET,大小為mem=指定的記憶體bank。

static int __init early_mem(char *p)
{
	static int usermem __initdata = 0;
	u64 size;
	u64 start;
	char *endp;

	/*
	 * If the user specifies memory size, we
	 * blow away any automatically generated
	 * size.
	 */
	if (usermem == 0) {
		usermem = 1;
		memblock_remove(memblock_start_of_DRAM(),
			memblock_end_of_DRAM() - memblock_start_of_DRAM());
	}

	start = PHYS_OFFSET;
	size  = memparse(p, &endp);
	if (*endp == '@')
		start = memparse(endp + 1, NULL);

	arm_add_memory(start, size);

	return 0;
}
對於ATAG_MEM,則在parse_tag_mem32新增一個記憶體bank。
static int __init parse_tag_mem32(const struct tag *tag)
{
	return arm_add_memory(tag->u.mem.start, tag->u.mem.size);
}

記憶體佈局初始化好後就會根據記憶體資訊進行地址對映,通過paging_init重新初始化頁表。

	parse_early_param();

	early_paging_init(mdesc, lookup_processor_type(read_cpuid_id()));
	setup_dma_zone(mdesc);
	sanity_check_meminfo();
	arm_memblock_init(mdesc);

	paging_init(mdesc);

mem=xxx比atag_mem優先順序較高,會覆蓋atag_mem的設定。

核心在setup_arch函式中進行上面的初始化,start_kernel函式執行完setup_arch後,接著初始化zone和頁分配器(zone實際上管理的是實體記憶體)。

	setup_arch(&command_line);
	mm_init_cpumask(&init_mm);
	setup_command_line(command_line);
        .............

	build_all_zonelists(NULL, NULL);
	page_alloc_init();

        ......
	mm_init();
static void __init mm_init(void)
{
	/*
	 * page_cgroup requires contiguous pages,
	 * bigger than MAX_ORDER unless SPARSEMEM.
	 */
	page_cgroup_init_flatmem();
	mem_init();
	kmem_cache_init();
	percpu_init_late();
	pgtable_init();
	vmalloc_init();
}

在mem_init中列印memory layout,通過這裡的資料可以找到修改這些layout的方法。

	printk(KERN_NOTICE "Virtual kernel memory layout:\n"
			"    vector  : 0x%08lx - 0x%08lx   (%4ld kB)\n"
#ifdef CONFIG_HAVE_TCM
			"    DTCM    : 0x%08lx - 0x%08lx   (%4ld kB)\n"
			"    ITCM    : 0x%08lx - 0x%08lx   (%4ld kB)\n"
#endif
			"    fixmap  : 0x%08lx - 0x%08lx   (%4ld kB)\n"
			"    vmalloc : 0x%08lx - 0x%08lx   (%4ld MB)\n"
			"    lowmem  : 0x%08lx - 0x%08lx   (%4ld MB)\n"
#ifdef CONFIG_HIGHMEM
			"    pkmap   : 0x%08lx - 0x%08lx   (%4ld MB)\n"
#endif
#ifdef CONFIG_MODULES
			"    modules : 0x%08lx - 0x%08lx   (%4ld MB)\n"
#endif
			"      .text : 0x%p" " - 0x%p" "   (%4td kB)\n"
			"      .init : 0x%p" " - 0x%p" "   (%4td kB)\n"
			"      .data : 0x%p" " - 0x%p" "   (%4td kB)\n"
			"       .bss : 0x%p" " - 0x%p" "   (%4td kB)\n",

			MLK(UL(CONFIG_VECTORS_BASE), UL(CONFIG_VECTORS_BASE) +
				(PAGE_SIZE)),
#ifdef CONFIG_HAVE_TCM
			MLK(DTCM_OFFSET, (unsigned long) dtcm_end),
			MLK(ITCM_OFFSET, (unsigned long) itcm_end),
#endif
			MLK(FIXADDR_START, FIXADDR_TOP),
			MLM(VMALLOC_START, VMALLOC_END),
			MLM(PAGE_OFFSET, (unsigned long)high_memory),
#ifdef CONFIG_HIGHMEM
			MLM(PKMAP_BASE, (PKMAP_BASE) + (LAST_PKMAP) *
				(PAGE_SIZE)),
#endif
#ifdef CONFIG_MODULES
			MLM(MODULES_VADDR, MODULES_END),
#endif

			MLK_ROUNDUP(_text, _etext),
			MLK_ROUNDUP(__init_begin, __init_end),
			MLK_ROUNDUP(_sdata, _edata),
			MLK_ROUNDUP(__bss_start, __bss_stop));
<span style="font-family: Arial, Helvetica, sans-serif;">
</span>
<span style="white-space: pre;">	</span>ARM晶片主要使用了Linux的兩種地址對映方式,I/O靜態對映(通過iotable_init函式)和ioremap動態對映。我的GT2440開發板啟動後核心的地址空間如下:

                  

在uboot啟動引數中,指定了mem=62M。從圖中可以發現,mem=62M指定了低端地址空間的大小,事實上是因為實體記憶體較小,沒有超過vmalloc_limit。mem=xxx不能達到實際實體記憶體的大小(我的開發板記憶體為64MB),否則系統執行一些耗記憶體的應用程式可能會段錯誤,本人曾經用mjpg-streamer驅動pl2303攝像頭的時候遇到過。一般至少需要預留2MB的空間,有人說是為DMA保留的,我目前不清楚原因。

        iotable_init和ioremap函式都是把實體地址對映到vmalloc區。先看看iotable_init,其一般在machine初始化的map_io函式中初始化。

        smdk2440_map_io函式中呼叫了s3c24xx_init_io

void __init s3c24xx_init_io(struct map_desc *mach_desc, int size)
{
	arm_pm_idle = s3c24xx_default_idle;

	/* initialise the io descriptors we need for initialisation */
	iotable_init(mach_desc, size);
	iotable_init(s3c_iodesc, ARRAY_SIZE(s3c_iodesc));

	if (cpu_architecture() >= CPU_ARCH_ARMv5) {
		samsung_cpu_id = s3c24xx_read_idcode_v5();
	} else {
		samsung_cpu_id = s3c24xx_read_idcode_v4();
	}

	s3c_init_cpu(samsung_cpu_id, cpu_ids, ARRAY_SIZE(cpu_ids));

	samsung_pwm_set_platdata(&s3c24xx_pwm_variant);
}
這裡映射了GPIO、IRQ、MEMCTRL、UART暫存器,即把S3C24XX_PA_XX對映到S3C24XX_VA_XX。
/* minimal IO mapping */
#define IODESC_ENT(x) { (unsigned long)S3C24XX_VA_##x, __phys_to_pfn(S3C24XX_PA_##x), S3C24XX_SZ_##x, MT_DEVICE }
static struct map_desc s3c_iodesc[] __initdata = {
	IODESC_ENT(GPIO),
	IODESC_ENT(IRQ),
	IODESC_ENT(MEMCTRL),
	IODESC_ENT(UART)
};
#define S3C_ADDR_BASE	0xF6000000
#ifndef __ASSEMBLY__
#define S3C_ADDR(x)	((void __iomem __force *)S3C_ADDR_BASE + (x))
#else
#define S3C_ADDR(x)	(S3C_ADDR_BASE + (x))
#endif
#define S3C_VA_IRQ	S3C_ADDR(0x00000000)	/* irq controller(s) */
#define S3C_VA_SYS	S3C_ADDR(0x00100000)	/* system control */
#define S3C_VA_MEM	S3C_ADDR(0x00200000)	/* memory control */
#define S3C_VA_TIMER	S3C_ADDR(0x00300000)	/* timer block */
#define S3C_VA_WATCHDOG	S3C_ADDR(0x00400000)	/* watchdog */
#define S3C_VA_UART	S3C_ADDR(0x01000000)	/* UART */
#define S3C24XX_VA_IRQ		S3C_VA_IRQ
#define S3C24XX_VA_MEMCTRL	S3C_VA_MEM
#define S3C24XX_VA_UART		S3C_VA_UART

#define S3C24XX_VA_TIMER	S3C_VA_TIMER
#define S3C24XX_VA_CLKPWR	S3C_VA_SYS
#define S3C24XX_VA_WATCHDOG	S3C_VA_WATCHDOG
我嘗試發現這些地址對映的規律,但發現無明顯的規律,基本是對映到“一個段"。事實上也是任意的,但其地址都是在vmalloc地址區域的末端。
在cpu->map_io中呼叫了s3c244x_map_io,這裡映射了CLKPWR等暫存器。
void __init s3c244x_map_io(void)
{
	/* register our io-tables */

	iotable_init(s3c244x_iodesc, ARRAY_SIZE(s3c244x_iodesc));
static struct map_desc s3c244x_iodesc[] __initdata = {
	IODESC_ENT(CLKPWR),
	IODESC_ENT(TIMER),
	IODESC_ENT(WATCHDOG),
};
iotable_init一般只對映基礎的元件,需要在核心啟動時候的先初始化的暫存器,其餘的暫存器在驅動中通過ioremap進行對映。

        以I2C驅動為例,在machine初始化中添加了一個I2C platform_deveice

static struct platform_device *smdk2440_devices[] __initdata = {
	&s3c_device_ohci,
	&s3c_device_lcd,
	&s3c_device_wdt,
	&s3c_device_i2c0,
	&s3c_device_iis,
};
       在其相應的driver i2c-s3c2410.c中s3c24xx_i2c_probe
	/* map the registers */

	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	i2c->regs = devm_ioremap_resource(&pdev->dev, res);

	if (IS_ERR(i2c->regs))
		return PTR_ERR(i2c->regs);

	dev_dbg(&pdev->dev, "registers %p (%p)\n",
		i2c->regs, res);
       這裡獲取I2C的實體地址範圍,通過ioremap進行了對映。

       地址對映之後便可以進行訪問了,但一般不直接訪問,而是通過include/asm-generic/io.h中定義的writel、readl等相關介面進行訪問,這樣會有更好的移植性。