임베디드 면접 용어 사전
인터럽트critical section

임계 구역

동시 접근을 막아야 하는 코드 구간. 인터럽트 비활성으로 보호하되 최대한 짧게 잡아야 한다.

공유 자원에 접근하는 구간으로, 중간에 끼어들면 데이터가 깨지는 코드 영역 (critical section).

왜 필요한가

volatile uint32_t counter;

// 메인 루프
counter++;          // 읽기 → +1 → 쓰기  (3단계)
메인: counter 읽기 (5)
      ↓ 인터럽트!
ISR:  counter = 0 으로 리셋
      ↓ 복귀
메인: 5 + 1 = 6 을 쓴다     ← ISR의 리셋이 사라졌다

volatile로는 막을 수 없다. volatile은 "실제로 접근하라"는 지시일 뿐 "중간에 끼어들지 마라"가 아니다.

베어메탈에서의 보호

uint32_t primask = __get_PRIMASK();   // 현재 상태 저장
__disable_irq();
// ── 임계 구역 ──
counter++;
// ──────────────
__set_PRIMASK(primask);               // 원래 상태로 복원  ★

★ 단순히 __enable_irq()를 부르면 안 된다. 이 코드가 이미 인터럽트가 꺼진 곳에서 호출됐다면 의도치 않게 인터럽트를 켜 버린다. 중첩된 임계 구역에서 흔한 버그다.

매크로로 정리하는 것이 관례다.

#define ENTER_CRITICAL()  uint32_t _pm = __get_PRIMASK(); __disable_irq()
#define EXIT_CRITICAL()   __set_PRIMASK(_pm)

BASEPRI — 더 나은 방법

__set_BASEPRI(0x40);     // 우선순위 4 이상(숫자가 큰=낮은)만 차단
// 임계 구역
__set_BASEPRI(0);

전체를 막지 않고 낮은 우선순위만 막는다. 안전 정지, 모터 과전류 같은 최우선 인터럽트는 계속 살아 있다.

RTOS에서 이 방식을 쓰며, 그 대가로 높은 우선순위 ISR에서는 RTOS API를 부를 수 없다는 제약이 생긴다.

가장 중요한 원칙 — 짧게

임계 구역 길이 = 인터럽트 지연 증가분

// ✗ 나쁨 — 통신 중에 인터럽트가 전부 막힌다
ENTER_CRITICAL();
uart_send_string("데이터 처리 중...");    // 수 ms
EXIT_CRITICAL();

// ✓ 좋음 — 복사만 보호하고 처리는 밖에서
ENTER_CRITICAL();
uint32_t snapshot = shared_data;          // 몇 사이클
EXIT_CRITICAL();
process(snapshot);

"공유 데이터를 스냅샷으로 복사하고 나와서 처리" 가 표준 패턴이다.

하드웨어의 원자적 연산 — 임계 구역 없이

GPIOA->BSRR = (1 << 5);          // 원자적 세트 (read-modify-write 아님)
GPIOA->BSRR = (1 << 5) << 16;    // 원자적 클리어

있으면 반드시 쓴다. 인터럽트를 막을 필요가 없어진다.

// LDREX/STREX — 배타적 접근 (Cortex-M3+)
do {
    val = __LDREXW(&counter);
    val++;
} while (__STREXW(val, &counter));   // 중간에 누가 건드렸으면 실패 → 재시도

락 없이 원자적 갱신이 가능하다. CAS와 같은 원리다.

RTOS에서는 무엇을 쓰나

  • 인터럽트와 공유 — → 인터럽트 차단 (또는 BASEPRI)
  • 태스크끼리 공유 — → 뮤텍스 ← 인터럽트를 막을 필요가 없다
  • 짧은 공유 — → taskENTER_CRITICAL() (스케줄러+인터럽트 차단)

태스크 간 보호에 인터럽트를 끄면 안 된다. 그 시간 동안 모든 인터럽트가 지연되기 때문이다. 뮤텍스는 다른 태스크만 막는다.

데이터 종류별 판단

  • 8/16/32비트 정렬된 단일 변수 읽기·쓰기 — → 32비트 MCU에서 원자적. 보호 불필요
  • 읽기-수정-쓰기 (counter++) — → 보호 필요
  • 64비트 값, 구조체, 배열 — → 보호 필요
  • 여러 변수의 일관성 (head, tail 동시) — → 보호 필요

단일 생산자-단일 소비자 링 버퍼는 head/tail을 각각 한쪽만 쓰면 보호 없이도 안전하다.

// ISR만 head를 쓰고, 메인만 tail을 쓴다면 락이 필요 없다
if (head != tail) { data = buf[tail]; tail = (tail + 1) & MASK; }

임베디드에서 매우 널리 쓰이는 lock-free 패턴이다. (단, head/tail이 원자적으로 읽고 쓰이는 크기여야 한다.)

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 인터럽트 — 벡터·우선순위·latency·재진입·ISR 금기·디바운싱