ARM板移植Linux系统启动(二)SPL

启动模式

面对一块完全的空板子,首先要做的当然是让它启动起来。emmc/nand中完全没有内容,意味着没有uboot,没有命令行,所有的硬件包括内存都没有初始化,除了CPU。在上电的CPU之上,还跑了一个小小的叫做BootROM的程序。
通常每块芯片,都在硬件中包含了一个BootROM,是一个小程序,上电后自动运行,无法修改,不同的厂家有不同的设计,但都是给定几种不同的启动模式,然后循环检测。TI这块板就支持各种模式,如emmc, nand flash, network, spi, uart等。
所谓的不同启动模式,本质上也是类似的,都是从某个设备中读取很少的一点数据放到一定的地方(一般是SRAM或者STACK),然后把PC指针交给它。所以并不是支持什么启动就可以直接在设备上放操作系统了,BootROM能做的只是很基础的硬件初始化工作,更多的事情是需要Uboot来完成。

你看到的是非授权版本!爬虫凶猛,请尊重知识产权!

转载请注明出处:http://conanwhf.github.io/2017/06/08/bootup-2-spl/

访问原文「ARM板移植Linux系统启动(二)SPL」获取最佳阅读体验并参与讨论


言归正传。对于我手头的这块板子,没有nand flash,没有外接emmc(TF卡),没有网口,板载emmc是空的,那么只能从UART启动了。TI对于Uart启动模式的设计,是将编译出来的u-boot-spl.bin(下文称为spl)使用xmodem通过串口传输过去,跑起来之后再开启ymodem接收模式,等待串口用ymodem传输u-boot.img(下文称为u-boot)完毕,将u-boot跑起来。

SRAM和DRAM

下载了代码,重新编译之后,却发现连spl都完全没有打印信息。经过跟踪代码添加打印,发现好像spl根本没有跑起来,这让人感到很奇怪。我手头的板子是改过内存芯片的,uboot跑不起来很正常,因为DRAM可能没有正确初始化。但SPL我认为应该是跑在SRAM里的,SRAM只要上电便可以使用,不应该没有任何反应。
阅读文档和Debug信息之后,发现竟然还有个配置选项,是先初始化DRAM,再让SPL从DRAM的固定地址启动。它们定义在编译uboot的config中:
configs/am335x_boneblack_defconfig

CONFIG_SPL_STACK_R_ADDR=0x82000000
CONFIG_SPL_STACK_R=y

跟踪代码,可以看到在ARM的初始化汇编程序中会调用这样的函数:
arch/arm/lib/crt0.S

# if !defined(CONFIG_SPL_BUILD) || defined(CONFIG_SPL_FRAMEWORK)
# ifdef CONFIG_SPL_BUILD
/* Use a DRAM stack for the rest of SPL, if requested */
bl      spl_relocate_stack_gd
cmp     r0, #0
movne   sp, r0
movne   r9, r0
# endif
ldr     r0, =__bss_start        /* this is auto-relocated! */

而函数spl_relocate_stack_gd则在common/spl/spl.c中实现。在config中将CONFIG_SPL_STACK_R=y删除,SPL则顺利地跑起来,有了打印信息。
这个部分当时我十分费解,因为SPL原本是不需要配置DRAM然后再跑的,但我看了一下TI和uboot的release,很多板子都用到了这样的配置,不明白为什么。解决这个问题后,我就将SPL放在SRAM里的设置保留了下去。后来继续做移植的时候,因为这个问题我掉了一个坑,才知道这样的设置是有原因的:
做到烧写bootloader的时候,发现我烧的bootloader跑不起来,总是出了一半打印信息就死了。经过回想,自己的bootloader除了不会影响的硬件设置改动,跟Beaglebone原生的release差别就在这个SRAM和DRAM的问题了,后来一试果然如此。大概是因为从eMMC启动的SPL需要初始化eMMC,有一些地方还是要用到DRAM,才这么设计的吧。

代码流程

SPL主要的内容分为三个部分:启动ARM的汇编,和uboot共享的设备初始化,以及SPL本身独有的代码。SPL自身的代码在common/spl/下,而每个板子特有的设备初始化代码在board/ti/am335x/下,通过查看scripts/Makefile.spl可以知道,用来引导启动的MLO文件实际上是基于u-boot-spl.bin做出来的,所以基本可以看作是同样的代码。整个SPL的作用可以说就是“初始化”,是各个层次的XXX_board_init()函数互相调用的过程,下面通过阅读代码把启动流程大致过一遍(代码部分提炼,删减不重要部分)。

arch/arm/cpu/armv7/start.S

这是上电后最开始的代码部分,对部分寄存器的状态进行设置,完成后调用_main

/* Set vector address in CP15 VBAR register */
ldr     r0, =_start
mcr     p15, 0, r0, c12, c0, 0  @Set VBAR
# endif

/* the mask ROM code should have PLL and others stable */
# ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl      cpu_init_cp15
# ifndef CONFIG_SKIP_LOWLEVEL_INIT_ONLY
bl      cpu_init_crit
# endif
# endif

bl      _main

arch/arm/lib/crt0.S

这段代码的开头注释写得很清楚,它是ARM启动很重要的一段过程,完成了代码段、基础硬件(RAM)、stack等的初始化和配置,最终将指针交给了board_init_r()

common/spl/spl.c

uboot中通用的SPL代码,基本的board_init_r()实现,首先初始化SPL需要的内存空间,然后通过spl_board_init()初始化板子的基础部分,接着按照board_boot_order中的设备依次尝试读取uboot数据,并启动uboot。

void board_init_r(gd_t *dummy1, ulong dummy2)
{
u32 spl_boot_list[]() = {
    BOOT_DEVICE_NONE,
    BOOT_DEVICE_NONE,
    BOOT_DEVICE_NONE,
    BOOT_DEVICE_NONE,
    BOOT_DEVICE_NONE,
};
struct spl_image_info spl_image;

debug(">>spl:board_init_r()\n");

if (!(gd->flags & GD_FLG_SPL_INIT)) {
    if (spl_init())
        hang();
    }

# ifdef CONFIG_SPL_BOARD_INIT
    spl_board_init();
# endif

memset(&spl_image, '\0', sizeof(spl_image));
board_boot_order(spl_boot_list);

if (boot_from_devices(&spl_image, spl_boot_list,
  ARRAY_SIZE(spl_boot_list))) {
    puts("SPL: failed to boot from all boot devices\n");
    hang();
}
switch (spl_image.os) {
    case IH_OS_U_BOOT:
        debug("Jumping to U-Boot\n");
    break;
    default:
        debug("Unsupported OS image.. Jumping nevertheless..\n");
}

debug("loaded - jumping to U-Boot...");
spl_board_prepare_for_boot();
jump_to_image_no_args(&spl_image);
}

arch/arm/mach-omap2/boot-common.c

这是OMAP芯片的通用代码,实现了spl_board_init()。首先保存中间状态,然后call preloader_consle_init()来初始化Uart和打印显示(位于common/spl/spl.c)。完成之后我们便能通过UART看到在SPL中的打印信息。
接下来是硬件相关部分:初始化SPL,I2C,MISC,WatchDog,最后call到am33xx的spl代码am33xx_spl_board_init()

void spl_board_init(void)
{
/*  * Save the boot parameters passed from romcode.
     * We cannot delay the saving further than this,
         * to prevent overwrites.
         */
        save_omap_boot_params();

        /* Prepare console output */
        preloader_console_init();

# if defined(CONFIG_SPL_NAND_SUPPORT) || defined(CONFIG_SPL_ONENAND_SUPPORT)
    gpmc_init();
# endif
# ifdef CONFIG_SPL_I2C_SUPPORT
    i2c_init(CONFIG_SYS_OMAP24_I2C_SPEED, CONFIG_SYS_OMAP24_I2C_SLAVE);
# endif
# if defined(CONFIG_AM33XX) && defined(CONFIG_SPL_MUSB_NEW_SUPPORT)
    arch_misc_init();
# endif
# if defined(CONFIG_HW_WATCHDOG)
    hw_watchdog_init();
# endif
# ifdef CONFIG_AM33XX
    am33xx_spl_board_init();
# endif
}

board/ti/am335x/board.c

特定板子的硬件实现,完成了am33x_spl_board_init()。这里根据board_is_XXX()函数确定不同的板子型号,继而进行不同的硬件初始化流程。要注意这个板子的型号是根据EEPROM中烧录的信息来确定的,如果硬件上有修改,需要手动进行一些特殊处理。我手头的板子就是没有这个EEPROM的,当时也给我造成了一点小麻烦。
这些硬件初始化都完成后,就可以一路跳回common/spl/spl.c, board_init_r(),跑后续的uboot启动流程。至此,SPL任务完成!