Sound Byte Libs 0.5.1-121-g3358a44
C++ firmware library for audio applications on 32-bit ARM Cortex-M processors
Loading...
Searching...
No Matches
ring_buffer.hpp
Go to the documentation of this file.
1#ifndef SBL_HW_UTIL_RING_BUFFER_HPP_
2#define SBL_HW_UTIL_RING_BUFFER_HPP_
3
4/**
5 * @file ring_buffer.hpp
6 * @brief Lock-free single-producer, single-consumer ring buffer
7 * @ingroup primitives
8 *
9 * Carries data from one execution context to another without blocking
10 * either: ISR → main on hardware, thread → thread on the workbench. Every
11 * operation is bounded and allocation-free.
12 *
13 * ## Ordering
14 *
15 * The indices are relaxed `std::atomic<uint32_t>` — no torn or invented
16 * accesses, and no data race in the C++ sense. Ordering between the slot
17 * contents and the index that publishes them comes from a fence pair:
18 *
19 * producer: write slot → release fence → store head
20 * consumer: load head → acquire fence → read slot → release fence → store tail
21 *
22 * The fence strength follows the target:
23 *
24 * - **Single-core bare metal** (Cortex-M): producer and consumer share one
25 * core — an ISR preempting a thread is like a signal handler — so the
26 * fences are `atomic_signal_fence`, compiler-only, zero instructions. A
27 * DMB would be the wrong strength, not a safer one (interrupt research
28 * doc §5.3).
29 * - **Host** (workbench, unit tests): producer and consumer may run on
30 * different cores, so the fences are `atomic_thread_fence` (DMB on
31 * AArch64).
32 *
33 * One slot is reserved to tell full from empty: capacity is Size − 1.
34 *
35 * Usage:
36 * @code
37 * RingBuffer<uint8_t, 256> rx;
38 *
39 * // Producer (ISR)
40 * if (!rx.push(byte)) { ++overflows; }
41 *
42 * // Consumer (main)
43 * uint8_t b;
44 * while (rx.pop(b)) { handle(b); }
45 * @endcode
46 *
47 * @tparam T Element type (copied in and out)
48 * @tparam Size Slot count, a power of 2
49 *
50 * @note push() from the producer context only, pop() from the consumer
51 * context only. empty()/full()/size() are snapshots from either side.
52 */
53
54#include <atomic>
55#include <cstdint>
56
57namespace sbl {
58namespace primitives {
59namespace buffers {
60
61namespace detail {
62
63#if defined(__arm__) && !defined(__linux__)
64inline void ring_acquire_fence() { std::atomic_signal_fence(std::memory_order_acquire); }
65inline void ring_release_fence() { std::atomic_signal_fence(std::memory_order_release); }
66#else
67inline void ring_acquire_fence() { std::atomic_thread_fence(std::memory_order_acquire); }
68inline void ring_release_fence() { std::atomic_thread_fence(std::memory_order_release); }
69#endif
70
71} // namespace detail
72
73template<typename T, uint32_t Size>
75 static_assert((Size & (Size - 1)) == 0, "Size must be power of 2");
76 static_assert(Size > 1, "Size must be greater than 1");
77 static_assert(Size <= 65536, "Size must fit in practical memory constraints");
78 static_assert(std::atomic<uint32_t>::is_always_lock_free,
79 "RingBuffer needs lock-free 32-bit atomics");
80
81public:
82 RingBuffer() = default;
83
84 /**
85 * @brief Push an element (producer side)
86 * @return false if the buffer is full; the element is not stored
87 * @note ISR-safe — bounded, non-blocking
88 */
89 bool push(const T& item) {
90 const uint32_t head = head_.load(std::memory_order_relaxed);
91 const uint32_t next = (head + 1) & kMask;
92 if (next == tail_.load(std::memory_order_relaxed)) {
93 return false;
94 }
95 detail::ring_acquire_fence(); // consumer finished with this slot
96 buffer_[head] = item;
97 detail::ring_release_fence(); // slot written before head publishes it
98 head_.store(next, std::memory_order_relaxed);
99 return true;
100 }
101
102 /**
103 * @brief Pop the oldest element (consumer side)
104 * @return false if the buffer is empty; `item` is unchanged
105 * @note ISR-safe — bounded, non-blocking
106 */
107 bool pop(T& item) {
108 const uint32_t tail = tail_.load(std::memory_order_relaxed);
109 if (tail == head_.load(std::memory_order_relaxed)) {
110 return false;
111 }
112 detail::ring_acquire_fence(); // head observed before the slot is read
113 item = buffer_[tail];
114 detail::ring_release_fence(); // slot read before tail frees it
115 tail_.store((tail + 1) & kMask, std::memory_order_relaxed);
116 return true;
117 }
118
119 bool empty() const {
120 return head_.load(std::memory_order_relaxed) ==
121 tail_.load(std::memory_order_relaxed);
122 }
123
124 bool full() const {
125 return ((head_.load(std::memory_order_relaxed) + 1) & kMask) ==
126 tail_.load(std::memory_order_relaxed);
127 }
128
129 /// Element count — a snapshot, may change immediately after the call.
130 uint32_t size() const {
131 return (head_.load(std::memory_order_relaxed) -
132 tail_.load(std::memory_order_relaxed)) & kMask;
133 }
134
135 static constexpr uint32_t capacity() { return Size - 1; }
136
137 /// Empty the buffer. Only while neither producer nor consumer runs.
138 void clear() {
139 head_.store(0, std::memory_order_relaxed);
140 tail_.store(0, std::memory_order_relaxed);
141 }
142
143private:
144 static constexpr uint32_t kMask = Size - 1;
145
146 T buffer_[Size] = {};
147 std::atomic<uint32_t> head_{0}; // producer writes, consumer reads
148 std::atomic<uint32_t> tail_{0}; // consumer writes, producer reads
149};
150
151} // namespace buffers
152} // namespace primitives
153} // namespace sbl
154
155#endif // SBL_HW_UTIL_RING_BUFFER_HPP_
void clear()
Empty the buffer. Only while neither producer nor consumer runs.
uint32_t size() const
Element count — a snapshot, may change immediately after the call.
bool push(const T &item)
Push an element (producer side)
bool pop(T &item)
Pop the oldest element (consumer side)
static constexpr uint32_t capacity()
Root namespace for all Sound Byte Libs code.
Definition assert.hpp:51