M4/SAMD51 Hardware Averaging with ADCs and DMAC gives incorrect conversions

Continuation of this post.

Hey everyone, I'm currently working with an Adafruit Feather M4 Express with the SD/RTC featherwing on top. Essentially, the project I'm working on requires high frequency voltage logging to SD card from 4 pins simultaneously (for now I'm using A0, A1, A3, A4).

I've been making use of the onboard DMAC and 6 descriptors to control the 2 ADCs. So far, I have this working near perfectly in the ADC's non-averaged/standard operation mode, but flipping to averaged mode gives some odd behavior and incorrect readings (e.g. a 3.3V readings is saved as ~4009 rather than ~4095, 1.1V as ~1275 rather than ~1360). Another interesting quirk is that in raw mode, with each pin hooked up to a different node of a simple voltage divider, I get the following results (12 bit readings of A0, A1, A3, A4 respectively):
image

Once switched over to averaging mode, I get the following incorrect results:
image

Now, the interesting thing is that I sometimes can get the correct result in ADC0 when printing via the debug flags (haven't checked with ADC1 yet, but I presume same phenomena occurs):
image

The following is my current code for hardware averaged results which causes the unintended behaviors:

/*
This is a demo for reading averaged samples from both ADCs on TWO PINS EACH.
The DMAC then takes these and dumps them into two buffers.
Trying to save something to disk but we'll see if that's possible

Made by paelen1234
*/


#include "FreeStack.h"
#include "SdFat.h"
#include "sdios.h"
#include "helper.h"

#define error(s) sd.errorHalt(&Serial, F(s))
const uint8_t SD_CS_PIN = 10;
#define SPI_CLOCK SD_SCK_MHZ(50)
#define SD_CONFIG SdSpiConfig(SD_CS_PIN, DEDICATED_SPI, SPI_CLOCK)
#define NUM_SAMPLES 1024 // the number of samples per pin.

SdFs sd;
FsFile file;

#define DEBUG_R0_P0 0
#define DEBUG_R0_P1 0
#define DEBUG_R1_P0 0
#define DEBUG_R1_P1 0
#define DEBUG_SD_BEGIN 1

volatile bool results0Part0Ready = false;
volatile bool results0Part1Ready = false;
volatile bool results1Part0Ready = false;
volatile bool results1Part1Ready = false;
uint16_t adcResults0[NUM_SAMPLES*2]; // ADC0 results array
uint16_t adcResults1[NUM_SAMPLES*2]; // ADC1 results array


typedef struct // DMAC descriptor struct
{
    uint16_t btctrl;
    uint16_t btcnt;
    uint32_t srcaddr;
    uint32_t dstaddr;
    uint32_t descaddr;
} dmadesc;

/*
A0  ADC_INPUTCTRL_MUXPOS_AIN2
A1  ADC_INPUTCTRL_MUXPOS_AIN5
A2  ADC_INPUTCTRL_MUXPOS_AIN8  ADC_INPUTCTRL_MUXPOS_AIN0
A3  ADC_INPUTCTRL_MUXPOS_AIN9  ADC_INPUTCTRL_MUXPOS_AIN1
A4  ADC_INPUTCTRL_MUXPOS_AIN4
*/

// ADC0 INPUTCTRL register MUXPOS settings 
uint32_t inputCtrl0[] = { ADC_INPUTCTRL_MUXPOS_AIN0,                              // AIN0 = A0
                          ADC_INPUTCTRL_MUXPOS_AIN5 };                            // AIN5 = A1
// ADC1 INPUTCTRL register MUXPOS settings 
uint32_t inputCtrl1[] = { ADC_INPUTCTRL_MUXPOS_AIN0,                              // AIN2 = A3
                          ADC_INPUTCTRL_MUXPOS_AIN1 };                            // AIN3 = A4



volatile dmadesc (*wrb)[DMAC_CH_NUM] __attribute__ ((aligned (16)));          // Write-back DMAC descriptors (changed to array pointer)
dmadesc (*descriptor_section)[DMAC_CH_NUM] __attribute__ ((aligned (16)));    // DMAC channel descriptors (changed to array pointer)
dmadesc descriptor __attribute__ ((aligned (16)));                            // Placeholder descriptor

void adc_init() {
    //////////////////////////////////////////////////////////
    // ADC0 Settings
    ADC0->INPUTCTRL.bit.MUXPOS = inputCtrl0[0];                                     // Set the initial analog input to A0
    while(ADC0->SYNCBUSY.bit.INPUTCTRL);                                            // Wait for synchronization
    ADC0->SAMPCTRL.bit.SAMPLEN = 0x01;                                              // Extend sampling time by SAMPLEN ADC cycles (12 + 1 + 2)/750kHz = 20us = 50kHz
    while(ADC0->SYNCBUSY.bit.SAMPCTRL);                                             // Wait for synchronization  
    ADC0->DSEQCTRL.reg = ADC_DSEQCTRL_AUTOSTART |                                   // Auto start a DMAC conversion upon ADC0 DMAC sequence completion
                         ADC_DSEQCTRL_INPUTCTRL;                                    // Change the ADC0 INPUTCTRL register on DMAC sequence
    ADC0->CTRLB.reg = ADC_CTRLB_RESSEL_16BIT;                                       // Set ADC resolution to 16 bits (averaging mode)
    while(ADC0->SYNCBUSY.bit.CTRLB);                                                // Wait for synchronization
    ADC0->CTRLA.reg = ADC_CTRLA_PRESCALER_DIV4;                                     // Divide Clock ADC GCLK by 4 (48MHz/64 = 750kHz) (12 + 1)/750kHz = 17.3us sample time 
    ADC0->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_16 |                                  // Required for averaging mode
                        ADC_AVGCTRL_ADJRES(4);
    // ADC0.AVGCTRL.bit.SAMPLENUM = 0x4;
    // ADC0.AVGCTRL.bit.ADJRES = 0x4;
    while (ADC0->SYNCBUSY.bit.AVGCTRL);                                             // Wait for synchronization

    
    ADC0->CTRLA.bit.ENABLE = 1;                                                     // Enable the ADC
    while(ADC0->SYNCBUSY.bit.ENABLE);                                               // Wait for synchronization
    ADC0->SWTRIG.bit.START = 1;                                                     // Initiate a software trigger to start an ADC conversion
    while(ADC0->SYNCBUSY.bit.SWTRIG);                                               // Wait for synchronization
    ADC0->DBGCTRL.bit.DBGRUN = 0;
    //////////////////////////////////////////////////////////

    //////////////////////////////////////////////////////////
    // ADC1 Settings
    ADC1->INPUTCTRL.bit.MUXPOS = inputCtrl1[0];                                     // Set the initial analog input to A2
    while(ADC1->SYNCBUSY.bit.INPUTCTRL);                                            // Wait for synchronization
    ADC1->SAMPCTRL.bit.SAMPLEN = 0x01;                                              // Extend sampling time by SAMPCTRL ADC cycles (12 + 1 + 2)/750kHz = 20us = 50kHz 
    while(ADC1->SYNCBUSY.bit.SAMPCTRL);                                             // Wait for synchronization
    ADC1->DSEQCTRL.reg = ADC_DSEQCTRL_AUTOSTART |                                   // Auto start a DMAC conversion upon ADC0 DMAC sequence completion
                         ADC_DSEQCTRL_INPUTCTRL;                                    // Change the ADC1 INPUTCTRL register on DMAC sequence
    ADC1->CTRLB.reg = ADC_CTRLB_RESSEL_16BIT;                                       // Set ADC resolution to 16 bits (averaging mode) 
    while(ADC1->SYNCBUSY.bit.CTRLB);                                                // Wait for synchronization
    ADC1->CTRLA.reg = ADC_CTRLA_PRESCALER_DIV4;                                     // Divide Clock ADC GCLK by 4 (48MHz/64 = 750kHz) (12 + 1)/750kHz = 17.3us sample time 
    ADC1->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_16 |                                  // Required for averaging mode
                        ADC_AVGCTRL_ADJRES(4);
    while (ADC1->SYNCBUSY.bit.AVGCTRL);                                             // Wait for synchronization

    ADC1->CTRLA.bit.ENABLE = 1;                                                     // Enable the ADC
    while(ADC1->SYNCBUSY.bit.ENABLE);                                               // Wait for synchronization
    ADC1->SWTRIG.bit.START = 1;                                                     // Initiate a software trigger to start an ADC conversion
    while(ADC1->SYNCBUSY.bit.SWTRIG);                                               // Wait for synchronization
    ADC1->DBGCTRL.bit.DBGRUN = 0;
    //////////////////////////////////////////////////////////
}

void dma_channels_enable() {
    DMAC->Channel[2].CHCTRLA.bit.ENABLE = 1;                                        // Enable DMAC channel 2 (ADC0 First Output Descriptor)
    DMAC->Channel[3].CHCTRLA.bit.ENABLE = 1;                                        // Enable DMAC channel 3 (ADC1 First Output Descriptor)
    DMAC->Channel[4].CHCTRLA.bit.ENABLE = 1;                                        // Enable DMAC channel 4 (ADC0 Sequencing / Input Descriptor)
    DMAC->Channel[5].CHCTRLA.bit.ENABLE = 1;                                        // Enable DMAC channel 5 (ADC1 Sequencing / Input Descriptor)
    delay(1);                                                                       // Wait a millisecond
}

void dma_init(){
    descriptor_section =  reinterpret_cast<dmadesc(*)[DMAC_CH_NUM]>(DMAC->BASEADDR.reg); //point array pointer to BASEADDR defined by SD.begin
    wrb =  reinterpret_cast<dmadesc(*)[DMAC_CH_NUM]>(DMAC->WRBADDR.reg);                 //point array pointer to WRBADDR defined by SD.begin
    DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf); 

    //////////////////////////////////////////////////////////
    // ADC0 DMA Sequencing setup (Input Descriptor)  
    DMAC->Channel[4].CHPRILVL.reg = DMAC_CHPRILVL_PRILVL_LVL3;                      // Set DMAC channel 4 to priority level 3 (highest)
    DMAC->Channel[4].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(ADC0_DMAC_ID_SEQ) |         // Set DMAC to trigger on ADC0 DMAC sequence
                                   DMAC_CHCTRLA_BURSTLEN_SINGLE |                   // One beat per burst
                                   DMAC_CHCTRLA_TRIGACT_BURST;                      // DMAC burst transfer
    DMAC->Channel[4].CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                         // Activate the suspend (SUSP) interrupt on DMAC channel 4

    descriptor.descaddr = (uint32_t)&(*descriptor_section)[4];                      // Descriptor linked to itself repeatedly
    descriptor.srcaddr = (uint32_t)inputCtrl0 + sizeof(uint32_t) * 2;               // Address of inputs to swap between
    descriptor.dstaddr = (uint32_t)&ADC0->DSEQDATA.reg;                             // Write the INPUT CTRL 
    descriptor.btcnt = 2;                                                           // Beat count is 2 (???) (code originally had it at 4) TODO TODO TODO TODO TODO
    descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_WORD |                                 // Beat size is WORD (32-bits)
                        DMAC_BTCTRL_SRCINC |                                        // Increment the source address
                        DMAC_BTCTRL_STEPSEL_SRC |                                   // Indicates that the source address is using these step settings
                        DMAC_BTCTRL_STEPSIZE_X1 |                                   // The Step size is set to 1x beats
                        DMAC_BTCTRL_VALID;                                          // Descriptor is valid
    
    memcpy(&(*descriptor_section)[4], &descriptor, sizeof(descriptor));             // Copy the descriptor to the descriptor section
    //////////////////////////////////////////////////////////

    //////////////////////////////////////////////////////////
    // ADC1 DMA Sequencing setup (Input Descriptor)  
    DMAC->Channel[5].CHPRILVL.reg = DMAC_CHPRILVL_PRILVL_LVL3;                      // Set DMAC channel 5 to priority level 3 (highest)
    DMAC->Channel[5].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(ADC1_DMAC_ID_SEQ) |         // Set DMAC to trigger on ADC1 DMAC sequence
                                   DMAC_CHCTRLA_BURSTLEN_SINGLE |                   // One beat per burst
                                   DMAC_CHCTRLA_TRIGACT_BURST;                      // DMAC burst transfer
    DMAC->Channel[5].CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                         // Activate the suspend (SUSP) interrupt on DMAC channel 5

    descriptor.descaddr = (uint32_t)&(*descriptor_section)[5];                      // Descriptor linked to itself repeatedly
    descriptor.srcaddr = (uint32_t)inputCtrl1 + sizeof(uint32_t) * 2;               // Address of inputs to swap between
    descriptor.dstaddr = (uint32_t)&ADC1->DSEQDATA.reg;                             // Write the INPUT CTRL 
    descriptor.btcnt = 2;                                                           // Beat count is 2 (???) (code originally had it at 4) TODO TODO TODO TODO TODO
    descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_WORD |                                 // Beat size is WORD (32-bits)
                        DMAC_BTCTRL_SRCINC |                                        // Increment the source address
                        DMAC_BTCTRL_STEPSEL_SRC |                                   // Indicates that the source address is using these step settings
                        DMAC_BTCTRL_STEPSIZE_X1 |                                   // The Step size is set to 1x beats
                        DMAC_BTCTRL_VALID;                                          // Descriptor is valid
    
    memcpy(&(*descriptor_section)[5], &descriptor, sizeof(descriptor));             // Copy the descriptor to the descriptor section
    //////////////////////////////////////////////////////////

    //////////////////////////////////////////////////////////
    // ADC0 Output Descriptors
    DMAC->Channel[2].CHPRILVL.reg = DMAC_CHPRILVL_PRILVL_LVL3;                      // Set DMAC channel 2 to priority level 2 (highest)
    DMAC->Channel[2].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(ADC0_DMAC_ID_RESRDY) |      // Set DMAC to trigger when ADC0 result is ready
                                   DMAC_CHCTRLA_TRIGACT_BURST;                      // DMAC burst transfer

    descriptor.descaddr = (uint32_t)&(*descriptor_section)[6];                      // Set up a linked descriptor
    descriptor.srcaddr = (uint32_t)&ADC0->RESULT.reg;                               // Take the result from the ADC0 RESULT register
    descriptor.dstaddr = (uint32_t)adcResults0 + sizeof(uint16_t) * NUM_SAMPLES;    // Place it in the first half of adcResults0 array
    descriptor.btcnt = NUM_SAMPLES;                                                 // Beat count
    descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD |                                // Beat size is HWORD (16-bits)
                        DMAC_BTCTRL_DSTINC |                                        // Increment the destination address
                        DMAC_BTCTRL_VALID |                                         // Descriptor is valid
                        DMAC_BTCTRL_BLOCKACT_SUSPEND;                               // Suspend DMAC channel 2 after block transfer
    memcpy(&(*descriptor_section)[2], &descriptor, sizeof(descriptor));             // Copy the descriptor to the descriptor section

    descriptor.descaddr = (uint32_t)&(*descriptor_section)[2];                      // Set up a linked descriptor
    descriptor.srcaddr = (uint32_t)&ADC0->RESULT.reg;                               // Take the result from the ADC0 RESULT register
    descriptor.dstaddr = (uint32_t)&adcResults0[NUM_SAMPLES] + sizeof(uint16_t) * NUM_SAMPLES;    // Place it in the second half of adcResults0 array
    descriptor.btcnt = NUM_SAMPLES;                                                 // Beat count
    descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD |                                // Beat size is HWORD (16-bits)
                        DMAC_BTCTRL_DSTINC |                                        // Increment the destination address
                        DMAC_BTCTRL_VALID |                                         // Descriptor is valid
                        DMAC_BTCTRL_BLOCKACT_SUSPEND;                               // Suspend DMAC channel 6 after block transfer
    memcpy(&(*descriptor_section)[6], &descriptor, sizeof(descriptor));             // Copy the descriptor to the descriptor section


    NVIC_SetPriority(DMAC_2_IRQn, 0);                                               // Set the Nested Vector Interrupt Controller (NVIC) priority for DMAC Channel 2
    NVIC_EnableIRQ(DMAC_2_IRQn);                                                    // Connect DMAC Channel 2 to Nested Vector Interrupt Controller (NVIC)
    DMAC->Channel[2].CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                         // Activate the suspend (SUSP) interrupt on DMAC channel 2
    //////////////////////////////////////////////////////////

    //////////////////////////////////////////////////////////
    // ADC1 Output Descriptors
    DMAC->Channel[3].CHPRILVL.reg = DMAC_CHPRILVL_PRILVL_LVL3;                      // Set DMAC channel 3 to priority level 3 (highest)
    DMAC->Channel[3].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(ADC1_DMAC_ID_RESRDY) |      // Set DMAC to trigger when ADC1 result is ready
                                   DMAC_CHCTRLA_TRIGACT_BURST;                      // DMAC burst transfer

    descriptor.descaddr = (uint32_t)&(*descriptor_section)[7];                      // Set up a linked descriptor
    descriptor.srcaddr = (uint32_t)&ADC1->RESULT.reg;                               // Take the result from the ADC1 RESULT register
    descriptor.dstaddr = (uint32_t)adcResults1 + sizeof(uint16_t) * NUM_SAMPLES;    // Place it in the first half of adcResults1 array
    descriptor.btcnt = NUM_SAMPLES;                                                 // Beat count
    descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD |                                // Beat size is HWORD (16-bits)
                        DMAC_BTCTRL_DSTINC |                                        // Increment the destination address
                        DMAC_BTCTRL_VALID |                                         // Descriptor is valid
                        DMAC_BTCTRL_BLOCKACT_SUSPEND;                               // Suspend DMAC channel 3 after block transfer
    memcpy(&(*descriptor_section)[3], &descriptor, sizeof(descriptor));             // Copy the descriptor to the descriptor section

    descriptor.descaddr = (uint32_t)&(*descriptor_section)[3];                      // Set up a linked descriptor
    descriptor.srcaddr = (uint32_t)&ADC1->RESULT.reg;                               // Take the result from the ADC1 RESULT register
    descriptor.dstaddr = (uint32_t)&adcResults1[NUM_SAMPLES] + sizeof(uint16_t) * NUM_SAMPLES;    // Place it in the second half of adcResults1 array
    descriptor.btcnt = NUM_SAMPLES;                                                 // Beat count
    descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD |                                // Beat size is HWORD (16-bits)
                        DMAC_BTCTRL_DSTINC |                                        // Increment the destination address
                        DMAC_BTCTRL_VALID |                                         // Descriptor is valid
                        DMAC_BTCTRL_BLOCKACT_SUSPEND;                               // Suspend DMAC channel 7 after block transfer
    memcpy(&(*descriptor_section)[7], &descriptor, sizeof(descriptor));             // Copy the descriptor to the descriptor section


    NVIC_SetPriority(DMAC_3_IRQn, 0);                                               // Set the Nested Vector Interrupt Controller (NVIC) priority for DMAC Channel 3
    NVIC_EnableIRQ(DMAC_3_IRQn);                                                    // Connect DMAC Channel 3 to Nested Vector Interrupt Controller (NVIC)
    DMAC->Channel[3].CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                         // Activate the suspend (SUSP) interrupt on DMAC channel 3
    //////////////////////////////////////////////////////////

}



void setup() {
    Serial.begin(9600);
    while(!Serial);                                                               // Wait for the console to open

    // Initialize SD card before setting up DMA
    if (!sd.begin(SD_CONFIG)) {
        Serial.println("SD initialization failed!");
        while (1);  // Halt if SD card initialization fails
    } else {
        Serial.println("SD initialization succeeded!");
    }

    // TODO make this use inputCtrl0 and inputCtrl1
    // Is this even necessary?
    pinMode(A0, INPUT);
    pinMode(A1, INPUT);
    pinMode(A2, INPUT);
    pinMode(A3, INPUT);

    adc_init();
    dma_init();
    dma_channels_enable();
    
    create_dat_file(&sd, &file);
}

// empty 18KB file creation takes 30-40 ms
// filling half a buffer takes 2-3 ms
// writing 1 half buffer takes 6-7 ms
// getting file size takes 0.1-0.4 ms
// file sync takes <0.1 ms

uint16_t a[NUM_SAMPLES*4]; // should be a0 a1 a2 a3, a0 a1 a2 a3, a0 a1 a2 a3
int rollovers = 0;

int dac = 1;
bool ascend = true;

bool R0_P0_dirty = false;
bool R0_P1_dirty = false;
bool R1_P0_dirty = false;
bool R1_P1_dirty = false;

void loop() {
    // DEBUGGING
    // stop the sketch on 12th file creation. for some reason it goes till 12? and not 10 or 11?
    if (rollovers > 10) {
        ascend = false;
        Serial.println("now read the data and see what is going on");
        Serial.println(micros());
        for (int i = 0; i < NUM_SAMPLES; i++) {
            Serial.print(a[i*4]);
            Serial.print(" ");
            Serial.print(a[i*4+1]);
            Serial.print(" ");
            Serial.print(a[i*4+2]);
            Serial.print(" ");
            Serial.print(a[i*4+3]);
            Serial.println(" ");

        }
        while (true);
    }

    // perform auto rollover check and count how many rollovers there have been
    rollovers += do_rollover_if_needed(&sd, &file, sizeof(a)) ? 1 : 0;

    // only write a if everything is dirty
    if (R0_P0_dirty && R0_P1_dirty && R1_P0_dirty && R1_P1_dirty) {
        // Serial.println(F("DIRTY"));
        // Serial.print("\t"); Serial.println(R0_P0_dirty);
        // Serial.print("\t"); Serial.println(R0_P1_dirty);
        // Serial.print("\t"); Serial.println(R1_P0_dirty);
        // Serial.print("\t"); Serial.println(R1_P1_dirty);
        // write the buffer to SD
        file.write(a, sizeof(a));
        R0_P0_dirty = false;
        R0_P1_dirty = false;
        R1_P0_dirty = false;
        R1_P1_dirty = false;

        Serial.println(micros());
    }



    if (results0Part0Ready) {
        R0_P0_dirty = true;

        // `i` should account for which half of buffer this is
        //   e.g. since this is first half of the buffer, it will go (0,1) to (2044,2045)
        //        and for r0p1, it should go (2048,2049) to (4092, 4093)
        for (uint16_t i = 0; i < NUM_SAMPLES; i++) {
            // fills in a like x x _ _ x x _ _ x x _ _
            // where x x is a0 a1, and _ _ is left for a2 a3
            uint16_t j = (i/2)*4 + i%2;

            // copy the data over 
            a[j] = adcResults0[i];
        }

        

        #if DEBUG_R0_P0
            Serial.print("hiii:");
            Serial.print(5000);
            Serial.print(",");

            Serial.print("lo:");
            Serial.print(0);
            Serial.print(",");

            Serial.print("R0_P0_reading0:");
            Serial.print(adcResults0[0]);
            Serial.print(", ");
            Serial.print("R0_P0_reading1:");
            Serial.print(adcResults0[1]);
            Serial.println();
        #endif

        results0Part0Ready = false;                                                   // Clear the results0 ready flag
    }

    if (results0Part1Ready) {
        R0_P1_dirty = true;

        // `i` should account for which half of buffer this is
        //   e.g. since this is second half of the buffer, it will go (2048,2049) to (4092, 4093)
        for (uint16_t i = NUM_SAMPLES; i < NUM_SAMPLES*2; i++) {
            // fills in `a` like x x _ _ x x _ _ x x _ _
            // where x x is a0 a1, and _ _ is left for a2 a3
            uint16_t j = (i/2)*4 + i%2;

            // copy the data over 
            a[j] = adcResults0[i];
        }


        #if DEBUG_R0_P1
            Serial.print("hiii:");
            Serial.print(5000);
            Serial.print(",");

            Serial.print("lo:");
            Serial.print(0);
            Serial.print(",");

            Serial.print("R0_P1_reading0:");
            Serial.print(adcResults0[1024]);
            Serial.print(", ");
            Serial.print("R0_P1_reading1:");
            Serial.print(adcResults0[1025]);
            Serial.println();
        #endif

        results0Part1Ready = false;                                                   // Clear the results1 ready flag
        
        Serial.println("AAAAA");
        while (true);
    }

    if (results1Part0Ready) {
        R1_P0_dirty = true;

        for (uint16_t i = 0; i < NUM_SAMPLES; i++) {
            // fills in a like _ _ x x _ _ x x _ _ x x
            // where _ _ is left for a0 a1, and x x is a2 a3
            uint16_t j = (i/2)*4 + 2 + i%2; 

            // copy the data over
            a[j] = adcResults1[i];
        }

        

        #if DEBUG_R1_P0
            Serial.print("hiii:");
            Serial.print(5000);
            Serial.print(",");

            Serial.print("lo:");
            Serial.print(0);
            Serial.print(",");

            Serial.print("R1_P0_reading0:");
            Serial.print(adcResults1[0]);
            Serial.print(", ");
            Serial.print("R1_P0_reading1:");
            Serial.print(adcResults1[1]);
            Serial.println();
        #endif

        results1Part0Ready = false;                                                   // Clear the results0 ready flag
    }

    if (results1Part1Ready) {
        R1_P1_dirty = true;

        // `i` should account for which half of buffer this is
        //   e.g. since this is second half of the buffer, it will go (2048,2049) to (4092, 4093)
        for (uint16_t i = NUM_SAMPLES; i < NUM_SAMPLES*2; i++) {
            // fills in `a` like _ _ x x _ _ x x _ _ x x
            // where _ _ is left for a0 a1, and x x is a2 a3
            uint16_t j = (i/2)*4 + i%2 + 2;

            // copy the data over 
            a[j] = adcResults1[i];
        }
        
        #if DEBUG_R1_P1
            Serial.print("hiii:");
            Serial.print(5000);
            Serial.print(",");

            Serial.print("lo:");
            Serial.print(0);
            Serial.print(",");

            Serial.print("R1_P1_reading0:");
            Serial.print(adcResults1[1024]);
            Serial.print(", ");
            Serial.print("R1_P1_reading1:");
            Serial.print(adcResults1[1025]);
            Serial.println();
        #endif

        results1Part1Ready = false;                                                   // Clear the results1 ready flag
    }   
}


void DMAC_2_Handler()                                                             // Interrupt handler for DMAC channel 2
{
    // Serial.println("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
    static uint8_t count0 = 0;                                                      // Initialise the count 
    if (DMAC->Channel[2].CHINTFLAG.bit.SUSP)                                        // Check if DMAC channel 2 has been suspended (SUSP) 
    {  
        DMAC->Channel[2].CHCTRLB.reg = DMAC_CHCTRLB_CMD_RESUME;                       // Restart the DMAC on channel 2
        DMAC->Channel[2].CHINTFLAG.bit.SUSP = 1;                                      // Clear the suspend (SUSP)interrupt flag
        if (count0)                                                                   // Test if the count0 is 1
        {
            results0Part1Ready = true;                                                  // Set the results 0 part 1 ready flag
        }
        else
        {
            results0Part0Ready = true;                                                  // Set the results 0 part 0 ready flag
        }
        count0 = (count0 + 1) % 2;                                                    // Toggle the count0 between 0 and 1 
    }
}

void DMAC_3_Handler()                                                             // Interrupt handler for DMAC channel 3
{
    // Serial.println("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
    static uint8_t count0 = 0;                                                      // Initialise the count 
    if (DMAC->Channel[3].CHINTFLAG.bit.SUSP)                                        // Check if DMAC channel 3 has been suspended (SUSP) 
    {  
        DMAC->Channel[3].CHCTRLB.reg = DMAC_CHCTRLB_CMD_RESUME;                       // Restart the DMAC on channel 3
        DMAC->Channel[3].CHINTFLAG.bit.SUSP = 1;                                      // Clear the suspend (SUSP)interrupt flag
        if (count0)                                                                   // Test if the count0 is 1
        {
            results1Part1Ready = true;                                                  // Set the results 0 part 1 ready flag
        }
        else
        {
            results1Part0Ready = true;                                                  // Set the results 0 part 0 ready flag
        }
        count0 = (count0 + 1) % 2;                                                    // Toggle the count0 between 0 and 1 
    }
}

The following is my current (working) code for raw results:

/*
This is a demo for reading samples from both ADCs on TWO PINS EACH.
The DMAC then takes these and dumps them into two buffers.
Trying to save something to disk but we'll see if that's possible

Made by paelen1234
*/


#include "FreeStack.h"
#include "SdFat.h"
#include "sdios.h"
#include "helper.h"

#define error(s) sd.errorHalt(&Serial, F(s))
const uint8_t SD_CS_PIN = 10;
#define SPI_CLOCK SD_SCK_MHZ(50)
#define SD_CONFIG SdSpiConfig(SD_CS_PIN, DEDICATED_SPI, SPI_CLOCK)
#define NUM_SAMPLES 1024 // the number of samples per pin.

SdFs sd;
FsFile file;

#define DEBUG_R0_P0 0
#define DEBUG_R0_P1 0
#define DEBUG_R1_P0 0
#define DEBUG_R1_P1 0
#define DEBUG_SD_BEGIN 1

volatile bool results0Part0Ready = false;
volatile bool results0Part1Ready = false;
volatile bool results1Part0Ready = false;
volatile bool results1Part1Ready = false;
uint16_t adcResults0[NUM_SAMPLES*2];                                                       // ADC0 results array
uint16_t adcResults1[NUM_SAMPLES*2];                                                       // ADC1 results array


typedef struct                                                                    // DMAC descriptor structure
{
    uint16_t btctrl;
    uint16_t btcnt;
    uint32_t srcaddr;
    uint32_t dstaddr;
    uint32_t descaddr;
} dmadesc;

/*
A0  ADC_INPUTCTRL_MUXPOS_AIN2
A1  ADC_INPUTCTRL_MUXPOS_AIN5
A2  ADC_INPUTCTRL_MUXPOS_AIN8  ADC_INPUTCTRL_MUXPOS_AIN0
A3  ADC_INPUTCTRL_MUXPOS_AIN9  ADC_INPUTCTRL_MUXPOS_AIN1
A4  ADC_INPUTCTRL_MUXPOS_AIN4
*/

// ADC INPUTCTRL register MUXPOS settings 
uint32_t inputCtrl0[] = { ADC_INPUTCTRL_MUXPOS_AIN0,                              // AIN0 = A0
                          ADC_INPUTCTRL_MUXPOS_AIN5 };                            // AIN5 = A1
uint32_t inputCtrl1[] = { ADC_INPUTCTRL_MUXPOS_AIN0,                              // AIN2 = A3
                          ADC_INPUTCTRL_MUXPOS_AIN1 };                            // AIN3 = A4



volatile dmadesc (*wrb)[DMAC_CH_NUM] __attribute__ ((aligned (16)));          // Write-back DMAC descriptors (changed to array pointer)
dmadesc (*descriptor_section)[DMAC_CH_NUM] __attribute__ ((aligned (16)));    // DMAC channel descriptors (changed to array pointer)
dmadesc descriptor __attribute__ ((aligned (16)));                            // Place holder descriptor

void adc_init() {
    //////////////////////////////////////////////////////////
    // ADC0 Settings
    ADC0->INPUTCTRL.bit.MUXPOS = inputCtrl0[0];                                     // Set the analog input to A0
    while(ADC0->SYNCBUSY.bit.INPUTCTRL);                                            // Wait for synchronization
    ADC0->SAMPCTRL.bit.SAMPLEN = 0x01;                                              // Extend sampling time by SAMPLEN ADC cycles (12 + 1 + 2)/750kHz = 20us = 50kHz
    while(ADC0->SYNCBUSY.bit.SAMPCTRL);                                             // Wait for synchronization  
    ADC0->DSEQCTRL.reg = ADC_DSEQCTRL_AUTOSTART |                                   // Auto start a DMAC conversion upon ADC0 DMAC sequence completion
                         ADC_DSEQCTRL_INPUTCTRL;                                    // Change the ADC0 INPUTCTRL register on DMAC sequence
    ADC0->CTRLB.reg = ADC_CTRLB_RESSEL_12BIT;                                       // Set ADC resolution to 12 bits 
    while(ADC0->SYNCBUSY.bit.CTRLB);                                                // Wait for synchronization
    ADC0->CTRLA.reg = ADC_CTRLA_PRESCALER_DIV4;                                     // Divide Clock ADC GCLK by 4 (48MHz/64 = 750kHz) (12 + 1)/750kHz = 17.3us sample time 
    ADC0->CTRLA.bit.ENABLE = 1;                                                     // Enable the ADC
    while(ADC0->SYNCBUSY.bit.ENABLE);                                               // Wait for synchronization
    ADC0->SWTRIG.bit.START = 1;                                                     // Initiate a software trigger to start an ADC conversion
    while(ADC0->SYNCBUSY.bit.SWTRIG);                                               // Wait for synchronization
    ADC0->DBGCTRL.bit.DBGRUN = 0;
    //////////////////////////////////////////////////////////

    //////////////////////////////////////////////////////////
    // ADC1 Settings
    ADC1->INPUTCTRL.bit.MUXPOS = inputCtrl1[0];                                     // Set the analog input to A2
    while(ADC1->SYNCBUSY.bit.INPUTCTRL);                                            // Wait for synchronization
    ADC1->SAMPCTRL.bit.SAMPLEN = 0x01;                                              // Extend sampling time by SAMPLEN ADC cycles (12 + 1 + 2)/750kHz = 20us = 50kHz 
    while(ADC1->SYNCBUSY.bit.SAMPCTRL);                                             // Wait for synchronization
    ADC1->DSEQCTRL.reg = ADC_DSEQCTRL_AUTOSTART |                                   // Auto start a DMAC conversion upon ADC0 DMAC sequence completion
                         ADC_DSEQCTRL_INPUTCTRL;                                    // Change the ADC0 INPUTCTRL register on DMAC sequence
    ADC1->CTRLB.reg = ADC_CTRLB_RESSEL_12BIT;                                       // Set ADC resolution to 12 bits 
    while(ADC1->SYNCBUSY.bit.CTRLB);                                                // Wait for synchronization
    ADC1->CTRLA.reg = ADC_CTRLA_PRESCALER_DIV4;                                     // Divide Clock ADC GCLK by 4 (48MHz/64 = 750kHz) (12 + 1)/750kHz = 17.3us sample time 
    ADC1->CTRLA.bit.ENABLE = 1;                                                     // Enable the ADC
    while(ADC1->SYNCBUSY.bit.ENABLE);                                               // Wait for synchronization
    ADC1->SWTRIG.bit.START = 1;                                                     // Initiate a software trigger to start an ADC conversion
    while(ADC1->SYNCBUSY.bit.SWTRIG);                                               // Wait for synchronization
    ADC1->DBGCTRL.bit.DBGRUN = 0;
    //////////////////////////////////////////////////////////
}

void dma_channels_enable() {
    DMAC->Channel[2].CHCTRLA.bit.ENABLE = 1;                                        // Enable DMAC channel 2 (ADC0 First Output Descriptor)
    DMAC->Channel[3].CHCTRLA.bit.ENABLE = 1;                                        // Enable DMAC channel 3 (ADC1 First Output Descriptor)
    DMAC->Channel[4].CHCTRLA.bit.ENABLE = 1;                                        // Enable DMAC channel 4 (ADC0 Sequencing / Input Descriptor)
    DMAC->Channel[5].CHCTRLA.bit.ENABLE = 1;                                        // Enable DMAC channel 5 (ADC1 Sequencing / Input Descriptor)
    delay(1);                                                                       // Wait a millisecond
}

void dma_init(){
    descriptor_section =  reinterpret_cast<dmadesc(*)[DMAC_CH_NUM]>(DMAC->BASEADDR.reg); //point array pointer to BASEADDR defined by SD.begin
    wrb =  reinterpret_cast<dmadesc(*)[DMAC_CH_NUM]>(DMAC->WRBADDR.reg);                 //point array pointer to WRBADDR defined by SD.begin
    DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf); 

    //////////////////////////////////////////////////////////
    // ADC0 DMA Sequencing setup (Input Descriptor)  
    DMAC->Channel[4].CHPRILVL.reg = DMAC_CHPRILVL_PRILVL_LVL3;                      // Set DMAC channel 4 to priority level 3 (highest)
    DMAC->Channel[4].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(ADC0_DMAC_ID_SEQ) |         // Set DMAC to trigger on ADC0 DMAC sequence
                                   DMAC_CHCTRLA_BURSTLEN_SINGLE |                   // One beat per burst
                                   DMAC_CHCTRLA_TRIGACT_BURST;                      // DMAC burst transfer
    DMAC->Channel[4].CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                         // Activate the suspend (SUSP) interrupt on DMAC channel 4

    descriptor.descaddr = (uint32_t)&(*descriptor_section)[4];                      // Set up a circular descriptor
    descriptor.srcaddr = (uint32_t)inputCtrl0 + sizeof(uint32_t) * 2;               // Configure the DMAC to set the (???)
    descriptor.dstaddr = (uint32_t)&ADC0->DSEQDATA.reg;                             // Write the INPUT CTRL 
    descriptor.btcnt = 2;                                                           // Beat count is 2 (???) (code originally had it at 4) TODO TODO TODO TODO TODO
    descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_WORD |                                 // Beat size is WORD (32-bits)
                        DMAC_BTCTRL_SRCINC |                                        // Increment the source address
                        DMAC_BTCTRL_STEPSEL_SRC |                                   // Indicates that the source address is using these step settings
                        DMAC_BTCTRL_STEPSIZE_X1 |                                   // The Step size is set to 1x beats
                        DMAC_BTCTRL_VALID;                                          // Descriptor is valid
    
    memcpy(&(*descriptor_section)[4], &descriptor, sizeof(descriptor));             // Copy the descriptor to the descriptor section
    //////////////////////////////////////////////////////////

    //////////////////////////////////////////////////////////
    // ADC1 DMA Sequencing setup (Input Descriptor)  
    DMAC->Channel[5].CHPRILVL.reg = DMAC_CHPRILVL_PRILVL_LVL3;                      // Set DMAC channel 5 to priority level 3 (highest)
    DMAC->Channel[5].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(ADC1_DMAC_ID_SEQ) |         // Set DMAC to trigger on ADC1 DMAC sequence
                                   DMAC_CHCTRLA_BURSTLEN_SINGLE |                   // One beat per burst
                                   DMAC_CHCTRLA_TRIGACT_BURST;                      // DMAC burst transfer
    DMAC->Channel[5].CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                         // Activate the suspend (SUSP) interrupt on DMAC channel 5

    descriptor.descaddr = (uint32_t)&(*descriptor_section)[5];                      // Set up a circular descriptor
    descriptor.srcaddr = (uint32_t)inputCtrl1 + sizeof(uint32_t) * 2;               // Configure the DMAC to set the (???)
    descriptor.dstaddr = (uint32_t)&ADC1->DSEQDATA.reg;                             // Write the INPUT CTRL 
    descriptor.btcnt = 2;                                                           // Beat count is 2 (???) (code originally had it at 4) TODO TODO TODO TODO TODO
    descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_WORD |                                 // Beat size is WORD (32-bits)
                        DMAC_BTCTRL_SRCINC |                                        // Increment the source address
                        DMAC_BTCTRL_STEPSEL_SRC |                                   // Indicates that the source address is using these step settings
                        DMAC_BTCTRL_STEPSIZE_X1 |                                   // The Step size is set to 1x beats
                        DMAC_BTCTRL_VALID;                                          // Descriptor is valid
    
    memcpy(&(*descriptor_section)[5], &descriptor, sizeof(descriptor));             // Copy the descriptor to the descriptor section
    //////////////////////////////////////////////////////////

    //////////////////////////////////////////////////////////
    // ADC0 Output Descriptors
    DMAC->Channel[2].CHPRILVL.reg = DMAC_CHPRILVL_PRILVL_LVL3;                      // Set DMAC channel 2 to priority level 2 (highest)
    DMAC->Channel[2].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(ADC0_DMAC_ID_RESRDY) |      // Set DMAC to trigger when ADC0 result is ready
                                   DMAC_CHCTRLA_TRIGACT_BURST;                      // DMAC burst transfer

    descriptor.descaddr = (uint32_t)&(*descriptor_section)[6];                      // Set up a linked descriptor
    descriptor.srcaddr = (uint32_t)&ADC0->RESULT.reg;                               // Take the result from the ADC0 RESULT register
    descriptor.dstaddr = (uint32_t)adcResults0 + sizeof(uint16_t) * NUM_SAMPLES;    // Place it in the middle of adcResults0 array
    descriptor.btcnt = NUM_SAMPLES;                                                 // Beat count
    descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD |                                // Beat size is HWORD (16-bits)
                        DMAC_BTCTRL_DSTINC |                                        // Increment the destination address
                        DMAC_BTCTRL_VALID |                                         // Descriptor is valid
                        DMAC_BTCTRL_BLOCKACT_SUSPEND;                               // Suspend DMAC channel 2 after block transfer
    memcpy(&(*descriptor_section)[2], &descriptor, sizeof(descriptor));             // Copy the descriptor to the descriptor section

    descriptor.descaddr = (uint32_t)&(*descriptor_section)[2];                      // Set up a linked descriptor
    descriptor.srcaddr = (uint32_t)&ADC0->RESULT.reg;                               // Take the result from the ADC0 RESULT register
    descriptor.dstaddr = (uint32_t)&adcResults0[NUM_SAMPLES] + sizeof(uint16_t) * NUM_SAMPLES;    // Place it in the adcResults0 array
    descriptor.btcnt = NUM_SAMPLES;                                                 // Beat count
    descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD |                                // Beat size is HWORD (16-bits)
                        DMAC_BTCTRL_DSTINC |                                        // Increment the destination address
                        DMAC_BTCTRL_VALID |                                         // Descriptor is valid
                        DMAC_BTCTRL_BLOCKACT_SUSPEND;                               // Suspend DMAC channel 6 after block transfer
    memcpy(&(*descriptor_section)[6], &descriptor, sizeof(descriptor));             // Copy the descriptor to the descriptor section


    NVIC_SetPriority(DMAC_2_IRQn, 0);                                               // Set the Nested Vector Interrupt Controller (NVIC) priority for DMAC Channel 2
    NVIC_EnableIRQ(DMAC_2_IRQn);                                                    // Connect DMAC Channel 2 to Nested Vector Interrupt Controller (NVIC)
    DMAC->Channel[2].CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                         // Activate the suspend (SUSP) interrupt on DMAC channel 2
    //////////////////////////////////////////////////////////

    //////////////////////////////////////////////////////////
    // ADC1 Output Descriptors
    DMAC->Channel[3].CHPRILVL.reg = DMAC_CHPRILVL_PRILVL_LVL3;                      // Set DMAC channel 3 to priority level 3 (highest)
    DMAC->Channel[3].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(ADC1_DMAC_ID_RESRDY) |      // Set DMAC to trigger when ADC1 result is ready
                                   DMAC_CHCTRLA_TRIGACT_BURST;                      // DMAC burst transfer

    descriptor.descaddr = (uint32_t)&(*descriptor_section)[7];                      // Set up a linked descriptor
    descriptor.srcaddr = (uint32_t)&ADC1->RESULT.reg;                               // Take the result from the ADC1 RESULT register
    descriptor.dstaddr = (uint32_t)adcResults1 + sizeof(uint16_t) * NUM_SAMPLES;    // Place it in the middle of adcResults1 array
    descriptor.btcnt = NUM_SAMPLES;                                                 // Beat count
    descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD |                                // Beat size is HWORD (16-bits)
                        DMAC_BTCTRL_DSTINC |                                        // Increment the destination address
                        DMAC_BTCTRL_VALID |                                         // Descriptor is valid
                        DMAC_BTCTRL_BLOCKACT_SUSPEND;                               // Suspend DMAC channel 3 after block transfer
    memcpy(&(*descriptor_section)[3], &descriptor, sizeof(descriptor));             // Copy the descriptor to the descriptor section

    descriptor.descaddr = (uint32_t)&(*descriptor_section)[3];                      // Set up a linked descriptor
    descriptor.srcaddr = (uint32_t)&ADC1->RESULT.reg;                               // Take the result from the ADC1 RESULT register
    descriptor.dstaddr = (uint32_t)&adcResults1[NUM_SAMPLES] + sizeof(uint16_t) * NUM_SAMPLES;    // Place it in the adcResults1 array
    descriptor.btcnt = NUM_SAMPLES;                                                 // Beat count
    descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD |                                // Beat size is HWORD (16-bits)
                        DMAC_BTCTRL_DSTINC |                                        // Increment the destination address
                        DMAC_BTCTRL_VALID |                                         // Descriptor is valid
                        DMAC_BTCTRL_BLOCKACT_SUSPEND;                               // Suspend DMAC channel 7 after block transfer
    memcpy(&(*descriptor_section)[7], &descriptor, sizeof(descriptor));             // Copy the descriptor to the descriptor section


    NVIC_SetPriority(DMAC_3_IRQn, 0);                                               // Set the Nested Vector Interrupt Controller (NVIC) priority for DMAC Channel 3
    NVIC_EnableIRQ(DMAC_3_IRQn);                                                    // Connect DMAC Channel 3 to Nested Vector Interrupt Controller (NVIC)
    DMAC->Channel[3].CHINTENSET.reg = DMAC_CHINTENSET_SUSP;                         // Activate the suspend (SUSP) interrupt on DMAC channel 3
    //////////////////////////////////////////////////////////

}



void setup() {
    Serial.begin(9600);
    while(!Serial);                                                               // Wait for the console to open

    // Initialize SD card before setting up DMA
    if (!sd.begin(SD_CONFIG)) {
        Serial.println("SD initialization failed!");
        while (1);  // Halt if SD card initialization fails
    } else {
        Serial.println("SD initialization succeeded!");
    }

    // TODO make this use inputCtrl0 and inputCtrl1
    pinMode(A0, INPUT);
    pinMode(A1, INPUT);
    pinMode(A2, INPUT);
    pinMode(A3, INPUT);

    // // Configure PB08 (A2) and PB09 (A3) for analog input
    // PORT->Group[1].PINCFG[8].bit.PMUXEN = 1;  // Enable peripheral multiplexer for PB08
    // PORT->Group[1].PINCFG[9].bit.PMUXEN = 1;  // Enable peripheral multiplexer for PB09

    // // Set peripheral function B (0x1) for analog
    // PORT->Group[1].PMUX[4].reg = PORT_PMUX_PMUXE(1) | PORT_PMUX_PMUXO(1);  // PB08 is even, PB09 is odd, they share PMUX[4]

    // Debug output
    // Serial.print("PB08 PINCFG: 0x");
    // Serial.println(PORT->Group[1].PINCFG[8].reg, HEX);
    // Serial.print("PB09 PINCFG: 0x");
    // Serial.println(PORT->Group[1].PINCFG[9].reg, HEX);
    // Serial.print("PB08/PB09 PMUX: 0x");
    // Serial.println(PORT->Group[1].PMUX[4].reg, HEX);


    adc_init();
    dma_init();
    dma_channels_enable();
    
    create_dat_file(&sd, &file);

    analogWriteResolution(12);
}

// empty 18KB file creation takes 30-40 ms
// filling half a buffer takes 2-3 ms
// writing 1 half buffer takes 6-7 ms
// getting file size takes 0.1-0.4 ms
// file sync takes <0.1 ms

uint16_t a[NUM_SAMPLES*4]; // should be a0 a1 a2 a3, a0 a1 a2 a3, a0 a1 a2 a3
int rollovers = 0;

int dac = 1;
bool ascend = true;

bool R0_P0_dirty = false;
bool R0_P1_dirty = false;
bool R1_P0_dirty = false;
bool R1_P1_dirty = false;

void loop() {
    // stop the sketch on 12th file creation. for some reason it goes till 12? and not 10 or 11?
    if (rollovers > 10) {
        ascend = false;
        Serial.println("now read the data and see what is going on");
        Serial.println(micros());
        for (int i = 0; i < NUM_SAMPLES; i++) {
            Serial.print(a[i*4]);
            Serial.print(" ");
            Serial.print(a[i*4+1]);
            Serial.print(" ");
            Serial.print(a[i*4+2]);
            Serial.print(" ");
            Serial.print(a[i*4+3]);
            Serial.println(" ");

        }
        while (true);
    }

    // ////////
    // // DAC stuff on A0 for debugging
    // if (dac <= 1) {
    //     ascend = true;
    // }
    // if (dac >= 4095) {
    //     ascend = false;
    // }
    // dac += ascend ? 5 : -5;
    // analogWrite(A0, dac);
    // ////////

    // perform auto rollover check and count how many rollovers there have been
    rollovers += do_rollover_if_needed(&sd, &file, sizeof(a)) ? 1 : 0;

    // only write a if everything is dirty
    if (R0_P0_dirty && R0_P1_dirty && R1_P0_dirty && R1_P1_dirty) {
        // Serial.println(F("DIRTY"));
        // Serial.print("\t"); Serial.println(R0_P0_dirty);
        // Serial.print("\t"); Serial.println(R0_P1_dirty);
        // Serial.print("\t"); Serial.println(R1_P0_dirty);
        // Serial.print("\t"); Serial.println(R1_P1_dirty);
        // write the buffer to SD
        file.write(a, sizeof(a));
        R0_P0_dirty = false;
        R0_P1_dirty = false;
        R1_P0_dirty = false;
        R1_P1_dirty = false;
        Serial.println(micros());
    }



    if (results0Part0Ready) {
        R0_P0_dirty = true;

        // `i` should account for which half of buffer this is
        //   e.g. since this is first half of the buffer, it will go (0,1) to (2044,2045)
        //        and for r0p1, it should go (2048,2049) to (4092, 4093)
        for (uint16_t i = 0; i < NUM_SAMPLES; i++) {
            // fills in a like x x _ _ x x _ _ x x _ _
            // where x x is a0 a1, and _ _ is left for a2 a3
            uint16_t j = (i/2)*4 + i%2;

            // copy the data over 
            a[j] = adcResults0[i];
        }

        

        #if DEBUG_R0_P0
            Serial.print("hiii:");
            Serial.print(5000);
            Serial.print(",");

            Serial.print("lo:");
            Serial.print(0);
            Serial.print(",");

            Serial.print("R0_P0_reading0:");
            Serial.print(adcResults0[0]);
            Serial.print(", ");
            Serial.print("R0_P0_reading1:");
            Serial.print(adcResults0[1]);
            Serial.println();
        #endif

        results0Part0Ready = false;                                                   // Clear the results0 ready flag
    }

    if (results0Part1Ready) {
        R0_P1_dirty = true;

        // `i` should account for which half of buffer this is
        //   e.g. since this is second half of the buffer, it will go (2048,2049) to (4092, 4093)
        for (uint16_t i = NUM_SAMPLES; i < NUM_SAMPLES*2; i++) {
            // fills in `a` like x x _ _ x x _ _ x x _ _
            // where x x is a0 a1, and _ _ is left for a2 a3
            uint16_t j = (i/2)*4 + i%2;

            // copy the data over 
            a[j] = adcResults0[i];
        }

        #if DEBUG_R0_P1
            Serial.print("hiii:");
            Serial.print(5000);
            Serial.print(",");

            Serial.print("lo:");
            Serial.print(0);
            Serial.print(",");

            Serial.print("R0_P1_reading0:");
            Serial.print(adcResults0[1024]);
            Serial.print(", ");
            Serial.print("R0_P1_reading1:");
            Serial.print(adcResults0[1025]);
            Serial.println();
        #endif

        results0Part1Ready = false;                                                   // Clear the results1 ready flag
    }

    if (results1Part0Ready) {
        R1_P0_dirty = true;

        for (uint16_t i = 0; i < NUM_SAMPLES; i++) {
            // fills in a like _ _ x x _ _ x x _ _ x x
            // where _ _ is left for a0 a1, and x x is a2 a3
            uint16_t j = (i/2)*4 + 2 + i%2; 

            // copy the data over
            a[j] = adcResults1[i];
        }

        

        #if DEBUG_R1_P0
            Serial.print("hiii:");
            Serial.print(5000);
            Serial.print(",");

            Serial.print("lo:");
            Serial.print(0);
            Serial.print(",");

            Serial.print("R1_P0_reading0:");
            Serial.print(adcResults1[0]);
            Serial.print(", ");
            Serial.print("R1_P0_reading1:");
            Serial.print(adcResults1[1]);
            Serial.println();
        #endif

        results1Part0Ready = false;                                                   // Clear the results0 ready flag
    }

    if (results1Part1Ready) {
        R1_P1_dirty = true;

        // `i` should account for which half of buffer this is
        //   e.g. since this is second half of the buffer, it will go (2048,2049) to (4092, 4093)
        for (uint16_t i = NUM_SAMPLES; i < NUM_SAMPLES*2; i++) {
            // fills in `a` like _ _ x x _ _ x x _ _ x x
            // where _ _ is left for a0 a1, and x x is a2 a3
            uint16_t j = (i/2)*4 + i%2 + 2;

            // copy the data over 
            a[j] = adcResults1[i];
        }
        
        #if DEBUG_R1_P1
            Serial.print("hiii:");
            Serial.print(5000);
            Serial.print(",");

            Serial.print("lo:");
            Serial.print(0);
            Serial.print(",");

            Serial.print("R1_P1_reading0:");
            Serial.print(adcResults1[1024]);
            Serial.print(", ");
            Serial.print("R1_P1_reading1:");
            Serial.print(adcResults1[1025]);
            Serial.println();
        #endif

        results1Part1Ready = false;                                                   // Clear the results1 ready flag
    }   
}


void DMAC_2_Handler()                                                             // Interrupt handler for DMAC channel 2
{
    // Serial.println("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
    static uint8_t count0 = 0;                                                      // Initialise the count 
    if (DMAC->Channel[2].CHINTFLAG.bit.SUSP)                                        // Check if DMAC channel 2 has been suspended (SUSP) 
    {  
        DMAC->Channel[2].CHCTRLB.reg = DMAC_CHCTRLB_CMD_RESUME;                       // Restart the DMAC on channel 2
        DMAC->Channel[2].CHINTFLAG.bit.SUSP = 1;                                      // Clear the suspend (SUSP)interrupt flag
        if (count0)                                                                   // Test if the count0 is 1
        {
            results0Part1Ready = true;                                                  // Set the results 0 part 1 ready flag
        }
        else
        {
            results0Part0Ready = true;                                                  // Set the results 0 part 0 ready flag
        }
        count0 = (count0 + 1) % 2;                                                    // Toggle the count0 between 0 and 1 
    }
}

void DMAC_3_Handler()                                                             // Interrupt handler for DMAC channel 3
{
    // Serial.println("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB");
    static uint8_t count0 = 0;                                                      // Initialise the count 
    if (DMAC->Channel[3].CHINTFLAG.bit.SUSP)                                        // Check if DMAC channel 3 has been suspended (SUSP) 
    {  
        DMAC->Channel[3].CHCTRLB.reg = DMAC_CHCTRLB_CMD_RESUME;                       // Restart the DMAC on channel 3
        DMAC->Channel[3].CHINTFLAG.bit.SUSP = 1;                                      // Clear the suspend (SUSP)interrupt flag
        if (count0)                                                                   // Test if the count0 is 1
        {
            results1Part1Ready = true;                                                  // Set the results 0 part 1 ready flag
        }
        else
        {
            results1Part0Ready = true;                                                  // Set the results 0 part 0 ready flag
        }
        count0 = (count0 + 1) % 2;                                                    // Toggle the count0 between 0 and 1 
    }
}

My hunch is that I'm somehow either improperly configuring the ADCs for averaging mode or that I'm overlooking something in the DMA descriptors that should be modified, but after looking at it all weekend I can't see where I'm going wrong. Please let me know if anything looks incorrect, or if anyone has ideas on what could be going wrong to cause this issue. Thanks!

That is not an Arduino Zero and hence your topic has been moved to a more suitable category on the forum.

My bad, I just copied where what I saw on the post that this branched from.