I recently started to use uboot, and now I need to port the 2014.4 version of uboot to the company's armv7 development board.
When searching online for articles about the uboot startup process, most of them are about older versions of uboot, so I decided to record the new version of uboot startup process and share it with everyone.
For uboot, I wrote a column to record some of my understanding. Friends who are interested can click the following link:
u-boot study notes
This is a hard-earned work, let’s share it with everyone. Please indicate the source when reprinting!
Author : kerneler
Email :karse0104@163.com
-------------------------------------------------------------------------
# (C) Copyright 2000-2013
# Wolfgang Denk, DENX Software Engineering, wd@denx.de.
#
# SPDX-License-Identifier: GPL-2.0+
VERSION = 2014
PATCHLEVEL = 04
SUBLEVEL =
EXTRAVERSION =
NAME =
-------------------------------------------------------------------------
As of the time I wrote this article, this version of uboot is the latest version.
The 2014.4 version of uboot starts to the command line and the important functions are: _start, _main, board_init_f, relocate_code, board_init_r.
1_start
For any program, the entry function is determined at link time, and the entry of uboot is determined by the link script. The default directory of the armv7 link script under uboot is arch/arm/cpu/u-boot.lds. This can be specified in the configuration file with CONFIG_SYS_LDSCRIPT.
The entry address is also determined by the connector and can be specified in the configuration file by CONFIG_SYS_TEXT_BASE. This will be added to the -Ttext option of the ld connector during compilation.
The configuration compilation principle of uboot is also worth learning. I would like to write another article to record it, so I won’t go into details here.
View u-boot.lds
----------------------------------------------------------------------------
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x00000000;
. = ALIGN(4);
.text :
{
*(.__image_copy_start)
CPUDIR/start.o (.text*)
*(.text*)
}
. = ALIGN(4);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
. = ALIGN(4);
.data : {
*(.data*)
}
----------------------------------------------------------------------------
The definitions of these macros in the linker script are in linkage.h. As the name suggests, the entry point of the program is _start., followed by the text segment, data segment, etc.
_start is in arch/arm/cpu/armv7/start.S, and the analysis is as follows:
------------------------------------------------------------------------------
.globl _start
_start: b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
#ifdef CONFIG_SPL_BUILD
_undefined_instruction: .word _undefined_instruction
_software_interrupt: .word _software_interrupt
_prefetch_abort: .word _prefetch_abort
_data_abort: .word _data_abort
_not_used: .word _not_used
_irq: .word _irq
_fiq: .word _fiq
_pad: .word 0x12345678 // now 16*4=64
#else
.globl _undefined_instruction
_undefined_instruction: .word undefined_instruction
.globl _software_interrupt
_software_interrupt: .word software_interrupt
.globl _prefetch_abort
_prefetch_abort: .word prefetch_abort
.globl _data_abort
_data_abort: .word data_abort
.globl _not_used
_not_used: .word not_used
.globl _race
_irq: .word irq
.globl_fiq
_fiq: .word fiq
_pad: .word 0x12345678 // now 16*4=64
#endif // CONFIG_SPL_BUILD
.global _end_vect
_end_vect:
.balignl 16,0xdeadbeef
------------------------------------------------------------------------------
.global declares _start as a global symbol, and _start will be linked to by the connector, which is the entry address in the link script.
The above code sets the arm exception vector table. The arm exception vector table is as follows:
address | abnormal | Enter Mode | describe |
0x00000000 | Reset | Management Model | When the reset level is valid, a reset exception occurs and the program jumps to the reset handler for execution. |
0x00000004 | Undefined instruction | Undefined mode | When an instruction that cannot be processed is encountered, an undefined instruction exception is generated |
0x00000008 | Software interrupts | Management Model | Execute SWI instruction to generate privileged operation instructions for programs in user mode |
0x0000000c | Stored Instructions | Abort Mode | The address of the processor prefetch instruction does not exist, or the address does not allow the current instruction to access, resulting in an instruction prefetch abort exception. |
0x00000010 | Data Operations | Abort Mode | When the address of the processor data access instruction does not exist or the address does not allow the current instruction to access, a data abort exception is generated. |
0x00000014 | Unused | Unused | Unused |
0x00000018 | IRQ | IRQ | When the external interrupt request is valid and the I bit in the CPSR is 0, an IRQ exception is generated. |
0x0000001c | FIQ | FIQ | When the fast interrupt request pin is valid and the F bit in the CPSR is 0, an FIQ exception is generated |
The 8 exceptions each occupy 4 bytes, so a jump instruction is filled in at the entrance of each exception, jumping directly to the corresponding exception handling function. The reset exception jumps directly to the reset function, and the other 7 exceptions use ldr to load the processing function entry address into pc.
The following assembly defines seven exception entry functions. CONFIG_SPL_BUILD is not defined here, so the latter one is used.
Next, in the definition of _end_vect, .balignl is used to specify that the following code should be aligned to 16 bytes, and 0xdeadbeef is used for the missing bytes, so as to facilitate more efficient memory access. Next, analyze the following code
------------------------------------------------------------
#ifdef CONFIG_USE_IRQ
// IRQ stack memory (calculated at run-time)
.globl IRQ_STACK_START
IRQ_STACK_START:
.word 0x0badc0de
// IRQ stack memory (calculated at run-time)
.globl FIQ_STACK_START
FIQ_STACK_START:
.word 0x0badc0de
#endif
// IRQ stack memory (calculated at run-time) + 8 bytes
.globl IRQ_STACK_START_IN
IRQ_STACK_START_IN:
.word 0x0badc0de
-----------------------------------------------------------
If interrupts are used in uboot, the starting address of the interrupt handling function stack is declared here. The value given here is 0x0badc0de, which is an illegal value. The comment also shows that this value will be recalculated at runtime. I found that the code is in interrupt_init.
-------------------------------------------------------------
reset:
bl save_boot_params
// disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
// except if in HYP mode already
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
-------------------------------------------------------------
After power-on or restart, the first instruction the processor gets is b reset, so it will jump directly to the reset function. Reset first jumps to save_boot_params, as follows:
/
ENTRY(cpu_init_cp15)
//
// Invalidate L1 I/D
//
mov r0, #0 @ set up for MCR
mcr p15, 0, r0, c8, c7, 0 @ invalidate TLBs
mcr p15, 0, r0, c7, c5, 0 @ invalidate icache
mcr p15, 0, r0, c7, c5, 6 @ invalidate BP array
mcr p15, 0, r0, c7, c10, 4 @ DSB
mcr p15, 0, r0, c7, c5, 4 @ ISB
//
// disable MMU stuff and caches
//
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002000 @ clear bits 13 (--V-)
bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM)
orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align
orr r0, r0, #0x00000800 @ set bit 11 (Z---) BTB
#ifdef CONFIG_SYS_ICACHE_OFF
bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache
#else
orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache
#endif
mcr p15, 0, r0, c1, c0, 0
#ifdef CONFIG_ARM_ERRATA_716044
mrc p15, 0, r0, c1, c0, 0 @ read system control register
orr r0, r0, #1 << 11 @ set bit #11
mcr p15, 0, r0, c1, c0, 0 @ write system control register
#endif
#if (defined(CONFIG_ARM_ERRATA_742230) || defined(CONFIG_ARM_ERRATA_794072))
mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register
orr r0, r0, #1 << 4 @ set bit #4
mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register
#endif
#ifdef CONFIG_ARM_ERRATA_743622
mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register
orr r0, r0, #1 << 6 @ set bit #6
mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register
#endif
#ifdef CONFIG_ARM_ERRATA_751472
mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register
orr r0, r0, #1 << 11 @ set bit #11
mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register
#endif
#ifdef CONFIG_ARM_ERRATA_761320
mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register
orr r0, r0, #1 << 21 @ set bit #21
mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register
#endif
mov pc, lr @ back to my caller
ENDPROC(cpu_init_cp15)
------------------------------------------------------------------------
The cpu_init_cp15 function configures the cp15 coprocessor related registers to set the processor's MMU, cache and tlb. If CONFIG_SYS_ICACHE_OFF is not defined, icache will be turned on. MMU and tlb will be turned off.
The specific configuration process can be compared with the cp15 registers, which will not be described in detail here.
Next, look at cpu_init_crit
--------------------------------------------------------------------------
/
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)
--------------------------------------------------------------------------
From the comments, we can understand that the lowlevel_init function called by cpu_init_crit is an initialization function related to a specific development board. In this function, some pll initialization will be done. If it is not started from mem, memory initialization will be done to facilitate subsequent copying to mem for operation.
The lowlevel_init function needs to be transplanted to implement clk initialization and ddr initialization
After returning from cpu_init_crit, the work of _start is completed, and then _main is called. Let's summarize the work of _start:
1 The part summarized above, initialize the exception vector table, set the svc mode, and disable interrupts
2 Configure cp15 and initialize mmu cache tlb
3 Board level initialization, pll memory initialization
2_main
The _main function is in arch/arm/lib/crt0.S. The function of the main function is explained in detail in the comments. Let's analyze it in sections.
------------------------------------------------------------------------------
ENTRY(_main)
// Set up initial C runtime environment and call board_init_f(0).
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr sp, =(CONFIG_SPL_STACK)
#else
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
bic sp, sp, #7 // 8-byte alignment for ABI compliance
sub sp, sp, #GD_SIZE // allocate one GD above SP
bic sp, sp, #7 // 8-byte alignment for ABI compliance
mov r9, sp // GD is above SP
mov r0, #0
bl board_init_f
----------------------------------------------------------------------------
First, load the value defined by CONFIG_SYS_INIT_SP_ADDR into the stack pointer sp. This macro definition is specified in the configuration header file.
This code provides the environment for the board_init_f C function call, that is, the stack pointer sp is initialized
8-byte alignment, then minus GD_SIZE, this macro definition refers to the size of the global structure gd, which is 160 bytes here. This structure is used to store some global information of uboot and requires a separate memory.
Finally, sp is saved in the r9 register. Therefore, the address in the r9 register is the first address of the gd structure.
If you want to use the gd structure in all the following codes, you must add the DECLARE_GLOBAL_DATA_PTR macro definition to the file, as follows:
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r9")
view plain cop
The first address of the gd structure is the value in r9.
The C language function stack grows downward, where sp is the top of the malloc space minus the gd bd space. At first I was puzzled. If sp is set here, then all future C function calls will be in the malloc space, and the stack space will overlap. Don't worry, you will understand after reading board_init_f.
Next, let's talk about the code above _main. Then r0 is assigned to 0, that is, parameter 0 is 0, and board_init_f is called
3.board_init_f
When porting uboot, make a minimal version first. Many configuration options are not turned on. For example, hardware such as fb mmc are not turned on by default. Only the basic ddr serial is configured. This ensures that uboot can start normally and enter the command line before adding others.
Our analysis here is based on the most streamlined version, so that we can explain the uboot startup process more concisely.
The board_init_f function mainly initializes the global information structure gd according to the configuration.
I don't quite understand the meaning of some of the members in the gd structure. Here I will only talk about the members that I understand and that play a role later.
gd->mon_len = (head)&__bss_end - (head)_start;
Initialize mon_len, which represents the size of the uboot code.
------------------------------------------------------------------------------
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
-------------------------------------------------------------------------------
Traverse and call all functions of init_sequence. The definition of init_sequence is as follows:
------------------------------------------------------------------------------
init_fnc_t *init_sequence[] = {
arch_cpu_init, // basic arch cpu dependent setup
mark_bootstage,
#ifdef CONFIG_OF_CONTROL
fdtdec_check_fdt,
#endif
#if defined(CONFIG_BOARD_EARLY_INIT_F)
board_early_init_f,
#endif
timer_init, // initialize timer
#ifdef CONFIG_BOARD_POSTCLK_INIT
board_postclk_init,
#endif
#ifdef CONFIG_FSL_ESDHC
get_clocks,
#endif
env_init, // initialize environment
init_baudrate, // initialze baudrate settings
serial_init, // serial communications setup
console_init_f, // stage 1 init of console
display_banner, // say that we are here
print_cpuinfo, // display cpu info (and speed)
#if defined(CONFIG_DISPLAY_BOARDINFO)
checkboard, // display board info
#endif
#if defined(CONFIG_HARD_I2C) || defined(CONFIG_SYS_I2C)
init_func_i2c,
#endif
dram_init, // configure available RAM banks
NULL,
};
------------------------------------------------------------------------------
arch_cpu_init needs to be implemented. To start uboot first, you can write an empty function here.
timer_init is implemented in lib/time.c. It is also an empty function, but it has the __WEAK keyword. If you implement it yourself, it will call this function you implemented.
For the most streamlined uboot, what needs to be done well is ddr and serial, so we are most concerned about serial_init, console_init_f and dram_init.
First look at serial_init
------------------------------------------------------------------------------
int serial_init(void)
{
return get_current()->start();
}
static struct serial_device *get_current(void)
{
struct serial_device *dev;
if (!(gd->flags & GD_FLG_RELOC))
dev = default_serial_console();
else if (!serial_current)
dev = default_serial_console();
else
dev = serial_current;
// We must have a console device
if (!dev) {
#ifdef CONFIG_SPL_BUILD
puts("Cannot find console\n");
hang();
#else
panic("Cannot find console\n");
#endif
}
return dev;
}
----------------------------------------------------------------------------------------------
gd->flags has not been initialized yet, serial_current is used to store the serial we are currently using, and it has not been initialized here either, so the final serial_device is default_serial_console(), which is implemented in the serial driver to return a default debugging serial port.
The serial_device structure represents a serial port device, and its members need to be implemented in your own serial driver.
In this way, get_current in serial_init obtains the default debugging serial port structure given in the serial port driver, executes start, and performs some specific serial port initialization.
console_init_f sets have_console in gd to 1. This function will not be described in detail.
display_banner,print_cpuinfo uses the current debugging serial port to print the uboot information.
Next is dram_init.
dram_init initializes gd->ram_size so that the code after board_init_f can plan the dram space.
The dram_init implementation can be achieved by defining macro definitions in the configuration file, or by reading the ddrc controller to obtain dram information.
Continue to analyze board_init_f, the remaining code will plan the sdram space!
-------------------------------------------------------------------
#if defined(CONFIG_SYS_MEM_TOP_HIDE)
//
* Subtract specified amount of memory to hide so that it won't
* get "touched" at all by U-Boot. By fixing up gd->ram_size
* the Linux kernel should now get passed the now "corrected"
* memory size and won't touch it either. This should work
* for arch/ppc and arch/powerpc. Only Linux board ports in
* arch/powerpc with bootwrapper support, that recalculate the
* memory size from the SDRAM controller setup will have to
* get fixed.
gd->ram_size -= CONFIG_SYS_MEM_TOP_HIDE;
#endif
addr = CONFIG_SYS_SDRAM_BASE + get_effective_memsize();
-------------------------------------------------------------------
The CONFIG_SYS_MEM_TOP_HIDE macro definition hides part of the memory space. The comment indicates that there is an interface in the kernel for the PPC processor to use the value provided by uboot, which we will not consider here.
The value of addr is CONFIG_SYS_SDRAM_BASE plus ram_size, which is the top of the available sdram.
--------------------------------------------------------------------
#if !(defined(CONFIG_SYS_ICACHE_OFF) && defined(CONFIG_SYS_DCACHE_OFF))
// reserve TLB table
gd->arch.tlb_size = PGTABLE_SIZE;
addr -= gd->arch.tlb_size;
// round down to next 64 kB limit
addr &= ~(0x10000 - 1);
gd->arch.tlb_addr = addr;
debug("TLB table from lx to lx\n", addr, addr + gd->arch.tlb_size);
#endif
// round down to next 4 kB limit
addr &= ~(4096 - 1);
debug("Top of RAM usable for U-Boot at: lx\n", addr);
---------------------------------------------------------------------
If icache and dcache are turned on, a tlb space of PATABLE_SIZE is reserved, and the first address of tlb storage is assigned to gd->arch.tlb_addr.
Finally, the value of addr is the address of tlb, which is 4kB aligned.
--------------------------------------------------------------------------------------
#ifdef CONFIG_LCD
#ifdef CONFIG_FB_ADDR
gd->fb_base = CONFIG_FB_ADDR;
#else
// reserve memory for LCD display (always full pages)
addr = lcd_setmem(addr);
gd->fb_base = addr;
#endif // CONFIG_FB_ADDR
#endif // CONFIG_LCD
// reserve memory for U-Boot code, data & bss
// round down to next 4 kB limit
addr -= gd->mon_len;
addr &= ~(4096 - 1);
debug("Reserving %ldk for U-Boot at: lx\n", gd->mon_len >> 10, addr);
--------------------------------------------------------------------------------------
If you need to use frambuffer, use configuration fb first address CONFIG_FB_ADDR or call lcd_setmem to get fb size. There are board-level related functions that need to be implemented, but in order to start uboot first, the fb option is not turned on. The addr value is the fb first address.
gd->fb_base stores the first address of fb.
Then -gd->mon_len reserves space for the uboot code. At this point, the value of addr is determined, and addr is used as the target addr of uboot relocate.
At this point, we can see that the uboot space is now divided from the top to the bottom.
First, let's summarize the division of sdram space above addr:
From high to low: top-->hide mem-->tlb space(16K)-->framebuffer space-->uboot code space-->addr
Next, we need to determine the value of addr_sp.
---------------------------------------------------------------------------------------
#ifndef CONFIG_SPL_BUILD
//
* reserve memory for malloc() arena
addr_sp = addr - TOTAL_MALLOC_LEN;
debug("Reserving %dk for malloc() at: lx\n",
TOTAL_MALLOC_LEN >> 10, addr_sp);
//
* (permanently) allocate a Board Info struct
* and a permanent copy of the "global" data
addr_sp -= sizeof (bd_t);
bd = (bd_t *) addr_sp;
gd->bd = bd;
debug("Reserving %zu Bytes for Board Info at: lx\n",
sizeof (bd_t), addr_sp);
#ifdef CONFIG_MACH_TYPE
gd->bd->bi_arch_number = CONFIG_MACH_TYPE; // board id for Linux
#endif
addr_sp -= sizeof (gd_t);
id = (gd_t *) addr_sp;
debug("Reserving %zu Bytes for Global Data at: lx\n",
sizeof (gd_t), addr_sp);
#ifndef CONFIG_ARM64
// setup stackpointer for exeptions
gd->irq_sp = addr_sp;
#ifdef CONFIG_USE_IRQ
addr_sp -= (CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ);
debug("Reserving %zu Bytes for IRQ stack at: lx\n",
CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ, addr_sp);
#endif
// leave 3 words for abort-stack
addr_sp -= 12;
// 8-byte alignment for ABI compliance
addr_sp &= ~0x07;
#else // CONFIG_ARM64
// 16-byte alignment for ABI compliance
addr_sp &= ~0x0f;
#endif // CONFIG_ARM64
---------------------------------------------------------------------------------------
First, reserve malloc len, which I define as 0x400000.
The comments state that a permanent copy is made for bd and gd.
Space is reserved for the global information bd_t structure, with the first address stored in gd->bd.
Leave space for the gd_t structure. The first address is stored in id.
Save this address in gd->irq_sp as the exception stack pointer. We do not use interrupts in uboot.
Finally, 12 bytes are reserved for abort stack, which I don’t understand.
At this point, the value of addr_sp is determined, and the space allocation above addr_sp is summarized.
From high to low: addr-->malloc len(0x400000)-->bd len-->gd len-->12 byte-->addr_sp (the stack grows downward, and the space below addr_sp is used as stack space)
-----------------------------------------------------------------------
gd->bd->bi_baudrate = gd->baudrate;
// Ram ist board specific, so move it to board code ...
dram_init_banksize();
display_dram_config(); // and display it
gd->relocaddr = addr;
gd->start_addr_sp = addr_sp;
gd->reloc_off = addr - (ulong)&_start;
debug("relocation Offset is: lx\n", gd->reloc_off);
if (new_fdt) {
memcpy(new_fdt, gd->fdt_blob, fdt_size);
gd->fdt_blob = new_fdt;
}
memcpy(id, (void *)gd, sizeof(gd_t));
-----------------------------------------------------------------------
Assign gd->baudrate to bd->bi_baudrate, gd->baudrate was initialized in baudrate_init above.
dram_init_banksize() is a board-level function that needs to be implemented. Get the bank information of ddr according to the ddrc on the board. Fill it in gd->bd->bi_dram[CONFIG_NR_DRAM_BANKS].
gd->relocaaddr is the target addr, gd->start_addr_sp is the target addr_sp, gd->reloc_off is the offset between the target addr and the actual code start address. reloc_off is very important and will be used as a parameter of the relocate_code function later to copy the code.
Finally, copy the data of the gd structure to the new address id.
The board_init_f function re-divides the sdram space. It can be seen that the stack space and the heap space are separated, so there is no problem before _main calls board_init_f.
And before the space is replanned, there is no problem of initializing the heap and using the heap space, such as the malloc function, so the previous problem of stack space overlap is an over-concern.
At this point, board_init_f ends and returns to _main
Four_main
After board_init_f is finished, the code is as follows:
-------------------------------------------------------------------------
#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 sp, [r9, #GD_START_ADDR_SP] // sp = gd->start_addr_sp
bic sp, sp, #7 // 8-byte alignment for ABI compliance
ldr r9, [r9, #GD_BD] // r9 = gd->bd
sub r9, r9, #GD_SIZE // new GD is below bd
adr lr, here
ldr r0, [r9, #GD_RELOC_OFF] // r0 = gd->reloc_off
add lr, lr, r0
ldr r0, [r9, #GD_RELOCADDR] // r0 = gd->relocaddr
b relocate_code
here:
-------------------------------------------------------------------------
This assembly is very interesting. The first four assembly lines implement the update of the new gd structure.
First, update sp and align sp to 8 bytes to facilitate the alignment of the stack opened by the subsequent functions.
Then get the gd->bd address to r9. Note that gd->bd has been updated to the newly allocated bd in board_init_f. The next assembly line will subtract the size of bd from r9, so that the newly allocated gd in board_init_f is obtained!
The subsequent assembly is to prepare for relocate_code. First, the here address is loaded, and then the new address offset is added to lr, which is the new here after code relocate. The relocate_code return bar goes to lr, which is the here of the new location!
Finally, save the new address of code in r0 and jump to relocate_code
五 relocate_code
The relocate_code function is in arch/arm/lib/relocate.S. This function implements copying the uboot code to relocaddr.
This part is the most core and the most difficult to understand code in the entire uboot. I wrote a separate article to introduce the working principle of this part. Friends who are interested can see the following link
http://blog.csdn.net/skyflying2012/article/details/37660265
I won’t go into details here.
We need to summarize here. From the above analysis, we can see that
The new version of uboot allocates sdram space from top to bottom.
No matter where uboot is started from, the code in spiflash, nandflash, sram, etc. will be relocated to a position at the top of sdram and continue to run.
I found a 2010.6 version of uboot and took a general look at the boot code. It determines whether relocate is needed by judging whether _start and TEXT_BASE (link address) are equal. If uboot is started from sdram, relocate is not needed.
The new version of uboot still has major changes in this regard.
I think there are two advantages to this change. First, you don't need to consider the boot method, all relocate code. Second, you don't need to consider the uboot link address, because it needs to be relocated.
Uboot sdram space planning diagram:
6_main
Return to _main from relocate_code, and then the last section of main code is as follows:
----------------------------------------------------------------------
// Set up final (full) environment
bl c_runtime_cpu_setup // we still call old routine here
ldr r0, =__bss_start // this is auto-relocated!
ldr r1, =__bss_end // this is auto-relocated!
mov r2, #0x00000000 // prepare zero to clear BSS
clbss_l:cmp r0, r1 // while not at end of BSS
strlo r2, [r0] // clear 32-bit BSS word
addlo r0, r0, #4 // move to next
blo clbss_l
bl coloured_LED_init
bl red_led_on
// call board_init_r(gd_t *id, ulong dest_addr)
mov r0, r9 // gd_t
ldr r1, [r9, #GD_RELOCADDR] // dest_addr
// call board_init_r
ldr pc, =board_init_r // this is auto-relocated!
// we should not return here.
------------------------------------------------------------------------------------
First jump to c_runtime_cpu_setup, as follows:
-----------------------------------------------------------------------
ENTRY(c_runtime_cpu_setup)
//
* If I-cache is enabled invalidate it
#ifndef CONFIG_SYS_ICACHE_OFF
mcr p15, 0, r0, c7, c5, 0 @ invalidate icache
mcr p15, 0, r0, c7, c10, 4 @ DSB
mcr p15, 0, r0, c7, c5, 4 @ ISB
#endif
//
* Move vector table
// Set vector address in CP15 VBAR register
ldr r0, =_start
mcr p15, 0, r0, c12, c0, 0 @Set VBAR
bx lr
ENDPROC(c_runtime_cpu_setup)
----------------------------------------------------------------------
If icache is enable, then icache will be invalidated to ensure that instructions are updated from sdram to cache.
Then update the first address of the exception vector table. Since the code is relocated, the exception vector table is also relocated.
Returning from c_runtime_cpu_setup, the following assembly is to clear the bss segment.
Next, coloured_LED_init and red_led_on are called respectively. Many development boards have LED indicator lights. Here, the power-on indicator light can be turned on for debugging purposes.
Finally, r0 is assigned the gd pointer, r1 is assigned the relocaddr, and the final board_init_r is entered!
7board_init_r
Parameter 1 is the new gd pointer, parameter 2 is the relocate addr, which is the new code address
-------------------------------------------------------------------------
gd->flags |= GD_FLG_RELOC; // tell others: relocation done
bootstage_mark_name(BOOTSTAGE_ID_START_UBOOT_R, "board_init_r");
monitor_flash_len = (ulong)&__rel_dyn_end - (ulong)_start;
// Enable caches
enable_caches();
debug("monitor flash len: lX\n", monitor_flash_len);
board_init(); // Setup chipselects
-------------------------------------------------------------------------
Set gd->flags, the flag has been relocated. I don't understand the function of the monitor_flash_len variable. Enable cache, and finally board_init is the board support function that needs to be implemented. Do the basic initialization of the development board.
--------------------------------------------------------------------------
#ifdef CONFIG_CLOCKS
set_cpu_clk_info();
#endif
serial_initialize();
debug("Now running in RAM - U-Boot at: lx\n", dest_addr);
--------------------------------------------------------------------------
If CONFIG_CLOCKS is turned on, set_cpu_clk_info is also a board support function that needs to be implemented.
Let's focus on serial_initialize. For the most streamlined uboot that can start normally, serial and ddr must work normally.
The implementation is in drivers/serial/serial.c as follows:
---------------------------------------------------------------------------
void serial_initialize(void)
{
mpc8xx_serial_initialize();
ns16550_serial_initialize();
pxa_serial_initialize();
s3c24xx_serial_initialize();
s5p_serial_initialize();
mpc512x_serial_initialize();。。。。
mxs_auart_initialize();
arc_serial_initialize();
vc0718_serial_initialize();
serial_assign(default_serial_console()->name);
}
---------------------------------------------------------------------------
All serial port drivers will implement a xxxx_serial_initialize function and add it to serial_initialize.
In the xxxx_serial_initialize function, all required serial ports (represented by the structure struct serial_device, which implements the basic transceiver configuration) are registered by calling serial_register. serial_register is as follows:
----------------------------------------------------------------------------
void serial_register(struct serial_device *dev)
{
#ifdef CONFIG_NEEDS_MANUAL_RELOC
if (dev->start)
dev->start += gd->reloc_off;
if (dev->stop)
dev->stop += gd->reloc_off;
if (dev->setbrg)
dev->setbrg += gd->reloc_off;
if (dev->getc)
dev->getc += gd->reloc_off;
if (dev->tstc)
dev->tstc += gd->reloc_off;
if (dev->putc)
dev->putc += gd->reloc_off;
if (dev->puts)
dev->puts += gd->reloc_off;
#endif
dev->next = serial_devices;
serial_devices = dev;
}
----------------------------------------------------------------------------
That is, add your serial_dev to the global linked list serial_devices.
You can imagine that if you have 4 serial ports, you can define 4 serial devices in your serial port driver, implement the corresponding transceiver configuration, and then register the 4 serial ports with serial_register.
Go back to serial-initialize, and finally call serial_assign. As we said before, default_serial_console means that you give a default debugging serial port in the serial port driver. serial_assign is as follows:
-----------------------------------------------------------------------------
int serial_assign(const char *name)
{
struct serial_device *s;
for (s = serial_devices; s; s = s->next) {
if (strcmp(s->name, name))
continue;
serial_current = s;
return 0;
}
return -SINGLE SELECT;
}
------------------------------------------------------------------------------
serial_assign is to find the specified default debugging serial port from the serial_devices list. The condition is the name of the serial port. Finally, serial_current is the current default serial port.
In summary, the work of serial_initialize is to register all serial ports in all serial drivers into the serial_devices list, and then find the specified default serial port.
-------------------------------------------------------------------------------------
// The Malloc area is immediately below the monitor copy in DRAM
malloc_start = dest_addr - TOTAL_MALLOC_LEN;
mem_malloc_init (malloc_start, TOTAL_MALLOC_LEN);
-------------------------------------------------------------------------------------
According to our previous analysis in board_init_f, the part below relocate addr is the reserved space of malloc. Here we get the first address of malloc, malloc_start.
-------------------------------------------------------------------------------------
void mem_malloc_init(ulong start, ulong size)
{
mem_malloc_start = start;
mem_malloc_end = start + size;
mem_malloc_brk = start;
memset((void *)mem_malloc_start, 0, size);
malloc_bin_reloc();
}
--------------------------------------------------------------------------------------
In mem_malloc_init, the space reserved by malloc is initialized, the starting address, the ending address, and cleared. We have already relocate, and there is no operation in malloc_bin_reloc.
The following code in board_init_r is to initialize some peripherals, such as mmc flash eth, setting of environment variables, and enabling of interrupts, etc. Here we need to talk about two functions about the serial port, stdio_init and console_init_r.
Looking at the stdio_init code, we only define serial, which will be transferred to serial_stdio_init, as follows:
--------------------------------------------------------------------------------------
void serial_stdio_init(void)
{
struct stdio_dev dev;
struct serial_device *s = serial_devices;
while (s) {
memset(&dev, 0, sizeof(dev));
strcpy(dev.name, s->name);
dev.flags = DEV_FLAGS_OUTPUT | DEV_FLAGS_INPUT;
dev.start = s->start;
dev.stop = s->stop;
dev.putc = s->putc;
dev.puts = s->puts;
dev.getc = s->getc;
dev.tstc = s->tstc;
stdio_register(&dev);
s = s->next;
}
}
--------------------------------------------------------------------------------------
Initialize a stdio_dev for all serial devices on the serial_devices list, with flag as output input, call stdio-register, and add stdio_dev to the global devs list.
As you can imagine, serial_stdio_init is implemented in drivers/serial/serial.c. uboot uses the kernel layering concept here. Under drivers/serial are specific serial drivers, which call serial_register to register in serial_devices. This can be said to be a universal serial driver layer. The universal serial layer calls serial-stdio-init to register all serials in the stdio device. This is the universal stdio layer.
It seems that the layering concept is still very important!
After calling stdio_init in board_init_r, console_init_r is called, as shown below
----------------------------------------------------------------------------------------------
int console_init_r(void)
{
struct stdio_dev *inputdev = NULL, *outputdev = NULL;
int i;
struct list_head *list = stdio_get_list();
struct list_head *pos;
struct stdio_dev *dev;
// Scan devices looking for input and output devices
list_for_each(pos, list) {
dev = list_entry(pos, struct stdio_dev, list);
if ((dev->flags & DEV_FLAGS_INPUT) && (inputdev == NULL)) {
inputdev = dev;
}
if ((dev->flags & DEV_FLAGS_OUTPUT) && (outputdev == NULL)) {
outputdev = dev;
}
if(inputdev && outputdev)
break;
}
if (outputdev != NULL) {
console_setfile(stdout, outputdev);
console_setfile(stderr, outputdev);
}
// Initializes input console
if (inputdev != NULL) {
console_setfile(stdin, inputdev);
}
#ifndef CONFIG_SYS_CONSOLE_INFO_QUIET
stdio_print_current_devices();
#endif // CONFIG_SYS_CONSOLE_INFO_QUIET
// Setting environment variables
for (i = 0; i < 3; i++) {
setenv(stdio_names[i], stdio_devices[i]->name);
}
gd->flags |= GD_FLG_DEVINIT; // device initialization completed
return 0;
}
----------------------------------------------------------------------------------------------
The first half of console_init_r is very clear. It searches for the dev with flag output or input from the devs.list list. If only serial has registered stdio_dev before, then outputdev and inputdev are both the first serial we registered.
Then console_setfile is called as follows:
-------------------------------------------------------------------------------
static int console_setfile(int file, struct stdio_dev * dev)
{
int error = 0;
if (dev == NULL)
return -1;
switch (file) {
case stdin:
case stdout:
case stderr:
// Start new device
if (dev->start) {
error = dev->start();
// If it's not started dont use it
if (error < 0)
break;
}
// Assign the new device (leaving the existing one started)
stdio_devices[file] = dev;
// Update monitor functions
// (to use the console stuff by other applications)
switch (file) {
case stdin:
gd->jt[XF_getc] = dev->getc;
gd->jt[XF_tstc] = dev->tstc;
break;
case stdout:
gd->jt[XF_putc] = dev->putc;
gd->jt[XF_puts] = dev->puts;
gd->jt[XF_printf] = printf;
break;
}
break;
default: // Invalid file ID
error = -1;
}
return error;
}
-------------------------------------------------------------------------------
First, run the start of the device, which is the start function of a specific serial implementation. Then put stdio_device into the stdio_devices global array. This array has 3 members, stdout, stderr, and stdin. Finally, an operation function will be set in gd.
In console_init_r, the flag status in gd will be changed to GD_FLG_DEVINIT. It means that the device initialization is complete.
After board_init_r completes the board-level initialization, it finally enters an infinite loop, prints the command line, and waits for command input and parsing. The uboot startup process is over here!
A lot of space is used above to explain the serial to console architecture under uboot from bottom to top. Let's take a look at the top-to-bottom process from printf to the final serial output in actual use.
First, let's look at printf, which is implemented in common/console.c as follows:
--------------------------------------------------------------------------------
int printf(const char *fmt, ...)
{
va_list args;
uint i;
char printbuffer[CONFIG_SYS_PBSIZE];
#if !defined(CONFIG_SANDBOX) && !defined(CONFIG_PRE_CONSOLE_BUFFER)
if (!gd->have_console)
return 0;
#endif
va_start(args, fmt);
// For this to work, printbuffer must be larger than
* anything we ever want to print.
i = vscnprintf(printbuffer, sizeof(printbuffer), fmt, args);
va_end(args);
// Print the string
puts(printbuffer);
return i;
}
--------------------------------------------------------------------------------
The string concatenation is the same as the general printf implementation, and finally puts is called. The puts implementation is also in console.c, as follows:
--------------------------------------------------------------------------------
void puts(const char *s)
{
#ifdef CONFIG_SANDBOX
if (!gd) {
os_puts(s);
return;
}
#endif
#ifdef CONFIG_SILENT_CONSOLE
if (gd->flags & GD_FLG_SILENT)
return;
#endif
#ifdef CONFIG_DISABLE_CONSOLE
if (gd->flags & GD_FLG_DISABLE_CONSOLE)
return;
#endif
if (!gd->have_console)
return pre_console_puts(s);
if (gd->flags & GD_FLG_DEVINIT) {
// Send to the standard output
fputs(stdout, s);
} else {
// Send directly to the handler
serial_puts(s);
}
}
------------------------------------------------------------------------------------------------
gd->have_console is set in console_init_f of board_init_f, and flag's GD_FLG_DEVINIT is set at the end of console_init_r in board_init_r just now.
If GD_FLG_DEVINIT is not set, indicating that the console is not registered, it is after board_init_f and before board_init_r is executed. At this time, serial_puts is called as follows:
--------------------------------------------------------------------------------
void serial_puts(const char *s)
{
get_current()->puts(s);
}
--------------------------------------------------------------------------------
Directly call the function in serial.c, which is completely consistent with the configuration of serial_init in board_init_f. It only finds a default serial port to use, and ignores other serial ports.
If GD_FLG_DEVINIT is set, it means that the console registration is completed. Call fputs as follows:
----------------------------------------------------------------------------------
void fputs(int file, const char *s)
{
if (file < MAX_FILES)
console_puts(file, s);
}
static inline void console_puts(int file, const char *s)
{
stdio_devices[file]->puts(s);
}
-----------------------------------------------------------------------------------
fputs calls console_puts to find the member stdio_device corresponding to stdout from the global stdio_devices, calls puts, and finally calls the puts function of the specific serial.
After analysis, it can be summarized as follows:
It can be seen that for serial, uboot implements a two-level initialization:
stage 1, only initializes the default console serial, and after printf to puts, it will directly call the puts function of the specific serial port to implement printing
stage 2, register all serials as stdio_device, and select the specified debugging serial port as the stdout, stdin, and stderr of stdio_devices. After printf to puts, it finds the corresponding stdio_device in the global stdio_devices, calls puts of stdio-device, and finally calls puts of the specific serial to implement printing.
To distinguish these two stages, the gd flag, GD_FLG_DEVINIT, is used.
Previous article:ARM CP15 Coprocessor Description
Next article:Another way to write Tiny210 character device driver
Recommended ReadingLatest update time:2024-11-17 02:59
- Popular Resources
- Popular amplifiers
Professor at Beihang University, dedicated to promoting microcontrollers and embedded systems for over 20 years.
- Innolux's intelligent steer-by-wire solution makes cars smarter and safer
- 8051 MCU - Parity Check
- How to efficiently balance the sensitivity of tactile sensing interfaces
- What should I do if the servo motor shakes? What causes the servo motor to shake quickly?
- 【Brushless Motor】Analysis of three-phase BLDC motor and sharing of two popular development boards
- Midea Industrial Technology's subsidiaries Clou Electronics and Hekang New Energy jointly appeared at the Munich Battery Energy Storage Exhibition and Solar Energy Exhibition
- Guoxin Sichen | Application of ferroelectric memory PB85RS2MC in power battery management, with a capacity of 2M
- Analysis of common faults of frequency converter
- In a head-on competition with Qualcomm, what kind of cockpit products has Intel come up with?
- Dalian Rongke's all-vanadium liquid flow battery energy storage equipment industrialization project has entered the sprint stage before production
- Allegro MicroSystems Introduces Advanced Magnetic and Inductive Position Sensing Solutions at Electronica 2024
- Car key in the left hand, liveness detection radar in the right hand, UWB is imperative for cars!
- After a decade of rapid development, domestic CIS has entered the market
- Aegis Dagger Battery + Thor EM-i Super Hybrid, Geely New Energy has thrown out two "king bombs"
- A brief discussion on functional safety - fault, error, and failure
- In the smart car 2.0 cycle, these core industry chains are facing major opportunities!
- Rambus Launches Industry's First HBM 4 Controller IP: What Are the Technical Details Behind It?
- The United States and Japan are developing new batteries. CATL faces challenges? How should China's new energy battery industry respond?
- Murata launches high-precision 6-axis inertial sensor for automobiles
- Ford patents pre-charge alarm to help save costs and respond to emergencies
- Why is there no CUBEMX project in the STM32L0 official HAL library routine?
- Measurement of radar power, spectrum and related parameters
- [ESP32-Audio-Kit Audio Development Board Review]——(2): Questions about play_mp3_control
- Is it popular to reduce the size of laptop keyboards now?
- (Bonus 5) GD32L233 Review - CRC (with the clearest article explaining CRC in history)
- Recruiting MCU development engineers (workplace: Beijing, Wuhan)
- ST FOC electrical angle problem
- Altium Designer software is abnormal, please solve it! ! !
- Car-to-everything design
- Unboxing