Phân tích chi tiết quy trình khởi động U-Boot

Giới thiệu

Việc nắm vững quy trình khởi động của U-Boot giúp hiểu rõ về các thiết bị ngoại vi được khởi tạo ở đâu, từ đó khi cần điều chỉnh các trình điều khiển thiết bị ngoại vi, chúng ta sẽ có cơ sở rõ ràng.

I. Linker Script u-boot.lds

Để phân tích quy trình khởi động của U-Boot, trước tiên cần xác định "điểm vào" (entry point) - dòng lệnh đầu tiên của chương trình. Chương trình được liên kết thông qua linker script, do đó có thể tìm thấy điểm vào thông qua linker script này. Nếu chưa biên dịch U-Boot, linker script nằm tại arch/arm/cpu/u-boot.lds. Tuy nhiên, đây không phải là linker script cuối cùng được sử dụng - nó chỉ là cơ sở để tạo ra script thực tế. Sau khi biên dịch U-Boot, file u-boot.lds sẽ được tạo ra trong thư mục gốc của U-Boot.

Mở file u-boot.lds:

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(boot_start)
SECTIONS
{
 . = 0x00000000;
 . = ALIGN(4);
 .text :
 {
  *(.__image_copy_start)
  *(.vectors)
  arch/arm/cpu/armv7/start.o (.text*)
 }
 .dynsym _image_binary_end : { *(.dynsym) }
 .dynbss : { *(.dynbss) }
 .dynstr : { *(.dynstr*) }
 .dynamic : { *(.dynamic*) }
 .plt : { *(.plt*) }
 .interp : { *(.interp*) }
 .gnu.hash : { *(.gnu.hash) }
 .gnu : { *(.gnu*) }
 .ARM.exidx : { *(.ARM.exidx*) }
 .gnu.linkonce.armexidx : { *(.gnu.linkonce.armexidx.*) }
}

ENTRY(boot_start) xác định điểm vào của chương trình là boot_start. Hàm boot_start được định nghĩa trong file arch/arm/lib/vectors.S:

.globl boot_start
    .section ".vectors", "ax"

boot_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
    .word CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
    ARM_VECTORS
#endif

    .globl reset_handler
    .globl undefined_handler
    .globl swi_handler
    .globl prefetch_handler
    .globl abort_handler
    .globl not_used_handler
    .globl irq_handler
    .globl fiq_handler

Phía sau boot_start là bảng vector ngắt (interrupt vector table). Chỉ thị .section dùng để chỉ định đoạn lưu trữ code; ".vectors" là tên đoạn; "ax" biểu thị đoạn có thể thực thi (executable) và ghi được (writable).

Sử dụng lệnh sau để tìm "__image_copy_start" trong U-Boot:

grep -nR "__image_copy_start"

Mở file u-boot.map để xem vị trí này. File u-boot.map là file ánh xạ của U-Boot, cho phép xem một file hoặc hàm được liên kết đến địa chỉ nào. Từ dòng 1629 có thể thấy __image_copy_start có giá trị 0XC0100000, và địa chỉ bắt đầu của .text cũng là 0XC0100000.

Từ đó có thể kết luận code trong vectors.S được lưu trong segment vectors. Địa chỉ bắt đầu của segment vectors là 0XC0100000, cho thấy toàn bộ U-Boot bắt đầu từ địa chỉ này.

II. Quy trình khởi động U-Boot

2.1 Hàm reset_handler

Từ u-boot.lds đã xác định được điểm vào là boot_start trong file arch/arm/lib/vectors.S.

.globl boot_start
    .section ".vectors", "ax"

boot_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
    .word CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
    ARM_VECTORS
#endif

    .globl reset_handler
    .globl undefined_handler
    .globl swi_handler
    .globl prefetch_handler
    .globl abort_handler
    .globl not_used_handler
    .globl irq_handler
    .globl fiq_handler

Hàm reset_handler nằm trong file arch/arm/cpu/armv7/start.S:

/*************************************************************************
 *
 * Startup Code (reset vector)
 *
 * Do important init only if we don't start from memory!
 * Setup memory and board specific bits prior to relocation.
 * Relocate armboot to ram. Setup stack.
 *
 *************************************************************************/

    .globl reset_handler
    .globl save_params_ret
    .type   save_params_ret,%function
#ifdef CONFIG_ARMV7_LPAE
    .global switch_to_hypervisor_ret
#endif

reset_handler:
    /* Allow the board to save important registers */
    b   save_boot_params

Hàm save_boot_params cũng được định nghĩa trong start.S:

/*************************************************************************
 *
 * void save_boot_params(u32 r0, u32 r1, u32 r2, u32 r3)
 *  __attribute__((weak));
 *
 * Stack pointer is not yet initialized at this moment
 * Don't save anything to stack even if compiled with -O0
 *
 *************************************************************************/
ENTRY(save_boot_params)
    b   save_params_ret

Hàm save_params_ret:

save_params_ret:
#ifdef CONFIG_ARMV7_LPAE
/*
 * check for Hypervisor support
 */
    mrc p15, 0, r0, c0, c1, 1
    and r0, r0, #CPUID_ARM_VIRT_MASK
    cmp r0, #(1 << CPUID_ARM_VIRT_SHIFT)
    beq switch_to_hypervisor
switch_to_hypervisor_ret:
#endif
    /*
     * 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
    teq r1, #0x1a
    bicne r0, r0, #0x1f
    orrne r0, r0, #0x13
    orr r0, r0, #0xc0
    msr cpsr,r0

Đoạn code này thực hiện:

  • Đọc giá trị thanh ghi cpsr vào r0
  • Trích xuất 5 bit thấp (bit0-bit4) để xác định chế độ hoạt động của processor
  • Kiểm tra xem processor có đang ở chế độ Hyp hay không
  • Nếu không ở chế độ Hyp, xóa các bit mode và thiết lập chế độ SVC
  • Vô hiệu hóa cả hai ngắt FIQ và IRQ bằng cách set bit I và F
Các chế độ hoạt động của Cortex-A7
M[4:0]Chế độ
10000User(usr)
10001FIQ(fiq)
10010IRQ(irq)
10011Supervisor(svc)
10110Monitor(mon)
10111Abort(abt)
11010Hyp(hyp)
11011Undefined(und)
11111System(sys)

Tiếp theo, code thực hiện thiết lập vector table:

/*
 * Setup vector:
 * (OMAP4 spl TEXT_BASE is not 32 byte aligned.
 * Continue to use ROM code vector only in OMAP4 spl)
 */
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
    /* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
    mrc p15, 0, r0, c1, c0, 0
    bic r0, #CR_V
    mcr p15, 0, r0, c1, c0, 0

#ifdef CONFIG_HAS_VBAR
    /* Set vector address in CP15 VBAR register */
    ldr r0, =boot_start
    mcr p15, 0, r0, c12, c0, 0
#endif
#endif

CR_V được định nghĩa trong arch/arm/include/asm/system.h:

#define CR_V (1 << 13)

Bit13 của thanh ghi SCTLR là bit V - điều khiển vị trí vector table:

  • Khi V=0: Vector table tại 0x00000000, có thể di chuyển được
  • Khi V=1: Vector table tại 0xFFFF0000, không thể di chuyển

Việc clear bit V nhằm chuẩn bị cho việc di chuyển vector table.

Tiếp tục thực thi:

#ifndef CONFIG_SKIP_LOWLEVEL_INIT
#ifdef CONFIG_CPU_V7A
    bl  cpu_init_cp15
#endif
#ifndef CONFIG_SKIP_LOWLEVEL_INIT_ONLY
    bl  cpu_init_crit
#endif
#endif

Hàm cpu_init_cp15 thực hiện cấu hình các thanh ghi CP15:

ENTRY(cpu_init_cp15)
    /*
     * Invalidate L1 I/D
     */
    mov r0, #0
    mcr p15, 0, r0, c8, c7, 0
    mcr p15, 0, r0, c7, c5, 0
    mcr p15, 0, r0, c7, c5, 6
    mcr     p15, 0, r0, c7, c10, 4
    mcr     p15, 0, r0, c7, c5, 4
    ...
    mov pc, r5
ENDPROC(cpu_init_cp15)

Hàm cpu_init_crit cũng được định nghĩa trong start.S:

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
ENDPROC(cpu_init_crit)

Hàm cpu_init_crit chỉ gọi hàm lowlevel_init.

2.2 Hàm lowlevel_init

Hàm lowlevel_init chủ yếu khởi tạo RAM bằng cách ghi các thanh ghi điều khiển RAM.

Hàm lowlevel_init được định nghĩa trong arch/arm/cpu/armv7/lowlevel_init.S:

#include 
#include 
#include 

.pushsection .text.s_init, "ax"
WEAK(s_init)
    bx  lr
ENDPROC(s_init)
.popsection

.pushsection .text.lowlevel_init, "ax"
WEAK(lowlevel_init)
    /*
     * 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
    bic sp, sp, #7
#ifdef CONFIG_SPL_DM
    mov r9, #0
#else
#ifdef CONFIG_SPL_BUILD
    ldr r9, =gdata
#else
    sub sp, sp, #GD_SIZE
    bic sp, sp, #7
    mov r9, sp
#endif
#endif
    push {ip, lr}
    bl  s_init
    pop {ip, pc}
ENDPROC(lowlevel_init)
.popsection

CONFIG_SYS_INIT_SP_ADDR được định nghĩa trong include/configs/stm32mp1.h:

#define CONFIG_SYS_SDRAM_BASE     STM32_DDR_BASE
#define CONFIG_SYS_INIT_SP_ADDR       CONFIG_SYS_TEXT_BASE

CONFIG_SYS_TEXT_BASE được định nghĩa trong file .config, do đó CONFIG_SYS_INIT_SP_ADDR có giá trị 0XC0100000. Stack pointer (sp) sẽ trỏ đến 0XC0100000.

GD_SIZE được định nghĩa:

#define GENERATED_GBL_DATA_SIZE 240
#define GENERATED_BD_INFO_SIZE 80
#define GD_SIZE 240
#define GD_BD 0
#define GD_MALLOC_BASE 164
#define GD_RELOCADDR 64
#define GD_RELOC_OFF 84
#define GD_START_ADDR_SP 80
#define GD_NEW_GD 88

Sau khi lowlevel_init hoàn thành, quay lại hàm cpu_init_crit, rồi quay về save_params_ret, tiếp theo sẽ thực thi hàm _main.

2.3 Hàm _main

Hàm _main được định nghĩa trong file arch/arm/lib/crt0.S:

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
    mov sp, r0
    bl  board_init_f_alloc_reserve
    mov sp, r0
    mov r9, r0
    bl  board_init_f_init_reserve

#if defined(CONFIG_SPL_EARLY_BSS)
    SPL_CLEAR_BSS
#endif

    mov r0, #0
    bl  board_init_f

#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 r0, [r9, #GD_START_ADDR_SP]
    bic r0, r0, #7
    mov sp, r0
    ldr r9, [r9, #GD_NEW_GD]

    adr lr, here
    ldr r0, [r9, #GD_RELOC_OFF]
    add lr, lr, r0
#if defined(CONFIG_CPU_V7M)
    orr lr, #1
#endif
    ldr r0, [r9, #GD_RELOCADDR]
    b   relocate_code

here:
    bl  relocate_vectors
    bl  c_runtime_cpu_setup
#endif

#if !defined(CONFIG_SPL_BUILD) || CONFIG_IS_ENABLED(FRAMEWORK)

#if !defined(CONFIG_SPL_EARLY_BSS)
    SPL_CLEAR_BSS
#endif

#ifdef CONFIG_SPL_BUILD
    bl  spl_relocate_stack_gd
    cmp r0, #0
    movne sp, r0
    movne r9, r0
#endif

#if ! defined(CONFIG_SPL_BUILD)
    bl  coloured_LED_init
    bl  red_led_on
#endif
    mov     r0, r9
    ldr r1, [r9, #GD_RELOCADDR]
#if CONFIG_IS_ENABLED(SYS_THUMB_BUILD)
    ldr lr, =board_init_r
    bx  lr
#else
    ldr pc, =board_init_r
#endif
#endif

ENDPROC(_main)

Hàm board_init_f_alloc_reserve trong common/init/board_init.c:

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;
}

Hàm này thực hiện:

  • Dành riêng vùng nhớ early malloc
  • Dành riêng vùngglobal_data (gd)
  • Trả về địa chỉ top mới (16-byte aligned)

CONFIG_SYS_MALLOC_F_LEN=0x3000, sizeof(struct global_data)=240 (GD_SIZE).

Trong file arch/arm/include/asm/global_data.h, con trỏ gd được định nghĩa:

gd là con trỏ đến cấu trúc gd_t, lưu trong thanh ghi r9.

Cấu trúc gd_t trong include/asm-generic/global_data.h:

typedef struct global_data {
    bd_t *bd;
    unsigned long flags;
    unsigned int baudrate;
    unsigned long cpu_clk;
    unsigned long bus_clk;
    unsigned long pci_clk;
    unsigned long mem_clk;
    ...
#if defined(CONFIG_TRANSLATION_OFFSET)
    fdt_addr_t translation_offset;
#endif
#if CONFIG_IS_ENABLED(WDT)
    struct udevice *watchdog_dev;
#endif
} gd_t;

Hàm board_init_f_init_reserve trong common/init/board_init.c:

void board_init_f_init_reserve(ulong base)
{
    struct global_data *gd_ptr;

    gd_ptr = (struct global_data *)base;
    memset(gd_ptr, '\0', sizeof(*gd));
#if !defined(CONFIG_ARM)
    arch_setup_gd(gd_ptr);
#endif
    base += roundup(sizeof(struct global_data), 16);

#if CONFIG_VAL(SYS_MALLOC_F_LEN)
    gd->malloc_base = base;
    base += CONFIG_VAL(SYS_MALLOC_F_LEN);
#endif

    if (CONFIG_IS_ENABLED(SYS_REPORT_STACK_F_USAGE))
        board_init_f_init_stack_protection();
}

Hàm này khởi tạo gd (xóa zero) và thiết lập gd->malloc_base là địa chỉ base cộng với kích thước gd.

2.4 Hàm board_init_f

Hàm _main gọi hàm board_init_f, hàm này thực hiện hai nhiệm vụ chính:

  1. Khởi tạo các thiết bị ngoại vi như serial, timer, in thông báo...
  2. Khởi tạo các thành viên của gd. U-Boot sẽ tự di chuyển đến vùng nhớ cuối của DRAM để nhường chỗ cho Linux kernel. Trước khi di chuyển, cần cấp phát vị trí và kích thước cho các phần của U-Boot như gd, malloc pool... Thông tin này được lưu trong các thành viên của gd.

Hàm board_init_f được định nghĩa trong common/board_f.c:

void board_init_f(ulong boot_flags)
{
    gd->flags = boot_flags;
    gd->have_console = 0;

    if (initcall_run_list(init_sequence_f))
        hang();

#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
        !defined(CONFIG_EFI_APP) && !CONFIG_IS_ENABLED(X86_64) && \
        !defined(CONFIG_ARC)
    hang();
#endif
}

Mảng init_sequence_f cũng được định nghĩa trong common/board_f.c:

static const init_fnc_t init_sequence_f[] = {
    setup_mon_len,
    fdtdec_setup,
    initf_malloc,
    log_init,
    initf_bootstage,
    setup_spl_handoff,
    initf_console_record,
    arch_cpu_init,
    mach_cpu_init,
    initf_dm,
    arch_cpu_init_dm,
    get_clocks,
    timer_init,
    env_init,
    init_baud_rate,
    serial_init,
    console_init_f,
    display_options,
    display_text_info,
#if defined(CONFIG_PPC) || defined(CONFIG_SH) || defined(CONFIG_X86)
    checkcpu,
#endif
    print_cpuinfo,
    show_board_info,
    INIT_FUNC_WATCHDOG_INIT
    INIT_FUNC_WATCHDOG_RESET
    init_func_vid,
    announce_dram_init,
    dram_init,
    INIT_FUNC_WATCHDOG_RESET
    testdram,
    INIT_FUNC_WATCHDOG_RESET
    init_post,
    INIT_FUNC_WATCHDOG_RESET
    setup_dest_addr,
    reserve_pram,
    reserve_round_4k,
    reserve_mmu,
    reserve_video,
    reserve_trace,
    reserve_uboot,
    reserve_malloc,
    reserve_board,
    setup_machine,
    reserve_global_data,
    reserve_fdt,
    reserve_bootstage,
    reserve_bloblist,
    reserve_arch,
    reserve_stacks,
    dram_init_banksize,
    show_dram_config,
    display_new_sp,
    reloc_fdt,
    reloc_bootstage,
    reloc_bloblist,
    setup_reloc,
    NULL,
};

Giải thích một số hàm quan trọng:

  • setup_mon_len: Thiết lập mon_len - kích thước toàn bộ code U-Boot
  • fdtdec_setup: Thiết lập con trỏ fdt_blob chứa địa chỉ device tree
  • initf_malloc: Khởi tạo các biến liên quan đến malloc trong gd
  • serial_init: Khởi tạo serial, thiết lập cờ GD_FLG_SERIAL_READY
  • console_init_f: Thiết lập gd->have_console = 1
  • dram_init: Thiết lập gd->ram_size (kích thước RAM)
  • setup_dest_addr: Thiết lập ram_base, ram_size, ram_top, relocaddr
  • reserve_uboot: Dành riêng vùng nhớ cho U-Boot sau khi di chuyển
  • reserve_malloc: Dành riêng vùng malloc và bộ nhớ MMU
  • reserve_stacks: Dành riêng vùng stack
  • setup_reloc: Thiết lập các thành viên khác của gd phục vụ cho việc di chuyển code

Sau khi board_init_f hoàn thành, bộ nhớ được phân bổ như sau:

2.5 Hàm relocate_code

Hàm relocate_code thực hiện sao chép code đến vị trí mới. Hàm này được định nghĩa trong arch/arm/lib/relocate.S.

Quy trình khởi động U-Boot tóm tắt:

  1. Từ điểm vào boot_start trong vectors.S
  2. Gọi reset_handler trong start.S
  3. Thiết lập chế độ SVC, vô hiệu hóa ngắt
  4. Cấu hình CP15, thiết lập vector table
  5. Gọi lowlevel_init để khởi tạo RAM
  6. Gọi _main để thiết lập môi trường C
  7. Gọi board_init_f để khởi tạo thiết bị ngoại vi và phân bổ bộ nhớ
  8. Gọi relocate_code để di chuyển code đến vị trí mới trong DRAM
  9. Gọi relocate_vectors để di chuyển vector table
  10. Gọi board_init_r để hoàn tất khởi động

Thẻ: U-Boot Bootloader ARM embedded-systems STM32

Đăng vào ngày 8 tháng 7 lúc 11:18