废弃全志A20广告机变身记(1)
前言
前几天在咸鱼上面捡到一块全志的A20广告机,总共花费了我50米。板子的电源是12V的,洗洗干净之后发现还能够继续使用,顿时欣喜若狂,因此在这里记录一下这块板子的探索记。
首先感谢这位大佬,他的文章给予了我很多帮助。
u-boot以及SD卡启动探究
uboot FIT image介绍
legacy uImage
1、Linux kernel在ARM架构中引入了device tree,全称是flattened device tree,因此可以简称为FDT。在编译Linux kernel的时候,不需要指定具体的架构或者soc,只需要告诉kernel这次编译需要那些板级和platform即可,最终会生成一个kernel image,以及多个具体的fdt image(dtb文件)。
2、另外从uboot的角度来讲,他要boot一个二进制文件,例如uboot中的kernel image,同时还需要解析里面的数据,包括内部的文件类型(kernel image、dtb文件、ramdisk image等等),还需要了解文件放置的位置,他的加载地址等。除此之外,文件在memory中的执行地址,文件是否被压缩,是否包含有一些完整的CRC校验码等。
3、这种比较老的image文件,它是对应的格式比较简单,就是在二进制文件上面加上一个header,当然这个header是可以在"include/image.h"
里面看到他的定义的。这种比较老的image格式文件称之为legacy uImage。
4、legacy uImage使用mkimage工具生成,mkimage工具可以在u-boot source code的tools/mkimage
里面找到。同时支持OS Kernel Image、RAMDISK Image等多种格式的image,支持gzip和bzip算法,能够进行CRC32 checksums等等。当然这种比较老的u-boot基本上已经被淘汰了,再去深究意义不大。
FIT uImage
device tree在kernel中普及之后,u-boot也马上跟进了device tree,最终推出了一种全新的image格式,这种格式的文件就是FIT(flattened image tree) uImage,FIT和FDT(flattened device tree)比较类似,利用的语法都是Device Tree Source files语法,生成的iamge文件也和dtb文件基本上一样。
FIT uImage的生成流程可以概括如下
其中的image source file(.its)和device tree source file(.dts)类似,负责描述要生成的image file信息,mkimage和dtc工具可以将.its文件以及对应的image data file打包成一个iamge file,最后将这个file下载到memory中,最后使用bootm命令加载即可。
Image source file语法
image source file语法和device tree source file语法基本上完全是一样的,只不过多了一些images、configurations等特殊的节点。
- image节点
这个节点是指定包含的二进制文件,可以指定多种类型的多个二进制文件,例如在sunxi7i.its文件中包含了kernel image 这个节点。 - configuration可以将不同类型的二进制文件,根据不同的场景组成一个一个的配置项。u-boot在boot的时候,以配置项为单位进行加载和执行,这样就可以根据不同的场景,方便后续的配置,实现boot u-boot的功能。
- image的编译FIT uImage的编译过程很简单的,根据实际的情况编写对应的image source file之后(假如名称是kernel_fdt.its),在命令中使用mkimage工具进行编译即可
mkimage -f kernel_fdt.its kernel_fdt.itb
,最后使用dfu工具将生成的idb文件下载到内存的某个指定地址即可。
u-boot平台代码的启动流程分析
在sunxi中,u-boot会从arch/arm/cpu/armv7/start.S
的_start接口处开始执行,在后续的u-boot移植中都是按照这个开始的。
.globl _start
_start:
b reset
start会跳转到reset,而reset部分的代码为
#ifdef CONFIG_SYS_RESET_SCTRL
bl reset_sctrl
#endif
/*
* Could be EL3/EL2/EL1, Initial State:
* Little Endian, MMU Disabled, i/dCache Disabled
*/
adr x0, vectors
switch_el x1, 3f, 2f, 1f
3: msr vbar_el3, x0
mrs x0, scr_el3
orr x0, x0, #0xf /* SCR_EL3.NS|IRQ|FIQ|EA */
msr scr_el3, x0
msr cptr_el3, xzr /* Enable FP/SIMD */
#ifdef COUNTER_FREQUENCY
ldr x0, =COUNTER_FREQUENCY
msr cntfrq_el0, x0 /* Initialize CNTFRQ */
#endif
b 0f
2: msr vbar_el2, x0
mov x0, #0x33ff
msr cptr_el2, x0 /* Enable FP/SIMD */
b 0f
1: msr vbar_el1, x0
mov x0, #3 << 20
msr cpacr_el1, x0 /* Enable FP/SIMD */
0:
/* Apply ARM core specific erratas */
bl apply_core_errata
/*
* Cache/BPB/TLB Invalidate
* i-cache is invalidated before enabled in icache_enable()
* tlb is invalidated before mmu is enabled in dcache_enable()
* d-cache is invalidated before enabled in dcache_enable()
*/
/* Processor specific initialization */
bl lowlevel_init
#ifdef CONFIG_ARMV8_MULTIENTRY
branch_if_master x0, x1, master_cpu
/*
* Slave CPUs
*/
slave_cpu:
wfe
ldr x1, =CPU_RELEASE_ADDR
ldr x0, [x1]
cbz x0, slave_cpu
br x0 /* branch to the given address */
master_cpu:
/* On the master CPU */
#endif /* CONFIG_ARMV8_MULTIENTRY */
bl _main
在这里主要的工作是reset
SCTRL
寄存器,具体的可以参考reset_sctrl
函数,由CONFIG_SYS_RESET_SCTRL
控制,一般这里是不用打开的。
之后会调用lowlevel_init函数,具体的功能解释参考u-boot的readme文档,如果是多CPU的场景,处理其他CPU的boot由相应的CONFIG_ARM_MULTIENTRY
控制。最后跳转到arm公共的_main中执行。
此外_main为ctr0,ctr0是C Runtimr Startup Code的简称,意思就是运行C代码之前的准备工作。关于_main函数,ctr0中有详细的介绍。
ctr0功能概括
首先,ctr0主要为C代码的运行配置环境,为调用board_init_f
函数做接口的准备,设置堆栈(为C语言函数的调用做准备,堆栈是必须的)。如果当前编译的是SPL(由CONFIG_SPL_BUILD
定义),可以单独定义堆栈基地址(CONFIG_SPL_STACK
),否则通过CONFIG_SYS_INIT_SP_ADDR
定义堆栈基地址。
调用board_init_f_alloc_reserve
接口,从堆栈开始的地方,为大名鼎鼎的GD(global data
)数据分配空间。
调用board_init_f_reserve
接口对GD进行初始化。一般对于编程而言,需要进行点灯操作来确定u-boot是否正确的运行了,在确定正确运行之后再进行后续的DRAM、DDR和system范围内的RAM等,当然这里的步骤都是针对SPL模式而言,对于传统模式类似。之后还需要计算代码后续还需要的一些参数,包括relocation
destination
、the future stack、the future GD location
等。如果是SPL模式,则会在_main函数结束之后直接返回,如果是正常的u-boot,则会继续执行后续的操作。
接下来就是清除BBS段,调用board_init_r函数,执行后续的操作。
u-boot小结
SPL是secondary program Loader
的简称,之所以称为secondary
是相对于ROM code
而言的。SPL是u-boot中独立的一个代码分支机构,由CONFIG_SPL_BUILD
配置项控制,是为了在正常的u-boot image
之外,提供一个独立的,小size的SPL image
。通常是用于那些SRAM比较小、无法直接装载并运行整个u-boot的平台。
如果使用了SPL功能,则u-boot的启动流程为
(1)、ROM code 加载SPL并运行。
(2)、SPL进行必要的初始化操作,加载u-boot并运行。
(3)、u-boot后续操作。
而整个配置过程所使用到的一些宏定义可以总结如下
CONFIG_SYS_RESET_SCTRL,控制是否在启动的时候reset SCTRL寄存器,一般不需要打开。
CONFIG_ARM_ERRATA_XXX,控制ARM core的勘误信息,一般不需要打开。
CONFIG_GICV2、CONFIG_GICV3,控制GIC的版本,用到的时候再说明。
CONFIG_ARMV8_MULTIENTRY,控制是否在u-boot中使用多CPU,一般不需要。
CONFIG_SPL_BUILD,是否是能SPL的编译,需要的话可以打开。
CONFIG_SPL_STACK,如果配置了CONFIG_SPL_BUILD,是否为SPL image配置单独的stack(SP基址),如果需要,通过该配置项配置,如果不需要,则使用CONFIG_SYS_INIT_SP_ADDR。
CONFIG_SYS_INIT_SP_ADDR,配置u-boot的stack(SP基址),对于u-boot功能来说,必须提供。
uboot Generic Board部分
u-boot启动中的具体板型包括board_init_r
和board_init_f
里面的内容,这里面的整个过程会持续到main_loop
,也就是传说中的命令行。
u-boot的策略就是声明一系列的API(如low_level_init、board_init_f、board_init_r
等等),并在u-boot的核心控制逻辑中调用他们。而作为平台开发者所做的事就是根据所需,实现他们。
与此同时,为了减少开发的工作量,u-boot为大部分API提供了通用的实现(一般是通过CONFIG
配置项或者若定义去控制是否实现编译)。以board_init_f
和board_init_r
两个板级初始化接口为例,u-boot分别在common/board_f.c
和common/board_r.c
里面进行了实现。
ifndef CONFIG_SPL_BUILD
…
# boards
obj-$(CONFIG_SYS_GENERIC_BOARD) += board_f.o
obj-$(CONFIG_SYS_GENERIC_BOARD) += board_r.o
…
endif # !CONFIG_SPL_BUILD
这两个通用的接口是由CONFIG_SYS_GENERIC_BOARD
配置项控制的,并且只会在u-boot image中被编译。再通过arch/Kconfig
中ARM平台有关的配置可知。
config ARM
bool "ARM architecture"
select CREATE_ARCH_SYMLINK
select HAVE_PRIVATE_LIBGCC if !ARM64
select HAVE_GENERIC_BOARD
select SYS_GENERIC_BOARD
select SUPPORT_OF_CONTROL
ARM平台已经自动使能了CONFIG_SYS_GENERIC_BOARD
配置,因此u-boot image
有关的板级启动流程是由Generic Board的代码实现的。
_main函数
_main
函数的分析已经进行过了,主要是设置初始化的堆栈,对应的基地址是由CONFIG_SYS_INIT_SP_ADDR
定义。
其次是分配global data
所需的空间,将堆栈设置为16bits
对齐之后,调用board_init_f_alloc_reserve
接口,从堆栈开始的地方,为u-boot
的global
data(struct global_data)
分配空间。如下
/* common/init/board_init.c */
ulong board_init_f_alloc_reserve(ulong top)
{
/* Reserve early malloc arena */
#if defined(CONFIG_SYS_MALLOC_F)
top -= CONFIG_SYS_MALLOC_F_LEN;
#endif
/* LAST : reserve GD (rounded up to a multiple of 16 bytes) */
top = rounddown(top-sizeof(struct global_data), 16);
return top;
}
需要注意的是,如果定义了CONFIG_SYS_MALLOC_F_LEN
之后,则会预先留出early malloc
所需的空间。
global data
的空间分配之后,调用board_init_f_init_reserve
,初始化global data
。所谓的初始化,无非就是一些清零的操作,不过有几个地方需要注意
(1)、如果不是ARM
平台(!CONFIG_ARM)
,则可以调用arch_setup_gd
接口,进行arch
级别的设置。当然前提是对应的arch
应该有这个接口。
(2)、如果定义了CONFIG_SYS_MALLOC_F
,则会初始化gd->malloc_base
。
执行前置(front)
操作,调用board_init_f
接口,执行前置的初始化操作。
然后执行relocation
操作,清除BBS
段,执行后续的rear
初始化操作。
global data思考
u-boot
是一个bootloader
,有些情况下,他可能只位于系统的只读存储器(ROM或者FLASH
)中,并从哪里开始执行,因此我们要怎么处理呢?
因此这种情况下,在执行u-boot
的期间(在将自己copy
到可读的存储器之前),他所在的存储空间是不可写的,这样会有两个问题
(1)、堆栈无法使用,无法执行函数的调用,即无法使用C环境。
(2)、没有data
段(或者正确初始化data段)可用,不同的函数或者代码之间无法通过全局变量的形式共享数据。
对于问题(1),通常的解决方案是:
u-boot运行起来之后,在那些不需要执行任何初始化动作即可使用的、可读写的存储区域,开辟一段堆栈(stack
)空间。
一般来说,大部分的平台如ARM
平台,都有自己的SRAM
可以作为堆栈空间。如果实在不行,也可以借助CPU
的data cache
的方法。
对于问题(2),通常的解决是:
首先,对于开发者而言,在u-boot
被copy
到可读写的RAM
之前,永远都不要使用全局变量。其次就是在relocation
之前,不同模块之间确实有通过全局全局变量传参数的需求时,可以考虑通过global data
实现。
gloabal data
为了在relocation
前通过全局变量的形式传递数据,u-boot
设计了一套巧妙的方法。首先,定义一个struct global_data
类型的数据结构,里面保存了各色各样需要传递的数据,这里对应的数据类型在”include/asm-generic/global_data.h”
中。
堆栈配置好之后,在堆栈开始的位置为struct global_data
预留空间,并将开始地址(也就是一个struct global_data
指针)保存在一个寄存器中,后续的传递都是通过保存在寄存器中的指针实现的。对于arm64
平台而言,该指针保存在了X18
寄存器中。
bl board_init_f_alloc_reserve
mov sp, x0
/* set up gd here, outside any C code */
mov x18, x0
bl board_init_f_init_reserve
上面的board_init_f_alloc_reserve
的返回值(x0)
就是global data
的指针。
/* arch/arm/include/asm/global_data.h */
#ifdef __clang__
#define DECLARE_GLOBAL_DATA_PTR
#define gd get_gd()
static inline gd_t *get_gd(void)
{
gd_t *gd_ptr;
#ifdef CONFIG_ARM64
…
__asm__ volatile("mov %0, x18\n" : "=r" (gd_ptr));
#else
…
}
#else
#ifdef CONFIG_ARM64
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("x18")
#else
…
#endif
global data
准备好之后,u-boot
会执行前置的板级初始化操作,即board_init_f
。所谓的前置的初始化动作,主要是relocation
之前的初始化操作,也就是说在执行board_init_f
的时候,u-boot
很有可能还在只读的存储器中。
对于ARM等平台而言,u-boot
提供了一个通用的board_init_f
接口,该接口使用u-boot
惯用的设计思路——u-boot
将需要在board_init_f
中初始化的内容,抽象为系列的API
。这些API
由u-boot
声明,由平台开发者根据实际情况来实现。
board_init_f
位于common/board_f.c
中,他的接口实现非常简单,如下
void board_init_f(ulong boot_flags)
{
…
gd->flags = boot_flags;
gd->have_console = 0;
if (initcall_run_list(init_sequence_f))
hang();
…
}
对global data
进行初始化之后,调用位于init_sequence_f
数组中的各种初始化API
。
trace_early_init
由CONFIG_TRACE
配置项控制,暂时可以不用关心。
initf_malloc
如果定义了CONFIG_SYS_MALLOC_F_LEN
,则调用initf_malloc
,初始化和malloc
有关的global data
。
arch_cpu_init cpu
级别的初始化操作,可以在需要的时候由CPU
有关的code
实现。
initf_dm driver model
有关的初始化操作,如果定义了CONFIG_DM
,则调用dm_init_and_scan
初始化并扫描系统所有的device
,如果定义了CONFIG_TIMER_EARLY
,调用dm_timer_init
初始化driver model
所需的timer
。
board_early_init_f
如果定义了CONFIG_BOARD_EARLY_INIT_F
,则调用board_early_init
接口,以完成特定的功能。
timer_init
初始化系统的timer
。
get_clocks
获取当前CPU
和BUS
的时钟频率,并保存在global data
中。
env_init
初始化环境变量有关的逻辑。
init_board_rate
获取当前使用的波特率,可以有两个途径,从baudrate
中获取或者从CONFIG_BAUDRATE
配置中获取。
serial_init
初始化serial
,包括u-boot serial core
以及具体的serial driver
,该函数执行之后,串口可用。
console_init_f
配置内容如下
/* Called before relocation - use serial functions */
int console_init_f(void)
{
gd->have_console = 1;
#ifdef CONFIG_SILENT_CONSOLE
if (getenv("silent") != NULL)
gd->flags |= GD_FLG_SILENT;
#endif
print_pre_console_buffer(PRE_CONSOLE_FLUSHPOINT1_SERIAL);
return 0;
}
初始化系统的控制台之后串口可以输出,相应的配置项为CONFIG_SILENT_CONSOLE
。
fdtdec_prepare_fdt
如果定义了CONFIG_OF_CONTROL
,调用fdtdec_prepare_fdt
接口,准备device tree
有关的内容。
display_options/display_text_info/print_cpuinfo/show_board_info
这个文件是通过控制台显示一些信息,可以用于debug
。
misc_init_f
如果使能了CONFIG_MISC_INIT_F
,则调用misc_init_f
执行misc driver
有关的初始化内容。
init_func_i2c
如果使能了CONFIG_HARD_I2C
或者CONFIG_SYS_I2C
,则调用init_func_i2c
执行i2c driver
有关的初始化。
init_func_spi
如果使能了CONFIG_HARD_SPI
,则调用init_func_spi
执行spi driver
有关的初始化。
announce_dram_init
宣布要进行DDR
的初始化动作了。
dram_init
调用dram_init
接口,初始化系统中的DDR,dram_init
应该由平台相关的代码实现。另外,如果DDR
在SPL
中已经实现过了,则不需要重新初始化,只需要将DDR信息保存在global data
中即可——gd->ram_size=?
。
testdram
如果定义了CONFIG_SYS_DRAM_TEST
,则会调用testdram
执行DDR
的测试操作,可以在开发阶段打开,系统稳定之后再关闭即可。
relocate
完成之后,真正的C语言运行环境才算建立起来,接下来会执行后续的板级初始化操作,即board_init_r
函数。board_init_r
和board_init_f
的设计思路基本上一样,也有一个很长的初始化序列——init_sequence_r
,该序列包含下面的初始化函数,逻辑比较简单,这里不做详细的介绍,权当是index
吧。
initr_trace,初始化并使能u-boot的tracing system,涉及的配置项有CONFIG_TRACE。
initr_reloc,设置relocation完成的标志。
initr_caches,使能dcache、icache等,涉及的配置项有CONFIG_ARM。
initr_malloc,malloc有关的初始化。
initr_dm,relocate之后,重新初始化DM,涉及的配置项有CONFIG_DM。
board_init,具体的板级初始化,需要board代码根据需要实现,涉及的配置项有CONFIG_ARM。
set_CPU_clk_info,initialize clock framework,涉及的配置项有CONFIG_CLOCKS。
initr_serial,重新初始化串口(暂时不知道意义)。
initr_announce,宣布已经在RAM中执行,会打印relocate后的地址。
board_early_init_r,由板级代码实现,涉及的配置项有CONFIG_BOARD_EARLY_INIT_R。
arch_early_init_r,由arch代码实现,涉及的配置项有CONFIG_ARCH_EARLY_INIT_R。
power_init_board,板级的power init代码,由板级代码实现,例如hold住power。
init_flash、init_nand、init_onenand、initr_mmc、initr_dataflash,各种flash设备的初始化。
initr_env,环境变量有关的初始化。
initr_secondary_cpu,初始化其他的CPU core。
stdio_add_devices,各种输入输出设备的初始化,如LCD driver等。
interrupt_init 中断有关的初始化。
initr_enable_interrupts,使能系统中断,涉及的配置项有CONFIG_ARM(ARM平台在u-boot是在开中断的情况下运行的)。
initr_status_led 状态指示LED的初始化,涉及的配置项有CONFIG_STATUS_LED、STATUS_LED_BOOT。
initr_ethaddr Ethernet的初始化,涉及配置项有CONFIG_CMD_NET。
board_late_init 由板级代码实现,涉及的配置项有CONFIG_BOARD_LATE_INIT。
run_main_loop/main_loop、执行到main_loop开始命令行的操作
SD卡启动探究
前面已经详细的介绍了u-boot的整个启动过程以及启动流程,但是这对于手中的这块废板子而言终究还是纸上得来终觉浅。
前面谈到的所有u-boot都是假设于在某一个地址上面启动的,实际使用中u-boot的启动位置可以是内部的EMMC、NAND FLASH、NOR FLASH
或者外部的SD卡。具体是那个启动地址还需要根据实际的处理器型号来进行确立。
A20的u-boot启动方式和其他Soc
的启动方式有很大的区别,相对于其他Soc的启动,增加了一个8kb字节的启动段。
因此对于普通的SPL启动而言,只需要把对应的uboot放在地址上即可。