Finally, we read all the code in the uboot phase. The general feeling is that the uboot code is really clever. Reading good code is like reading a good book, reading it repeatedly, thinking carefully, and summarizing it in time. This is the king!
Well, I don't need to talk much nonsense. Here I will summarize my experience in uboot reading. I mainly want to take a note for myself. The experts can see that this article has completely passed ~ No. Thank you ~
Where does uboot start? You must check the LDs file, which is a link file. This file is used to guide the GCC linker to link various sections in each component file of our program ). Because I learned the smdk2410 Development Board, the uboot. LDS file is under the Board/smdk2410/directory. The uboot. LDS file is as follows:
Output_format ("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") // specify the default output file format as Elf, 32-bit arm instruction, small-end. The second and third parameters in brackets are used when the user uses the command line option-EB
// And-El File Format
/* Output_format ("elf32-arm", "elf32-arm", "elf32-arm ")*/
Output_arch (ARM) // The execution platform of the output file is arm
Entry (_ start) // specifies the entry point of the code segment for output executable files
Sections // defines a section block, and the linker assigns a loading address for each section Block.
{
. = 0x00000000; // address counter, specifying the current address as 0
. = Align (4); // 4-byte alignment, because this is an arm command and 32 is the instruction set
. Text: // define a code segment
{
CPU/ARM920T/start. O (. Text) // place the start. o file at the beginning of the code segment, where _ start is the entry point
* (. Text) // place the text segments of all other files after start. O.
}
. = Align (4); // still four bytes aligned
. Rodata: {* (. rodata)} // defines the read-only data segment. It should be a constant (guess). The rodata segments of all input files are put here in sequence.
. = Align (4 );
. Data: {* (. Data)} // defines the data segment. The data segments of all input files are placed here in sequence.
. = Align (4 );
. Got: {* (. Got)} // defines the got segment, which is unknown ~
. = .;
_ U_boot_1__start = .; // Save the address value recorded in the current address counter to _ u_boot_cmd_start. In the second stage of the Code, this segment is used to save the uboot command. With this symbol, you can find this segment //
Location
. U_boot_cmd: {* (. u_boot_cmd)} Put the u_boot_cmd segments of all input files here. * Is a wildcard, indicating each file
_ U_boot_1__end = .;
. = Align (4 );
_ Bss_start =.; // record the loading address of the BSS segment
. BSS :{*(. BSS)} // This is the BSS segment used to save uninitialized data. uboot will not transfer this segment when it transfers itself to ram in the first-stage code, however, the C language requires that the BSS segment be initialized to 0. Therefore, uboot clears the BSS after it moves itself to flash.
_ End = .;
}
Well, the above is a simple analysis of LDS. If you want to know more about the linked files, you can go to the Niang search. We can't do without searching!
Based on the analysis above, we need to find start. s file, which is compiled into the target file start. O is linked to the beginning of the code segment, and the _ start symbol in the file is the entry point. (Note: The symbol itself is an absolute address. During the link, the connector replaces the symbol with an absolute address. The _ start symbol indicates that the absolute address is the loading address of uboot in Ram, in the config. MK file definition: text_base = 0x33f80000 ).
Well, let's talk a little bit about it. Go to the start. s file and start the first stage of uboot.
This file is in the ARM920T Directory, which indicates that the code in this file is based on the arm's processor and has little to do with our board smdk2410.
Find the _ start symbol and add a global symbol in front of it, which means that the symbol is global and can be seen in other files. (in C language, A symbol is a variable. Therefore, the symbol can be understood as a global variable in C)
The first line of code starting with _ start:
. 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
_ 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
At the beginning, there must be eight jump commands. If you have played 51 single-chip microcomputer, you should also know that when the program runs abnormally, the hardware will automatically come to these addresses to find the corresponding processing program. A total of eight commands, accounting for 32 bytes. Here, jump to B is a relative jump command, not to pull the absolute address, and LDR jump command I have never understood at the beginning, after analysis, finally research clearly. This is the case. The URL of uboot is config. the loading address defined in Mk. All symbols are calculated based on this address, that is, the offset from the position of the symbol to _ start plus this address value, in fact, our code is executed at zero address and is not in that config. the loaded address specified in MK starts execution, which causes a problem. The symbols in the program do not correspond to the actual address. If a program needs to jump to a symbol, the command cannot be found (the program goes to config. of course, the command cannot be found for the high address defined by MK ). What is the addressing mechanism of LDR?
Here, LDR is actually relative addressing. For example, the ldrpc and _ FIQ commands calculate the offset between the _ FIQ symbol and the current PC (How many codes are involved in the calculation, calculate the offset value based on the amount of code), and then add or subtract the offset value from the PC as the pointer to access the content at the address pointed to by the pointer, this content is of course the content in this symbol (absolute address). Copy this content to the PC. What is this so-called content?
You can find it from the following program:
_ FIQ:. Word FIQ
The content here is actually the pointer of The FIQ processing function, so LDR will finally pass the pointer value to the PC, so the PC will execute the FIQ processing function. This mechanism is the basis for a deep understanding of uboot's first-stage code. I didn't quite understand it at the beginning of reading, and I finally found relevant information to understand these things, I still need to search more ~ . Word: put a word value in this symbol ~
_ Text_base:
. Word text_base // here, the value of the uboot loading address defined in config. mk is recorded in this absolute address for later use. The last few items are similar. They all take the recorded addresses in the LDs file to the current file for ease of use.
. Globl _ armboot_start
_ Armboot_start:
. Word _ start
/*
* These are defined in the board-specific Linker script.
*/
. Globl _ bss_start
_ Bss_start:
. Word _ bss_start
. Globl _ bss_end
_ Bss_end:
. Word _ end
The following code starts power-on Reset:
Reset:
/*
* Set the CPU to svc32 Mode
*/
Mrs r0, CPSR // read the CPSR program State Register value to R0
Bic r0, R0, # 0x1f // convert 0 ~ 4-digit resetting
ORR r0, R0, #0xd3 // set the working mode to 10011, that is, SVC mode (management mode), and prohibit fast interruption and interruption (corresponding bit setting 1)
Msr cpsr, R0 // save R0 to CPSR
Set the Register address below:
# If defined (config_s3c2400)
# Define pwtcon 0x15300000
# Define intmsk 0x14400008/* interupt-controller base addresses */
# Define clkdivn 0x14800014/* Clock divisor register */
# Elif defined (config_s3c2410)
# Define pwtcon 0x53000000
# Define intmsk 0x4a000008/* interupt-controller base addresses */
# Define intsubmsk 0x4a00001c
# Define clkdivn 0x4c000014/* Clock divisor register */
# Endif
Here, the watchdog controller and the interrupt controller clock division controller are set. These addresses are recorded in macros. For details about the Register address, refer to the related manual of the device. Because I have studied S3C2440, this device is not defined here, so we need to add it here when porting uboot to the 2440 Development Board ~ Of course, you need to define macros such as config_s3c2410 in your own header file. Because these registers of 2410 are exactly the same as those of 2440, you can also use 2410 directly, just modify it slightly.
LDR r0, = pwtcon // This is a pseudo command. Unlike the previous use of LDR commands, the execution of this pseudo command is to pass the value of pwtcon to the R0 register. The compiler is responsible for translating pseudo commands into executable commands understood by arm. This command is actually translated into an mov or LDR command. When the data to be transmitted is less than 256, mov is used, when the number of commands passed is greater than 256, A file pool is automatically generated at the end of the file, and the pwtcon value is placed in the file pool. Then, LDR can find this value by means of relative addressing, these are all completed by the compiler and transparent to programmers ~
MoV R1, #0x0
STR R1, [R0] // use the R0 register as the pointer and assign 0x0 to the memory unit pointed to by the pointer (actually the setting register of the watchdog ~) The function is to disable the watchdog, otherwise the board will be restarted continuously (I have encountered this situation)
Continue viewing the program:
The following Program sets the interrupt shielding register to 1, which shields all the interruptions.
MoV R1, #0 xffffffff
LDR r0, = intmsk
STR R1, [R0]
Continue with the program:
For 2410 and 2440, there is also the concept of sub-Interrupt. Some interruptions (such as uart1 & 2) are first handled by the sub-interrupt shielding register. here we need to block all sub-interrupts. For our board 2440, you can make a slight modification and add a macro to judge it.
# If defined (config_s3c2410)
LDR R1, = 0x3ff
LDR r0, = intsubmsk
STR R1, [R0]
# Endif
Set the clock frequency Register as follows:
According to the notes, we can see that this Code sets the fclk hclk pclk frequency ratio, fclk is used for the CPU kernel clock, hclk is used for the AHB Bus device, and pclk is used for the APB bus device.
/* Fclk: hclk: pclk = */
/* Default fclk is 120 MHz! */
LDR r0, = clkdivn
MoV R1, #3
STR R1, [R0]
For our 2440, we also need to set several other registers here. When porting, add the following code:
# If defined (config_s3c2440)
# Define mpllcon 0x4c000004
# Define upllcon 0x4c000008
LDR r0, = clkdivn
MoV R1, #5
STR R1, [R0]
LDR r0, = mpllcon
LDR R1, = 0x7f021
STR R1, [R0]
LDR r0, = upllcon
LDR R1, = 0x38022
STR R1, [R0]
# Else
/* Fclk: hclk: pclk = */
/* Default fclk is 120 MHz! */
LDR r0, = clkdivn
MoV R1, #3
STR R1, [R0]
# Endif
The three registers clkdivn, mpllcon, upllcon, and clkdivn are used to set the clock division ratio. mpllcon sets the fclk frequency, upllcon sets the uclk frequency, and uclk is used for the USB device clock.
Set uclk to 48 MHz (usually 96 or 48), fclk to 405 MHz, and fclk: hclk: pclk =.
The following program:
# Ifndef config_skip_lowlevel_init
BL cpu_init_crit
# Endif
Config_skip_lowlevel_init is not defined here, so it jumps to cpu_init_crit. Note that BL is relatively addressing and does not involve absolute addresses. In addition, this code must be executed when uboot is started normally. If it is started from Ram, comment out this code.
Code at cpu_init_crit:
/*
* Flush V4 I/D caches
*/
MoV r0, #0
MCR P15, 0, R0, C7, C7, 0/* flush V3/v4 cache */
MCR P15, 0, R0, C8, C7, 0/* flush V4 TLB */
/*
* Disable MMU stuff and caches
*/
MRC P15, 0, R0, C1, C0, 0
Bic r0, R0, #0x00002300 @ clear bits 13, 9: 8 (-- V--- RS)
Bic r0, R0, #0x00000087 @ clear bits 7, 2: 0 (B ----cam)
ORR r0, R0, #0x00000002 @ set bit 2 (a) Align
ORR r0, R0, #0x00001000 @ set bit 12 (I) I-Cache
MCR P15, 0, R0, C1, C0, 0
/*
* Before relocating, We Have To Setup RAM Timing
* Because memory timing is board-dependend, you will
* Find A lowlevel_init.s in your board directory.
*/
MoV IP, LR
BL lowlevel_init
MoV LR, IP
MoV PC, LR
This Program sets the arm coprocessor CP15 registers, CP15 has 16 32-bit registers, arm_920t manual can find the specific content. Registers 7 and 8 are used for cache (instruction and data buffer) and TLB (page table cache) Operations respectively. The first piece of code above is to write 0 to the C7 and C8 registers, so that the cache and TLB are invalid.
The C1 register will be read to R0 later, and some bitwise AND reset operations are performed on R0. Finally, write it back. The specific operation is to clear 13 9 8 7 1 0, and then set it to 2 12. 0 indicates that the abnormal vector table is at 0x00000000 address; otherwise, the abnormal vector table is high. If the value of 12th is 1, the instruction cache is enabled. If the value of 9 or 8 is used to set the memory access permission, if the value of 7 is 0, the instruction is in the small-end mode. In addition, data caching is prohibited, enabling address alignment check and MMU disabling.
After these settings are complete, you need to jump to lowlevel_init and perform some settings. Before that, note that the LR register is the R14 register, which is the link register, when a redirection command such as B or BL is used, the current Pc value is automatically saved in LR, so that when the subprogram is executed, it needs to be redirected back, you only need to assign the LR value to the PC. We are currently in the arm operation status (the other is the thumb operation status), SVC operation mode, this mode excludes a link register. However, when we jump to cpu_init_crit, LR stores the current Pc value. If we want to jump to lowlevel_init another subroutine, we must first save the value in the current lr to avoid overwriting, unable to return to the original main program for execution, save lr to the IP register (R12), and then jump to another subroutine. After the return, you only need to assign the IP value to LR. Next, jump to lowlevel_init to see:
Because this subroutine is actually a bus-related register, which is related to the hardware of the Board, the lowlevel_init.s file is in the Board/smdk2410/directory.
This subroutine code:
LDR r0, = smrdata
LDR R1, _ text_base
Sub r0, R0, r1
LDR R1, = bwscon/* bus width status controller */
Add R2, R0, #13*4
0:
LDR R3, [R0], #4
STR R3, [R1], #4
CMP R2, R0
BNE 0b
/* Everything is fine now */
MoV PC, LR
It is mainly to assign the 13 values at the beginning of the smrdata symbol to the memory control register group in sequence. For specific settings of these registers, refer to the manual.
Finally, assign the LR register value to the PC and return it to the previous subroutine. Then, write the IP address to the LR, and then assign it to the PC. Then we return to our previous main program.
# Ifndef config_skip_relocate_uboot
Relocate:/* relocate U-boot 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 // The actual startup address returned by ADR, which is a pseudocommand and translated into the current Pc value on sub or add. If they are equal, the actual start address is the same as the load address, indicating that the instance is started from Ram. Jump directly to the set Stack
LDR R2, _ armboot_start // assign the value of _ start to R2, which is the address of uboot loading in Ram. The relative addressing method is also used here, as shown below.
LDR R3, _ bss_start // assign the starting address of the BSS segment to r3
Sub R2, R3, R2/* R2 <-size of armboot */R2 Save the uboot size to be copied
Add R2, R0, R2/* R2 <-source end address */R0 to save the actual startup address, that is, 0 address
Copy_loop: // The R0 and R1 are used as pointers in sequence below (R1 stores the uboot to the ram loading address), and each time the 8 bytes of data are copied from norfalsh to ram
Ldmia R0 !, {R3-r10}/* copy from Source Address [R0] */
Stmia R1 !, {R3-r10}/* Copy to target address [R1] */
CMP r0, R2/* Until source end addreee [R2] */
Ble copy_loop
# Endif/* config_skip_relocate_uboot */
The above section judges whether uboot starts from Ram or nor flash, and if it is started from nor flash, uboot copies itself to ram. The role of ADR is to obtain the address value of the _ start offset relative to the PC, that is, the obtained address is the relative address, which is also to avoid inconsistency between the start address and the load address.
The above section only applies to norflash startup. If you start from nandflash, you need to add some programs:
BL bbootfrmnorflash/* determine whether u‐boot is enabled in NAND Flash or nor flash */
CMP r0, #0/* R0 stores bbootfrmnorflash function return values. If 0 is returned, it indicates NAND Flash.
Start. Otherwise, the system starts in nor flash */
Beq nand_boot/* jump to the NAND Flash startup Code */
/* Nor flash startup Code */
B stack_setup/* skip the NAND Flash startup Code */
Nand_boot:
/* NAND Flash startup Code */
Stack_setup:
/* Other code */
For more information, see other porting tutorials on the Internet.
Continue pasting:
Stack_setup:
LDR r0, _ text_base/* upper 128 kib: relocated uboot */
Sub r0, R0, # cfg_malloc_len/* malloc area */base address minus the size left for the heap (save the environment variable in the heap)
Sub r0, R0, # cfg_gbl_data_size/* bdinfo */minus the size left for the Global Data Structure
# Ifdef config_use_irq
Sub r0, R0, # (config_stacksize_irq + config_stacksize_fiq) // subtract the size left for the interrupt Stack
# Endif/
Sub sp, R0, #12/* Leave 3 words for abort-stack */leave some space to stop the stack, and assign the R0 value to the stack pointer.
The above section sets the stack. Because the second-stage uboot code is written in C language, you must set the running stack.
Continue with the program:
Clear_bss:
LDR r0, _ bss_start/* Find start of BSS segment */
LDR R1, _ bss_end/* Stop here */
MoV R2. #0x00000000/* clear */
Clbss_l: Str R2, [R0]/* clear loop ...*/
Add r0, R0, #4
CMP r0, r1
Ble clbss_l
It can be seen that this program should be very simple, that is, to clear the BSS segment starting from _ bss_start in Ram, because the C language requires that all BSS have initial values, previously, when we moved the uboot itself to ram, we didn't move the BSS segment because it has no useful values. We just need to assign a value of 0 to ram.
The last program of the first stage is as follows:
Ldr pc, _ start_armboot
_ Start_armboot:. Word start_armboot
Assigning a value to a PC is actually a jump command. The C language jumps to the second stage to continue execution. Here we still use the relative jump, that is, the value obtained by adding or subtracting the offset from this symbol to the PC as a pointer to access the data in the memory and save it to the destination register (PC. Then, the value of start_armboot is assigned to the PC. Start_armboot is the c function pointer of the second stage, that is, the function address, of course, this address is calculated based on the loading address text_base (the addresses calculated during the link should all be based on the loading address), that is, this address must exist in Ram, so jump to this function is equivalent to jump to ram, which completes the jump from flash to Ram and starts to execute the second-stage code.
Good! The first-stage code analysis is here. Of course, I will continue to analyze the second-stage code ......