Read all analog inputs periodically in the background

frankzappa

Well-known member
Hello!

What is the preferred method to read all 10 analog sensors on a teensy 4 continuously while allowing for the loop to run without any waiting for the ADC's?

I've been doing this which allows for processing on the previous sensor pair while waiting for sensors to complete.

Code:
void loop()
{
  adc->startSynchronizedSingleRead(A1, A3);
 
  //do processing on previous sensor pair
  ConditionAndStoreSignals(headSensor[0].GetRawValue(), 0);
  ConditionAndStoreSignals(headSensor[1].GetRawValue(), 1);
 
  while (adc->adc0->isConverting() || adc->adc1->isConverting()); //wait for adc
  headSensor[2].ReadValue( (adc->adc0->readSingle()));
  headSensor[3].ReadValue( (adc->adc1->readSingle()));
  adc->startSynchronizedSingleRead(A4, A6);
 
  //do processing on previous sensor pair
  ConditionAndStoreSignals(headSensor[2].GetRawValue(), 2);
  ConditionAndStoreSignals(headSensor[3].GetRawValue(), 3);

 //and so on until I loop through all sensors.

It works good, I can get about 33kHz per sensor which is adequate but I get a bit of jittering which is not optimal. I'm wondering if this can be set up to run in the background somehow, maybe using DMA and interrupt timers? I have searhed the forum and can't find anything about switching between sensors this way to read all of them without interfering with the processing. Is this possible?
 
Last edited:
I've tried to get stuff working with chatGPT but haven't been successful. I would like to be able to get all 10 sensors read once and then I should be able to do work on those 10 readings when they are available. I need minimal latency. Of course a few cycles of latency is not important but preferrably keep it very low.
 
Sorry it has been a very long time since I did anything with the ADC code. You might be able to do something like that
using DMA and maybe chaining the ADC reads to each other... I do not remember any of the details, but I believe
that some of the support is in the ADC_ETC subsystem (Chapter 67).

You might try searching the forum for ADC_ETC. I know we were playing with examples where we might read from multiple Analog pins.
 
Sorry it has been a very long time since I did anything with the ADC code. You might be able to do something like that
using DMA and maybe chaining the ADC reads to each other... I do not remember any of the details, but I believe
that some of the support is in the ADC_ETC subsystem (Chapter 67).

You might try searching the forum for ADC_ETC. I know we were playing with examples where we might read from multiple Analog pins.
Thanks for the reply. It's exciting to hear that is could at least be possible to do this. :D I will start by searching for it on the forum, thanks.
 
Sorry it has been a very long time since I did anything with the ADC code. You might be able to do something like that
using DMA and maybe chaining the ADC reads to each other... I do not remember any of the details, but I believe
that some of the support is in the ADC_ETC subsystem (Chapter 67).

You might try searching the forum for ADC_ETC. I know we were playing with examples where we might read from multiple Analog pins.
I've been doing some research and here is my setup so far for the ADC_ETC to chain 10 ADC conversions and use DMA to write them to memory. I have not tested this yet, was thinking to use a timer to trigger these chains. I'm assuming that the ADC_ETC result registers (TRIG[n].RESULT_1_0, TRIG[n].RESULT_3_2, etc.) are laid out sequentially in memory for each trigger and that the DMA will read 10 consecutive 16-bit values starting from TRIG[0].RESULT_1_0, which will include all the results from TRIG[0] and TRIG[1]. Not sure if this is correct. Do I need to set up DMA further or will this suffice? I haven't figured out how to test this if it works yet. I guess I can try slowing the frequency down to like 1 second intervals and print out the result.

Code:
void setupADC_ETC_DMA() {
    // Enable ADC_ETC and configure it globally
    IMXRT_ADC_ETC.CTRL =
        (1     << 0   ) |  // Enable TRIG0 for XBAR triggering
        (1     << 1   ) |  // Enable TRIG1 for XBAR triggering
        (0     << 8   ) |  // Disable EXT0_TRIG (TSC0) if not used
        (0     << 12 ) |  // Disable EXT1_TRIG (TSC1) if not used
        (0x0 << 16 ) |  // Set PRE_DIVIDER to default (no division)
        (1     << 29 ) |  // Use pulsed DMA_REQ signal mode
        (0     << 30 );   // Ensure TSC is not bypassing ADC2

    // Configure TRIG0_CTRL: Set chain length, enable SYNC_MODE, and set priority
    IMXRT_ADC_ETC.TRIG[0].CTRL =
        (7 << 8) |  // Set TRIG_CHAIN to 7 (for 8 conversions)
        (7 << 12) |  // Set TRIG_PRIORITY to 7 (highest priority, if needed)
        (1 << 16);   // Enable SYNC_MODE for synchronized ADC operation

    // Configure TRIG1_CTRL: Set chain length, enable SYNC_MODE, and set priority
    IMXRT_ADC_ETC.TRIG[1].CTRL =
        (1 << 8) |  // Set TRIG_CHAIN to 1 (for 2 conversions)
        (6 << 12) |  // Set TRIG_PRIORITY to 6 (lower than TRIG0 if needed)
        (1 << 16);   // Enable SYNC_MODE for synchronized ADC operation

    // Define the chain for TRIG[0] to read from ADC0 and ADC1, covering sensors 0-7
    IMXRT_ADC_ETC.TRIG[0].CHAIN_1_0 =
        (0 << 0) |  // CSEL0: ADC0, Channel 0 (A0)
        (0x1 << 4) |  // HWTS0: Hardware trigger source for ADC0, Channel 0 (8 bits)
        (1 << 12) |  // B2B0: Enable back-to-back conversion for the next channel
        (0 << 13) |  // IE0: No interrupt when finished
        (1 << 16) |  // CSEL1: ADC1, Channel 0 (A1)
        (0x2 << 20) |  // HWTS1: Hardware trigger source for ADC1, Channel 0 (8 bits)
        (1 << 28) |  // B2B1: Enable back-to-back conversion for the next channel
        (0 << 29);   // IE1: No interrupt when finished

    // Continue the chain for the remaining sensors in TRIG[0]
    IMXRT_ADC_ETC.TRIG[0].CHAIN_3_2 =
        (2 << 0) |  // CSEL2: ADC0, Channel 2 (A2)
        (0x3 << 4) |  // HWTS2: Hardware trigger source for ADC0, Channel 2 (8 bits)
        (1 << 12) |  // B2B2: Enable back-to-back conversion for the next channel
        (0 << 13) |  // IE2: No interrupt when finished
        (3 << 16) |  // CSEL3: ADC1, Channel 2 (A3)
        (0x4 << 20) |  // HWTS3: Hardware trigger source for ADC1, Channel 2 (8 bits)
        (1 << 28) |  // B2B3: Enable back-to-back conversion for the next channel
        (0 << 29);   // IE3: No interrupt when finished

    IMXRT_ADC_ETC.TRIG[0].CHAIN_5_4 =
        (4 << 0) |  // CSEL4: ADC0, Channel 4 (A4)
        (0x5 << 4) |  // HWTS4: Hardware trigger source for ADC0, Channel 4 (8 bits)
        (1 << 12) |  // B2B4: Enable back-to-back conversion for the next channel
        (0 << 13) |  // IE4: No interrupt when finished
        (5 << 16) |  // CSEL5: ADC1, Channel 4 (A5)
        (0x6 << 20) |  // HWTS5: Hardware trigger source for ADC1, Channel 4 (8 bits)
        (1 << 28) |  // B2B5: Enable back-to-back conversion for the next channel
        (0 << 29);   // IE5: No interrupt when finished

    IMXRT_ADC_ETC.TRIG[0].CHAIN_7_6 =
        (6 << 0) |  // CSEL6: ADC0, Channel 6 (A6)
        (0x7 << 4) |  // HWTS6: Hardware trigger source for ADC0, Channel 6 (8 bits)
        (1 << 12) |  // B2B6: Enable back-to-back conversion for the next channel
        (0 << 13) |  // IE6: No interrupt when finished
        (7 << 16) |  // CSEL7: ADC1, Channel 6 (A7)
        (0x8 << 20) |  // HWTS7: Hardware trigger source for ADC1, Channel 6 (8 bits)
        (1 << 28) |  // B2B7: Enable back-to-back conversion for the next channel
        (0 << 29);   // IE7: No interrupt when finished

    // Set up the last sensors in TRIG[1]
    IMXRT_ADC_ETC.TRIG[1].CHAIN_1_0 =
        (8 << 0) |  // CSEL0: ADC0, Channel 8 (A8)
        (0x9 << 4) |  // HWTS0: Hardware trigger source for ADC0, Channel 8 (8 bits)
        (1 << 12) |  // B2B0: Enable back-to-back conversion for the next channel
        (0 << 13) |  // IE0: No interrupt when finished
        (9 << 16) |  // CSEL1: ADC1, Channel 9 (A9)
        (0xA << 20) |  // HWTS1: Hardware trigger source for ADC1, Channel 9 (8 bits)
        (1 << 28) |  // B2B1: Enable back-to-back conversion for the next channel
        (0 << 29);   // IE1: No interrupt when finished

    // Enable DMA requests for TRIG0 and TRIG1
    IMXRT_ADC_ETC.DMA_CTRL |= (1 << 0) |  // Enable DMA request for TRIG0
        (1 << 1);   // Enable DMA request for TRIG1

    // Setup DMA to handle ADC results automatically
    DMAChannel dmaChannel;
    static volatile uint16_t adcBuffer[10];  // Buffer to store 10 ADC results

    // Configure DMA to transfer data from all relevant result registers
    dmaChannel.source(IMXRT_ADC_ETC.TRIG[0].RESULT_1_0);   // Start with first result register
    dmaChannel.destination(adcBuffer);                     // Destination buffer in memory
    dmaChannel.transferSize(16);                           // 16-bit transfers
    dmaChannel.transferCount(10);                          // Transfer all 10 results

    dmaChannel.triggerAtHardwareEvent(DMAMUX_SOURCE_ADC_ETC);  // Trigger DMA on ADC_ETC completion
    dmaChannel.enable();                                   // Enable the DMA channel
}
 
Ok that was completely wrong, here is a simple test to start converting with the ADC_ETC with just a single chain and print out the result register. It sets up but I'm getting zero's from the sensors.

I'm not sure if the pin mappings are correct but in any way it should output some random number.

Code:
#include <ADC.h>
ADC* adc = new ADC();
void adc_etc_reset() {
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_SOFTRST; // Perform a soft reset
    IMXRT_ADC_ETC.CTRL &= ~ADC_ETC_CTRL_SOFTRST; // Clear the reset bit
    delay(5);
}
void setup_adc_etc() {
    adc_etc_reset();
    // Enable TRIG0 in ADC_ETC
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_TRIG_ENABLE(0x01);  // Enable only TRIG0
    // Set up TRIG0 to use a chain length of 2 (to sample two sensors)
    IMXRT_ADC_ETC.TRIG[0].CTRL = ADC_ETC_TRIG_CTRL_TRIG_CHAIN(1); // Chain length 2 (0 and 1)
    // Define the chains for TRIG0 (A0 and A1)
    IMXRT_ADC_ETC.TRIG[0].CHAIN_1_0 =
        ADC_ETC_TRIG_CHAIN_CSEL0(7) | ADC_ETC_TRIG_CHAIN_B2B0 |  // A0 (ADC0)
        ADC_ETC_TRIG_CHAIN_CSEL1(8) | ADC_ETC_TRIG_CHAIN_B2B1;   // A1 (ADC1)
}
void setup_adc() {
    
    // Configure ADC0
    adc->adc0->setAveraging(4);
    adc->adc0->setResolution(12);
    adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
    adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
    // Configure ADC1
    adc->adc1->setAveraging(4);
    adc->adc1->setResolution(12);
    adc->adc1->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
    adc->adc1->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
    delay(100);
    // Set ADC hardware trigger mode
    ADC1_CFG |= ADC_CFG_ADTRG;  // Enable hardware trigger on ADC1
    ADC1_HC0 = 16;  // Select ADC_ETC as the hardware trigger source
    ADC2_CFG |= ADC_CFG_ADTRG;  // Enable hardware trigger on ADC2
    ADC2_HC0 = 16;  // Select ADC_ETC as the hardware trigger source
}
void trigger_adc_etc() {
    IMXRT_ADC_ETC.TRIG[0].CTRL |= ADC_ETC_TRIG_CTRL_SW_TRIG;  // Trigger TRIG0
}
void print_adc_results() {
    Serial.println("Reading ADC Results directly from memory:");
    // Extract and print the results from TRIG0
    uint32_t result1_0 = IMXRT_ADC_ETC.TRIG[0].RESULT_1_0;
    Serial.print("TRIG0 CHAIN_0 Result: ");
    Serial.println(result1_0 & 0xFFF);  // Extract and print lower 12 bits (result for chain 0)
    Serial.print("TRIG0 CHAIN_1 Result: ");
    Serial.println((result1_0 >> 16) & 0xFFF);  // Extract and print upper 12 bits (result for chain 1)
}
void setup() {
    delay(100);  // Ensure ADC_ETC setup completes
    Serial.begin(9600);  // Initialize Serial for output
    setup_adc();
    setup_adc_etc();
    Serial.println("Setup complete");
}
void loop() {
    trigger_adc_etc();  // Trigger the ADC conversions
    delay(1000);  // Wait for ADC conversions to complete
    print_adc_results();  // Print the results from the ADC
    delay(1000);  // Additional delay before the next loop
}
 
Last edited:
Sorry it has been years since I played with any of this stuff.
@mjs513 and I did some of the initial stuff to add support to the ADC library for Teensy 4.x. We had our own library back then:
https://github.com/KurtE/ADCL_t4 which I have since then archived it out.

At the time we were bouncing back and forth some sketches for testing out ADC features. I included one here that used ADC_ETC.
But I remember very very little about all of this stuff. Again much of it from 5 years ago. Things you have to remember in a lot of these
sub-system, is that they don't use the Teensy pin number, but instead you need to map the pin number to the hardware channel or the like.

I don't remember how much of these multiple pin functions were migrated into the ADC library.

Sorry maybe not much help, but might give you some clues.
 
Sorry it has been years since I played with any of this stuff.
@mjs513 and I did some of the initial stuff to add support to the ADC library for Teensy 4.x. We had our own library back then:
https://github.com/KurtE/ADCL_t4 which I have since then archived it out.

At the time we were bouncing back and forth some sketches for testing out ADC features. I included one here that used ADC_ETC.
But I remember very very little about all of this stuff. Again much of it from 5 years ago. Things you have to remember in a lot of these
sub-system, is that they don't use the Teensy pin number, but instead you need to map the pin number to the hardware channel or the like.

I don't remember how much of these multiple pin functions were migrated into the ADC library.

Sorry maybe not much help, but might give you some clues.
You are more helpful than you realise. I will look through the library you linked to. Thank you.

It seems the ADC_ETC is not executing properly in my code, it’s supposed to set a flag when it’s triggering the ADC which it doesn’t.

I found this in the datasheet

67.4 Initialization
Before using the ADC_ETC module, the following initialization procedure must be performed:
1. Configure the Global Control bits, include selecting DMA mode, enabling triggers which will be used, and setting pre-division factor.
2. Set DMA enable bits as needed.
3. Configure control bits for each enabled trigger, include enabling or disabling
synchronization mode, assigning trigger priority, setting the length of trigger chain,
selecting trigger mode and setting the initial and interval delay of this trigger.
4. According to the length of trigger chain, several segments of the chain should be
configured. The configurations of each segment contain:
a. setting the DONE interrupt for this chain, enabling or disabling B2B mode; b. selecting which trigger of ADC will be triggered, setting the ADC command.
After the initialization of ADC_ETC, the initialization of the ADC module should also be confirmed before launching a trigger to ADC_ETC.

I wonder if I’m missing something obvious. Maybe the order of initialisation is wrong. ADC first then ADC_ETC. Perhaps I should try the other way around. I will have to continue tomorrow.
 
I'm pulling my hair with this. It seems like it would be very simple to do but I just can't figure it out.

Can someone run this code and see if they can help me figure out why the ADC is not being triggered by the ADC_ETC?

Code:
#include <Arduino.h>
#include <ADC.h>

ADC *adc = new ADC();

void setupADC() {
    adc->adc0->setAveraging(4);
    adc->adc0->setResolution(12);
    adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::HIGH_SPEED);
    adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::HIGH_SPEED);
    ADC1_CFG |= ADC_CFG_ADTRG;  // Enable hardware trigger on ADC1
    ADC1_HC0 = 16;              // Select ADC_ETC as the hardware trigger source
    Serial.print("ADC1_CFG: 0x");
    Serial.println(ADC1_CFG, HEX);
    Serial.print("ADC1_HC0: 0x");
    Serial.println(ADC1_HC0, HEX);
}

void setupADC_ETC() {
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_SOFTRST;
    delay(5);
    IMXRT_ADC_ETC.CTRL &= ~ADC_ETC_CTRL_SOFTRST;
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_TRIG_ENABLE(0x01);
    IMXRT_ADC_ETC.TRIG[0].CTRL = ADC_ETC_TRIG_CTRL_TRIG_CHAIN(0) | ADC_ETC_TRIG_CTRL_SYNC_MODE;
    IMXRT_ADC_ETC.TRIG[0].CHAIN_1_0 =
        ADC_ETC_TRIG_CHAIN_CSEL0(7) | ADC_ETC_TRIG_CHAIN_B2B0 | ADC_ETC_TRIG_CHAIN_IE0(1);
    Serial.print("ADC_ETC_CTRL: 0x");
    Serial.println(IMXRT_ADC_ETC.CTRL, HEX);
    Serial.print("TRIG0_CTRL: 0x");
    Serial.println(IMXRT_ADC_ETC.TRIG[0].CTRL, HEX);
    Serial.print("TRIG0_CHAIN_1_0: 0x");
    Serial.println(IMXRT_ADC_ETC.TRIG[0].CHAIN_1_0, HEX);
}

void triggerADC_ETC() {
    Serial.println("Triggering ADC_ETC...");
    Serial.print("Before Trigger - TRIG0_CTRL: 0x");
    Serial.println(IMXRT_ADC_ETC.TRIG[0].CTRL, HEX);

    IMXRT_ADC_ETC.TRIG[0].CTRL |= ADC_ETC_TRIG_CTRL_SW_TRIG;

    delayMicroseconds(100);  // Increased delay to allow time for conversion to complete

    Serial.print("After Trigger - TRIG0_CTRL: 0x");
    Serial.println(IMXRT_ADC_ETC.TRIG[0].CTRL, HEX);
}

void readADC_ETC_Result() {
    // Check if the chain 0 has completed (31st bit in CHAIN_1_0)
    if (!(IMXRT_ADC_ETC.TRIG[0].CHAIN_1_0 & (1 << 31))) {
        Serial.println("TRIG chain not completed for CHAIN_0");
        return;
    }

    // Read the result from the ADC
    uint32_t result0 = IMXRT_ADC_ETC.TRIG[0].RESULT_1_0 & 0xFFF;
    Serial.print("ADC_ETC Conversion Result: ");
    Serial.println(result0);
}

void setup() {
    delay(5000);

    setupADC();
    setupADC_ETC();

    Serial.println("Setup complete. Triggering ADC_ETC.");
}

void loop() {
    triggerADC_ETC();
    delay(1000);
    readADC_ETC_Result();
    delay(1000);
}
 
It seems doable if you know what you are doing which I don’t: Set up the ADC_ETC to trigger a chain of ADC conversions. Synchronised mode, first chain is 8 long and select back to back conversions. Second chain is 2 long. ADC must be set to trigger from hardware. Control ADC_ETC from a timer via XBAR and access the result registers with DMA. This is my superficial understanding.
 
I assume you looked back at the main ADC forum thread, that talked about some of this a few years ago.
Like some of the posts near:

Yes, I'm trying to figure it out. It seems to do what I want. The only thing is it's up to 8 pins so I would have to add an extra trig chain with chain length of 2. But I haveto get the first chain to work first.
Code:
IMXRT_ADC_ETC.TRIG[0].CHAIN_1_0
IMXRT_ADC_ETC.TRIG[0].CHAIN_3_2
IMXRT_ADC_ETC.TRIG[0].CHAIN_5_4
IMXRT_ADC_ETC.TRIG[0].CHAIN_7_6
IMXRT_ADC_ETC.TRIG[1].CHAIN_1_0
 
@KurtE I managed to get this working sort of. This has been a nightmare. Here is my code. I implemented two ADC_ETC chains of length 5 and put adc 1 on trig 0 and adc 2 on trig 4. Used sync mode so that I can trigger adc 1 and adc 2 will follow it. I use a clock to trigger via xbar and use DMA to store the data in two buffers. Problem is I'm occasionally getting the wrong values. I'm assuming that this needs to be synched so that I'm not accessing the buffer that is currently being written to which I believe is what's happening. It seems to work at high frequencies although I only tested up to 30kHz. Only problem is data seems to get out of sync.

Any advice how to set up double buffers so that I always have access to the latest 10 sensor values but not risk them being overwriteen or accessing the buffer currently being written to? Maybe a better approach is recommended like some type of circular buffer approach?

Here is the code so far:

Code:
#include "Arduino.h"
#include "ADC.h"
#include <DMAChannel.h>

// Pin array for 10 sensors
int pins[] = {A0, A1, A2, A3, A4, A5, A6, A7, A8, A9};

// Global array to store ADC results for 10 sensors
volatile uint16_t adc_results[10];
volatile bool adc_conversion_complete = false;
const float frequency = 20000.0f;

elapsedMillis printTimer = 0; //timer for printing results less frequently

// DMA buffers for double-buffering ADC results
DMAMEM __attribute__((aligned(32))) static uint16_t adc_buffer_0[10];
DMAMEM __attribute__((aligned(32))) static uint16_t adc_buffer_1[10];
static volatile bool buffer_ready = false;

// DMA channel for ADC_ETC
DMAChannel dma;

// Forward declarations
void adc_init();
void adc_etc_reset();
void adc_etc_setup();
void setup_timer_and_xbar(const float frequency);
void start_timer();

extern "C" {
    extern void xbar_connect(unsigned int input, unsigned int output);
}

void setup() {
    while (!Serial && millis() < 5000);
    Serial.println("Setup ADC");

    // ADC settings
    ADC *adc = new ADC();
    adc->adc0->setAveraging(4);
    adc->adc0->setResolution(12);
    adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
    adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
    adc->adc0->singleMode();

    adc->adc1->setAveraging(4);
    adc->adc1->setResolution(12);
    adc->adc1->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
    adc->adc1->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
    adc->adc1->singleMode();

    adc_init();
    adc_etc_reset();
    adc_etc_setup();
    setup_timer_and_xbar(frequency);
    start_timer();
}

void loop() {
    static uint32_t i = 0;
    if (buffer_ready && printTimer > 1000) {
        buffer_ready = false;
        adc_conversion_complete = false;

        // Process the ADC results from the completed buffer
        uint16_t* active_buffer = (dma.TCD->DADDR == adc_buffer_0) ? adc_buffer_1 : adc_buffer_0;

        for (int i = 0; i < 10; i++) {
            Serial.printf("ADC Result[%d]: %d\n", i, active_buffer[i]);
        }

        // Additional processing can be added here
        delay(1000);  // Delay for demonstration; adjust as needed
    }
}

void adc_init() {
    ADC1_CFG |= ADC_CFG_ADTRG; // Hardware trigger
    ADC1_HC0 = 16; // ADC_ETC channel, 144 int enabled, 16 int disabled
    ADC1_GC &= ~ADC_GC_ADCO; // Continuous conversion disabled, same as adc->adcX->singleMode()

    ADC2_CFG |= ADC_CFG_ADTRG; // Hardware trigger
    ADC2_HC0 = 16; // ADC_ETC channel
    ADC2_GC &= ~ADC_GC_ADCO; // Continuous conversion disabled
}

void adc_etc_reset() {
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_SOFTRST; // SOFTRST
    IMXRT_ADC_ETC.CTRL &= ~ADC_ETC_CTRL_SOFTRST; // Clear SOFTRST
    delay(5);
}

void adc_etc_setup() {
    const uint8_t chainLength0 = 5;  // Chain length for the first 5 sensors (TRIG[0])
    const uint8_t chainLength4 = 5;  // Chain length for the last 5 sensors (TRIG[4])
    const uint8_t trigger_adc_0 = 0; // TRIG[0] for ADC1
    const uint8_t trigger_adc_4 = 4; // TRIG[4] for ADC2
    const uint8_t priority0 = 1;     // Higher priority for TRIG[0]
    const uint8_t priority4 = 2;     // Lower priority for TRIG[4]

    // Hard-coded pin assignments for sensors
    int adc_0_pinArray[5] = { A0, A1, A2, A3, A4 };  // Pins for ADC1
    int adc_1_pinArray[5] = { A5, A6, A7, A8, A9 };  // Pins for ADC2

    uint8_t adc_pin_channel_0[5];
    uint8_t adc_pin_channel_1[5];

    // Map each pin to its ADC channel
    for (int i = 0; i < 5; i++) {
        adc_pin_channel_0[i] = ADC::channel2sc1aADC0[adc_0_pinArray[i]];
        adc_pin_channel_1[i] = ADC::channel2sc1aADC1[adc_1_pinArray[i]];
    }

    // Enable triggers for TRIG[0] and TRIG[4], and enable DMA mode
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_TRIG_ENABLE((1 << trigger_adc_0) | (1 << trigger_adc_4)) | ADC_ETC_CTRL_DMA_MODE_SEL;

    // Configure TRIG[0] for ADC1
    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CTRL = ADC_ETC_TRIG_CTRL_TRIG_CHAIN(chainLength0 - 1) | ADC_ETC_TRIG_CTRL_SYNC_MODE;

    // Set up each chain in TRIG[0] with DMA-enabled configuration
    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_1_0 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[0]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_0[1]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_3_2 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[2]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_0[3]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_5_4 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(1) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[4]);

    // Configure TRIG[4] for ADC2
    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CTRL = ADC_ETC_TRIG_CTRL_TRIG_CHAIN(chainLength4 - 1) | ADC_ETC_TRIG_CTRL_SYNC_MODE;

    // Set up each chain in TRIG[4] with DMA-enabled configuration
    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_1_0 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[0]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_1[1]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_3_2 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[2]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_1[3]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_5_4 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[4]);

    // Enable the DMA for the trigger events
    IMXRT_ADC_ETC.DMA_CTRL = ADC_ETC_DMA_CTRL_TRIQ_ENABLE(trigger_adc_0) | ADC_ETC_DMA_CTRL_TRIQ_ENABLE(trigger_adc_4);

    // DMA configuration to handle double buffering
    dma.begin(true);
    dma.TCD->SADDR = &IMXRT_ADC_ETC.TRIG[trigger_adc_0].RESULT_1_0; // Source address
    dma.TCD->SOFF = 0;
    dma.TCD->ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // Source and destination size
    dma.TCD->NBYTES_MLNO = 2; // Number of bytes transferred per DMA request
    dma.TCD->SLAST = 0;
    dma.TCD->DADDR = adc_buffer_0; // Destination address (initial buffer)
    dma.TCD->DOFF = 2; // Destination offset
    dma.TCD->CITER_ELINKNO = 10; // 10 sensors per trigger
    dma.TCD->DLASTSGA = -sizeof(adc_buffer_0); // Toggle between buffers
    dma.TCD->BITER_ELINKNO = 10;
    dma.TCD->CSR = DMA_TCD_CSR_INTMAJOR; // Interrupt when buffer is full
    dma.triggerAtHardwareEvent(DMAMUX_SOURCE_ADC_ETC); // Trigger DMA at ADC_ETC event
    dma.attachInterrupt([]() { // DMA interrupt handler
        buffer_ready = true;  // Set buffer ready flag when DMA completes
        dma.clearInterrupt();
    });
    dma.enable();
}



void setup_timer_and_xbar(const float frequency) {
    CCM_CCGR2 |= CCM_CCGR2_XBAR1(CCM_CCGR_ON); // Enable clock for XBAR

    // Connect TMR4 Timer output to ADC_ETC Trigger input
    xbar_connect(XBARA1_IN_QTIMER4_TIMER0, XBARA1_OUT_ADC_ETC_TRIG00); // Connect Timer to trigger TRIG[0]
}

void start_timer() {
    // Configure TMR4 Timer directly
    TMR4_ENBL &= ~(1 << 0); // Disable Timer
    TMR4_SCTRL0 = TMR_SCTRL_OEN | TMR_SCTRL_FORCE;
    TMR4_CSCTRL0 = TMR_CSCTRL_CL1(1) | TMR_CSCTRL_TCF1EN;
    TMR4_CNTR0 = 0;
    TMR4_LOAD0 = 0;
    TMR4_COMP10 = (float)F_BUS_ACTUAL / frequency / 2; // Set compare value based on desired frequency
    TMR4_CMPLD10 = (float)F_BUS_ACTUAL / frequency / 2; // Set compare value
    TMR4_CTRL0 = TMR_CTRL_CM(1) | TMR_CTRL_PCS(8) | TMR_CTRL_LENGTH | TMR_CTRL_OUTMODE(3); // Timer configuration
    TMR4_ENBL |= (1 << 0); // Enable Timer
}
 
@KurtE I managed to get this working sort of. This has been a nightmare. Here is my code. I implemented two ADC_ETC chains of length 5 and put adc 1 on trig 0 and adc 2 on trig 4. Used sync mode so that I can trigger adc 1 and adc 2 will follow it. I use a clock to trigger via xbar and use DMA to store the data in two buffers. Problem is I'm occasionally getting the wrong values. I'm assuming that this needs to be synched so that I'm not accessing the buffer that is currently being written to which I believe is what's happening. It seems to work at high frequencies although I only tested up to 30kHz. Only problem is data seems to get out of sync.

Any advice how to set up double buffers so that I always have access to the latest 10 sensor values but not risk them being overwriteen or accessing the buffer currently being written to? Maybe a better approach is recommended like some type of circular buffer approach?

Here is the code so far:

Code:
#include "Arduino.h"
#include "ADC.h"
#include <DMAChannel.h>

// Pin array for 10 sensors
int pins[] = {A0, A1, A2, A3, A4, A5, A6, A7, A8, A9};

// Global array to store ADC results for 10 sensors
volatile uint16_t adc_results[10];
volatile bool adc_conversion_complete = false;
const float frequency = 20000.0f;

elapsedMillis printTimer = 0; //timer for printing results less frequently

// DMA buffers for double-buffering ADC results
DMAMEM __attribute__((aligned(32))) static uint16_t adc_buffer_0[10];
DMAMEM __attribute__((aligned(32))) static uint16_t adc_buffer_1[10];
static volatile bool buffer_ready = false;

// DMA channel for ADC_ETC
DMAChannel dma;

// Forward declarations
void adc_init();
void adc_etc_reset();
void adc_etc_setup();
void setup_timer_and_xbar(const float frequency);
void start_timer();

extern "C" {
    extern void xbar_connect(unsigned int input, unsigned int output);
}

void setup() {
    while (!Serial && millis() < 5000);
    Serial.println("Setup ADC");

    // ADC settings
    ADC *adc = new ADC();
    adc->adc0->setAveraging(4);
    adc->adc0->setResolution(12);
    adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
    adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
    adc->adc0->singleMode();

    adc->adc1->setAveraging(4);
    adc->adc1->setResolution(12);
    adc->adc1->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
    adc->adc1->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
    adc->adc1->singleMode();

    adc_init();
    adc_etc_reset();
    adc_etc_setup();
    setup_timer_and_xbar(frequency);
    start_timer();
}

void loop() {
    static uint32_t i = 0;
    if (buffer_ready && printTimer > 1000) {
        buffer_ready = false;
        adc_conversion_complete = false;

        // Process the ADC results from the completed buffer
        uint16_t* active_buffer = (dma.TCD->DADDR == adc_buffer_0) ? adc_buffer_1 : adc_buffer_0;

        for (int i = 0; i < 10; i++) {
            Serial.printf("ADC Result[%d]: %d\n", i, active_buffer[i]);
        }

        // Additional processing can be added here
        delay(1000);  // Delay for demonstration; adjust as needed
    }
}

void adc_init() {
    ADC1_CFG |= ADC_CFG_ADTRG; // Hardware trigger
    ADC1_HC0 = 16; // ADC_ETC channel, 144 int enabled, 16 int disabled
    ADC1_GC &= ~ADC_GC_ADCO; // Continuous conversion disabled, same as adc->adcX->singleMode()

    ADC2_CFG |= ADC_CFG_ADTRG; // Hardware trigger
    ADC2_HC0 = 16; // ADC_ETC channel
    ADC2_GC &= ~ADC_GC_ADCO; // Continuous conversion disabled
}

void adc_etc_reset() {
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_SOFTRST; // SOFTRST
    IMXRT_ADC_ETC.CTRL &= ~ADC_ETC_CTRL_SOFTRST; // Clear SOFTRST
    delay(5);
}

void adc_etc_setup() {
    const uint8_t chainLength0 = 5;  // Chain length for the first 5 sensors (TRIG[0])
    const uint8_t chainLength4 = 5;  // Chain length for the last 5 sensors (TRIG[4])
    const uint8_t trigger_adc_0 = 0; // TRIG[0] for ADC1
    const uint8_t trigger_adc_4 = 4; // TRIG[4] for ADC2
    const uint8_t priority0 = 1;     // Higher priority for TRIG[0]
    const uint8_t priority4 = 2;     // Lower priority for TRIG[4]

    // Hard-coded pin assignments for sensors
    int adc_0_pinArray[5] = { A0, A1, A2, A3, A4 };  // Pins for ADC1
    int adc_1_pinArray[5] = { A5, A6, A7, A8, A9 };  // Pins for ADC2

    uint8_t adc_pin_channel_0[5];
    uint8_t adc_pin_channel_1[5];

    // Map each pin to its ADC channel
    for (int i = 0; i < 5; i++) {
        adc_pin_channel_0[i] = ADC::channel2sc1aADC0[adc_0_pinArray[i]];
        adc_pin_channel_1[i] = ADC::channel2sc1aADC1[adc_1_pinArray[i]];
    }

    // Enable triggers for TRIG[0] and TRIG[4], and enable DMA mode
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_TRIG_ENABLE((1 << trigger_adc_0) | (1 << trigger_adc_4)) | ADC_ETC_CTRL_DMA_MODE_SEL;

    // Configure TRIG[0] for ADC1
    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CTRL = ADC_ETC_TRIG_CTRL_TRIG_CHAIN(chainLength0 - 1) | ADC_ETC_TRIG_CTRL_SYNC_MODE;

    // Set up each chain in TRIG[0] with DMA-enabled configuration
    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_1_0 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[0]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_0[1]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_3_2 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[2]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_0[3]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_5_4 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(1) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[4]);

    // Configure TRIG[4] for ADC2
    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CTRL = ADC_ETC_TRIG_CTRL_TRIG_CHAIN(chainLength4 - 1) | ADC_ETC_TRIG_CTRL_SYNC_MODE;

    // Set up each chain in TRIG[4] with DMA-enabled configuration
    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_1_0 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[0]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_1[1]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_3_2 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[2]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_1[3]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_5_4 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[4]);

    // Enable the DMA for the trigger events
    IMXRT_ADC_ETC.DMA_CTRL = ADC_ETC_DMA_CTRL_TRIQ_ENABLE(trigger_adc_0) | ADC_ETC_DMA_CTRL_TRIQ_ENABLE(trigger_adc_4);

    // DMA configuration to handle double buffering
    dma.begin(true);
    dma.TCD->SADDR = &IMXRT_ADC_ETC.TRIG[trigger_adc_0].RESULT_1_0; // Source address
    dma.TCD->SOFF = 0;
    dma.TCD->ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // Source and destination size
    dma.TCD->NBYTES_MLNO = 2; // Number of bytes transferred per DMA request
    dma.TCD->SLAST = 0;
    dma.TCD->DADDR = adc_buffer_0; // Destination address (initial buffer)
    dma.TCD->DOFF = 2; // Destination offset
    dma.TCD->CITER_ELINKNO = 10; // 10 sensors per trigger
    dma.TCD->DLASTSGA = -sizeof(adc_buffer_0); // Toggle between buffers
    dma.TCD->BITER_ELINKNO = 10;
    dma.TCD->CSR = DMA_TCD_CSR_INTMAJOR; // Interrupt when buffer is full
    dma.triggerAtHardwareEvent(DMAMUX_SOURCE_ADC_ETC); // Trigger DMA at ADC_ETC event
    dma.attachInterrupt([]() { // DMA interrupt handler
        buffer_ready = true;  // Set buffer ready flag when DMA completes
        dma.clearInterrupt();
    });
    dma.enable();
}



void setup_timer_and_xbar(const float frequency) {
    CCM_CCGR2 |= CCM_CCGR2_XBAR1(CCM_CCGR_ON); // Enable clock for XBAR

    // Connect TMR4 Timer output to ADC_ETC Trigger input
    xbar_connect(XBARA1_IN_QTIMER4_TIMER0, XBARA1_OUT_ADC_ETC_TRIG00); // Connect Timer to trigger TRIG[0]
}

void start_timer() {
    // Configure TMR4 Timer directly
    TMR4_ENBL &= ~(1 << 0); // Disable Timer
    TMR4_SCTRL0 = TMR_SCTRL_OEN | TMR_SCTRL_FORCE;
    TMR4_CSCTRL0 = TMR_CSCTRL_CL1(1) | TMR_CSCTRL_TCF1EN;
    TMR4_CNTR0 = 0;
    TMR4_LOAD0 = 0;
    TMR4_COMP10 = (float)F_BUS_ACTUAL / frequency / 2; // Set compare value based on desired frequency
    TMR4_CMPLD10 = (float)F_BUS_ACTUAL / frequency / 2; // Set compare value
    TMR4_CTRL0 = TMR_CTRL_CM(1) | TMR_CTRL_PCS(8) | TMR_CTRL_LENGTH | TMR_CTRL_OUTMODE(3); // Timer configuration
    TMR4_ENBL |= (1 << 0); // Enable Timer
}
Again it has been a few years since I played with this stuff. I personally do very little with Analog. I was hoping someone who uses it might
be able to fill in the holes.

If i remember correctly, when I tried the chain to DMA, it interspersed the results in the buffer. Don't remember if I had issues with the results getting out of sync or not. The main thing I tried as a version of my probably never to be completed Well Monitor program. Where I have coils around the power lines going to 4 things. Well1, Well2, Pressure pump, and heater, to at a minimum log when each of them turn on or off, and potentially how much current they were taking...
So I tracked the 60hz power to determine the RMS... and logged when they were on or off... I had two analog pins per ADC...
Which was working, but I decide to keep it simpler...

I only process one of the two Analog pins per ADC at a time. I have an interval time I have setup. When the interrupt happens, I swap which of the two sets of pins I am using as well as the resulting buffers And then start up the ADCs. At one point I monitored when the count of reads completed, but later not as setup the timer such that it would always complete the reads before the next timer...

You could probably do something similar... That is you could setup your chains to interrupt you and maybe stop the reads when it completes and you could simply swap buffers and restart...

Or you could chain two DMA settings to each other, each the size of one full pass through the ADC. Have both of them interrupt you (interrupt on completion), where you can process the one set while the new set is being captured, or ...
 
I’m already swapping buffers with interrupts. I have two buffers but the problem is what if I’m reading from the buffer just before it’s about to switch the flag?

I think there is a function to prevent those scenarios but I have to find it. Something with three letters, (dsm) or something. Also I could disable interrupts while I copy the 10 values from the ”ready” dma buffer.

I’m thinking to add a third buffer and always make sure to read from the one in the middle. That way if the buffer I’m reading from is about to go stale as I’m reading it the new data will not be written to it but the buffer next to it. Having buffer a, b and c and swap them in a circular way. B would be the ready buffer, c would be the buffer that is next in line to be overwritten and a is the buffer currently being overwritten.
 
This seems to work. I used a third buffer, this ensures that it's impossible to access a buffer that is currently being overwritten unless you access it and linger on it for more than the time it takes to read all sensors in worst case, and twice the time in best case. This code can easily be adapted to read up to 16 sensors, as long as it's an even number of sensors. I have not yet thoroughly tested the code but it appears to produce accurate sensor readings. Here is the code if anyone finds it useful. This should be a standard feature in the teensy IMO.

Code:
#include "Arduino.h"
#include "ADC.h"
#include <DMAChannel.h>

// Pin array for 10 sensors
int pins[] = {A0, A1, A2, A3, A4, A5, A6, A7, A8, A9};

const float frequency = 40000.0f;

elapsedMillis printTimer = 0; // Timer for printing results less frequently

// DMA buffers for triple-buffering ADC results
DMAMEM __attribute__((aligned(32))) static uint16_t adc_buffer_A[10];
DMAMEM __attribute__((aligned(32))) static uint16_t adc_buffer_B[10];
DMAMEM __attribute__((aligned(32))) static uint16_t adc_buffer_C[10];
static volatile uint16_t* current_read_buffer = nullptr;  // Pointer to the buffer currently being read
static volatile uint16_t* next_write_buffer = adc_buffer_A;  // Pointer to the buffer currently being written by DMA
static volatile uint16_t* pending_buffer = nullptr;  // Pointer to the buffer that will be next in line to read

// DMA channel for ADC_ETC
DMAChannel dma;

// Forward declarations
void adc_init();
void adc_etc_reset();
void adc_etc_setup();
void setup_timer_and_xbar(const float frequency);
void start_timer();
void dma_isr();

extern "C" {
    extern void xbar_connect(unsigned int input, unsigned int output);
}

void setup() {
    while (!Serial && millis() < 5000);
    Serial.println("Setup ADC");

    // ADC settings
    ADC *adc = new ADC();
    adc->adc0->setAveraging(4);
    adc->adc0->setResolution(12);
    adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
    adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
    adc->adc0->singleMode();

    adc->adc1->setAveraging(4);
    adc->adc1->setResolution(12);
    adc->adc1->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
    adc->adc1->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
    adc->adc1->singleMode();

    adc_init();
    adc_etc_reset();
    adc_etc_setup();
    setup_timer_and_xbar(frequency);
    start_timer();
}

void loop() {
    static uint16_t local_buffer[10]; // Local buffer to hold the copied data

    // Check if a new buffer is ready and safe to read
    if (pending_buffer != nullptr) {
        __disable_irq(); // Disable interrupts to safely switch buffer pointers and copy data
        current_read_buffer = pending_buffer;  // Set the current buffer to the pending one
        pending_buffer = nullptr; // Clear the pending buffer pointer
      
        // Copy the data quickly to the local buffer
        memcpy(local_buffer, (void*)current_read_buffer, sizeof(local_buffer));
        __enable_irq(); // Re-enable interrupts immediately after copying

        // Process the ADC results from the safe-to-read local buffer
        for (int i = 0; i < 10; i++) {
            Serial.printf("ADC Result[%d]: %d\n", i, local_buffer[i]);
        }

        delay(1000);  // Delay for demonstration; adjust as needed
    }
}

void adc_init() {
    ADC1_CFG |= ADC_CFG_ADTRG; // Hardware trigger
    ADC1_HC0 = 16; // ADC_ETC channel, 144 int enabled, 16 int disabled
    ADC1_GC &= ~ADC_GC_ADCO; // Continuous conversion disabled, same as adc->adcX->singleMode()

    ADC2_CFG |= ADC_CFG_ADTRG; // Hardware trigger
    ADC2_HC0 = 16; // ADC_ETC channel
    ADC2_GC &= ~ADC_GC_ADCO; // Continuous conversion disabled
}

void adc_etc_reset() {
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_SOFTRST; // SOFTRST
    asm volatile("dsb"); // Ensure SOFTRST operation is completed
    IMXRT_ADC_ETC.CTRL &= ~ADC_ETC_CTRL_SOFTRST; // Clear SOFTRST
    delay(5);
}

void adc_etc_setup() {
    const uint8_t chainLength0 = 5;  // Chain length for the first 5 sensors (TRIG[0])
    const uint8_t chainLength4 = 5;  // Chain length for the last 5 sensors (TRIG[4])
    const uint8_t trigger_adc_0 = 0; // TRIG[0] for ADC1
    const uint8_t trigger_adc_4 = 4; // TRIG[4] for ADC2
    const uint8_t priority0 = 1;     // Higher priority for TRIG[0]
    const uint8_t priority4 = 2;     // Lower priority for TRIG[4]

    // Hard-coded pin assignments for sensors
    int adc_0_pinArray[5] = { A0, A1, A2, A3, A4 };  // Pins for ADC1
    int adc_1_pinArray[5] = { A5, A6, A7, A8, A9 };  // Pins for ADC2

    uint8_t adc_pin_channel_0[5];
    uint8_t adc_pin_channel_1[5];

    // Map each pin to its ADC channel
    for (int i = 0; i < 5; i++) {
        adc_pin_channel_0[i] = ADC::channel2sc1aADC0[adc_0_pinArray[i]];
        adc_pin_channel_1[i] = ADC::channel2sc1aADC1[adc_1_pinArray[i]];
    }

    // Enable triggers for TRIG[0] and TRIG[4], and enable DMA mode
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_TRIG_ENABLE((1 << trigger_adc_0) | (1 << trigger_adc_4)) | ADC_ETC_CTRL_DMA_MODE_SEL;

    // Configure TRIG[0] for ADC1
    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CTRL = ADC_ETC_TRIG_CTRL_TRIG_CHAIN(chainLength0 - 1) | ADC_ETC_TRIG_CTRL_SYNC_MODE;

    // Set up each chain in TRIG[0] with DMA-enabled configuration
    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_1_0 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[0]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_0[1]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_3_2 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[2]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_0[3]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_5_4 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(1) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[4]);

    // Configure TRIG[4] for ADC2
    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CTRL = ADC_ETC_TRIG_CTRL_TRIG_CHAIN(chainLength4 - 1) | ADC_ETC_TRIG_CTRL_SYNC_MODE;

    // Set up each chain in TRIG[4] with DMA-enabled configuration
    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_1_0 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[0]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_1[1]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_3_2 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[2]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_1[3]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_5_4 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[4]);

    // Enable the DMA for the trigger events
    IMXRT_ADC_ETC.DMA_CTRL = ADC_ETC_DMA_CTRL_TRIQ_ENABLE(trigger_adc_0) | ADC_ETC_DMA_CTRL_TRIQ_ENABLE(trigger_adc_4);

    // DMA configuration to handle triple buffering
    dma.begin(true);
    dma.TCD->SADDR = &IMXRT_ADC_ETC.TRIG[trigger_adc_0].RESULT_1_0; // Source address
    dma.TCD->SOFF = 0;
    dma.TCD->ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // Source and destination size
    dma.TCD->NBYTES_MLNO = 2; // Number of bytes transferred per DMA request
    dma.TCD->SLAST = 0;
    dma.TCD->DADDR = next_write_buffer; // Initial destination address (buffer A)
    dma.TCD->DOFF = 2; // Destination offset
    dma.TCD->CITER_ELINKNO = 10; // 10 sensors per trigger
    dma.TCD->DLASTSGA = -sizeof(adc_buffer_A); // Cycle through buffers
    dma.TCD->BITER_ELINKNO = 10;
    dma.TCD->CSR = DMA_TCD_CSR_INTMAJOR; // Interrupt when buffer is full
    dma.triggerAtHardwareEvent(DMAMUX_SOURCE_ADC_ETC); // Trigger DMA at ADC_ETC event
    dma.attachInterrupt(dma_isr); // Attach DMA interrupt handler
    dma.enable();
}

void dma_isr() {
    __disable_irq(); // Prevents other interrupts while handling DMA
    asm volatile("dsb"); // Ensure all previous instructions are complete
    dma.clearInterrupt(); // Clear the interrupt flag

    // Update the buffer pointers for triple buffering
    if (next_write_buffer == adc_buffer_A) {
        pending_buffer = adc_buffer_B;
        next_write_buffer = adc_buffer_C;
    } else if (next_write_buffer == adc_buffer_B) {
        pending_buffer = adc_buffer_C;
        next_write_buffer = adc_buffer_A;
    } else {
        pending_buffer = adc_buffer_A;
        next_write_buffer = adc_buffer_B;
    }

    dma.TCD->DADDR = next_write_buffer; // Update DMA destination address
    __enable_irq(); // Re-enable interrupts
}

void setup_timer_and_xbar(const float frequency) {
    CCM_CCGR2 |= CCM_CCGR2_XBAR1(CCM_CCGR_ON); // Enable clock for XBAR

    // Connect TMR4 Timer output to ADC_ETC Trigger input
    xbar_connect(XBARA1_IN_QTIMER4_TIMER0, XBARA1_OUT_ADC_ETC_TRIG00); // Connect Timer to trigger TRIG[0]
}

void start_timer() {
    // Configure TMR4 Timer directly
    TMR4_ENBL &= ~(1 << 0); // Disable Timer
    TMR4_SCTRL0 = TMR_SCTRL_OEN | TMR_SCTRL_FORCE;
    TMR4_CSCTRL0 = TMR_CSCTRL_CL1(1) | TMR_CSCTRL_TCF1EN;
    TMR4_CNTR0 = 0;
    TMR4_LOAD0 = 0;
    TMR4_COMP10 = (float)F_BUS_ACTUAL / frequency / 2; // Set compare value based on desired frequency
    TMR4_CMPLD10 = (float)F_BUS_ACTUAL / frequency / 2; // Set compare value
    TMR4_CTRL0 = TMR_CTRL_CM(1) | TMR_CTRL_PCS(8) | TMR_CTRL_LENGTH | TMR_CTRL_OUTMODE(3); // Timer configuration
    TMR4_ENBL |= (1 << 0); // Enable Timer

    // Debug output to verify timer setup
    Serial.printf("Timer started with calculated period: %lu ticks\n", (uint32_t)(F_BUS_ACTUAL / (frequency * 2)));
    Serial.flush();
}
 
Last edited:
Ok, here is a version that works completely autonomously in the background without interrupts and this should work to read up to 16 sensors at the same time. Uses eDMA features to switch between the three buffers and keep a variable to indicate which buffer was last filled. Seems to work but haven't thoroughly tested. This should allow the sensors to be read with no processor involvement what so ever and since there are three buffers it's always safe to access without risking getting values that are about to be overwritten.

Awesome!

I will have to test in my program to see if it works but at first glance it appears to be working.

Oh and I'm in PlatformIO but I think it should work in arduino anyway.

The previous code I posted here actually only gets the first 5 values from trig[0] so doesn't work properly. This one corrects that.

ADC_ETC is set to trig[0] and trig[4]. This is important because adc1 is in trig[0] and adc2 is in trig [4] and react to one trigger in sync mode. ADC1 has to be in trig 0-3 and acd2 needs to be in trig 4-6 for this to work because they are in different channels.

DMA switches the source buffer from trig[0} to trig [4] after 5 values have been read and the destination buffer is switched after 10 values have been moved by dma to the next buffer by linking to the next buffer in the order of the three buffers. Also keep track of the last read buffer with a variable that points to the read buffer so we know which one we can read at all times.

Code:
#include "Arduino.h"
#include "ADC.h"
#include <DMAChannel.h>

// Pin array for 10 sensors
int pins[] = {A0, A1, A2, A3, A4, A5, A6, A7, A8, A9};

float frequency = 40000.0f;

// DMA buffers for triple-buffering ADC results
DMAMEM __attribute__((aligned(32))) static uint16_t adc_buffer_A[10];
DMAMEM __attribute__((aligned(32))) static uint16_t adc_buffer_B[10];
DMAMEM __attribute__((aligned(32))) static uint16_t adc_buffer_C[10];
static volatile uint16_t* current_read_buffer = nullptr;  // Pointer to the buffer currently being read
static volatile uint8_t buffer_status = 0; // Tracks which buffer was last filled: 0 for A, 1 for B, 2 for C

// DMA channel for ADC_ETC
DMAChannel dma;

// Forward declarations
void adc_init();
void adc_etc_reset();
void adc_etc_setup();
void setupEDMA();
void setup_timer_and_xbar(const float frequency);
void start_timer();

extern "C" {
    extern void xbar_connect(unsigned int input, unsigned int output);
}

void setup() {
    while (!Serial && millis() < 5000);
    Serial.println("Setup ADC");

    // ADC settings
    ADC *adc = new ADC();
    adc->adc0->setAveraging(4);
    adc->adc0->setResolution(12);
    adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
    adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
    adc->adc0->singleMode();

    adc->adc1->setAveraging(4);
    adc->adc1->setResolution(12);
    adc->adc1->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
    adc->adc1->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
    adc->adc1->singleMode();

    adc_init();
    adc_etc_reset();
    adc_etc_setup();
    setup_timer_and_xbar(frequency); // Set the frequency
    setupEDMA(); // Initialize eDMA with triple-buffering configuration
    start_timer();
}

void loop() {
    static uint16_t local_buffer[10]; // Local buffer to hold the copied data

    // Check which buffer was last filled and is safe to read
    switch (buffer_status) {
        case 0:
            current_read_buffer = adc_buffer_A;
            break;
        case 1:
            current_read_buffer = adc_buffer_B;
            break;
        case 2:
            current_read_buffer = adc_buffer_C;
            break;
        default:
            return; // Invalid status, do nothing
    }

    // Copy data to local buffer and process it
    memcpy(local_buffer, (void*)current_read_buffer, sizeof(local_buffer));

    // Process the ADC results from the safe-to-read local buffer
    for (int i = 0; i < 10; i++) {
        Serial.printf("ADC Result[%d]: %d\n", i, local_buffer[i]);
    }

    delay(1000);  // Delay for demonstration; adjust as needed
}

void adc_init() {
    ADC1_CFG |= ADC_CFG_ADTRG; // Hardware trigger
    ADC1_HC0 = 16; // ADC_ETC channel, 144 int enabled, 16 int disabled
    ADC1_GC &= ~ADC_GC_ADCO; // Continuous conversion disabled, same as adc->adcX->singleMode()

    ADC2_CFG |= ADC_CFG_ADTRG; // Hardware trigger
    ADC2_HC0 = 16; // ADC_ETC channel
    ADC2_GC &= ~ADC_GC_ADCO; // Continuous conversion disabled
}

void adc_etc_reset() {
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_SOFTRST; // SOFTRST
    asm volatile("dsb"); // Ensure SOFTRST operation is completed
    IMXRT_ADC_ETC.CTRL &= ~ADC_ETC_CTRL_SOFTRST; // Clear SOFTRST
    delay(5);
}

void adc_etc_setup() {
    const uint8_t chainLength0 = 5;  // Chain length for the first 5 sensors (TRIG[0])
    const uint8_t chainLength4 = 5;  // Chain length for the last 5 sensors (TRIG[4])
    const uint8_t trigger_adc_0 = 0; // TRIG[0] for ADC1
    const uint8_t trigger_adc_4 = 4; // TRIG[4] for ADC2

    // Hard-coded pin assignments for sensors
    int adc_0_pinArray[5] = { A0, A1, A2, A3, A4 };  // Pins for ADC1
    int adc_1_pinArray[5] = { A5, A6, A7, A8, A9 };  // Pins for ADC2

    uint8_t adc_pin_channel_0[5];
    uint8_t adc_pin_channel_1[5];

    // Map each pin to its ADC channel
    for (int i = 0; i < 5; i++) {
        adc_pin_channel_0[i] = ADC::channel2sc1aADC0[adc_0_pinArray[i]];
        adc_pin_channel_1[i] = ADC::channel2sc1aADC1[adc_1_pinArray[i]];
    }

    // Enable triggers for TRIG[0] and TRIG[4], and enable DMA mode
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_TRIG_ENABLE((1 << trigger_adc_0) | (1 << trigger_adc_4)) | ADC_ETC_CTRL_DMA_MODE_SEL;

    // Configure TRIG[0] for ADC1
    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CTRL = ADC_ETC_TRIG_CTRL_TRIG_CHAIN(chainLength0 - 1) | ADC_ETC_TRIG_CTRL_SYNC_MODE;

    // Set up each chain in TRIG[0] with DMA-enabled configuration
    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_1_0 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[0]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_0[1]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_3_2 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[2]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_0[3]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_5_4 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(1) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[4]);

    // Configure TRIG[4] for ADC2
    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CTRL = ADC_ETC_TRIG_CTRL_TRIG_CHAIN(chainLength4 - 1) | ADC_ETC_TRIG_CTRL_SYNC_MODE;

    // Set up each chain in TRIG[4] with DMA-enabled configuration
    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_1_0 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[0]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_1[1]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_3_2 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[2]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_1[3]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_5_4 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[4]);

    // Enable the DMA for the trigger events
    IMXRT_ADC_ETC.DMA_CTRL = ADC_ETC_DMA_CTRL_TRIQ_ENABLE(trigger_adc_0) | ADC_ETC_DMA_CTRL_TRIQ_ENABLE(trigger_adc_4);
}

void setupEDMA() {
    // TCD0: Transfer from TRIG[0] (ADC1) into Buffer A (5 sensors)
    DMA_TCD0_SADDR = &IMXRT_ADC_ETC.TRIG[0].RESULT_1_0; // Source address
    DMA_TCD0_SOFF = 2; // Move by 2 bytes to read each 16-bit value within 32-bit result
    DMA_TCD0_ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // 16-bit size for source and destination
    DMA_TCD0_NBYTES_MLNO = 2; // Transfer 16 bits per read
    DMA_TCD0_SLAST = -20; // Adjust back after reading all 10 values (5 pairs)
    DMA_TCD0_DADDR = adc_buffer_A; // Destination address (Buffer A)
    DMA_TCD0_DOFF = 2; // Move to the next position in the destination buffer
    DMA_TCD0_CITER_ELINKNO = 10; // 10 transfers (5 pairs of ADC values)
    DMA_TCD0_BITER_ELINKNO = 10;
    DMA_TCD0_DLASTSGA = 0;  // No buffer status update yet
    DMA_TCD0_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(1); // Link to the next TCD

    // TCD1: Transfer from TRIG[4] (ADC2) into Buffer A (5 sensors) - Continues TCD0
    DMA_TCD1_SADDR = &IMXRT_ADC_ETC.TRIG[4].RESULT_1_0; // Source address
    DMA_TCD1_SOFF = 2; // Move by 2 bytes to read each 16-bit value within 32-bit result
    DMA_TCD1_ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // 16-bit size for source and destination
    DMA_TCD1_NBYTES_MLNO = 2; // Transfer 16 bits per read
    DMA_TCD1_SLAST = -20; // Adjust back after reading all 10 values (5 pairs)
    DMA_TCD1_DADDR = &adc_buffer_A[5]; // Destination address (next half of Buffer A)
    DMA_TCD1_DOFF = 2; // Move to the next position in the destination buffer
    DMA_TCD1_CITER_ELINKNO = 10; // 10 transfers (5 pairs of ADC values)
    DMA_TCD1_BITER_ELINKNO = 10;
    DMA_TCD1_DLASTSGA = (int32_t)(&buffer_status) - (int32_t)(&adc_buffer_A[5]); // Update buffer_status to 0 (Buffer A) after all 10 sensors
    DMA_TCD1_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(2); // Link to the next TCD

    // TCD2: Transfer into Buffer B (5 sensors)
    DMA_TCD2_SADDR = &IMXRT_ADC_ETC.TRIG[0].RESULT_1_0; // Source address
    DMA_TCD2_SOFF = 2; // Move by 2 bytes to read each 16-bit value within 32-bit result
    DMA_TCD2_ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // 16-bit size for source and destination
    DMA_TCD2_NBYTES_MLNO = 2; // Transfer 16 bits per read
    DMA_TCD2_SLAST = -20; // Adjust back after reading all 10 values (5 pairs)
    DMA_TCD2_DADDR = adc_buffer_B; // Destination address (Buffer B)
    DMA_TCD2_DOFF = 2; // Move to the next position in the destination buffer
    DMA_TCD2_CITER_ELINKNO = 10; // 10 transfers (5 pairs of ADC values)
    DMA_TCD2_BITER_ELINKNO = 10;
    DMA_TCD2_DLASTSGA = 0; // No buffer status update yet
    DMA_TCD2_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(3); // Link to the next TCD

    // TCD3: Transfer into Buffer B (next 5 sensors) and update buffer_status
    DMA_TCD3_SADDR = &IMXRT_ADC_ETC.TRIG[4].RESULT_1_0; // Source address
    DMA_TCD3_SOFF = 2; // Move by 2 bytes to read each 16-bit value within 32-bit result
    DMA_TCD3_ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // 16-bit size for source and destination
    DMA_TCD3_NBYTES_MLNO = 2; // Transfer 16 bits per read
    DMA_TCD3_SLAST = -20; // Adjust back after reading all 10 values (5 pairs)
    DMA_TCD3_DADDR = &adc_buffer_B[5]; // Destination address (next half of Buffer B)
    DMA_TCD3_DOFF = 2; // Move to the next position in the destination buffer
    DMA_TCD3_CITER_ELINKNO = 10; // 10 transfers (5 pairs of ADC values)
    DMA_TCD3_BITER_ELINKNO = 10;
    DMA_TCD3_DLASTSGA = (int32_t)(&buffer_status) - (int32_t)(&adc_buffer_B[5]); // Update buffer_status to 1 (Buffer B)
    DMA_TCD3_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(4); // Link to the next TCD

    // TCD4: Transfer into Buffer C (5 sensors)
    DMA_TCD4_SADDR = &IMXRT_ADC_ETC.TRIG[0].RESULT_1_0; // Source address
    DMA_TCD4_SOFF = 2; // Move by 2 bytes to read each 16-bit value within 32-bit result
    DMA_TCD4_ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // 16-bit size for source and destination
    DMA_TCD4_NBYTES_MLNO = 2; // Transfer 16 bits per read
    DMA_TCD4_SLAST = -20; // Adjust back after reading all 10 values (5 pairs)
    DMA_TCD4_DADDR = adc_buffer_C; // Destination address (Buffer C)
    DMA_TCD4_DOFF = 2; // Move to the next position in the destination buffer
    DMA_TCD4_CITER_ELINKNO = 10; // 10 transfers (5 pairs of ADC values)
    DMA_TCD4_BITER_ELINKNO = 10;
    DMA_TCD4_DLASTSGA = 0; // No buffer status update yet
    DMA_TCD4_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(5); // Link to the next TCD

    // TCD5: Transfer into Buffer C (next 5 sensors) and update buffer_status
    DMA_TCD5_SADDR = &IMXRT_ADC_ETC.TRIG[4].RESULT_1_0; // Source address
    DMA_TCD5_SOFF = 2; // Move by 2 bytes to read each 16-bit value within 32-bit result
    DMA_TCD5_ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // 16-bit size for source and destination
    DMA_TCD5_NBYTES_MLNO = 2; // Transfer 16 bits per read
    DMA_TCD5_SLAST = -20; // Adjust back after reading all 10 values (5 pairs)
    DMA_TCD5_DADDR = &adc_buffer_C[5]; // Destination address (next half of Buffer C)
    DMA_TCD5_DOFF = 2; // Move to the next position in the destination buffer
    DMA_TCD5_CITER_ELINKNO = 10; // 10 transfers (5 pairs of ADC values)
    DMA_TCD5_BITER_ELINKNO = 10;
    DMA_TCD5_DLASTSGA = (int32_t)(&buffer_status) - (int32_t)(&adc_buffer_C[5]); // Update buffer_status to 2 (Buffer C)
    DMA_TCD5_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(0); // Link back to TCD0

    // Enable DMA requests for all channels involved
    IMXRT_DMA.ERQ |= (1 << 0); // Enable the request for channel 0

    // Properly configure the DMAMUX for each channel
    DMAMUX_CHCFG0 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
    DMAMUX_CHCFG1 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
    DMAMUX_CHCFG2 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
    DMAMUX_CHCFG3 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
    DMAMUX_CHCFG4 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
    DMAMUX_CHCFG5 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
}




void setup_timer_and_xbar(const float frequency) {
    CCM_CCGR2 |= CCM_CCGR2_XBAR1(CCM_CCGR_ON); // Enable clock for XBAR

    // Connect TMR4 Timer output to ADC_ETC Trigger input
    xbar_connect(XBARA1_IN_QTIMER4_TIMER0, XBARA1_OUT_ADC_ETC_TRIG00); // Connect Timer to trigger TRIG[0]
}

void start_timer() {
    // Configure TMR4 Timer directly
    TMR4_ENBL &= ~(1 << 0); // Disable Timer
    TMR4_SCTRL0 = TMR_SCTRL_OEN | TMR_SCTRL_FORCE;
    TMR4_CSCTRL0 = TMR_CSCTRL_CL1(1) | TMR_CSCTRL_TCF1EN;
    TMR4_CNTR0 = 0;
    TMR4_LOAD0 = 0;
    TMR4_COMP10 = (float)F_BUS_ACTUAL / frequency / 2; // Set compare value based on desired frequency
    TMR4_CMPLD10 = (float)F_BUS_ACTUAL / frequency / 2; // Set compare value
    TMR4_CTRL0 = TMR_CTRL_CM(1) | TMR_CTRL_PCS(8) | TMR_CTRL_LENGTH | TMR_CTRL_OUTMODE(3); // Timer configuration
    TMR4_ENBL |= (1 << 0); // Enable Timer

    // Debug output to verify timer setup
    Serial.printf("Timer started with calculated period: %lu ticks\n", (uint32_t)(F_BUS_ACTUAL / (frequency * 2)));
    Serial.flush();
}
 
Damn, it seems to read all ten values correctly but for some reason I'm getting the same values over and over from the sensors so it seems something is not updating correctly.
 
You need to clear the CPU's cache of adc_buffer_A/B/C before reading from them, e.g. arm_dcache_delete(adc_buffer_A, sizeof(adc_buffer_A));
 
You need to clear the CPU's cache of adc_buffer_A/B/C before reading from them, e.g. arm_dcache_delete(adc_buffer_A, sizeof(adc_buffer_A));
There are more errors. I think it's a data alignment issue. I'm reading 16 bits at a time and moving to the next 16 bit but perhaps it needs to be read 32 bits at a time. Right now I'm trying to write the buffer address with the TCD into a variable so I can access the right buffer but it outputs invalid buffer address.

Code:
#include "Arduino.h"
#include "ADC.h"
#include <DMAChannel.h>

// Pin array for 10 sensors
int pins[] = {A0, A1, A2, A3, A4, A5, A6, A7, A8, A9};

float frequency = 5000.0f;

// DMA buffers for triple-buffering ADC results
DMAMEM __attribute__((aligned(32))) static uint16_t adc_buffer_A[10];
DMAMEM __attribute__((aligned(32))) static uint16_t adc_buffer_B[10];
DMAMEM __attribute__((aligned(32))) static uint16_t adc_buffer_C[10];

// Array to hold addresses of the buffers
DMAMEM __attribute__((aligned(32))) static volatile uint16_t* buffer_addresses[3] = {adc_buffer_A, adc_buffer_B, adc_buffer_C};

DMAMEM volatile uint16_t* buffer_status_storage = nullptr;
// DMA channel for ADC_ETC
DMAChannel dma;

// Forward declarations
void adc_init();
void adc_etc_reset();
void adc_etc_setup();
void setupEDMA();
void setup_timer_and_xbar(const float frequency);
void start_timer();

extern "C" {
    extern void xbar_connect(unsigned int input, unsigned int output);
}

void setup() {
    while (!Serial && millis() < 5000);
    Serial.println("Setup ADC");

    // ADC settings
    ADC *adc = new ADC();
    adc->adc0->setAveraging(4);
    adc->adc0->setResolution(12);
    adc->adc0->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
    adc->adc0->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
    adc->adc0->singleMode();

    adc->adc1->setAveraging(4);
    adc->adc1->setResolution(12);
    adc->adc1->setConversionSpeed(ADC_CONVERSION_SPEED::VERY_HIGH_SPEED);
    adc->adc1->setSamplingSpeed(ADC_SAMPLING_SPEED::VERY_HIGH_SPEED);
    adc->adc1->singleMode();

    adc_init();
    adc_etc_reset();
    adc_etc_setup();
    setup_timer_and_xbar(frequency); // Set the frequency
    setupEDMA(); // Initialize eDMA with triple-buffering configuration
    start_timer();
}

void loop() {
    static uint16_t local_buffer[10]; // Local buffer to hold the copied data

    // Clear the cache for each buffer before accessing the data
    arm_dcache_delete((void*)adc_buffer_A, sizeof(adc_buffer_A));
    arm_dcache_delete((void*)adc_buffer_B, sizeof(adc_buffer_B));
    arm_dcache_delete((void*)adc_buffer_C, sizeof(adc_buffer_C));

    // Read the stored buffer address from buffer_status_storage
    uint16_t* current_read_buffer = (uint16_t*)buffer_status_storage; // Correct cast without volatile type

    // Check if the address is valid (points to one of the known buffers)
    if (current_read_buffer != adc_buffer_A &&
        current_read_buffer != adc_buffer_B &&
        current_read_buffer != adc_buffer_C) {
        Serial.println("Invalid buffer address, aborting read.");
        delay(1000);
        return;
    }

    // Copy data to local buffer and process it
    memcpy(local_buffer, adc_buffer_A, sizeof(local_buffer));

    // Process the ADC results from the safe-to-read local buffer
    for (int i = 0; i < 10; i++) {
        Serial.printf("ADC Result[%d]: %d\n", i, local_buffer[i]);
    }

    delay(1000);  // Delay for demonstration; adjust as needed
}



void adc_init() {
    ADC1_CFG |= ADC_CFG_ADTRG; // Hardware trigger
    ADC1_HC0 = 16; // ADC_ETC channel, 144 int enabled, 16 int disabled
    ADC1_GC &= ~ADC_GC_ADCO; // Continuous conversion disabled, same as adc->adcX->singleMode()

    ADC2_CFG |= ADC_CFG_ADTRG; // Hardware trigger
    ADC2_HC0 = 16; // ADC_ETC channel
    ADC2_GC &= ~ADC_GC_ADCO; // Continuous conversion disabled
}

void adc_etc_reset() {
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_SOFTRST; // SOFTRST
    asm volatile("dsb"); // Ensure SOFTRST operation is completed
    IMXRT_ADC_ETC.CTRL &= ~ADC_ETC_CTRL_SOFTRST; // Clear SOFTRST
    delay(5);
}

void adc_etc_setup() {
    const uint8_t chainLength0 = 5;  // Chain length for the first 5 sensors (TRIG[0])
    const uint8_t chainLength4 = 5;  // Chain length for the last 5 sensors (TRIG[4])
    const uint8_t trigger_adc_0 = 0; // TRIG[0] for ADC1
    const uint8_t trigger_adc_4 = 4; // TRIG[4] for ADC2

    // Hard-coded pin assignments for sensors
    int adc_0_pinArray[5] = { A0, A1, A2, A3, A4 };  // Pins for ADC1
    int adc_1_pinArray[5] = { A5, A6, A7, A8, A9 };  // Pins for ADC2

    uint8_t adc_pin_channel_0[5];
    uint8_t adc_pin_channel_1[5];

    // Map each pin to its ADC channel
    for (int i = 0; i < 5; i++) {
        adc_pin_channel_0[i] = ADC::channel2sc1aADC0[adc_0_pinArray[i]];
        adc_pin_channel_1[i] = ADC::channel2sc1aADC1[adc_1_pinArray[i]];
    }

    // Enable triggers for TRIG[0] and TRIG[4], and enable DMA mode
    IMXRT_ADC_ETC.CTRL = ADC_ETC_CTRL_TRIG_ENABLE((1 << trigger_adc_0) | (1 << trigger_adc_4)) | ADC_ETC_CTRL_DMA_MODE_SEL;

    // Configure TRIG[0] for ADC1
    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CTRL = ADC_ETC_TRIG_CTRL_TRIG_CHAIN(chainLength0 - 1) | ADC_ETC_TRIG_CTRL_SYNC_MODE;

    // Set up each chain in TRIG[0] with DMA-enabled configuration
    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_1_0 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[0]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_0[1]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_3_2 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[2]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_0[3]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_0].CHAIN_5_4 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(1) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_0[4]);

    // Configure TRIG[4] for ADC2
    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CTRL = ADC_ETC_TRIG_CTRL_TRIG_CHAIN(chainLength4 - 1) | ADC_ETC_TRIG_CTRL_SYNC_MODE;

    // Set up each chain in TRIG[4] with DMA-enabled configuration
    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_1_0 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[0]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_1[1]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_3_2 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[2]) |
        ADC_ETC_TRIG_CHAIN_B2B1 |
        ADC_ETC_TRIG_CHAIN_IE1(0) |
        ADC_ETC_TRIG_CHAIN_HWTS1(1) |
        ADC_ETC_TRIG_CHAIN_CSEL1(adc_pin_channel_1[3]);

    IMXRT_ADC_ETC.TRIG[trigger_adc_4].CHAIN_5_4 =
        ADC_ETC_TRIG_CHAIN_B2B0 |
        ADC_ETC_TRIG_CHAIN_IE0(0) |
        ADC_ETC_TRIG_CHAIN_HWTS0(1) |
        ADC_ETC_TRIG_CHAIN_CSEL0(adc_pin_channel_1[4]);

    // Enable the DMA for the trigger events
    IMXRT_ADC_ETC.DMA_CTRL = ADC_ETC_DMA_CTRL_TRIQ_ENABLE(trigger_adc_0) | ADC_ETC_DMA_CTRL_TRIQ_ENABLE(trigger_adc_4);
}


void setupEDMA() {
    // ================== Buffer A Configuration ==================
    // TCD0: Transfer 5 sensor values from TRIG[0] (ADC1) into Buffer A
    DMA_TCD0_SADDR = &IMXRT_ADC_ETC.TRIG[0].RESULT_1_0; // Source address from TRIG[0]
    DMA_TCD0_SOFF = 2; // Move 2 bytes after each 16-bit value read
    DMA_TCD0_ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // 16-bit transfer size
    DMA_TCD0_NBYTES_MLNO = 2; // Transfer 2 bytes per iteration
    DMA_TCD0_SLAST = -10; // No adjustment
    DMA_TCD0_DADDR = adc_buffer_A; // Destination address starts at Buffer A
    DMA_TCD0_DOFF = 2; // Move 2 bytes to next position in Buffer A
    DMA_TCD0_CITER_ELINKNO = 5; // 5 transfers for 5 sensors
    DMA_TCD0_BITER_ELINKNO = 5;
    DMA_TCD0_DLASTSGA = 0; // No adjustment after completion
    DMA_TCD0_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(1); // Link to TCD1

    // TCD1: Transfer 5 sensor values from TRIG[4] (ADC2) into Buffer A
    DMA_TCD1_SADDR = &IMXRT_ADC_ETC.TRIG[4].RESULT_1_0; // Source address from TRIG[4]
    DMA_TCD1_SOFF = 2; // Move 2 bytes after each read
    DMA_TCD1_ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // 16-bit transfer size
    DMA_TCD1_NBYTES_MLNO = 2; // Transfer 2 bytes per iteration
    DMA_TCD1_SLAST = -10; // No adjustment
    DMA_TCD1_DADDR = &adc_buffer_A[5]; // Continue writing to second half of Buffer A
    DMA_TCD1_DOFF = 2; // Move 2 bytes to next position
    DMA_TCD1_CITER_ELINKNO = 5; // 5 transfers for 5 sensors
    DMA_TCD1_BITER_ELINKNO = 5;
    DMA_TCD1_DLASTSGA = 0; // No adjustment after completion
    DMA_TCD1_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(2); // Link to TCD2

    // TCD2: Write address of Buffer A into buffer_status_storage, then link to Buffer B configuration
    DMA_TCD2_SADDR = &buffer_addresses[0]; // Source address: pointer to Buffer A in the array
    DMA_TCD2_SOFF = 0; // No offset
    DMA_TCD2_ATTR = DMA_TCD_ATTR_SSIZE(2) | DMA_TCD_ATTR_DSIZE(2); // 32-bit transfer size
    DMA_TCD2_NBYTES_MLNO = 4; // Transfer 4 bytes (address)
    DMA_TCD2_SLAST = 0; // No adjustment
    DMA_TCD2_DADDR = &buffer_status_storage; // Destination address: buffer status storage
    DMA_TCD2_DOFF = 0; // No offset
    DMA_TCD2_CITER_ELINKNO = 1; // Only one transfer
    DMA_TCD2_BITER_ELINKNO = 1;
    DMA_TCD2_DLASTSGA = 0; // No adjustment after writing
    DMA_TCD2_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(3); // Link to TCD3 (Buffer B)

    // ================== Buffer B Configuration ==================
    // TCD3: Transfer 5 sensor values from TRIG[0] (ADC1) into Buffer B
    DMA_TCD3_SADDR = &IMXRT_ADC_ETC.TRIG[0].RESULT_1_0; // Source address from TRIG[0]
    DMA_TCD3_SOFF = 2; // Move 2 bytes after each 16-bit value read
    DMA_TCD3_ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // 16-bit transfer size
    DMA_TCD3_NBYTES_MLNO = 2; // Transfer 2 bytes per iteration
    DMA_TCD3_SLAST = -10; // No adjustment
    DMA_TCD3_DADDR = adc_buffer_B; // Destination address starts at Buffer B
    DMA_TCD3_DOFF = 2; // Move 2 bytes to next position in Buffer B
    DMA_TCD3_CITER_ELINKNO = 5; // 5 transfers for 5 sensors
    DMA_TCD3_BITER_ELINKNO = 5;
    DMA_TCD3_DLASTSGA = 0; // No adjustment after completion
    DMA_TCD3_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(4); // Link to TCD4

    // TCD4: Transfer 5 sensor values from TRIG[4] (ADC2) into Buffer B
    DMA_TCD4_SADDR = &IMXRT_ADC_ETC.TRIG[4].RESULT_1_0; // Source address from TRIG[4]
    DMA_TCD4_SOFF = 2; // Move 2 bytes after each read
    DMA_TCD4_ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // 16-bit transfer size
    DMA_TCD4_NBYTES_MLNO = 2; // Transfer 2 bytes per iteration
    DMA_TCD4_SLAST = -10; // No adjustment
    DMA_TCD4_DADDR = &adc_buffer_B[5]; // Continue writing to second half of Buffer B
    DMA_TCD4_DOFF = 2; // Move 2 bytes to next position
    DMA_TCD4_CITER_ELINKNO = 5; // 5 transfers for 5 sensors
    DMA_TCD4_BITER_ELINKNO = 5;
    DMA_TCD4_DLASTSGA = 0; // No adjustment after completion
    DMA_TCD4_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(5); // Link to TCD5

    // TCD5: Write address of Buffer B into buffer_status_storage, then link to Buffer C configuration
    DMA_TCD5_SADDR = &buffer_addresses[1]; // Source address: pointer to Buffer B in the array
    DMA_TCD5_SOFF = 0; // No offset
    DMA_TCD5_ATTR = DMA_TCD_ATTR_SSIZE(2) | DMA_TCD_ATTR_DSIZE(2); // 32-bit transfer size
    DMA_TCD5_NBYTES_MLNO = 4; // Transfer 4 bytes (address)
    DMA_TCD5_SLAST = 0; // No adjustment
    DMA_TCD5_DADDR = &buffer_status_storage; // Destination address: buffer status storage
    DMA_TCD5_DOFF = 0; // No offset
    DMA_TCD5_CITER_ELINKNO = 1; // Only one transfer
    DMA_TCD5_BITER_ELINKNO = 1;
    DMA_TCD5_DLASTSGA = 0; // No adjustment after writing
    DMA_TCD5_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(6); // Link to TCD6 (Buffer C)

    // ================== Buffer C Configuration ==================
    // TCD6: Transfer 5 sensor values from TRIG[0] (ADC1) into Buffer C
    DMA_TCD6_SADDR = &IMXRT_ADC_ETC.TRIG[0].RESULT_1_0; // Source address from TRIG[0]
    DMA_TCD6_SOFF = 2; // Move 2 bytes after each 16-bit value read
    DMA_TCD6_ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // 16-bit transfer size
    DMA_TCD6_NBYTES_MLNO = 2; // Transfer 2 bytes per iteration
    DMA_TCD6_SLAST = -10; // No adjustment
    DMA_TCD6_DADDR = adc_buffer_C; // Destination address starts at Buffer C
    DMA_TCD6_DOFF = 2; // Move 2 bytes to next position in Buffer C
    DMA_TCD6_CITER_ELINKNO = 5; // 5 transfers for 5 sensors
    DMA_TCD6_BITER_ELINKNO = 5;
    DMA_TCD6_DLASTSGA = 0; // No adjustment after completion
    DMA_TCD6_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(7); // Link to TCD7

    // TCD7: Transfer 5 sensor values from TRIG[4] (ADC2) into Buffer C
    DMA_TCD7_SADDR = &IMXRT_ADC_ETC.TRIG[4].RESULT_1_0; // Source address from TRIG[4]
    DMA_TCD7_SOFF = 2; // Move 2 bytes after each read
    DMA_TCD7_ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1); // 16-bit transfer size
    DMA_TCD7_NBYTES_MLNO = 2; // Transfer 2 bytes per iteration
    DMA_TCD7_SLAST = -10; // No adjustment
    DMA_TCD7_DADDR = &adc_buffer_C[5]; // Continue writing to second half of Buffer C
    DMA_TCD7_DOFF = 2; // Move 2 bytes to next position
    DMA_TCD7_CITER_ELINKNO = 5; // 5 transfers for 5 sensors
    DMA_TCD7_BITER_ELINKNO = 5;
    DMA_TCD7_DLASTSGA = 0; // No adjustment after completion
    DMA_TCD7_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(8); // Link to TCD8

    // TCD8: Write address of Buffer C into buffer_status_storage, then link back to Buffer A configuration
    DMA_TCD8_SADDR = &buffer_addresses[2]; // Source address: pointer to Buffer C in the array
    DMA_TCD8_SOFF = 0; // No offset
    DMA_TCD8_ATTR = DMA_TCD_ATTR_SSIZE(2) | DMA_TCD_ATTR_DSIZE(2); // 32-bit transfer size
    DMA_TCD8_NBYTES_MLNO = 4; // Transfer 4 bytes (address)
    DMA_TCD8_SLAST = -10; // No adjustment
    DMA_TCD8_DADDR = &buffer_status_storage; // Destination address: buffer status storage
    DMA_TCD8_DOFF = 0; // No offset
    DMA_TCD8_CITER_ELINKNO = 1; // Only one transfer
    DMA_TCD8_BITER_ELINKNO = 1;
    DMA_TCD8_DLASTSGA = 0; // No adjustment after writing
    DMA_TCD8_CSR = DMA_TCD_CSR_MAJORELINK | DMA_TCD_CSR_MAJORLINKCH(0); // Link back to TCD0 (Buffer A)

    // Enable DMA requests for all channels involved
    IMXRT_DMA.ERQ |= (1 << 0);

    // Configure the DMAMUX for each channel
    DMAMUX_CHCFG0 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
    DMAMUX_CHCFG1 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
    DMAMUX_CHCFG2 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
    DMAMUX_CHCFG3 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
    DMAMUX_CHCFG4 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
    DMAMUX_CHCFG5 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
    DMAMUX_CHCFG6 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
    DMAMUX_CHCFG7 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
    DMAMUX_CHCFG8 = DMAMUX_CHCFG_ENBL | DMAMUX_SOURCE_ADC_ETC;
}





void setup_timer_and_xbar(const float frequency) {
    CCM_CCGR2 |= CCM_CCGR2_XBAR1(CCM_CCGR_ON); // Enable clock for XBAR

    // Connect TMR4 Timer output to ADC_ETC Trigger input
    xbar_connect(XBARA1_IN_QTIMER4_TIMER0, XBARA1_OUT_ADC_ETC_TRIG00); // Connect Timer to trigger TRIG[0]
}

void start_timer() {
    // Configure TMR4 Timer directly
    TMR4_ENBL &= ~(1 << 0); // Disable Timer
    TMR4_SCTRL0 = TMR_SCTRL_OEN | TMR_SCTRL_FORCE;
    TMR4_CSCTRL0 = TMR_CSCTRL_CL1(1) | TMR_CSCTRL_TCF1EN;
    TMR4_CNTR0 = 0;
    TMR4_LOAD0 = 0;
    TMR4_COMP10 = (float)F_BUS_ACTUAL / frequency / 2; // Set compare value based on desired frequency
    TMR4_CMPLD10 = (float)F_BUS_ACTUAL / frequency / 2; // Set compare value
    TMR4_CTRL0 = TMR_CTRL_CM(1) | TMR_CTRL_PCS(8) | TMR_CTRL_LENGTH | TMR_CTRL_OUTMODE(3); // Timer configuration
    TMR4_ENBL |= (1 << 0); // Enable Timer

    // Debug output to verify timer setup
    Serial.printf("Timer started with calculated period: %lu ticks\n", (uint32_t)(F_BUS_ACTUAL / (frequency * 2)));
    Serial.flush();
}
 
Back
Top