Expand description
Stack-allocated ring buffers for no-std embedded targets.
§Primitives
| Type | When to reach for it |
|---|---|
RingBuf | Single-owner ring — simple, no atomics, &mut access. |
SeqRing | Lock-free SPSC ring that overwrites old entries (lossy, high-throughput). |
EventBuf | Lock-free SPSC ring with backpressure — rejects pushes when full. |
All three are fixed-size, zero-allocation, and generic over T: Copy.
§Common traits
| Trait | Role | Implementors |
|---|---|---|
Sink<T> | Accept events | RingBuf, seq_ring::Producer, event_buf::Producer |
Source<T> | Yield events | seq_ring::Consumer, event_buf::Consumer |
Link<In,Out> | Both | Blanket impl for any Sink<In> + Source<Out> |
The traits::forward function transfers items from any Source to any
Sink, making it easy to bridge different buffer types.
§Static bring-up
static_spsc! declares a static buffer together with
named handle types, so a signature need not spell out
event_buf::Producer<'static, T, N>:
ph_eventing::static_spsc! {
pub mod telemetry: EventBuf<u32, 64>;
}
fn on_sample(tx: &telemetry::Tx, v: u32) { let _ = tx.push(v); }
let (tx, rx) = telemetry::take().expect("first take");
on_sample(&tx, 1);
assert_eq!(rx.pop(), Some(1));§Quick start — RingBuf
use ph_eventing::RingBuf;
let mut ring = RingBuf::<u32, 4>::new();
ring.push(1);
ring.push(2);
ring.push(3);
assert_eq!(ring.latest(), Some(3));§Quick start — SeqRing
use ph_eventing::SeqRing;
let ring = SeqRing::<u32, 64>::new();
let producer = ring.try_producer().expect("producer");
let mut consumer = ring.try_consumer().expect("consumer");
producer.push(42);
consumer.poll_one(|seq, v| {
assert_eq!(seq, 1);
assert_eq!(*v, 42);
});§Quick start — EventBuf
use ph_eventing::EventBuf;
let buf = EventBuf::<u32, 4>::new();
let producer = buf.try_producer().expect("producer");
let consumer = buf.try_consumer().expect("consumer");
assert!(producer.push(1).is_ok());
assert!(producer.push(2).is_ok());
assert_eq!(consumer.pop(), Some(1));§Quick start — forward
use ph_eventing::{SeqRing, EventBuf};
use ph_eventing::traits::{Source, Sink, forward};
let seq = SeqRing::<u32, 8>::new();
let sp = seq.try_producer().expect("producer");
let mut sc = seq.try_consumer().expect("consumer");
sp.push(1); sp.push(2);
let eb = EventBuf::<u32, 8>::new();
let mut ep = eb.try_producer().expect("producer");
let (n, err) = forward(&mut sc, &mut ep, 10);
assert_eq!(n, 2);
assert!(err.is_none());§No-std
The crate is #![no_std] by default. Tests require std.
§Targets without atomics
SeqRing and EventBuf require 32-bit atomics. For targets that lack them
(for example thumbv6m-none-eabi), enable
portable-atomic-unsafe-assume-single-core or portable-atomic-critical-section.
The crate always compiles those modules, so no-atomic targets need one of
those features even when only RingBuf is used. RingBuf itself uses no
atomics.
§Safety and concurrency
-
RingBufhas no atomics and no interior mutability — standard Rust borrow rules apply. It stores slots asMaybeUninit<T>and reads only live entries, so it does containunsafe. -
SeqRingandEventBufare SPSC by design: exactly one producer and one consumer must be active. Usetry_producer()/try_consumer(), which returnNonerather than panicking — on a microcontroller a panic is a reset. The panickingproducer()/consumer()are deprecated since 0.2.0. Using unsafe to bypass these constraints is undefined behavior.The examples here use
.expect(...)for brevity, which is a panic. That is fine in a doctest on a host; in firmware, branch on theNone:
let Some(tx) = buf.try_producer() else {
return; // already claimed -- report it, do not reset the device
};EventBufis race-free by construction — its producer and consumer never touch the same slot — and passes Miri with the data-race detector enabled.SeqRingis a seqlock and carries a known formal data race. The copy is never returned and never becomes an invalid value, but the access is undefined behaviour by the letter of the memory model. Practical consequence: running Miri over a test that drives this ring from two threads reports UB inside this crate — that is the deviation, not a new bug. It is a deliberate trade of formal soundness for accepting anyT: Copy; theseq_ringmodule docs give the alternatives and why each was rejected.EventBufhas no such caveat, but applies backpressure rather than overwriting, so it is not a drop-in replacement.
§Using it across contexts
The typical embedded shape is a producer in an interrupt handler and a consumer in a task loop.
SeqRingandEventBufareSyncwhenT: Send, so&bufcan be handed to both contexts. TheProducerandConsumerhandles areSend + !Sync: move each into the context that owns it, never share one.- The handles borrow the buffer, so the buffer must outlive them.
new()is aconst fnon the normal build, sostatic BUF: EventBuf<u32, 64> = EventBuf::new();works. (Under--cfg loomit is non-const because Loom’s atomics are not const-constructible.) Handles still borrow the buffer, so an ISR / task split typically pairs thestaticwith aStaticCellor similar for the handles themselves.
N is fixed at compile time and the buffer lives inline —
N * size_of::<T>() bytes, no allocation. For EventBuf it is the
backpressure threshold; for SeqRing it is how far the consumer may lag
before entries are lost. It need not be a power of two.
§SeqRing semantics
- Sequence numbers are monotonically increasing
u32values;0is reserved for “empty”. poll_one/poll_up_todrain in-order and returnPollStats;poll_one_valuereturns(seq, T)without a hook.latest/latest_valueread the newest value without advancing the consumer cursor.- If the consumer lags by more than
N, it skips ahead and reports drops viaPollStats. - Once every
2^32 - 1pushes the sequence counter wraps, and a few extra entries are dropped there becausepushskips the reserved sequence0: exactly one for a power-of-twoN, none ifNdivides2^32 - 1, up toN - 1otherwise. Reported as ordinary drops, never a stale or torn value. Prefer a power of two forN; see theseq_ringmodule docs. Consumer::droppedsaturates rather than wrapping;usizeis 32 bits on the targets this crate ships to, so a long-lived lagging consumer can reach the top of the range.
§EventBuf semantics
pushreturnsErr(val)when the buffer is full — no data is silently lost.popreturns the oldest item, orNonewhen empty.peekcopies the oldest item without advancing the consumer cursor.drain(max, hook)consumes up tomaxitems through a callback.
Re-exports§
pub use event_buf::EventBuf;pub use ring::RingBuf;pub use seq_ring::PollStats;pub use seq_ring::SeqRing;pub use traits::Link;pub use traits::Sink;pub use traits::Source;
Modules§
- event_
buf - Bounded SPSC event buffer with backpressure — no heap, no alloc.
- ring
- Fixed-size, stack-allocated ring buffer — no heap, no alloc, no atomics.
- seq_
ring - Lock-free SPSC overwrite ring for high-rate telemetry in no-std contexts.
- traits
- Common traits for event producers and consumers.
Macros§
- static_
spsc - Declare a
staticSPSC buffer with its handle types and a paired take.