這幾個月我一直專注在 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;
}