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(®ion)?;
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(®ion);
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(®ion)?.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(®ion)?;
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}