From a134340726f94fd1b6df3f894aae503518e7c3fa Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Tue, 8 Dec 2020 13:53:34 +0100 Subject: [PATCH] Adding direct and DMA collection support for DI0 timestamps --- src/digital_input_stamper.rs | 146 +++++++++++++++++++++++++++-------- src/main.rs | 11 +-- src/timers.rs | 58 +++++++++++++- 3 files changed, 173 insertions(+), 42 deletions(-) diff --git a/src/digital_input_stamper.rs b/src/digital_input_stamper.rs index 3df8eee..fe8c86b 100644 --- a/src/digital_input_stamper.rs +++ b/src/digital_input_stamper.rs @@ -1,66 +1,146 @@ -use super::{SAMPLE_BUFFER_SIZE, hal, timers, DmaConfig, PeripheralToMemory, Transfer}; +///! Digital Input 0 (DI0) reference clock timestamper +///! +///! This module provides a means of timestamping the rising edges of an external reference clock on +///! the DI0 with a timer value from TIM5. +///! +///! This module only supports input clocks on DI0 and may or may not utilize DMA to collect +///! timestamps. +///! +///! # Design +///! An input capture channel is configured on DI0 and fed into TIM5's capture channel 4. TIM5 is +///! then run in a free-running mode with a configured frequency and period. Whenever an edge on DI0 +///! triggers, the current TIM5 capture value is recorded as a timestamp. This timestamp can be +///! either directly read from the timer channel or can be collected asynchronously via DMA +///! collection. +///! +///! When DMA is used for timestamp collection, a DMA transfer is configured to collect as many +///! timestamps as there are samples, but it is intended that this DMA transfer should never +///! complete. Instead, when all samples are collected, the module pauses the DMA transfer and +///! checks to see how many timestamps were collected. These collected timestamps are then returned +///! for further processing. +///! +///! To prevent silently discarding timestamps, the TIm5 input capture over-capture interrupt is +///! used. Any over-capture event (which indicates an overwritten timestamp) then generates an ISR +///! which handles the over-capture. +///! +///! # Tradeoffs +///! It appears that DMA transfers can take a significant amount of time to disable (400ns) if they +///! are being prematurely stopped (such is the case here). As such, for a sample batch size of 1, +///! this can take up a significant amount of the total available processing time for the samples. +///! To avoid this, the module does not use DMA when the sample batch size is one. Instead, the +///! module manually checks for any captured timestamps from the timer capture channel manually. In +///! this mode, the maximum input clock frequency supported is equal to the configured sample rate. +///! +///! There is a small window while the DMA buffers are swapped where a timestamp could potentially +///! be lost. To prevent this, the `acuire_buffer()` method should not be pre-empted. Any lost +///! timestamp will trigger an over-capture interrupt. +use super::{ + hal, timers, DmaConfig, PeripheralToMemory, Transfer, SAMPLE_BUFFER_SIZE, +}; +// The DMA buffers must exist in a location where DMA can access. By default, RAM uses DTCM, which +// is off-limits to the normal DMA peripheral. Instead, we use AXISRAM. #[link_section = ".axisram.buffers"] -static mut BUF: [[u16; SAMPLE_BUFFER_SIZE]; 2] = [[0; SAMPLE_BUFFER_SIZE]; 2]; +static mut BUF: [[u32; SAMPLE_BUFFER_SIZE]; 2] = [[0; SAMPLE_BUFFER_SIZE]; 2]; +/// The timestamper for DI0 reference clock inputs. pub struct InputStamper { _di0_trigger: hal::gpio::gpioa::PA3>, - next_buffer: Option<&'static mut [u16; SAMPLE_BUFFER_SIZE]>, - transfer: Transfer< - hal::dma::dma::Stream6, - timers::tim5::Channel4InputCapture, - PeripheralToMemory, - &'static mut [u16; SAMPLE_BUFFER_SIZE], + next_buffer: Option<&'static mut [u32; SAMPLE_BUFFER_SIZE]>, + transfer: Option< + Transfer< + hal::dma::dma::Stream6, + timers::tim5::Channel4InputCapture, + PeripheralToMemory, + &'static mut [u32; SAMPLE_BUFFER_SIZE], + >, >, + capture_channel: Option, } impl InputStamper { + /// Construct the DI0 input timestamper. + /// + /// # Args + /// * `trigger` - The capture trigger input pin. + /// * `stream` - The DMA stream to use for collecting timestamps. + /// * `timer_channel - The timer channel used for capturing timestamps. + /// * `batch_size` - The number of samples collected per processing batch. pub fn new( trigger: hal::gpio::gpioa::PA3>, stream: hal::dma::dma::Stream6, timer_channel: timers::tim5::Channel4, + batch_size: usize, ) -> Self { // Utilize the TIM5 CH4 as an input capture channel - use TI4 (the DI0 input trigger) as the // capture source. - timer_channel.listen_dma(); let input_capture = timer_channel.to_input_capture(timers::tim5::CC4S_A::TI4); - // Set up the DMA transfer. - let dma_config = DmaConfig::default() - .transfer_complete_interrupt(true) - .memory_increment(true) - .peripheral_increment(false); + // Listen for over-capture events, which indicates an over-run of DI0 timestamps. + input_capture.listen_overcapture(); - // TODO: This needs to operate in double-buffer+circular mode so that we don't potentially - // drop input timestamps. - let mut timestamp_transfer: Transfer<_, _, PeripheralToMemory, _> = - Transfer::init( - stream, - input_capture, - unsafe { &mut BUF[0] }, - None, - dma_config, - ); + // For small batch sizes, the overhead of DMA can become burdensome to the point where + // timing is not met. The DMA requires 500ns overhead, whereas a direct register read only + // requires ~80ns. When batches of 2-or-greater are used, use a DMA-based approach. + let (transfer, input_capture) = if batch_size >= 2 { + input_capture.listen_dma(); - timestamp_transfer.start(|_| {}); + // Set up the DMA transfer. + let dma_config = DmaConfig::default().memory_increment(true); + + let mut timestamp_transfer: Transfer<_, _, PeripheralToMemory, _> = + Transfer::init( + stream, + input_capture, + unsafe { &mut BUF[0] }, + None, + dma_config, + ); + + timestamp_transfer.start(|_| {}); + (Some(timestamp_transfer), None) + } else { + (None, Some(input_capture)) + }; Self { next_buffer: unsafe { Some(&mut BUF[1]) }, - transfer: timestamp_transfer, + transfer, + capture_channel: input_capture, _di0_trigger: trigger, } } - pub fn acquire_buffer(&mut self) -> &[u16] { - let next_buffer = self.next_buffer.take().unwrap(); - let (prev_buffer, _, remaining_transfers) = - self.transfer.next_transfer(next_buffer).unwrap(); + /// Get all of the timestamps that have occurred during the last processing cycle. + pub fn acquire_buffer(&mut self) -> &[u32] { + // If we are using DMA, finish the transfer and swap over buffers. + if self.transfer.is_some() { + let next_buffer = self.next_buffer.take().unwrap(); - let valid_count = prev_buffer.len() - remaining_transfers; + let (prev_buffer, _, remaining_transfers) = self + .transfer + .as_mut() + .unwrap() + .next_transfer(next_buffer) + .unwrap(); + let valid_count = prev_buffer.len() - remaining_transfers; - self.next_buffer.replace(prev_buffer); + self.next_buffer.replace(prev_buffer); - &self.next_buffer.as_ref().unwrap()[..valid_count] + // Note that we likely didn't finish the transfer, so only return the number of + // timestamps actually collected. + &self.next_buffer.as_ref().unwrap()[..valid_count] + } else { + // If we aren't using DMA, just manually check the input capture channel for a + // timestamp. + match self.capture_channel.as_mut().unwrap().latest_capture() { + Some(stamp) => { + self.next_buffer.as_mut().unwrap()[0] = stamp; + &self.next_buffer.as_ref().unwrap()[..1] + } + None => &[], + } + } } } diff --git a/src/main.rs b/src/main.rs index e798294..469f114 100644 --- a/src/main.rs +++ b/src/main.rs @@ -808,6 +808,7 @@ const APP: () = { trigger, dma_streams.6, timestamp_timer_channels.ch4, + SAMPLE_BUFFER_SIZE, ) }; @@ -1030,11 +1031,6 @@ const APP: () = { } } - #[task(binds=DMA1_STR6, priority = 2)] - fn di0_timestamp(_: di0_timestamp::Context) { - panic!("DI0 Timestamp overflow") - } - #[task(binds = ETH, priority = 1)] fn eth(_: eth::Context) { unsafe { ethernet::interrupt_handler() } @@ -1060,6 +1056,11 @@ const APP: () = { panic!("DAC1 output error"); } + #[task(binds = TIM5, priority = 3)] + fn di0(_: di0::Context) { + panic!("DI0 timestamp overrun"); + } + extern "C" { // hw interrupt handlers for RTIC to use for scheduling tasks // one per priority diff --git a/src/timers.rs b/src/timers.rs index a3c2dce..74b4731 100644 --- a/src/timers.rs +++ b/src/timers.rs @@ -2,7 +2,7 @@ use super::hal; macro_rules! timer_channels { - ($name:ident, $TY:ident) => { + ($name:ident, $TY:ident, u32) => { paste::paste! { /// The timer used for managing ADC sampling. @@ -32,12 +32,14 @@ macro_rules! timer_channels { self.channels.take().unwrap() } + /// Get the prescaler of a timer. #[allow(dead_code)] pub fn get_prescaler(&self) -> u16 { let regs = unsafe { &*hal::stm32::$TY::ptr() }; regs.psc.read().psc().bits() + 1 } + /// Manually set the prescaler of the timer. #[allow(dead_code)] pub fn set_prescaler(&mut self, prescaler: u16) { let regs = unsafe { &*hal::stm32::$TY::ptr() }; @@ -45,12 +47,14 @@ macro_rules! timer_channels { regs.psc.write(|w| w.psc().bits(prescaler - 1)); } + /// Get the period of the timer. #[allow(dead_code)] pub fn get_period(&self) -> u32 { let regs = unsafe { &*hal::stm32::$TY::ptr() }; regs.arr.read().arr().bits() } + /// Manually set the period of the timer. #[allow(dead_code)] pub fn set_period(&mut self, period: u32) { let regs = unsafe { &*hal::stm32::$TY::ptr() }; @@ -107,8 +111,10 @@ macro_rules! timer_channels { ($index:expr, $TY:ty, $ccmrx:expr) => { paste::paste! { + /// A capture/compare channel of the timer. pub struct [< Channel $index >] {} + /// A capture channel of the timer. pub struct [< Channel $index InputCapture>] {} impl [< Channel $index >] { @@ -153,8 +159,52 @@ macro_rules! timer_channels { } } + impl [< Channel $index InputCapture >] { + /// Get the latest capture from the channel. + #[allow(dead_code)] + pub fn latest_capture(&mut self) -> Option { + // Note(unsafe): This channel owns all access to the specific timer channel. + // Only atomic operations on completed on the timer registers. + let regs = unsafe { &*<$TY>::ptr() }; + let sr = regs.sr.read(); + let ccx = regs.[< ccr $index >].read(); + if sr.[< cc $index if >]().bit_is_set() { + regs.sr.modify(|_, w| w.[< cc $index if >]().clear_bit()); + Some(ccx.ccr().bits()) + } else { + None + } + } + + /// Listen for over-capture events on the timer channel. + /// + /// # Note + /// An over-capture event is when a previous capture was lost due to a new capture. + /// + /// "Listening" is equivalent to enabling the interrupt for the event. + #[allow(dead_code)] + pub fn listen_overcapture(&self) { + // Note(unsafe): This channel owns all access to the specific timer channel. + // Only atomic operations on completed on the timer registers. + let regs = unsafe { &*<$TY>::ptr() }; + regs.dier.modify(|_, w| w.[]().set_bit()); + } + + /// Allow the channel to generate DMA requests. + #[allow(dead_code)] + pub fn listen_dma(&self) { + // Note(unsafe): This channel owns all access to the specific timer channel. + // Only atomic operations on completed on the timer registers. + let regs = unsafe { &*<$TY>::ptr() }; + regs.dier.modify(|_, w| w.[< cc $index de >]().set_bit()); + } + } + + // Note(unsafe): This manually implements DMA support for input-capture channels. This + // is safe as it is only completed once per channel and each DMA request is allocated to + // each channel as the owner. unsafe impl TargetAddress for [< Channel $index InputCapture >] { - type MemSize = u16; + type MemSize = u32; const REQUEST_LINE: Option = Some(DMAReq::[< $TY _CH $index >]as u8); @@ -167,5 +217,5 @@ macro_rules! timer_channels { }; } -timer_channels!(SamplingTimer, TIM2); -timer_channels!(TimestampTimer, TIM5); +timer_channels!(SamplingTimer, TIM2, u32); +timer_channels!(TimestampTimer, TIM5, u32);