這幾個月我一直專注在 MProject 的 MCU 開發案。由於 MCU 的資源有限,記憶體管理就顯得格外重要。一開始我以為只要靠 C++ 的物件導向及 vector STL,就能避免 C 語言中大量指標與手動管理的麻煩;但實際上,STL 在 MCU 上容易造成記憶體碎片化,進而導致不可預期的異常。 最明顯的徵兆是:程式架構在運行前幾分鐘都很穩定,但無法長時間(數小時、數天)穩定運作,這往往代表架構本身有隱藏問題。每當遇到這類 bug,我的第一步就是檢查記憶體使用狀況。不過 MCU 沒有像 PC 那樣的工具,必須自己設計機制將資訊輸出,才能追蹤問題。

這次我發現,vector STL 一開始用起來都很正常,但隨著時間推移,記憶體回收不易,無法再分配較大區塊,導致程式當機。最後我回歸 C 語法,自己實作 RingBuffer,問題果然迎刃而解,系統也穩定許多。因此,我建立了新的設計原則:所有經常使用的記憶體宣告都盡量用固定陣列方式,讓編譯時就能掌握資源用量,也讓程式邏輯之外的行為更可控。

這種方式讓我更清楚程式運作的來龍去脈,對 MCU 開發尤其重要。下面分享我用 C++ STL 風格實作的 RingBuffer,不但能快速建立不同類型的緩衝區,也讓維護更容易,不必像 C 語法每種型態都寫一套。


DYRingBuffer STL 風格環形緩衝區

特色說明:

  • std::array 實現固定大小,避免動態分配造成碎片化。
  • 保留一格判斷滿/空,確保操作安全。
  • 支援批量操作、強制覆蓋、撤銷讀取等進階功能。
  • 可直接查看內部狀態,方便除錯
/*
 * DYRingBuffer.hpp
 *
 *  Created on: Sep 2, 2025
 *      Author: danny
 */

#ifndef INC_DYRINGBUFFER_HPP_
#define INC_DYRINGBUFFER_HPP_

#include <array>
#include <cstdint>
#include <cstddef>
#include <iostream>

/**
 * @brief STL 風格的環形緩衝區
 * @tparam T 元素類型
 * @tparam N 緩衝區大小
 * 
 * 保持傳統 RingBuffer 的精神:
 * - 緩衝區滿的判斷是基於 head 追上 tail
 * - 實際可用空間是 N-1 (保留一個位置用於判斷滿/空狀態)
 */
template<typename T, size_t N>
class DYRingBuffer
{
    static_assert(N > 1, "RingBuffer size must be greater than 1");

public:
    // STL 風格類型定義
    using value_type = T;
    using size_type = size_t;
    using reference = T&;
    using const_reference = const T&;

private:
    std::array<T, N> buffer_;
    size_type head_ = 0;    // 寫入指針
    size_type tail_ = 0;    // 讀取指針
    size_type last_tail_ = 0; // 上一個 tail 位置(用於 undo)

public:
    /**
     * @brief 建構子
     */
    DYRingBuffer() = default;

    /**
     * @brief 寫入數據到緩衝區
     * @param data 要寫入的數據
     * @return 0 成功,-1 緩衝區滿
     */
    int write(const T& data)
    {
        size_type next_head = (head_ + 1) % N;

        // 檢查緩衝區是否滿(head 追上 tail)
        if (next_head == tail_) {
            return -1; // 緩衝區滿,寫入失敗
        }

        buffer_[head_] = data;
        head_ = next_head;
        return 0; // 寫入成功
    }

    /**
     * @brief 從緩衝區讀取數據
     * @param data 輸出參數,存放讀取的數據
     * @return 0 成功,-1 緩衝區空
     */
    int read(T& data)
    {
        // 檢查緩衝區是否空(head 等於 tail)
        if (head_ == tail_) {
            return -1; // 緩衝區空,讀取失敗
        }

        last_tail_ = tail_; // 保存當前 tail 位置
        data = buffer_[tail_];
        tail_ = (tail_ + 1) % N;
        return 0; // 讀取成功
    }

    /**
     * @brief 撤銷上一次讀取操作
     */
    void undo_read()
    {
        tail_ = last_tail_; // 恢復到上一個 tail 位置
    }

    /**
     * @brief 獲取可讀取的數據長度
     * @return 可讀取的數據數量
     */
    size_type available() const
    {
        if (head_ >= tail_) {
            return head_ - tail_;
        } else {
            return N - (tail_ - head_);
        }
    }

    /**
     * @brief 獲取可寫入的空間大小
     * @return 可寫入的空間數量
     */
    size_type free_space() const
    {
        return (N - 1) - available(); // 總空間 - 1 - 已使用空間
    }

    /**
     * @brief 檢查緩衝區是否為空
     * @return true 為空,false 不為空
     */
    bool empty() const
    {
        return head_ == tail_;
    }

    /**
     * @brief 檢查緩衝區是否已滿
     * @return true 已滿,false 未滿
     */
    bool full() const
    {
        return ((head_ + 1) % N) == tail_;
    }

    /**
     * @brief 清空緩衝區
     */
    void clear()
    {
        head_ = 0;
        tail_ = 0;
        last_tail_ = 0;
    }

    /**
     * @brief 獲取緩衝區總容量
     * @return 緩衝區總大小
     */
    constexpr size_type capacity() const
    {
        return N - 1; // 實際可用容量(保留一個位置用於判斷滿/空)
    }

    /**
     * @brief 獲取緩衝區實際大小
     * @return 緩衝區陣列大小
     */
    constexpr size_type buffer_size() const
    {
        return N;
    }

    // === STL 風格的額外功能 ===

    /**
     * @brief 窺視數據(不移動指針)
     * @param data 輸出參數,存放窺視的數據
     * @return 0 成功,-1 緩衝區空
     */
    int peek(T& data) const
    {
        if (head_ == tail_) {
            return -1; // 緩衝區空
        }

        data = buffer_[tail_];
        return 0;
    }

    /**
     * @brief 移動語義的寫入
     * @param data 要寫入的數據(移動)
     * @return 0 成功,-1 緩衝區滿
     */
    int write(T&& data)
    {
        size_type next_head = (head_ + 1) % N;

        if (next_head == tail_) {
            return -1;
        }

        buffer_[head_] = std::move(data);
        head_ = next_head;
        return 0;
    }

    /**
     * @brief 批量寫入
     * @param data 數據陣列指針
     * @param count 要寫入的數據數量
     * @return 實際寫入的數據數量
     */
    size_type write_bulk(const T* data, size_type count)
    {
        size_type written = 0;
        for (size_type i = 0; i < count; ++i) {
            if (write(data[i]) == 0) {
                written++;
            } else {
                break; // 緩衝區滿了
            }
        }
        return written;
    }

    /**
     * @brief 批量讀取
     * @param data 輸出陣列指針
     * @param count 要讀取的數據數量
     * @return 實際讀取的數據數量
     */
    size_type read_bulk(T* data, size_type count)
    {
        size_type read_count = 0;
        for (size_type i = 0; i < count; ++i) {
            if (read(data[i]) == 0) {
                read_count++;
            } else {
                break; // 緩衝區空了
            }
        }
        return read_count;
    }

    /**
     * @brief 強制寫入(覆蓋最舊的數據)
     * @param data 要寫入的數據
     * @return 總是返回 0(成功)
     */
    int force_write(const T& data)
    {
        size_type next_head = (head_ + 1) % N;

        // 如果緩衝區滿了,移動 tail 指針(丟棄最舊的數據)
        if (next_head == tail_) {
            tail_ = (tail_ + 1) % N;
        }

        buffer_[head_] = data;
        head_ = next_head;
        return 0;
    }

    // === 除錯和狀態 ===

    /**
     * @brief 獲取內部狀態(除錯用)
     */
    struct Status
    {
        size_type head;
        size_type tail;
        size_type last_tail;
        size_type available;
        size_type free_space;
        bool is_empty;
        bool is_full;
    };

    Status get_status() const
    {
        return {
            head_,
            tail_,
            last_tail_,
            available(),
            free_space(),
            empty(),
            full()
        };
    }
};
#endif /* INC_DYRINGBUFFER_HPP_ */
#include <array>
#include <cstdint>
#include <cstddef>
#include <iostream>
#include "DYRingBuffer.hpp

void DYRingBuffer_Example()
{
    printf("DYRingBuffer STL 風格範例\n");
    printf("=============================\n");

    // 建立一個 uint8_t 的環形緩衝區
    DYRingBuffer<uint8_t, 8> buffer; // 實際可用容量是 7

    printf("緩衝區容量: %zu (陣列大小: %zu)\n", 
           buffer.capacity(), buffer.buffer_size());

    // 寫入測試
    printf("\n寫入測試:\n");
    for (uint8_t i = 1; i <= 6; i++) {
        int result = buffer.write(i * 10);
        printf("寫入 %d: %s\n", i * 10, result == 0 ? "成功" : "失敗");
    }

    auto status = buffer.get_status();
    printf("狀態: head=%zu, tail=%zu, 可用=%zu, 剩餘=%zu, 滿=%s\n",
           status.head, status.tail, status.available, status.free_space,
           status.is_full ? "是" : "否");

    // 窺視測試
    uint8_t peek_data;
    if (buffer.peek(peek_data) == 0) {
        printf("窺視到: %d\n", peek_data);
    }

    // 讀取測試
    printf("\n讀取測試:\n");
    uint8_t data;
    for (int i = 0; i < 3; i++) {
        if (buffer.read(data) == 0) {
            printf("讀取: %d\n", data);
        }
    }

    // 撤銷讀取測試
    printf("\n撤銷上一次讀取:\n");
    buffer.undo_read();
    if (buffer.read(data) == 0) {
        printf("重新讀取: %d\n", data);
    }

    // 批量操作測試
    printf("\n批量操作測試:\n");
    uint8_t write_data[] = {100, 200, 255};
    size_t written = buffer.write_bulk(write_data, 3);
    printf("批量寫入 %zu 個元素\n", written);

    uint8_t read_data[5];
    size_t read_count = buffer.read_bulk(read_data, 5);
    printf("批量讀取 %zu 個元素: ", read_count);
    for (size_t i = 0; i < read_count; i++) {
        printf("%d ", read_data[i]);
    }
    printf("\n");

    // 強制寫入測試
    printf("\n強制寫入測試(覆蓋模式):\n");
    DYRingBuffer<char, 4> char_buffer; // 實際容量 3

    // 填滿緩衝區
    for (char c = 'A'; c <= 'C'; c++) {
        char_buffer.write(c);
        printf("寫入: %c\n", c);
    }

    printf("緩衝區已滿,強制寫入 'D':\n");
    char_buffer.force_write('D'); // 會覆蓋 'A'

    char c;
    printf("讀取結果: ");
    while (char_buffer.read(c) == 0) {
        printf("%c ", c);
    }
    printf("\n");
}

int main()
{
    DYRingBuffer_Example();

    return 0;
}