目录
0. 前言
这次的工作主要是把某项目设备上的uboot版本从2017.09升级到2022.04,是作为该项目整个BSP升级计划的一部分。2017.09版本上的设备板级文件也要适当移植到2022.04版本上。
原来按照指导同事的估计,顶多需要三天,作者却花了将近一个月!当中弯路很多,记下以鉴后人!
1. SPL
SPL指的是 Second Program Loader,原本是为了简化uboot启动过程的工具。SPL加载的文件叫做u-boot.img...... 但这次首要的任务就是要去掉SPL,编译出u-boot.imx。我们的项目不需要多余的东西。但是纵观uboot的源代码, CONFIG_SPL_BUILD之类的条件编译比比皆是。选不选SPL,同一个函数就会进入另外的实现分支,可能会出现不能正确运作的问题。
去掉SPL,首先就要在 buildroot-menuconfig --> bootloader里面去掉 install SPL...(如果使用buildroot来构建uboot的话),再在uboot-menuconfig --> SPL/TPL 确保不选择 Enable SPL。其他有关SPL的选项也要注意配置。
2. imximage.cfg
在板级文件目录下,比如说./board/freecale/mx6slevk/,除了源文件、Makefile、Kconfig等,作者还第一次看到 imximage.cfg 文件。这种cfg文件其实是为了高效地批量地配置CPU寄存器而设置的。其中最关键的是里面的DCD(Device Configuration Data)部分:
/*
* Device Configuration Data (DCD)
*
* Each entry must have the format:
* Addr-type Address Value
*
* where:
* Addr-type register length (1,2 or 4 bytes)
* Address absolute address of the register
* value value to be stored in the register
*/
DATA 4 0x020c4018 0x00260324
DATA 4 0x020c4068 0xffffffff
DATA 4 0x020c406c 0xffffffff
DATA 4 0x020c4070 0xffffffff
DATA 4 0x020c4074 0xffffffff
DATA 4 0x020c4078 0xffffffff
DATA 4 0x020c407c 0xffffffff
DATA 4 0x020c4080 0xffffffff
DATA 4 0x020e0344 0x00003030
DATA 4 0x020e0348 0x00003030
DATA 4 0x020e034c 0x00003030
......
言简意赅的说明,什么寄存器要设什么值。本项目设备有个隐患是DDR不太稳定,这就需要设置DDR寄存器。
NXP提供了DDR压力测试工具,来测试DDR的表现,这个工具会给出DDR的几个寄存器应该的值,将这些值同步到cfg文件即可。作者还没试过此类DDR压力测试,采用的是老法师已经配好的cfg文件,名字改成 imximage.cfg,放在设备的板级文件夹下即可。
PS: 在 uboot-menuconfig里面要显式的声明DCD配置的地方,也就是放 imximage.cfg的地方。一般是 uboot-menuconfig --> ARM architecture --> DCD script, 或者是 uboot-menuconfig --> Boot images --> Extra Options。
3. CONFIG_XX 与 条件编译
纵观这次升级移植工作,卡住最久的地方就是上电后,uboot没有一点打印。
尝试了N多思路,最后才想起是不是串口初始化本身有问题。
举例说,在 ./board/freescale/mx6sabresd/mx6sabresd.c
.......
static void setup_iomux_uart(void)
{
SETUP_IOMUX_PADS(uart1_pads);
}
......
int board_early_init_f(void)
{
setup_iomux_uart();
return 0;
}
......
这个 SETUP_IOMUX_PADS(x) 宏是用来设置imx6的管脚复用的相关寄存器组。我们这个宏的具体实现:
./arch/arm/include/asm/mach-imx/iomux-v3.h
/* macros for declaring and using pinmux array */
#if defined(CONFIG_MX6QDL)
......
#define SETUP_IOMUX_PADS(x) \
imx_iomux_v3_setup_multiple_pads(x, ARRAY_SIZE(x)/2)
#elif defined(CONFIG_MX6Q) || defined(CONFIG_MX6D)
......
#define SETUP_IOMUX_PADS(x) \
imx_iomux_v3_setup_multiple_pads(x, ARRAY_SIZE(x))
#elif defined(CONFIG_MX6UL) || defined(CONFIG_MX6ULL)
......
#define SETUP_IOMUX_PADS(x) \
imx_iomux_v3_setup_multiple_pads(x, ARRAY_SIZE(x))
#else
......
#define SETUP_IOMUX_PADS(x) \
imx_iomux_v3_setup_multiple_pads(x, ARRAY_SIZE(x))
#endif
问题1就出在这里,作者在工程里一直使能的是CONFIG_MX6DL,而2017.09的工程里使能的是CONFIG_MX6QDL。所以 SETUP_IOMUX_PADS(x) 实现方式不一样。串口相关寄存器的设置也不同!
解决了这个问题后(使能CONFIG_MX6QDL),发现串口还是没有打印,继续由后往前推。
在./arch/arm/lib/crt0.S
/*
* entry point of crt0 sequence
*/
ENTRY(_main)
/*
* Set up initial C runtime environment and call board_init_f(0).
*/
#if defined(CONFIG_TPL_BUILD) && defined(CONFIG_TPL_NEEDS_SEPARATE_STACK)
......
#endif
bic r0, r0, #7 /* 8-byte alignment for ABI compliance */
mov sp, r0
bl board_init_f_alloc_reserve
mov sp, r0
/* set up gd here, outside any C code */
mov r9, r0
bl board_init_f_init_reserve
#if defined(CONFIG_DEBUG_UART) && CONFIG_IS_ENABLED(SERIAL)
bl debug_uart_init
#endif
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_EARLY_BSS)
CLEAR_BSS
#endif
mov r0, #0
bl board_init_f
#if ! defined(CONFIG_SPL_BUILD)
......
ENDPROC(_main)
在执行 board_init_f() 之前,串口的初始化其实还会有变数。通过对比发现问题2,2017.09的工程里没有使能CONFIG_DEBUG_UART,而新工程却使能了,导致串口先被执行debug_uart_init。
当作者消掉CONFIG_DEBUG_UART,再编译。串口printf终于可以打印了,uboot调试至此走上了快车道。
这个重大的教训让作者明白:uboot-menuconfig 里多如牛毛的 CONFIG_XXX 宏,它们与uboot源代码的桥梁就是大量的条件编译语句!我们在uboot-menuconfig里对各种宏的使能与否,直接导致了各种函数在源代码里的实现路径,甚至是会不会被具体实现(不然可能只是weak函数)!
4. 板级文件的核心内容
乍一看板级文件夹的主源文件,如 ./board/freescale/mx6sabresd/mx6sabresd.c,洋洋洒洒900多行。其实因为有大量条件编译,可能就执行里面三分之一的代码。
而且板级文件的核心内容其实就是配合 board_f.c及其他启动文件,把 串口 和 SD/MMC 两个外设初始化。SD/MMC 是硬盘,是必需品。串口 是uboot唯一的人机交互手段。其他诸如I2C,SPI,显示,网口等非核心功能都可以注释掉(甚至最终状态下它们都不是必须的)。注意串口 和 SD/MMC 两个外设的设备号或者总线号,容易搞错。
5. 总结:学会收敛问题
uboot作为一个大型的可交互的应用程序,要进行调试,必须先有串口打印功能。如果一上来,没有打印,必须首先把串口初始化和打印功能给弄出来。
./common/board_f.c 可以说是,uboot启动后第一个执行的C语言源文件。如下图,切入口是 board_init_f(),它执行的是 init_sequence_f[ ] 函数指针数组里的所有满足条件编译的函数。
static const init_fnc_t init_sequence_f[] = {
setup_mon_len,
#ifdef CONFIG_OF_CONTROL
fdtdec_setup,
#endif
#ifdef CONFIG_TRACE_EARLY
trace_early_init,
#endif
initf_malloc,
log_init,
initf_bootstage, /* uses its own timer, so does not need DM */
#ifdef CONFIG_BLOBLIST
bloblist_init,
#endif
setup_spl_handoff,
#if defined(CONFIG_CONSOLE_RECORD_INIT_F)
console_record_init,
#endif
#if defined(CONFIG_HAVE_FSP)
arch_fsp_init,
#endif
arch_cpu_init, /* basic arch cpu dependent setup */
mach_cpu_init, /* SoC/machine dependent CPU setup */
initf_dm,
arch_cpu_init_dm,
#if defined(CONFIG_BOARD_EARLY_INIT_F)
board_early_init_f,
#endif
#if defined(CONFIG_PPC) || defined(CONFIG_SYS_FSL_CLK) || defined(CONFIG_M68K)
/* get CPU and bus clocks according to the environment variable */
get_clocks, /* get CPU and bus clocks (etc.) */
#endif
#if !defined(CONFIG_M68K)
timer_init, /* initialize timer */
#endif
#if defined(CONFIG_BOARD_POSTCLK_INIT)
board_postclk_init,
#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 */
checkcpu,
#if defined(CONFIG_SYSRESET)
print_resetinfo,
#endif
#if defined(CONFIG_DISPLAY_CPUINFO)
print_cpuinfo, /* display cpu info (and speed) */
#endif
#if defined(CONFIG_DTB_RESELECT)
embedded_dtb_select,
#endif
#if defined(CONFIG_DISPLAY_BOARDINFO)
show_board_info,
#endif
INIT_FUNC_WATCHDOG_INIT
#if defined(CONFIG_MISC_INIT_F)
misc_init_f,
#endif
INIT_FUNC_WATCHDOG_RESET
#if CONFIG_IS_ENABLED(SYS_I2C_LEGACY)
init_func_i2c,
#endif
#if defined(CONFIG_VID) && !defined(CONFIG_SPL)
init_func_vid,
#endif
announce_dram_init,
dram_init, /* configure available RAM banks */
#ifdef CONFIG_POST
post_init_f,
#endif
INIT_FUNC_WATCHDOG_RESET
#if defined(CONFIG_SYS_DRAM_TEST)
testdram,
#endif /* CONFIG_SYS_DRAM_TEST */
INIT_FUNC_WATCHDOG_RESET
#ifdef CONFIG_POST
init_post,
#endif
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_dest_addr,
#ifdef CONFIG_OF_BOARD_FIXUP
fix_fdt,
#endif
#ifdef CONFIG_PRAM
reserve_pram,
#endif
reserve_round_4k,
arch_reserve_mmu,
reserve_video,
reserve_trace,
reserve_uboot,
reserve_malloc,
reserve_board,
reserve_global_data,
reserve_fdt,
reserve_bootstage,
reserve_bloblist,
reserve_arch,
reserve_stacks,
dram_init_banksize,
show_dram_config,
INIT_FUNC_WATCHDOG_RESET
setup_bdinfo,
display_new_sp,
INIT_FUNC_WATCHDOG_RESET
reloc_fdt,
reloc_bootstage,
reloc_bloblist,
setup_reloc,
#if defined(CONFIG_X86) || defined(CONFIG_ARC)
copy_uboot_to_ram,
do_elf_reloc_fixups,
#endif
clear_bss,
#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
!CONFIG_IS_ENABLED(X86_64)
jump_to_copy,
#endif
NULL,
};
void board_init_f(ulong boot_flags)
{
gd->flags = boot_flags;
gd->have_console = 0;
if (initcall_run_list(init_sequence_f))
hang();
#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
!defined(CONFIG_EFI_APP) && !CONFIG_IS_ENABLED(X86_64) && \
!defined(CONFIG_ARC)
/* NOTREACHED - jump_to_copy() does not return */
hang();
#endif
}
在init_sequence_f[ ]中,真正有关串口初始化和printf功能准备的是这三句:
static const init_fnc_t init_sequence_f[] = {
......
#if defined(CONFIG_BOARD_EARLY_INIT_F)
board_early_init_f,
#endif
......
init_baud_rate, /* initialze baudrate settings */
serial_init, /* serial communications setup */
......
};
作者甚至尝试把这三句放在init_sequence_f[ ]的前三排,结果后面列表里的所有函数都可以用printf打印出信息。(作者首先是在能用的 2017.09老版本uboot上做这个实验)。
所以把 printf 尽早靠前的使能好,就可尽量提早的发现程序在C语言源代码里执行到哪一步出问题了;如果在串口功能没问题的情况下,还是没打印,也可以把问题收敛到最开始汇编文件的范围里。
所以这次的一大思路: 首先确保串口初始化和printf()正确可用 ---> 再用printf() 来定位问题。