LwRB 使用說明
LwRB (lightweight ring buffer) 是一個輕量級的環(huán)形緩沖區(qū),除了支持 ring buffer 常規(guī)的讀寫操作,它還有自己的特色,比如支持讀寫事件通知,支持 DMA 零拷貝收發(fā)數(shù)據(jù)。
- LwRB 項目工程為:https://github.com/MaJerle/lwrb。
- RT-Thread 移植版本為:https://github.com/Jackistang/lwrb2rtt 。
下面依次介紹相應(yīng)功能的使用方式:
LwRB 原理淺析
詳細(xì)內(nèi)容請參考 LwRB - How it works,以下僅寫出我的理解。
LwRB 內(nèi)部有一塊地址連續(xù)的緩沖區(qū),這個緩沖區(qū)是用戶調(diào)用 lwrb_init() 時指定的,接口如下:
uint8_t lwrb_init(LWRB_VOLATILE lwrb_t* buff, void* buffdata, size_t size);
而且 LwRB 內(nèi)部維護(hù)了一個讀(R)、寫(W)指針,分別指向下一個可讀、下一個可寫的位置。寫入一個字節(jié)數(shù)據(jù)時,將數(shù)據(jù)放入寫指針的位置,并將寫指針加 1 ,若超出緩沖區(qū)范圍,則回滾到緩沖區(qū)起始處。

例如上圖例程,緩沖區(qū)大小 S 為 8 :
- 初始時:
R與W相等,代表 LwRB 為空。 - 寫入 4 個字節(jié):
W移動到 4 的位置,此時 LwRB 存儲了 4 個字節(jié)。 - 寫入 3 個字節(jié):
W移動到 7 的位置,此時 LwRB 存儲了 7 個字節(jié),而且W == R-1, 或者說W == (R + S - 1) % S,代表 LwRB 已滿。 - 讀出 5 個字節(jié),并再次寫入 4 個字節(jié):
W移動到 3,R移動到 5,此時 LwRB 存儲了 6 個字節(jié)。 - 寫入 1 個字節(jié):
W移動到 4,此時 LwRB 存儲了 7 個字節(jié),而且W == R-1,或者說W == (R + S - 1) % S,代表 LwRB 已滿。
需要注意的是,lwrb 內(nèi)部 W == R 代表 LwRB 為空,W == (R + S - 1) % S 代表 LwRB 為滿。
讀寫操作
LwRB 里的讀寫操作接口為:
/* Read/Write functions */
size_t lwrb_write(LWRB_VOLATILE lwrb_t* buff, const void* data, size_t btw);
size_t lwrb_read(LWRB_VOLATILE lwrb_t* buff, void* data, size_t btr);
size_t lwrb_peek(LWRB_VOLATILE lwrb_t* buff, size_t skip_count, void* data, size_t btp);
-
lwrb_write()用于向 LwRB 里寫入數(shù)據(jù),并移動寫指針。 -
lwrb_read()用于從 LwRB 里讀出數(shù)據(jù),并移動讀指針。 -
lwrb_peek()用于從 LwRB 里讀出數(shù)據(jù),不移動讀指針。
這里需要區(qū)分 lwrb_read() 和 lwrb_peek() 的不同之處,read 是真正地從 LwRB 里把數(shù)據(jù)取出來,會修改讀指針,read 之后該數(shù)據(jù)就被 LwRB 移除了;而 peek 操作從 LwRB 里讀數(shù)據(jù),但并不取出來,不會修改讀指針,peek 之后數(shù)據(jù)仍在 LwRB 里。
lwrb_peek() 函數(shù)還有一些特殊的用法,函數(shù)原型如下:
size_t lwrb_peek(LWRB_VOLATILE lwrb_t* buff, size_t skip_count, void* data, size_t btp);
-
buff- LwRB 對象 -
skip_count- 跳過幾個數(shù)據(jù)再開始讀取 -
data- 讀取數(shù)據(jù)緩沖區(qū) -
btp- 需要讀取的字節(jié)數(shù)(bytes to peek)
同時該函數(shù)返回實際讀取的字節(jié)數(shù)。以下來理解如何使用:
假設(shè) LwRB 內(nèi)部狀態(tài)如下:W 處于 4 的位置,R 處于 6 的位置。

此時我們想要從 LwRB 里讀取 4 個字節(jié),調(diào)用 lwrb_peek(&buff, 0, data, 4); ,則最終得到的數(shù)據(jù)為:6, 7, 0, 1 ,該函數(shù)內(nèi)部幫我們處理了讀指針回滾的情況,這是目前許多 ring buffer 不具備的功能。而且該函數(shù)還支持跳過最開始處的字節(jié),例如調(diào)用 lwrb_peek(&buff, 2, data, 4); ,則最終得到的數(shù)據(jù)為:0, 1, 2, 3,該函數(shù)內(nèi)部自動跳過了讀指針對開始處的 6, 7 這兩個字節(jié)數(shù)據(jù)。
例程可參考:getting_started.c 和 lwrb_peek.c 。
讀寫事件通知
詳細(xì)內(nèi)容請參考 LwRB - Events,以下僅寫出我的理解。
讀寫事件通知接口如下:
typedef void (*lwrb_evt_fn)(LWRB_VOLATILE struct lwrb* buff, lwrb_evt_type_t evt, size_t bp);
void lwrb_set_evt_fn(LWRB_VOLATILE lwrb_t* buff, lwrb_evt_fn fn);
lwrb 的事件通知功能是用戶調(diào)用 lwrb_set_evt_fn() 向 lwrb 注冊一個回調(diào)函數(shù),然后 lwrb 每次進(jìn)行讀寫數(shù)據(jù)時,都會通過這個回調(diào)函數(shù)通知用戶當(dāng)前寫入了多少個字節(jié),或讀取了多少個字節(jié)。
該功能可用于打印日志,釋放信號量等,更多使用場景還待挖掘。
例程可參考:event.c 。
DMA 零拷貝收發(fā)數(shù)據(jù)
詳細(xì)內(nèi)容請參考 LwRB - DMA for embedded systems,以下僅寫出我的理解。
LwRB 提供的零拷貝功能,按照工作方式可分為兩種類型:
- 從 LwRB 里零拷貝讀取數(shù)據(jù)
- 向 LwRB 里零拷貝寫入數(shù)據(jù)
先介紹從 LwRB 里零拷貝讀取數(shù)據(jù),相關(guān)接口為:
/* Read data block management */
void* lwrb_get_linear_block_read_address(LWRB_VOLATILE lwrb_t* buff);
size_t lwrb_get_linear_block_read_length(LWRB_VOLATILE lwrb_t* buff);
size_t lwrb_skip(LWRB_VOLATILE lwrb_t* buff, size_t len);
假設(shè)當(dāng)前 LwRB 內(nèi)部狀態(tài)如下圖所示:

LwRB 內(nèi)部緩沖區(qū)已滿,W 處于 4 的位置,R 處于 5 的位置。
接口 lwrb_get_linear_block_read_address() 可獲取當(dāng)前 LwRB 內(nèi)部緩沖區(qū)第一個可讀取字節(jié)的地址,例如上圖中的 R,而 lwrb_get_linear_block_read_length() 函數(shù)獲取當(dāng)前 LwRB 內(nèi)部緩沖區(qū)可讀取的,且地址連續(xù)的字節(jié)數(shù),例如上圖中會得到結(jié)果 3,表示可連續(xù)讀取后續(xù)的數(shù)據(jù) 5, 6, 7 。
利用這兩個函數(shù),我們就得到了一個指針 p 和一個大小 len ,p 指向數(shù)據(jù)的起始地址,len 代表數(shù)據(jù)的字節(jié)大小,將 p 和 len 傳給 DMA,即可直接數(shù)據(jù)發(fā)送了。

DMA 發(fā)送完成后,我們還需要更新 LwRB 里的 R 指針,通過 lwrb_skip() 函數(shù)移動 R 指針,如下圖所示:

至此,我們就完成了從 LwRB 里零拷貝讀取數(shù)據(jù)的流程,例程可參考:zero_copy_from_lwrb_memory.c 。
向 LwRB 里零拷貝寫入數(shù)據(jù)的操作也類似,相關(guān)接口為:
/* Write data block management */
void* lwrb_get_linear_block_write_address(LWRB_VOLATILE lwrb_t* buff);
size_t lwrb_get_linear_block_write_length(LWRB_VOLATILE lwrb_t* buff);
size_t lwrb_advance(LWRB_VOLATILE lwrb_t* buff, size_t len);
假設(shè)當(dāng)前 LwRB 內(nèi)部狀態(tài)如下圖所示:

LwRB 內(nèi)部緩沖區(qū)為空,R 和 W 都處于 4 的位置。
接口 lwrb_get_linear_block_write_address() 可獲取 LwRB 內(nèi)部緩沖區(qū)第一個可寫入字節(jié)的地址,例如上圖中的 W,而 lwrb_get_linear_block_read_length() 函數(shù)返回 LwRB 當(dāng)前可連續(xù)地址寫入的大小,例如上圖中的結(jié)果為 4,表示可連續(xù)寫入 4, 5, 6, 7 處的數(shù)據(jù)。
利用這兩個函數(shù),我們就得到了一片地址連續(xù)的緩沖區(qū),包括其首地址 p 和大小 len,將其傳入 DMA 即可開始接收數(shù)據(jù)了。
DMA 接收完成后,我們還需要更新 LwRB 內(nèi)部的 W 指針,通過 lwrb_advance() 函數(shù)移動 W 指針,如下圖所示:

至此,我們就完成了向 LwRB 里零拷貝寫入數(shù)據(jù)的流程,例程可參考:zero_copy_to_lwrb_memory.c 。
線程安全
當(dāng)只有一個寫線程和一個讀線程時,LwRB 是線程安全的,不需要額外加鎖。
詳細(xì)內(nèi)容請參考 LwRB - Thread safety。