Cortex-M에 내장된 인터럽트 컨트롤러(Nested Vectored Interrupt Controller). 이름 그대로 중첩(Nested) 과 벡터(Vectored) 를 지원한다.
이름의 세 요소
-
Nested — : 우선순위가 높은 인터럽트가 낮은 것을 중단시킬 수 있다 Vectored : 인터럽트마다 핸들러 주소가 정해져 있어 바로 점프
- (하나의 핸들러에서 원인을 찾는 방식이 아니다) Interrupt Controller : 활성화·우선순위·대기 상태를 관리
Vectored의 이점 — 원인을 판별하는 코드가 필요 없어 지연이 짧다. AVR 등 일부 MCU는 공통 핸들러에서 원인을 조사해야 한다.
주요 레지스터
NVIC_EnableIRQ(USART1_IRQn); // ISER — 활성화
NVIC_DisableIRQ(USART1_IRQn); // ICER
NVIC_SetPriority(USART1_IRQn, 5); // IPR — 우선순위
NVIC_ClearPendingIRQ(USART1_IRQn); // ICPR — 대기 취소
NVIC_GetPendingIRQ(USART1_IRQn); // ISPR
우선순위 — 숫자가 작을수록 높다
0 = 가장 높음 15 = 가장 낮음 (4비트 구현 기준)
직관과 반대라 자주 헷갈린다. "1순위, 2순위"처럼 생각하면 된다.
우선순위 그룹 — 면접에서 자주 묻는다
우선순위 비트를 선점(preempt) 우선순위와 부(sub) 우선순위로 나눈다.
NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
4비트를 2:2 로 분할
┌───────────┬──────────┐
│ 선점 2비트│ 부 2비트 │
└───────────┴──────────┘
0~3 0~3
| 역할 | |
|---|---|
| 선점 우선순위 | 다른 인터럽트를 중단시킬 수 있는가를 결정 |
| 부 우선순위 | 선점 우선순위가 같을 때 누가 먼저 처리될지 |
선점 우선순위가 같으면 → 중첩되지 않는다. 순서대로 처리 선점 우선순위가 다르면 → 높은 쪽이 낮은 쪽을 중단시킨다
부 우선순위는 중첩을 만들지 못한다. 이미 실행 중인 것을 끊지는 못하고, 대기 중인 것들 사이의 순서만 정한다.
실무 설계 그룹 4 (선점 4비트, 부 0비트) → 모든 인터럽트가 서로 선점 가능 그룹 0 (선점 0비트, 부 4비트) → 중첩 없음. 순서만 관리
STM32 HAL은 기본적으로 그룹 4를 쓴다.
중첩의 위험 — 스택
- 우선순위가 다른 인터럽트 5개가 겹치면
- 자동 스태킹 32바이트 × 5 = 160바이트
-
- 각 핸들러의 지역 변수
MSP 크기를 정할 때 최악의 중첩을 고려해야 한다. 중첩을 원하지 않으면 우선순위를 모두 같게 두면 된다.
인터럽트 마스킹
__disable_irq(); // PRIMASK = 1 — 모든 인터럽트 차단 (NMI, HardFault 제외)
__enable_irq();
__set_BASEPRI(0x40); // 이 값보다 낮은 우선순위만 차단 ★
__set_BASEPRI(0); // 해제
★ BASEPRI가 실무에서 중요하다. 전체를 막으면 긴급한 안전 인터럽트까지 막히지만, BASEPRI를 쓰면 일반 인터럽트만 막고 최우선 인터럽트는 살려 둘 수 있다.
FreeRTOS가 이 방식을 쓴다 — configMAX_SYSCALL_INTERRUPT_PRIORITY보다
높은 우선순위의 ISR은 커널이 절대 막지 않으며, 대신 그 ISR에서는
RTOS API를 부르면 안 된다.
대기(pending) 상태 — 놓치기 쉬운 함정
인터럽트가 발생했지만 아직 처리되지 않은 상태 = pending
- 인터럽트를 비활성화해도 pending 플래그는 남는다
- 다시 활성화하는 순간 즉시 발생한다
NVIC_DisableIRQ(EXTI0_IRQn);
// … 설정 변경 …
NVIC_ClearPendingIRQ(EXTI0_IRQn); // ★ 이걸 안 하면 즉시 튄다
NVIC_EnableIRQ(EXTI0_IRQn);
부트로더에서 앱으로 점프할 때 이 정리를 빠뜨려 앱이 시작하자마자 없는 핸들러로 가는 사고가 흔하다.
플래그 클리어 순서 — 무한 인터럽트의 원인
void EXTI0_IRQHandler(void) {
handle();
EXTI->PR = EXTI_PR_PR0; // ★ 주변장치 플래그를 반드시 지운다
}
주변장치의 플래그를 안 지우면 핸들러를 나가자마자 다시 진입해 무한 루프에 빠진다. "인터럽트가 계속 발생한다"의 원인 1위다.
더 미묘한 문제 — 플래그를 지운 직후 바로 리턴하면, 쓰기가 버퍼에 남아 있어 CPU가 아직 플래그가 있다고 보고 재진입할 수 있다.
EXTI->PR = EXTI_PR_PR0;
__DSB(); // 쓰기 완료를 보장
드물지만 고속 인터럽트에서 실제로 발생한다.