beaglebone ai底层原理分析:spl阶段
- 1.本文说明
- 2.为什么需要SPL
- 3.SPL的工作流程
- 3.1 链接脚本分析
- 3.2 代码执行流程
- 3.3 从board_init_f看驱动初始化
- 3.4 代码重定位
- 4.beaglebone ai的led调试
- 5.总结
1.本文说明
在去深入分析一款芯片的使用的时候,往往需要关注其启动的流程与底层初始化的代码。只有掌握了这些信息,做代码优化和裁剪才能游刃有余,在特定的环境下,以最佳的方式去使用好芯片。
我们往往在进行Linux做开发的时候,都会从uboot去引导加载代码,uboot也可以分为两个阶段,第一个阶段就是SPL(Secondary programloader),主要用于将uboot的第二阶段的代码搬移到片外内存去执行。而第二阶段则从文件系统中搬移kernel代码开始执行。
代码语言:javascript复制 -------- ---------------- ---------------- ----------
| Boot | Terminology #1 | Terminology #2 | Actual |
| stage | | | program |
| number | | | name |
-------- ---------------- ---------------- ----------
| 1 | Primary | - | ROM code |
| | Program | | |
| | Loader | | |
| | | | |
| 2 | Secondary | 1st stage | u-boot |
| | Program | bootloader | SPL |
| | Loader (SPL) | | |
| | | | |
| 3 | - | 2nd stage | u-boot |
| | | bootloader | |
| | | | |
| 4 | - | - | kernel |
| | | | |
-------- ---------------- ---------------- ----------
本文会通过beaglebone ai的开发板,从头分析spl阶段的初始化流程以及ti am5729芯片底层初始化的编程方法,并且通过点灯调试法来跟踪程序执行的顺序。
2.为什么需要SPL
当芯片上电后,会执行在片内的ROM里的程序,这是由芯片固化且无法进行编程和烧录。比如开发板设置拨码开关选择启动模式,然后ROM则会执行相应的程序从spi flash或者SD卡或者nandflash中开始搬移特定大小的程序到SRAM中。此时由于外设都未初始化,只能在SRAM中执行程序。受到价格等因素的影响,SRAM不可能做到很大,比如一般为4K/8K/16K等。这么小的内存使用,完全不能支持完整的UBOOT运行起来,所以此时需要一个SPL阶段,初始化必要的外设,关闭看门狗,关闭cache,初始化DDR,然后重定位UBOOT,让其在DDR中去执行,由于DDR的内存容量很大,完全运行UBOOT是没问题的。简单的来说,SPL的功能就是将uboot启动起来的一个缓冲区。当然,如果在某些的特定的用法上,完全可以用SPL去引导kernel启动,这样可以大大减少系统的启动时间。
对于beaglebone ai采用ti的am5729芯片,在使用beaglebone ai这个开发板的时候,需要注意其启动顺序。首先是SD卡,接着是eMMC。
3.SPL的工作流程
为了实践与理论的结合测试,目前选择的开发板为beaglebone ai,主控为德州仪器的AM5729。
代码语言:javascript复制git clone -b v2019.07-rc4 https://github.com/u-boot/u-boot --depth=1
cd u-boot/
为该UBOOT打上AM5729的补丁
代码语言:javascript复制wget -c https://github.com/eewiki/u-boot-patches/raw/master/v2019.07-rc4/0001-am57xx_evm-fixes.patch
patch -p1 < 0001-am57xx_evm-fixes.patch
接着本次实验会通过sd卡进行启动调试,所以可以通过下面的脚本生成可以在beaglebone ai上启动的spl程序。
代码语言:javascript复制export DISK=/dev/sdc
sudo dd if=/dev/zero of=${DISK} bs=1M count=10
sudo dd if=MLO of=${DISK} count=2 seek=1 bs=128k
sudo dd if=u-boot.img of=${DISK} count=4 seek=1 bs=384k
制作完成后,插入sd卡,并且插入调试串口,则可以进行调试工作。
3.1 链接脚本分析
在分析程序的布局的时候,链接脚本就是程序的布局,看懂链接脚本,大致可以看到程序分布的情况。
具体可以看u-boot/spl/u-boot-spl.lds
。
该链接脚本的前面两句给定了地址范围。
代码语言:javascript复制MEMORY { .sram : ORIGIN = 0x40300000, LENGTH = ((0x4037C000 - 0x00000400) - 0x40300000) }
MEMORY { .sdram : ORIGIN = 0x80a00000, LENGTH = 0x80000 }
表示了sram的起始地址为0x40300000
,长度为(0x4037C000 - 0x00000400) - 0x40300000
。其中起始地址是需要关注的。翻看《AM572x Technical Reference Manual》。查看Memory Mapping
章节。
可以看到OCMC_RAM1的起始地址正好是0x40300000
,大小为512KB。而SPL在链接的脚本空间申明的时候,有部分空间剩余,用作了其他的用途。
下面来看另外一个重要的信息
代码语言:javascript复制ENTRY(_start)
SECTIONS
{
.text :
{
__start = .;
*(.vectors)
arch/arm/cpu/armv7/start.o (.text*)
*(.text*)
} >.sram
其中ENTRY(_start)
是告诉编译器,入口地址是_start。首先存放的是.text
代码段,放在最开始的是中断向量表,紧接着存放start.S的代码段,然后存放其他的代码段数据。
3.2 代码执行流程
曾经看到一张流程图很经典
代码语言:javascript复制https://www.processon.com/view/59772ef1e4b0ec9af088e533#map
由于uboot具有通用性,可以作为分析参考
首先要找到.vectors
段的代码,因为按照链接脚本,这个是最新链接的,不难找到在arch/arm/lib/vectors.S
下直接从_start
处开始执行,然后通过b reset
跳转到archarmcpuarmv7start.S
的reset
函数。
下面来简单分析一下该初始化流程:
函数一开始就跳转
代码语言:javascript复制reset:
/* Allow the board to save important registers */
b save_boot_params
该处实际上什么都没做。
然后关闭中断,退出HYP模式
代码语言:javascript复制 mrs r0, cpsr
and r1, r0, #0x1f @ mask mode bits
teq r1, #0x1a @ test for HYP mode
bicne r0, r0, #0x1f @ clear all mode bits
orrne r0, r0, #0x13 @ set SVC mode
orr r0, r0, #0xc0 @ disable FIQ and IRQ
msr cpsr,r0
接着重新设置了中断向量表,然后将中断向量表的地址指向_start
地址处,也就是前面分析的那里。
#ifdef CONFIG_HAS_VBAR
/* Set vector address in CP15 VBAR register */
ldr r0, =_start
mcr p15, 0, r0, c12, c0, 0 @Set VBAR
#endif
而后,开始了底层初始化
代码语言:javascript复制#ifdef CONFIG_CPU_V7A
bl cpu_init_cp15
#endif
#ifndef CONFIG_SKIP_LOWLEVEL_INIT_ONLY
bl cpu_init_crit
#endif
其中cpu_init_cp15
是使icache、dcache无效,cp15为arm的协处理器,可以设置cache、mmu、TLBs等等。这些后面再专门分析,反正就是对cache进行了无效操作。
cpu_init_crit
是做一些系统底层级别的初始化,对于am5729这里,未做任何事。
ENTRY(cpu_init_crit)
/*
* Jump to board specific initialization...
* The Mask ROM will have already initialized
* basic memory. Go here to bump up clock rate and handle
* wake up conditions.
*/
b lowlevel_init @ go setup pll,mux,memory
ENDPROC(cpu_init_crit)
然后就通过b lowlevel_init
跳转到了arch/arm/cpu/armv7/lowlevel_init.S
文件,开始执行lowlevel_init
函数。
一开始就是设置SP指针,为其设置栈空间。需要注意的是,栈的空间目前是在SRAM里。
代码语言:javascript复制 /*
* Setup a temporary stack. Global data is not available yet.
*/
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr sp, =CONFIG_SPL_STACK
#else
ldr sp, =CONFIG_SYS_INIT_SP_ADDR
#endif
然后跳转到了s_init
* Save the old lr(passed in ip) and the current lr to stack
*/
push {ip, lr}
/*
* Call the very early init function. This should do only the
* absolute bare minimum to get started. It should not:
*
* - set up DRAM
* - use global_data
* - clear BSS
* - try to start a console
*
* For boards with SPL this should be empty since SPL can do all of
* this init in the SPL board_init_f() function which is called
* immediately after this.
*/
bl s_init
pop {ip, pc}
通过跟踪发现s_init其实什么都没有做,可以在arch/arm/mach-omap2/hwinit-common.c
文件中看到空实现。
void s_init(void)
{
}
返回到archarmcpuarmv7start.S
,然后执行了b _main
。该处才是重点:
直接跳转到了
archarmlibcrt0.S
函数处。crt0就是C runtime environment and call board_init_f(0)
的意思。
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)
ldr r0, =(CONFIG_TPL_STACK)
#elif defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr r0, =(CONFIG_SPL_STACK)
#else
ldr r0, =(CONFIG_SYS_INIT_SP_ADDR)
#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
mov r0, #0
bl board_init_f
代码分析到这里,就有一个非常值得注意的地方了,引入了一个名为global data的数据结构体。
当设置了sp指针之后,于是可以跳转到board_init_f_alloc_reserve
与board_init_f_init_reserve
这两个C语言的代码中去了。
其中board_init_f_alloc_reserve
申请了一个global data
也就是uboot中常见的gd全局结构体,该结构体记录着初始化的信息,同时可以进行spl与uboot与kernel的消息的共享传递。由于这段空间在ram中,并且是独立存在的,并不会受到其他程序的影响。
对于board_init_f_alloc_reserve
的具体实现可以看common/init/board_init.c
。
ulong board_init_f_alloc_reserve(ulong top)
{
/* Reserve early malloc arena */
#if CONFIG_VAL(SYS_MALLOC_F_LEN)
top -= CONFIG_VAL(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;
}
top为传递过来的pc指针,接着CONFIG_VAL(SYS_MALLOC_F_LEN)
是spl可以动态分配的内存,最后就是排列global data的区域了。
board_init_f_init_reserve
主要用于初始化global data
。这个没什么好说的。接着执行board_init_f
。需要注意的是,这个是与具体开发板的实现密切相关的,对于beaglebone ai来说,第一处位于archarmmach-omap2hwinit-common.c
是spl阶段使用的,第二处位于common/board_f.c
中,不同体系芯片均可使用,是通用代码。下面主要分析一下spl里的board_init_f
,看具体做了什么。
3.3 从board_init_f看驱动初始化
前面的体系架构初始化已经差不多了,接下来就和beaglebone ai密切相关了。
1.读取芯片ID信息
通过调用函数init_omap_revision
,设置CPU的状态
void init_cpu_configuration(void)
{
u32 l2actlr;
asm volatile("mrc p15, 1, %0, c15, c0, 0" : "=r"(l2actlr));
/*
* L2ACTLR: Ensure to enable the following:
* 3: Disable clean/evict push to external
* 4: Disable WriteUnique and WriteLineUnique transactions from master
* 8: Disable DVM/CMO message broadcast
*/
l2actlr |= 0x118;
omap_smc1(OMAP5_SERVICE_L2ACTLR_SET, l2actlr);
}
2.获取外设时钟频率信息
代码语言:javascript复制hw_data_init();
3.关闭看门狗wdt
通过设置WDT寄存器,来使得watchdog功能关闭。
4.设置各外设pll分频
5.设置时钟
6.保存初始化阶段各种启动参数
7.初始化SDRAM
3.4 代码重定位
得益于sram的大小有512KB,所以可以不用搬移uboot的代码到sdram中,直接在sram中执行是可行的。此时需要将代码重定位。
具体可以看arch/arm/lib/crt0.S
的实现细节。会跳转到arch/arm/lib/relocate.S
将代码重定位。这段代码挺有意思,后面再慢慢分析。uboot中有很多有趣的代码,可以慢慢阅读。接着就跳转到uboot的第二阶段执行去了。到这里spl分析执行完成。
4.beaglebone ai的led调试
单独拿出来作为一个章节,是因为在板子初期阶段,点灯调试法非常的有效。但是往往TI的芯片启动比较的复杂,此时借助点灯调试,可以很容易跟踪代码的执行流程。
开发提供了5个用户LED。
各LED的作用如下:
对于TI的芯片的GPIO编程,我是阅读了一段时间,才弄明白。由于《AM572X Techncal Reference Manual》手册有8000多页,阅读起来非常费劲,而且也是第一次接触这个芯片,使用起来要熟悉一段时间。
下面是收获的结果
1.gpio的模式配置(引脚复用功能)。
2.gpio的输入、输出、中断配置。
模式配置可以看第18章节《Control Module》。
比如我们配置引脚GPIO3_17,也就是D2。
首先需要配置模式为GPIO模式:
然后配置该引脚输出,可以参考第27章节《General-Purpose Interface》
1.设置控制寄存器
2.设置模式为输出
3.输出对应的引脚
完整的C代码如下
代码语言:javascript复制
void led_on(void)
{
int reg;
__raw_writel(0x2000E, (0x4A003520));//gpio3_15 pull_up
__raw_writel(0x2000E, (0x4A003528));//gpio3_17 pull_up
__raw_writel(0x0, (0x48057000 0x130));
reg = __raw_readl((0x48057000 0x134));
//output enabled
__raw_writel( (reg & 0xFFFD7FFF), (0x48057000 0x134));
__raw_writel((0x28000), (0x48057000 0x13C));//gpio3_15 & gpio3_17 set 1
}
将代码编译后,则可以看到两个led正常的亮起。可以通过点灯来判断程序的执行逻辑。
5.总结
本文主要通过分析beaglebone ai上运行uboot的spl的执行流程,来分析底层的一些初始化的逻辑,对于uboot的使用和对于了解ti am5729芯片的底层非常有用。同时介绍了一下ti芯片的gpio配置的方法。通过点灯调试,将跟踪到更加有效的汇编阶段的信息。