_main
来自于crt0.s函数的注释说明:
/*
* This file handles the target-independent stages of the U-Boot
* start-up where a C runtime environment is needed. Its entry point
* is _main and is branched into from the target's start.S file.
*
* _main execution sequence is:
*
* 1. Set up initial environment for calling board_init_f().
* This environment only provides a stack and a place to store
* the GD ('global data') structure, both located in some readily
* available RAM (SRAM, locked cache...). In this context, VARIABLE
* global data, initialized or not (BSS), are UNAVAILABLE; only
* CONSTANT initialized data are available.
* 建立 调用 board_init_f()函数 的初始化环境,主要是常量数据。
* 2. Call board_init_f(). This function prepares the hardware for
* execution from system RAM (DRAM, DDR...) As system RAM may not
* be available yet, , board_init_f() must use the current GD to
* store any data which must be passed on to later stages. These
* data include the relocation destination, the future stack, and
* the future GD location.
* 调用board_init_f(),PCB硬件的基本初始化,为下面的步骤提供数据临时存储空间。
* (the following applies only to non-SPL builds)
*
* 3. Set up intermediate environment where the stack and GD are the
* ones allocated by board_init_f() in system RAM, but BSS and
* initialized non-const data are still not available.
* 设置中间环境为stack和GD。
* 4. Call relocate_code(). This function relocates U-Boot from its
* current location into the relocation destination computed by
* board_init_f().
* U-boot代码的内存重定向。
* 5. Set up final environment for calling board_init_r(). This
* environment has BSS (initialized to 0), initialized non-const
* data (initialized to their intended value), and stack in system
* RAM. GD has retained values set by board_init_f(). Some CPUs
* have some work left to do at this point regarding memory, so
* call c_runtime_cpu_setup.
* 调用board_init_r(),完整最终环境的设置。
* 6. Branch to board_init_r().
跳转到board_init_r()函数。
_main函数在arch/arm/lib/crt0.S中,mian函数的作用在注释中有详细的说明,我们分段来分析一下
首先将CONFIG_SYS_INIT_SP_ADDR定义的值加载到栈指针sp中,这个宏定义在配置头文件中指定。
这段代码是为board_init_f C函数调用提供环境,也就是栈指针sp初始化。
8字节对齐,然后减掉GD_SIZE,这个宏定义是指的全局结构体gd的大小,是160字节。在此处,这个结构体用来保存uboot一些全局信息,需要一块单独的内存。
最后将sp保存在r9寄存器中。因此r9寄存器中的地址就是gd结构体的首地址。
在后面所有code中如果要使用gd结构体,必须在文件中加入DECLARE_GLOBAL_DATA_PTR宏定义,定义如下:
gd结构体首地址就是r9中的值。
C语言函数栈是向下生长,这里sp为malloc空间顶端减去gd bd空间开始的,起初很纳闷,sp设在这里,以后的C函数调用不都会在malloc空间了吗,堆栈空间不就重合了嘛,不用急,看完board_init_f就明白了。
接着说_main上面一段代码,接着r0赋为0,也就是参数0为0,调用board_init_f
board_init_f (函数在arch/arm/xxx/board.c位置)
移植uboot先做一个最精简版本,很多配置选项都没有打开,比如fb mmc等硬件都默认不打开,只配置基本的ddr serial,这样先保证uboot能正常启动进入命令行,然后再去添加其他。
我们这里分析就是按最精简版本来,这样可以更加简洁的说明uboot的启动流程。
board_init_f函数主要是根据配置对全局信息结构体gd进行初始化。
gd结构体中有个别成员意义我也不是很理解,这里我只说我理解并且在后面起到作用的成员。
初始化mon_len,代表uboot code的大小。
遍历调用init_sequence所有函数,init_sequence定义如下:
arch_cpu_init需要实现,要先启动uboot,这里可以先写一个空函数。
timer_init在lib/time.c中有实现,也是空函数,但是有__WEAK关键字,如果自己实现,则会调用自己实现的这个函数
对最精简uboot,需要做好就是ddr和serial,所以我们最关心是serial_init,console_init_f以及dram_init.
先看serial_init
gd->flags还没做初始化,serial_current用来存放我们当前要使用的serial,这里也还没做初始化,所以最终serial_device就是default_serial_console(),这个在serial驱动中有实现,来返回一个默认的调试串口。
serial_device结构体代表了一个串口设备,其中的成员都需要在自己的serial驱动中实现。
这样在serial_init中get_current获取就是串口驱动中给出的默认调试串口结构体,执行start,做一些特定串口初始化。
console_init_f将gd中have_console置1,这个函数不详细说了。
display_banner,print_cpuinfo利用现在的调试串口打印了uboot的信息。
接下来就是dram_init。
dram_init对gd->ram_size初始化,以便board_init_f后面代码对dram空间进行规划。
dram_init实现可以通过配置文件定义宏定义来实现,也可以通过对ddrc控制器读获取dram信息。
继续分析board_init_f,剩余代码将会对sdram空间进行规划!
CONFIG_SYS_MEM_TOP_HIDE宏定义是将一部分内存空间隐藏,注释说明对于ppc处理器在内核中有接口来实现使用uboot提供的值,这里咱们不考虑。
addr的值由CONFIG_SYS_SDRAM_BASE加上ram_size。也就是到了可用sdram的顶端。
如果打开了icache以及dcache,则预留出PATABLE_SIZE大小的tlb空间,tlb存放首地址赋值给gd->arch.tlb_addr。
最后addr此时值就是tlb的地址,4kB对齐。
如果需要使用frambuffer,使用配置fb首地址CONFIG_FB_ADDR或者调用lcd_setmem获取fb大小,这里面有板级相关函数需要实现,不过为了先能启动uboot,没有打开fb选项。addr值就是fb首地址。
gd->fb_base保存fb首地址。
接着-gd->mon_len为uboot的code留出空间,到这里addr的值就确定,addr作为uboot relocate的目标addr。
到这里,可以看出uboot现在空间划分是从顶端往下进行的。
先总结一下addr之上sdram空间的划分:
由高到低 : top-->hide mem-->tlb space(16K)-->framebuffer space-->uboot code space-->addr
接下来要确定addr_sp的值。
首先预留malloc len,这里我定义的是0x400000.
注释中说明,为bd,gd做一个永久的copy。
留出了全局信息bd_t结构体的空间,首地址存在gd->bd。
留出gd_t结构体的空间。首地址存在id中。
将此地址保存在gd->irq_sp中作为异常栈指针。uboot中我们没有用到中断。
最后留出12字节,for abort stack,这个没看懂。
到这里addr_sp值确定,总结一下addr_sp之上空间分配。
由高到低 : addr-->malloc len(0x400000)-->bd len-->gd len-->12 byte-->addr_sp(栈往下增长,addr_sp之下空间作为栈空间)
给bd->bi_baudrate赋值gd->baudrate,gd->baudrate是在前面baudrate_init中初始化。
dram_init_banksize()是需要实现的板级函数。根据板上ddrc获取ddr的bank信息。填充在gd->bd->bi_dram[CONFIG_NR_DRAM_BANKS]。
gd->relocaaddr为目标addr,gd->start_addr_sp为目标addr_sp,gd->reloc_off为目标addr和现在实际code起始地址的偏移。reloc_off非常重要,会作为后面relocate_code函数的参数,来实现code的拷贝。
最后将gd结构体的数据拷贝到新的地址id上。
board_init_f函数将sdram空间重新进行了划分,可以看出栈空间和堆空间是分开的,就不存在_main调用board_init_f之前的那个问题啦。
并且在重新规划空间完成之前并没有出现初始化堆,以及使用堆空间的问题,比如malloc函数,所以之前的堆栈空间重合的问题是过虑了。
至此,board_init_f结束,回到_main
_main
board_init_f结束后,代码如下:
这段汇编很有意思,前4条汇编实现了新gd结构体的更新。
首先更新sp,并且将sp 8字节对齐,方便后面函数开辟栈能对齐,
然后获取gd->bd地址到r9中,需要注意,在board_init_f中gd->bd已经更新为新分配的bd了,下一条汇编将r9减掉bd的size,这样就获取到了board_init_f中新分配的gd了!
后面汇编则是为relocate_code做准备,首先加载here地址,然后加上新地址偏移量给lr,则是code relocate后的新here了,relocate_code返回条转到lr,则是新位置的here!
最后在r0中保存code的新地址,跳转到relocate_code
relocate_code
relocate_code函数在arch/arm/lib/relocate.S中,这个函数实现了将uboot code拷贝到relocaddr。
这部分算是整个uboot中最核心也是最难理解的代码,我单独写了一篇文章来介绍这一部分的工作原理,感兴趣的朋友可以看下面这个链接
http://blog.csdn.net/skyflying2012/article/details/37660265
这里就不再详说了。
到这里需要总结一下,经过上面的分析可以看出,
新版uboot在sdram空间分配上,是自顶向下,
不管uboot是从哪里启动,spiflash,nandflash,sram等跑到这里code都会被从新定位到sdram上部的一个位置,继续运行。
我找了一个2010.6版本的uboot大体看了一下启动代码,是通过判断_start和TEXT_BASE(链接地址)是否相等来确定是否需要relocate。如果uboot是从sdram启动则不需要relocate。
新版uboot在这方面还是有较大变动。
这样变动我考虑好处可能有二,一是不用考虑启动方式,all relocate code。二是不用考虑uboot链接地址,因为都要重新relocate。
uboot sdram空间规划图:
_main
从relocate_code回到_main中,接下来是main最后一段代码,如下:
首先跳转到c_runtime_cpu_setup,如下:
如果icache是enable,则无效掉icache,保证从sdram中更新指令到cache中。
接着更新异常向量表首地址,因为code被relocate,所以异常向量表也被relocate。
从c_runtime_cpu_setup返回,下面一段汇编是将bss段清空。
接下来分别调用了coloured_LED_init以及red_led_on,很多开发板都会有led指示灯,这里可以实现上电指示灯亮,有调试作用。
最后r0赋值gd指针,r1赋值relocaddr,进入最后的board_init_r !
board_init_r
参数1是新gd指针,参数2是relocate addr,也就是新code地址
置位gd->flags,标志已经relocate。monitor_flash_len这个变量作用没看懂。使能cache,最后board_init是需要实现的板级支持函数。做开发板的基本初始化。
如果打开CONFIG_CLOCKS,set_cpu_clk_info也是需要实现的板级支持函数。
重点来说一些serial_initialize,对于最精简能正常启动的uboot,serial和ddr是必须正常工作的。
实现在drivers/serial/serial.c中,如下:
所有串口驱动都会实现一个xxxx_serial_initialize函数,并且添加到serial_initialize中, xxxx_serial_initialize函数中是将所有需要的串口(用结构体struct serial_device表示,其中实现了基本的收 发 配置)调用serial_register注册,serial_register如下:
可以想象,如果你有4个串口,则再你的串口驱动中分别定义4个serial device,并实现对应的收发配置,然后serial_register注册者4个串口。
回到serial-initialize,最后调用serial_assign,default_serial_console我们之前说过,就是你在串口驱动给出一个默认调试串口,serial_assign如下:
serial_assign就是从serial_devices链表中找到指定的默认调试串口,条件就是串口的name,最后serial_current就是当前的默认串口了。
总结一下,serial_initialize工作是将所有serial驱动中所有串口注册到serial_devices链表中,然后找到指定的默认串口。
根据咱们之前board_init_f中的分析,relocate addr之下的部分就是malloc的预留空间了。这里获取malloc首地址malloc_start.
mem_malloc_init中就是对malloc预留的空间初始化,起始地址,结束地址,清空。咱们已经relocate,malloc_bin_reloc中无操作了。
board_init_r接下来的代码是做一些外设的初始化,比如mmc flash eth,环境变量的设置,还有中断的使能等,这里需要说一下是关于串口的2个函数,stdio_init和console_init_r.
看stdio_init代码,我们只定义了serial,会调到serial_stdio_init,如下:
将serial_devices链表上所有serial device同样初始化一个stdio_dev,flag为output input,调用stdio-register,将stdio_dev添加到全局devs链表中。
可以想象,serial_stdio_init是在drivers/serial/serial.c中实现,uboot在这里是利用的内核分层思想,drivers/serial下是特定serial驱动,分别调用serial_register注册到serial_devices中,这可以说是通用的serial驱动层,
通用serial层调用serial-stdio-init将所有serial注册到stdio device中,这就是通用的stdio层。
看来分层思想还是非常重要的!
board_init_r中调用完stdio_init后又调用了console_init_r,如下
console_init_r前半部分很清楚了,从devs.list链表中查找flag为output或者input的dev,如果只有serial之前注册了stdio_dev,则outputdev inputdev都是咱们注册的第一个serial。
之后调用console_setfile,如下:
首先运行设备的start,就是特定serial实现的start函数。然后将stdio_device放到stdio_devices全局数组中,这个数组3个成员,stdout,stderr,stdin。最后还会在gd中设一下操作函数。
在console_init_r中最后会改变gd中flag状态,为GD_FLG_DEVINIT。表示设备初始化完成。
board_init_r进行完板级初始化后最后进入死循环,打印命令行,等待命令输入和解析。到这里uboot的启动过程就全部结束了!
上面用很大篇幅自下往上解释uboot下serial到console的架构,那来看一下实际使用时由printf到最后serial输出这个自上到下的流程吧。
首先来看printf,实现在common/console.c中如下:
字符串的拼接跟一般printf实现一样,最后调用puts,puts实现也在console.c中,如下:
gd->have_console在board_init_f的console_init_f中置位,flag的GD_FLG_DEVINIT则是在刚才board_init_r中console_init_r最后置位。
如果GD_FLG_DEVINIT没有置位,表明console没有注册,是在board_init_f之后,board_init_r执行完成之前,这时调用serial_puts,如下:
直接调到serial.c中的函数,完全符合board_init_f中serial_init的配置,仅仅找到一个默认串口来使用,其他串口暂且不管。
如果GD_FLG_DEVINIT置位,表明console注册完成。调用fputs,如下:
fputs调console_puts从全局stdio_devices中找到对应stdout对应的成员stdio_device,调用puts,最终也是会调用到特定serial的puts函数。
分析后总结一下:
可以看出,对于serial,uboot实现了一个2级初始化:
stage 1,仅初始化default console serial,printf到puts后会直接调用特定串口的puts函数,实现打印
stage 2,将所有serial注册为stdio_device,并挑出指定调试串口作为stdio_devices的stdout stdin stderr。printf到puts后再到全局stdio_devices中找到对应stdio_device,调用stdio-device的puts,最终调用特定serial的puts,实现打印。
区分这2个stage,是利用gd的flag,GD_FLG_DEVINIT。
原文出处:
http://blog.csdn.net/skyflying2012/article/details/25804209