Exercise: Implement a Ring Buffer
Ring buffers are one of the most useful embedded data structures. They let a producer and consumer exchange data efficiently using a fixed block of memory. UART reception, logging, event queues, and DMA handoff paths all use the same core idea.
Learning Objectives
By the end of this exercise, you should be able to:
- implement a byte-oriented circular buffer in C;
- track head, tail, count, full, and empty conditions correctly;
- test wraparound behavior instead of assuming it works;
- prepare the design for later interrupt-driven use.
Prerequisites
- Comfortable with fixed-width types, arrays, and pointers.
- Basic understanding of producer-consumer behavior.
- Ability to compile and run a small C test program on host or target.
Concrete Task
Implement a ring buffer with these operations:
- initialize buffer state;
- push one byte;
- pop one byte;
- report full, empty, and current count.
Use a power-of-two-free design first. Do not rely on bitmask tricks until the logic is correct.
Data Structure and API
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#define RB_CAPACITY 16u
typedef struct {
uint8_t data[RB_CAPACITY];
size_t head;
size_t tail;
size_t count;
} ring_buffer_t;
void rb_init(ring_buffer_t *rb);
bool rb_is_empty(const ring_buffer_t *rb);
bool rb_is_full(const ring_buffer_t *rb);
bool rb_push(ring_buffer_t *rb, uint8_t value);
bool rb_pop(ring_buffer_t *rb, uint8_t *out_value);
Complete Implementation
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#define RB_CAPACITY 16u
typedef struct {
uint8_t data[RB_CAPACITY];
size_t head;
size_t tail;
size_t count;
} ring_buffer_t;
void rb_init(ring_buffer_t *rb)
{
if (rb == NULL) {
return;
}
rb->head = 0u;
rb->tail = 0u;
rb->count = 0u;
}
bool rb_is_empty(const ring_buffer_t *rb)
{
return (rb == NULL) || (rb->count == 0u);
}
bool rb_is_full(const ring_buffer_t *rb)
{
return (rb != NULL) && (rb->count == RB_CAPACITY);
}
bool rb_push(ring_buffer_t *rb, uint8_t value)
{
if ((rb == NULL) || rb_is_full(rb)) {
return false;
}
rb->data[rb->head] = value;
rb->head = (rb->head + 1u) % RB_CAPACITY;
rb->count++;
return true;
}
bool rb_pop(ring_buffer_t *rb, uint8_t *out_value)
{
if ((rb == NULL) || (out_value == NULL) || rb_is_empty(rb)) {
return false;
}
*out_value = rb->data[rb->tail];
rb->tail = (rb->tail + 1u) % RB_CAPACITY;
rb->count--;
return true;
}
Minimal Verification Program
#include <assert.h>
#include <stdint.h>
int main(void)
{
ring_buffer_t rb;
uint8_t value = 0u;
rb_init(&rb);
assert(rb_is_empty(&rb));
assert(!rb_is_full(&rb));
for (uint8_t i = 0u; i < RB_CAPACITY; i++) {
assert(rb_push(&rb, i));
}
assert(rb_is_full(&rb));
assert(!rb_push(&rb, 99u));
for (uint8_t i = 0u; i < RB_CAPACITY; i++) {
assert(rb_pop(&rb, &value));
assert(value == i);
}
assert(rb_is_empty(&rb));
assert(!rb_pop(&rb, &value));
for (uint8_t i = 0u; i < 8u; i++) {
assert(rb_push(&rb, (uint8_t)(10u + i)));
}
for (uint8_t i = 0u; i < 4u; i++) {
assert(rb_pop(&rb, &value));
}
for (uint8_t i = 0u; i < 8u; i++) {
assert(rb_push(&rb, (uint8_t)(20u + i)));
}
assert(rb.count == 12u);
for (uint8_t i = 0u; i < 4u; i++) {
assert(rb_pop(&rb, &value));
assert(value == (uint8_t)(14u + i));
}
for (uint8_t i = 0u; i < 8u; i++) {
assert(rb_pop(&rb, &value));
assert(value == (uint8_t)(20u + i));
}
assert(rb_is_empty(&rb));
return 0;
}
This test intentionally exercises wraparound instead of checking only the simplest fill-then-empty case.
Build on a host machine with:
cc -std=c11 -Wall -Wextra -Wpedantic -O2 ring_buffer_test.c -o ring_buffer_test
./ring_buffer_test
Expected Behavior
- Push succeeds until capacity is reached.
- Push fails cleanly when full.
- Pop returns bytes in first-in, first-out order.
- After wraparound, the buffer still preserves order.
- Empty and full states are unambiguous because
countis tracked explicitly.
Verification Steps
- Compile the implementation and test program with warnings enabled.
- Run the test on host or target; all assertions should pass.
- Add temporary prints or debugger watches for
head,tail, andcount. - Confirm
headwraps fromRB_CAPACITY - 1back to0. - Confirm
tailwraps independently. - Confirm
countnever exceedsRB_CAPACITY.
Common Failure Symptoms
| Symptom | Likely cause |
|---|---|
| data comes out in wrong order | head/tail update order wrong |
| full and empty look identical | insufficient state tracking |
| wraparound corrupts entries | modulo or index math error |
| pop succeeds on empty buffer | missing state check |
| push overwrites unread data | full-condition bug |
Debugging Guidance
- Draw the buffer on paper and trace
head,tail, andcountafter each operation. - Test with a tiny capacity such as
4to expose wraparound quickly. - Keep push and pop symmetric; asymmetry often reveals the bug.
- If using an ISR later, remember that shared access may need atomic protection.
- Add a watch expression for
(head + capacity - tail) % capacitywhen debugging a count-free variant.
Extension Challenge
Extend the design for UART RX:
- producer: ISR pushes received bytes;
- consumer: main loop pops bytes and parses commands.
Then decide how to handle overflow:
- drop newest byte;
- drop oldest byte;
- set an overflow flag and reject input until drained.
Document the policy explicitly.
Concise Explained Solution
The implementation works because the buffer stores data in a fixed array, head marks the next write position, tail marks the next read position, and count disambiguates full versus empty. Each push writes then advances head; each pop reads then advances tail. Modulo arithmetic wraps indices cleanly to the start of the array.
Common Mistakes
- Trying to optimize with bitmasks before the logic is proven.
- Forgetting that
head == tailcan mean either full or empty depending on design. - Omitting null-pointer checks in reusable utility code.
- Testing only the no-wrap case.
Summary
This exercise builds a core embedded primitive: bounded FIFO storage with deterministic memory use. Once you can implement and verify a ring buffer confidently, interrupt-driven serial drivers and event queues become much easier to reason about.
Further Reading
- Embedded Artistry: Circular Buffer in C
- SEI CERT C: ARR and STR Rules
- Microchip AVR USART Documentation