TIL

STM32 NVIC, 인터럽트 우선순위 완전 정복

onepaperhoon 2026. 3. 24. 21:38
"버튼 누르면 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 설정이 실제 통신 안정성에 어떤 영향을 주는지 실습해 볼 예정입니다.