Skip to main content

dsfb_rf/
zero_copy.rs

1//! Zero-copy residual source trait for DMA buffer integration.
2//!
3//! ## Motivation
4//!
5//! On embedded RF platforms (Zynq UltraScale+ RFSoC, USRP E310), the IQ
6//! sample stream arrives in DMA buffers mapped into the processor's address
7//! space. Copying these samples into an intermediate buffer adds latency
8//! and CPU overhead that is unacceptable in high-throughput pipelines.
9//!
10//! The `ResidualSource` trait allows the DSFB engine to tap directly into
11//! a DMA buffer — or any other memory-mapped IQ source — without copying.
12//! The engine reads residual norms from the source via an immutable borrow,
13//! maintaining the non-intrusion contract (no write path, no mutation of
14//! upstream data).
15//!
16//! ## Non-Intrusion Guarantee
17//!
18//! The trait requires only `&self` access to the source. The engine never
19//! takes a mutable reference to the DMA buffer. This is enforced at the
20//! type level: `ResidualSource::residual_norms()` returns `&[f32]`, an
21//! immutable slice. The Rust borrow checker prevents any write path from
22//! being introduced without a compilation error.
23//!
24//! ## Design
25//!
26//! - `no_std`, `no_alloc`, zero `unsafe`
27//! - Trait-based: implementors provide platform-specific DMA access
28//! - Engine accepts `&dyn ResidualSource` or `&impl ResidualSource`
29//! - Compatible with `volatile` memory-mapped I/O via safe wrappers
30
31/// A source of residual norm observations.
32///
33/// Implementors provide access to a contiguous slice of f32 residual norms.
34/// This is the zero-copy interface between the platform's IQ data path
35/// and the DSFB engine.
36///
37/// ## Example: DMA Buffer Source
38///
39/// ```rust,ignore
40/// struct DmaResidualSource {
41///     buffer: &'static [f32],  // memory-mapped DMA region
42///     len: usize,
43/// }
44///
45/// impl ResidualSource for DmaResidualSource {
46///     fn residual_norms(&self) -> &[f32] {
47///         &self.buffer[..self.len]
48///     }
49///     fn snr_estimate_db(&self) -> f32 { 15.0 }
50///     fn sample_count(&self) -> usize { self.len }
51/// }
52/// ```
53pub trait ResidualSource {
54    /// Borrow the current residual norm buffer as an immutable slice.
55    ///
56    /// This is the zero-copy tap point. The DSFB engine reads from this
57    /// slice without copying. The source retains ownership.
58    fn residual_norms(&self) -> &[f32];
59
60    /// Current SNR estimate in dB. Return `f32::NAN` if unknown.
61    fn snr_estimate_db(&self) -> f32;
62
63    /// Number of valid samples in the current buffer.
64    fn sample_count(&self) -> usize;
65}
66
67/// A simple owned-slice residual source for testing and host-side pipelines.
68///
69/// Wraps a `&[f32]` slice as a `ResidualSource`. Zero-copy: no allocation,
70/// no copying — just a reference wrapper.
71pub struct SliceSource<'a> {
72    norms: &'a [f32],
73    snr_db: f32,
74}
75
76impl<'a> SliceSource<'a> {
77    /// Wrap an existing slice as a residual source.
78    #[inline]
79    pub const fn new(norms: &'a [f32], snr_db: f32) -> Self {
80        Self { norms, snr_db }
81    }
82}
83
84impl<'a> ResidualSource for SliceSource<'a> {
85    #[inline]
86    fn residual_norms(&self) -> &[f32] { self.norms }
87    #[inline]
88    fn snr_estimate_db(&self) -> f32 { self.snr_db }
89    #[inline]
90    fn sample_count(&self) -> usize { self.norms.len() }
91}
92
93/// A fixed-capacity ring buffer residual source for embedded/bare-metal.
94///
95/// Accepts streamed residual norms one at a time and exposes the most
96/// recent N observations as a contiguous slice. All storage is stack-allocated.
97pub struct RingSource<const N: usize> {
98    buffer: [f32; N],
99    head: usize,
100    count: usize,
101    snr_db: f32,
102}
103
104impl<const N: usize> RingSource<N> {
105    /// Create a new ring source.
106    pub const fn new(snr_db: f32) -> Self {
107        Self {
108            buffer: [0.0; N],
109            head: 0,
110            count: 0,
111            snr_db,
112        }
113    }
114
115    /// Push a new residual norm into the ring buffer.
116    pub fn push(&mut self, norm: f32) {
117        self.buffer[self.head] = norm;
118        self.head = (self.head + 1) % N;
119        if self.count < N { self.count += 1; }
120    }
121
122    /// Update the SNR estimate.
123    pub fn set_snr_db(&mut self, snr_db: f32) {
124        self.snr_db = snr_db;
125    }
126}
127
128impl<const N: usize> ResidualSource for RingSource<N> {
129    fn residual_norms(&self) -> &[f32] {
130        // Return the filled portion of the buffer.
131        // Note: for a ring buffer, the "most recent N" are not necessarily
132        // contiguous from index 0. We return the full filled buffer — the
133        // engine processes observations sequentially via observe().
134        &self.buffer[..self.count.min(N)]
135    }
136
137    fn snr_estimate_db(&self) -> f32 { self.snr_db }
138    fn sample_count(&self) -> usize { self.count.min(N) }
139}
140
141// ── Tests ──────────────────────────────────────────────────────────────────
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn slice_source_returns_original_data() {
149        let data = [0.1_f32, 0.2, 0.3, 0.4];
150        let src = SliceSource::new(&data, 15.0);
151        assert_eq!(src.residual_norms(), &data);
152        assert_eq!(src.snr_estimate_db(), 15.0);
153        assert_eq!(src.sample_count(), 4);
154    }
155
156    #[test]
157    fn ring_source_accumulates() {
158        let mut ring = RingSource::<4>::new(10.0);
159        ring.push(0.1);
160        ring.push(0.2);
161        assert_eq!(ring.sample_count(), 2);
162        ring.push(0.3);
163        ring.push(0.4);
164        ring.push(0.5); // overwrites oldest
165        assert_eq!(ring.sample_count(), 4);
166    }
167
168    #[test]
169    fn ring_source_is_residual_source() {
170        let mut ring = RingSource::<8>::new(20.0);
171        for i in 0..5 {
172            ring.push(i as f32 * 0.1);
173        }
174        let src: &dyn ResidualSource = &ring;
175        assert_eq!(src.sample_count(), 5);
176        assert_eq!(src.snr_estimate_db(), 20.0);
177        assert_eq!(src.residual_norms().len(), 5);
178    }
179
180    #[test]
181    fn zero_copy_no_allocation() {
182        // This test verifies the zero-copy property: the SliceSource
183        // wraps an existing slice without any allocation or copying.
184        let original = [1.0_f32, 2.0, 3.0];
185        let src = SliceSource::new(&original, 0.0);
186        let borrowed = src.residual_norms();
187        // The pointers should be identical (same memory)
188        assert_eq!(borrowed.as_ptr(), original.as_ptr());
189    }
190}