Why a ring buffer
A ring buffer (circular buffer) is the standard tool for passing data between a producer and a consumer when:
- The producer and consumer run at different rates
- You want no dynamic allocation
- You need ISR → task communication without a mutex
It’s the hardware primitive in disguise: UARTs, SPI DMAs, and audio codecs all use ring buffers internally. Your firmware should too.
Requirements for an embedded ring buffer
- No heap — fixed capacity, known at compile time
- No blocking —
pushandpopreturn immediately - ISR-safe — safe when one side is a producer ISR and the other a consumer task on a single-core MCU
- Power-of-two capacity — enables fast modulo via bitwise AND
Implementation
1// ring_buffer.h
2#pragma once
3#include <array>
4#include <cstddef>
5#include <optional>
6
7template <typename T, size_t Capacity>
8class RingBuffer {
9 static_assert((Capacity & (Capacity - 1)) == 0,
10 "Capacity must be a power of two");
11 static constexpr size_t MASK = Capacity - 1;
12
13public:
14 // Push an item. Returns false if full.
15 bool push(const T& item) {
16 const size_t w = write_;
17 const size_t next = (w + 1) & MASK;
18 if (next == read_) return false; // full
19 buf_[w] = item;
20 write_ = next;
21 return true;
22 }
23
24 // Pop an item. Returns empty optional if empty.
25 std::optional<T> pop() {
26 const size_t r = read_;
27 if (r == write_) return std::nullopt; // empty
28 T item = buf_[r];
29 read_ = (r + 1) & MASK;
30 return item;
31 }
32
33 bool empty() const { return read_ == write_; }
34 bool full() const { return ((write_ + 1) & MASK) == read_; }
35
36 size_t size() const {
37 return (write_ - read_) & MASK;
38 }
39
40 size_t capacity() const { return Capacity - 1; } // one slot reserved as sentinel
41
42private:
43 std::array<T, Capacity> buf_{};
44 volatile size_t write_ = 0;
45 volatile size_t read_ = 0;
46};
Key design choices
volatile on indices — prevents the compiler from caching the read or write
index in a register. On a single-core Cortex-M where one side is an ISR,
volatile is sufficient. On multi-core or with optimising compilers that reorder
stores, use std::atomic<size_t> with memory_order_relaxed instead (see below).
One slot sentinel — the buffer holds Capacity - 1 items, not Capacity. The
“full” condition is (write + 1) % Capacity == read. This avoids the ambiguity
between full and empty when read == write. Allocate Capacity one larger than
you need: RingBuffer<uint8_t, 32> holds 31 bytes.
Power-of-two — (index + 1) & MASK replaces (index + 1) % Capacity. On
Cortex-M0 (no hardware divider), division is a software call — tens of cycles.
The bitmask is one instruction.
Usage
1static RingBuffer<uint8_t, 64> uartRxBuf; // holds 63 bytes
2
3// ISR — UART receive
4void USART1_IRQHandler() {
5 uint8_t byte = USART1->DR & 0xFF;
6 uartRxBuf.push(byte); // returns false if full — byte dropped
7}
8
9// Task / main loop
10void processUart() {
11 while (auto byte = uartRxBuf.pop()) {
12 handle(*byte);
13 }
14}
1// With structs
2struct SensorSample { uint32_t tick; float value; };
3static RingBuffer<SensorSample, 16> sampleBuf;
4
5// ISR:
6sampleBuf.push({ HAL_GetTick(), adcToVoltage(ADC1->DR) });
7
8// Task:
9while (auto s = sampleBuf.pop()) {
10 filter.update(s->value);
11}
Atomic variant for multi-core / strict ordering
Replace volatile with std::atomic for correct behaviour under compiler
reordering (always correct; volatile alone is sufficient only on single-core
targets where you trust the compiler won’t move loads/stores across a volatile
access).
1#include <atomic>
2
3template <typename T, size_t Capacity>
4class AtomicRingBuffer {
5 static_assert((Capacity & (Capacity - 1)) == 0, "");
6 static constexpr size_t MASK = Capacity - 1;
7
8public:
9 bool push(const T& item) {
10 const size_t w = write_.load(std::memory_order_relaxed);
11 const size_t next = (w + 1) & MASK;
12 if (next == read_.load(std::memory_order_acquire)) return false;
13 buf_[w] = item;
14 write_.store(next, std::memory_order_release);
15 return true;
16 }
17
18 std::optional<T> pop() {
19 const size_t r = read_.load(std::memory_order_relaxed);
20 if (r == write_.load(std::memory_order_acquire)) return std::nullopt;
21 T item = buf_[r];
22 read_.store((r + 1) & MASK, std::memory_order_release);
23 return item;
24 }
25
26 bool empty() const {
27 return read_.load(std::memory_order_acquire)
28 == write_.load(std::memory_order_acquire);
29 }
30
31private:
32 std::array<T, Capacity> buf_{};
33 std::atomic<size_t> write_{0};
34 std::atomic<size_t> read_ {0};
35};
This is an SPSC (single-producer, single-consumer) queue — safe when exactly one thread writes and one thread reads. For multiple producers, see lock-free queues.
Complete drop-in header
1// ring_buffer.h — single header, no dependencies beyond <array> and <optional>
2#pragma once
3#include <array>
4#include <atomic>
5#include <cstddef>
6#include <optional>
7
8template <typename T, size_t Cap>
9class RingBuffer {
10 static_assert(Cap && !(Cap & (Cap-1)), "Cap must be power of two");
11 static constexpr size_t M = Cap - 1;
12public:
13 bool push(const T& v) {
14 size_t w = w_.load(std::memory_order_relaxed);
15 size_t n = (w + 1) & M;
16 if (n == r_.load(std::memory_order_acquire)) return false;
17 b_[w] = v;
18 w_.store(n, std::memory_order_release);
19 return true;
20 }
21 std::optional<T> pop() {
22 size_t r = r_.load(std::memory_order_relaxed);
23 if (r == w_.load(std::memory_order_acquire)) return {};
24 T v = b_[r];
25 r_.store((r + 1) & M, std::memory_order_release);
26 return v;
27 }
28 bool empty() const { return r_.load(std::memory_order_acquire) == w_.load(std::memory_order_acquire); }
29 bool full() const { size_t w = w_.load(std::memory_order_relaxed); return ((w+1)&M) == r_.load(std::memory_order_acquire); }
30private:
31 std::array<T, Cap> b_{};
32 std::atomic<size_t> w_{0}, r_{0};
33};
Quick reference
| Parameter | Recommendation |
|---|---|
| Capacity | Power of two; one more than max items needed |
| ISR ↔ task, single-core | volatile indices or std::atomic relaxed |
| ISR ↔ task, multi-core / strict | std::atomic acquire/release |
| Multiple producers | Use MPSC queue instead |
| Item type | Trivially copyable — no constructors in ISR context |