#include <Adafruit_ZeroDMA.h>
#include <malloc.h> // memalign() function

#include "utility/dma.h"
static volatile uint32_t _channelMask = 0; // Bitmask of allocated channels

// DMA descriptor list entry point (and writeback buffer) per channel
__attribute__((__aligned__(16))) static DmacDescriptor // 128 bit alignment
  _descriptor[DMAC_CH_NUM] SECTION_DMAC_DESCRIPTOR,
  _writeback[DMAC_CH_NUM]  SECTION_DMAC_DESCRIPTOR;

// Pointer to ZeroDMA object for each channel is needed for the
// ISR (in C, outside of class context) to access callbacks.
static Adafruit_ZeroDMA *_dmaPtr[DMAC_CH_NUM] = { 0 }; // Init to NULL

// Adapted from ASF3 interrupt_sam_nvic.c:

static volatile unsigned long cpu_irq_critical_section_counter = 0;
static volatile unsigned char cpu_irq_prev_interrupt_state     = 0;

static void cpu_irq_enter_critical(void) {
    if(!cpu_irq_critical_section_counter) {
        if(__get_PRIMASK() == 0) { // IRQ enabled?
            __disable_irq();   // Disable it
            __DMB();
            cpu_irq_prev_interrupt_state = 1;
        } else {
            // Make sure the to save the prev state as false
            cpu_irq_prev_interrupt_state = 0;
        }
    }

    cpu_irq_critical_section_counter++;
}

static void cpu_irq_leave_critical(void) {
    // Check if the user is trying to leave a critical section
    // when not in a critical section
    if(cpu_irq_critical_section_counter > 0) {
        cpu_irq_critical_section_counter--;

        // Only enable global interrupts when the counter
        // reaches 0 and the state of the global interrupt flag
        // was enabled when entering critical state */
        if((!cpu_irq_critical_section_counter) &&
            cpu_irq_prev_interrupt_state) {
                __DMB();
                __enable_irq();
        }
    }
}

// CONSTRUCTOR -------------------------------------------------------------

// Constructor initializes Adafruit_ZeroDMA basics but does NOT allocate a
// DMA channel (that's done in allocate()) or start a job (that's done in
// startJob()).  This is because constructors in a global context are called
// before a sketch's setup() function, which may have some other hardware
// initialization of its own, don't want it clobbering us.
Adafruit_ZeroDMA::Adafruit_ZeroDMA(void) {
    channel           = 0xFF;  // Channel not yet allocated
    jobStatus         = DMA_STATUS_OK;
    hasDescriptors    = false; // No descriptors allocated yet
    loopFlag          = false;
    peripheralTrigger = 0;     // Software trigger only by default
    triggerAction     = DMA_TRIGGER_ACTON_TRANSACTION;
    memset(callback, 0, sizeof(callback));
}

// TODO: add destructor? Should stop job, delete descriptors, free channel.

// INTERRUPT SERVICE ROUTINE -----------------------------------------------

// This is a C function that exists outside the Adafruit_ZeroDMA context.
// DMA channel number is determined from the INTPEND register, from this
// we get a ZeroDMA object pointer through the _dmaPtr[] array.
// (It's done this way because jobStatus and callback[] are protected
// elements in the ZeroDMA object -- we can't touch them in C, but the
// next function after this, being part of the ZeroDMA class, can.)

#ifdef __SAMD51__
void DMAC_0_Handler(void) {
#else
void DMAC_Handler(void) {
#endif
    cpu_irq_enter_critical();

    uint8_t channel = DMAC->INTPEND.bit.ID; // Channel # causing interrupt
    if(channel < DMAC_CH_NUM) {
        Adafruit_ZeroDMA *dma;
        if((dma = _dmaPtr[channel])) { // -> Channel's ZeroDMA object
#ifdef __SAMD51__
            // Call IRQ handler with channel #
            dma->_IRQhandler(channel);
#else
            DMAC->CHID.bit.ID = channel;
            // Call IRQ handler with interrupt flag(s)
            dma->_IRQhandler(DMAC->CHINTFLAG.reg);
#endif
        }
    }

    cpu_irq_leave_critical();
}

#ifdef __SAMD51__
void DMAC_1_Handler(void) __attribute__((weak, alias("DMAC_0_Handler")));
void DMAC_2_Handler(void) __attribute__((weak, alias("DMAC_0_Handler")));
void DMAC_3_Handler(void) __attribute__((weak, alias("DMAC_0_Handler")));
void DMAC_4_Handler(void) __attribute__((weak, alias("DMAC_0_Handler")));
#endif

void Adafruit_ZeroDMA::_IRQhandler(uint8_t flags) {
#ifdef __SAMD51__
    // 'flags' is initially passed in as channel number,
    // from which we look up the actual interrupt flags...
    flags = DMAC->Channel[flags].CHINTFLAG.reg;
#endif
    if(flags & DMAC_CHINTENCLR_TERR) {
        // Clear error flag
#ifdef __SAMD51__
        DMAC->Channel[channel].CHINTFLAG.reg = DMAC_CHINTENCLR_TERR;
#else
        DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_TERR;
#endif
        jobStatus           = DMA_STATUS_ERR_IO;
        if(callback[DMA_CALLBACK_TRANSFER_ERROR]) {
            callback[DMA_CALLBACK_TRANSFER_ERROR](this);
        }
    } else if(flags & DMAC_CHINTENCLR_TCMPL) {
        // Clear transfer complete flag
#ifdef __SAMD51__
        DMAC->Channel[channel].CHINTFLAG.reg = DMAC_CHINTENCLR_TCMPL;
#else
        DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_TCMPL;
#endif
        jobStatus           = DMA_STATUS_OK;
        if(callback[DMA_CALLBACK_TRANSFER_DONE]) {
            callback[DMA_CALLBACK_TRANSFER_DONE](this);
        }
    } else if(flags & DMAC_CHINTENCLR_SUSP) {
        // Clear channel suspend flag
#ifdef __SAMD51__
        DMAC->Channel[channel].CHINTFLAG.reg = DMAC_CHINTENCLR_SUSP;
#else
        DMAC->CHINTFLAG.reg = DMAC_CHINTENCLR_SUSP;
#endif
        jobStatus           = DMA_STATUS_SUSPEND;
        if(callback[DMA_CALLBACK_CHANNEL_SUSPEND]) {
            callback[DMA_CALLBACK_CHANNEL_SUSPEND](this);
        }
    }
}

// DMA CHANNEL FUNCTIONS ---------------------------------------------------

// Allocates channel for ZeroDMA object
ZeroDMAstatus Adafruit_ZeroDMA::allocate(void) {

    if(channel < DMAC_CH_NUM) return DMA_STATUS_OK; // Already alloc'd!

    // Find index of first free DMA channel.  As currently written,
    // this "does not play well with others" as it assumes _channelMask
    // is the final arbiter of channels in use (this is true only within
    // this library -- but other DMA-driven code may have allocated its
    // own channel(s) elsewhere, sometimes with an equally broken
    // approach).  A possible alternate approach, I haven't tested this
    // yet, might be to loop through each channel, set DMAC->CHID.bit.ID
    // and then test whether CHCTRLA.bit.ENABLE is set?  But for now...
    for(channel=0; (channel < DMAC_CH_NUM) &&
      (_channelMask & (1 << channel)); channel++);
    // Doesn't help that code later does a software reset of the DMA
    // controller, which would blow out other DMA-using libraries
    // anyway (or they're just as likely to blow out this one).
    // I think it's just an all-or-nothing affair...use one library
    // for DMA everything, never mix and match.

    if(channel >= DMAC_CH_NUM) { // No free channel!
        return DMA_STATUS_ERR_NOT_FOUND;
    }

    cpu_irq_enter_critical();

    if(!_channelMask) { // No channels allocated yet; initialize DMA!
#if (SAML21) || (SAML22) || (SAMC20) || (SAMC21)
        PM->AHBMASK.bit.DMAC_       = 1;
#elif defined(__SAMD51__)
        MCLK->AHBMASK.bit.DMAC_     = 1; // Initialize DMA clocks
#else
        PM->AHBMASK.bit.DMAC_       = 1; // Initialize DMA clocks
        PM->APBBMASK.bit.DMAC_      = 1;
#endif
        DMAC->CTRL.bit.DMAENABLE    = 0; // Disable DMA controller
        DMAC->CTRL.bit.SWRST        = 1; // Perform software reset

        // Initialize descriptor list addresses
        DMAC->BASEADDR.bit.BASEADDR = (uint32_t)_descriptor;
        DMAC->WRBADDR.bit.WRBADDR   = (uint32_t)_writeback;
        memset(_descriptor, 0, sizeof(_descriptor));
        memset(_writeback , 0, sizeof(_writeback));

        // Re-enable DMA controller with all priority levels
        DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xF);

        // Enable DMA interrupt at lowest priority
#ifdef __SAMD51__
        IRQn_Type irqs[] = { DMAC_0_IRQn, DMAC_1_IRQn, DMAC_2_IRQn,
                             DMAC_3_IRQn, DMAC_4_IRQn };
        for(uint8_t i=0; i<(sizeof irqs / sizeof irqs[0]); i++) {
            NVIC_EnableIRQ(irqs[i]);
            NVIC_SetPriority(irqs[i], (1<<__NVIC_PRIO_BITS)-1);
        }
#else
        NVIC_EnableIRQ(DMAC_IRQn);
        NVIC_SetPriority(DMAC_IRQn, (1 << __NVIC_PRIO_BITS) - 1);
#endif
    }

    _channelMask    |= 1 << channel; // Mark channel as allocated
    _dmaPtr[channel] = this;         // Channel-index-to-object pointer

    // Reset the allocated channel
#ifdef __SAMD51__
    DMAC->Channel[channel].CHCTRLA.bit.ENABLE  = 0;
    DMAC->Channel[channel].CHCTRLA.bit.SWRST   = 1;
#else
    DMAC->CHID.bit.ID         = channel;
    DMAC->CHCTRLA.bit.ENABLE  = 0;
    DMAC->CHCTRLA.bit.SWRST   = 1;
#endif

    // Clear software trigger
    DMAC->SWTRIGCTRL.reg     &= ~(1 << channel);

    // Configure default behaviors
#ifdef __SAMD51__
    DMAC->Channel[channel].CHPRILVL.bit.PRILVL = 0;
    DMAC->Channel[channel].CHCTRLA.bit.TRIGSRC = peripheralTrigger;
    DMAC->Channel[channel].CHCTRLA.bit.TRIGACT = triggerAction;
    DMAC->Channel[channel].CHCTRLA.bit.BURSTLEN =
      DMAC_CHCTRLA_BURSTLEN_SINGLE_Val; // Single-beat burst length
#else
    DMAC->CHCTRLB.bit.LVL     = 0;
    DMAC->CHCTRLB.bit.TRIGSRC = peripheralTrigger;
    DMAC->CHCTRLB.bit.TRIGACT = triggerAction;
#endif

    cpu_irq_leave_critical();

    return DMA_STATUS_OK;
}

void Adafruit_ZeroDMA::setPriority(dma_priority pri) const {
#ifdef __SAMD51__
    DMAC->Channel[channel].CHPRILVL.bit.PRILVL = pri;
#else
    DMAC->CHCTRLB.bit.LVL = pri;
#endif
}

// Deallocate DMA channel
// TODO: should this delete/deallocate the descriptor list?
ZeroDMAstatus Adafruit_ZeroDMA::free(void) {

    ZeroDMAstatus status = DMA_STATUS_OK;

    cpu_irq_enter_critical(); // jobStatus is volatile

    if(jobStatus == DMA_STATUS_BUSY) {
        status = DMA_STATUS_BUSY; // Can't leave when busy
    } else if((channel < DMAC_CH_NUM) && (_channelMask & (1 << channel))) {
        // Valid in-use channel; release it
        _channelMask &= ~(1 << channel); // Clear bit
        if(!_channelMask) {              // No more channels in use?
#ifdef __SAMD51__
            NVIC_DisableIRQ(DMAC_0_IRQn); // Disable DMA interrupt
            DMAC->CTRL.bit.DMAENABLE = 0; // Disable DMA
            MCLK->AHBMASK.bit.DMAC_  = 0; // Disable DMA clock
#else
            NVIC_DisableIRQ(DMAC_IRQn);   // Disable DMA interrupt
            DMAC->CTRL.bit.DMAENABLE = 0; // Disable DMA
            PM->APBBMASK.bit.DMAC_   = 0; // Disable DMA clocks
            PM->AHBMASK.bit.DMAC_    = 0;
#endif
        }
        _dmaPtr[channel] = NULL;
        channel          = 0xFF;
    } else {
        status = DMA_STATUS_ERR_NOT_INITIALIZED; // Channel not in use
    }

    cpu_irq_leave_critical();

    return status;
}

// Start DMA transfer job.  Channel and descriptors should be allocated
// before calling this.
ZeroDMAstatus Adafruit_ZeroDMA::startJob(void) {
    ZeroDMAstatus status = DMA_STATUS_OK;

    cpu_irq_enter_critical(); // Job status is volatile

    if(jobStatus == DMA_STATUS_BUSY) {
        status = DMA_STATUS_BUSY; // Resource is busy
    } else if(channel >= DMAC_CH_NUM) {
        status = DMA_STATUS_ERR_NOT_INITIALIZED; // Channel not in use
    } else if(!hasDescriptors || (_descriptor[channel].BTCNT.reg <= 0)) {
        status = DMA_STATUS_ERR_INVALID_ARG; // Bad transfer size
    } else {
        uint8_t i, interruptMask = 0;
        for(i=0; i<DMA_CALLBACK_N; i++) {
            if(callback[i]) interruptMask |= (1 << i);
        }
        jobStatus            = DMA_STATUS_BUSY;
#ifdef __SAMD51__
        DMAC->Channel[channel].CHINTENSET.reg =
          DMAC_CHINTENSET_MASK &  interruptMask;
        DMAC->Channel[channel].CHINTENCLR.reg =
          DMAC_CHINTENCLR_MASK & ~interruptMask;
        DMAC->Channel[channel].CHCTRLA.bit.ENABLE = 1;
#else
        DMAC->CHID.bit.ID    = channel;
        DMAC->CHINTENSET.reg = DMAC_CHINTENSET_MASK &  interruptMask;
        DMAC->CHINTENCLR.reg = DMAC_CHINTENCLR_MASK & ~interruptMask;
        DMAC->CHCTRLA.bit.ENABLE = 1; // Enable the transfer channel
#endif
    }

    cpu_irq_leave_critical();

    return status;
}

// Set and enable callback function for ZeroDMA object. This can be called
// before or after channel and/or descriptors are allocated, but needs
// to be called before job is started.
void Adafruit_ZeroDMA::setCallback(
  void (*cb)(Adafruit_ZeroDMA *), dma_callback_type type) {
    callback[type] = cb;
}

// Suspend/resume don't quite do what I thought -- avoid using for now.
void Adafruit_ZeroDMA::suspend(void) const {
    cpu_irq_enter_critical();
#ifdef __SAMD51__
    DMAC->Channel[channel].CHCTRLB.reg |= DMAC_CHCTRLB_CMD_SUSPEND;
#else
    DMAC->CHID.bit.ID  = channel;
    DMAC->CHCTRLB.reg |= DMAC_CHCTRLB_CMD_SUSPEND;
#endif
    cpu_irq_leave_critical();
}

#define MAX_JOB_RESUME_COUNT 10000
void Adafruit_ZeroDMA::resume(void) {
    cpu_irq_enter_critical(); // jobStatus is volatile
    if(jobStatus == DMA_STATUS_SUSPEND) {
        int      count;
        uint32_t bitMask   = 1 << channel;
#ifdef __SAMD51__
        DMAC->Channel[channel].CHCTRLB.reg |= DMAC_CHCTRLB_CMD_RESUME;
#else
        DMAC->CHID.bit.ID  = channel;
        DMAC->CHCTRLB.reg |= DMAC_CHCTRLB_CMD_RESUME;
#endif

        for(count = 0; (count < MAX_JOB_RESUME_COUNT) &&
          !(DMAC->BUSYCH.reg & bitMask); count++);

        jobStatus = (count < MAX_JOB_RESUME_COUNT) ?
          DMA_STATUS_BUSY : DMA_STATUS_ERR_TIMEOUT;
    }
    cpu_irq_leave_critical();
}

// Abort is OK though.
void Adafruit_ZeroDMA::abort(void) {
    if(channel <= DMAC_CH_NUM) {
        cpu_irq_enter_critical();
#ifdef __SAMD51__
        DMAC->Channel[channel].CHCTRLA.reg = 0; // Disable channel
#else
        DMAC->CHID.bit.ID = channel; // Select channel
        DMAC->CHCTRLA.reg = 0;       // Disable
#endif
        jobStatus         = DMA_STATUS_ABORTED;
        cpu_irq_leave_critical();
    }
}

// Set DMA peripheral trigger.
// This can be done before or after channel is allocated.
void Adafruit_ZeroDMA::setTrigger(uint8_t trigger) {
    peripheralTrigger = trigger; // Save value for allocate()

    // If channel already allocated, configure peripheral trigger
    // (old lib required configure before alloc -- either way OK now)
    if(channel < DMAC_CH_NUM) {
        cpu_irq_enter_critical();
#ifdef __SAMD51__
        DMAC->Channel[channel].CHCTRLA.bit.TRIGSRC = trigger;
#else
        DMAC->CHID.bit.ID         = channel;
        DMAC->CHCTRLB.bit.TRIGSRC = trigger;
#endif
        cpu_irq_leave_critical();
    }
}

// Set DMA trigger action.
// This can be done before or after channel is allocated.
void Adafruit_ZeroDMA::setAction(dma_transfer_trigger_action action) {
    triggerAction = action; // Save value for allocate()

    // If channel already allocated, configure trigger action
    // (old lib required configure before alloc -- either way OK now)
    if(channel < DMAC_CH_NUM) {
        cpu_irq_enter_critical();
#ifdef __SAMD51__
        DMAC->Channel[channel].CHCTRLA.bit.TRIGACT = action;
#else
        DMAC->CHID.bit.ID         = channel;
        DMAC->CHCTRLB.bit.TRIGACT = action;
#endif
        cpu_irq_leave_critical();
    }
}

// Issue software trigger. Channel must be allocated & descriptors added!
void Adafruit_ZeroDMA::trigger(void) const {
    if((channel <= DMAC_CH_NUM) & hasDescriptors) {
        DMAC->SWTRIGCTRL.reg |= (1 << channel);
    }
}

// Returns true if DMA transfer in progress.
bool Adafruit_ZeroDMA::isActive(void) const {
    return _writeback[channel].BTCTRL.bit.VALID;
}

// DMA DESCRIPTOR FUNCTIONS ------------------------------------------------

// Allocates a new DMA descriptor (if needed) and appends it to the
// channel's descriptor list.  Returns pointer to DmacDescriptor,
// or NULL on various errors.  You'll want to keep the pointer for
// later if you need to modify or free the descriptor.
// Channel must be allocated first!
DmacDescriptor *Adafruit_ZeroDMA::addDescriptor(
  void           *src,
  void           *dst,
  uint32_t        count,
  dma_beat_size   size,
  bool            srcInc,
  bool            dstInc,
  uint32_t        stepSize,
  bool            stepSel) {

    // Channel must be allocated first
       if(channel >= DMAC_CH_NUM) return NULL;

    // Can't do while job's busy
    if(jobStatus == DMA_STATUS_BUSY) return NULL;

    DmacDescriptor *desc;

    // Scan descriptor list to find last entry.  If an entry's
    // DESCADDR value is 0, that's the end of the list and it's
    // currently un-looped.  If the DESCADDR value is the same
    // as the first entry, that's the end of the list and it's
    // looped.  Either way, set the last entry's DESCADDR value
    // to the new descriptor, and the descriptor's own DESCADDR
    // will be set later either to 0 or the list head.
    if(hasDescriptors) {
        // DMA descriptors must be 128-bit (16 byte) aligned.
        // memalign() is considered 'obsolete' but it's replacements
        // (aligned_alloc() or posix_memalign()) are not currently
        // available in the version of ARM GCC in use, but this is,
        // so here we are.
        if(!(desc = (DmacDescriptor *)memalign(16, sizeof(DmacDescriptor)))) {
            return NULL;
        }
        DmacDescriptor *prev = &_descriptor[channel];
        while(prev->DESCADDR.reg &&
             (prev->DESCADDR.reg != (uint32_t)&_descriptor[channel])) {
            prev = (DmacDescriptor *)prev->DESCADDR.reg;
        }
        prev->DESCADDR.reg = (uint32_t)desc;
    } else {
        desc = &_descriptor[channel];
    }
    hasDescriptors = true;

    uint8_t bytesPerBeat; // Beat transfer size IN BYTES
    switch(size) {
       default:                  bytesPerBeat = 1; break;
       case DMA_BEAT_SIZE_HWORD: bytesPerBeat = 2; break;
       case DMA_BEAT_SIZE_WORD:  bytesPerBeat = 4; break;
    }

    desc->BTCTRL.bit.VALID     = true;
    desc->BTCTRL.bit.EVOSEL    = DMA_EVENT_OUTPUT_DISABLE;
    desc->BTCTRL.bit.BLOCKACT  = DMA_BLOCK_ACTION_NOACT;
    desc->BTCTRL.bit.BEATSIZE  = size;
    desc->BTCTRL.bit.SRCINC    = srcInc;
    desc->BTCTRL.bit.DSTINC    = dstInc;
    desc->BTCTRL.bit.STEPSEL   = stepSel;
    desc->BTCTRL.bit.STEPSIZE  = stepSize;
    desc->BTCNT.reg            = count;
    desc->SRCADDR.reg          = (uint32_t)src;

    if(srcInc) {
        if(stepSel) {
            desc->SRCADDR.reg += bytesPerBeat * count * (1 << stepSize);
        } else {
            desc->SRCADDR.reg += bytesPerBeat * count;
        }
    }

    desc->DSTADDR.reg          = (uint32_t)dst;

    if(dstInc) {
        if(!stepSel) {
            desc->DSTADDR.reg += bytesPerBeat * count * (1 << stepSize);
        } else {
            desc->DSTADDR.reg += bytesPerBeat * count;
        }
    }

    desc->DESCADDR.reg = loopFlag ? (uint32_t)&_descriptor[channel] : 0;

    return desc;
}

// Modify DMA descriptor with a new source address, destination address &
// block transfer count.  All other attributes (including increment enables,
// etc.) are unchanged.  Mostly for changing the data being pushed to a
// peripheral (DAC, SPI, whatev.)
void Adafruit_ZeroDMA::changeDescriptor(DmacDescriptor *desc,
  void *src, void *dst, uint32_t count) {

    uint8_t bytesPerBeat; // Beat transfer size IN BYTES
    switch(desc->BTCTRL.bit.BEATSIZE) {
       default:                  bytesPerBeat = 1; break;
       case DMA_BEAT_SIZE_HWORD: bytesPerBeat = 2; break;
       case DMA_BEAT_SIZE_WORD:  bytesPerBeat = 4; break;
    }

    if(count) desc->BTCNT.reg = count;

    if(src) {
        desc->SRCADDR.reg = (uint32_t)src;
        if(desc->BTCTRL.bit.SRCINC) {
            if(desc->BTCTRL.bit.STEPSEL) {
                desc->SRCADDR.reg += desc->BTCNT.reg *
                  bytesPerBeat * (1 << desc->BTCTRL.bit.STEPSIZE);
            } else {
                desc->SRCADDR.reg += desc->BTCNT.reg * bytesPerBeat;
            }
        }
    }

    if(dst) {
        desc->DSTADDR.reg = (uint32_t)dst;
        if(desc->BTCTRL.bit.DSTINC) {
            if(!desc->BTCTRL.bit.STEPSEL) {
                desc->DSTADDR.reg += desc->BTCNT.reg *
                  bytesPerBeat * (1 << desc->BTCTRL.bit.STEPSIZE);
            } else {
                desc->DSTADDR.reg += desc->BTCNT.reg * bytesPerBeat;
            }
        }
    }

// I think this code is here by accident -- disabling for now.
#if 0
    cpu_irq_enter_critical();
    jobStatus          = DMA_STATUS_OK;
#ifdef __SAMD51__
    DMAC->Channel[channel].CHCTRLA.bit.ENABLE = 1;
#else
    DMAC->CHID.bit.ID  = channel;
    DMAC->CHCTRLA.reg |= DMAC_CHCTRLA_ENABLE;
#endif
    cpu_irq_leave_critical();
#endif
}

// TODO: delete descriptor, delete whole descriptor chain

// Select whether channel's descriptor list should repeat or not.
// This can be done before or after channel & any descriptors are allocated.
void Adafruit_ZeroDMA::loop(boolean flag) {
    // The loop selection is 'sticky' -- that is, you can enable or
    // disable looping before a descriptor list is built, or after
    // the fact.  This requires some extra steps in the library code
    // but avoids a must-do-in-X-order constraint on user.
    loopFlag = flag;

    if(hasDescriptors) { // Descriptor list already started?
        // Scan descriptor list to find last entry.  If an entry's
        // DESCADDR value is 0, that's the end of the list and it's
        // currently un-looped.  If the DESCADDR value is the same
        // as the first entry, that's the end of the list and it's
        // already looped.
        DmacDescriptor *desc = &_descriptor[channel];
        while(desc->DESCADDR.reg &&
          (desc->DESCADDR.reg != (uint32_t)&_descriptor[channel])) {
            desc = (DmacDescriptor *)desc->DESCADDR.reg;
        }
        // Loop or unloop descriptor list as appropriate
        desc->DESCADDR.reg = loopFlag ?  (uint32_t)&_descriptor[channel] : 0;
    }
}

// MISCELLANY --------------------------------------------------------------

void Adafruit_ZeroDMA::printStatus(ZeroDMAstatus s) const {
    if(s == DMA_STATUS_JOBSTATUS) s = jobStatus;
    Serial.print("Status: ");
    switch(s) {
       case DMA_STATUS_OK:
            Serial.println("OK");
            break;
       case DMA_STATUS_ERR_NOT_FOUND:
            Serial.println("NOT FOUND");
            break;
       case DMA_STATUS_ERR_NOT_INITIALIZED:
            Serial.println("NOT INITIALIZED");
            break;
       case DMA_STATUS_ERR_INVALID_ARG:
            Serial.println("INVALID ARGUMENT");
            break;
       case DMA_STATUS_ERR_IO:
            Serial.println("IO ERROR");
            break;
       case DMA_STATUS_ERR_TIMEOUT:
            Serial.println("TIMEOUT");
            break;
       case DMA_STATUS_BUSY:
            Serial.println("BUSY");
            break;
       case DMA_STATUS_SUSPEND:
            Serial.println("SUSPENDED");
            break;
       case DMA_STATUS_ABORTED:
            Serial.println("ABORTED");
            break;
       default:
            Serial.print("Unknown 0x");
            Serial.println((int)s);
            break;
    }
}
