인터럽트에서 최소한만 하고 실제 작업은 나중으로 미루는 설계 패턴. 임베디드 인터럽트 설계의 기본이다.
왜 필요한가
ISR이 길면
→ 같거나 낮은 우선순위 인터럽트가 전부 대기
→ 메인 루프·태스크가 멈춤
→ 다른 인터럽트를 놓칠 수 있다 (같은 소스가 또 오면 유실)
→ 최악 인터럽트 지연이 커져 실시간 요구를 못 지킨다
"ISR은 짧게"의 구체적 실천 방법이 deferred 처리다.
두 단계로 나눈다
- 상반부 (top half) — — ISR
- 하드웨어가 요구하는 최소한만
- 데이터 레지스터 읽기 (안 읽으면 다음 데이터가 덮인다)
- 플래그 클리어
- 버퍼에 저장 / 신호 보내기
- 즉시 리턴
하반부 (bottom half) — 태스크 또는 메인 루프
- 파싱, 계산, 로깅, 통신, 화면 갱신
- 오래 걸려도 괜찮다
"놓치면 안 되는 것만 ISR에서" 가 판단 기준이다.
구현 ① — 플래그 (베어메탈)
volatile uint8_t data_ready = 0;
volatile uint8_t buf[64];
void USART1_IRQHandler(void) {
buf[idx++] = USART1->DR;
if (idx >= 64) { idx = 0; data_ready = 1; }
}
int main(void) {
while (1) {
if (data_ready) { data_ready = 0; parse(buf); }
__WFI();
}
}
가장 단순하다. 다만 처리 중에 새 데이터가 오면 버퍼가 덮인다.
구현 ② — 링 버퍼 (베어메탈, 권장)
static volatile uint8_t buf[256];
static volatile uint16_t head = 0, tail = 0;
#define MASK 255 // 256은 2의 거듭제곱
void USART1_IRQHandler(void) {
uint16_t next = (head + 1) & MASK;
if (next != tail) { buf[head] = USART1->DR; head = next; }
// 가득 차면 버린다 (또는 오버런 카운터 증가)
}
int main(void) {
while (1) {
while (tail != head) {
uint8_t c = buf[tail];
tail = (tail + 1) & MASK;
process(c);
}
}
}
ISR만 head를 쓰고 메인만 tail을 쓰므로 락이 필요 없다(단일 생산자-소비자). 임베디드에서 가장 널리 쓰이는 패턴이다.
구현 ③ — 세마포어 (RTOS)
void USART1_IRQHandler(void) {
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(rx_sem, &woken);
portYIELD_FROM_ISR(woken); // ★ 높은 태스크가 깼으면 즉시 전환
}
void rx_task(void *p) {
for (;;) {
xSemaphoreTake(rx_sem, portMAX_DELAY);
process(); // 우선순위를 갖고 실행된다
}
}
★ portYIELD_FROM_ISR이 중요하다. 이것을 빠뜨리면
다음 틱(최대 1ms)까지 전환이 미뤄져 응답이 느려진다.
구현 ④ — 큐 (RTOS, 가장 권장)
void USART1_IRQHandler(void) {
uint8_t c = USART1->DR;
BaseType_t woken = pdFALSE;
xQueueSendFromISR(rx_queue, &c, &woken); // 신호 + 데이터를 함께
portYIELD_FROM_ISR(woken);
}
세마포어 + 공유 변수보다 안전하다. 데이터가 큐 안에 복사되므로 경쟁 조건이 없다.
구현 ⑤ — 태스크 알림 (RTOS, 가장 빠름)
vTaskNotifyGiveFromISR(task_handle, &woken);
// 태스크 쪽
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
큐·세마포어보다 약 45% 빠르고 RAM도 덜 쓴다(별도 객체가 필요 없음). 1:1 신호 전달이면 이것이 최선이다.
구현 ⑥ — 저우선순위 인터럽트로 미루기
void TIM2_IRQHandler(void) { // 높은 우선순위
capture = TIM2->CCR1;
NVIC_SetPendingIRQ(EXTI15_10_IRQn); // 낮은 우선순위 인터럽트를 직접 유발
TIM2->SR &= ~TIM_SR_CC1IF;
}
void EXTI15_10_IRQHandler(void) { // 낮은 우선순위
heavy_calculation(capture); // 다른 인터럽트를 막지 않는다
}
RTOS 없이도 우선순위 계층을 만드는 기법이다. 사용하지 않는 인터럽트 벡터를 소프트웨어 전용으로 쓴다.
무엇을 ISR에 남길지 판단
반드시 ISR에서 ✅ 데이터 레지스터 읽기 (안 하면 다음 데이터에 덮인다 — 오버런) ✅ 플래그 클리어 (안 하면 무한 재진입) ✅ 하드웨어가 요구하는 시간 제약이 있는 응답
미뤄도 되는 것 ❌ 파싱, 체크섬 검증, 상태 머신 전이 ❌ 로깅, 화면 갱신, 통신 송신 ❌ 부동소수점 계산
오버런 감지를 잊지 말 것
if (USART1->SR & USART_SR_ORE) {
overrun_count++; // 데이터를 놓쳤다는 증거를 남긴다
(void)USART1->DR; // 플래그 클리어
}
미룬 결과 데이터를 놓쳤다면 그것을 알아야 한다. 카운터를 두고 모니터링하면 버퍼 크기나 처리 속도를 조정할 근거가 된다.