stabilizer/hardware/
adc.rs

1//! Stabilizer ADC management interface
2//!
3//! # Design
4//!
5//! Stabilizer ADCs are connected to the MCU via a simplex, SPI-compatible interface. The ADCs
6//! require a setup conversion time after asserting the CSn (convert) signal to generate the ADC
7//! code from the sampled level. Once the setup time has elapsed, the ADC data is clocked out of
8//! MISO. The internal setup time is managed by the SPI peripheral via a CSn setup time parameter
9//! during SPI configuration, which allows offloading the management of the setup time to hardware.
10//!
11//! Because of the SPI-compatibility of the ADCs, a single SPI peripheral + DMA is used to automate
12//! the collection of multiple ADC samples without requiring processing by the CPU, which reduces
13//! overhead and provides the CPU with more time for processing-intensive tasks, like DSP.
14//!
15//! The automation of sample collection utilizes three DMA streams, the SPI peripheral, and two
16//! timer compare channel for each ADC. One timer comparison channel is configured to generate a
17//! comparison event every time the timer is equal to a specific value. Each comparison then
18//! generates a DMA transfer event to write into the SPI CR1 register to initiate the transfer.
19//! This allows the SPI interface to periodically read a single sample. The other timer comparison
20//! channel is configured to generate a comparison event slightly before the first (~10 timer
21//! cycles). This channel triggers a separate DMA stream to clear the EOT flag within the SPI
22//! peripheral. The EOT flag must be cleared after each transfer or the SPI peripheral will not
23//! properly complete the single conversion. Thus, by using two DMA streams and timer comparison
24//! channels, the SPI can regularly acquire ADC samples.
25//!
26//! In order to collect the acquired ADC samples into a RAM buffer, a final DMA transfer is
27//! configured to read from the SPI RX FIFO into RAM. The request for this transfer is connected to
28//! the SPI RX data signal, so the SPI peripheral will request to move data into RAM whenever it is
29//! available. When enough samples have been collected, a transfer-complete interrupt is generated
30//! and the ADC samples are available for processing.
31//!
32//! After a complete transfer of a batch of samples, the inactive buffer is available to the
33//! user for processing. The processing must complete before the DMA transfer of the next batch
34//! completes.
35//!
36//! ## Starting Data Collection
37//!
38//! Because the DMA data collection is automated via timer count comparisons and DMA transfers, the
39//! ADCs can be initialized and configured, but will not begin sampling the external ADCs until the
40//! sampling timer is enabled. As such, the sampling timer should be enabled after all
41//! initialization has completed and immediately before the embedded processing loop begins.
42//!
43//!
44//! ## Batch Sizing
45//!
46//! The ADCs collect a group of N samples, which is referred to as a batch. The size of the batch
47//! is configured by the user at compile-time to allow for a custom-tailored implementation. Larger
48//! batch sizes generally provide for lower overhead and more processing time per sample, but come
49//! at the expense of increased input -> output latency.
50//!
51//!
52//! # Note
53//!
54//! While there are two ADCs, only a single ADC is configured to generate transfer-complete
55//! interrupts. This is done because it is assumed that the ADCs will always be sampled
56//! simultaneously. If only a single ADC is used, it must always be ADC0, as ADC1 will not generate
57//! transfer-complete interrupts.
58//!
59//! There is a very small amount of latency between sampling of ADCs due to bus matrix priority. As
60//! such, one of the ADCs will be sampled marginally earlier before the other because the DMA
61//! requests are generated simultaneously. This can be avoided by providing a known offset to the
62//! sample DMA requests, which can be completed by setting e.g. ADC0's comparison to a counter
63//! value of 0 and ADC1's comparison to a counter value of 1.
64//!
65//! In this implementation, double buffer mode DMA transfers are used because the SPI RX FIFOs
66//! have finite depth, FIFO access is slower than AXISRAM access, and because the single
67//! buffer mode DMA disable/enable and buffer update sequence is slow.
68use stm32h7xx_hal as hal;
69
70use rtic::Mutex;
71
72use grounded::uninit::{GroundedArrayCell, GroundedCell};
73
74use super::design_parameters::SampleBuffer;
75use super::timers;
76
77use hal::{
78    dma::{
79        config::Priority,
80        dma::{DMAReq, DmaConfig},
81        traits::TargetAddress,
82        DMAError, MemoryToPeripheral, PeripheralToMemory, Transfer,
83    },
84    spi::{HalDisabledSpi, HalEnabledSpi, HalSpi},
85};
86
87/// A type representing an ADC sample.
88#[derive(Copy, Clone, Default)]
89pub struct AdcCode(pub u16);
90
91impl AdcCode {
92    // The ADC has a differential input with a range of +/- 4.096 V and 16-bit resolution.
93    // The gain into the two inputs is 1/5.
94    const FULL_SCALE: f32 = 5.0 / 2.0 * 4.096;
95    const VOLT_PER_LSB: f32 = -Self::FULL_SCALE / i16::MIN as f32;
96    const LSB_PER_VOLT: f32 = 1. / Self::VOLT_PER_LSB;
97}
98
99impl From<u16> for AdcCode {
100    /// Construct an ADC code from a provided binary (ADC-formatted) code.
101    fn from(value: u16) -> Self {
102        Self(value)
103    }
104}
105
106impl From<i16> for AdcCode {
107    /// Construct an ADC code from the stabilizer-defined code (i16 full range).
108    fn from(value: i16) -> Self {
109        Self(value as u16)
110    }
111}
112
113impl From<AdcCode> for i16 {
114    /// Get a stabilizer-defined code from the ADC code.
115    fn from(code: AdcCode) -> i16 {
116        code.0 as i16
117    }
118}
119
120impl From<AdcCode> for u16 {
121    /// Get an ADC-frmatted binary value from the code.
122    fn from(code: AdcCode) -> u16 {
123        code.0
124    }
125}
126
127impl From<AdcCode> for f32 {
128    /// Convert raw ADC codes to/from voltage levels.
129    ///
130    /// # Note
131    /// This does not account for the programmable gain amplifier at the signal input.
132    fn from(code: AdcCode) -> f32 {
133        i16::from(code) as f32 * AdcCode::VOLT_PER_LSB
134    }
135}
136
137impl TryFrom<f32> for AdcCode {
138    type Error = ();
139
140    fn try_from(voltage: f32) -> Result<AdcCode, ()> {
141        let code = voltage * Self::LSB_PER_VOLT;
142        if !(i16::MIN as f32..=i16::MAX as f32).contains(&code) {
143            Err(())
144        } else {
145            Ok(AdcCode::from(code as i16))
146        }
147    }
148}
149
150// The following data is written by the timer ADC sample trigger into the SPI CR1 to start the
151// transfer. Data in AXI SRAM is not initialized on boot, so the contents are random. This value is
152// initialized during setup.
153#[link_section = ".axisram.buffers"]
154static SPI_START: GroundedCell<[u32; 1]> = GroundedCell::uninit();
155
156// The following data is written by the timer flag clear trigger into the SPI IFCR register to clear
157// the EOT flag. Data in AXI SRAM is not initialized on boot, so the contents are random. This
158// value is initialized during setup.
159#[link_section = ".axisram.buffers"]
160static SPI_EOT_CLEAR: GroundedCell<[u32; 1]> = GroundedCell::uninit();
161
162// The following global buffers are used for the ADC sample DMA transfers. Two buffers are used for
163// each transfer in a ping-pong buffer configuration (one is being acquired while the other is being
164// processed). Note that the contents of AXI SRAM is uninitialized, so the buffer contents on
165// startup are undefined. The dimensions are `ADC_BUF[adc_index][ping_pong_index][sample_index]`.
166#[link_section = ".axisram.buffers"]
167static ADC_BUF: GroundedArrayCell<[SampleBuffer; 2], 2> =
168    GroundedArrayCell::uninit();
169
170macro_rules! adc_input {
171    ($name:ident, $index:literal, $trigger_stream:ident, $data_stream:ident, $clear_stream:ident,
172     $spi:ident, $trigger_channel:ident, $dma_req:ident, $clear_channel:ident, $dma_clear_req:ident) => {
173
174        paste::paste! {
175
176            /// $spi-CR is used as a type for indicating a DMA transfer into the SPI control
177            /// register whenever the tim2 update dma request occurs.
178            struct [< $spi CR >] {
179                _channel: timers::tim2::$trigger_channel,
180            }
181            impl [< $spi CR >] {
182                pub fn new(_channel: timers::tim2::$trigger_channel) -> Self {
183                    Self { _channel }
184                }
185            }
186
187            // Note(unsafe): This structure is only safe to instantiate once. The DMA request is
188            // hard-coded and may only be used if ownership of the timer2 $trigger_channel compare
189            // channel is assured, which is ensured by maintaining ownership of the channel.
190            unsafe impl TargetAddress<MemoryToPeripheral> for [< $spi CR >] {
191
192                type MemSize = u32;
193
194                /// SPI DMA requests are generated whenever TIM2 CHx ($dma_req) comparison occurs.
195                const REQUEST_LINE: Option<u8> = Some(DMAReq::$dma_req as u8);
196
197                /// Whenever the DMA request occurs, it should write into SPI's CR1 to start the
198                /// transfer.
199                fn address(&self) -> usize {
200                    // Note(unsafe): It is assumed that SPI is owned by another DMA transfer. This
201                    // is only safe because we are writing to a configuration register.
202                    let regs = unsafe { &*hal::stm32::$spi::ptr() };
203                    &regs.cr1 as *const _ as usize
204                }
205            }
206
207            /// $spi-IFCR is used as a type for indicating a DMA transfer into the SPI flag clear
208            /// register whenever the tim3 compare dma request occurs. The flag must be cleared
209            /// before the transfer starts.
210            struct [< $spi IFCR >] {
211                _channel: timers::tim3::$clear_channel,
212            }
213
214            impl [< $spi IFCR >] {
215                pub fn new(_channel: timers::tim3::$clear_channel) -> Self {
216                    Self { _channel }
217                }
218            }
219
220            // Note(unsafe): This structure is only safe to instantiate once. The DMA request is
221            // hard-coded and may only be used if ownership of the timer3 $clear_channel compare
222            // channel is assured, which is ensured by maintaining ownership of the channel.
223            unsafe impl TargetAddress<MemoryToPeripheral> for [< $spi IFCR >] {
224                type MemSize = u32;
225
226                /// SPI DMA requests are generated whenever TIM3 CHx ($dma_clear_req) comparison
227                /// occurs.
228                const REQUEST_LINE: Option<u8> = Some(DMAReq::$dma_clear_req as u8);
229
230                /// Whenever the DMA request occurs, it should write into SPI's IFCR to clear the
231                /// EOT flag to allow the next transmission.
232                fn address(&self) -> usize {
233                    // Note(unsafe): It is assumed that SPI is owned by another DMA transfer and
234                    // this DMA is only used for writing to the configuration registers.
235                    let regs = unsafe { &*hal::stm32::$spi::ptr() };
236                    &regs.ifcr as *const _ as usize
237                }
238            }
239
240            /// Represents data associated with ADC.
241            pub struct $name {
242                transfer: Transfer<
243                    hal::dma::dma::$data_stream<hal::stm32::DMA1>,
244                    hal::spi::Spi<hal::stm32::$spi, hal::spi::Disabled, u16>,
245                    PeripheralToMemory,
246                    &'static mut [u16],
247                    hal::dma::DBTransfer,
248                >,
249                trigger_transfer: Transfer<
250                    hal::dma::dma::$trigger_stream<hal::stm32::DMA1>,
251                    [< $spi CR >],
252                    MemoryToPeripheral,
253                    &'static mut [u32; 1],
254                    hal::dma::DBTransfer,
255                >,
256                clear_transfer: Transfer<
257                    hal::dma::dma::$clear_stream<hal::stm32::DMA1>,
258                    [< $spi IFCR >],
259                    MemoryToPeripheral,
260                    &'static mut [u32; 1],
261                    hal::dma::DBTransfer,
262                >,
263            }
264
265            impl $name {
266                /// Construct the ADC input channel.
267                ///
268                /// # Args
269                /// * `spi` - The SPI interface used to communicate with the ADC.
270                /// * `trigger_stream` - The DMA stream used to trigger each ADC transfer by
271                ///    writing a word into the SPI TX FIFO.
272                /// * `data_stream` - The DMA stream used to read samples received over SPI into a data buffer.
273                /// * `clear_stream` - The DMA stream used to clear the EOT flag in the SPI peripheral.
274                /// * `trigger_channel` - The ADC sampling timer output compare channel for read triggers.
275                /// * `clear_channel` - The shadow sampling timer output compare channel used for
276                ///   clearing the SPI EOT flag.
277                pub fn new(
278                    spi: hal::spi::Spi<hal::stm32::$spi, hal::spi::Enabled, u16>,
279                    trigger_stream: hal::dma::dma::$trigger_stream<
280                        hal::stm32::DMA1,
281                    >,
282                    data_stream: hal::dma::dma::$data_stream<hal::stm32::DMA1>,
283                    clear_stream: hal::dma::dma::$clear_stream<hal::stm32::DMA1>,
284                    trigger_channel: timers::tim2::$trigger_channel,
285                    clear_channel: timers::tim3::$clear_channel,
286                    batch_size: usize,
287                ) -> Self {
288                    // The flag clear DMA transfer always clears the EOT flag in the SPI
289                    // peripheral. It has the highest priority to ensure it is completed before the
290                    // transfer trigger.
291                    let clear_config = DmaConfig::default()
292                        .priority(Priority::VeryHigh)
293                        .circular_buffer(true);
294
295                    // Note(unsafe): Because this is a Memory->Peripheral transfer, this data is
296                    // never actually modified. It technically only needs to be immutably
297                    // borrowed, but the current HAL API only supports mutable borrows.
298                    let spi_eot_clear = unsafe {
299                        let ptr = SPI_EOT_CLEAR.get();
300                        ptr.write([1 << 3]);
301                        &mut *ptr
302                    };
303
304                    // Generate DMA events when the timer hits zero (roll-over). This must be before
305                    // the trigger channel DMA occurs, as if the trigger occurs first, the
306                    // transmission will not occur.
307                    clear_channel.listen_dma();
308                    clear_channel.to_output_compare(0);
309
310                    let clear_transfer: Transfer<
311                        _,
312                        _,
313                        MemoryToPeripheral,
314                        _,
315                        _,
316                    > = Transfer::init(
317                        clear_stream,
318                        [< $spi IFCR >]::new(clear_channel),
319                        spi_eot_clear,
320                        None,
321                        clear_config,
322                    );
323
324                    // Generate DMA events when an output compare of the timer hits the specified
325                    // value.
326                    trigger_channel.listen_dma();
327                    trigger_channel.to_output_compare(2 + $index);
328
329                    // The trigger stream constantly writes to the SPI CR1 using a static word
330                    // (which is a static value to enable the SPI transfer).  Thus, neither the
331                    // memory or peripheral address ever change. This is run in circular mode to be
332                    // completed at every DMA request.
333                    let trigger_config = DmaConfig::default()
334                        .priority(Priority::High)
335                        .circular_buffer(true);
336
337                    // Note(unsafe): This word is initialized once per ADC initialization to verify
338                    // it is initialized properly.
339                    // Note(unsafe): Because this is a Memory->Peripheral transfer, this data is never
340                    // actually modified. It technically only needs to be immutably borrowed, but the
341                    // current HAL API only supports mutable borrows.
342                    // Write a binary code into the SPI control register to initiate a transfer.
343                    let spi_start = unsafe {
344                        let ptr = SPI_START.get();
345                        ptr.write([0x201]);
346                        &mut *ptr
347                    };
348
349                    // Construct the trigger stream to write from memory to the peripheral.
350                    let trigger_transfer: Transfer<
351                        _,
352                        _,
353                        MemoryToPeripheral,
354                        _,
355                        _,
356                    > = Transfer::init(
357                        trigger_stream,
358                        [< $spi CR >]::new(trigger_channel),
359                        spi_start,
360                        None,
361                        trigger_config,
362                    );
363
364                    // The data stream constantly reads from the SPI RX FIFO into a RAM buffer. The peripheral
365                    // stalls reads of the SPI RX FIFO until data is available, so the DMA transfer completes
366                    // after the requested number of samples have been collected. Note that only ADC1's (sic!)
367                    // data stream is used to trigger a transfer completion interrupt.
368                    let data_config = DmaConfig::default()
369                        .memory_increment(true)
370                        .double_buffer(true)
371                        .transfer_complete_interrupt($index == 1)
372                        .priority(Priority::VeryHigh);
373
374                    // A SPI peripheral error interrupt is used to determine if the RX FIFO
375                    // overflows. This indicates that samples were dropped due to excessive
376                    // processing time in the main application (e.g. a second DMA transfer completes
377                    // before the first was done with processing). This is used as a flow control
378                    // indicator to guarantee that no ADC samples are lost.
379                    let mut spi = spi.disable();
380                    spi.listen(hal::spi::Event::Error);
381
382                    let adc_bufs = unsafe {
383                        ADC_BUF.initialize_all_with(|| Default::default());
384                        ADC_BUF.get_element_mut_unchecked($index).split_at_mut(1)
385                    };
386
387                    // The data transfer is always a transfer of data from the peripheral to a RAM
388                    // buffer.
389                    let data_transfer: Transfer<_, _, PeripheralToMemory, _, _> =
390                        Transfer::init(
391                            data_stream,
392                            spi,
393                            // Note(unsafe): The ADC_BUF[$index] is "owned" by this peripheral.
394                            // It shall not be used anywhere else in the module.
395                            &mut adc_bufs.0[0][..batch_size],
396                            Some(&mut adc_bufs.1[0][..batch_size]),
397                            data_config,
398                        );
399
400                    Self {
401                        transfer: data_transfer,
402                        trigger_transfer,
403                        clear_transfer,
404                    }
405                }
406
407                /// Enable the ADC DMA transfer sequence.
408                pub fn start(&mut self) {
409                    self.transfer.start(|spi| {
410                        spi.enable_dma_rx();
411
412                        spi.inner().cr2.modify(|_, w| w.tsize().bits(1));
413                        spi.inner().cr1.modify(|_, w| w.spe().set_bit());
414                    });
415
416                    self.clear_transfer.start(|_| {});
417                    self.trigger_transfer.start(|_| {});
418
419                }
420
421                /// Wait for the transfer of the currently active buffer to complete,
422                /// then call a function on the now inactive buffer and acknowledge the
423                /// transfer complete flag.
424                ///
425                /// NOTE(unsafe): Memory safety and access ordering is not guaranteed
426                /// (see the HAL DMA docs).
427                pub fn with_buffer<F, R>(&mut self, f: F) -> Result<R, DMAError>
428                where
429                    F: FnOnce(&mut &'static mut [u16]) -> R,
430                {
431                    unsafe { self.transfer.next_dbm_transfer_with(|buf, _current| {
432                        f(buf)
433                    })}
434                }
435            }
436
437            // This is not actually a Mutex. It only re-uses the semantics and macros of mutex-trait
438            // to reduce rightward drift when jointly calling `with_buffer(f)` on multiple DAC/ADCs.
439            impl Mutex for $name {
440                type T = &'static mut [u16];
441                fn lock<R>(&mut self, f: impl FnOnce(&mut Self::T) -> R) -> R {
442                    self.with_buffer(f).unwrap()
443                }
444            }
445        }
446    };
447}
448
449adc_input!(
450    Adc0Input, 0, Stream0, Stream1, Stream2, SPI2, Channel1, Tim2Ch1, Channel1,
451    Tim3Ch1
452);
453adc_input!(
454    Adc1Input, 1, Stream3, Stream4, Stream5, SPI3, Channel2, Tim2Ch2, Channel2,
455    Tim3Ch2
456);