导读:前文对U-Boot架构设计做了分析,本文来梳理一下U-Boot在具体板子上的移植工作,主要记录整体思路、要点,以触类旁通而记之。
1.收集Boot相关信息
NanoPC-T3 是友善之臂为企业用户设计的主板,采用三星八核Cortex -A53架构的S5P6818处理器。其主要技术参数:
- CPU: S5P6818, 动态运行主频400Mhz--1.4GHz 8核
- DDR3 RAM: 1GB,采用2片K4B4G1646D
- SD: 标准SD卡槽一个
- eMMC:8GB
- HDMI: HDMI 1.4a, Type A型口,1080P高清显示
- DVP Camera接口: 24pin, 0.5mm间距,FPC贴片竖座
- GPIO扩展接口: 4个UART, 1路I2C, 1路SPI, 3路PWM,9个GPIO
- …
详细参见http://www.arm9.net/nanopc-t3...
1.1 SOC Boot相关信息
需要将采用该SOC的方式以U-Boot引导,那么首先应该将Boot的相关信息进行收集分析。
支持的Boot模式:
- 外部静态存储器ExternalStatic Memory Boot
复位首条指令直接从外部静态存储器引导。
- 内部ROM boot
- NAND boot with Error Correction
- SD/MMC/SDFS (SD FAT File system) boot
- SPI Serial EEPROM boot
- UART boot
- USB Boot
自< SEC_S5P6818X_Users_Manual_preliminary_Ver_0.00.pdf>
SOC内置了20KB的iROMBOOT,这是一段固化的boot程序,通过识别外部Boot配置进入相应的Boot模式,完成内部boot到用户boot程序的第一阶段Boot。
从datasheet看见该SOC内部实现了iROMBOOT,而SPL(Secondaryprogramloader)是u-boot第一阶段执行的代码。主要负责搬移u-boot第二阶段的代码到。iROMBOOT已经实现了这一功能,故不需要用户实现SPL了。
1.2 NanoPC-T3板级信息
由于boot一般与板子的设计配置相关,大多由IO口配置进行选择,既然支持上述如何之多的Boot模式,那么拿到的板子又支持哪些引导模式呢?
从上图中可看出:
可见nanoPC T3仅支持SDMMC Boot模式,进入对应的章节,可看出支持三个SD通道。通过从SD存储卡,MMC存储卡和eMMC中读取iROMBOOT并通过使用SDHC模块将其加载到内存中,iROMBOOT执行用户启动代码。此方法称为SDHCBOOT。SDCLK输出400 kHz用于识别,输出24 MHz用于数据传输。
那么如何进入对应的通道呢?
SD3已经电路下拉为低:
- 默认CAM1_D3为高,用户boot通道默认为CH0,电路板设计为SD卡
SD卡是(secure digital memory card)安全数码卡,是一种基于半导体快闪记忆器的新一代记忆设备,是在MMC基础上发展起来的,其内部存储介质大都为NAND Flash增加了两个主要特色:
可以设置所存储的使用权限,防止数据被他人复制;第二是传输速度比2.11版mmc卡快。
特性:
1)可选通信协议:SD模式和SPI模式
2)可变时钟频率:0~25MHz
3)通信电压范围:2.0~3.6V
4)数据寿命:10万次编程/擦除
5)正向兼容MMC卡;
6)运行在25M的频率上,数据带宽是4位,因此最大传输速率是12.5MHz(12.5兆字节每秒) <自百度知道>
- 按住BOOT按键,将板子上电,此时用户boot通道为SD CH2,此时为板载eMMC
eMMC (Embedded Multi Media Card)是MMC协会订立、主要针对手机或平板电脑等产品的内嵌式存储器标准规格。eMMC在封装中集成了一个控制器,提供标准接口并管理闪存,使得手机厂商就能专注于产品开发的其它部分,并缩短向市场推出产品的时间。其内部存储介质大都为NAND Flash。
硬件地址空间:
该SOC具有两个内存控制器:
- MCU-A:DDR3 / LVDDR3(低压DDR3)/ LPDDR3 / LPDDR2
- MCU-A由DREX和DDRPHY组成
- 支持DDR3 /LVDDR3(低压DDR3)/ LPDDR3 / LPDDR2内存
- 支持2 GB的8/16/32位SDRAM
- 单个存储区(32位数据总线宽度)
- 支持掉电模式
- 支持自我刷新模式
- MCU-S:静态存储器
- 静态内存
- Static两个静态存储芯片选择
- NAND闪存接口
- 23位地址支持使用锁存器地址
- SLC NAND,带ECC的MLC NAND(支持BCH算法)
- 静态内存映射阴影
故地址空间为:0x4000 0000—7FFF FFFF
开发调试需要实现串口交互,那么串口是哪一个?
板上网口采用什么芯片需要,RTL8211E-VB-CG 集成10/100/1000M以太网收发器,显示接口:RGB LCD接口/HDMI
至此,第一阶段收集了那些为移植需要准备的信息?
- S5P6818,8核,armv8,64位SOC
- Boot模式,SDMMC Boot模式,支持两个通道,默认通道Ch0板载eMMC,按住Boot按键,CH0:SD Boot,不需要SPL
- 内存地址:0x40000000—7FFF FFFF 1G DDR3(2片K4B4G1646D 4Gbit/chip)
- 控制台:UART0
- 网口:RTL8211E-VB-CG 集成10/100/1000M以太网收发器。
- LCD:RGB LCD接口/HDMI
2.移植
2.1 明确移植内容
在具体实施之前,首先须总体上明确How to do:
2.2明确Boot流程
明确Boot流程,需要明确有哪些地方需要根据所选SOC,板级硬件设计做出移植:
如前文分析,u-boot的启动流程主要分亮部分。
第一阶段:芯片复位,执行复位跳转,从自各自芯片的start.S开始执行汇编代码
一般而言,start.S位于- arch/ic_xxx/cpu/start.S
如:
- arch/arm/cpu/armv7/start.S
- arch/powerpc/cpu/mpc83xx/start.S
-arch/mips/cpu/start.S
为什么这是复位入口呢?这取决于如何链接,由u-boot.lds指定:
链接文件位于./arch/arm/cpu/armv8/u-boot.lds
注:因为nanoPC-T3 S5P6818的SOC是一颗8核A53核,A53是armv8架构。
第二阶段:lowlevel_init(),board_init_f(),board_init_r():
lowlevel_init():
- 目的:允许执行达到oard_init_f()的基本初始化
- 没有global_data或BSS。
- 没有堆栈(ARMv7可能有一个堆栈,但是很快就会被删除)
- 不得设置SDRAM或使用控制台
- 必须只做最少的工作,以允许执行继续到board_init_f()
- 几乎不需要
- 从此函数正常返回
board_init_f():
- 目的:初始化CPU运行环境以准备运行board_init_r():即初始化SDRAM和UART
- global_data可用
- 堆栈位于SRAM中
- BSS不可用,因此您不能使用全局/静态变量,只能使用堆栈变量和global_data
- 非SPL特定说明:
- 调用dram_init()设置DRAM。如果已经在SPL中完成,则无法执行任何操作
- SPL特定说明:
- 可以根据需要使用自己的版本覆盖整个board_init_f()函数。
- preloader_console_init()可以在依据需要调用
- 须初始化SDRAM,以及初始化UART
- -不需要清除BSS段,由crt0.S完成
- -必须从此函数正常返回(不要直接调用board_init_r())
此处清除了BSS。对于SPL,如果定义了CONFIG_SPL_STACK_R,则此时将堆栈和global_data重定位到CONFIG_SPL_STACK_R_ADDR之下。对于非SPL,U-Boot被重定位以在内存顶部运行。
board_init_r():
- 目的:主要执行,通用代码
- global_data可用
- SDRAM可用
- BSS可用,可以使用所有静态/全局变量
- 执行最终继续到main_loop()
非SPL特定说明:
- U-Boot重定位到内存顶部,并且现在从那里开始运行。
SPL特定说明:
- 如果定义了CONFIG_SPL_STACK_R并且
CONFIG_SPL_STACK_R_ADDR指向SDRAM,则堆栈可选地位于SDRAM中 - 在这里可以调用preloader_console_init()-通常是通过定义CONFIG_SPL_BOARD_INIT然后提供包含此调用的spl_board_init()函数来完成的
- 加载U-Boot或(在falcon模式下)Linux
- 如果定义了CONFIG_SPL_STACK_R并且
以上关于lowlevel_init(),board_init_f(),board_init_r()翻译自./README
这里有几个概念需要进一步解析,方便下面理解:
- 堆(heap):堆的管理由C库实现的,Linux中是一般由Glibc,取决于选用什么C库。内存的获取与释放由程序员通过调用C库中的malloc/free进行操作,C库中的动态内存管理单元具体实现。大体上在工程中指定堆的大小,C库中的堆管理器将这片内存进行动态管理,这里需要用到一些数据结构算法对这片内存区进行动态管理。如果要对堆的管理实现进行分析,可以参考glibc的源代码。
- 栈(stack):由编译器实现栈的管理,由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆/栈的大小由下面的宏定义:
- CONFIG_SYS_MALLOC_LEN 指定堆大小
- CONFIG_SYS_RESERVE_MEM_SIZE/ CONFIG_SYS_MALLOC_LEN一起指定栈大小
Stack size= CONFIG_SYS_RESERVE_MEM_SIZE-
CONFIG_SYS_MALLOC_LEN
如下图所示:
这几个宏定义了内存上界、堆上下界、栈的上下界有编译器、C库实现越界保护机制。
global_data:
该结构体收集板子的基本信息,内存,名称,CPU时钟,环境变量,标准IO,设备驱动句柄等等。
红色部分信息将堆分配情况与U-Boot全局数据结构关联。
- bss段:bss段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。bss是英文Block Started by Symbol的简称。bss段属于静态内存分配。
- data段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。 数据段属于静态内存分配。
text段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。 这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
一个程序本质上由这三个段由链接器链接而成。
2.3 创建板子文件
位置board/my_vendor/my_board/my_board.c,对于nanoPC-T3而言
./board/s5p6818/nanopi3/ board.c
- dram_init实现内存初始化
board_init实现GPIO初始化、启动设备初始化等操作,这与板子的硬件息息相关。
需声明DECLARE_GLOBAL_DATA_PTR指针,因为本模块需要对gd成员进行初始化,ARM32等价于r9寄存器,ARM64等价于寄存器x18
位于./arch/arm/include/asm/global_data.h
2.4 创建Kconfig配置文件
board/my_vendor/my_board/Kconfig
需定义Kconfig对于board需要的相关变量SYS_BOARD,SYS_VENDOR,SYS_CONFIG_NAME。
由于S5P6818是一颗64位芯片,那么32位/64位将变成可选配置。故这里新创建了S5P6818_FEATURES用以配置32位/64位,当取值为no表示为32位配置。
- SYS_BOARD,SYS_VENDOR用于给kuild识别在那个board,verder目录需去编译相应源文件
- SYS_CONFIG_NAME用于给kuild识别在哪里去寻找板子头文件
include/configs/SYS_CONFIG_NAME.h
其工作原理如下:
2.6 创建板子Makefile
board/my_vendor/my_board/Makefile对nanoPC-T3而言:
obj-y:=board.o hwrev.o onewire.o lcds.o
由于需要实现LCD以及单线触摸控制器,则需要将上述文件编译。
2.7 创建板子defconfig
configs/my_board_defconfig,对nanoPC-T3而言:
s5p6818_nanopi3
defconfig
对于新版的U-boot已支持make menuconfig,可以运行该命令进行其他配置,然后将生成的.config文件拷贝至此。
mv .config ./configs/s5p6818_nanopi3_defconfig
主要裁剪支持的驱动,以及shell命令等
注:旧版本的U-Boot使用boards.cfg添加板子。
2.8 创建板子头文件
include/confi gs/my_board.h,对nanoPC-T3而言:
include/confi gs/ s5p6818_nanopi3.h
环境变量:
这些定义由./common下环境变量管理模块负责维护,其结构体定义如下,主要为字符串常量。
2.8 引导分析
为了能够重新映射内存,U-Boot随后跳至其链接地址。为了能够在C中实现初始化代码,在内部双端RAM中设置了一个(很小的)初始堆栈(如果CPU提供了诸如MPC8xx或MPC8260之类的功能),或者在数据的锁定部分中缓存。之后,U-Boot初始化CPU内核,缓存和SIU。
接下来,使用初步映射来映射所有可用的存储体。例如,将它们放在512 MB边界上(0x20000000的倍数:0x00000000和0x20000000上的SDRAM,0x40000000和0x60000000上的Flash,0x80000000上的SRAM)。然后,将UPMA编程用于SDRAM访问。使用临时配置,将运行简单的内存测试,以确定SDRAM存储区的大小。
如果有多个SDRAM存储区,并且存储区的大小不同,则首先映射最大的存储区。对于相等的大小,将首先映射第一个存储区(CS2#)。第一个映射始终是针对地址0x00000000的,后面紧跟着任何其他存储区以创建从0开始的连续存储器。
然后,监视器将自身安装在SDRAM区域的高端,并分配内存供malloc()和全局Board Info数据使用;同样,将异常矢量代码复制到低RAM页面中,并建立最终堆栈。
只有在此重定位之后,才能拥有“正常”的C环境。由于受到多种方式的限制,主要是因为从ROM运行,并且必须将代码重定位到RAM中的新地址运行。
至于上述过程重定向,主要利用链接绑定以实现符号表位置可知,从将u-boot映象拷贝到内存可实现运行切换。
/*指定输出格式elf64-littleaarch64*/
OUTPUT_FORMAT("elf64-littleaarch64", "elf64-littleaarch64", "elf64-littleaarch64")
OUTPUT_ARCH(aarch64) /*指定arch类型*/
ENTRY(_start) /*指定入口为start.o
SECTIONS
{
. = 0x00000000;/**/
. = ALIGN(8);/*64位 故应8字节对齐*/
.text : /*text段*/
{
*(.__image_copy_start) /*映象拷贝起始位置*/
CPUDIR/start.o (.text*)
*(.text*)
}
/*定义只读数据段*/
. = ALIGN(8);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
. = ALIGN(8);
.data : {/*定义数据段*/
*(.data*)
}
. = ALIGN(8);
. = .;
/* .u_boot_list:所有以u_boot_list名称开始的段,
都链接于此,主要是一些uboot 命令相关代码;
KEEP关键字是为了保证所有的段都被加进来,
SORT表示按照u_boot_list*后的段名子进行排序
*/
. = ALIGN(8);
.u_boot_list : {
KEEP(*(SORT(.u_boot_list*)));
}
. = ALIGN(8);
/*__image_copy_ end存放了一个地址,取值待链接时确定,
相当于创建一个位置标签;
代表程序重定位(relocate)时拷贝的结束地址 */
.image_copy_end :
{
*(.__image_copy_end)
}
. = ALIGN(8);
/*
此处含义与__image_copy_start相同,定义一个位置标签,
表示重定位时全局变量的LABLE的起始地址;
关于重定位代码的说明需要另外说明
*/
.rel_dyn_start :
{
*(.__rel_dyn_start)
}
/*.rel.dyn:存放重定位代码时全局变量的位置信息*/
.rela.dyn : {
*(.rela*)
}
/* __rel_dyn_end存放了一个地址,取值待链接时确定,
相当于创建一个位置标签;
表示重定位时全局变量的LABLE的结束地址
*/
.rel_dyn_end :
{
*(.__rel_dyn_end)
}
/*.end代码结束段*/
_end = .;
. = ALIGN(8);
/*bss段起始*/
.bss_start : {
KEEP(*(.__bss_start));
}
.bss : {
*(.bss*)
. = ALIGN(8);
}
/*bss段结束*/
.bss_end : {
KEEP(*(.__bss_end));
}
/*.dynstr动态连接符号表的字符串部分,与.dynsym联用*/
/DISCARD/ : { *(.dynsym) }
/DISCARD/ : { *(.dynstr*) }
/DISCARD/ : { *(.dynamic*) }
/DISCARD/ : { *(.plt*) } /*过程连接表(Procedure Linking Table)*/
/DISCARD/ : { *(.interp*) }
/DISCARD/ : { *(.gnu*) }
}
而在./arch/arm/lib/sections.c 定义了对应的全局符号,这样就实现了链接地址绑定:
char __bss_start[0] __attribute__((section(".__bss_start")));
char __bss_end[0] __attribute__((section(".__bss_end")));
char __image_copy_start[0] __attribute__((section(".__image_copy_start")));
char __image_copy_end[0] __attribute__((section(".__image_copy_end")));
char __rel_dyn_start[0] __attribute__((section(".__rel_dyn_start")));
char __rel_dyn_end[0] __attribute__((section(".__rel_dyn_end")));
char __secure_start[0] __attribute__((section(".__secure_start")));
char __secure_end[0] __attribute__((section(".__secure_end")));
char _end[0] __attribute__((section(".__end")));
下面对crt0_64.S 汇编代码做简要注释帮助理解重定向过程:
ENTRY(_main) /*声明入口*/
/*
* Set up initial C runtime environment and call board_init_f(0).
*/
/*利用load/store指令装载
CONFIG_SYS_INIT_SP_ADDR地址 给SP指针
为调用board_init_f创建C运行环境*/
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr x0, =(CONFIG_SPL_STACK)
#else
ldr x0, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
bic sp, x0, #0xf /* 16-byte alignment for ABI compliance */
bl board_init_f_mem
mov sp, x0
mov x0, #0
bl board_init_f /*调用board_init_f*/
/*
注:B或BL指令引起处理器转移到“子程序名”处开始执行。
两者的不同之处在于BL指令在转移到子程序执行之前,
将其下一条指令的地址拷贝到R14(LR,链接寄存器)。
由于BL指令保存了下条指令的地址,因此使用指令
“MOV PC ,LR”即可实现子程序的返回。而B指令则无
法实现子程序的返回,只能实现单纯的跳转。 用户在
编程的时候,可根据具体应用选用合适的子程序调用语句。*/
/*如果SPL没有使能*/
#if !defined(CONFIG_SPL_BUILD)
/*
* Set up intermediate environment (new sp and gd) and call
* relocate_code(addr_moni). Trick here is that we'll return
* 'here' but relocated.
*/
ldr x0, [x18, #GD_START_ADDR_SP] /* x0 <- gd->start_addr_sp */
bic sp, x0, #0xf /* 16-byte alignment for ABI compliance */
ldr x18, [x18, #GD_BD] /* x18 <- gd->bd */
sub x18, x18, #GD_SIZE /* new GD is below bd */
adr lr, relocation_return
ldr x9, [x18, #GD_RELOC_OFF] /* x9 <- gd->reloc_off */
add lr, lr, x9 /* new return address after relocation */
ldr x0, [x18, #GD_RELOCADDR] /* x0 <- gd->relocaddr */
b relocate_code
relocation_return:
/* Set up final (full) environment */
bl c_runtime_cpu_setup /* still call old routine */
/* TODO: For SPL, call spl_relocate_stack_gd() to
alloc stack relocation */
/* Clear BSS section */
ldr x0, =__bss_start /* this is auto-relocated! */
ldr x1, =__bss_end /* this is auto-relocated! */
mov x2, #0
clear_loop:
str x2, [x0]
add x0, x0, #8
cmp x0, x1
b.lo clear_loop
/* call board_init_r(gd_t *id, ulong dest_addr) */
mov x0, x18 /* gd_t */
ldr x1, [x18, #GD_RELOCADDR] /* dest_addr */
b board_init_r /* PC relative jump */
/* NOTREACHED - board_init_r() does not return */
#endif /* !CONFIG_SPL_BUILD */
ENDPROC(_main)
总结:
- 思路须清楚,不要一开始关注细节
- 采用渐进明细、逐步迭代的方法论
- u-boot 其核心在于将系统成功引导。
首发:嵌入式客栈
作者:逸珺
推荐阅读
更多硬核嵌入式技术干货请关注嵌入式客栈专栏。