← Embedded C 강의 목록으로
🔌
응용
응용 · 선수: 21강

22. 링버퍼와 인터럽트 기반 UART 수신

UART로 데이터가 쏟아지면 수신 인터럽트가 매 바이트마다 걸립니다. ISR에서 다 처리하려 들면 ISR이 길어지고, 메인이 느리면 다음 바이트가 이전 바이트를 덮어써 유실됩니다. 해법은 ISR은 버퍼에 넣기만 하고 처리는 한가한 메인이 맡는 분업입니다. 그 핵심 자료구조가 링버퍼(원형 버퍼)입니다. 생산자(ISR)와 소비자(메인)가 서로 다른 인덱스만 건드리면 인터럽트를 끄지 않고도 안전합니다.

링버퍼FIFOUART인터럽트SPSCvolatile
소요 시간
약 1.5~2시간
난이도
📊 고급
선수 조건
🎯 21강
결과물
head/tail FIFO 링버퍼를 2의 거듭제곱 마스킹으로 구현하고, 단일 생산자/소비자(SPSC) + volatile로 UART RX 인터럽트와 안전하게 결합할 수 있습니다.

이 강의에서 배우는 것

  • 1인터럽트 수신에서 데이터 유실이 왜 생기는지 설명한다
  • 2링버퍼의 head/tail FIFO 구조를 이해한다
  • 3빈/가득 판정과 2의 거듭제곱 마스킹을 구현한다
  • 4단일 생산자/소비자(SPSC) 환경의 안전성과 volatile을 이해한다
  • 5링버퍼를 UART RX 인터럽트와 결합하는 패턴을 안다

소개

RX 인터럽트는 바이트마다 발생합니다. ISR이 길거나 메인이 제때 안 가져가면 다음 바이트가 수신 레지스터를 덮어써 사라집니다. "빨리 받아 어딘가 쌓아두고, 처리는 나중에"가 필요합니다.

핵심 개념

1) 링버퍼 구조와 빈/가득 판정

고정 배열을 원형으로 씁니다. head(쓰기)·tail(읽기)이 끝에 도달하면 0으로 감싸 돕니다(FIFO). 빈 상태는 head==tail, 가득 상태는 ((head+1)&MASK)==tail. 가득 판정을 위해 슬롯 하나를 비우므로 크기 N이면 용량은 N-1.

c
#define RB_SIZE 8u
#define RB_MASK (RB_SIZE - 1u)
next = (head + 1) & RB_MASK;   /* 2의 거듭제곱이면 & 로 빠르게 wrap */

2) SPSC 안전성과 volatile

생산자(ISR)는 head만, 소비자(메인)는 tail만 씁니다. 서로 다른 변수를 갱신하므로 단일 생산자/단일 소비자(SPSC)에서는 락이나 인터럽트 차단 없이도 안전합니다. 단, head/tail은 ISR·메인 공유라 volatile 로 선언해야 합니다.

3) UART RX 인터럽트와 결합

c
void USART1_IRQHandler(void) {
    if (USART1->SR & USART_SR_RXNE) {
        uint8_t b = (uint8_t)USART1->DR;   /* 읽으면 RXNE 클리어 */
        rb_push(&rx_rb, b);                /* 생산자: head 만 */
    }
}
/* 메인: if (rb_pop(&rx_rb, &c)) { ...느긋하게 처리... }  (소비자: tail 만) */
ℹ️

USART1->CR1 에 USART_CR1_RXNEIE(수신 인터럽트)를 켜고 NVIC_EnableIRQ(USART1_IRQn)를 호출하면 RX 인터럽트가 동작합니다. 처리는 메인이 합니다.

핵심 예제

c
uint8_t rb_push(ringbuffer_t *rb, uint8_t data) {
    uint16_t next = (uint16_t)((rb->head + 1u) & RB_MASK);
    if (next == rb->tail) return 0u;          /* 가득 — 거절(유실 방지) */
    rb->buf[rb->head] = data; rb->head = next; return 1u;
}
uint8_t rb_pop(ringbuffer_t *rb, uint8_t *out) {
    if (rb->head == rb->tail) return 0u;       /* 비었음 */
    *out = rb->buf[rb->tail];
    rb->tail = (uint16_t)((rb->tail + 1u) & RB_MASK); return 1u;
}

검증: 크기 8(용량 7)에 8개째 push는 FULL로 거절, 꺼내면 들어간 순서대로(FIFO) 나옵니다. 빌드: gcc pc_test.c ringbuffer.c -o pc_test.

자주 하는 실수

Q. head/tail에 volatile을 안 붙여도 PC에선 잘 돼요.

A. PC(단일 스레드)에선 안 보일 수 있지만, ISR·메인이 공유하는 임베디드에선 컴파일러가 캐싱해 갱신을 못 봅니다. 반드시 volatile 을 붙이세요.

Q. 크기를 10으로 했더니 wrap이 이상해요.

A. & (N-1) 마스킹은 N이 2의 거듭제곱일 때만 맞습니다. 10이면 % 10을 쓰거나 8·16처럼 2의 거듭제곱으로 잡으세요.

Q. 버퍼가 가득 찼는데 push가 덮어써요.

A. push에서 ((head+1)&MASK)==tail이면 거절(0 반환)해야 합니다. 안 그러면 tail을 추월해 아직 안 읽은 데이터를 덮어씁니다.

정리

  • 인터럽트 수신은 ISR이 버퍼에 넣고 메인이 꺼내 처리하는 분업이 안전하다
  • 링버퍼는 head/tail로 도는 FIFO이며 빈/가득을 인덱스로 판정한다
  • 크기를 2의 거듭제곱으로 잡으면 & MASK로 빠르게 wrap한다(용량 N-1)
  • SPSC에서는 head/tail 분리 + volatile로 락 없이 안전하다
  • UART RX ISR은 rb_push만, 메인은 rb_pop 후 처리

과제

  1. rb_count() 가 래핑 케이스에서도 올바른 개수를 주는지 확인
  2. 버퍼 크기를 16으로 늘리고 MASK를 맞게 수정
  3. 줄바꿈(\n)까지 모아 한 줄을 조립하는 line_assemble() 추가
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드(과제·정답 포함)는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗