由於嵌入式系統的資源有限性,循環緩沖區數據結構體(Circular Buffer Data Structures)被大量的使用。
循環緩沖區(也稱為環形緩沖區)是固定大小的緩沖區,工作原理就像內存是連續的且可循環的一樣。在生成和使用內存時,不需將原來的數據全部重新清理掉,只要調整head/tail 指針即可。當添加數據時,head 指針前進。當使用數據時,tail 指針向前移動。當到達緩沖區的尾部時,指針又回到緩沖區的起始位置。
目錄:
- 為什么使用循環緩沖區
- C 實例
- 使用封裝
- API設計
- 確認緩沖區是否已滿
- 循環緩沖區容器類型
- 實例
- 使用
為什么使用循環緩沖區?
循環緩沖區通常用作固定大小的隊列。固定大小的隊列對於嵌入式系統的開發非常友好,因為開發人員通常會嘗試使用靜態數據存儲的方法而不是動態分配。
循環緩沖區對於數據寫入和讀出以不同速率發生的情況也是非常有用的結構:最新數據始終可用。如果讀取數據的速度跟不上寫入數據的速度,舊的數據將被新寫入的數據覆蓋。通過使用循環緩沖區,能夠保證我們始終使用最新的數據。
有關其他的用例,請查看Embedded.com上的Ring Buffer Basics。
C實例
我們將使用C語言來開始實現,我們將會碰到一些設計上的挑戰。
使用封裝
我們將創建一個Circular Buffer庫,來避免直接操作結構體。
在我們的庫文件頭部,前置聲明結構體:
// Opaque circular buffer structure typedef struct CIRCULAR_BUFFER_T circular_buf_t;
我們不希望用戶直接操作 circular_buf_t 結構體,因為他們可能會覺得可以取消對值的引用。取而代之我們創建一個句柄類型來給用戶使用。
最簡單的方法是將cbuf_handle_t定義為一個指向circular buffer的指針。這會避免我們在函數中進行強制轉換指針。
// Handle type, the way users interact with the API typedef circular_buf_t* cbuf_handle_t;
另一種方法是使句柄為uintptr_t或void *值。在程序內,我們將句柄轉換為適當的指針類型。保證circular buffer類型對用戶隱藏,與數據交互的唯一方法是通過句柄。
我們堅持簡單的句柄實現,來使代碼簡單明了。
API Design
首先,我們應該思考用戶如何與循環緩沖區交互:
- 用戶需要使用一個 buffer 和 size 來初始化循環緩沖區容器
- 用戶需要銷毀循環緩沖區容器
- 用戶需要 reset 循環緩沖區容器
- 用戶需要能夠從緩沖區取出下一個值
- 用戶需要知道緩沖區是滿還是空
- 用戶需要知道當前緩沖區元素的數量
- 用戶需要知道緩沖區的最大容量
使用這個列表,我們能夠合並一個API到庫中。用戶將使用我們在初始化期間創建的不透明句柄類型和緩沖區庫進行交互。
在此實例中,我們選擇使用 uint8_t 作為基礎數據類型。你可以使用任意你喜歡的特定類型 - 但要注意適當地處理底層緩沖區和字節數。
/// Pass in a storage buffer and size /// Returns a circular buffer handle cbuf_handle_t circular_buf_init(uint8_t* buffer, size_t size); /// Free a circular buffer structure. /// Does not free data buffer; owner is responsible for that void circular_buf_free(cbuf_handle_t cbuf); /// Reset the circular buffer to empty, head == tail void circular_buf_reset(cbuf_handle_t cbuf); /// Put version 1 continues to add data if the buffer is full /// Old data is overwritten void circular_buf_put(cbuf_handle_t cbuf, uint8_t data); /// Put Version 2 rejects new data if the buffer is full /// Returns 0 on success, -1 if buffer is full int circular_buf_put2(cbuf_handle_t cbuf, uint8_t data); /// Retrieve a value from the buffer /// Returns 0 on success, -1 if the buffer is empty int circular_buf_get(cbuf_handle_t cbuf, uint8_t * data); /// Returns true if the buffer is empty bool circular_buf_empty(cbuf_handle_t cbuf); /// Returns true if the buffer is full bool circular_buf_full(cbuf_handle_t cbuf); /// Returns the maximum capacity of the buffer size_t circular_buf_capacity(cbuf_handle_t cbuf); /// Returns the current number of elements in the buffer size_t circular_buf_size(cbuf_handle_t cbuf);
確認緩沖區是否已滿
在繼續之前,我們應該花費一點時間去討論一個方法去確認緩沖的空滿。
循環緩沖區的 “full” 和 “empty” 看起來是相同的:head 和 tail 指針是相等的。有兩種方法區分 full 和 empty:
浪費緩沖區中的一個數據槽:
- Full:tail + 1 == head
- Empty:head == tail
使用一個bool標志位和其他邏輯來區分:
- Full:full
- Empty:(head == tail) && (!full)
與其浪費一個數據槽,下面方法使用了bool標志位。使用標志位的方法要求在 get 和 put 函數中使用其他邏輯來更新標志。
緩沖區容器類型
現在我們已經確定了需要支持的操作,可以開始設計循環緩沖區容器了。
我們使用容器結構體來管理緩沖區狀態。為了保留封裝,容器結構體定義在library.c文件中,而不是頭文件中。
我們需要跟蹤以下信息:
- 基礎數據緩沖區
- 緩沖區的最大范圍
- “head”指針的當前位置(添加元素時增加)
- “tail”指針的當前位置(讀取元素后增加)
- 一個標志位來指示緩沖區是否已滿
// The hidden definition of our circular buffer structure struct circular_buf_t { uint8_t * buffer; size_t head; size_t tail; size_t max; //of the buffer bool full; };
現在,容器已經設計完成,接下來完成庫函數。
實例
需要注意的是,每一個API都需要一個初始化緩沖區的句柄。我們不使用條件語句來填充我們的代碼,而是使用斷言以“Design by Contract”樣式來強制執行我們的API要求。
這樣如果程序處理不當,將直接終止程序。
初始化和復位
init 函數:初始化循環緩沖區。我們的API是用戶提供底層 buffer 和 buffer size,API返回一個 circular buffer 句柄。
我們需要在庫端創建一個循環緩沖區容器。為了簡單起見,我使用了 malloc 函數。不能使用動態內存的系統只需修改 init 函數來使用其他方法實現創建目的。例如從循環緩沖區的靜態池中分配。
另一種方法是破壞封裝,允許用戶靜態聲明循環緩沖區容器結構。在這種情況下,circular_buf_init 需要更新來采用結構指針,或者初始化能夠在堆棧上創建一個容器結構體並返回它。但是,由於封裝被破壞,用戶將無需使用例程就能修改結構體。
所以我們使用第一種方法。
// User provides struct void circular_buf_init(circular_buf_t* cbuf, uint8_t* buffer, size_t size); // Return a struct circular_buf_t circular_buf_init(uint8_t* buffer, size_t size)
創建容器之后,我們需要填充數據並在其上調用 reset 函數。在 init 返回之前,我們要確保緩沖區容器是在空狀態下創建的。
cbuf_handle_t circular_buf_init(uint8_t* buffer, size_t size) { assert(buffer && size); cbuf_handle_t cbuf = malloc(sizeof(circular_buf_t)); assert(cbuf); cbuf->buffer = buffer; cbuf->max = size; circular_buf_reset(cbuf); assert(circular_buf_empty(cbuf)); return cbuf; }
reset 函數:目的是將緩沖區置為 “空” 狀態,需要更新 head,tail 和 full 。
void circular_buf_reset(cbuf_handle_t cbuf) { assert(cbuf); cbuf->head = 0; cbuf->tail = 0; cbuf->full = false; }
當我們有了一個創建循環緩沖區容器的方法,同樣的我們也需要一個能夠銷毀容器的等效方法。我們可以調用 free 函數來釋放容器。但不要嘗試釋放底層緩沖區,釋放容器指針就好,因為根據我們的初始化方法,我們不需要也不能理會底層緩沖區。
void circular_buf_free(cbuf_handle_t cbuf) { assert(cbuf); free(cbuf); }
狀態檢查
接下來,我們將實現與緩沖區容器狀態相關的函數部分。
full 函數:很容易實現,因為我們已經有一個標志位來表示滿狀態了:
bool circular_buf_full(cbuf_handle_t cbuf) { assert(cbuf); return cbuf->full; }
empty 函數:因為我們已經有 full 標志位來區分空滿狀態了,我們只需將 full 標志位和“head == tail”的檢查結果合並處理。
bool circular_buf_empty(cbuf_handle_t cbuf) { assert(cbuf); return (!cbuf->full && (cbuf->head == cbuf->tail)); }
capacity 函數:由於在初始化階段就已經設定了緩沖區的容量大小,所以只需要返回這個值即可:
size_t circular_buf_capacity(cbuf_handle_t cbuf) { assert(cbuf); return cbuf->max; }
預期計算緩沖區元素的數量是一個棘手的問題,許多人建議使用除法來計算,但在測試的時候遇到了許多奇怪的情況。所以我選擇了條件語句進行簡化運算。
關於緩沖區的元素數量有以下三種情況:
① 緩沖區狀態是 full ,我們就知道當前的容量已經達到了最大;
② head >= tail,只需將兩個值相減就可以得出大小;
③ tail > head,我們需要用最大值來抵消差值,才能得到正確的大小;
size_t circular_buf_size(cbuf_handle_t cbuf) { assert(cbuf); size_t size = cbuf->max; if(!cbuf->full) { if(cbuf->head >= cbuf->tail) { size = (cbuf->head - cbuf->tail); } else { size = (cbuf->max + cbuf->head - cbuf->tail); } } return size; }
添加和刪除數據
有了這些功能之后,是時候開始深入研究了:從隊列中添加和刪除數據。
從循環緩沖區添加和刪除數據需要操縱 head 和 tail 指針。當向緩沖區添加數據時,我們將新的數據插入當前 head 指針所在的位置,然后將 head 指針向前移一位。當從緩沖區刪除數據時,我們從當前 tail 指針的位置取出數據,然后將 tail 指針向前移一位。
但是,向緩沖區添加數據時需要更多考慮。如果緩沖區是滿狀態,我們需要同時移動 tail 和 head 指針。我們還需要檢查插入數據是否會出發 full 條件。
我們將實現兩個版本的 put 函數,因此,讓我們將指針前進函數提起到一個輔助函數中。如果緩沖區已滿,移動 tail 指針。我們每次都向前移動一位 head 指針。當指針移動之后,我們通過檢查 “head == tail” 的結果來判斷是否填充 full 標志位。
注意下面使用的除法(%)運算符。當填充的數據達到最大值時,除法運算將導致 head 和 tail 指針重置為0。這樣確保head 和 tail 指針始終是底層數據緩沖區的有效索引。
static void advance_pointer(cbuf_handle_t cbuf) { assert(cbuf); if(cbuf->full) { cbuf->tail = (cbuf->tail + 1) % cbuf->max; } cbuf->head = (cbuf->head + 1) % cbuf->max; cbuf->full = (cbuf->head == cbuf->tail); }
我們可以寫一個類似的輔助函數當從緩沖區刪除數據時調用。當刪除數據時,full 標志置為 flase ,tail 指針向前移一位。
static void retreat_pointer(cbuf_handle_t cbuf) { assert(cbuf); cbuf->full = false; cbuf->tail = (cbuf->tail + 1) % cbuf->max; }
我們將創建兩個版本的 put 函數。第一個版本向緩沖區插入數據並向前移動指針。如果緩沖區已滿,舊數據將會被覆蓋。這是循環緩沖區的標准使用案例。
void circular_buf_put(cbuf_handle_t cbuf, uint8_t data) { assert(cbuf && cbuf->buffer); cbuf->buffer[cbuf->head] = data; advance_pointer(cbuf); }
第二個版本如果緩沖區已滿 put 函數將返回 error。這里只提供一個示范樣例,在我們的系統中並沒有使用這個變體。
int circular_buf_put2(cbuf_handle_t cbuf, uint8_t data) { int r = -1; assert(cbuf && cbuf->buffer); if(!circular_buf_full(cbuf)) { cbuf->buffer[cbuf->head] = data; advance_pointer(cbuf); r = 0; } return r; }
從緩沖區刪除數據,我們取出 tail 指針位置的值並更新 tail 指針。如果緩沖區是空的,我們不返回數據或者修改指針值。相反,我們返回 error 給用戶。
int circular_buf_get(cbuf_handle_t cbuf, uint8_t * data) { assert(cbuf && data && cbuf->buffer); int r = -1; if(!circular_buf_empty(cbuf)) { *data = cbuf->buffer[cbuf->tail]; retreat_pointer(cbuf); r = 0; } return r; }
這樣就完成了循環緩沖區庫的實現。
使用
在使用這個庫時,用戶負責創建 circular_buf_init 的底層數據緩沖區,將會返回 cbuf_handle_t :
uint8_t * buffer = malloc(EXAMPLE_BUFFER_SIZE * sizeof(uint8_t)); cbuf_handle_t cbuf = circular_buf_init(buffer, EXAMPLE_BUFFER_SIZE);
該處理用於和其他剩余的所有庫函數交互:
bool full = circular_buf_full(cbuf); bool empty = circular_buf_empty(cbuf); printf("Current buffer size: %zu\n", circular_buf_size(cbuf);
當處理完之后不要忘記 free 底層數據緩沖區和容器:
free(buffer); circular_buf_free(cbuf);
源資源帖:https://embeddedartistry.com/blog/2017/05/17/creating-a-circular-buffer-in-c-and-c/