임베디드 면접 용어 사전
MCU·ARMvector table

벡터 테이블

예외·인터럽트별 핸들러 주소를 모아 둔 표. VTOR로 위치를 옮길 수 있어 부트로더 구성에 쓰인다.

예외·인터럽트가 발생했을 때 어느 함수로 점프할지 적어 둔 주소 표.

구조

오프셋   내용
0x00    초기 MSP 값          ← 함수 주소가 아니라 스택 꼭대기 주소!
0x04    Reset_Handler
0x08    NMI_Handler
0x0C    HardFault_Handler
0x10    MemManage_Handler
0x14    BusFault_Handler
0x18    UsageFault_Handler
…
0x3C    SysTick_Handler
0x40    IRQ0  (벤더 정의 시작)
0x44    IRQ1
…

0번 항목만 예외적으로 데이터(스택 주소)이고 나머지는 전부 함수 포인터다.

C로 작성하면

extern uint32_t _estack;

__attribute__((section(".isr_vector")))
void (* const vector_table[])(void) = {
    (void (*)(void))&_estack,      // 0: 스택 꼭대기
    Reset_Handler,
    NMI_Handler,
    HardFault_Handler,
    …
};

const가 중요하다 — 없으면 .data로 가서 RAM을 잡아먹고 링커 스크립트의 .isr_vector 배치와도 어긋난다.

링커 스크립트에서 가장 앞에 배치한다. .text : {

  • KEEP(*(.isr_vector)) — /* KEEP: 참조가 없어도 제거하지 말 것 */ (.text) } > FLASH

KEEP을 빠뜨리면 링커가 "아무도 참조하지 않는 배열"로 보고 가비지 컬렉션(--gc-sections)으로 제거해 버린다. 그러면 부팅 자체가 안 된다.

Thumb 비트 — 놓치기 쉬운 함정

Cortex-M의 함수 주소는 최하위 비트가 1이어야 한다(Thumb 상태).

C에서 함수 이름을 쓰면 컴파일러가 자동으로 +1 한 값을 넣는다 직접 계산한 주소를 넣으면 0이 되어 → HardFault (Invalid State)

void (*app)(void) = (void (*)(void))(*(uint32_t *)(APP_ADDR + 4));
app();     // 플래시에서 읽은 값에는 이미 Thumb 비트가 포함되어 있다

부트로더에서 앱으로 점프할 때 플래시에 저장된 값을 그대로 쓰면 안전하다.

약한 심볼 — 기본 핸들러

void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));

void Default_Handler(void) { while (1); }    // 무한 루프

사용자가 같은 이름의 함수를 정의하면 그것이 우선하고, 정의하지 않으면 Default_Handler가 연결된다.

여기서 유명한 사고가 나온다.

  • ① 핸들러 이름의 철자를 틀린다 (USART1_IRQHandler → USART_IRQHandler)
  • ② C++ 파일에 작성하면서 extern "C" 를 빠뜨린다 (이름 맹글링)
  • 둘 다 링크는 성공한다. 약한 심볼이 그대로 남으므로
  • 인터럽트가 발생하면 Default_Handler 의 무한 루프에 빠진다
  • "인터럽트가 안 걸린다"고 착각하지만 실제로는 걸리고 있다

디버거로 멈춰 보면 Default_Handler에 있는 것으로 확인된다. 이 증상을 알아보는 것이 임베디드 디버깅의 기본기다.

VTOR — 테이블 위치 옮기기

SCB->VTOR = 0x08008000;   // 앱의 벡터 테이블 위치
__DSB();

용도

  • ① 부트로더 → 애플리케이션 전환
  • ② RAM에 테이블을 복사해 런타임에 핸들러를 바꾸기
  • ③ 여러 펌웨어 뱅크(A/B 업데이트)

정렬 제약 — VTOR은 테이블 크기 이상의 2의 거듭제곱으로 정렬되어야 한다. 인터럽트가 60개면 테이블이 약 400바이트이므로 512바이트 정렬이 필요하다. 링커 스크립트에서 . = ALIGN(512); 를 넣어야 한다. 이 제약을 놓치면 일부 인터럽트만 이상하게 동작한다.

RAM에 두는 경우

static void (*ram_vectors[64])(void) __attribute__((aligned(512)));

memcpy(ram_vectors, (void *)SCB->VTOR, sizeof(ram_vectors));
ram_vectors[UART_IRQn + 16] = my_new_handler;   // 런타임 교체
SCB->VTOR = (uint32_t)ram_vectors;

드라이버를 동적으로 붙이거나 테스트에서 핸들러를 바꿔 낄 때 쓴다. +16은 시스템 예외 16개를 건너뛰는 오프셋이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — MCU·ARM Cortex-M — 레지스터·버스·동작모드·NVIC·부팅