예외·인터럽트가 발생했을 때 어느 함수로 점프할지 적어 둔 주소 표.
구조
오프셋 내용
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개를 건너뛰는 오프셋이다.