近日研究了一段时间的Eloader和U-boot的代码,大致分析了他们的启动的过程,并且将心得记录下来。
本文所讲述的启动内核代码分为两个阶段,是已经将代码分成两个不同的bin文件了:eloader.bin和u-boot.bin文件,eloader是用来启动u-boot的,然后u-boot就是我们所熟悉的启动linux的内核了。
首先先讲解一下eloader,其实我也搞不太清楚这个eloader和u-boot到底为什么要分开,而且eloader有没有给u-boot什么参数,慢慢研究吧!
一句一条的分析,知道的就会进行解释,不知道只能我们共同探讨了:
1. 首先来研究我们这个芯片的启动流程吧:
之前很多博客中写到过很多ARM芯片中的启动的过程,是可能直接将nand中的4K代码直接通过硬件方式,也就是所说的steppingstone,直接copy到RAM 0~4K处,然后开始在0x00000000处执行,还有的是直接在Nor-flash中执行,因为直接将nor-flash映射到0x00000000处直接执行。
但是此处的执行方式有一点点区别,这里并没有steppingstone,这里是将Serial Flash中0x3F0000~0x3FFFFF直接映射到0xFFFF0000~0xFFFFFFFF,然后程序就可以从0xFFFF0000开始执行了,就我自己的理解,就是在Serial Flash上直接运行的,因为我们这里所用的Serial Flash是直接在片上的,然而SDRAM不是在片上的,需要对它进行初始化之后才能使用,所以这里我认为就是直接在Serial Flash上直接进行运行的,而且对于CPU来说,Serial Flash上0x3F0000~0x3FFFFF,就是0xFFFF0000~0xFFFFFFFF。
那这样我们大家都同意程序运行的第一条程序是在0xFFFF0000处开始运行的,而且我会在后面会进行验证,此时我会将eloader.bin文件通过烧写器写到板子Serial flash0x3F0000处,也就是说程序运行的第一条程序就是运行eloader.bin的第一条指令。
2. 现在来分析源代码:
_TEXT_BASE:
.word TEXT_BASE
这里的TEXT_BASE在/Makefile中有定义
# entry point of text section
TEXT_BASE=0x0
_armboot_start:
.word _start
/*
* These are defined in the board-specific linker script.
*/
_bss_start:
.word __bss_start
_bss_end:
.word _end
上面程序中的几个参数是在/Nonsec_bl1.lds中有定义:
SECTIONS
{
. = 0x0;
. = ALIGN(4);
.text :
{
start.o (.text)
*(.text)
}
. = ALIGN(4);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
.got : { *(.got) }
. = ALIGN(4);
__bss_start = .;
.bss : { *(.bss) }
. = ALIGN(4);
_end = .;
}
然后继续往下分析,首先将CPU设置为
SVC32 mode,然CPU处于一个比较强大的模式,可以操控更多的资源
mrs r0, cpsr
bic r0, r0, #0x1f
orr r0, r0, #0xd3
msr cpsr,r0
mrc p15, 0, r0, c0, c0, 5 @get cpu id
and r0, r0, #0x3
后面也有很多关于协处理器的操作的。
ldr r1, =REG_RESET_STATUS
ldr r3, [r1]
tst r3, #0x10 @check if AP reset from suspend to memory status(STM)
bne continue
ldr r1, =REG_MCSBOOT_BASE
add r1, r1, r0, lsl #2
ldr r2, [r1]
cmp r2, #0 @ check if it's context restore
movne pc, r2
这里关于寄存器的设定,我可以找到它的寄存器在哪里,但是不知道作用是什么?
relocate: @ relocate itself to RAM
adr r0, _start @ r0 <- current position of code
ldr r1, _TEXT_BASE @ test if we run from flash or RAM
cmp r0, r1 @ don't reloc during debug
beq stack_setup
这里不会进行relocate,因为此时_start=_TEXT_BASE=0,它会直接跳转到stack_setup去
stack_setup:
ldr r0, _stack_start @ upper 128 KiB: relocated uboot
sub sp, r0, #12 @ leave 3 words for abort-stack
bic sp, sp, #7 @ 8-byte alignment for ABI compliance
mov r0, sp
ldr r1, =STACK_SIZE
sub r0, r0, r1
cps #18 @ change to irq mode
mov sp, r0
cps #19
关于_start_start和STACK_SIZE会在Start.S中有定义:
#define STACK_SIZE 0x10000
_stack_start:
.word 0x80000
这里为了调用C函数,设置简单的栈。可以同cps对cpsr寄存器进行设置,相当于用到了mrs指令。
clear_bss:
ldr r0, _bss_start @ find start of bss segment
ldr r1, _bss_end @ stop here
mov r2, #0x00000000 @ clear value
clbss_l:
str r2, [r0] @ clear BSS location
cmp r0, r1 @ are we at the end yet
add r0, r0, #4 @ increment clear index pointer
bne clbss_l @ keep clearing till at end
这里是为了初始化_bss段,bss段是存储的未初始化静态变量的,已经初始化的静态变量会存放在.data段中。
然后通过下面指令就跳转到main函数中:
ldr pc, _start_armboot @ jump to C code
_start_armboot:
.word main
相当于直接将,main函数的地址给到pc寄存器中,就会跳转到main函数了。
3. main函数
3.1 load_timing_table():
board_id = *(unsigned int*)BOARD_INFO_ADDR;
cpu_freq = *(unsigned int*)(BOARD_INFO_ADDR+4);
timing_tbl_size = *(unsigned int*)(BOARD_INFO_ADDR+8);
timing_tbl = (sdram_timing_t*)(BOARD_INFO_ADDR+0xc);
这里是直接在Serial flash中读入一些板级数据和初始化sdram的寄存器的地址和值?之前有人问过我,为什么这里还没有对SDRAM进行初始化,就可以读出SDRAM的值,这里不是SDRAM,我们将timing_table.bin烧录到的是Serial Flash0x3F8000中,不是SDRAM中。
然后将board_id写入到寄存器中:
writel(board_id, (void*)0xd839341c);
0xd8390000~0xd839ffff属于PMU里面的,然后0xd839341c是控制PMU 中MCBOOT (keep the address for system recover)
3.2利用timing_table.bin读出来的值对SDRAM进行初始化init_sdram_timing():
size = timing_tbl_size;
for (i=0; i<size; i++) {
<span style="white-space:pre"> </span>if ((timing_tbl[i].address < 0xd839d200) || (timing_tbl[i].address > 0xd839d2ff)) continue;
writel(timing_tbl[i].value, timing_tbl[i].address);
if (timing_tbl[i].delay) {
delay(timing_tbl[i].delay);
}
}
这里都是涉及到底层的SDRAM寄存器的操作,有对MPLL,DLL和MIU一些寄存器的设定。
/* enable peripheral clock */
writel(0xffffffff, (void *) 0xd8130250);
writel(0xffffffff, (void *) 0xd8130254);
这两句是设定clock enable register0和clock enable register1,0xd8130000~0xd813ffff是属于PMC(Power Management Controller)寄存器组的。
下面这一句完全不懂是什么?并且没有找到相关的技术手册:
/* now AXI fabric decoder not correct,
* close remap register this will open
* when final AXI fabric OK
*/
writel(0, (void *)0xd8120500);
3.3通过读出来的数据改变cpu的频率switch_cpu_freq();
3.4 handle_stm()这个函数完全不知道干了什么;
3.5 memory_auto_tune();这里是与MIU寄存器相关的设定
3.6 pmc_writel(pmc_readl(0x250) | (1<<2), 0x250):
这里是使能UART1 clock,是属于PMC的一部分。
3.7 初始化UART1 的GPIO口:
elite_gpio_pinmux_set(0);
elite_gpio_func_en(93, 0); //TXD
elite_gpio_func_en(94, 0); //RXD
elite_gpio_pullupdown_ctrl0_set(93, ELITE_PINCONF_PULL_UP); //TXD
elite_gpio_pullupdown_ctrl0_set(94, ELITE_PINCONF_PULL_DOWN); //RXD
第一句是pin-sharing selection register。
后面几句是设置UART1 TXD/RXD Signale GPIO Enable和UART1RXD/TXD Signal pull-up/down control0
3.8 串口初始化serial_init() -> serial_setbrg():
这里有一个很重要的结构体,就是pUart_Reg,它将关于串口寄存器的值都包含进来了:
typedef struct _UART_REG_ {
volatile unsigned long URTDR; /* Transmit data register */
volatile unsigned long URRDR; /* Receive data register */
volatile unsigned long URDIV; /* UART clock divisor & Bard rate divisor */
volatile unsigned long URLCR; /* Line control register */
volatile unsigned long URICR; /* IrDA control register */
volatile unsigned long URIER; /* Interrupt enable register */
volatile unsigned long URISR; /* Interrupt status register */
volatile unsigned long URUSR; /* Status register */
volatile unsigned long URFCR; /* Fifo control register */
volatile unsigned long URFIDX; /* Fifo index register */
volatile unsigned long URBKR; /* Break signal count value register */
volatile unsigned long URTOD; /* UART time out divisor register */
volatile unsigned long Resv30_FFF[0x3F4]; /* 0x0030 - 0x0FFF Reserved */
volatile unsigned char TXFifo[32]; /* 0x1000 - 0x101F 16x8 bit asynchronous Fifo */
volatile unsigned char RXFifo[32]; /* 0x1020 - 0x103F 16x10 bit asynchronous Fifo */
} UART_REG, *PUART_REG;
然后在serial_setbrg()这个函数里面就是这个结构体的值赋值就好了。
3.9 将u-boot.bin从Serial Flash拷贝到SDRAM中去load_os_image():
3.9.1 初始化SPI sf_init():
reg_base = ELITE_SPI_FLASH_REG_BASE;
writeb(0x00, (void *)(reg_base + SPI_RD_WR_CTRL));
phy_flash_base = 0xffffffff - SPI_FLASH_SIZE + 1;
for (i = 0; i < 32; i++)
{
if ((SPI_FLASH_SIZE >> i) == 0x8000)
phy_flash_size = i;
}
writel(phy_flash_base | (phy_flash_size << 8), (void *)(reg_base + CHIP_SEL_0_CFG));
writel(0x00000000, (void *)(reg_base + SPI_IF_CFG));
需要对SPI相关的寄存器进行操作,这里有人就会问为什么前面不需要初始化Serial Flash就可以用到里面的内容,因为这里是初始化SPI总线,是要将Serial Flash 与SDRAM联系起来或是传递数据用的。
3.9.2 将存储在Serial Flash中的u-boot.bin拷贝到SDRAM 0x3F80000处sf_read(OS_FLASH_OFFSET, OS_MEM_ADDR, OS_IMAGE_SIZE):
writel(0XFFFFFFFF, (void *)(reg_base + SPI_ERROR_STATUS));
writel(readl((void *)(reg_base + SPI_IF_CFG))|(1 << 6), (void *)(reg_base + SPI_IF_CFG));
while (len > 0)
{
if (len > 32)
burst_size = 32;
else
burst_size = len;
writeb(0x3, (void*)(reg_base + SPI_PROG_CMD_WBF));
writeb((offset>>16) & 0xff, (void*)(reg_base + SPI_PROG_CMD_WBF+1));
writeb((offset>>8) & 0xff, (void*)(reg_base + SPI_PROG_CMD_WBF+2));
writeb(offset & 0xff, (void*)(reg_base + SPI_PROG_CMD_WBF+3));
writel((0x4<<24)|(burst_size<<16)|(chip_index<<1)|0x1, (void*)(reg_base + SPI_PROG_CMD_CTRL));
while(readl((void *)(reg_base + SPI_ERROR_STATUS)) & (1 << 31));
/* wait data ready */
while (readl((void*)(reg_base + SPI_PROG_CMD_CTRL)) & (1 << 0));
for (i = 0; i < burst_size; i++)
*(unsigned char*)(buf + i) = readb((void*)(reg_base + SPI_PROG_CMD_RBF+i));
len -= burst_size;
offset += burst_size;
buf += burst_size;
}
writel(readl((void *)(reg_base + SPI_IF_CFG)) & ~(1 << 6), (void *)(reg_base + SPI_IF_CFG));
首先会传递命令0x03到SPI_PROG_CMD_WBF中,告诉它我要进行读命令了,然后继续传递地址到此命令中去,然后在从SPI_PROG_CMD_RBF中读出刚刚写进去的地址中的内容,一次性读取4个字节的内容。然后将这些值传递到SDRAM 0x3F80000处,进而完成了u-boot.bin的拷贝工作。
3.10 直接跳转到0x3F80000,进入到u-boot.bin中的第一条指令处:
#define OS_MEM_ADDR 0x3f80000
typedef void (*bl2)(void);
bl2 bl2_entry;
bl2_entry = (bl2*)OS_MEM_ADDR;
bl2_entry();
首先定义了一个函数指针,然后将这个指针的值赋予0x3F80000,然后直接调用这个指针函数,就相当于直接跳转到这个指针处,也就是0x3F80000。然后在此之前会遇到几行代码:
__asm__ volatile("dsb");
__asm__ volatile("isb");
这个是内嵌到C语言中的汇编代码,
dsb值指数据同步隔离,isb是指指令同步隔离,意思等待之前的数据和指令全部执行完毕之后,才能进行后面的操作;
/* turn off car mode */
*(volatile unsigned int*)REG_CAR_SETTING = 1;
这里这个寄存器有两种mode:car mode和normal mode
这个我不知道是干嘛用的?
3.11 验证eloader的代码运行地址ldr pc, _start_armboot @ jump to C code
在这条代码之前加入:
mov r0, pc
将pc当前的值,给到r0处,这时候r0就是调用main()函数传递进来的第一个参数,在main()函数中加入:
void main(unsigned int temp)
{
printf("Miles: temp = %x\n", temp);
}
这样就会将pc的值传递到temp,并打印出来,结果为0xFFFF0108,说明是直接运行在Serial Flash上的。