← Advanced C (Pre-Kernel)C PROGRAMMING 1.7.G

Ring Buffer

Wraparound mechanics, the three full/empty disambiguation strategies, and a byte ring buffer put/get — the UART driver interview classic.

StatusCollected
NotesChapter test: 5/5 (100%), first attempt, unaided

Three pieces, and this is the one everyone warns is harder than it looks — a fixed-size circular buffer with a head and a tail that wrap back to 0 instead of running off the end.

Wraparound

#define SIZE 8

unsigned int advance(unsigned int idx) {
    return (idx + 1) % SIZE;         /* or (idx + 1) & (SIZE - 1) if SIZE is a power of 2 */
}

The bitwise-AND form only works when SIZE is a power of 2 — x % SIZE and x & (SIZE - 1) are only equivalent in that specific case, since the mask only clears the exact bits that a power-of-2 modulus would wrap on.

The full-vs-empty problem

The actual hard part: head == tail alone is ambiguous — it means empty when nothing's been added yet, but it also means the exact same thing when the buffer has completely filled and wrapped back around to meet itself. Three ways to resolve it:

StrategyHow it works
Count fieldtrack element count separately — empty is count == 0, full is count == SIZE
Sacrifice a slotnever fill the last slot — full is (head+1) % SIZE == tail, one slot always wasted
Flag bitan explicit is_full flag set on the write that causes head to catch tail

A byte ring buffer — the UART pattern

typedef struct {
    uint8_t buf[SIZE];
    unsigned int head, tail, count;
} ring_buffer_t;

int put(ring_buffer_t *rb, uint8_t byte) {
    if (rb->count == SIZE) return -1;   /* full */
    rb->buf[rb->tail] = byte;
    rb->tail = (rb->tail + 1) % SIZE;
    rb->count++;
    return 0;
}

int get(ring_buffer_t *rb, uint8_t *out) {
    if (rb->count == 0) return -1;      /* empty */
    *out = rb->buf[rb->head];
    rb->head = (rb->head + 1) % SIZE;
    rb->count--;
    return 0;
}

This is the UART RX-buffer shape directly: the interrupt handler calls put every time a byte arrives off the wire, and whatever's actually reading the device calls get to drain it — running independently, at different speeds, without either side needing to know anything about the other's timing.

Notes from readers

Comments — via GitHub