Adding documentation for ADCs and DACs

This commit is contained in:
Ryan Summers 2020-12-15 16:46:12 +01:00
parent a71f790574
commit 8e4a7c8fa9
2 changed files with 126 additions and 30 deletions

View File

@ -1,18 +1,75 @@
///! Stabilizer ADC management interface ///! Stabilizer ADC management interface
///! ///!
///! The Stabilizer ADCs utilize a DMA channel to trigger sampling. The SPI streams are configured ///! # Design
///! for full-duplex operation, but only RX is connected to physical pins. A timer channel is
///! configured to generate a DMA write into the SPI TXFIFO, which initiates a SPI transfer and
///! results in an ADC sample read for both channels.
///! ///!
///! In order to read multiple samples without interrupting the CPU, a separate DMA transfer is ///! Stabilizer ADCs are connected to the MCU via a simplex, SPI-compatible interface. The ADCs
///! configured to read from each of the ADC SPI RX FIFOs. Due to the design of the SPI peripheral, ///! require a setup conversion time after asserting the CSn (convert) signal to generate the ADC
///! these DMA transfers stall when no data is available in the FIFO. Thus, the DMA transfer only ///! code from the sampled level. Once the setup time has elapsed, the ADC data is clocked out of
///! completes after all samples have been read. When this occurs, a CPU interrupt is generated so ///! MISO. The internal setup time is managed by the SPI peripheral via a CSn setup time parameter
///! that software can process the acquired samples from both ADCs. Only one of the ADC DMA streams ///! during SPI configuration, which allows offloading the management of the setup time to hardware.
///! is configured to generate an interrupt to handle both transfers, so it is necessary to ensure ///!
///! both transfers are completed before reading the data. This is usually not significant for ///! Because of the SPI-compatibility of the ADCs, a single SPI peripheral + DMA is used to automate
///! busy-waiting because the transfers should complete at approximately the same time. ///! the collection of multiple ADC samples without requiring processing by the CPU, which reduces
///! overhead and provides the CPU with more time for processing-intensive tasks, like DSP.
///!
///! The automation of sample collection utilizes two DMA streams, the SPI peripheral, and a timer
///! compare channel for each ADC. The timer comparison channel is configured to generate a
///! comparison event every time the timer is equal to a specific value. Each comparison then
///! generates a DMA transfer event to write into the SPI TX buffer. Although the SPI is a simplex,
///! RX-only interface, it is configured in full-duplex mode and the TX pin is left disconnected.
///! This allows the SPI interface to periodically read a single word whenever a word is written to
///! the TX side. Thus, by running a continuous DMA transfer to periodically write a value into the
///! TX FIFO, we can schedule the regular collection of ADC samples in the SPI RX buffer.
///!
///! In order to collect the acquired ADC samples into a RAM buffer, a second DMA transfer is
///! configured to read from the SPI RX FIFO into RAM. The request for this transfer is connected to
///! the SPI RX data signal, so the SPI peripheral will request to move data into RAM whenever it is
///! available. When enough samples have been collected, a transfer-complete interrupt is generated
///! and the ADC samples are available for processing.
///!
///! The SPI peripheral internally has an 8- or 16-byte TX and RX FIFO, which corresponds to a 4- or
///! 8-sample buffer for incoming ADC samples. During the handling of the DMA transfer completion,
///! there is a small window where buffers are swapped over where it's possible that a sample could
///! be lost. In order to avoid this, the SPI RX FIFO is effectively used as a "sample overflow"
///! region and can buffer a number of samples until the next DMA transfer is configured. If a DMA
///! transfer is still not set in time, the SPI peripheral will generate an input-overrun interrupt.
///! This interrupt then serves as a means of detecting if samples have been lost, which will occur
///! whenever data processing takes longer than the collection period.
///!
///!
///! ## Starting Data Collection
///!
///! Because the DMA data collection is automated via timer count comparisons and DMA transfers, the
///! ADCs can be initialized and configured, but will not begin sampling the external ADCs until the
///! sampling timer is enabled. As such, the sampling timer should be enabled after all
///! initialization has completed and immediately before the embedded processing loop begins.
///!
///!
///! ## Batch Sizing
///!
///! The ADCs collect a group of N samples, which is referred to as a batch. The size of the batch
///! is configured by the user at compile-time to allow for a custom-tailored implementation. Larger
///! batch sizes generally provide for more processing time per sample, but come at the expense of
///! increased input -> output latency.
///!
///!
///! # Note
///!
///! While there are two ADCs, only a single ADC is configured to generate transfer-complete
///! interrupts. This is done because it is assumed that the ADCs will always be sampled
///! simultaneously. If only a single ADC is used, it must always be ADC0, as ADC1 will not generate
///! transfer-complete interrupts.
///!
///! There is a very small amount of latency between sampling of ADCs due to bus matrix priority. As
///! such, one of the ADCs will be sampled marginally earlier before the other because the DMA
///! requests are generated simultaneously. This can be avoided by providing a known offset to the
///! sample DMA requests, which can be completed by setting e.g. ADC0's comparison to a counter
///! value of 0 and ADC1's comparison to a counter value of 1.
///!
///! In this implementation, single buffer mode DMA transfers are used because the SPI RX FIFO can
///! be used as a means to both detect and buffer ADC samples during the buffer swap-over. Because
///! of this, double-buffered mode does not offer any advantages over single-buffered mode (unless
///! double-buffered mode offers less overhead when accessing data).
use super::{ use super::{
hal, sampling_timer, DMAReq, DmaConfig, MemoryToPeripheral, hal, sampling_timer, DMAReq, DmaConfig, MemoryToPeripheral,
PeripheralToMemory, Priority, TargetAddress, Transfer, SAMPLE_BUFFER_SIZE, PeripheralToMemory, Priority, TargetAddress, Transfer, SAMPLE_BUFFER_SIZE,

View File

@ -1,8 +1,45 @@
///! Stabilizer DAC management interface ///! Stabilizer DAC management interface
///! ///!
///! The Stabilizer DAC utilize a DMA channel to generate output updates. A timer channel is ///! # Design
///! configured to generate a DMA write into the SPI TXFIFO, which initiates a SPI transfer and ///!
///! results in DAC update for both channels. ///! Stabilizer DACs are connected to the MCU via a simplex, SPI-compatible interface. Each DAC
///! accepts a 16-bit output code.
///!
///! In order to maximize CPU processing time, the DAC code updates are offloaded to hardware using
///! a timer compare channel, DMA stream, and the DAC SPI interface.
///!
///! The timer comparison channel is configured to generate a DMA request whenever the comparison
///! occurs. Thus, whenever a comparison happens, a single DAC code can be written to the output. By
///! configuring a DMA stream for a number of successive DAC codes, hardware can regularly update
///! the DAC without requiring the CPU.
///!
///! ## Multiple Samples to Single DAC Codes
///!
///! For some applications, it may be desirable to generate a single DAC code from multiple ADC
///! samples. In order to maintain timing characteristics between ADC samples and DAC code outputs,
///! applications are required to generate one DAC code for each ADC sample. To accomodate mapping
///! multiple inputs to a single output, the output code can be repeated a number of times in the
///! output buffer corresponding with the number of input samples that were used to generate it.
///!
///!
///! # Note
///!
///! There is a very small amount of latency between updating the two DACs due to bus matrix
///! priority. As such, one of the DACs will be updated marginally earlier before the other because
///! the DMA requests are generated simultaneously. This can be avoided by providing a known offset
///! to other DMA requests, which can be completed by setting e.g. DAC0's comparison to a
///! counter value of 2 and DAC1's comparison to a counter value of 3. This will have the effect of
///! generating the DAC updates with a known latency of 1 timer tick to each other and prevent the
///! DMAs from racing for the bus. As implemented, the DMA channels utilize natural priority of the
///! DMA channels to arbitrate which transfer occurs first.
///!
///!
///! # Future Improvements
///!
///! In this implementation, single buffer mode DMA transfers are used. As a result of this, it's
///! possible that a timer comparison could be missed during the swap-over, which will result in a
///! delay of a single output code. In the future, this can be remedied by utilize double-buffer
///! mode for the DMA transfers.
use super::{ use super::{
hal, sampling_timer, DMAReq, DmaConfig, MemoryToPeripheral, TargetAddress, hal, sampling_timer, DMAReq, DmaConfig, MemoryToPeripheral, TargetAddress,
Transfer, SAMPLE_BUFFER_SIZE, Transfer, SAMPLE_BUFFER_SIZE,
@ -90,12 +127,11 @@ macro_rules! dac_output {
let mut spi = spi.disable(); let mut spi = spi.disable();
spi.listen(hal::spi::Event::Error); spi.listen(hal::spi::Event::Error);
// Allow the SPI FIFOs to operate using only DMA data channels. // AXISRAM is uninitialized. As such, we manually zero-initialize it here before
spi.enable_dma_tx(); // starting the transfer.
for byte in DAC_BUF[$index].iter_mut() {
// Enable SPI and start it in infinite transaction mode. *byte = 0;
spi.inner().cr1.modify(|_, w| w.spe().set_bit()); }
spi.inner().cr1.modify(|_, w| w.cstart().started());
// Construct the trigger stream to write from memory to the peripheral. // Construct the trigger stream to write from memory to the peripheral.
let transfer: Transfer<_, _, MemoryToPeripheral, _> = let transfer: Transfer<_, _, MemoryToPeripheral, _> =
@ -108,6 +144,15 @@ macro_rules! dac_output {
trigger_config, trigger_config,
); );
transfer.start(|spi| {
// Allow the SPI FIFOs to operate using only DMA data channels.
spi.enable_dma_tx();
// Enable SPI and start it in infinite transaction mode.
spi.inner().cr1.modify(|_, w| w.spe().set_bit());
spi.inner().cr1.modify(|_, w| w.cstart().started());
});
Self { Self {
transfer, transfer,
// Note(unsafe): This buffer is only used once and provided for the next DMA transfer. // Note(unsafe): This buffer is only used once and provided for the next DMA transfer.
@ -131,15 +176,9 @@ macro_rules! dac_output {
&mut self, &mut self,
next_buffer: &'static mut [u16; SAMPLE_BUFFER_SIZE], next_buffer: &'static mut [u16; SAMPLE_BUFFER_SIZE],
) { ) {
// If the last transfer was not complete, we didn't write all our previous DAC codes.
// Wait for all the DAC codes to get written as well.
if self.first_transfer {
self.first_transfer = false
} else {
// Note: If a device hangs up, check that this conditional is passing correctly, as // Note: If a device hangs up, check that this conditional is passing correctly, as
// there is no time-out checks here in the interest of execution speed. // there is no time-out checks here in the interest of execution speed.
while !self.transfer.get_transfer_complete_flag() {} while !self.transfer.get_transfer_complete_flag() {}
}
// Start the next transfer. // Start the next transfer.
self.transfer.clear_interrupts(); self.transfer.clear_interrupts();