멀티코어 ARM SoC가 부팅할 때 인터럽트 컨트롤러(GIC)는 커널이 넘겨받기 한참 전, EL3(Secure Monitor)에서부터 설정돼 있어야 한다. 초기화가 빠지거나 순서가 틀리면 커널 입장에서는 GIC가 있는데도 인터럽트가 아예 안 들어오거나 특정 코어만 조용한 상태로 부팅된다. TF-A(Trusted Firmware-A, 예전 이름 ARM Trusted Firmware/ATF)는 GICv3 드라이버를 자체 내장하고 있고, 플랫폼 포트는 이 드라이버가 제공하는 4개 함수를 정해진 순서로 호출하기만 하면 된다. 이 글에서는 TF-A 공식 저장소의 실제 소스(QEMU 플랫폼 포트 기준)로 이 초기화 흐름을 정리한다.
드라이버 데이터 구조체
플랫폼은 자신의 GIC 레지스터 베이스 주소와 인터럽트 속성을 gicv3_driver_data_t 구조체 하나에 채워 드라이버에 넘긴다.
typedef struct gicv3_driver_data {
uintptr_t gicd_base;
uintptr_t gicr_base;
const interrupt_prop_t *interrupt_props;
unsigned int interrupt_props_num;
unsigned int rdistif_num;
uintptr_t *rdistif_base_addrs;
mpidr_hash_fn mpidr_to_core_pos;
} gicv3_driver_data_t;gicd_base는 Distributor(전역 설정) 레지스터, gicr_base는 Redistributor(코어별 설정) 레지스터의 시작 주소다. rdistif_base_addrs는 코어마다 다른 Redistributor 프레임 주소를 담을 배열로, 드라이버 초기화 과정에서 자동으로 채워진다.
초기화 함수 4단계
| 함수 | 호출 시점 | 역할 |
|---|---|---|
gicv3_driver_init(data) | 1회 (primary CPU) | 드라이버 데이터 검증, GIC 버전 확인, 코어별 Redistributor 주소 탐색 |
gicv3_distif_init(void) | 1회 | Distributor(GICD) 활성화 — ARE_S/ARE_NS 비트 설정, SPI 기본 속성·그룹 구성 |
gicv3_rdistif_init(proc_num) | 코어마다 | 호출 코어의 Redistributor 전원 on, SGI/PPI 기본 속성 설정 |
gicv3_cpuif_enable(proc_num) | 코어마다 | 시스템 레지스터(ICC_*_EL1/EL3)로 CPU 인터페이스 활성화, 인터럽트 그룹 enable |
앞의 두 함수는 GIC 전체에 딱 한 번만 적용되는 전역 설정이고, 뒤의 두 함수는 코어마다 그 코어를 대표하는 Redistributor·CPU 인터페이스를 개별적으로 깨우는 작업이라 코어 수만큼 반복 호출해야 한다.
QEMU 플랫폼 포트의 실제 호출 순서
TF-A가 지원하는 QEMU virt 플랫폼의 qemu_gicv3.c 전체다. 네 함수가 실제로 어떤 순서로 엮이는지 그대로 보여준다.
static const interrupt_prop_t qemu_interrupt_props[] = {
PLATFORM_G1S_PROPS(INTR_GROUP1S),
PLATFORM_G0_PROPS(INTR_GROUP0)
};
static uintptr_t qemu_rdistif_base_addrs[PLATFORM_CORE_COUNT];
static unsigned int qemu_mpidr_to_core_pos(unsigned long mpidr)
{
return (unsigned int)plat_core_pos_by_mpidr(mpidr);
}
static const gicv3_driver_data_t qemu_gicv3_driver_data = {
.gicd_base = GICD_BASE,
.gicr_base = GICR_BASE,
.interrupt_props = qemu_interrupt_props,
.interrupt_props_num = ARRAY_SIZE(qemu_interrupt_props),
.rdistif_num = PLATFORM_CORE_COUNT,
.rdistif_base_addrs = qemu_rdistif_base_addrs,
.mpidr_to_core_pos = qemu_mpidr_to_core_pos
};
void plat_qemu_gic_init(void)
{
gicv3_driver_init(&qemu_gicv3_driver_data);
gicv3_distif_init();
gicv3_rdistif_init(plat_my_core_pos());
gicv3_cpuif_enable(plat_my_core_pos());
}plat_qemu_gic_init()은 BL31(EL3 런타임 펌웨어) 부팅 과정에서 호출되며, primary CPU가 이 네 줄을 순서대로 실행한다. gicv3_cpuif_enable() 내부를 보면 왜 순서가 중요한지 드러난다 — Redistributor가 아직 깨어있지 않은 상태에서 CPU 인터페이스부터 활성화하면 레지스터 접근이 정의되지 않은 동작으로 이어진다.
void gicv3_cpuif_enable(unsigned int proc_num)
{
uintptr_t gicr_base;
u_register_t scr_el3;
unsigned int icc_sre_el3;
assert(gicv3_driver_data != NULL);
assert(proc_num < gicv3_driver_data->rdistif_num);
assert(gicv3_driver_data->rdistif_base_addrs != NULL);
assert(IS_IN_EL3());
/* Mark the connected core as awake */
gicr_base = gicv3_driver_data->rdistif_base_addrs[proc_num];
gicv3_rdistif_mark_core_awake(gicr_base);
/* Program the idle priority in the PMR */
write_icc_pmr_el1(GIC_PRI_MASK);
/* Enable Group0 interrupts */
write_icc_igrpen0_el1(IGRPEN1_EL1_ENABLE_G0_BIT);
/* Enable Group1 Secure interrupts */
write_icc_igrpen1_el3(read_icc_igrpen1_el3() |
IGRPEN1_EL3_ENABLE_G1S_BIT);
/* ... SRE 레지스터 설정 등 생략 ... */
}gicr_base는 rdistif_base_addrs[proc_num]에서 가져온다 — 이 배열이 비어 있으면(즉 gicv3_rdistif_init()이 먼저 그 코어의 Redistributor를 깨워두지 않았으면) 이 함수는 애초에 유효한 값을 읽을 수 없다.
코어 전원 관리와의 연결
부팅 시 한 번 실행되는 plat_qemu_gic_init()과 별개로, 코어가 나중에 오프라인됐다가 다시 켜질 때는 driver_init/distif_init을 다시 부르지 않는다. 전역 설정은 그대로 두고 코어별 설정만 다시 해주면 되기 때문이다.
void qemu_pwr_gic_on_finish(void)
{
gicv3_rdistif_init(plat_my_core_pos());
gicv3_cpuif_enable(plat_my_core_pos());
}
void qemu_pwr_gic_off(void)
{
gicv3_cpuif_disable(plat_my_core_pos());
gicv3_rdistif_off(plat_my_core_pos());
}PSCI(Power State Coordination Interface)로 코어를 끄고 켤 때 이 두 함수가 각각 CPU_OFF/CPU_ON 경로에서 호출된다. 부팅 시퀀스 전체(driver_init → distif_init → rdistif_init → cpuif_enable)와 전원 재개 시퀀스(rdistif_init → cpuif_enable)가 뒤의 두 단계만 공유한다는 걸 코드로 바로 확인할 수 있다.
주의사항
| 항목 | 내용 |
|---|---|
| 함수 시그니처는 TF-A 버전마다 다르다 | 과거 버전 문서·블로그에는 gicv3_distif_init(uintptr_t gicd_base)처럼 주소를 직접 인자로 받는 시그니처가 남아있는 경우가 있다. 현재(main 브랜치) 기준으로는 gicd_base 등을 gicv3_driver_init() 시점에 등록한 gicv3_driver_data 전역 포인터에서 읽어오므로 나머지 3개 함수는 인자를 받지 않거나 proc_num만 받는다 |
| rdistif_init·cpuif_enable은 코어마다 호출해야 한다 | primary CPU에서 한 번만 부르면 나머지 코어는 Redistributor가 잠든 채로 남아 인터럽트를 못 받는다. secondary CPU 부팅 경로나 PSCI CPU_ON 핸들러에서 각 코어 자신이 호출해야 한다 |
| driver_init은 캐시 코히런시를 신경 써야 한다 | HW_ASSISTED_COHERENCY가 없는 플랫폼은, secondary CPU가 캐시 꺼진 상태로 부팅하며 이 드라이버 데이터를 읽으므로 primary CPU 쪽에서 flush_dcache_range()로 명시적으로 밀어줘야 한다(실제 소스에도 이 처리가 들어있다) |
| 이 글의 코드는 실행이 아니라 소스 인용이다 | TF-A는 부트 펌웨어라 일반 리눅스 서버에서 재현 실행이 어렵다. 위 코드는 TF-A 공식 저장소(ARM-software/arm-trusted-firmware, main 브랜치)에서 그대로 가져온 것이며, 직접 빌드해 QEMU로 부팅까지는 확인하지 않았다 |
마무리
TF-A의 GICv3 초기화는 driver_init(전역, 1회) → distif_init(전역, 1회) → rdistif_init(코어별) → cpuif_enable(코어별) 네 단계로 고정돼 있고, 플랫폼 포트가 할 일은 gicv3_driver_data_t에 자신의 레지스터 주소를 채우고 이 순서를 지켜 호출하는 것뿐이다. 새 보드를 포팅할 때 인터럽트가 특정 코어에서만 안 들어온다면, 이 네 단계 중 코어별 단계(rdistif_init/cpuif_enable)가 해당 코어의 부팅 경로에서 빠졌는지부터 의심해볼 만하다.