STM32 NVIC, 인터럽트 우선순위 완전 정복
"버튼 누르면 UART 수신이 씹혀요", "두 인터럽트가 동시에 터졌는데 어떻게 되나요?"
NVIC를 모르면 반드시 겪는 문제들입니다.
1. 인터럽트가 뭔가요?
CPU는 기본적으로 코드를 위에서 아래로 순서대로 실행합니다.
그런데 현실에서는 "지금 당장" 처리해야 할 일이 생깁니다.
- 버튼이 눌렸다
- UART로 데이터가 들어왔다
- 타이머가 만료됐다
이럴 때 CPU에게 "하던 일 잠깐 멈추고 이거 먼저 처리해"라고 신호를 보내는 것이 인터럽트 (Interrupt)입니다.
일반 실행 흐름:
main() ──────────────────────────────────────►
[코드 A] [코드 B] [코드 C] [코드 D]
인터럽트 발생 시:
main() ──────────────┐ ┌──────────────►
[코드 A] [코드 B] [코드 C] [코드 D]
│ ↑
▼ │ 복귀
[ISR 실행] ───┘
CPU는 하던 일을 잠깐 멈추고 ISR(Interrupt Service Routine)을 실행한 뒤, 원래 자리로 돌아옵니다.
2. NVIC란?
NVIC = Nested Vectored Interrupt Controller
이름을 풀면
| 단어 | 의미 |
| Nested | 인터럽트 안에서 더 높은 우선순위 인터럽트가 끼어들 수 있음 |
| Vectored | 각 인터럽트마다 처리 함수(ISR) 주소가 벡터 테이블에 등록됨 |
| Interrupt Controller | 인터럽트를 관리하는 하드웨어 블록 |
쉽게 말하면 "인터럽트 교통정리 담당자"입니다.
여러 인터럽트가 동시에 발생했을 때:
- 어떤 걸 먼저 처리할지 결정하고
- 더 급한 인터럽트가 오면 현재 ISR을 중단하고 그것부터 처리하게 해주는
하드웨어 모듈이 NVIC입니다.
💡NVIC는 ARM Cortex-M 코어에 내장된 기능입니다.
STM32 뿐 아니라 Cortex-M 기반 MCU라면 모두 동일한 개념이 적용됩니다.
3. NVIC 핵심 개념
우선순위 숫자, 낮을수록 높다
NVIC에서 우선순위 숫자가 낮을수록 높습니다.
Priority 0 → 가장 높은 우선순위 (가장 급함)
Priority 1 → 그 다음
Priority 2 → 그 다음
...
Priority 15 → 가장 낮은 우선순위
처음엔 헷갈리지만, "0번이 1등"이라고 기억하면 됩니다.
Priority Group (우선순위 그룹)
STM32는 우선순위를 두 파트로 나눠서 관리합니다.
4비트 우선순위 레지스터
┌────────────────────────────────────────┐
│ Preemption Priority │ Subpriority │
└────────────────────────────────────────┘
이 4비트를 어떻게 나눌지 결정하는 것이 Priority Group 설정입니다.
| Priority Group | Preemption 비트 | Subpriority 비트 | Preemption 단계 | Subpriority 단계 |
| NVIC_PRIORITYGROUP_0 | 0비트 | 4비트 | 1단계 | 16단계 |
| NVIC_PRIORITYGROUP_1 | 1비트 | 3비트 | 2단계 | 8단계 |
| NVIC_PRIORITYGROUP_2 | 2비트 | 2비트 | 4단계 | 4단계 |
| NVIC_PRIORITYGROUP_3 | 3비트 | 1비트 | 8단계 | 2단계 |
| NVIC_PRIORITYGROUP_4 | 4비트 | 0비트 | 16단계 | 1단계 |
STM32 HAL의 기본값은 NVIC_PRIORITYGROUP_4 (Preemption 4비트, Subpriority 없음)입니다.
4. Preemption vs Subpriority, 뭐가 다른가요?
이게 NVIC에서 가장 헷갈리는 부분입니다. 핵심만 짚겠습니다.
Preemption Priority (선점 우선순위)
"실행 중인 ISR을 중단시킬 수 있는가"를 결정합니다.
상황: ISR_A 실행 중 → ISR_B 발생
ISR_B의 Preemption이 ISR_A보다 높으면:
ISR_A 중단 → ISR_B 실행 → ISR_A 재개 ← 선점(Preemption) 발생
ISR_B의 Preemption이 ISR_A보다 낮거나 같으면:
ISR_A 완료 → ISR_B 실행 ← 선점 없음
Subpriority (부우 선순위)
"같은 Preemption 우선순위끼리 동시에 발생했을 때 누가 먼저냐"를 결정합니다.
- Subpriority는 선점을 일으키지 않습니다
- 동시에 pending 상태일 때 처리 순서만 결정합니다
정리
| 비교 항목 | Preemption Priority | Subpriority |
| 선점 가능 여부 | 가능 | 불가능 |
| 영향 범위 | 실행 중인 ISR을 끊음 | 대기 중인 인터럽트 순서만 결정 |
| 중요도 | 더 중요 | 덜 중요 |
💡실무 팁 : 대부분의 STM32 프로젝트에서는 NVIC_PRIORITYGROUP_4를 쓰고 Preemption만 관리합니다. Subpriority까지 쓰면 복잡도만 올라갑니다.
5. STM32 CubeMX에서 NVIC 설정하기
CubeMX에서 NVIC 설정은 두 곳에서 합니다.
① System Core → NVIC 탭
Pinout & Configuration
└── System Core
└── NVIC
├── Priority Group 선택 ← 전체 그룹 설정
└── 각 인터럽트별 Enable / Priority 설정
② 각 Peripheral 설정 탭의 NVIC 섹션
예를 들어 USART2를 설정할 때:
Connectivity → USART2 → NVIC Settings 탭
[✓] USART2 global interrupt Preemption: 1 Sub: 0
설정 예시 — 버튼(EXTI) + UART 수신 공존
Priority Group: NVIC_PRIORITYGROUP_4 (Preemption 4비트)
EXTI0 (버튼) Preemption: 2
USART2 (UART RX) Preemption: 1 ← 더 높은 우선순위
TIM2 (타이머) Preemption: 3
UART 수신이 더 급하다고 판단해 우선순위를 높게 (숫자 낮게) 설정한 예입니다.
6. 실제 코드로 확인하기
CubeMX가 생성해 주는 코드를 보면 NVIC 설정이 어떻게 되는지 이해할 수 있습니다.
HAL이 생성하는 초기화 코드
/* main.c 또는 stm32f4xx_hal_msp.c */
/* Priority Group 설정 (HAL_Init() 내부에서 자동 호출) */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
/* USART2 인터럽트 설정 */
HAL_NVIC_SetPriority(USART2_IRQn, 1, 0); // Preemption=1, Sub=0
HAL_NVIC_EnableIRQ(USART2_IRQn);
/* EXTI0 (버튼) 인터럽트 설정 */
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0); // Preemption=2, Sub=0
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
ISR 등록 — stm32 f4 xx_it.c
/* USART2 인터럽트 핸들러 */
void USART2_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart2); // HAL이 내부에서 콜백 호출
}
/* EXTI0 인터럽트 핸들러 */
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}
콜백 함수에서 실제 처리
/* UART 수신 완료 콜백 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART2)
{
/* 수신된 데이터 처리 */
/* ISR 안이므로 최대한 짧게 — 플래그 세우고 main에서 처리 권장 */
rx_flag = 1;
}
}
/* GPIO EXTI 콜백 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
/* 버튼 처리 */
}
}
선점이 실제로 일어나는 상황
/* 시나리오:
EXTI0 ISR 실행 중 → USART2 인터럽트 발생
USART2 Preemption(1) < EXTI0 Preemption(2) → 선점 발생 */
void EXTI0_IRQHandler(void)
{
// 여기 실행 중...
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
// ↑ 이 중간에 USART2 인터럽트가 오면
// EXTI0 ISR이 중단되고 USART2_IRQHandler가 먼저 실행됨
// USART2 완료 후 여기로 복귀
}
7. 자주 하는 실수 모음
❌ 실수 1: ISR 안에서 오래 걸리는 작업
/* 나쁜 예 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_Delay(100); // ISR 안에서 delay — 절대 금지!
process_large_buffer(); // 무거운 연산 — 금지!
}
/* 좋은 예 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
rx_flag = 1; // 플래그만 세우고
// 실제 처리는 main loop에서
}
ISR은 최대한 짧게, 플래그 세우고 빠져나오는 것이 원칙입니다.
❌ 실수 2: FreeRTOS와 함께 쓸 때 우선순위 범위 착각
FreeRTOS를 사용하는 경우 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 이상의 우선순위(낮은 숫자)를 가진 ISR 안에서는 FreeRTOS API를 호출하면 안 됩니다.
/* FreeRTOS 사용 시 — ISR 안에서는 FromISR 버전 사용 */
xQueueSendFromISR(queue, &data, &xHigherPriorityTaskWoken);
❌ 실수 3: Priority Group 중간에 변경
Priority Group을 바꾸면 이미 설정된 모든 인터럽트 우선순위 해석이 달라집니다. 반드시 초기화 시점에 한 번만 설정하세요.
8. 마치며
NVIC를 이해하면 임베디드 개발에서 겪는 타이밍 버그의 절반은 원인을 파악할 수 있게 됩니다.
우선순위 숫자가 낮을수록 높다.
Preemption은 선점, Subpriority는 대기 순서.
ISR은 짧게, 무거운 일은 main loop에서.
이 세 가지만 기억해도 NVIC 관련 문제의 대부분을 다룰 수 있습니다.
다음 글에서는 UART Interrupt 모드와 DMA 모드를 비교하며, NVIC 설정이 실제 통신 안정성에 어떤 영향을 주는지 실습해 볼 예정입니다.