Skip to main content

consortium_dbg/
host.rs

1//! A-core shared-memory reader for the CDBG debug ring.
2//!
3//! The caller (A-core runtime or board integration) maps the MCU's debug SRAM region
4//! and passes the base pointer and length to [`HostRing::from_raw_parts`].  This module
5//! does not perform any mapping itself.
6//!
7//! [`HostRing`] is the allocating convenience over [`RingReader`]: it validates the
8//! ring the same way and takes frames through the same
9//! [`try_recv`](TryRecvTransport::try_recv), returning each as a `Vec`.
10#![cfg(feature = "host")]
11
12use consortium_core::{TryError, TryRecvTransport, UncachedRegion};
13
14use crate::{
15    descriptor::{Descriptor, MAX_FRAME_LEN},
16    ring::{RingError, RingReader},
17};
18
19/// Errors produced by the A-core host reader.
20#[derive(thiserror::Error, Debug)]
21pub enum HostError {
22    /// CDBG magic bytes did not match the expected `b"CDBG"` value.
23    #[error("CDBG magic mismatch")]
24    MagicMismatch,
25    /// CDBG version field did not match the expected version.
26    #[error("CDBG version mismatch")]
27    VersionMismatch,
28    /// Descriptor `capacity` field is incompatible with the mapped region length.
29    #[error("descriptor capacity {descriptor} exceeds mapped length {mapped}")]
30    CapacityMismatch {
31        /// Capacity recorded in the descriptor (0 when the region is too small to hold a header).
32        descriptor: u16,
33        /// Number of bytes that were actually mapped.
34        mapped: usize,
35    },
36    /// A ring pointer lies outside the valid `[DATA_START, capacity)` range.
37    #[error("ring pointer {ptr} is out of bounds (capacity {capacity})")]
38    PointerOutOfBounds {
39        /// The offending pointer value.
40        ptr: u16,
41        /// The descriptor's stated capacity.
42        capacity: u16,
43    },
44    /// ELF bytes could not be parsed or contained no defmt table.
45    #[cfg(feature = "decode")]
46    #[error("ELF parsing failed or missing defmt table")]
47    ElfParse,
48    /// defmt stream decoding failed with a non-recoverable error.
49    #[cfg(feature = "decode")]
50    #[error("defmt decode error: {0:?}")]
51    Decode(defmt_decoder::DecodeError),
52    /// The ring became inconsistent after it was opened (a cursor or capacity the
53    /// writer left out of range).
54    #[error("{0}")]
55    Ring(RingError),
56}
57
58impl From<RingError> for HostError {
59    fn from(error: RingError) -> Self {
60        match error {
61            RingError::MagicMismatch => Self::MagicMismatch,
62            RingError::VersionMismatch => Self::VersionMismatch,
63            RingError::CapacityMismatch { descriptor, mapped } => {
64                Self::CapacityMismatch { descriptor, mapped }
65            }
66            RingError::PointerOutOfBounds { ptr, capacity } => {
67                Self::PointerOutOfBounds { ptr, capacity }
68            }
69            other => Self::Ring(other),
70        }
71    }
72}
73
74/// A-core reader over an already-mapped CDBG debug ring.
75///
76/// Only the A-core process modifies `read_ptr`; the MCU only writes `write_ptr` and
77/// packet data. The mapping is treated as uncached, as UIO maps it.
78pub struct HostRing {
79    reader: RingReader<UncachedRegion>,
80}
81
82impl HostRing {
83    /// Wraps an already-mapped CDBG debug region.
84    ///
85    /// Reads the descriptor at `base` and validates the CDBG magic, version, capacity
86    /// against `len`, and both ring pointers before returning.
87    ///
88    /// # Safety
89    /// - `base` must be valid for reads and writes for at least `len` bytes.
90    /// - No other code may write to `read_ptr` (descriptor offset `0x08`) for the
91    ///   lifetime of the returned `HostRing`.
92    pub unsafe fn from_raw_parts(base: *mut u8, len: usize) -> Result<Self, HostError> {
93        consortium_log::trace!(
94            "HostRing::from_raw_parts(base = {:#x}, len = {:#x})",
95            base as usize,
96            len
97        );
98        // SAFETY: the caller guarantees `base` is valid for reads and writes for `len`
99        // bytes while the ring lives.
100        let region = unsafe { UncachedRegion::new(base, len) };
101        // SAFETY: the caller guarantees no other code writes `read_ptr`, so this is
102        // the ring's only reader.
103        let reader = unsafe { RingReader::new(region) }?;
104        let desc = reader.snapshot();
105        consortium_log::trace!(
106            "HostRing::from_raw_parts: descriptor capacity = {:#x}, read_ptr = {:#x}, write_ptr = {:#x}",
107            desc.capacity,
108            desc.read_ptr,
109            desc.write_ptr
110        );
111        Ok(Self { reader })
112    }
113
114    /// Returns the number of bytes in the mapped region supplied at construction time.
115    pub fn mapped_len(&self) -> usize {
116        use consortium_core::Region;
117        self.reader.region().len()
118    }
119
120    /// Returns a point-in-time snapshot of the descriptor fields read from shared memory.
121    pub fn snapshot(&self) -> Descriptor {
122        self.reader.snapshot()
123    }
124
125    /// Returns the next available packet payload, or `Ok(None)` when the ring is empty.
126    pub fn read_packet(&mut self) -> Result<Option<Vec<u8>>, HostError> {
127        let mut buf = [0u8; MAX_FRAME_LEN];
128        match self.reader.try_recv(&mut buf) {
129            Ok(len) => Ok(Some(buf[..len].to_vec())),
130            Err(TryError::WouldBlock) => Ok(None),
131            Err(TryError::Fail(error)) => Err(error.into()),
132        }
133    }
134
135    /// Drains the packets that were in the ring when the call began, returning them
136    /// in arrival order.
137    ///
138    /// Bounded by one snapshot of the writer's cursor ([`RingReader::drain`]):
139    /// packets the firmware appends meanwhile are left for the next call, so a
140    /// writer that keeps up with the reader cannot hold the caller here.
141    pub fn drain_packets(&mut self) -> Result<Vec<Vec<u8>>, HostError> {
142        let mut packets = Vec::new();
143        self.reader.drain(|packet| packets.push(packet.to_vec()))?;
144        Ok(packets)
145    }
146}
147
148/// Parsed defmt table extracted from a firmware ELF file.
149///
150/// Constructed once per firmware image; cheap to share across [`DefmtSession`]s.
151#[cfg(feature = "decode")]
152pub struct DefmtContext {
153    table: defmt_decoder::Table,
154}
155
156#[cfg(feature = "decode")]
157impl DefmtContext {
158    /// Parses the defmt symbol table from firmware ELF bytes.
159    ///
160    /// Returns `Err(HostError::ElfParse)` when the bytes are not a valid ELF file or
161    /// when the ELF contains no `.defmt` section.
162    pub fn from_elf_bytes(bytes: &[u8]) -> Result<Self, HostError> {
163        consortium_log::trace!("parsing defmt table from ELF bytes ({} bytes)", bytes.len());
164        let table = defmt_decoder::Table::parse(bytes)
165            .map_err(|e| {
166                consortium_log::error!("defmt table parse failed: {:?}", e);
167                HostError::ElfParse
168            })?
169            .ok_or_else(|| {
170                consortium_log::error!("defmt table missing from ELF");
171                HostError::ElfParse
172            })?;
173        consortium_log::info!(
174            "defmt table parsed: encoding = {:?}, has_timestamp = {}",
175            table.encoding(),
176            table.has_timestamp()
177        );
178        Ok(Self { table })
179    }
180}
181
182/// Stateful defmt stream decoder tied to a [`DefmtContext`].
183///
184/// Feed raw packet payloads from a [`HostRing`] into [`push_packet`]; the session
185/// accumulates bytes across calls and invokes the callback for each complete frame.
186///
187/// The lifetime `'ctx` is that of the backing [`DefmtContext`]; the session must not
188/// outlive it.
189///
190/// [`push_packet`]: DefmtSession::push_packet
191#[cfg(feature = "decode")]
192pub struct DefmtSession<'ctx> {
193    decoder: Box<dyn defmt_decoder::StreamDecoder + 'ctx>,
194}
195
196#[cfg(feature = "decode")]
197impl<'ctx> DefmtSession<'ctx> {
198    /// Creates a new decoding session backed by `ctx`.
199    pub fn new(ctx: &'ctx DefmtContext) -> Self {
200        Self {
201            decoder: ctx.table.new_stream_decoder(),
202        }
203    }
204
205    /// Feeds `bytes` into the stream decoder and invokes `on_frame` for each complete frame.
206    ///
207    /// A packet payload may contain a partial defmt frame; `UnexpectedEof` from the
208    /// decoder is treated as "need more bytes" and returns `Ok(())`.  Any other decode
209    /// error is returned as `Err(HostError::Decode(...))`.
210    pub fn push_packet(
211        &mut self,
212        bytes: &[u8],
213        mut on_frame: impl FnMut(&defmt_decoder::Frame<'_>),
214    ) -> Result<(), HostError> {
215        self.decoder.received(bytes);
216        loop {
217            match self.decoder.decode() {
218                Ok(frame) => on_frame(&frame),
219                Err(defmt_decoder::DecodeError::UnexpectedEof) => return Ok(()),
220                Err(e) => return Err(HostError::Decode(e)),
221            }
222        }
223    }
224}