임베디드 면접 용어 사전
RTOSFromISR

deferred 처리

ISR에서 최소한만 하고 실제 작업을 태스크로 넘기는 패턴. ISR 안전 API(…FromISR)로 알린다.

인터럽트에서 최소한만 하고 실제 작업은 나중으로 미루는 설계 패턴. 임베디드 인터럽트 설계의 기본이다.

왜 필요한가

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;             // 플래그 클리어
}

미룬 결과 데이터를 놓쳤다면 그것을 알아야 한다. 카운터를 두고 모니터링하면 버퍼 크기나 처리 속도를 조정할 근거가 된다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — RTOS — 스케줄링·뮤텍스/세마포어·우선순위 역전·데드락·스택