Skip to main content

consortium_dbg/
ring.rs

1// Copyright 2026 Ethan Wu
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! The `CDBG` ring as a Consortium transport.
18//!
19//! A log ring is the simplest memory-backed link there is: one writer (firmware),
20//! one reader (the host), no doorbell, and a hand-off that never waits — a frame
21//! either fits in the ring or is dropped with [`Flags::Overflow`] latched. That is
22//! exactly `consortium-core`'s non-waiting pair, so the ring implements it:
23//!
24//! - [`RingWriter`] is a [`TrySendTransport`]: [`try_send`](TrySendTransport::try_send)
25//!   appends one frame, or reports [`TryError::WouldBlock`] when the reader has not
26//!   yet freed enough room. It never waits, so the `defmt` logger and the panic
27//!   handler can call it; [`ConsortiumLogger`](crate::logger::ConsortiumLogger)
28//!   writes every frame through it.
29//! - [`RingReader`] is a [`TryRecvTransport`]: [`try_recv`](TryRecvTransport::try_recv)
30//!   copies the next frame into a caller buffer, allocation-free, so a `no_std`
31//!   reader works as well as the host's [`HostRing`](crate::host::HostRing), which
32//!   is built on it.
33//!
34//! With no doorbell there is nothing to park on; a reader that wants to wait polls,
35//! or spins with `consortium_core::wait::spin_until`.
36//!
37//! Both sides keep the ring's descriptor discipline: the writer only writes
38//! `write_ptr`, `capacity`, `flags` and packet data; the reader only writes
39//! `read_ptr` and each packet's consumed bit.
40//!
41//! # Coherent regions only
42//!
43//! That discipline holds only on a coherent mapping. The `CDBG` layout packs
44//! writer-owned and reader-owned fields into the same cache lines — `write_ptr`,
45//! `read_ptr` and `flags` sit in one 16-byte descriptor, and each packet's consumed
46//! bit shares a line with writer-owned payload. Cache maintenance cannot keep them
47//! apart: a clean writes back the *whole* line, so each side's flush would overwrite
48//! the other's latest update with its own stale copy. Both ends therefore refuse a
49//! region that reports [`is_coherent`](Region::is_coherent) `false`
50//! ([`RingError::NonCoherentRegion`]), such as a
51//! [`CachedRegion`](consortium_core::CachedRegion). Supporting cached peers would
52//! need the independently written state on separate cache lines — a `CDBG` format
53//! change.
54
55use consortium_core::{
56    Region, TransportError, TryError, TryRecvTransport, TrySendTransport,
57    descriptor::DescriptorHeaderError,
58};
59
60use crate::{
61    descriptor::{
62        DESCRIPTOR_SIZE, Descriptor, FLAGS_OFFSET, Flags, MAGIC, MAX_FRAME_LEN, ReadOutcome,
63        VERSION,
64    },
65    packet::PACKET_HEADER_SIZE,
66    utils::wv_u16,
67};
68
69/// Why a ring could not be opened, or a frame could not be moved.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub enum RingError {
72    /// The region does not start with the `CDBG` magic.
73    MagicMismatch,
74    /// The descriptor is a `CDBG` version this crate cannot read.
75    VersionMismatch,
76    /// The descriptor's `capacity` does not fit the region.
77    CapacityMismatch {
78        /// Capacity recorded in the descriptor (0 when the region is too small to
79        /// hold a descriptor at all).
80        descriptor: u16,
81        /// The region's length.
82        mapped: usize,
83    },
84    /// A ring cursor lies outside `[DATA_START, capacity)`.
85    PointerOutOfBounds {
86        /// The offending cursor.
87        ptr: u16,
88        /// The descriptor's capacity.
89        capacity: u16,
90    },
91    /// A region to initialize is too short for a descriptor and one frame, or
92    /// longer than a `u16` capacity can describe.
93    RegionLength {
94        /// The region's length.
95        len: usize,
96    },
97    /// The frame can never fit: it is longer than one packet carries, or than the
98    /// ring's data area. It was dropped and [`Flags::Overflow`] latched.
99    FrameTooLarge {
100        /// The frame's length.
101        len: usize,
102        /// The longest frame this ring accepts.
103        max: usize,
104    },
105    /// The next frame is longer than the receive buffer; it was left unread.
106    BufferTooSmall {
107        /// The buffer's length.
108        len: usize,
109        /// The frame's length.
110        needed: usize,
111    },
112    /// The writer latched [`Flags::Error`]; the ring accepts no more frames.
113    Faulted,
114    /// The region is not cache-coherent. The `CDBG` layout shares cache lines
115    /// between writer- and reader-owned fields, so it needs a coherent mapping.
116    NonCoherentRegion,
117}
118
119impl core::fmt::Display for RingError {
120    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
121        match self {
122            Self::MagicMismatch => f.write_str("CDBG magic mismatch"),
123            Self::VersionMismatch => f.write_str("CDBG version mismatch"),
124            Self::CapacityMismatch { descriptor, mapped } => write!(
125                f,
126                "descriptor capacity {descriptor} exceeds mapped length {mapped}"
127            ),
128            Self::PointerOutOfBounds { ptr, capacity } => write!(
129                f,
130                "ring pointer {ptr} is out of bounds (capacity {capacity})"
131            ),
132            Self::RegionLength { len } => write!(f, "a {len}-byte region cannot hold a ring"),
133            Self::FrameTooLarge { len, max } => {
134                write!(f, "a {len}-byte frame exceeds the ring's {max}-byte limit")
135            }
136            Self::BufferTooSmall { len, needed } => {
137                write!(f, "a {needed}-byte frame does not fit a {len}-byte buffer")
138            }
139            Self::Faulted => f.write_str("the ring writer latched an error"),
140            Self::NonCoherentRegion => f.write_str("a CDBG ring needs a cache-coherent region"),
141        }
142    }
143}
144
145impl core::error::Error for RingError {}
146
147/// Refuse a region whose cache maintenance would let the two sides overwrite each
148/// other's fields (see the module docs).
149fn require_coherent<R: Region>(region: &R) -> Result<(), RingError> {
150    if region.is_coherent() {
151        Ok(())
152    } else {
153        Err(RingError::NonCoherentRegion)
154    }
155}
156
157/// Read and validate the descriptor at the start of `region`.
158///
159/// Checks that the region is coherent, the magic, the version, that `capacity`
160/// fits the region, and that both cursors lie in the data area — so every later
161/// cursor dereference stays inside the region.
162pub(crate) fn validate<R: Region>(region: &R) -> Result<Descriptor, RingError> {
163    require_coherent(region)?;
164    let len = region.len();
165    if len < DESCRIPTOR_SIZE {
166        return Err(RingError::CapacityMismatch {
167            descriptor: 0,
168            mapped: len,
169        });
170    }
171    // SAFETY: the region is valid for `len >= DESCRIPTOR_SIZE` bytes.
172    let desc = unsafe { Descriptor::from_raw_ptr(base(region)) };
173    desc.header.check(MAGIC, VERSION).map_err(|e| match e {
174        DescriptorHeaderError::BadVersion(..) => RingError::VersionMismatch,
175        _ => RingError::MagicMismatch,
176    })?;
177    if desc.capacity as usize > len {
178        return Err(RingError::CapacityMismatch {
179            descriptor: desc.capacity,
180            mapped: len,
181        });
182    }
183    let data_start = DESCRIPTOR_SIZE as u16;
184    for ptr in [desc.read_ptr, desc.write_ptr] {
185        if ptr < data_start || ptr >= desc.capacity {
186            return Err(RingError::PointerOutOfBounds {
187                ptr,
188                capacity: desc.capacity,
189            });
190        }
191    }
192    Ok(desc)
193}
194
195fn base<R: Region>(region: &R) -> *mut u8 {
196    // SAFETY: `Region::as_ptr` only obliges the caller to stay within `len` bytes;
197    // every access below is bounded by a descriptor `capacity` checked against it.
198    unsafe { region.as_ptr() }
199}
200
201/// The longest frame a ring of `capacity` bytes accepts.
202fn max_frame(capacity: u16) -> usize {
203    (capacity as usize)
204        .saturating_sub(DESCRIPTOR_SIZE + PACKET_HEADER_SIZE)
205        .min(MAX_FRAME_LEN)
206}
207
208/// The firmware end of a `CDBG` ring: appends frames, never waits.
209pub struct RingWriter<R> {
210    region: R,
211    capacity: u16,
212}
213
214impl<R: Region> RingWriter<R> {
215    /// Reset `region` to a fresh, empty ring and write into it.
216    ///
217    /// Debug SRAM often survives a warm reset, so the data area is zeroed before the
218    /// cursors are parked: a stale packet header that still parses as valid would
219    /// otherwise read as unread data and refuse the first real frames.
220    ///
221    /// # Safety
222    ///
223    /// This must be the ring's only writer for as long as it lives: two writers
224    /// would race on `write_ptr` and the packet data.
225    pub unsafe fn init(region: R) -> Result<Self, RingError> {
226        require_coherent(&region)?;
227        let len = region.len();
228        let Ok(capacity) = u16::try_from(len) else {
229            return Err(RingError::RegionLength { len });
230        };
231        if max_frame(capacity) == 0 {
232            return Err(RingError::RegionLength { len });
233        }
234        let base = base(&region);
235        for offset in DESCRIPTOR_SIZE..len {
236            // SAFETY: `offset < len`, inside the region; a byte volatile store has no
237            // alignment requirement and is valid for device or normal memory.
238            #[allow(clippy::multiple_unsafe_ops_per_block)]
239            unsafe {
240                base.add(offset).write_volatile(0)
241            };
242        }
243        // SAFETY: a fresh descriptor parks both cursors at the data start and clears
244        // every flag; `len >= DESCRIPTOR_SIZE` was checked through `max_frame`.
245        unsafe { Descriptor::new(capacity).write_to_raw_ptr(base) };
246        Ok(Self { region, capacity })
247    }
248
249    /// Continue writing into the ring already in `region`, after validating it.
250    ///
251    /// # Safety
252    ///
253    /// As for [`init`](Self::init): this must be the ring's only writer.
254    pub unsafe fn attach(region: R) -> Result<Self, RingError> {
255        let capacity = validate(&region)?.capacity;
256        Ok(Self { region, capacity })
257    }
258
259    /// Continue writing into a ring of `capacity` bytes the caller has just
260    /// validated or initialized, skipping [`attach`](Self::attach)'s checks.
261    ///
262    /// For the logger's per-frame path, where `acquire()` has already done that
263    /// work for this very frame.
264    ///
265    /// # Safety
266    ///
267    /// As for [`init`](Self::init), and the descriptor at the start of `region`
268    /// must be a valid `CDBG` descriptor of exactly `capacity` bytes, with
269    /// `capacity <= region.len()`.
270    #[cfg(feature = "logger")]
271    pub(crate) unsafe fn attach_unchecked(region: R, capacity: u16) -> Self {
272        Self { region, capacity }
273    }
274
275    /// The region back.
276    pub fn into_region(self) -> R {
277        self.region
278    }
279
280    /// A point-in-time copy of the descriptor.
281    pub fn snapshot(&self) -> Descriptor {
282        // SAFETY: the descriptor was validated or written at construction.
283        unsafe { Descriptor::from_raw_ptr(base(&self.region)) }
284    }
285
286    /// Latch [`Flags::Panic`], keeping every other flag, so the reader can tell a
287    /// crash from a clean end of stream.
288    pub fn mark_panic(&mut self) {
289        let mut desc = self.snapshot();
290        desc.flags.insert(Flags::Panic);
291        // SAFETY: the flags word is writer-owned and inside the descriptor.
292        unsafe { wv_u16(base(&self.region), FLAGS_OFFSET, desc.flags.bits()) };
293    }
294}
295
296impl<R: Region> TransportError for RingWriter<R> {
297    type Error = RingError;
298}
299
300/// Appends one frame as one packet.
301///
302/// - `Ok`: the frame is in the ring and [`Flags::Overflow`] is cleared.
303/// - [`TryError::WouldBlock`]: the ring is full of unread frames; the frame was
304///   dropped and `Overflow` latched. A later attempt succeeds once the reader
305///   drains.
306/// - [`RingError::FrameTooLarge`]: the frame can never fit; dropped, `Overflow`
307///   latched.
308/// - [`RingError::Faulted`]: the writer latched [`Flags::Error`]; nothing written.
309///
310/// Logs nothing: it runs inside the `defmt` logger, which must not re-enter
311/// `defmt`.
312impl<R: Region> TrySendTransport for RingWriter<R> {
313    fn try_send(&mut self, frame: &[u8]) -> Result<(), TryError<RingError>> {
314        let base = base(&self.region);
315        // SAFETY: the descriptor was validated or written at construction.
316        let mut desc = unsafe { Descriptor::from_raw_ptr(base) };
317        if desc.flags.contains(Flags::Error) {
318            return Err(TryError::Fail(RingError::Faulted));
319        }
320        // `capacity` is this writer's own field, so a different value means the ring
321        // was re-initialized underneath it; never trust it past the region.
322        if desc.capacity != self.capacity {
323            return Err(TryError::Fail(RingError::CapacityMismatch {
324                descriptor: desc.capacity,
325                mapped: self.region.len(),
326            }));
327        }
328        // SAFETY: `base` owns this descriptor's ring, `capacity` bytes long and
329        // inside the region (checked at construction).
330        let wrote = unsafe { desc.write_one_frame(base, frame) };
331        if wrote {
332            return Ok(());
333        }
334        let max = self.max_send_size();
335        if frame.len() > max {
336            Err(TryError::Fail(RingError::FrameTooLarge {
337                len: frame.len(),
338                max,
339            }))
340        } else {
341            Err(TryError::WouldBlock)
342        }
343    }
344
345    fn max_send_size(&self) -> usize {
346        max_frame(self.capacity)
347    }
348}
349
350/// The reading end of a `CDBG` ring: takes frames, never waits, never allocates.
351pub struct RingReader<R> {
352    region: R,
353}
354
355impl<R: Region> RingReader<R> {
356    /// Read the ring in `region`, after validating its descriptor.
357    ///
358    /// # Safety
359    ///
360    /// This must be the ring's only reader for as long as it lives: two readers
361    /// would race on `read_ptr`.
362    pub unsafe fn new(region: R) -> Result<Self, RingError> {
363        validate(&region)?;
364        Ok(Self { region })
365    }
366
367    /// The region back.
368    pub fn into_region(self) -> R {
369        self.region
370    }
371
372    /// The region this reader reads.
373    pub fn region(&self) -> &R {
374        &self.region
375    }
376
377    /// A point-in-time copy of the descriptor.
378    pub fn snapshot(&self) -> Descriptor {
379        // SAFETY: the descriptor was validated at construction.
380        unsafe { Descriptor::from_raw_ptr(base(&self.region)) }
381    }
382
383    /// A snapshot whose writer-written fields still keep reads inside the region.
384    ///
385    /// The writer owns `capacity` and the reader owns `read_ptr`, but both live in
386    /// memory the other core can write: re-check them on every attempt so a
387    /// misbehaving writer cannot steer a read outside the region.
388    fn checked_snapshot(&self) -> Result<Descriptor, RingError> {
389        let desc = self.snapshot();
390        if desc.capacity as usize > self.region.len() {
391            return Err(RingError::CapacityMismatch {
392                descriptor: desc.capacity,
393                mapped: self.region.len(),
394            });
395        }
396        if (desc.read_ptr as usize) < DESCRIPTOR_SIZE || desc.read_ptr >= desc.capacity {
397            return Err(RingError::PointerOutOfBounds {
398                ptr: desc.read_ptr,
399                capacity: desc.capacity,
400            });
401        }
402        Ok(desc)
403    }
404
405    /// Take every frame that was in the ring when the call began, handing each to
406    /// `on_frame`, and return how many there were.
407    ///
408    /// The batch is bounded by one snapshot of `write_ptr`: frames the writer
409    /// appends while the drain runs wait for the next call. A drain that chased the
410    /// live cursor could run for as long as the writer kept up, starving whatever
411    /// the caller does between drains (decoding, emitting, checking for shutdown).
412    /// Allocation-free; a frame is never longer than
413    /// [`MAX_FRAME_LEN`].
414    pub fn drain(&mut self, mut on_frame: impl FnMut(&[u8])) -> Result<usize, RingError> {
415        let mut desc = self.checked_snapshot()?;
416        let mut buf = [0u8; MAX_FRAME_LEN];
417        let mut frames = 0;
418        loop {
419            // SAFETY: as in `try_recv`. `desc` keeps the snapshot's `write_ptr`, so
420            // the loop stops at the frames that existed when it began; its
421            // `read_ptr` is this reader's own, advanced by each read.
422            match unsafe { desc.try_read_frame(base(&self.region), &mut buf) } {
423                ReadOutcome::Empty => return Ok(frames),
424                ReadOutcome::Frame(len) => {
425                    frames += 1;
426                    on_frame(&buf[..len]);
427                }
428                // A `MAX_FRAME_LEN` buffer holds every frame a writer can produce.
429                ReadOutcome::TooSmall { needed } => {
430                    return Err(RingError::BufferTooSmall {
431                        len: buf.len(),
432                        needed,
433                    });
434                }
435            }
436        }
437    }
438}
439
440impl<R: Region> TransportError for RingReader<R> {
441    type Error = RingError;
442}
443
444/// Copies the next frame into `buf` and consumes it.
445///
446/// [`TryError::WouldBlock`] means the ring is empty. A frame longer than `buf` is
447/// [`RingError::BufferTooSmall`] and stays unread, so the caller can retry with a
448/// buffer of [`max_recv_size`](TryRecvTransport::max_recv_size) bytes, which
449/// always fits.
450impl<R: Region> TryRecvTransport for RingReader<R> {
451    fn try_recv(&mut self, buf: &mut [u8]) -> Result<usize, TryError<RingError>> {
452        let mut desc = self.checked_snapshot()?;
453        // SAFETY: `checked_snapshot` confirmed `capacity` fits the region and
454        // `read_ptr` lies in the data area; `try_read_frame` refuses any packet that
455        // would run past `capacity`.
456        match unsafe { desc.try_read_frame(base(&self.region), buf) } {
457            ReadOutcome::Empty => Err(TryError::WouldBlock),
458            ReadOutcome::Frame(len) => Ok(len),
459            ReadOutcome::TooSmall { needed } => Err(TryError::Fail(RingError::BufferTooSmall {
460                len: buf.len(),
461                needed,
462            })),
463        }
464    }
465
466    fn max_recv_size(&self) -> usize {
467        MAX_FRAME_LEN
468    }
469}