TF-A·TrustZone 통합 — BL31·secure world·SMC 흐름 적용
#한 줄 요약
“ARMv8 BSP는 TF-A의 BL31이 secure monitor로 상주하고, U-Boot은 그 위의 BL33으로 동작합니다.” ARM Trusted Firmware-A는 거의 모든 Cortex-A 보드에서 반드시 통합해야 하는 secure world의 기반층입니다.
#TF-A가 필요한 이유
ARMv8 아키텍처에서 부팅 직후 모든 코드가 가장 높은 권한 레벨인 EL3에서 시작합니다. EL3는 secure monitor를 위한 자리이며, 이 자리에 머무를 코드가 있어야 OS가 SMC 호출로 PSCI·secure storage·trusted apps에 접근할 수 있습니다. ARM은 이 역할을 표준화한 reference 구현으로 *Trusted Firmware-A (TF-A)*를 제공합니다.
i.MX 8M, RK3588, AM62, STM32MP1, Qualcomm SDM 같은 ARMv8 SoC는 모두 TF-A를 BL31로 채택합니다. vendor가 직접 작성한 SMC monitor를 쓰는 경우는 거의 없으며, vendor는 TF-A에 자기 SoC port만 추가합니다.
TF-A 없이는 PSCI를 통한 CPU on/off가 불가능하므로 SMP 부팅도 안 됩니다. TrustZone-aware DRAM 컨트롤러 설정도 BL31의 일이며, OP-TEE 같은 TEE OS도 BL31의 boot 후에 깨어납니다.
#BL 단계 모델
TF-A는 부팅을 BL0부터 BL33까지 단계로 나눕니다. 각 단계의 역할과 다음 단계로의 전이는 다음과 같습니다.
| 단계 | 역할 | 다음 단계로 넘기는 일 |
|---|---|---|
| BL0 | BootROM (SoC 내부, fix) | boot mode pin 읽기, BL1 적재 |
| BL1 | Trusted Boot ROM (선택적, ROM 안에 fuse-burn 가능) | BL2 인증·적재 |
| BL2 | Trusted Boot Firmware (DDR 초기화·서명 검증) | SoC vendor마다 SPL 대체 또는 보완 |
| BL31 | EL3 Runtime Firmware (Secure Monitor) | PSCI·SMC handler로 상주, BL32(Secure World OS, OP-TEE 등, 선택) 기동 |
| BL33 | Non-secure firmware (U-Boot) | Linux 부팅 준비 |
| Linux Kernel | EL2 또는 EL1에서 실행 | — |
BL0에서 시작해 BL33(U-Boot)까지 순차로 내려가며, BL31이 BL32를 곁가지로 기동한 뒤 BL33으로 이어집니다.
BL31은 상주 코드입니다. Linux로 진입한 뒤에도 SMC instruction이 호출되면 BL31이 깨어나 처리하고 EL1으로 돌아갑니다. SMC instruction은 사실상 secure world로의 trap입니다.
i.MX 8M Plus의 실제 흐름은 다음과 같습니다.
BootROM → flash.bin 적재 (offset 32KB) flash.bin = SPL + DDR firmware + tee.bin + bl31.bin + u-boot.itb → SPL이 SRAM에서 동작 (DDR 초기화) → SPL이 DDR로 bl31.bin·tee.bin·u-boot.itb 적재 → BL31 점프 → BL31이 OP-TEE(BL32)·U-Boot(BL33) 시작SPL이 BL2 역할을 겸하는 경우입니다. RK3588은 BootROM이 직접 BL2를 적재하고 BL2가 BL31을 호출하는 더 표준적인 흐름입니다.
#TF-A 소스 트리
trusted-firmware-a/├── bl1/├── bl2/├── bl2u/├── bl31/├── bl32/├── plat/│ ├── nxp/│ │ └── imx/│ │ └── imx8m/│ │ ├── imx8mp/│ │ ├── imx8mq/│ │ └── ...│ ├── rockchip/│ │ ├── rk3588/│ │ └── ...│ ├── ti/│ │ ├── k3/│ │ └── ...│ ├── st/│ │ └── stm32mp1/│ └── arm/├── drivers/├── lib/├── include/└── services/ └── std_svc/주요 디렉터리의 역할은 다음과 같습니다.
| 디렉터리 | 역할 |
|---|---|
bl31/ | EL3 runtime |
bl32/ | BL32 reference (TSP) |
plat/nxp/imx/imx8m/imx8mp/ | i.MX 8M Plus port |
plat/arm/ | Juno, FVP reference |
drivers/ | UART, crypto, GIC, console |
lib/ | libc, fdt, optee_utils |
services/std_svc/ | PSCI 등 표준 서비스 |
새 BSP에 TF-A를 통합할 때 직접 만드는 디렉터리는 plat/<vendor>/<soc>/<board>/ 하나입니다. SoC는 이미 vendor가 port를 maintain하고 있으므로 우리는 보드별 plat_io_storage.c·platform_def.h·plat_psci.c 정도만 만듭니다.
#BL31 빌드 — i.MX 8M Plus 사례
git clone https://github.com/ARM-software/arm-trusted-firmware.gitcd arm-trusted-firmware
export CROSS_COMPILE=aarch64-linux-gnu-
# BL31 빌드 (DEBUG=1이면 console 출력 포함)make PLAT=imx8mp bl31 DEBUG=1 LOG_LEVEL=40 \ BL32_BASE=0x56000000 BL32_SIZE=0x02000000
# 결과ls build/imx8mp/debug/bl31.binBL32_BASE·BL32_SIZE는 OP-TEE가 들어갈 자리입니다. OP-TEE 없이 부팅한다면 안 줘도 됩니다.
LOG_LEVEL은 BL31의 console 출력량입니다. 운용에서는 0(none) 또는 10(notice)이고, 개발에서는 40(verbose)으로 둡니다.
빌드 결과 bl31.bin은 위치 독립적 raw binary입니다. SPL이 미리 정해진 주소에 적재한 뒤 점프합니다. i.MX 8M Plus는 0x970000이 BL31의 entry입니다.
#platform_def.h — 보드별 메모리 맵
plat/nxp/imx/imx8m/imx8mp/include/platform_def.h에 보드의 메모리 맵이 정의됩니다.
#define BL31_BASE U(0x970000)#define BL31_LIMIT U(0x990000)
#define BL32_BASE U(0x56000000) /* OP-TEE */#define BL32_LIMIT U(0x58000000)
#define BL33_BASE U(0x40200000) /* U-Boot proper */
#define IMX_BOOT_UART_BASE U(0x30890000) /* UART2 */#define IMX_BOOT_UART_CLK_IN_HZ 24000000
#define PLATFORM_STACK_SIZE 0x800#define PLATFORM_CORE_COUNT 4
#define MAX_MMAP_REGIONS 16#define MAX_XLAT_TABLES 8
#define PLAT_NS_IMAGE_OFFSET BL33_BASEBL31_BASE는 SRAM 안의 작은 영역입니다(보통 64~128KB). DDR이 깨어나기 전부터 동작해야 하므로 SRAM에 머뭅니다.
BL32_BASE는 OP-TEE가 들어갈 DDR 안 영역입니다. 이 영역은 TZASC(TrustZone Address Space Controller)로 secure-only로 보호됩니다. Linux에서 이 주소를 read하면 fault가 납니다.
BL33_BASE는 U-Boot proper의 적재 주소입니다. U-Boot defconfig의 CONFIG_SYS_TEXT_BASE와 일치해야 합니다.
#PSCI 구현
TF-A의 가장 큰 가치는 PSCI(Power State Coordination Interface)입니다. PSCI는 ARM SoC의 CPU on/off, 시스템 reset, suspend를 표준화한 SMC API입니다.
/* plat/nxp/imx/imx8m/imx8mp/imx8mp_psci.c (요약) */
static int imx_pwr_domain_on(u_register_t mpidr){ unsigned int cpu_id = MPIDR_AFFLVL0_VAL(mpidr); uint64_t entry = BL31_BASE;
/* CPU 전원 켜기 (SoC 특정 GPC register) */ imx_set_cpu_secure_entry(cpu_id, entry); imx_set_cpu_pwr_on(cpu_id);
return PSCI_E_SUCCESS;}
static void imx_system_reset(void){ /* SoC reset (WDOG 또는 SCFW SMC) */ writel(SRC_BASE_ADDR + 0x0, 0x1); while (1) wfi();}
const plat_psci_ops_t imx_plat_psci_ops = { .pwr_domain_on = imx_pwr_domain_on, .pwr_domain_off = imx_pwr_domain_off, .pwr_domain_suspend = imx_pwr_domain_suspend, .pwr_domain_on_finish = imx_pwr_domain_on_finish, .pwr_domain_suspend_finish = imx_pwr_domain_suspend_finish, .system_off = imx_system_off, .system_reset = imx_system_reset, .validate_power_state = imx_validate_power_state, .get_sys_suspend_power_state = imx_get_sys_suspend_power_state,};
int plat_setup_psci_ops(uintptr_t sec_entrypoint, const plat_psci_ops_t **psci_ops){ imx_mailbox_init(sec_entrypoint); *psci_ops = &imx_plat_psci_ops; return 0;}이 ops 테이블을 등록해 두면 Linux의 arm,psci-1.0 드라이버가 SMC로 PSCI_CPU_ON을 호출할 때마다 imx_pwr_domain_on()이 실행됩니다.
#device tree에서의 PSCI
Linux DT에 PSCI node가 있어야 secondary CPU bring-up이 동작합니다.
psci { compatible = "arm,psci-1.0"; method = "smc";};
cpus { #address-cells = <1>; #size-cells = <0>;
cpu0: cpu@0 { device_type = "cpu"; compatible = "arm,cortex-a53"; reg = <0x0>; enable-method = "psci"; next-level-cache = <&A53_L2>; };
cpu1: cpu@1 { device_type = "cpu"; compatible = "arm,cortex-a53"; reg = <0x1>; enable-method = "psci"; next-level-cache = <&A53_L2>; }; /* cpu2, cpu3 동일 */};enable-method = "psci"가 PSCI 사용을 의미합니다. 이 한 줄이 없으면 Linux는 SMP 부팅을 안 합니다. Ch 9에서 SMP bring-up 디버깅을 자세히 다룹니다.
#TrustZone 메모리 분할
TF-A는 SoC의 TZASC(TrustZone Address Space Controller)를 설정해 DDR 일부를 secure-only로 만듭니다.
/* plat_arch_setup() 또는 bl31_plat_arch_setup()에서 호출 */
#define TZASC_BASE 0x32F80000
void imx_init_tzasc(void){ /* Region 0: 0x40000000 ~ 0x55FFFFFF — Non-secure (Linux DDR) */ writel(0x40000000, TZASC_BASE + 0x100); writel(0x55FFFFFF, TZASC_BASE + 0x104); writel(0xC0000000, TZASC_BASE + 0x110); /* NS access */
/* Region 1: 0x56000000 ~ 0x57FFFFFF — Secure-only (OP-TEE) */ writel(0x56000000, TZASC_BASE + 0x120); writel(0x57FFFFFF, TZASC_BASE + 0x124); writel(0x80000000, TZASC_BASE + 0x130); /* S-only access */
/* Region 2: 0x58000000 ~ 0xFFFFFFFF — Non-secure */ writel(0x58000000, TZASC_BASE + 0x140); writel(0xFFFFFFFF, TZASC_BASE + 0x144); writel(0xC0000000, TZASC_BASE + 0x150);}설정 후 OP-TEE 영역은 Linux에서 접근 불가입니다. dmesg에서 OP-TEE 영역에 read를 시도하면 SError exception이 발생합니다.
비슷한 컨트롤러로 GIC의 secure interrupt 그룹, IOMUX의 secure-only mux, SDMA의 secure channel 등이 있습니다. 모두 BL31이 설정합니다.
#OP-TEE를 BL32로 통합
OP-TEE를 secure OS로 함께 부팅한다면 BL32 슬롯에 들어갑니다.
# OP-TEE 빌드git clone https://github.com/OP-TEE/optee_os.gitcd optee_os
make CROSS_COMPILE64=aarch64-linux-gnu- CROSS_COMPILE=arm-linux-gnueabihf- \ PLATFORM=imx-mx8mpevk CFG_TEE_CORE_LOG_LEVEL=3 \ CFG_TZDRAM_START=0x56000000 CFG_TZDRAM_SIZE=0x02000000 \ CFG_TEE_RAM_VA_SIZE=0x00200000 \ O=out
# 결과ls out/core/tee-raw.bin이 tee-raw.bin을 bl31.bin과 함께 flash.bin에 묶습니다. i.MX의 경우 NXP imx-mkimage 도구가 이 묶음을 만듭니다.
# imx-mkimagemake SOC=iMX8MP flash_evk_tee \ AHAB_IMG=ahab-container.img \ BL33=u-boot.itb \ BL32=tee-raw.bin \ BL31=bl31.bin \ DDR_FW_PATH=lpddr4_pmu_train_*.bin
ls iMX8M/flash.bin이 flash.bin을 SD나 eMMC에 굽습니다. BootROM이 이 단일 binary를 적재해 분해한 뒤 각 단계로 점프합니다.
#SMC 호출 — Linux에서 BL31로
Linux에서 SMC instruction을 호출하면 EL3로 trap하면서 BL31이 핸들러를 실행합니다.
/* Linux kernel arch/arm64/include/asm/psci.h 흐름 */
static int __invoke_psci_fn_smc(unsigned long function_id, unsigned long arg0, unsigned long arg1, unsigned long arg2){ struct arm_smccc_res res; arm_smccc_smc(function_id, arg0, arg1, arg2, 0, 0, 0, 0, &res); return res.a0;}
/* PSCI_CPU_ON 호출 */psci_ops.cpu_on = __invoke_psci_fn_smc;psci_ops.cpu_on(0x84000003, mpidr, entry_point, context_id);arm_smccc_smc()는 단순히 smc #0 instruction을 실행합니다. 이 순간 EL3로 진입하고 BL31의 runtime_exceptions.S가 처리합니다.
Linux EL1 │ │ smc #0 ▼BL31 EL3 vector (runtime_exceptions.S) │ ▼smc_handler64 → runtime_svc_handler │ ▼std_svc_smc_handler → psci_smc_handler │ ▼psci_cpu_on → plat_psci_ops.pwr_domain_on │ ▼imx_pwr_domain_on (우리 SoC 코드) │ ▼ eretLinux EL1 (PSCI_E_SUCCESS)이 round trip이 μs 단위로 일어납니다. Linux는 이 SMC가 보안 처리되는 normal RPC처럼 사용합니다.
#SiP service — vendor 확장
표준 PSCI 외에도 vendor가 SoC-specific SMC service를 추가할 수 있습니다. 이를 SiP(Silicon Provider) service라고 부릅니다.
#define IMX_SIP_GPC 0xC2000000#define IMX_SIP_BUILDINFO 0xC2000003#define IMX_SIP_LPDDR_DVFS 0xC2000004#define IMX_SIP_AARCH32 0xC2000005#define IMX_SIP_SRC 0xC2000006#define IMX_SIP_HAB 0xC2000007
static uintptr_t imx_sip_handler(unsigned int smc_fid, u_register_t x1, u_register_t x2, u_register_t x3, u_register_t x4, void *cookie, void *handle, u_register_t flags){ switch (smc_fid) { case IMX_SIP_LPDDR_DVFS: SMC_RET1(handle, dram_dvfs_handler(x1, x2, x3)); case IMX_SIP_GPC: SMC_RET1(handle, imx_gpc_handler(x1, x2, x3)); case IMX_SIP_SRC: SMC_RET1(handle, imx_src_handler(x1, x2, x3, x4)); default: return SMC_UNK; }}
DECLARE_RT_SVC( imx_sip_svc, OEN_SIP_START, OEN_SIP_END, SMC_TYPE_FAST, NULL, imx_sip_handler);Linux의 i.MX DDR DVFS 드라이버는 이 SMC를 호출해 BL31에게 DDR 주파수 변경을 요청합니다. DDR controller register는 EL3-only이므로 Linux가 직접 못 만지고, BL31을 거쳐야 합니다.
#TF-A 디버깅
TF-A가 시작은 했는데 그 뒤가 멈춘다면 콘솔 출력이 가장 큰 단서입니다.
NOTICE: BL31: v2.10.0NOTICE: BL31: Built : 14:23:01, May 19 2026INFO: GICv3 with legacy support detected.INFO: ARM GICv3 driver initialized in EL3INFO: TZC: Configured region 0INFO: BL31: Initialising Exception Handling FrameworkINFO: BL31: Initializing runtime servicesINFO: BL31: cortex_a53: CPU workaround for 855873 was appliedINFO: BL31: Initializing BL32INFO: SPSR = 0x3c9INFO: Entry point address = 0x56000000NOTICE: OP-TEE 4.0.0 ...INFO: BL31: Preparing for EL3 exit to normal worldINFO: Entry point address = 0x40200000INFO: SPSR = 0x3c9이 로그가 전부 안 나오면 BL31의 어디서 멈췄는지 알아내는 게 다음 작업입니다. LOG_LEVEL=40으로 verbose하게 빌드한 뒤 console에서 마지막 출력을 봅니다.
JTAG으로 BL31의 entry(0x970000)에 break를 걸어 step-by-step 실행할 수도 있습니다. gdb-multiarch로 OpenOCD와 연결합니다.
(gdb) target remote :3333(gdb) symbol-file build/imx8mp/debug/bl31/bl31.elf(gdb) break bl31_main(gdb) cContinuing.Breakpoint 1, bl31_main () at bl31/bl31_main.c:7373 NOTICE("BL31: %s\n", version_string);.elf 파일이 있어야 source-level debug가 됩니다. make DEBUG=1로 빌드한 결과를 사용합니다.
#자주 하는 실수
#BL31 entry address가 SRAM 밖
BL31_BASE를 DDR 안에 두면 SRAM에서 동작해야 하는 부팅 초기에 DDR이 깨기 전이라 fault가 납니다. SoC vendor가 권장한 SRAM 안 주소를 사용합니다.
#PLATFORM_CORE_COUNT가 실제 코어 수와 다름
이 값이 부족하면 secondary CPU bring-up 시 PSCI가 invalid CPU로 거절합니다. 이 값이 많으면 BL31이 없는 CPU에 SMC를 보내려다 hang합니다.
#TZASC 영역이 OP-TEE 영역과 안 맞음
BL32_BASE·BL32_SIZE와 TZASC region 정의가 다르면 OP-TEE가 자기 영역인데 read fault를 받거나, Linux가 secure 영역에 접근 시도해서 SError를 받습니다. 두 정의를 반드시 일치시킵니다.
#BL33_BASE와 U-Boot의 CONFIG_SYS_TEXT_BASE 불일치
이 둘이 다르면 BL31이 점프했을 때 U-Boot이 링크된 주소가 아닌 곳에서 실행되어 GP 레지스터 참조가 망가집니다. 두 빌드 시스템에서 반드시 같은 주소를 씁니다.
#SiP SMC ID 충돌
vendor가 SiP service ID를 임의로 정해 표준 PSCI 영역(0x84000000~)과 겹치면 PSCI 동작이 망가집니다. SMC ID 공간을 명확히 분리합니다.
#secure-status = "okay" 잊음
DT에서 secure-only 디바이스를 정의할 때 status = "okay"만 두면 non-secure에서는 보이는데 secure에서는 안 보이는 상태가 됩니다. secure-status = "okay"를 함께 둡니다.
#정리
- ARMv8 BSP에서 TF-A의 BL31은 secure monitor로 EL3에 상주하며 PSCI·SMC·TrustZone을 책임집니다.
- BL2(또는 SPL)가 DDR을 초기화하고 BL31·BL32·BL33을 DDR에 적재한 뒤 BL31로 점프합니다.
plat/<vendor>/<soc>/<board>/에 메모리 맵(platform_def.h)과 PSCI 콜백(<board>_psci.c)을 추가하면 새 보드 port가 됩니다.- TZASC 컨트롤러로 DDR의 일부를 secure-only로 분리합니다. OP-TEE 영역이 대표입니다.
- DT의
psci { method = "smc"; }와cpu@N { enable-method = "psci"; }가 SMP 부팅을 가능하게 합니다. - Linux는 SMC instruction으로 BL31에 trap하며, 이 round trip이 PSCI·SiP service의 기반입니다.
- 디버깅은
LOG_LEVEL=40 DEBUG=1빌드 후 console 마지막 출력 + JTAG의 조합이 가장 효과적입니다.
#다음 편
Ch 8 — Linux 커널 설정에서는 TF-A가 깨운 환경 위에서 Linux 커널을 설정하고 빌드하는 흐름을 다룹니다.
#관련 항목
BSP Development · 7 of 21
- 1 BSP의 본질 분해 — 새 보드 부팅을 위한 코드의 자리
- 2 SoC 데이터시트 읽기 — Pin Mux·Clock·Memory Map 파악법
- 3 새 보드 Device Tree 설계 — node·property·phandle 작성 흐름
- 4 Pin Mux와 Clock Tree 분석 — 보드 부팅의 첫 두 관문
- 5 DDR 매개변수 결정 — 보드별 Timing·Training 추출
- 6 U-Boot 새 보드 포팅 — defconfig·board.c·DTS 작성 흐름
- 7 TF-A·TrustZone 통합 — BL31·secure world·SMC 흐름 적용
- 8 Linux 커널 BSP 설정 — defconfig·Kconfig·DT 통합
- 9 Multi-core SMP Bring-up — PSCI·Secondary CPU 깨우기
- 10 첫 부팅 추적 — 0%부터 login prompt까지의 단계 분석
- 11 부트로그 디버깅 — earlyprintk·loglevel·serial 추적
- 12 BSP 드라이버 추가 — 보드별 Peripheral 통합 흐름
- 13 BSP Power Management — Suspend/Resume·Runtime PM·Regulator
- 14 BSP Thermal과 Watchdog — Trip Point·Cooling Device·Hardware Reset
- 15 BSP 부트 시간 최적화 — Bootchart·initcall_debug·Parallel Init
- 16 BSP RootFS 통합 — Buildroot·Yocto와 보드별 패키지 묶기
- 17 BSP 이미지 패키징 — Flash Layout·Partition·GPT 설계
- 18 BSP OTA와 Field Recovery — A/B 슬롯·롤백·BootCount
- 19 BSP Stability Testing — Stress·Soak·Power Cycle 시나리오
- 20 BSP 양산 환경 구축 — CI/CD·재현 가능 빌드·서명
- 21 BSP 유지보수 — 업스트림 기여·커널 버전업·LTS 전략
관련 글
BSP 유지보수 — 업스트림 기여·커널 버전업·LTS 전략
BSP의 장기 유지 — 업스트림 기여로 부담 줄이기, LTS 버전 선택, 커널 버전업 전략.
같은 시리즈에서 이어 읽기
BSP 양산 환경 구축 — CI/CD·재현 가능 빌드·서명
BSP를 양산으로 옮기는 단계 — CI 빌드, 재현성, 코드 서명, 키 관리.
같은 시리즈에서 이어 읽기
BSP Stability Testing — Stress·Soak·Power Cycle 시나리오
양산 BSP의 안정성 검증 — stress 도구, 장기간 soak, thermal 사이클, EMC 영향.
같은 시리즈에서 이어 읽기
이 글을 참조하는 글 (9)
- Buildroot SDK 생성·배포 — make sdk와 application 워크플로 — Buildroot Practical
- Buildroot Toolchain 선택 — Internal vs External 비교 — Buildroot Practical
- BSP 드라이버 추가 — 보드별 Peripheral 통합 흐름 — BSP Development
- Multi-core SMP Bring-up — PSCI·Secondary CPU 깨우기 — BSP Development
- Linux 커널 BSP 설정 — defconfig·Kconfig·DT 통합 — BSP Development
- U-Boot 새 보드 포팅 — defconfig·board.c·DTS 작성 흐름 — BSP Development
- DDR 매개변수 결정 — 보드별 Timing·Training 추출 — BSP Development
- ARM 임베디드 부트 4단계 분해 — BL1·SPL·TPL·U-Boot Proper의 역할 — Bootloader Internals
- Das U-Boot vs TF-A vs EDK II — 임베디드 부트로더 생태계 비교 — Bootloader Internals