노트 / 자료구조와 알고리즘
링 버퍼: 인터럽트와 메인 루프 사이의 다리
UART로 들어오는 바이트를 잃지 않고 쌓아 두는 원형 큐. 인터럽트가 쓰고 메인 루프가 읽는 구조입니다.
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만 움직입니다.
전체 코드
#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 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →