T4.1: Synchronizing DMA ADC reads with an external DAC write?

Scientist

Well-known member
I am trying to synchronize reading an ADC value on a T4.1 with updating a DAC output on an external DAC (likely the built in DAC on a SAMD51). At the same time I want to use dual DMA buffers to do this in the background as in the adc_timer_dma example.

I can see two possible ways to do this - have the timer (Qtimer on a T4.x) that is clocking the ADC reads also provide an external clock to drive the external DAC. Or I could have the clock from the external DAC provide the trigger for the Teensy ADC. Either would work for my application - but I am not sure what is the path of least resistance?

I have been reading through the source code for Pedvide's most excellent ADC library and see where he sets up QuadTimer4 to drive the ADC for DMA sampling - is there a way with the XBAR system to also connect the timer output to an IO pin? That seems like it would be an elegant solution. Or is there a 'conversion complete' signal I could route to a digital output pin?

Thanks!
 
Last edited:
Nearly all of the quad timer input/output pins can be remapped using the crossbar. You just need to read the reference manual to find the crossbar assignments and write the code, because there's no library support for it.
 
I guess I am not quite understanding XBAR correctly. I added the following code snippets to the 'adc_timer_dma' example from the ADC library, and no luck so far:
Code:
  pinMode(4,OUTPUT);
  //Setup XBAR for conversion complete signal
  Serial.println("Setup XBAR");
  CCM_CCGR2 |= CCM_CCGR2_XBAR1(CCM_CCGR_ON);  //turn on XBAR clock (I think this is also done in ADC)
  xbar_connect(XBARA1_IN_ADC_ETC0_COCO0, XBARA1_OUT_IOMUX_XBAR_INOUT04); //connect conversion complete to pin4
 
 
  void xbar_connect(unsigned int input, unsigned int output)
{
    if (input >= 88) return;
    if (output >= 132) return;
#if 1
    volatile uint16_t *xbar = &XBARA1_SEL0 + (output / 2);
    uint16_t val = *xbar;
    if (!(output & 1)) {
        val = (val & 0xFF00) | input;
    } else {
        val = (val & 0x00FF) | (input << 8);
    }
    *xbar = val;
#else
    // does not work, seems 8 bit access is not allowed
    volatile uint8_t *xbar = (volatile uint8_t *)XBARA1_SEL0;
    xbar[output] = input;
#endif
}

Maybe I need to configure COCO0, or is it a different COCO? Am I missing some configuration to drive pin4?

For completeness here is the entire code:
Code:
/* Example for triggering the ADC with Timer using DMA instead of interrupts
    Valid for the current Teensy 3.x and 4.0.


  Timers:
    On Teensy 3.x this uses the PDB timer.

    On Teensy 4, this uses one or two of the unused QTimers.

    Setting it up:  The variables readPin must be defined for a pin that is valid for the first (or only)
    ADC.  If the processor has a second ADC and is enabled, than readPin2 must be configured to be a pin
    that is valid on the second ADC.

  DMA: using AnalogBufferDMA with two buffers, this runs in continuous mode and when one buffer fills
    an interrupt is signaled, which sets flag saying it has data, which this test application
    scans the data, and computes things like a minimum, maximum, average values and an RMS value.
    For the RMS it keeps the average from the previous set of data.
*/

#define ADC_USE_DMA 1
#define ADC_USE_TIMER 1

#if defined(ADC_USE_DMA) && defined(ADC_USE_TIMER)

#include <ADC.h>
#include <DMAChannel.h>
#include <AnalogBufferDMA.h>

#define PRINT_DEBUG_INFO

// This version uses both ADC1 and ADC2
#ifdef ADC_DUAL_ADCS
const int readPin_adc_0 = A0;
const int readPin_adc_1 = A2;
#else
const int readPin_adc_0 = A0;
const int readPin_adc_1 = 26;
#endif

ADC *adc = new ADC(); // adc object
const uint32_t initial_average_value = 2048;

extern void dumpDMA_structures(DMABaseClass *dmabc);
elapsedMillis elapsed_sinc_last_display;

// Going to try two buffers here  using 2 dmaSettings and a DMAChannel

const uint32_t buffer_size = 1600;
DMAMEM static volatile uint16_t __attribute__((aligned(32))) dma_adc_buff1[buffer_size];
DMAMEM static volatile uint16_t __attribute__((aligned(32))) dma_adc_buff2[buffer_size];
AnalogBufferDMA abdma1(dma_adc_buff1, buffer_size, dma_adc_buff2, buffer_size);

#ifdef ADC_DUAL_ADCS
DMAMEM static volatile uint16_t __attribute__((aligned(32))) dma_adc_buff2_1[buffer_size];
DMAMEM static volatile uint16_t __attribute__((aligned(32))) dma_adc_buff2_2[buffer_size];
AnalogBufferDMA abdma2(dma_adc_buff2_1, buffer_size, dma_adc_buff2_2, buffer_size);
#endif

void setup() {
  Serial.begin(9600);
  while (!Serial && millis() < 5000) ;

  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(readPin_adc_0, INPUT); // Not sure this does anything for us
  #ifdef ADC_DUAL_ADCS
  pinMode(readPin_adc_1, INPUT);
  #endif

  pinMode(4,OUTPUT);

  //Setup XBAR for conversion complete signal
  Serial.println("Setup XBAR");
  CCM_CCGR2 |= CCM_CCGR2_XBAR1(CCM_CCGR_ON);  //turn on XBAR clock (I think this is also done in ADC)
  xbar_connect(XBARA1_IN_ADC_ETC0_COCO0, XBARA1_OUT_IOMUX_XBAR_INOUT04); //connect conversion complete to pin4
 
  Serial.println("Setup both ADCs");
  // Setup both ADCs
  adc->adc0->setAveraging(8); // set number of averages
  adc->adc0->setResolution(12); // set bits of resolution
  #ifdef ADC_DUAL_ADCS
  adc->adc1->setAveraging(8); // set number of averages
  adc->adc1->setResolution(12); // set bits of resolution
  #endif

  // enable DMA and interrupts
  //Serial.println("before enableDMA"); Serial.flush();


  // setup a DMA Channel.
  // Now lets see the different things that RingbufferDMA setup for us before
  abdma1.init(adc, ADC_0/*, DMAMUX_SOURCE_ADC_ETC*/);
  abdma1.userData(initial_average_value); // save away initial starting average
#ifdef ADC_DUAL_ADCS
  abdma2.init(adc, ADC_1/*, DMAMUX_SOURCE_ADC_ETC*/);
  abdma2.userData(initial_average_value); // save away initial starting average
#endif
  //Serial.println("After enableDMA"); Serial.flush();

  // Start the dma operation..
  adc->adc0->startSingleRead(readPin_adc_0); // call this to setup everything before the Timer starts, differential is also possible
  adc->adc0->startTimer(3000); //frequency in Hz

  // Start the dma operation..
#ifdef ADC_DUAL_ADCS
  adc->adc1->startSingleRead(readPin_adc_1); // call this to setup everything before the Timer starts, differential is also possible
  adc->adc1->startTimer(3000); //frequency in Hz
#endif

  print_debug_information();

  Serial.println("End Setup");
  elapsed_sinc_last_display = 0;
}

void loop() {

  // Maybe only when both have triggered?
#ifdef ADC_DUAL_ADCS
  if ( abdma1.interrupted() && (abdma2.interrupted())) {
    if ( abdma1.interrupted()) {
      ProcessAnalogData(&abdma1, 0);
    }
    if ( abdma2.interrupted()) {
      ProcessAnalogData(&abdma2, 1);
    }
    Serial.println();
    elapsed_sinc_last_display = 0;
  }
#else
  if ( abdma1.interrupted()) {
    ProcessAnalogData(&abdma1, 0);
    Serial.println();
    elapsed_sinc_last_display = 0;
  }
#endif
  if (elapsed_sinc_last_display > 5000) {
    // Nothing in 5 seconds, show a heart beat.
    digitalWriteFast(13, HIGH);
    delay(250);
    digitalWriteFast(13, LOW);
    delay(250);
    digitalWriteFast(13, HIGH);
    delay(250);
    digitalWriteFast(13, LOW);
    elapsed_sinc_last_display = 0;
  }
}

void ProcessAnalogData(AnalogBufferDMA *pabdma, int8_t adc_num) {
  uint32_t sum_values = 0;
  uint16_t min_val = 0xffff;
  uint16_t max_val = 0;

  uint32_t average_value = pabdma->userData();

  volatile uint16_t *pbuffer = pabdma->bufferLastISRFilled();
  volatile uint16_t *end_pbuffer = pbuffer + pabdma->bufferCountLastISRFilled();

  float sum_delta_sq = 0.0;
  if ((uint32_t)pbuffer >= 0x20200000u)  arm_dcache_delete((void*)pbuffer, sizeof(dma_adc_buff1));
  while (pbuffer < end_pbuffer) {
    if (*pbuffer < min_val) min_val = *pbuffer;
    if (*pbuffer > max_val) max_val = *pbuffer;
    sum_values += *pbuffer;
    int delta_from_center = (int) * pbuffer - average_value;
    sum_delta_sq += delta_from_center * delta_from_center;

    pbuffer++;
  }

  int rms = sqrt(sum_delta_sq / buffer_size);
  average_value = sum_values / buffer_size;
  Serial.printf(" %d - %u(%u): %u <= %u <= %u %d ", adc_num, pabdma->interruptCount(), pabdma->interruptDeltaTime(), min_val,
                average_value, max_val, rms);
  pabdma->clearInterrupt();

  pabdma->userData(average_value);
}

void xbar_connect(unsigned int input, unsigned int output)
{
    if (input >= 88) return;
    if (output >= 132) return;
#if 1
    volatile uint16_t *xbar = &XBARA1_SEL0 + (output / 2);
    uint16_t val = *xbar;
    if (!(output & 1)) {
        val = (val & 0xFF00) | input;
    } else {
        val = (val & 0x00FF) | (input << 8);
    }
    *xbar = val;
#else
    // does not work, seems 8 bit access is not allowed
    volatile uint8_t *xbar = (volatile uint8_t *)XBARA1_SEL0;
    xbar[output] = input;
#endif
}

void print_debug_information()
{
#ifdef PRINT_DEBUG_INFO
  // Lets again try dumping lots of data.
  Serial.println("\n*** DMA structures for ADC_0 ***");
  dumpDMA_structures(&(abdma1._dmachannel_adc));
  dumpDMA_structures(&(abdma1._dmasettings_adc[0]));
  dumpDMA_structures(&(abdma1._dmasettings_adc[1]));
  Serial.println("\n*** DMA structures for ADC_1 ***");
  dumpDMA_structures(&(abdma2._dmachannel_adc));
  dumpDMA_structures(&(abdma2._dmasettings_adc[0]));
  dumpDMA_structures(&(abdma2._dmasettings_adc[1]));

#if defined(__IMXRT1062__)

  Serial.println("\n*** ADC and ADC_ETC ***");
  Serial.printf("ADC1: HC0:%x HS:%x CFG:%x GC:%x GS:%x\n", IMXRT_ADC1.HC0, IMXRT_ADC1.HS,  IMXRT_ADC1.CFG, IMXRT_ADC1.GC, IMXRT_ADC1.GS);
  Serial.printf("ADC2: HC0:%x HS:%x CFG:%x GC:%x GS:%x\n", IMXRT_ADC2.HC0, IMXRT_ADC2.HS,  IMXRT_ADC2.CFG, IMXRT_ADC2.GC, IMXRT_ADC2.GS);
  Serial.printf("ADC_ETC: CTRL:%x DONE0_1:%x DONE2_ERR:%x DMA: %x\n", IMXRT_ADC_ETC.CTRL,
                IMXRT_ADC_ETC.DONE0_1_IRQ, IMXRT_ADC_ETC.DONE2_ERR_IRQ, IMXRT_ADC_ETC.DMA_CTRL);
  for (uint8_t trig = 0; trig < 8; trig++) {
    Serial.printf("    TRIG[%d] CTRL: %x CHAIN_1_0:%x\n",
                  trig, IMXRT_ADC_ETC.TRIG[trig].CTRL, IMXRT_ADC_ETC.TRIG[trig].CHAIN_1_0);
  }
#endif
#endif
}

#ifdef PRINT_DEBUG_INFO
void dumpDMA_structures(DMABaseClass *dmabc)
{
  Serial.printf("%x %x:", (uint32_t)dmabc, (uint32_t)dmabc->TCD);

  Serial.printf("SA:%x SO:%d AT:%x NB:%x SL:%d DA:%x DO: %d CI:%x DL:%x CS:%x BI:%x\n", (uint32_t)dmabc->TCD->SADDR,
                dmabc->TCD->SOFF, dmabc->TCD->ATTR, dmabc->TCD->NBYTES, dmabc->TCD->SLAST, (uint32_t)dmabc->TCD->DADDR,
                dmabc->TCD->DOFF, dmabc->TCD->CITER, dmabc->TCD->DLASTSGA, dmabc->TCD->CSR, dmabc->TCD->BITER);
}
#endif

#else // make sure the example can run for any boards (automated testing)
void setup() {}
void loop() {}
#endif // ADC_USE_TIMER and DMA
 
The teensy pin number definitions are not the same as the crossbar pins. Not every physical pin is suitable for a crossbar output. Pin #4 is, but it's XBAR_INOUT08.
You will have to set the pad mux for pin #4 to XBAR1 (ALT3), calling pinMode() is not enough because that sets the pad mux to GPIO.
 
Thanks jmarsh! I can switch this to XBAR_INOUT08, but I am strugglig to find the registers to set the pad to XBAR1 - any pointers?
 
Back
Top