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}