"버튼 누르면 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 설정이 실제 통신 안정성에 어떤 영향을 주는지 실습해 볼 예정입니다.
'TIL' 카테고리의 다른 글
| UART 통신, 제대로 이해하기 (0) | 2026.03.24 |
|---|
