blob: f1d75dc098ccc66110708afa23485621399fa3d4 [file]
// Copyright 2026 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#![no_std]
/// A const-generic templated, fixed-size lock-free FIFO ring buffer.
pub struct RingBuffer<const SIZE: usize> {
buffer: [u8; SIZE],
head: usize,
tail: usize,
}
impl<const SIZE: usize> RingBuffer<SIZE> {
/// Creates a new, empty `RingBuffer` instance.
#[must_use]
pub const fn new() -> Self {
Self {
buffer: [0; SIZE],
head: 0,
tail: 0,
}
}
/// Pushes a byte to the back of the ring buffer.
/// Returns `false` if the buffer is full, or `true` on success.
pub fn push(&mut self, value: u8) -> bool {
let next = (self.head + 1) % self.buffer.len();
if next == self.tail {
false // Full
} else {
self.buffer[self.head] = value;
self.head = next;
true
}
}
/// Pushes a slice of bytes to the back of the ring buffer.
///
/// Supports partial pushes if the buffer is almost full, and returns the number
/// of bytes successfully pushed. Performs at most two block copies.
pub fn push_slice(&mut self, data: &[u8]) -> usize {
if data.is_empty() {
return 0;
}
let mut bytes_written = 0;
let buffer_len = self.buffer.len();
{
// This mutably borrows `self` so it is wrapped in a sub-scope so that
// the borrow is dropped before `self.head` is updated below.
let (segment1, segment2) = self.free_space_segments();
for segment in [segment1, segment2] {
if !segment.is_empty() && bytes_written < data.len() {
let write_len = core::cmp::min(segment.len(), data.len() - bytes_written);
segment[..write_len]
.copy_from_slice(&data[bytes_written..bytes_written + write_len]);
bytes_written += write_len;
}
}
}
self.head = (self.head + bytes_written) % buffer_len;
bytes_written
}
/// Returns the two contiguous segments of free space in the buffer, in logical order.
pub fn free_space_segments(&mut self) -> (&mut [u8], &mut [u8]) {
let len = self.buffer.len();
let head = self.head;
let tail = self.tail;
let (left, right) = self.buffer.split_at_mut(head);
if head >= tail {
let (right_len, left_len) = if tail > 0 {
(len - head, tail - 1)
} else {
(len - 1 - head, 0)
};
(&mut right[..right_len], &mut left[..left_len])
} else {
// When head < tail, the free space is always in a single contiguous buffer.
let right_len = tail - 1 - head;
(&mut right[..right_len], &mut [])
}
}
/// Pops a byte from the front of the ring buffer.
/// Returns `Some(byte)` if data is available, or `None` if empty.
pub fn pop(&mut self) -> Option<u8> {
if self.head == self.tail {
None // Empty
} else {
let value = self.buffer[self.tail];
self.tail = (self.tail + 1) % self.buffer.len();
Some(value)
}
}
/// Pops a slice of the maximum contiguous segment of at most `max_len` bytes.
///
/// Invokes the closure `f` with a reference to this slice, then advances the ring buffer
/// by the amount of bytes consumed. Returns the closure's result.
pub fn pop_slice<R>(&mut self, max_len: usize, f: impl FnOnce(&[u8]) -> R) -> R {
if self.head == self.tail {
return f(&[]);
}
let segment = if self.head >= self.tail {
&self.buffer[self.tail..self.head]
} else {
&self.buffer[self.tail..self.buffer.len()]
};
let segment_len = core::cmp::min(segment.len(), max_len);
let result = f(&segment[..segment_len]);
self.tail = (self.tail + segment_len) % self.buffer.len();
result
}
/// Returns `true` if the ring buffer contains no elements.
#[must_use]
pub fn is_empty(&self) -> bool {
self.head == self.tail
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ring_buffer_basic() {
let mut ring_buffer = RingBuffer::<4>::new();
assert!(ring_buffer.is_empty());
// A ring buffer of size 4 can only hold 3 items (due to head/tail tracking boundary)
assert!(ring_buffer.push(1));
assert!(ring_buffer.push(2));
assert!(ring_buffer.push(3));
assert!(!ring_buffer.push(4)); // Full
assert_eq!(ring_buffer.pop(), Some(1));
assert_eq!(ring_buffer.pop(), Some(2));
assert_eq!(ring_buffer.pop(), Some(3));
assert_eq!(ring_buffer.pop(), None);
assert!(ring_buffer.is_empty());
}
#[test]
fn test_ring_buffer_wrap_around() {
let mut ring_buffer = RingBuffer::<3>::new();
assert!(ring_buffer.push(10));
assert!(ring_buffer.push(20));
assert_eq!(ring_buffer.pop(), Some(10));
assert!(ring_buffer.push(30)); // Wraps around head pointer
assert_eq!(ring_buffer.pop(), Some(20));
assert_eq!(ring_buffer.pop(), Some(30));
assert!(ring_buffer.is_empty());
}
#[test]
fn test_ring_buffer_pop_slice_contiguous() {
let mut ring_buffer = RingBuffer::<5>::new();
assert!(ring_buffer.push(1));
assert!(ring_buffer.push(2));
assert!(ring_buffer.push(3));
// Pop at most 2 bytes
let popped = ring_buffer.pop_slice(2, |slice| {
assert_eq!(slice, &[1, 2]);
slice.len()
});
assert_eq!(popped, 2);
// Verify remaining byte
assert_eq!(ring_buffer.pop(), Some(3));
assert!(ring_buffer.is_empty());
}
#[test]
fn test_ring_buffer_pop_slice_wrap_around() {
let mut ring_buffer = RingBuffer::<4>::new();
assert!(ring_buffer.push(10));
assert!(ring_buffer.push(20));
assert!(ring_buffer.push(30));
// 0 1 2 3
// [T _ _ H]
// 10 20 30 .
assert_eq!(ring_buffer.pop(), Some(10));
// 0 1 2 3
// _ [T _ H]
// . 20 30 .
assert_eq!(ring_buffer.pop(), Some(20));
// 0 1 2 3
// _ _ [T H]
// . . 30 .
assert!(ring_buffer.push(40));
// 0 1 2 3
// H] _ [T _
// . . 30 40
assert!(ring_buffer.push(50));
// 0 1 2 3
// _ H] [T _
// 50 . 30 40
// Active elements: 30 (at 2), 40 (at 3), 50 (at 0)
// Contiguous segment from tail (2) to end of buffer (3) is &[30, 40]
let popped1 = ring_buffer.pop_slice(5, |slice| {
assert_eq!(slice, &[30, 40]);
slice.len()
});
assert_eq!(popped1, 2);
// 0 1 2 3
// [T H] _ _
// 50 . . .
// Next contiguous segment is &[50]
let popped2 = ring_buffer.pop_slice(5, |slice| {
assert_eq!(slice, &[50]);
slice.len()
});
assert_eq!(popped2, 1);
// 0 1 2 3
// _ [TH] _ _
// 50 . . .
assert!(ring_buffer.is_empty());
}
#[test]
fn test_ring_buffer_push_slice_basic() {
let mut ring_buffer = RingBuffer::<5>::new();
let pushed = ring_buffer.push_slice(&[1, 2, 3]);
assert_eq!(pushed, 3);
assert_eq!(ring_buffer.pop(), Some(1));
assert_eq!(ring_buffer.pop(), Some(2));
assert_eq!(ring_buffer.pop(), Some(3));
assert!(ring_buffer.is_empty());
}
#[test]
fn test_ring_buffer_push_slice_partial() {
let mut ring_buffer = RingBuffer::<4>::new();
// RingBuffer of size 4 has capacity 3
let pushed = ring_buffer.push_slice(&[10, 20, 30, 40]);
assert_eq!(pushed, 3); // Partial push, 40 is dropped
assert_eq!(ring_buffer.pop(), Some(10));
assert_eq!(ring_buffer.pop(), Some(20));
assert_eq!(ring_buffer.pop(), Some(30));
assert!(ring_buffer.is_empty());
}
#[test]
fn test_ring_buffer_push_slice_wrap_around() {
let mut ring_buffer = RingBuffer::<5>::new();
assert_eq!(ring_buffer.push_slice(&[1, 2, 3]), 3);
assert_eq!(ring_buffer.pop(), Some(1)); // tail = 1
// head = 3, tail = 1. Contiguous free end is 3..5.
let pushed = ring_buffer.push_slice(&[4, 5]);
assert_eq!(pushed, 2); // head wraps to 0
assert_eq!(ring_buffer.pop(), Some(2));
assert_eq!(ring_buffer.pop(), Some(3)); // tail = 3
// head = 0, tail = 3. Contiguous free segment is 0..2.
let pushed2 = ring_buffer.push_slice(&[6, 7]);
assert_eq!(pushed2, 2);
assert_eq!(ring_buffer.pop(), Some(4));
assert_eq!(ring_buffer.pop(), Some(5));
assert_eq!(ring_buffer.pop(), Some(6));
assert_eq!(ring_buffer.pop(), Some(7));
assert!(ring_buffer.is_empty());
}
}