U-Boot 보드 초기화 시퀀스 — board_init_f와 board_init_r 분리 이유
#한 줄 요약
“U-Boot은 부트를 두 단계로 나눕니다.” —
board_init_f는 DRAM이 없거나 작은 SRAM에서 동작하는 pre-relocation,board_init_r은 DRAM에 복사된 후에 동작하는 post-relocation. 같은 binary이지만 실행 환경이 완전히 다릅니다.
U-Boot이 시작하는 순간 어디서 동작하는지를 봅니다. SoC SRAM(수십 KB)에서 시작해 DRAM training이 끝나면 자기 자신을 DRAM에 복사하고, 복사된 DRAM 코드로 점프합니다. 점프 전이 board_init_f, 점프 후가 board_init_r. 두 단계를 분리해서 이해하는 것이 U-Boot 흐름의 핵심입니다.
#왜 두 단계인가
가장 단순한 답은 DRAM이 처음에는 없다는 것입니다.
부트 시점 0
- SoC 내부 SRAM — 동작 가능 (수십 KB)
- DRAM 영역(
0x40000000) — 죽어 있음
DRAM training 완료 후
- SoC 내부 SRAM — 여전히 동작 가능
- DRAM 영역(
0x40000000) — 살아남, 수 GB 사용 가능
부트 초기에는 SRAM 안의 작은 공간에서 동작해야 합니다. 페이지 테이블, 스택, malloc 영역이 모두 SRAM 안에 들어가야 합니다. DRAM이 깨어난 후에는 DRAM의 충분한 공간으로 옮겨 가는 것이 자연스럽습니다.
또 다른 이유는 코드가 ROM/Flash에서 직접 실행되는 경우입니다. NOR flash에서 부분적으로 실행하다가 DRAM에 복사 후 더 빠르게 실행하는 패턴.
이 복사 + 점프가 relocation입니다.
#메모리 레이아웃 변화
board_init_f와 board_init_r의 환경 차이를 메모리 맵으로 봅니다.
board_init_r 시점에는 DRAM의 충분한 공간에서 동작합니다. 스택이 크고, malloc 영역도 크고, 마음껏 driver를 probe합니다.
#init_sequence_f — pre-relocation 흐름
common/board_f.c에 board_init_f의 흐름이 함수 배열로 정의됩니다.
static const init_fnc_t init_sequence_f[] = { setup_mon_len, /* monitor 길이 측정 */#ifdef CONFIG_OF_CONTROL fdtdec_setup, /* control DTB 확정 */#endif initf_malloc, /* SRAM 안의 작은 malloc init */ log_init, initf_bootstage, bootstage_mark_name, initf_console_record, arch_cpu_init, /* CPU 초기 설정 (cache, mmu off) */ mach_cpu_init, /* SoC별 초기화 */ initf_dm, /* Driver Model init (pre-reloc만) */ arch_cpu_init_dm, timer_init, env_init, init_baud_rate, serial_init, /* console UART driver 활성화 */ console_init_f, /* 첫 printf 가능 */ display_options, /* "U-Boot 2024.04..." 출력 */ checkcpu, print_cpuinfo, show_board_info, misc_init_f, /* 보드 hook */ init_func_i2c, dram_init, /* DRAM 크기 측정 */ setup_dest_addr, /* relocation 목적지 계산 */ reserve_round_4k, setup_bdinfo, display_new_sp, reloc_fdt, /* DTB를 DRAM으로 복사 */ setup_reloc, NULL};
void board_init_f(ulong boot_flags){ if (initcall_run_list(init_sequence_f)) hang(); /* 이후 relocate_code()로 점프 */}각 함수가 0을 반환하면 다음으로, 0이 아니면 hang. 부트 디버깅 시 어느 함수에서 죽는지 확인하는 것이 첫걸음입니다.
#핵심 함수들
| 함수 | 책임 |
|---|---|
arch_cpu_init | cache flush, MMU 끔, 아키텍처 초기 설정 |
initf_dm | Driver Model init (DM_FLAG_PRE_RELOC만) |
serial_init | console UART driver probe |
console_init_f | 첫 printf 사용 가능 |
dram_init | DRAM 크기 측정 (보드별 hook) |
setup_dest_addr | DRAM 안의 relocation 목적지 계산 |
reloc_fdt | DTB를 DRAM으로 복사 |
setup_reloc | relocation 정보 준비 |
#dram_init — 보드 hook
DRAM 크기는 보드별로 다릅니다. 보드 코드가 dram_init()을 override합니다.
/* board/<vendor>/<board>/<board>.c */
int dram_init(void){ /* PHYS_SDRAM_SIZE는 보드 헤더에서 정의 */ gd->ram_size = PHYS_SDRAM_SIZE; return 0;}크기를 fuse나 DDR controller 레지스터에서 동적으로 읽기도 합니다.
int dram_init(void){ u32 size = read_ddr_size_from_fuse(); gd->ram_size = size; return 0;}gd->ram_size가 전체 부트 흐름에서 DRAM 크기의 source of truth입니다.
#gd_t — 전역 데이터
board_init_f가 전역 변수를 마음대로 못 쓰는 이유는 bss가 아직 zero-init 안 됐을 수 있고 DRAM이 없을 수 있기 때문입니다. 대신 gd_t 구조체에 모든 상태를 보관합니다.
struct global_data { struct bd_info *bd; unsigned long flags; unsigned int baudrate; unsigned long cpu_clk; unsigned long bus_clk; unsigned long mem_clk; phys_size_t ram_size; unsigned long mon_len; unsigned long irq_sp; unsigned long start_addr_sp; unsigned long reloc_off; /* relocation 오프셋 */ struct global_data *new_gd; struct udevice *cur_serial_dev; void *fdt_blob; /* control DTB */ ...};
#define gd ((volatile gd_t *)gd_ptr)gd는 전역 포인터로 항상 접근 가능합니다. ARM에서는 보통 r9 또는 x18 레지스터에 고정되어 있습니다.
#ifdef CONFIG_ARM64#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("x18")#else#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r9")#endif이 레지스터를 망가뜨리지 않는 것이 ARM assembly 코드의 규칙입니다.
#relocation — DRAM으로 옮기기
board_init_f 끝에서 DRAM의 어디로 옮길지를 계산합니다.
static int setup_dest_addr(void){ /* DRAM 끝에서 mon_len만큼 아래 */ gd->ram_top = gd->ram_base + get_effective_memsize(); gd->relocaddr = gd->ram_top - gd->mon_len; gd->relocaddr &= ~(4096 - 1); /* 4KB align */
return 0;}DRAM의 맨 위에 U-Boot을 둡니다. 아래는 비워서 커널 적재 영역으로 씁니다.
DRAM 2GB:0x40000000 +-------------------+ | 빈 영역 | | (커널/initrd 적재) | | | +-------------------+ | | +-------------------+ | malloc 영역 | +-------------------+ | stack | +-------------------+ | bss | +-------------------+ | data | +-------------------+ | text (U-Boot 코드) |0xBFE00000 +-------------------+ ← gd->relocaddr | (4KB align) |0xC0000000 +-------------------+relocation 함수는 arch별 assembly입니다.
ENTRY(relocate_code) ldr x1, __image_copy_start_ofs ... /* DRAM의 새 주소로 코드 복사 */1: ldp x10, x11, [x1], #16 stp x10, x11, [x0], #16 cmp x1, x2 b.lo 1b
/* 다시 rela.dyn fixup */fixloop: ldp x0, x1, [x12], #16 ...
/* board_init_r로 점프 */ bl board_init_rENDPROC(relocate_code)복사 후 PC가 새 주소로 점프합니다. 이 시점부터는 모든 코드가 DRAM에서 실행됩니다.
#init_sequence_r — post-relocation 흐름
common/board_r.c에 board_init_r의 흐름이 정의됩니다.
static init_fnc_t init_sequence_r[] = { initr_trace, initr_reloc, /* gd->flags에 RELOC 표시 */ initr_caches, /* MMU + cache enable */ initr_reloc_global_data, /* fdt_blob 등 포인터 재계산 */ initr_barrier, initr_malloc, /* malloc 영역을 DRAM으로 */ initr_bootstage, initr_dm, /* DM 재초기화, 모든 driver */ initr_dm_devices, arch_initr_trap, initr_announce, /* "U-Boot is now running from DRAM" */ dm_announce, initr_serial, stdio_init, initr_env, /* 환경 변수 적재 */ initr_secondary_cpu, /* SMP 깨우기 (ARMv7) */ initr_pci, initr_pci_ep, stdio_add_devices, initr_jumptable, console_init_r, /* console 인터프리터 활성화 */ initr_eth, /* ethernet init */ initr_post, /* post-init hook */ run_main_loop, /* 명령 인터프리터 시작 */ NULL};
void board_init_r(gd_t *new_gd, ulong dest_addr){ gd = new_gd; ... while (init_sequence_r[i]) { init_sequence_r[i](); i++; }}run_main_loop가 명령 인터프리터를 시작하는 마지막 줄입니다. 이 함수는 반환하지 않습니다.
void main_loop(void){ bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
cli_init(); autoboot_command(...); /* bootcmd 실행 */
cli_loop(); /* 명령 입력 대기, 영원히 */}autoboot_command가 환경 변수 bootcmd를 실행합니다. bootcmd가 커널을 부트하면 cli_loop에 도달하지 않습니다. autoboot이 중단되면 cli_loop에서 프롬프트가 떠 명령 입력을 받습니다.
#board 코드가 hook할 수 있는 지점
보드별 초기화 코드가 끼어들 수 있는 곳은 여러 군데 있습니다.
#pre-relocation hook
int board_early_init_f(void){ /* board_init_f 초기, pinctrl 설정 등 */ return 0;}
int dram_init(void){ /* DRAM 크기 보고 */ gd->ram_size = ...; return 0;}
int dram_init_banksize(void){ /* multi-bank DRAM 정보 */ gd->bd->bi_dram[0].start = PHYS_SDRAM; gd->bd->bi_dram[0].size = PHYS_SDRAM_SIZE; return 0;}
int misc_init_f(void){ /* 기타 pre-reloc 초기화 */ return 0;}#post-relocation hook
int board_init(void){ /* board_init_r 중간 단계 hook */ /* GPIO 설정, PMIC tuning 등 */ return 0;}
int board_late_init(void){ /* 거의 끝, 환경 변수 동적 설정 */ env_set("board_name", get_board_revision()); return 0;}
int misc_init_r(void){ /* 환경 변수 외 기타 */ return 0;}
int last_stage_init(void){ /* 가장 마지막, cli_loop 직전 */ return 0;}#kernel handoff hook
int ft_board_setup(void *blob, struct bd_info *bd){ /* DTB fixup, booti 직전 호출 */ fdt_setprop(blob, ...); return 0;}
int board_prep_linux(struct bootm_headers *images){ /* Linux 점프 직전 */ return 0;}보드 .c 파일에 원하는 hook 함수를 정의하면 기본 weak 구현을 override합니다.
#bdinfo — runtime 상태 확인
U-Boot 명령 인터프리터의 bdinfo 명령이 gd_t의 핵심 필드를 보여줍니다.
=> bdinfoboot_params = 0x00000000DRAM bank = 0x00000000-> start = 0x40000000-> size = 0x80000000flashstart = 0x00000000flashsize = 0x00000000flashoffset = 0x00000000baudrate = 115200 bpsrelocaddr = 0xbfe00000reloc off = 0x7fc00000Build = 64-bitcurrent eth = ethernet@30be0000ethaddr = 00:04:9f:01:23:45IP addr = <NULL>fdt_blob = 0x0000000000000000new_fdt = 0xbfdb6000fdt_size = 0x00018000lmb_dump_all: memory.cnt = 0x1 memory[0] [0x40000000-0xbfffffff], 0x80000000 bytes reserved.cnt = 0x4 reserved[0] [0xbfd71008-0xbfffffff], 0x0028eff8 bytes ...arch_number = 0x00000000TLB addr = 0xbfff0000irq_sp = 0x000000000000bff70sp start = 0x00000000bff70530Early malloc usage: 850 / 2000relocaddr이 U-Boot이 적재된 위치. reloc off가 원본 주소와의 차. 이 값이 symbol address fixup에 사용됩니다.
#bootstage — 시간 측정
U-Boot이 각 단계의 timestamp를 자동 기록합니다.
=> bootstage reportTimer summary in microseconds (24 records): Mark Elapsed Stage 0 0 reset 102 102 SPL 159847 159745 end SPL 159912 65 board_init_f 181122 21210 arch_cpu_init 181135 13 initf_dm 181201 66 console_init_f 359862 178661 dram_init 360100 238 setup_dest_addr 360125 25 reloc_fdt 360218 93 end board_init_f 360230 12 board_init_r 362544 2314 arch_initr_trap 364112 1568 initr_eth 364450 338 end board_init_r 364462 12 main_loop 364582 120 bootm_start...bootstage report로 부트 시간 병목을 찾습니다. dram_init이 178ms라면 DDR training이 차지하는 시간입니다.
#보드 .c 파일의 전체 모습
i.MX 8M Plus EVK의 board 코드 골격입니다.
#include <common.h>#include <env.h>#include <init.h>#include <miiphy.h>#include <netdev.h>#include <asm/arch/clock.h>#include <asm/arch/sys_proto.h>
DECLARE_GLOBAL_DATA_PTR;
int board_init(void){ /* GPIO 초기화, board-specific PMIC 설정 */ return 0;}
int board_early_init_f(void){ init_uart_clk(1); return 0;}
int dram_init(void){ gd->ram_size = PHYS_SDRAM_SIZE; return 0;}
int dram_init_banksize(void){ gd->bd->bi_dram[0].start = PHYS_SDRAM; gd->bd->bi_dram[0].size = PHYS_SDRAM_SIZE; return 0;}
int board_phys_sdram_size(phys_size_t *size){ *size = PHYS_SDRAM_SIZE; return 0;}
int board_late_init(void){ env_set("board_name", "EVK"); env_set("board_rev", "iMX8MP"); return 0;}
#if defined(CONFIG_OF_BOARD_SETUP)int ft_board_setup(void *blob, struct bd_info *bd){ /* DTB fixup */ return 0;}#endif각 hook이 어디서 호출되는지 알면 원하는 곳에 코드를 넣을 수 있습니다.
#자주 하는 실수
#전역 변수를 board_init_f에서 수정
board_init_f는 bss가 zero-init 안 됐을 수 있고, DRAM이 없을 수도 있습니다. 전역 변수 대신 gd_t의 필드에 저장합니다.
/* Bad */static int my_state = 0;
int board_early_init_f(void){ my_state = 42; /* 어떻게 될지 모름 */ return 0;}
/* Good */int board_early_init_f(void){ gd->arch.my_state = 42; return 0;}gd_t에 arch 필드를 정의해서 보드별 상태를 저장합니다.
#dram_init 누락
보드 .c 파일에 dram_init()이 없으면 weak 기본 구현이 호출되고, 그게 0을 반환하지만 ram_size는 0입니다. 이후 setup_dest_addr이 0을 가지고 relocation 주소를 계산해 망함. 반드시 정의.
#DECLARE_GLOBAL_DATA_PTR 빠뜨림
C 파일 상단에 DECLARE_GLOBAL_DATA_PTR;을 반드시 둡니다. 이게 없으면 gd 매크로가 정의되지 않은 변수가 됩니다.
#relocation 후 원본 메모리 사용
relocation 전에 SRAM 안의 데이터를 가리키는 포인터가 있다면, relocation 후 그 SRAM이 사라졌을 수 있습니다. 모든 포인터는 gd_t의 relocated 버전을 써야 합니다.
#board_init_f에서 DM_FLAG_PRE_RELOC 없는 driver 사용
board_init_f에서는 DM_FLAG_PRE_RELOC가 있는 driver만 사용 가능. 일반 driver는 device 인스턴스 자체가 만들어지지 않습니다.
#gd 레지스터를 덮어씀
ARM assembly에서 r9(또는 x18)를 다른 용도로 사용하면 gd가 깨집니다. 인라인 어셈블리에서 특히 주의.
#board_init vs board_init_f 헷갈림
이름이 비슷해서 혼동하기 쉽습니다.
board_init_f: pre-relocation의 전체 흐름 함수 (override 안 함)board_init: post-relocation의 보드 hook 함수 (override 함)board_early_init_f: pre-relocation의 보드 hook 함수 (override 함)
보드 코드가 작성하는 것은 board_init과 board_early_init_f.
#ft_board_setup이 호출 안 됨
defconfig에 CONFIG_OF_BOARD_SETUP=y가 없으면 fixup이 호출되지 않습니다. 빌드 통과해도 실제 부팅 시 fixup이 빠집니다.
CONFIG_OF_BOARD_SETUP=y#정리
- U-Boot은 board_init_f(pre-relocation, SRAM)와 board_init_r(post-relocation, DRAM)로 나뉩니다.
- pre-relocation은 전역 변수를 못 쓰고, DRAM이 없거나 작은 SRAM에서 동작합니다. 모든 상태는
gd_t에. init_sequence_f배열의 함수가 순차로 호출됩니다. 하나라도 0이 아니면 hang.gd_t는 전역 포인터로 ARM에서 r9(32bit) 또는 x18(64bit) 레지스터에 고정.- relocation은 DRAM의 맨 위로 U-Boot을 복사하고 점프합니다. PC가 새 주소로 옮겨가는 지점.
init_sequence_r은 DM 재초기화, env 적재, console 인터프리터까지 진행해 main_loop에 도달합니다.- 보드 코드의 hook은
board_early_init_f,dram_init,board_init,board_late_init,ft_board_setup등. bdinfo로 runtime 상태,bootstage report로 각 단계의 소요 시간을 확인합니다.
#다음 편
Ch 9: DRAM 초기화에서는 DDR controller training의 실제 흐름을 봅니다. ZQ calibration, PHY training, write/read leveling이 왜 그렇게 길고 까다로운지, vendor tool이 어떻게 parameter를 뽑아내는지.
#관련 항목
Bootloader Internals · 8 of 37
- 1ROM부터 init까지 — 임베디드 부팅 단계의 빈자리 분석
- 2Das U-Boot vs TF-A vs EDK II — 임베디드 부트로더 생태계 비교
- 3U-Boot 빌드 시스템 분석 — Kconfig·Makefile·defconfig 동작 추적
- 4ARM 임베디드 부트 4단계 분해 — BL1·SPL·TPL·U-Boot Proper의 역할
- 5U-Boot Falcon Mode — SPL이 U-Boot Proper 없이 커널 직접 부팅
- 6Device Tree DTB 부트로더 처리 — 로딩 시점과 fixup 메커니즘 추적
- 7U-Boot Driver Model 내부 — uclass·driver·device 추상화 구조
- 8U-Boot 보드 초기화 시퀀스 — board_init_f와 board_init_r 분리 이유
- 9DDR Controller 프로그래밍과 PHY Training — SPL의 가장 어려운 작업
- 10임베디드 스토리지 부팅 분석 — MMC·SCSI·NAND·SPI Flash 비교
- 11임베디드 네트워크 부팅 — TFTP·PXE·BOOTP 시퀀스 분석
- 12U-Boot USB 부팅 — fastboot·UMS·USB host 메커니즘
- 13U-Boot 환경 변수와 bootcmd — 부팅 시나리오 정의하기
- 14Modern U-Boot bootflow / bootmeth — 새 추상화 레이어 분석
- 15FIT image 구조 분석 — multi-image·hash·configuration 추적
- 16U-Boot Verified Boot — RSA 서명과 public key 임베딩 흐름
- 17임베디드 A/B 부팅 이중화 — OTA 안전성을 위한 부트 슬롯 설계
- 18U-Boot의 EFI 호환 분석 — bootefi 명령과 EFI loader 동작 원리
- 19Linux Boot ABI — ARM/ARM64 커널 진입 규약 추적
- 20임베디드 펌웨어 업데이트 — RAUC vs SWUpdate 비교
- 21새 보드 U-Boot 포팅 실전 — defconfig 작성부터 첫 부팅까지
- 22부트로더 디버깅 기법 — DEBUG·JTAG·serial·post-mortem 분석
- 23SoC BootROM·eFuse·OTP — 부팅의 0단계 분석
- 24SPL·TPL 내부 해부 — 가장 작은 부트 단계의 동작 추적
- 25ARM Trusted Firmware-A 통합 — BL1·BL2·BL31·BL32·BL33 흐름
- 26DDR Training과 PHY Calibration — 보드별 파라미터 튜닝
- 27임베디드 Chain of Trust — 다단계 서명 검증의 전체 흐름
- 28임베디드 Flash Layout 설계 — partition·NAND·eMMC·UBI 비교
- 29U-Boot Distro Boot — extlinux·boot.scr 표준화 분석
- 30부트로더 CI 구축 — build matrix와 자동 부팅 테스트
- 31TF-A BL31 EL3 Runtime 분석 — PSCI·SDEI·RAS dispatcher 추적
- 32PSCI와 SMCCC ABI — ARM 표준 SMC 호출 규약 분석
- 33ARM64 Secondary Core Bring-up — PSCI CPU_ON 호출부터 EL1 진입까지
- 34U-Boot PCIe Enumeration — 부트로더가 디바이스를 찾는 흐름 분석
- 35EFI·UEFI에서 CXL 초기화 — CEDT 생성과 HDM Decoder 사전 설정
- 36부트 시 메모리 토폴로지 결정 — DDR + CXL.mem 통합 인식
- 37UEFI Secure Boot 인증서 만료 — 2011→2023 CA 롤오버와 PQC 대비
관련 글
U-Boot PCIe Enumeration — 부트로더가 디바이스를 찾는 흐름 분석
U-Boot PCIe 열거 과정 — Root Complex 초기화·Config Space scan·BAR sizing·resource 할당, CXL DVSEC 인식까지.
U-Boot Distro Boot — extlinux·boot.scr 표준화 분석
보드별 다른 부트 스크립트를 표준화 — U-Boot Distro Boot, extlinux.conf, boot.scr의 차이와 선택.
SPL·TPL 내부 해부 — 가장 작은 부트 단계의 동작 추적
SPL과 TPL의 정확한 역할, SRAM 안에 들어가는 코드 구조, DDR이 없는 환경에서 어떻게 동작하는가.
이 글을 참조하는 글 (5)
- DDR Training과 PHY Calibration — 보드별 파라미터 튜닝— Bootloader Internals
- DDR Controller 프로그래밍과 PHY Training — SPL의 가장 어려운 작업— Bootloader Internals
- U-Boot Driver Model 내부 — uclass·driver·device 추상화 구조— Bootloader Internals
- Device Tree DTB 부트로더 처리 — 로딩 시점과 fixup 메커니즘 추적— Bootloader Internals
- ARM 임베디드 부트 4단계 분해 — BL1·SPL·TPL·U-Boot Proper의 역할— Bootloader Internals