consortium-dbg 0.6.0

defmt-over-shared-memory logging and panic handling for Consortium
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! Shared memory defmt descriptor definitions.
//!
//! This module defines the `CDBG` descriptor: a fixed-size header placed at the
//! start of a shared-memory region, immediately followed by a circular buffer of
//! defmt log frames. The MCU (firmware) side is the *writer* and the application
//! (Linux/host) side is the *reader*; the two communicate purely through the
//! descriptor fields and the ring data, with no locks.
//!
//! Ownership of each field is split so that, in steady state, each side only
//! mutates fields it owns: the writer owns `write_ptr`, `capacity`, and `flags`;
//! the reader owns `read_ptr`. `write_ptr == read_ptr` means the ring is empty.
//!
//! ```text
//! Offset  Size  Owner   Field
//! 0x00    6     N/A     header (refer to `DescriptorHeader`)
//! 0x06    2     mcu     write_ptr
//! 0x08    2     app     read_ptr
//! 0x0A    2     mcu     capacity
//! 0x0C    2     mcu     flags (error flags, etc.)
//! 0x0E    2     n/a     reserved
//! 0x10    vary  mcu     data (circular buffer of log data)
//! ```

use bitflags::bitflags;
use consortium_core::descriptor::{DescriptorHeader, DescriptorMagic};

use crate::packet::{PACKET_HEADER_SIZE, Packet};
use crate::utils::{rv_u16, wv_u16};

/// In-memory mirror of the shared `CDBG` descriptor.
///
/// `#[repr(C)]` keeps the field order matching the on-wire layout documented in
/// the module header, but reads/writes still go through the explicit offset
/// constants below (via volatile helpers) rather than relying on struct layout.
#[repr(C)]
pub struct Descriptor {
    /// Magic + version stamp shared by all Consortium descriptors.
    pub header: DescriptorHeader,
    /// Writer-owned cursor: byte offset where the next frame will be written.
    pub write_ptr: u16,
    /// Reader-owned cursor: byte offset of the next frame to be read.
    /// `read_ptr == write_ptr` means the ring is empty.
    pub read_ptr: u16,
    /// Total byte length of the shared region (header + data). Acts as the
    /// wrap-around bound for the circular buffer; usable data is
    /// `[DATA_START, capacity)`.
    pub capacity: u16,
    /// Writer-owned status flags (e.g. overflow).
    pub flags: Flags,
    _reserved1: [u8; 2],
}

bitflags! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub struct Flags: u16 {
        /// Writer is mid-write; readers should not consume the in-flight frame.
        const Writing = 0b0000_0000_0000_0001;
        /// A non-recoverable error occurred on the writer side.
        const Error = 0b0000_0000_0001_0000;
        /// The last frame was dropped because it did not fit without
        /// overwriting unread data.
        const Overflow = 0b0000_0000_0010_0000;
        /// The writer panicked; the stream ended abnormally and the frames up to
        /// this point are the pre-panic log, followed by the panic message.
        const Panic = 0b0000_0000_0100_0000;
    }
}

/// Identifies a Consortium defmt debug descriptor.
pub const MAGIC: DescriptorMagic = DescriptorMagic::new(b"CDBG");
/// On-wire layout version; bump when the descriptor/packet format changes.
pub const VERSION: u8 = 0;

// Byte offsets of each field within the shared region. All field access goes
// through these constants so the volatile reads/writes stay layout-agnostic.
pub const WRITE_PTR_OFFSET: usize = 0x06;
pub const READ_PTR_OFFSET: usize = 0x08;
pub const CAPACITY_OFFSET: usize = 0x0A;
pub const FLAGS_OFFSET: usize = 0x0C;

/// Size of the descriptor header in bytes.
pub const DESCRIPTOR_SIZE: usize = core::mem::size_of::<Descriptor>();
/// First byte usable for ring data, i.e. just past the descriptor header.
/// Both cursors wrap back to this offset rather than to 0.
const DATA_START: u16 = DESCRIPTOR_SIZE as u16;
/// Largest payload one frame carries: the packet `len` field is a `u8`.
pub const MAX_FRAME_LEN: usize = u8::MAX as usize;

/// What [`Descriptor::try_read_frame`] found.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReadOutcome {
    /// Nothing unread in the ring.
    Empty,
    /// One frame of this many payload bytes was copied out and consumed.
    Frame(usize),
    /// The next frame's payload is `needed` bytes, more than the buffer holds; it
    /// was left unread.
    TooSmall {
        /// The frame's payload length.
        needed: usize,
    },
}

impl Descriptor {
    /// Builds a fresh descriptor for a region of `capacity` total bytes, with
    /// both cursors parked at `DATA_START` (empty ring) and no flags set.
    pub const fn new(capacity: u16) -> Self {
        Self {
            header: DescriptorHeader::new(MAGIC, VERSION),
            write_ptr: DATA_START,
            read_ptr: DATA_START,
            capacity,
            flags: Flags::empty(),
            _reserved1: [0; 2],
        }
    }

    /// Returns `true` if the descriptor header has the expected magic and version.
    pub fn check(&self) -> bool {
        self.header.check(MAGIC, VERSION).is_ok()
    }

    /// Reads a `Descriptor` from shared memory at `ptr`.
    ///
    /// # Safety
    /// `ptr` must point to a valid, readable shared-memory region of at least
    /// `DESCRIPTOR_SIZE` bytes with consistent volatile layout.
    pub unsafe fn from_raw_ptr(ptr: *const u8) -> Self {
        // SAFETY: caller guarantees the descriptor header is present at `ptr`.
        let header = unsafe { DescriptorHeader::from_raw_ptr(ptr) };
        // SAFETY: caller guarantees the descriptor fields are valid and coherent.
        let write_ptr = unsafe { rv_u16(ptr, WRITE_PTR_OFFSET) };
        // SAFETY: caller guarantees the descriptor fields are valid and coherent.
        let read_ptr = unsafe { rv_u16(ptr, READ_PTR_OFFSET) };
        // SAFETY: caller guarantees the descriptor fields are valid and coherent.
        let capacity = unsafe { rv_u16(ptr, CAPACITY_OFFSET) };
        // SAFETY: caller guarantees the descriptor fields are valid and coherent.
        let flags = Flags::from_bits_retain(unsafe { rv_u16(ptr, FLAGS_OFFSET) });
        Self {
            header,
            write_ptr,
            read_ptr,
            capacity,
            flags,
            _reserved1: [0; 2],
        }
    }

    /// Writes this descriptor to shared memory at `ptr`.
    ///
    /// # Safety
    /// `ptr` must point to a valid, writable shared-memory region of at least
    /// `DESCRIPTOR_SIZE` bytes.
    pub unsafe fn write_to_raw_ptr(&self, ptr: *mut u8) {
        // SAFETY: caller guarantees `ptr` covers the descriptor header.
        unsafe { DescriptorHeader::new(MAGIC, VERSION).write_to_raw_ptr(ptr) };
        // SAFETY: caller guarantees the descriptor fields are valid and coherent.
        unsafe { wv_u16(ptr, WRITE_PTR_OFFSET, self.write_ptr) };
        // SAFETY: caller guarantees the descriptor fields are valid and coherent.
        unsafe { wv_u16(ptr, READ_PTR_OFFSET, self.read_ptr) };
        // SAFETY: caller guarantees the descriptor fields are valid and coherent.
        unsafe { wv_u16(ptr, CAPACITY_OFFSET, self.capacity) };
        // SAFETY: caller guarantees the descriptor fields are valid and coherent.
        unsafe { wv_u16(ptr, FLAGS_OFFSET, self.flags.bits()) };
    }

    /// Copies the next available frame's payload into `buf` and consumes it.
    ///
    /// The one read path: [`read_one_frame`](Self::read_one_frame), the host reader,
    /// and [`RingReader`](crate::ring::RingReader) all go through it. It allocates
    /// nothing, so it serves `no_std` readers too.
    ///
    /// A frame whose payload is longer than `buf` is left in the ring, unconsumed,
    /// and reported as [`ReadOutcome::TooSmall`] so the caller can retry with a
    /// larger buffer; one of [`MAX_FRAME_LEN`] bytes always fits.
    ///
    /// # Safety
    /// `base` must point to the start of the shared-memory region owning this descriptor,
    /// valid for at least `self.capacity` bytes.
    pub unsafe fn try_read_frame(&mut self, base: *mut u8, buf: &mut [u8]) -> ReadOutcome {
        // The writer wraps early when a frame does not fit in the tail before
        // `capacity`, jumping `write_ptr` back to `DATA_START` and leaving a gap
        // the reader cannot predict from packet lengths alone. When the reader's
        // linear advance lands in that gap it finds no fresh packet; it must then
        // follow the writer's wrap to `DATA_START` rather than stall there. At
        // most one wrap can occur per call, so a single retry suffices.
        for _ in 0..2 {
            if self.read_ptr == self.write_ptr {
                return ReadOutcome::Empty;
            }
            // SAFETY: caller guarantees the ring buffer covers `self.read_ptr`.
            let packet_ptr = unsafe { base.add(self.read_ptr as usize) };
            // SAFETY: `packet_ptr` points into the shared ring buffer.
            let frame_header = unsafe { Packet::try_from_raw_ptr(packet_ptr) };

            // A packet that would run past `capacity` was never written by a writer
            // (it wraps first), so it is stale or corrupt: treat it like a bad header
            // rather than read past the ring.
            let in_ring = |header: &Packet| {
                self.read_ptr as usize + PACKET_HEADER_SIZE + header.len as usize
                    <= self.capacity as usize
            };
            let mut frame_header = match frame_header {
                Some(header) if header.is_valid() && in_ring(&header) => header,
                // No fresh frame here. If we are already at `DATA_START` the ring
                // is genuinely drained (a partially written or stale head); stop.
                // Otherwise the writer wrapped early — follow it and retry once.
                _ => {
                    if self.read_ptr == DATA_START {
                        return ReadOutcome::Empty;
                    }
                    self.read_ptr = DATA_START;
                    // SAFETY: caller guarantees the descriptor fields are valid and coherent.
                    unsafe { wv_u16(base, READ_PTR_OFFSET, self.read_ptr) };
                    continue;
                }
            };

            let len = frame_header.len as usize;
            let Some(payload) = buf.get_mut(..len) else {
                return ReadOutcome::TooSmall { needed: len };
            };
            // SAFETY: `packet_ptr` points to the valid frame described by `frame_header`.
            unsafe { frame_header.read_payload(packet_ptr, payload) };
            // SAFETY: `packet_ptr` points to the valid frame described by `frame_header`.
            unsafe { frame_header.mark_as_consumed(packet_ptr) };
            // SAFETY: `base` points to the descriptor-owned ring buffer.
            self.read_ptr = unsafe { self.next_read_ptr(base) };
            // SAFETY: caller guarantees the descriptor fields are valid and coherent.
            unsafe { wv_u16(base, READ_PTR_OFFSET, self.read_ptr) };
            return ReadOutcome::Frame(len);
        }
        ReadOutcome::Empty
    }

    /// Reads the next available defmt frame from the circular buffer, returning its payload.
    ///
    /// Returns `None` if no frame is available or if the current frame header is invalid.
    ///
    /// # Safety
    /// `base` must point to the start of the shared-memory region owning this descriptor,
    /// valid for at least `self.capacity` bytes.
    #[cfg(feature = "host")]
    pub unsafe fn read_one_frame(&mut self, base: *mut u8) -> Option<Vec<u8>> {
        let mut buf = [0u8; MAX_FRAME_LEN];
        // SAFETY: forwarded to the caller.
        match unsafe { self.try_read_frame(base, &mut buf) } {
            ReadOutcome::Frame(len) => Some(buf[..len].to_vec()),
            // A `MAX_FRAME_LEN` buffer holds every frame, so `TooSmall` cannot occur.
            ReadOutcome::Empty | ReadOutcome::TooSmall { .. } => None,
        }
    }

    /// Drains all available defmt frames from the circular buffer, returning them as a `Vec`.
    ///
    /// # Safety
    /// Same requirements as [`read_one_frame`]: `base` must be valid for at least
    /// `self.capacity` bytes.
    ///
    /// [`read_one_frame`]: Self::read_one_frame
    #[cfg(feature = "host")]
    pub unsafe fn read_bulk_frames(&mut self, base: *mut u8) -> Vec<Vec<u8>> {
        let mut frames = Vec::new();
        // SAFETY: caller guarantees `base` points to this descriptor`s ring buffer.
        while let Some(frame) = unsafe { self.read_one_frame(base) } {
            frames.push(frame);
        }
        frames
    }

    /// Writes a single defmt frame into the circular buffer.
    ///
    /// Returns `true` on success; returns `false` and sets the `Overflow` flag if the
    /// frame does not fit without overwriting unread data.
    ///
    /// # Safety
    /// `base` must point to the start of the shared-memory region owning this descriptor,
    /// valid for at least `self.capacity` bytes.
    pub unsafe fn write_one_frame(&mut self, base: *mut u8, frame: &[u8]) -> bool {
        // SAFETY: caller guarantees `base` points to this descriptor`s ring buffer.
        match unsafe { self.allocate_write_ptr(base, frame.len()) } {
            Some(p) => {
                // SAFETY: `p` was allocated within the ring buffer.
                let packet_ptr = unsafe { base.add(p as usize) };
                // SAFETY: `packet_ptr` points to enough space for the full packet.
                unsafe { Packet::write_full_packet_to_raw_ptr(packet_ptr, frame) };
                self.flags.remove(Flags::Overflow);
                self.write_ptr = self.advance_ptr(p, frame.len());
                // SAFETY: caller guarantees the descriptor fields are valid and coherent.
                unsafe { wv_u16(base, WRITE_PTR_OFFSET, self.write_ptr) };
                // SAFETY: caller guarantees the descriptor fields are valid and coherent.
                unsafe { wv_u16(base, FLAGS_OFFSET, self.flags.bits()) };
                true
            }
            None => {
                self.flags.insert(Flags::Overflow);
                // SAFETY: caller guarantees the descriptor fields are valid and coherent.
                unsafe { wv_u16(base, FLAGS_OFFSET, self.flags.bits()) };
                false
            }
        }
    }

    /// Computes the `read_ptr` value after consuming the frame at the current
    /// `read_ptr`, wrapping back to `DATA_START` once it reaches `capacity`.
    ///
    /// If the current slot does not hold a valid packet header, the cursor is
    /// reset to the start of the data area.
    ///
    /// # Safety
    /// `base` must point to this descriptor's ring buffer, valid for at least
    /// `self.capacity` bytes.
    // Read-path helper shared by every reader (`try_read_frame`) and the logger's
    // own drain path.
    pub(crate) unsafe fn next_read_ptr(&mut self, base: *const u8) -> u16 {
        // SAFETY: caller guarantees the ring buffer covers `self.read_ptr`.
        let cursor = unsafe { base.add(self.read_ptr as usize) };
        // SAFETY: `cursor` points into the shared ring buffer.
        let Some(frame_header) = (unsafe { Packet::try_from_raw_ptr(cursor) }) else {
            return DESCRIPTOR_SIZE as u16;
        };
        let next_padding = frame_header.len as usize + PACKET_HEADER_SIZE;
        let next_ptr = self.read_ptr.wrapping_add(next_padding as u16);
        if next_ptr >= self.capacity {
            DATA_START
        } else {
            next_ptr
        }
    }

    /// Reserves a write position for a packet of `frame_len` payload bytes.
    ///
    /// Returns the offset to write at, or `None` if the frame is rejected:
    /// the payload exceeds the per-frame limit (`u8::MAX`), the packet cannot
    /// fit in the data area at all, or wrapping would overwrite an unread frame
    /// (back-pressure). When the remaining tail is too small, the write wraps to
    /// `DATA_START`.
    ///
    /// # Safety
    /// `base` must point to this descriptor's ring buffer, valid for at least
    /// `self.capacity` bytes.
    pub(crate) unsafe fn allocate_write_ptr(
        &mut self,
        base: *mut u8,
        frame_len: usize,
    ) -> Option<u16> {
        // Packet length field is a single byte, so payloads larger than u8::MAX
        // cannot be represented.
        if frame_len > u8::MAX as usize {
            return None;
        }

        let packet_len = PACKET_HEADER_SIZE + frame_len;
        // Reject frames that can never fit, even in an empty ring.
        if packet_len > self.capacity as usize - DATA_START as usize {
            return None;
        }

        let mut ptr = self.write_ptr;
        // Not enough room before the end of the region: wrap to the data start.
        if ptr as usize + packet_len > self.capacity as usize {
            ptr = DATA_START;
        }

        // SAFETY: `ptr` has been wrapped into the ring buffer range.
        let cursor = unsafe { base.add(ptr as usize) };
        // SAFETY: `cursor` points into the shared ring buffer.
        // A still-valid packet at the target slot means the reader has not yet
        // consumed it; refuse to clobber it.
        if unsafe { Packet::try_from_raw_ptr(cursor) }.is_some_and(|packet| packet.is_valid()) {
            None
        } else {
            Some(ptr)
        }
    }

    /// Returns the `write_ptr` value after a packet of `frame_len` payload bytes
    /// is written at `ptr`, wrapping to `DATA_START` at the end of the region.
    fn advance_ptr(&self, ptr: u16, frame_len: usize) -> u16 {
        let next_ptr = ptr.wrapping_add((PACKET_HEADER_SIZE + frame_len) as u16);
        if next_ptr >= self.capacity {
            DATA_START
        } else {
            next_ptr
        }
    }

    /// Block until host has read all pending data. (defmt's Logger trait)
    ///
    /// Spins on each pending frame until the reader marks it consumed
    /// (invalidates its header), advancing `read_ptr` until the ring drains.
    ///
    /// # Safety
    /// `base` must point to this descriptor's ring buffer, valid for at least
    /// `self.capacity` bytes.
    #[cfg(feature = "logger")]
    pub(crate) unsafe fn flush(&mut self, base: *mut u8) {
        // SAFETY: caller guarantees the ring buffer covers `self.read_ptr`.
        let cursor = unsafe { base.add(self.read_ptr as usize) };
        // SAFETY: `cursor` points into the shared ring buffer.
        let Some(mut frame_header) = (unsafe { Packet::try_from_raw_ptr(cursor) }) else {
            return;
        };
        while self.read_ptr != self.write_ptr && self.read_ptr != DESCRIPTOR_SIZE as u16 {
            while frame_header.is_valid() {
                core::hint::spin_loop();
            }
            // SAFETY: `base` points to the descriptor-owned ring buffer.
            self.read_ptr = unsafe { self.next_read_ptr(base) };
            // SAFETY: caller guarantees the ring buffer covers `self.read_ptr`.
            let cursor = unsafe { base.add(self.read_ptr as usize) };
            // SAFETY: `cursor` points into the shared ring buffer.
            match unsafe { Packet::try_from_raw_ptr(cursor) } {
                Some(header) => frame_header = header,
                None => return,
            }
        }
    }
}