공유 자원에 접근하는 구간으로, 중간에 끼어들면 데이터가 깨지는 코드 영역 (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이 원자적으로 읽고 쓰이는 크기여야 한다.)