본문으로 건너뛰기
BSP Development · 17/21

BSP 이미지 패키징 — Flash Layout·Partition·GPT 설계

· Hawk · 7분 읽기

#한 줄 요약

flash image는 섹터 단위의 layout입니다. 한 번 양산되면 바꾸기 어렵습니다. 파티션 layout과 partition table 형식을 처음부터 잘 잡는 것이 핵심입니다.

빌드 시스템이 toolchain, kernel, rootfs를 다 만들어 줘도 그것들이 어떻게 한 binary로 묶이는가는 별도 문제입니다. SD 카드의 0번 섹터, eMMC의 boot1 파티션, NAND의 MTD partition table이 다 다릅니다. 잘못 설계하면 OTA를 못 하는 BSP가 만들어집니다.

이번 글은 partition table 종류, A/B 슬롯, GPT 구조, genimage로 이미지를 조립하는 방법을 다룹니다.

#Partition Table 종류

형식최대 파티션최대 크기적용
MBR4 (primary)2TB단순한 SD 카드
GPT1289.4ZB표준 (UEFI 포함)
BCT / TOC1SoC 특정SoC 특정NVIDIA, Mediatek
MTD partitionskernel cmdlineN/Araw NAND/NOR flash

eMMC와 SD 카드에서는 GPT를 자주 사용하지만, 부트 ROM·펌웨어 요구사항이나 기존 제품 호환성에 따라 MBR 또는 SoC 전용 레이아웃을 사용할 수도 있습니다. raw NAND는 보통 MTD partition을 씁니다. SoC가 자체 boot header 형식을 요구하는 경우(NVIDIA Tegra, MediaTek)도 있으므로, 새 BSP는 먼저 vendor 부트 레이아웃을 확인해야 합니다.

#GPT 구조

GPT는 LBA 단위로 다음을 배치합니다.

LBA 0 : Protective MBR (legacy tool 호환)
LBA 1 : GPT Header (primary)
LBA 2~33 : Partition Entry Array (128 entries × 128B = 16KB)
LBA 34~ : Partition data
...
LBA -33 ~ -2 : Partition Entry Array (backup)
LBA -1 : GPT Header (backup)

핵심 특징을 짚습니다.

  • 16KB 예약. LBA 033이 partition table용입니다. 033 = 34 LBA = 17KB이지만 보통 첫 partition을 최소 1MB부터 시작하도록 정렬합니다.
  • backup GPT. 디스크 끝에 동일 정보가 한 번 더 있습니다. primary가 깨져도 복구 가능합니다.
  • partition UUID. 각 파티션은 고유 UUID를 갖습니다. kernel의 root=PARTUUID=...로 참조하면 sda/mmcblk0/nvme0n1 같은 device 이름 변화에 흔들리지 않습니다.

sgdisk나 parted로 확인할 수 있습니다.

Terminal window
$ sgdisk -p /dev/mmcblk0
Disk /dev/mmcblk0: 30543872 sectors, 14.6 GiB
Logical sector size: 512 bytes
Disk identifier (GUID): A1B2C3D4-E5F6-7890-ABCD-EF1234567890
First usable sector is 34, last usable sector is 30543838
Total free space is 2014 sectors (1007.0 KiB)
Number Start End Size Code Name
1 2048 67583 32.0 MiB 8300 boot
2 67584 2164735 1024.0 MiB 8300 rootfs-a
3 2164736 4261887 1024.0 MiB 8300 rootfs-b
4 4261888 30543838 12.5 GiB 8300 data

#A/B Partition Layout

OTA를 안전하게 하려면 두 개의 rootfs 슬롯이 필요합니다. 업데이트 중 전원이 끊겨도 이전 슬롯으로 부팅 가능해야 하기 때문입니다.

GPT
├─ p1: bootloader-env (1MB) # U-Boot 환경, 부팅 슬롯 정보
├─ p2: boot-a (32MB) # kernel + dtb + initramfs (slot A)
├─ p3: boot-b (32MB) # kernel + dtb + initramfs (slot B)
├─ p4: rootfs-a (1GB) # rootfs (slot A)
├─ p5: rootfs-b (1GB) # rootfs (slot B)
├─ p6: persist (256MB) # 영구 데이터 (factory cert 등)
├─ p7: data (remainder) # 사용자 데이터

A/B 슬롯의 동작 순서는 다음과 같습니다.

  1. 부팅 직후: 부트로더가 BOOT_SLOT 환경 변수를 읽음. 기본 A.
  2. 슬롯 A의 boot와 rootfs를 mount.
  3. 정상 부팅 완료 시 application이 BOOT_OK=1을 씀.
  4. OTA agent가 슬롯 B에 새 이미지를 씀.
  5. 검증 후 BOOT_SLOT=B로 변경 + TRY_COUNT=3 설정.
  6. 재부팅. B 슬롯이 부팅됨.
  7. B가 정상 부팅하면 BOOT_OK=1. 실패하면 부트로더가 TRY_COUNT 감소 후 A로 fallback.

U-Boot 환경 변수로 다음과 같이 구현합니다.

bootcount=0
bootlimit=3
altbootcmd=run boot_alt
bootcmd=run boot_main
boot_main=if test "$boot_slot" = "B"; then run boot_b; else run boot_a; fi
boot_a=load mmc 0:2 ${kernel_addr_r} boot/Image; load mmc 0:4 ...; bootm
boot_b=load mmc 0:3 ${kernel_addr_r} boot/Image; load mmc 0:5 ...; bootm
boot_alt=if test "$boot_slot" = "B"; then setenv boot_slot A; else setenv boot_slot B; fi; saveenv; reset

bootcount와 bootlimit은 U-Boot가 자동으로 다루는 변수입니다. 부팅 시 +1, application의 bootcount=0로 reset. limit 초과 시 altbootcmd 실행.

#SoC 부트 헤더와 첫 섹터

SoC마다 부트 ROM이 읽는 첫 영역이 다릅니다. GPT가 이 영역을 침범하면 부팅이 안 됩니다.

SoC부트 영역위치
i.MX 6/7IVT + DCD + bootloaderoffset 1024 byte
i.MX 8Mcontainer headeroffset 32KB
STM32MP1FSBL headeroffset 0 (SD/eMMC)
RK3399IDB headersector 64 (32KB)
TegraBCTsector 0
TI AM335xMLOFAT 파티션 또는 raw

GPT의 partition 1 시작 위치를 SoC 부트 영역 뒤로 옮겨야 합니다. 보통 첫 partition을 1MB 또는 8MB부터 시작하는 방식이 안전합니다.

i.MX 8M의 예입니다.

0x000000 (0KB) : (사용 안 함)
0x008000 (32KB) : flash.bin (SPL + ATF + U-Boot, ~2MB)
0x100000 (1MB) : GPT primary header
0x100200 (1MB+) : Partition entry array
0x800000 (8MB) : partition 1 (boot)
...

GPT primary header가 1MB offset에 있으면 표준 GPT 도구로 인식이 안 될 수 있습니다. SoC vendor가 권장하는 layout을 따르거나, GPT를 8MB 이후에 두고 그 앞은 unused 영역으로 비워 두는 패턴이 흔합니다.

#genimage로 이미지 조립

Pengutronix의 genimage가 Buildroot/Yocto와 무관하게 standalone으로 자주 쓰입니다. 한 cfg 파일로 multi-partition disk image를 만듭니다.

genimage.cfg
image boot.vfat {
vfat {
files = {
"Image",
"mybsp.dtb",
"boot.scr",
}
}
size = 32M
}
image rootfs.ext4 {
ext4 {
label = "rootfs"
}
mountpoint = "/"
size = 1G
}
image sdcard.img {
hdimage {
partition-table-type = "gpt"
}
partition u-boot {
in-partition-table = "no"
image = "u-boot.bin"
offset = 32K
size = 2M
}
partition boot-a {
partition-type-uuid = U
image = "boot.vfat"
}
partition boot-b {
partition-type-uuid = U
image = "boot.vfat"
}
partition rootfs-a {
partition-type-uuid = L
partition-uuid = "12345678-9abc-def0-1234-56789abcdef0"
image = "rootfs.ext4"
}
partition rootfs-b {
partition-type-uuid = L
partition-uuid = "12345678-9abc-def0-1234-56789abcdef1"
image = "rootfs.ext4"
}
partition data {
partition-type-uuid = L
size = 4G
}
}

in-partition-table = "no"는 raw 영역(SoC가 읽는 부트 영역)에 쓰지만 GPT에는 표시하지 않는다는 뜻입니다.

빌드:

Terminal window
$ genimage \
--inputpath output/images \
--outputpath output/images \
--rootpath output/target \
--config genimage.cfg
INFO: vfat(boot.vfat): cmd: "MAKEDOSFS=mkfs.fat ..."
INFO: ext4(rootfs.ext4): cmd: "mkfs.ext4 ..."
INFO: hdimage(sdcard.img): writing GPT

sdcard.img를 dd나 bmaptool로 SD에 굽습니다.

Terminal window
$ sudo dd if=sdcard.img of=/dev/sdX bs=4M status=progress conv=fsync

bmaptool은 빈 영역을 건너뛰어 훨씬 빠릅니다.

Terminal window
$ bmaptool create -o sdcard.bmap sdcard.img
$ sudo bmaptool copy sdcard.img /dev/sdX

#Android Sparse Image

Android는 .simg (sparse image) 형식을 씁니다. 0으로 채워진 큰 영역을 메타데이터로만 표시해 전송량을 줄입니다. fastboot로 flash 할 때 사용됩니다.

Terminal window
# raw → sparse
$ img2simg rootfs.ext4 rootfs.simg
# sparse → raw
$ simg2img rootfs.simg rootfs.ext4
$ fastboot flash rootfs rootfs.simg

16GB raw image가 1GB sparse로 줄어드는 일이 흔합니다. 양산 line에서 flash 속도에 직접 영향이 있습니다.

#Raw NAND/NOR Flash (MTD)

raw flash는 GPT가 없습니다. MTD partition을 kernel cmdline 또는 device tree로 정의합니다.

mtdparts=spi-nor:512K(u-boot),128K(env),64K(dtb),4M(kernel),-(rootfs)

또는 DT:

&spi0 {
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <40000000>;
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition@0 {
label = "u-boot";
reg = <0x0 0x80000>;
};
partition@80000 {
label = "env";
reg = <0x80000 0x20000>;
};
partition@a0000 {
label = "kernel";
reg = <0xa0000 0x400000>;
};
partition@4a0000 {
label = "rootfs";
reg = <0x4a0000 0xb60000>;
};
};
};
};

NAND rootfs는 ubinize로 UBI volume을 만들어 둡니다. UBI는 wear-leveling과 bad block management를 해 줍니다.

Terminal window
$ mkfs.ubifs -r rootfs/ -m 2048 -e 124KiB -c 1000 -o rootfs.ubifs
$ ubinize -o rootfs.ubi -m 2048 -p 128KiB ubinize.cfg

raw 쓰기는 flash_erase + nandwrite:

Terminal window
$ flash_erase /dev/mtd4 0 0
$ nandwrite -p /dev/mtd4 rootfs.ubi

#양산 라인의 flashing 도구

도구SoC인터페이스
uuuNXP i.MXUSB
fastbootQualcomm, MediaTek, AndroidUSB
dfu-utilSTM, AllwinnerUSB DFU
STM32CubeProgrammerSTM32USB / UART / JTAG
RKDevToolRockchipUSB
sunxi-felAllwinnerUSB FEL
bmaptool일반SD card

i.MX의 uuu 예입니다.

Terminal window
$ cat << 'EOF' > flash.uuu
uuu_version 1.4.0
SDP: boot -f flash.bin
SDPU: write -f u-boot.itb -offset 0x57c00
SDPU: jump
FB: ucmd setenv fastboot_buffer 0x42000000
FB: flash gpt sdcard.gpt
FB: flash boot-a boot.vfat
FB: flash rootfs-a rootfs.ext4
FB: done
EOF
$ sudo uuu flash.uuu

양산 line이라면 PC + USB hub + 보드 8대 같은 setup에서 동시 flash가 표준입니다. uuu, fastboot 모두 multi-board 모드를 지원합니다.

#자주 하는 실수

GPT primary가 SoC 부트 영역과 겹침. i.MX 8M의 SPL은 32KB부터, GPT는 보통 1MB부터. 첫 partition은 8MB 이후로 두는 것이 안전합니다.

partition UUID 미사용. kernel cmdline에 root=/dev/mmcblk0p2로 박으면 SD가 USB로 바뀌면 panic. 항상 PARTUUID=.

A/B 슬롯에서 환경 변수 동기화 실패. 두 슬롯이 같은 bootloader-env partition을 공유해야 하는데 각 slot의 rootfs에 따로 두면 inconsistent. env는 별도 partition.

dd 대신 USB writer GUI 사용. Windows의 일부 GUI tool이 GPT를 깨뜨립니다. 항상 dd + sync나 bmaptool.

bmap 없이 raw image 전송. 16GB image 전체를 보내는 건 낭비입니다. bmap은 사용된 영역만 표시합니다.

sparse image 직접 mount 시도. .simg는 그냥 mount 안 됩니다. simg2img로 변환 후.

#정리

  • 첫 섹터의 의미는 SoC마다 다릅니다. SoC vendor 가이드의 layout을 출발점으로 삼아야 합니다.
  • GPT가 표준입니다. MBR은 legacy SD 카드 정도, raw NAND는 MTD partition.
  • A/B 슬롯은 OTA 안전성의 출발점입니다. boot 슬롯과 rootfs 슬롯을 짝지어 두 세트 둡니다.
  • partition UUID로 root를 지정해야 device 이름 변화에 영향을 받지 않습니다.
  • genimage는 단일 cfg로 multi-partition disk image를 만드는 표준 도구입니다.
  • bmaptool은 빈 영역을 건너뛰어 이미지 기록 시간을 줄일 수 있습니다. 실제 절감 폭은 이미지의 사용 영역과 저장 장치·USB 경로에 따라 달라집니다.
  • Android sparse image는 fastboot 환경에서 큰 image의 전송 시간을 단축합니다.
  • 양산 flashing은 SoC별 전용 도구(uuu, fastboot, RKDevTool, dfu-util)를 PC line에서 multi-board로 운영합니다.

#다음 편 예고

Ch 18: OTA와 field recovery에서는 A/B 슬롯을 활용한 OTA, RAUC/SWUpdate/Mender 같은 update agent, 그리고 현장 복구 시나리오를 다룹니다.

#관련 항목