Adding direct and DMA collection support for DI0 timestamps
This commit is contained in:
parent
f2e4f497fa
commit
a134340726
@ -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<hal::gpio::Alternate<hal::gpio::AF2>>,
|
||||
next_buffer: Option<&'static mut [u16; SAMPLE_BUFFER_SIZE]>,
|
||||
transfer: Transfer<
|
||||
hal::dma::dma::Stream6<hal::stm32::DMA1>,
|
||||
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<hal::stm32::DMA1>,
|
||||
timers::tim5::Channel4InputCapture,
|
||||
PeripheralToMemory,
|
||||
&'static mut [u32; SAMPLE_BUFFER_SIZE],
|
||||
>,
|
||||
>,
|
||||
capture_channel: Option<timers::tim5::Channel4InputCapture>,
|
||||
}
|
||||
|
||||
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<hal::gpio::Alternate<hal::gpio::AF2>>,
|
||||
stream: hal::dma::dma::Stream6<hal::stm32::DMA1>,
|
||||
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 => &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
11
src/main.rs
11
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
|
||||
|
@ -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<u32> {
|
||||
// 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.[<cc $index ie>]().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<PeripheralToMemory> for [< Channel $index InputCapture >] {
|
||||
type MemSize = u16;
|
||||
type MemSize = u32;
|
||||
|
||||
const REQUEST_LINE: Option<u8> = 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);
|
||||
|
Loading…
Reference in New Issue
Block a user