그라운드 임베디드 EN 유튜브 구독

노트 / 자료구조와 알고리즘

링 버퍼: 인터럽트와 메인 루프 사이의 다리

UART로 들어오는 바이트를 잃지 않고 쌓아 두는 원형 큐. 인터럽트가 쓰고 메인 루프가 읽는 구조입니다.

GROUND EMBEDDED · 정리 노트자료구조 02링 버퍼 1#define N 64// 2의 거듭제… 2static volatile uint8_t buf[N]; 3static volatile uint16_t head, tail; 4void rb_put(uint8_t b) {// ISR에서 …

UART 수신 인터럽트는 바이트가 올 때마다 짧게 실행되어야 합니다. 받은 바이트를 처리까지 하면 다음 바이트를 놓칩니다. 그래서 인터럽트는 버퍼에 넣기만 하고, 해석은 메인 루프가 합니다. 이 사이를 잇는 것이 링 버퍼입니다.

구조

배열 하나와 인덱스 두 개(head, tail)로 만듭니다. 끝에 도달하면 처음으로 돌아갑니다.

#define N 64                      // 2의 거듭제곱이면 나머지 연산 대신 & 사용
static volatile uint8_t buf[N];
static volatile uint16_t head, tail;

void rb_put(uint8_t b) {          // ISR에서 호출
    uint16_t next = (head + 1) & (N - 1);
    if (next != tail) { buf[head] = b; head = next; }  // 가득 차면 버림
}
int rb_get(uint8_t *b) {          // 메인 루프에서 호출
    if (tail == head) return 0;   // 비었음
    *b = buf[tail]; tail = (tail + 1) & (N - 1);
    return 1;
}

잠금 없이 안전한 이유

쓰는 쪽은 head만, 읽는 쪽은 tail만 바꿉니다. 생산자와 소비자가 하나씩이고 인덱스 읽기가 원자적이면 인터럽트를 끄지 않아도 됩니다. 칸 하나를 비워 두는 이유는 "가득 참"과 "비었음"을 구분하기 위해서입니다.

크기 정하기

보레이트와 메인 루프 최악 주기로 계산합니다. 115200bps면 초당 약 11520바이트, 메인 루프가 5ms 걸리면 그 사이 약 58바이트가 들어오므로 64바이트로는 빠듯하고 128바이트가 안전합니다.

그림으로 보기

배열 하나와 인덱스 두 개가 전부입니다. 쓰는 쪽은 head만, 읽는 쪽은 tail만 움직입니다.

[2]0x41tail (다음에 읽을 곳)[3]0x42데이터[4]0x43데이터[5]비어 있음head (다음에 쓸 곳)
크기 8인 링 버퍼. tail부터 head 직전까지가 아직 읽지 않은 데이터입니다. 끝에 닿으면 0번으로 돌아갑니다.
무료 전자책이 주제를 한 권으로 이어 읽기 (PDF 3권)→

전체 코드

#define RB_SIZE 64u                      // 2의 거듭제곱
typedef struct {
    uint8_t buf[RB_SIZE];
    volatile uint16_t head;              // ISR만 바꿈
    volatile uint16_t tail;              // 메인 루프만 바꿈
} ring_t;

static inline bool rb_put(ring_t *r, uint8_t d) {      // ISR에서 호출
    uint16_t next = (r->head + 1u) & (RB_SIZE - 1u);
    if (next == r->tail) return false;                 // 가득 참
    r->buf[r->head] = d;
    r->head = next;                                    // 데이터를 쓴 뒤에 옮김
    return true;
}

static inline bool rb_get(ring_t *r, uint8_t *d) {     // 메인 루프에서 호출
    if (r->tail == r->head) return false;              // 비어 있음
    *d = r->buf[r->tail];
    r->tail = (r->tail + 1u) & (RB_SIZE - 1u);
    return true;
}

설계에서 정해야 할 것

  • 크기를 2의 거듭제곱으로: 나머지 연산(%) 대신 AND 한 번으로 되돌아갑니다. 나눗셈 명령이 없는 Cortex-M0에서는 차이가 큽니다.
  • 한 칸을 비워 둔다: head와 tail이 같으면 "비어 있음"으로 약속했기 때문에, 가득 찬 상태는 한 칸 남았을 때로 정의합니다. 64칸 버퍼에는 63바이트까지 들어갑니다.
  • 잠금이 필요 없는 조건: 쓰는 쪽이 하나, 읽는 쪽이 하나일 때만입니다. 두 개의 인터럽트가 같은 버퍼에 쓰면 보호가 필요합니다.
  • 크기 정하기: 메인 루프가 가장 오래 자리를 비우는 시간 × 초당 들어오는 바이트 수보다 커야 합니다. 115200 bps는 초당 약 11520바이트이므로, 메인 루프가 5 ms 동안 못 읽으면 58바이트가 쌓입니다.

현업에서 자주 터지는 문제

  • 가득 찼을 때 조용히 버리면 원인을 찾기 어렵습니다. 넘침 횟수를 세는 카운터를 하나 두고 디버그 출력에 포함합니다.
  • head를 데이터보다 먼저 옮기면, 읽는 쪽이 아직 쓰지 않은 칸을 읽을 수 있습니다. 순서를 지킵니다.
  • 인덱스가 MCU의 워드 크기보다 크면 읽는 도중 값이 바뀔 수 있습니다. 8비트 MCU에서는 인덱스를 uint8_t로 둡니다.
영상으로 이어 보기공부법과 진로 시리즈 15편2025 임베디드 프로그래밍 공부 순서 | 공부 방법 | 로드맵PROJECT COURSE배운 자료구조를 실제 펌웨어에 넣어 보고 싶다면밸런싱 로봇 하나를 요구사항, 회로와 PCB, 펌웨어, 앱까지 실무 순서대로 만드는 강의입니다. 10파트 165강.강의 살펴보기 →FREE PDF무료 전자책 3권 받기이 주제를 한 권으로 이어서 읽고 싶다면. BLDC 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →