Зачем кольцевой буфер

Кольцевой буфер (циклический буфер) — стандартный инструмент для передачи данных между производителем и потребителем, когда:

  • Производитель и потребитель работают с разной скоростью
  • Нужно избежать динамического выделения памяти
  • Требуется связь 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