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 ®s.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 ®s.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);