새 보드 U-Boot 포팅 실전 — defconfig 작성부터 첫 부팅까지
새 보드를 받아 U-Boot부터 띄울 때, 백지에서 시작하면 며칠을 그냥 잡아먹습니다. 다행히 거의 항상 비슷한 보드가 이미 mainline U-Boot에 들어 있습니다. 그 보드를 복사해 차이만 고치는 게 정석입니다.
#한 줄 요약
같은 SoC·비슷한 주변 회로의 reference 보드를 템플릿으로 복사한 다음, defconfig·DT·보드 코드·환경 변수 다섯 군데를 차례로 바꾸면 대부분 첫 시리얼 출력까지 도달합니다.
#시작 — 비슷한 보드 찾기
U-Boot 소스에서 configs/를 훑어봅니다.
ls configs | grep -i imx8mimx8mm_evk_defconfigimx8mn_evk_defconfigimx8mp_evk_defconfigimx8mq_evk_defconfigimx8mq_phanbell_defconfigverdin-imx8mm_defconfigverdin-imx8mp_defconfigSoC가 같고 DDR 종류와 PMIC가 비슷한 것이 후보입니다. imx8mm_evk처럼 NXP reference는 가장 많이 검증되어 있으니 후보 1번으로 둡니다.
# 후보 보드의 파일 리스트grep -l "imx8mm_evk" configs/ \ board/freescale/imx8mm_evk/* \ arch/arm/dts/imx8mm-evk*.dts*#바꿀 파일 여섯 군데
새 보드 boardx를 추가한다고 하면, 손대는 곳은 보통 이렇습니다.
1. configs/boardx_defconfig
2. board/myvendor/boardx/
- ├── Kconfig
- ├── Makefile
- ├── boardx.c (보드 초기화, DRAM 크기 검출)
- ├── spl.c (SPL 단계 DDR 초기화)
- ├── lpddr4_timing.c (DDR 타이밍, NXP 도구로 생성)
- └── MAINTAINERS
3. arch/arm/dts/boardx.dts
- arch/arm/dts/boardx-u-boot.dtsi
4. include/configs/boardx.h
5. board/myvendor/Kconfig (subsystem 등록)
6. arch/arm/mach-imx/imx8m/Kconfig (보드 선택 옵션 추가)
-u-boot.dtsi는 U-Boot 단계 전용 DT 추가분입니다. 본 DT는 Linux와 공유하고, U-Boot 만의 필요(예: SPL 단계 사용 device, bootph-pre-ram 같은 노드 속성)는 .dtsi로 끼워 넣습니다.
#defconfig 복사
cp configs/imx8mm_evk_defconfig configs/boardx_defconfig열어서 보드 식별 부분을 바꿉니다.
CONFIG_ARM=yCONFIG_ARCH_IMX8M=yCONFIG_SYS_TEXT_BASE=0x40200000CONFIG_TARGET_BOARDX=yCONFIG_SYS_LOAD_ADDR=0x40400000CONFIG_SYS_CONFIG_NAME="boardx"CONFIG_DEFAULT_DEVICE_TREE="boardx"CONFIG_SPL_LDSCRIPT="arch/arm/cpu/armv8/u-boot-spl.lds"CONFIG_SPL=yCONFIG_SPL_FIT=yCONFIG_BOOTCOMMAND="run distro_bootcmd"CONFIG_BOOTSTD_FULL=yCONFIG_DEFAULT_FDT_FILE="boardx.dtb"CONFIG_TARGET_BOARDX=y는 우리가 아직 만들지 않은 옵션입니다. 그래서 arch/arm/mach-imx/imx8m/Kconfig에 등록해 둬야 합니다.
# arch/arm/mach-imx/imx8m/Kconfig 에 추가config TARGET_BOARDX bool "boardx" select BINMAN select IMX8MM select SUPPORT_SPL select OF_BOARD_FIXUP help boardx is a custom imx8mm-based industrial board.#board/myvendor/boardx/Kconfig·Makefile
board/myvendor/Kconfig에 등록:
if TARGET_BOARDXconfig SYS_BOARD default "boardx"
config SYS_VENDOR default "myvendor"
config SYS_CONFIG_NAME default "boardx"
source "board/myvendor/boardx/Kconfig"endifboard/myvendor/boardx/Kconfig:
config SYS_BOARD_PINMUX default "boardx_pinmux"
config IMX_CONFIG default "board/myvendor/boardx/imximage.cfg"board/myvendor/boardx/Makefile:
obj-y += boardx.oifdef CONFIG_SPL_BUILDobj-y += spl.o lpddr4_timing.oendifSPL_BUILD 시점에는 SPL용 파일만, U-Boot proper 시점에는 보드 본체 코드만 빌드됩니다. 두 단계가 한 트리에서 분리 컴파일된다는 점이 처음엔 헷갈리지만 익숙해지면 자연스럽습니다.
#boardx.c — 최소 보드 초기화
#include <common.h>#include <init.h>#include <asm/arch/sys_proto.h>#include <asm/io.h>
int board_init(void){ /* DRAM, console 이후 한 번만 호출 */ return 0;}
int dram_init(void){ /* DRAM 크기를 SoC 레지스터에서 읽음 */ gd->ram_size = imx_ddr_size(); return 0;}
int board_phys_sdram_size(phys_size_t *size){ *size = imx_ddr_size(); return 0;}
int board_late_init(void){ /* env로 보드 식별, MAC 주소 등 */ env_set("board_name", "boardx"); env_set("board_rev", "A1"); return 0;}처음 한 사이클에는 이 정도면 충분합니다. 나중에 PHY reset·LED·watchdog feed 등을 더해 갑니다.
#spl.c — SPL 단계 DDR
가장 위험한 부분입니다. DDR 타이밍이 틀리면 SPL이 메모리 첫 접근에서 hang합니다.
#include <common.h>#include <hang.h>#include <init.h>#include <spl.h>#include <asm/arch/clock.h>#include <asm/arch/ddr.h>
extern struct dram_timing_info dram_timing;
void spl_dram_init(void){ ddr_init(&dram_timing);}
void board_init_f(ulong dummy){ int ret;
arch_cpu_init(); init_uart_clk(1);
ret = spl_init(); if (ret) { debug("spl_init failed: %d\n", ret); hang(); }
preloader_console_init(); enable_tzc380(); spl_dram_init(); board_init_r(NULL, 0);}dram_timing은 NXP DDR Tool 또는 벤더가 제공한 calibration 결과를 그대로 가져옵니다. 직접 계산하지 않습니다. 잘못된 timing은 언젠가 메모리 corruption으로 드러나는데, 그 시점이 부팅 직후가 아니라 부팅한 지 한참 뒤 random crash로 나타나니 무서운 버그입니다.
#DT — boardx.dts
reference 보드의 DT를 복사해서 시작합니다.
/dts-v1/;
#include "imx8mm.dtsi"#include "imx8mm-boardx.dtsi"
/ { model = "MyVendor BoardX"; compatible = "myvendor,boardx", "fsl,imx8mm";
chosen { stdout-path = &uart2; };
memory@40000000 { device_type = "memory"; reg = <0x0 0x40000000 0 0x40000000>; /* 1 GB */ };
reg_usdhc2_vmmc: regulator-usdhc2-vmmc { compatible = "regulator-fixed"; regulator-name = "VSD_3V3"; regulator-min-microvolt = <3300000>; regulator-max-microvolt = <3300000>; gpio = <&gpio2 19 GPIO_ACTIVE_HIGH>; enable-active-high; };};
&uart2 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_uart2>; status = "okay";};
&usdhc2 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_usdhc2>; vmmc-supply = <®_usdhc2_vmmc>; cd-gpios = <&gpio2 12 GPIO_ACTIVE_LOW>; bus-width = <4>; status = "okay";};-u-boot.dtsi로 U-Boot 전용 속성을 추가합니다.
&{/soc@0} { bootph-all;};
&osc_24m { bootph-all;};
&uart2 { bootph-all;};
&usdhc2 { bootph-pre-ram;};bootph-pre-ram이 SPL에서 쓰이는 노드, bootph-all이 모든 단계에서 쓰이는 노드를 표시합니다. 이걸 빠뜨리면 SPL이 해당 device를 못 봅니다.
#첫 빌드와 첫 부팅
다 채웠으면 빌드:
make boardx_defconfigmake -j$(nproc) ARCH=arm CROSS_COMPILE=aarch64-linux-gnu-u-boot.bin·spl/u-boot-spl.bin·flash.bin(NXP의 묶인 이미지)이 나옵니다. flash.bin을 SD에 dd해 보드를 켭니다.
sudo dd if=flash.bin of=/dev/sdb bs=1k seek=33 conv=fsync시리얼 콘솔(115200 8N1)을 띄우고 전원을 넣습니다. 아무것도 안 나오는 게 흔한 첫 결과입니다.
#첫 시리얼 출력까지의 단계
이 흐름이 사실상 표준 디버깅 순서입니다.
각 단계에서 어떤 출력이 나오는지 예시:
[2] U-Boot SPL 2026.04 (May 09 2026 - 16:01:23) DDRINFO: start DRAM init DDRINFO: DRAM rate 3000MTS DDRINFO: ddrphy calibration done DDRINFO: complete DRAM PHY training
[3] U-Boot 2026.04 (May 09 2026 - 16:01:23) CPU: i.MX8MM rev1.0 1600 MHz (running at 1200 MHz) ... Net: eth0: ethernet@30be0000 Hit any key to stop autoboot:각 단계에서 멈추면 그 구간의 코드가 의심 대상입니다. 1단계가 안 되면 DEBUG_UART 설정·UART pinmux·UART clock, 2단계가 안 되면 DDR timing, 3단계가 안 되면 cache/MMU/clock, 4단계가 안 되면 MMC/Net driver, 5단계가 안 되면 DTB·cmdline·kernel ABI입니다.
#MAINTAINERS와 commit 분리
mainline에 올릴 생각이면 board/myvendor/boardx/MAINTAINERS에 자신을 넣습니다.
BOARDX BOARDM: Hawk Yoon <hawking90a@gmail.com>S: MaintainedF: arch/arm/dts/boardx.dtsF: arch/arm/dts/boardx-u-boot.dtsiF: board/myvendor/boardx/F: configs/boardx_defconfigF: include/configs/boardx.hcommit은 기능별로 분리합니다. 통째로 한 patch보다 다음 흐름이 받아들여지기 좋습니다.
arm: dts: add boardx device treeboard: myvendor: add boardx board supportconfigs: add boardx_defconfigdoc: add boardx documentation
#자주 하는 실수
새 보드 포팅에서 자주 만나는 함정입니다.
- DDR timing을 reference 보드 그대로 둔다. 같은 SoC라도 PCB layout·DDR chip·전압이 다르면 언젠가 깨집니다. DDR Tool로 재calibration이 정석입니다.
-u-boot.dtsi를 잊는다.bootph-pre-ram속성이 없어 SPL에서 device가 안 보이는데, 에러는 *“MMC not found”*처럼 device 차원에서 나오기 때문에 원인 찾기 까다롭습니다.CONFIG_SYS_TEXT_BASE를 reference에서 안 바꾼다. DRAM 크기와 layout이 다르면 부팅 도중 자기 자신을 덮어쓰기 합니다.- UART 핀mux는 켰는데 UART clock이 안 켜진 상태. 가장 흔한 “첫 출력이 안 나오는” 원인.
init_uart_clk()호출 빠짐. - defconfig 옵션을 너무 많이 켠다. 첫 부팅까지는 최소 기능만 켜고, 부팅 후 하나씩 추가하는 편이 디버깅하기 좋습니다.
- DT 노드는 추가했는데
status="okay"를 빼먹어 device가 disable된 채로 부팅. - eMMC·NOR boot mode 핀이 안 맞아서 SoC ROM이 SD가 아닌 다른 매체를 본다. SoC datasheet의 BOOT_MODE 핀 도해를 PCB와 한 번 더 대조해야 합니다.
#정리
- 새 보드는 비슷한 reference 보드 복사로 시작하는 것이 가장 빠르고 안전합니다.
- 손대는 곳은
configs/·board/<vendor>/<board>/·arch/arm/dts/·include/configs/네 곳이 핵심입니다. - DDR timing은 직접 계산하지 않고 벤더 도구의 결과를 그대로 가져옵니다.
-u-boot.dtsi로 SPL 단계의 device를 명시(bootph-pre-ram)해야 합니다.- 첫 시리얼 출력 → SPL → DDR init → proper U-Boot → autoboot → Linux 다섯 단계의 어디서 멈췄는지가 디버깅 분기점입니다.
CONFIG_DEBUG_UART는 SPL보다도 이른 시점의 출력을 켜는 마지막 보루입니다.- mainline 제출을 위해서는 기능별 commit 분리와 MAINTAINERS 등록이 필요합니다.
#다음 장 예고
다음 장이 시리즈 마지막입니다. 새 보드를 포팅하면서 자주 만나는 부팅 안 됨 상황을 어떻게 진단하는지를 정리합니다. DEBUG_UART·JTAG·OpenOCD·post-mortem·실패 매트릭스까지 다루고, 그다음 시리즈로 어떻게 이어갈지도 안내합니다.
#관련 항목
Bootloader Internals · 21 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)
- 부트로더 CI 구축 — build matrix와 자동 부팅 테스트— Bootloader Internals
- 부트로더 디버깅 기법 — DEBUG·JTAG·serial·post-mortem 분석— Bootloader Internals
- 임베디드 펌웨어 업데이트 — RAUC vs SWUpdate 비교— Bootloader Internals
- U-Boot 보드 초기화 시퀀스 — board_init_f와 board_init_r 분리 이유— Bootloader Internals
- U-Boot Driver Model 내부 — uclass·driver·device 추상화 구조— Bootloader Internals