인터럽트가 발생했을 때 실행되는 함수(Interrupt Service Routine).
작성 원칙 — 딱 하나로 요약하면
최대한 짧게. 나머지는 메인 루프로 미룬다.
왜 — ISR이 실행되는 동안
- ① 같거나 낮은 우선순위 인터럽트가 전부 대기한다
- ② 메인 루프가 멈춘다
- ③ 실시간 요구를 어길 수 있다
ISR에서 하지 말아야 할 것
| 금지 | 이유 |
|---|---|
| printf / sprintf | 매우 느리고(수백 μs) 재진입 불가 |
| malloc / free | 힙 자료구조를 건드림. 재진입 불가 |
| 긴 루프·복잡한 계산 | 다른 인터럽트를 굶긴다 |
| 블로킹 대기 | while (!flag) — 영영 못 나올 수 있다 |
| RTOS 블로킹 API | xQueueSend가 아니라 xQueueSendFromISR |
| 부동소수점 (FPU 없으면) | 소프트웨어 에뮬레이션으로 수백 사이클 |
printf 디버깅이 ISR에서 특히 위험하다 — 타이밍이 완전히 바뀌어
원래 버그가 사라지거나 새 버그가 생긴다(하이젠버그).
표준 패턴 — deferred 처리
volatile uint8_t data_ready = 0;
volatile uint8_t rx_buf[64];
void USART1_IRQHandler(void) {
if (USART1->SR & USART_SR_RXNE) {
rx_buf[idx++] = USART1->DR; // 읽기만 (읽으면 플래그도 클리어)
if (idx >= 64) data_ready = 1;
}
}
int main(void) {
while (1) {
if (data_ready) {
data_ready = 0;
parse_packet(rx_buf); // 오래 걸려도 안전
}
}
}
반드시 지킬 것
① 공유 변수에 volatile
volatile uint8_t flag; // 없으면 메인 루프가 무한 대기에 빠진다
② 플래그 클리어
void EXTI0_IRQHandler(void) {
handle();
EXTI->PR = EXTI_PR_PR0; // 안 지우면 무한 재진입
}
어떤 플래그는 데이터 레지스터를 읽는 것으로 자동 클리어된다 (UART의 RXNE). 데이터시트를 확인해야 한다.
③ 여러 바이트 공유 데이터는 원자성 확보
// 메인 루프에서
__disable_irq();
uint32_t snapshot = shared_counter; // 32비트 MCU면 사실 원자적이지만
__enable_irq(); // 구조체·64비트는 반드시 보호
④ 재진입 가능한 함수만 호출
static 지역 변수를 쓰는 함수를 ISR과 메인이 함께 부르면 데이터가 깨진다.
RTOS 환경에서
void USART1_IRQHandler(void) {
BaseType_t woken = pdFALSE;
xQueueSendFromISR(queue, &data, &woken); // FromISR 버전 필수
portYIELD_FROM_ISR(woken); // 더 높은 태스크가 깨면 즉시 전환
}
FromISR 접미사가 붙은 API만 사용해야 한다.
일반 버전은 블로킹할 수 있는데, ISR에서 블로킹하면 시스템이 멈춘다.
우선순위 제약 — FreeRTOS에서 RTOS API를 부르는 ISR은
configMAX_SYSCALL_INTERRUPT_PRIORITY보다 낮거나 같은 우선순위여야 한다.
더 높은 ISR에서 API를 부르면 커널의 임계 구역 보호가 무력화되어
간헐적으로 리스트가 깨지는 최악의 버그가 된다.
공유 하드웨어 인터럽트
void EXTI9_5_IRQHandler(void) { // 5~9번 핀이 같은 벡터를 공유
if (EXTI->PR & EXTI_PR_PR5) { handle5(); EXTI->PR = EXTI_PR_PR5; }
if (EXTI->PR & EXTI_PR_PR7) { handle7(); EXTI->PR = EXTI_PR_PR7; }
}
어느 소스인지 판별하고 해당 플래그만 지워야 한다. 전부 지우면 아직 처리 안 한 다른 원인을 잃는다.
ISR 실행 시간 측정
void TIM2_IRQHandler(void) {
GPIOA->BSRR = DEBUG_PIN;
// …
GPIOA->BSRR = DEBUG_PIN << 16;
TIM2->SR &= ~TIM_SR_UIF;
}
오실로스코프의 펄스 폭이 실행 시간이다. 이 값이 인터럽트 주기의 몇 %인지가 CPU 부하의 직접적 지표가 된다. 50%를 넘으면 설계를 다시 봐야 한다.