beaglebone ai底层原理分析:spl阶段

2020-11-25 11:51:07 浏览数 (1)

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.Sreset函数。

下面来简单分析一下该初始化流程:

函数一开始就跳转

代码语言: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地址处,也就是前面分析的那里。

代码语言:javascript复制
#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这里,未做任何事。

代码语言:javascript复制
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

代码语言:javascript复制
  * 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文件中看到空实现。

代码语言:javascript复制
void s_init(void)
{
}

返回到archarmcpuarmv7start.S,然后执行了b _main。该处才是重点:

直接跳转到了

archarmlibcrt0.S函数处。crt0就是C runtime environment and call board_init_f(0)的意思。

代码语言:javascript复制
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_reserveboard_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

代码语言:javascript复制
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的状态

代码语言:javascript复制
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配置的方法。通过点灯调试,将跟踪到更加有效的汇编阶段的信息。

0 人点赞