Fundamentals 15 min read

Why Use a Lightweight Ring Buffer? Deep Dive into LwRB for Embedded Systems

This article explains the need for circular buffers in embedded development, introduces the lightweight LwRB library, details its memory‑safe and thread‑safe design using C11 atomics, walks through core data structures and two‑stage read/write algorithms, provides complete code examples, and compares ring buffers with normal buffers and message queues.

Liangxu Linux
Liangxu Linux
Liangxu Linux
Why Use a Lightweight Ring Buffer? Deep Dive into LwRB for Embedded Systems

Why a Ring Buffer?

In embedded development, scenarios such as serial port reception, sensor acquisition, or network packet handling require efficient management of continuous data streams within limited memory. A circular (ring) buffer allows data to flow in and out continuously without the costly reset of a linear buffer, making full use of every byte of storage.

LwRB Overview

LwRB (Lightweight Ring Buffer) is a small, generic ring‑buffer library written in C. It has gained thousands of stars on GitHub and is used in many embedded projects.

https://github.com/MaJerle/lwrb

Key Features

Zero dynamic memory allocation – uses only static memory, avoiding fragmentation.

High performance – employs optimized memcpy instead of per‑byte loops.

Thread safety – uses C11 atomic operations for single‑producer‑single‑consumer concurrency.

DMA‑friendly – supports zero‑copy operations, ideal for hardware DMA.

Core Principles

Ring Buffer Model

The buffer is defined by a read pointer R, a write pointer W, and a total size S. The pointers move circularly around the fixed‑size array.

R pointer (read) – acts as the consumer.

W pointer (write) – acts as the producer.

Buffer size S – total length of the storage area.

Key rules:

When W == R the buffer is empty.

When W == (R‑1) % S the buffer is full.

The usable capacity is S‑1 bytes because one slot must be reserved to distinguish empty from full.

Memory‑Safety Mechanism

LwRB checks the free space before writing to prevent overflow.

// Safety check before write
free = lwrb_get_free(buff);
if (free == 0 || (free < btw && (flags & LWRB_FLAG_WRITE_ALL))) {
    return 0; // abort to avoid overflow
}
btw = BUF_MIN(free, btw); // limit write size to available space

Thread‑Safety Implementation

LwRB relies on C11 atomic operations, which guarantee that reads and writes of the pointers are indivisible and cannot be interrupted by other threads.

C11 introduced atomic operations to solve data‑race problems. An atomic operation is executed as a single, uninterruptible step, eliminating the need for mutexes in simple producer‑consumer scenarios.
#define LWRB_LOAD(var, type)  atomic_load_explicit(&(var), (type))
#define LWRB_STORE(var, val, type) atomic_store_explicit(&(var), (val), (type))

Key Code

Core Data Structure

typedef struct lwrb {
    uint8_t *buff;          // buffer data pointer
    lwrb_sz_t size;         // buffer size
    lwrb_sz_atomic_t r_ptr; // read pointer (atomic)
    lwrb_sz_atomic_t w_ptr; // write pointer (atomic)
    lwrb_evt_fn evt_fn;    // optional event callback
    void *arg;              // user data for callback
} lwrb_t;

Separate pointer and size – allows buffers of any length.

Atomic pointers – ensure thread‑safe read/write indices.

Event mechanism – flexible callback for custom actions.

Two‑Stage Write Strategy

LwRB writes data in two phases: first the linear part, then the wrap‑around part if needed.

// Phase 1: linear part
tocopy = BUF_MIN(buff->size - w_ptr, btw);
BUF_MEMCPY(&buff->buff[w_ptr], d_ptr, tocopy);
d_ptr += tocopy;
w_ptr += tocopy;
btw -= tocopy;
// Phase 2: wrap‑around part
if (btw > 0) {
    BUF_MEMCPY(buff->buff, d_ptr, btw);
    w_ptr = btw;
}
// Phase 3: atomic update of the write pointer
LWRB_STORE(buff->w_ptr, w_ptr, memory_order_release);

Two‑Stage Read Strategy

// Phase 1: linear part
tocopy = BUF_MIN(buff->size - r_ptr, btr);
BUF_MEMCPY(d_ptr, &buff->buff[r_ptr], tocopy);
// Phase 2: wrap‑around part
if (btr > 0) {
    BUF_MEMCPY(d_ptr, buff->buff, btr);
    r_ptr = btr;
}

Peek Function

The peek API lets you look at data without moving the read pointer, which is useful for protocol parsing.

lwrb_sz_t lwrb_peek(const lwrb_t *buff, lwrb_sz_t skip_count, void *data, lwrb_sz_t btp);

Examples

Minimal Example

#include <stdio.h>
#include <string.h>
#include "lwrb/lwrb.h"

int main(void) {
    lwrb_t buff = {0};
    uint8_t buff_data[8] = {0};
    lwrb_init(&buff, buff_data, sizeof(buff_data));
    lwrb_write(&buff, "0123", 4);
    printf("Bytes in buffer: %d
", (int)lwrb_get_full(&buff));
    uint8_t data[8] = {0};
    size_t len = lwrb_read(&buff, data, sizeof(data));
    printf("Number of bytes read: %d, data: %s
", (int)len, data);
    return 0;
}

Ring Buffer vs Normal Buffer Overwrite Behavior

// Demonstrates how a ring buffer overwrites old data while a normal buffer cannot.
// (Full source code omitted for brevity – see original article for complete listing.)

Result:

Ring buffer automatically overwrites the oldest data when full, ideal for real‑time streams.

Normal buffer stops writing when full, requiring manual data movement and risking loss.

Ring Buffer vs Message Queue

Similarities

Both follow the producer‑consumer model, decoupling producer and consumer speeds.

Both act as an intermediate layer to handle mismatched processing rates.

Both need synchronization mechanisms (mutex, semaphore, etc.) for concurrent access.

Differences

Data structure – Ring buffer uses a fixed‑size array; message queues typically use linked lists or dynamically growing arrays.

Data unit – Ring buffer works with a byte stream or fixed‑size blocks, lacking inherent message boundaries; message queues handle discrete messages with explicit boundaries.

Size flexibility – Ring buffer size is static; message queues can grow dynamically or have configurable limits.

Priority support – Ring buffers are FIFO only; message queues often support priority ordering.

Overflow handling – Ring buffers may overwrite old data or block; message queues usually block the producer or return an error, preserving existing messages.

Memory efficiency – Ring buffers have no allocation overhead; message queues may incur fragmentation and allocation cost.

Typical use cases – Ring buffers excel in high‑frequency, continuous data streams (audio, video, sensor data); message queues suit structured, discrete message passing (IPC, service communication).

Conclusion

Ring buffers offer high performance and low memory overhead, making them ideal for real‑time, memory‑constrained environments, but they require careful handling of message boundaries.

Message queues provide greater flexibility, priority handling, and safety for structured messages, at the cost of higher memory usage and slightly lower throughput.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

thread safetyRing Buffer
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.