Зачем кольцевой буфер
Кольцевой буфер (циклический буфер) — стандартный инструмент для передачи данных между производителем и потребителем, когда:
- Производитель и потребитель работают с разной скоростью
- Нужно избежать динамического выделения памяти
- Требуется связь ISR → задача без мьютекса
Это аппаратный примитив под другим названием: UART’ы, SPI DMA и аудиокодеки используют кольцевые буферы внутри. Ваша прошивка тоже должна.
Требования к кольцевому буферу для встраиваемых
- Без кучи — фиксированная ёмкость, известная на этапе компиляции
- Без блокировок —
pushиpopвозвращаются немедленно - Безопасен для ISR — безопасен, когда одна сторона — ISR-производитель, другая — задача-потребитель на одноядерном МК
- Ёмкость — степень двойки — позволяет быстрый остаток через битовое AND
Реализация
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 "Ёмкость должна быть степенью двойки");
11 static constexpr size_t MASK = Capacity - 1;
12
13public:
14 // Добавить элемент. Возвращает false если полный.
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; // полный
19 buf_[w] = item;
20 write_ = next;
21 return true;
22 }
23
24 // Извлечь элемент. Возвращает пустой optional если пустой.
25 std::optional<T> pop() {
26 const size_t r = read_;
27 if (r == write_) return std::nullopt; // пустой
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; } // один слот зарезервирован как сторожевой
41
42private:
43 std::array<T, Capacity> buf_{};
44 volatile size_t write_ = 0;
45 volatile size_t read_ = 0;
46};
Ключевые решения при проектировании
volatile на индексах — предотвращает кэширование компилятором индексов чтения
или записи в регистрах. На одноядерном Cortex-M, где одна сторона — ISR,
volatile достаточен. На многоядерных или при оптимизирующих компиляторах,
переупорядочивающих записи, вместо этого используйте std::atomic<size_t>
с memory_order_relaxed (см. ниже).
Сторожевой слот — буфер хранит Capacity - 1 элементов, а не Capacity.
Условие «полный»: (write + 1) % Capacity == read. Это устраняет неоднозначность
между полным и пустым при read == write. Выделяйте Capacity на один больше
нужного: RingBuffer<uint8_t, 32> хранит 31 байт.
Степень двойки — (index + 1) & MASK заменяет (index + 1) % Capacity. На
Cortex-M0 (без аппаратного делителя) деление — программный вызов, занимающий
десятки тактов. Битовая маска — одна инструкция.
Использование
1static RingBuffer<uint8_t, 64> uartRxBuf; // хранит 63 байта
2
3// ISR — приём UART
4void USART1_IRQHandler() {
5 uint8_t byte = USART1->DR & 0xFF;
6 uartRxBuf.push(byte); // возвращает false если полный — байт теряется
7}
8
9// Задача / главный цикл
10void processUart() {
11 while (auto byte = uartRxBuf.pop()) {
12 handle(*byte);
13 }
14}
1// Со структурами
2struct SensorSample { uint32_t tick; float value; };
3static RingBuffer<SensorSample, 16> sampleBuf;
4
5// ISR:
6sampleBuf.push({ HAL_GetTick(), adcToVoltage(ADC1->DR) });
7
8// Задача:
9while (auto s = sampleBuf.pop()) {
10 filter.update(s->value);
11}
Атомарный вариант для многоядерных / строгого упорядочивания
Замените volatile на std::atomic для корректного поведения при
переупорядочивании компилятором (всегда корректно; volatile достаточен только
на одноядерных платформах, где вы доверяете компилятору не перемещать
загрузки/записи через volatile-обращение).
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};
Это SPSC-очередь (один производитель, один потребитель) — безопасна, когда ровно один поток пишет и один читает. Для нескольких производителей см. lock-free очереди.
Полный заголовочный файл
1// ring_buffer.h — один заголовок, зависимости только от <array> и <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 должен быть степенью двойки");
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};
Краткий справочник
| Параметр | Рекомендация |
|---|---|
| Ёмкость | Степень двойки; на один больше максимального числа элементов |
| ISR ↔ задача, одноядерный | Индексы volatile или std::atomic relaxed |
| ISR ↔ задача, многоядерный / строгий | std::atomic acquire/release |
| Несколько производителей | Используйте MPSC-очередь |
| Тип элемента | Тривиально копируемый — нет конструкторов в контексте ISR |