ARM板移植Linux系统启动(三)UBOOT移植

经过这些年的演变,U-boot已经从一个简单的loader慢慢发展成了一个小小系统,硬件上原生支持各种平台、外设,软件上支持Fat&ext234等文件系统、传输协议、测试工具,可以说相当完善了。这种进化,负面影响是让整体变得更复杂臃肿,但也有更多的正面影响,是让我们的移植工作更加简单、模块化。

Uboot代码结构

代码结构是了解uboot的基础,作为一个成熟的开源软件,uboot的代码结构很清晰。绝大多数的代码都是通用的,所以在移植过程中,基本上只需要找到自己的对应平台,修改板子特有的部分即可。

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

转载请注明出处:http://conanwhf.github.io/2017/06/09/bootup-3-uboot/

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


├── api uboot提供的API接口
├── arch 与体系结构相关的代码
│ ├── arm
│ │ ├── mach-omap2 BeagleBoneBlack使用的架构
│ ├── x86
├── board 根据不同的具体开发板而定制的代码
│ ├── ti 厂商:TI
│ │ ├── am335x BeagleBoneBlack使用的平台,主要的改动应该在这里,包括内存初始化
│ │ ├── beagle
│ │ ├── common
│ │ ├── evm
├── cmd 通用命令行处理工具
├── common 通用核心代码,主要给uboot使用,部分SPL
│ ├── spl SPL的通用代码
├── configs 配置文件
├── disk 磁盘分区相关
├── doc 文档
├── drivers 各种驱动,uboot使用
├── dts dtb文件的编译脚本
├── examples 范例
├── fs 文件系统代码
├── include 公用头文件
│ ├── configs 不同板子配置选项的头文件
├── lib 通用库
├── net 网络相关
├── post Power On Self Test,开机自检程序
├── scripts 编译脚本
├── test 测试程序
├── tools 相关小工具
如果是想要重新编译和优化uboot,那么通常修改配置文件即可;如果是一块硬件上有修改的板子,那么则需要另外关注board/XXX/部分,基于原有的初始化代码进行修改;如果是完全新设计的芯片,则必须根据芯片不同模块的datasheet自己添加一套初始化代码,只有arm平台的一些基本内容可以通用了。

启动流程

前一篇讲SPL的启动的时候提到,最初从start.S call到了crt0.Scrt0.S会先后调用board_init_f()board_init_r()两个函数,当作为SPL编译时board_init_f()是直接返回的,而在uboot时是有用的。大致上来说,board_init_f()侧重于基础硬件的初始化,以及软件堆栈等方面的准备,更底层一点;而board_init_r()则更多的是软件方面的初始化,并且最终完成uboot启动过程或进入命令行。

common/board_f.c

这是uboot(实际上是uboot第二阶段,u-boot.img所包含的内容,下文不再区分)的入口函数board_init_f()所在的文件。可以看到函数board_init_f()很简单,初始化全局变量后,就根据一个init_sequence_f依次运行不同的函数,失败则把系统hang住,成功当然就是进入正常的启动流程中去了。init_sequence_f在同一个文件中也有定义,很长,我删减无关平台的内容大致看看:

static init_fnc_t init_sequence_f[] = {
//==========基本数据init,malloc数据块
    setup_mon_len,
#ifdef CONFIG_OF_CONTROL
    fdtdec_setup,
#endif
#ifdef CONFIG_TRACE
    trace_early_init,
#endif
    initf_malloc,
    initf_console_record,
//初始化CPU
    arch_cpu_init,      /* basic arch cpu dependent setup */
    mach_cpu_init,      /* SoC/machine dependent CPU setup */
    initf_dm,
    arch_cpu_init_dm,
    mark_bootstage,     /* need timer, go after init dm */
#if defined(CONFIG_BOARD_EARLY_INIT_F)
    board_early_init_f,
#endif
//==========Timer
#if defined(CONFIG_ARM) || defined(CONFIG_MIPS) || \
        defined(CONFIG_BLACKFIN) || defined(CONFIG_NDS32) || \
        defined(CONFIG_SH) || defined(CONFIG_SPARC)
    timer_init,     /* initialize timer */
#endif
//==========初始化环境变量
    env_init,       /* initialize environment */
//==========初始化串口,并打印信息
    init_baud_rate,     /* initialze baudrate settings */
    serial_init,        /* serial communications setup */
    console_init_f,     /* stage 1 init of console */
    display_options,    /* say that we are here */
    display_text_info,  /* show debugging info if required */
    print_cpuinfo,      /* display cpu info (and speed) */
#if defined(CONFIG_DISPLAY_BOARDINFO)
    show_board_info,
#endif
//==========基本外设初始化,包括I2C,SPI,WatchDog等
    INIT_FUNC_WATCHDOG_INIT
#if defined(CONFIG_MISC_INIT_F)
    misc_init_f,
#endif
    INIT_FUNC_WATCHDOG_RESET
#if defined(CONFIG_HARD_I2C) || defined(CONFIG_SYS_I2C)
    init_func_i2c,
#endif
#if defined(CONFIG_HARD_SPI)
    init_func_spi,
#endif
//==========内存初始化
    announce_dram_init,
    /* TODO: unify all these dram functions? */
#if defined(CONFIG_ARM) || defined(CONFIG_X86) || defined(CONFIG_NDS32) || \
        defined(CONFIG_MICROBLAZE) || defined(CONFIG_AVR32) || \
        defined(CONFIG_SH)
    dram_init,      /* configure available RAM banks */
#endif
    INIT_FUNC_WATCHDOG_RESET
#if defined(CONFIG_SYS_DRAM_TEST)
    testdram,
#endif /* CONFIG_SYS_DRAM_TEST */
    INIT_FUNC_WATCHDOG_RESET
//==========一堆显示相关的初始化,略
    /*
     * Now that we have DRAM mapped and working, we can
     * relocate the code and continue running from DRAM.
     *
     * Reserve memory at end of RAM for (top down in that order):
     *  - area that won't get touched by U-Boot and Linux (optional)
     *  - kernel log buffer
     *  - protected RAM
     *  - LCD framebuffer
     *  - monitor code
     *  - board info struct
     */
//==========将环境变量重置,并打印相关信息
    setup_machine,
    reserve_global_data,
    reserve_fdt,
    reserve_arch,
    reserve_stacks,
    setup_dram_config,
    show_dram_config,
    display_new_sp,
#ifdef CONFIG_SYS_EXTBDINFO
    setup_board_extra,
#endif
    INIT_FUNC_WATCHDOG_RESET
    reloc_fdt,
    setup_reloc,
    NULL,
};

common/board_r.c

类似的,在board_init_r()中也会按顺序调用这么一个函数列表,函数名都很容易看懂,具体就不列出来了。跟board_init_f()不同的是,它最后有一个run_main_loop

init_fnc_t init_sequence_r[] = {
    initr_trace,
    initr_reloc,
    ......
    run_main_loop,
};

这个函数会进入一个死循环,尝试自动启动系统或者进入uboot的命令行:

static int run_main_loop(void)
{
ifdef CONFIG_SANDBOX
sandbox_main_loop_init();
endif
/* main_loop() can return to retry autoboot, if so just run it again */
for (;;)
main_loop();
return 0;
} 

common/main.c

函数main_loop()的实现在main.c,可以看到,做好最后的准备工作后,uboot会尝试用autoboot_command(s)来启动kernel,如果失败,则进入cli_loop()函数中。

void main_loop(void)
{
        const char *s;

        bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");

#ifdef CONFIG_VERSION_VARIABLE
        setenv("ver", version_string);  /* set version variable */
#endif /* CONFIG_VERSION_VARIABLE */

        cli_init();

        run_preboot_environment_command();

#if defined(CONFIG_UPDATE_TFTP)
        update_tftp(0UL, NULL, NULL);
#endif /* CONFIG_UPDATE_TFTP */

        s = bootdelay_process();
        if (cli_process_fdt(&s))
                cli_secure_boot_cmd(s);

        autoboot_command(s);

        cli_loop();
        panic("No CLI available");
}

cli_loop()

这个函数比较简单,追踪code可以看到,common/cli.c, cli_loop()->common/cli_simple.c, cli_simple_loop()cli_simple_loop()就是uboot的命令行了,不断读取用户输入来做出反应,同样也是个死循环。所以绝大部分的时候,main_loop()中的循环并不会进行多次,而是一旦自动启动失败就进入了命令行,不会再次自动尝试启动。

修改移植

一般简单的板子移植,主要是内存初始化代码,和硬件配置选项,以及启动变量。硬件的驱动是不需要自己写的,但要配置准确。这些修改主要集中在两个地方:配置头文件和平台对应的board.c。以BeagleBoneBlack为例,它的特别内容都在以下两个文件中:

  • board/ti/am335x/board.c
  • include/configs/am335x_evm.h
    这是编译时脚本根据config中CONFIG_TARGET_AM335X_EVM=y来对应的,不同的平台文件会有所不同,但都可以通过查看Makefile和Kconfig来找到正确的文件。下文均以BeagleBoneBlack为例。

启动方式

当uboot的启动流程进入到autoboot,其历史使命基本上也就完成了。但对于一个移植的板子来说,配置自动启动的选项却是一个很重要的问题。启动方式的列表定义在在am335x_evm.h中:

define BOOT_TARGET_DEVICES(func) \
func(MMC, mmc, 0) \
func(LEGACY_MMC, legacy_mmc, 0) \
func(MMC, mmc, 1) \
func(LEGACY_MMC, legacy_mmc, 1) \
func(NAND, nand, 0) \
func(PXE, pxe, na) \
func(DHCP, dhcp, na)
endif

很容易看到这个启动方式是emmc,nand,PXE,DHCP,可以自己按照需要调整顺序或者添删。

内存初始化

board/ti/am335x/board.c中的函数sdram_init()就是内存的初始化函数,它会根据EEPROM得到的板子型号,对应不同的DRAM配置。阅读代码不难发现,配置具体信息是定义在arch/arm/include/asm/arch-am33xx/ddr_defs.h。如果需要修改或者添加不同的内存配置,修改这两个文件即可。

其他硬件调整

绝大多数硬件的配置定义都在am335x_evm.h中,包括各种模块的Base address、初始化值、硬件定义等等,读者可以自行阅读。这些设置有的是根据芯片的手册而定,比如说memory的初始化值;有的是根据硬件设计而定,比如USB的Host & PERIPHERAL设置;有的是可以根据需要自行决定,比如CONFIG_SYS_BOOTM_LEN。具体改什么、怎么改,要根据情况分析而定。