시작하기 앞서 ARM cortex-M의 NVIC(Nested Vector Interrupt Controller) 구조를 설명하자면 NVIC는 인터럽트를 효율적으로 관리하기 위해 CPU와 밀접하게 결합된 하드웨어 모듈입니다. 중첩 인터럽트(Nested Inturrupts) 구조로 더 높은 우선순위의 인터럽트가 발생하면 현재 실행 중인 인터럽트 서비스 루틴(ISR)을 중단하고 즉시 실행시키는 방식에 더해 백터화된 처리(Vectored) 방법을 사용해 인터럽트 발생 시 하드웨어가 직접 해당 ISR의 주소(Vector)를 찾아가기 때문에 소프트웨어적으로 분기 처리할 필요가 없어 지연 시간(Latency)이 짧은 특징이 있습니다.
폴링(Polling) vs 인터럽트(Interrupt)
폴링 방식:
CPU가 주기적으로 상태를 확인
while (1)
{
if (button_pressed()) // 계속 확인
do_something();
}
단점: CPU 자원 낭비, 다른 작업 불가
인터럽트 방식:
이벤트 발생 시 CPU에 신호 → CPU가 현재 작업 중단 → ISR 실행 → 복귀
장점: CPU 효율적 사용, 빠른 응답, 다중 이벤트 처리
인터럽트 처리 흐름
정상 실행 중
↓
인터럽트 발생 (버튼, 타이머, UART 등)
↓
현재 상태 저장 (Context Save) → 스택에 PC, LR, R0-R3, R12, xPSR 자동 저장
↓
ISR(Interrupt Service Routine) 실행
↓
현재 상태 복원 (Context Restore)
↓
인터럽트 발생 지점으로 복귀
Exception vs Interrupt
Exception은 CPU가 처리해야 하는 모든 예외적인 상황을 아우르는 가장 큰 개념이며, Interrupt는 그 중 일부인 외부 신호를 의미합니다.
Exception (예외): ARM Cortex-M 코어 내부에서 발생
- Reset
- NMI (Non-Maskable Interrupt)
- HardFault
- MemManage
- BusFault
- UsageFault
- SVCall
- PendSV
- SysTick
Interrupt (인터럽트): 외부 주변장치(Peripheral)에서 발생
- EXTI (External Interrupt) - GPIO 핀
- TIM (Timer)
- USART
- SPI, I2C
- ADC
- DMA
- 기타 주변장치
IRQ(Interrupt Request Queue) 번호
Exception 번호: 음수 (ARM 정의)
Reset = -15
NMI = -14
HardFault = -13
SysTick = -1
Interrupt 번호: 양수 (제조사 정의, STM32F4 기준)
WWDG = 0
EXTI0 = 6
TIM1_UP = 25
USART1 = 37
...
NVIC(Nested Vectored Interrupt Controller)
ARM Cortex-M의 인터럽트 컨트롤러
핵심 기능:
1. 인터럽트 활성화/비활성화
2. 우선순위 설정
3. 인터럽트 중첩(Nesting) 처리
4. 인터럽트 보류(Pending) 상태 관리
5. 벡터 테이블을 통한 ISR 주소 관리
특징:
- 최대 240개의 외부 인터럽트 지원 (STM32F4는 약 90개 사용)
- 하드웨어 기반 우선순위 처리 → 소프트웨어 오버헤드 최소화
- Tail-chaining: 연속된 인터럽트를 효율적으로 처리
- Late-arriving: 더 높은 우선순위 인터럽트 자동 처리
NVIC 레지스터 구성
ISER (Interrupt Set-Enable Register) - 인터럽트 활성화
ICER (Interrupt Clear-Enable Register) - 인터럽트 비활성화
ISPR (Interrupt Set-Pending Register) - 소프트웨어로 인터럽트 발생
ICPR (Interrupt Clear-Pending Register) - Pending 상태 제거
IABR (Interrupt Active Bit Register) - 현재 처리 중인 인터럽트 확인
IPR (Interrupt Priority Register) - 우선순위 설정
접근 방법 (직접):
NVIC->ISER[0] |= (1 << IRQ_NUMBER); // 활성화
NVIC->IPR[n] = priority << 4; // 우선순위 (상위 4비트 사용)
접근 방법 (HAL):
HAL_NVIC_EnableIRQ(IRQn);
HAL_NVIC_SetPriority(IRQn, preempt, sub);
벡터 테이블 구조
플래시 시작 주소(0x08000000)에 위치
오프셋 내용
0x0000 스택 초기 주소 (MSP)
0x0004 Reset_Handler 주소
0x0008 NMI_Handler 주소
0x000C HardFault_Handler 주소
0x0010 MemManage_Handler 주소
0x0014 BusFault_Handler 주소
0x0018 UsageFault_Handler 주소
...
0x0040 SysTick_Handler 주소
0x0044 WWDG_IRQHandler 주소 (IRQ 0)
0x0048 PVD_IRQHandler 주소 (IRQ 1)
...
0x009C EXTI0_IRQHandler 주소 (IRQ 6)
...
인터럽트 발생 시:
1. NVIC가 해당 IRQ 번호 확인
2. 벡터 테이블에서 ISR 주소 읽기
3. 해당 주소로 점프하여 ISR 실행
startup_stm32f4xx.s 에서의 벡터 테이블
; startup 파일에서 실제 벡터 테이블 정의
g_pfnVectors:
.word _estack ; 스택 최상위 주소
.word Reset_Handler
.word NMI_Handler
.word HardFault_Handler
; ...
.word EXTI0_IRQHandler
; ...
.word TIM1_UP_TIM10_IRQHandler
; ...
.word USART1_IRQHandler
벡터 테이블 재배치 (VTOR)
// 부트로더 사용 시 벡터 테이블 주소 변경
// SCB->VTOR 레지스터에 새 주소 기록
SCB->VTOR = FLASH_BASE | 0x20000; // 예: 0x08020000
// CubeMX 생성 코드에서는 SystemInit() 내에서 처리됨
Priority 레지스터 구조
ARM Cortex-M의 IPR 레지스터: 8비트
STM32F4는 상위 4비트만 사용 (하위 4비트 무시)
8비트 중 사용: [7:4] → 0~15 단계
값이 낮을수록 우선순위가 높음
비교:
IPR = 0x00 → 최고 우선순위 (0)
IPR = 0xF0 → 최저 우선순위 (15)
Priority 비트 분할: Priority Group
4비트를 두 그룹으로 분할:
1. Preempt Priority (선점 우선순위): 상위 비트
2. Sub Priority (부 우선순위): 하위 비트
Priority Group 설정 (SCB->AIRCR 레지스터):
NVIC_PRIORITYGROUP_0: [0비트 선점 / 4비트 서브] → 선점 0단계, 서브 0~15
NVIC_PRIORITYGROUP_1: [1비트 선점 / 3비트 서브] → 선점 0~1, 서브 0~7
NVIC_PRIORITYGROUP_2: [2비트 선점 / 2비트 서브] → 선점 0~3, 서브 0~3
NVIC_PRIORITYGROUP_3: [3비트 선점 / 1비트 서브] → 선점 0~7, 서브 0~1
NVIC_PRIORITYGROUP_4: [4비트 선점 / 0비트 서브] → 선점 0~15, 서브 없음 (기본값)
Preempt Priority (선점 우선순위)
핵심 개념: 인터럽트 중첩(Nesting) 결정
낮은 Preempt Priority 값 = 더 높은 우선순위
동작:
ISR_A 실행 중 → ISR_B 발생
- ISR_B의 Preempt Priority < ISR_A → ISR_A 중단, ISR_B 먼저 실행 (선점)
- ISR_B의 Preempt Priority >= ISR_A → ISR_B는 ISR_A 완료 후 실행 (선점 불가)
예시 (Priority Group 4 기준):
TIM1_IRQ : Preempt = 1 (높은 우선순위)
USART1_IRQ: Preempt = 3 (낮은 우선순위)
USART1 ISR 실행 중 → TIM1 인터럽트 발생
→ USART1 ISR 중단 → TIM1 ISR 실행 → 복귀 → USART1 ISR 재개
Sub Priority (부 우선순위)
핵심 개념: 동일 Preempt Priority에서의 처리 순서 결정
인터럽트 중첩에는 영향을 주지 않음
동작:
동일 Preempt Priority를 가진 인터럽트 A, B가 동시 발생
- Sub Priority가 낮은 쪽이 먼저 처리됨
- 어느 쪽도 상대방을 선점하지 못함
예시:
EXTI0_IRQ: Preempt = 2, Sub = 0 (먼저 처리)
EXTI1_IRQ: Preempt = 2, Sub = 1 (나중 처리)
EXTI0, EXTI1 동시 발생 → EXTI0 먼저, EXTI1 대기 → EXTI0 완료 후 EXTI1 실행
단, EXTI1 ISR 실행 중 EXTI0 발생해도 선점 불가 (Preempt 동일)
우선순위 비교 요약
Preempt Priority Sub Priority
선점 가능 여부 O X
동시 발생 처리순서 O O
값이 낮을수록 높은 우선순위 높은 우선순위
실무 가이드라인
NVIC_PRIORITYGROUP_4 (기본값, 권장):
- 선점 0~15단계, 서브 없음
- 단순하고 직관적
- 중첩 처리만 필요한 대부분의 경우 적합
- CubeMX 기본 설정
NVIC_PRIORITYGROUP_2:
- 선점 0~3단계, 서브 0~3단계
- 중첩과 동시 발생 모두 세분화 필요한 경우
NVIC_PRIORITYGROUP_0:
- 선점 없음, 서브만 15단계
- 인터럽트 중첩을 허용하지 않을 때
- 단순한 시스템
주의: Priority Group은 시스템 전체에서 하나만 설정
실행 중 변경 금지 (예측 불가 동작)
NVIC 설정 화면
1. System Core → NVIC 선택
2. Priority Group 설정
- NVIC → Priority Group 드롭다운에서 선택
- 기본값: 4 bits for pre-emption priority, 0 bits for subpriority
3. 각 인터럽트 설정
- Enable 체크박스 체크
- Preemption Priority 값 입력 (0이 최고)
- Sub Priority 값 입력
4. Code Generation → 자동으로 MX_NVIC_Init() 생성
주요 인터럽트 권장 우선순위 (Priority Group 4 기준)
SysTick : Preempt = 15 (가장 낮게, HAL_Delay 기반)
USART1 : Preempt = 5 (빠른 응답 필요)
TIM 일반 : Preempt = 6
EXTI (버튼) : Preempt = 10 (낮은 우선순위 무방)
DMA : Preempt = 4 (데이터 손실 방지)
기본 함수
// 우선순위 그룹 설정 (시스템 초기화 시 1회만)
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
// 우선순위 설정
// IRQn: 인터럽트 번호 (IRQn_Type 열거형)
// PreemptPriority: 선점 우선순위
// SubPriority: 부 우선순위
HAL_NVIC_SetPriority(USART1_IRQn, 5, 0);
// 인터럽트 활성화
HAL_NVIC_EnableIRQ(USART1_IRQn);
// 인터럽트 비활성화
HAL_NVIC_DisableIRQ(USART1_IRQn);
// 소프트웨어로 인터럽트 발생 (테스트용)
HAL_NVIC_SetPendingIRQ(USART1_IRQn);
// Pending 상태 확인
uint32_t pending = HAL_NVIC_GetPendingIRQ(USART1_IRQn);
// Pending 상태 제거
HAL_NVIC_ClearPendingIRQ(USART1_IRQn);
// 현재 활성(Active) 인터럽트 확인
uint32_t active = HAL_NVIC_GetActive(USART1_IRQn);
// 우선순위 읽기
uint32_t preempt, sub;
HAL_NVIC_GetPriority(USART1_IRQn, NVIC_PRIORITYGROUP_4, &preempt, &sub);
전역 인터럽트 제어
// 모든 인터럽트 비활성화 (임계 구역 진입)
__disable_irq(); // 또는 PRIMASK 레지스터 직접 조작
// 모든 인터럽트 활성화 (임계 구역 종료)
__enable_irq();
// 임계 구역 예시 (공유 변수 보호)
void update_shared_data(void)
{
__disable_irq();
shared_variable = new_value; // 원자적 처리 보장
__enable_irq();
}
// NMI, HardFault를 제외한 인터럽트 마스킹 (BASEPRI 레지스터)
// 특정 우선순위 이하만 마스킹 (FreeRTOS에서 사용)
__set_BASEPRI(priority_value);
__set_BASEPRI(0); // 마스킹 해제
ISR 함수명 규칙
// 함수명은 startup 파일의 벡터 테이블과 반드시 일치
// stm32f4xx_it.c 파일에 작성
void EXTI0_IRQHandler(void) // EXTI 0번 핀
void EXTI1_IRQHandler(void) // EXTI 1번 핀
void EXTI15_10_IRQHandler(void) // EXTI 10~15번 핀 (공유)
void TIM1_UP_TIM10_IRQHandler(void) // TIM1 Update, TIM10
void USART1_IRQHandler(void) // USART1
void DMA1_Stream0_IRQHandler(void) // DMA1 Stream0
HAL 방식 ISR 구현
// stm32f4xx_it.c
void EXTI0_IRQHandler(void)
{
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); // HAL 핸들러 호출
// HAL이 내부적으로 Pending 클리어 후 콜백 호출
}
// main.c 또는 별도 파일
// 콜백 함수 오버라이드 (__weak 키워드 + 함수)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0)
{
// 실제 처리 코드 작성
button_flag = 1;
}
}
직접 ISR 구현 (HAL 미사용)
void EXTI0_IRQHandler(void)
{
// Pending 비트 확인
if (EXTI->PR & EXTI_PR_PR0)
{
// Pending 비트 클리어 (1을 써서 클리어)
EXTI->PR |= EXTI_PR_PR0;
// 처리 코드
button_flag = 1;
}
}
시나리오: 모터 제어 + 통신 + 사용자 입력
// 우선순위 계획 (Priority Group 4)
//
// Preempt 0: 예약 (사용 금지, NMI 대비)
// Preempt 1: 모터 PWM 타이머 (최고 시간 정밀도 필요)
// Preempt 2: DMA 완료 인터럽트 (데이터 손실 방지)
// Preempt 3: USART RX (통신 데이터 손실 방지)
// Preempt 5: ADC 변환 완료
// Preempt 10: 버튼 입력 (느린 응답 무방)
// Preempt 15: SysTick (HAL 타임베이스)
void system_nvic_init(void)
{
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
// 모터 제어 타이머 (TIM1 Update)
HAL_NVIC_SetPriority(TIM1_UP_TIM10_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(TIM1_UP_TIM10_IRQn);
// DMA1 Stream5 (USART1 RX)
HAL_NVIC_SetPriority(DMA1_Stream5_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn);
// USART1
HAL_NVIC_SetPriority(USART1_IRQn, 3, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
// ADC
HAL_NVIC_SetPriority(ADC_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(ADC_IRQn);
// 버튼 (EXTI0)
HAL_NVIC_SetPriority(EXTI0_IRQn, 10, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
중첩 동작 확인 코드
// 두 타이머 인터럽트로 중첩 동작 확인
// TIM2: Preempt 1 (높은 우선순위)
// TIM3: Preempt 3 (낮은 우선순위)
volatile uint32_t tim2_enter_count = 0;
volatile uint32_t tim3_enter_count = 0;
volatile uint8_t nesting_detected = 0;
volatile uint8_t in_tim3_isr = 0;
void TIM2_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim2);
}
void TIM3_IRQHandler(void)
{
in_tim3_isr = 1;
tim3_enter_count++;
HAL_TIM_IRQHandler(&htim3);
in_tim3_isr = 0;
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2)
{
tim2_enter_count++;
// TIM3 ISR 실행 중에 TIM2가 선점했는지 확인
if (in_tim3_isr)
{
nesting_detected = 1;
printf("Nesting! TIM2 preempted TIM3\r\n");
}
HAL_Delay(1); // 의도적 지연 (중첩 유발)
}
if (htim->Instance == TIM3)
{
printf("TIM3 count: %lu\r\n", tim3_enter_count);
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_TIM2_Init();
MX_TIM3_Init();
// TIM2: 높은 우선순위, 빠른 주기
HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
// TIM3: 낮은 우선순위, 느린 주기
HAL_NVIC_SetPriority(TIM3_IRQn, 3, 0);
HAL_NVIC_EnableIRQ(TIM3_IRQn);
HAL_TIM_Base_Start_IT(&htim2);
HAL_TIM_Base_Start_IT(&htim3);
while (1)
{
if (nesting_detected)
{
printf("Nesting confirmed. TIM2: %lu, TIM3: %lu\r\n",
tim2_enter_count, tim3_enter_count);
nesting_detected = 0;
}
HAL_Delay(1000);
}
}
HAL 함수 사용 시 주의사항
// HAL_Delay()는 SysTick 인터럽트를 사용
// ISR 내에서 HAL_Delay() 호출 시 SysTick 우선순위가 ISR보다 낮으면 무한 대기
// 잘못된 예시:
// SysTick Priority = 15, USART1 Priority = 3
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
HAL_Delay(100); // SysTick(15)이 USART1(3)보다 낮아 동작 안 함 (교착)
}
// 올바른 해결책 1: HAL_Delay 대신 루프 사용
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
// ISR 내부는 최대한 빠르게 처리, 지연 금지
rx_flag = 1; // 플래그만 설정
}
// 올바른 해결책 2: SysTick 우선순위를 가장 높게 설정
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0); // SysTick 최고 우선순위
// CubeMX에서 uwTickPrio 변수 확인 (기본값: 15)
// HAL_Init() 내부에서 설정됨
런타임 우선순위 확인
void print_nvic_info(IRQn_Type irq)
{
uint32_t preempt, sub;
HAL_NVIC_GetPriority(irq, NVIC_PRIORITYGROUP_4, &preempt, &sub);
uint32_t enabled = NVIC_GetEnableIRQ(irq);
uint32_t pending = NVIC_GetPendingIRQ(irq);
uint32_t active = NVIC_GetActive(irq);
printf("IRQ %d: Preempt=%lu, Sub=%lu, En=%lu, Pending=%lu, Active=%lu\r\n",
(int)irq, preempt, sub, enabled, pending, active);
}
void debug_all_nvic(void)
{
printf("\r\n=== NVIC Status ===\r\n");
print_nvic_info(TIM1_UP_TIM10_IRQn);
print_nvic_info(USART1_IRQn);
print_nvic_info(EXTI0_IRQn);
print_nvic_info(DMA1_Stream5_IRQn);
printf("Priority Group: %lu\r\n", HAL_NVIC_GetPriorityGrouping());
}
문제 1: ISR이 실행되지 않음
// 진단 순서:
// 1. NVIC EnableIRQ 호출 여부 확인
printf("TIM2 Enable: %lu\r\n", NVIC_GetEnableIRQ(TIM2_IRQn));
// 2. 주변장치 인터럽트 활성화 여부 확인 (예: 타이머)
// TIM->DIER 레지스터의 UIE 비트 확인
printf("TIM2 DIER: 0x%08lX\r\n", TIM2->DIER);
// 3. Pending 비트가 클리어되는지 확인
// ISR 진입 후 Pending 미클리어 시 즉시 재진입
// HAL_XXX_IRQHandler() 내부에서 자동 처리됨
// 4. 벡터 테이블의 함수명 확인
// startup 파일과 stm32f4xx_it.c 함수명 일치 여부
문제 2: 예상치 못한 HardFault 발생
// ISR 내에서 스택 오버플로우 발생 가능
// ISR은 별도 스택 프레임 사용 (자동 저장: 8 레지스터 × 4바이트 = 32바이트)
// ISR 내 지역 변수 최소화
// HardFault 핸들러에서 원인 파악
void HardFault_Handler(void)
{
// MSP, PSP 레지스터로 스택 상태 확인
volatile uint32_t* sp = (uint32_t*)__get_MSP();
printf("HardFault!\r\n");
printf("PC : 0x%08lX\r\n", sp[6]); // 스택에 자동 저장된 PC
printf("LR : 0x%08lX\r\n", sp[5]);
printf("PSR: 0x%08lX\r\n", sp[7]);
while (1); // 무한 루프로 디버거 연결 대기
}
문제 3: 인터럽트 폭주 (Interrupt Storm)
// 증상: 시스템이 응답 없이 ISR만 반복 실행
// 원인: ISR 내에서 Pending 비트 미클리어
// EXTI 예시:
void EXTI0_IRQHandler(void)
{
// Pending 비트 클리어를 누락하면 즉시 재진입
// EXTI->PR |= EXTI_PR_PR0; // 이 줄 없으면 폭주
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); // HAL이 자동 클리어
}
// 타이머 예시:
void TIM2_IRQHandler(void)
{
// Update Flag 클리어 없으면 폭주
// __HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE); // HAL이 처리
HAL_TIM_IRQHandler(&htim2); // 내부에서 자동 클리어
}
ISR 내부에서 해야 할 것과 하지 말아야 할 것
해야 할 것:
- 빠른 처리 (수 마이크로초 이내)
- 플래그 변수 설정
- 하드웨어 Pending 비트 클리어
- 링 버퍼에 데이터 저장
- 간단한 상태 업데이트
하지 말아야 할 것:
- HAL_Delay() 등 블로킹 함수 호출
- printf() 직접 호출 (UART 블로킹)
- 복잡한 연산
- 동적 메모리 할당 (malloc/free)
- 무한 루프
플래그 기반 처리 패턴
// ISR: 최소한의 처리만
volatile uint8_t uart_rx_flag = 0;
volatile uint8_t uart_rx_data = 0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
uart_rx_data = rx_buffer; // 데이터 저장
uart_rx_flag = 1; // 플래그 설정
// 다음 수신 준비
HAL_UART_Receive_IT(&huart1, &rx_buffer, 1);
}
}
// 메인 루프: 실제 처리
int main(void)
{
// ...
while (1)
{
if (uart_rx_flag)
{
uart_rx_flag = 0;
process_received_data(uart_rx_data); // 메인 루프에서 처리
}
}
}
volatile 키워드 사용
// ISR과 메인 루프가 공유하는 변수는 반드시 volatile 선언
// volatile 없으면 컴파일러 최적화로 변수 읽기를 생략할 수 있음
volatile uint8_t flag = 0; // 올바른 선언
volatile uint32_t counter = 0;
// 잘못된 예시 (volatile 누락):
uint8_t flag = 0; // 컴파일러가 최적화로 while(!flag)를 무한 루프로 만들 수 있음
// 메인 루프에서:
while (!flag); // volatile 없으면 항상 0으로 읽을 수 있음
NVIC 구조
우선순위 시스템
HAL 설정
1. Priority Group 4 (기본값) 우선순위 범위
Preempt Priority: 0 (최고) ~ 15 (최저)
Sub Priority: 없음 (0 고정)
2. 인터럽트 활성화 순서
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4); // 1회만
HAL_NVIC_SetPriority(IRQn, preempt, sub);
HAL_NVIC_EnableIRQ(IRQn);
3. ISR 설계 원칙
빠르게 처리 → 플래그 설정 → 메인 루프에서 실제 처리
공유 변수에 volatile 선언
HAL_Delay() 등 블로킹 함수 ISR 내 사용 금지
| 시스템 역할 | 권장 Preempt Priority | 이유 |
|---|---|---|
| 안전 긴급 처리 | 0 ~ 1 | 최고 응답성 필요 |
| 실시간 제어 타이머 | 1 ~ 2 | 주기 정확도 필수 |
| DMA 완료 | 2 ~ 3 | 데이터 손실 방지 |
| 통신(UART, SPI) | 3 ~ 5 | 오버런 방지 |
| 일반 타이머 | 5 ~ 8 | 중간 우선순위 |
| 사용자 입력 | 8 ~ 12 | 느린 응답 무방 |
| SysTick (HAL) | 15 | HAL 기본값 유지 권장 |