/****************************************************************************
 * arch/arm/src/samd5e5/sam_dmac.c
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <assert.h>
#include <debug.h>
#include <errno.h>

#include <nuttx/irq.h>
#include <nuttx/arch.h>
#include <nuttx/mutex.h>
#include <nuttx/semaphore.h>
#include <arch/samd5e5/chip.h>

#include "arm_internal.h"
#include "sched/sched.h"

#include "sam_dmac.h"
#include "sam_periphclks.h"

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

/* Configuration ************************************************************/

/* Condition out the whole file unless DMA is selected in the configuration */

#ifdef CONFIG_SAMD5E5_DMAC

/* If SAMD/L support is enabled, then OS DMA support should also be enabled */

#ifndef CONFIG_ARCH_DMA
#  warning "SAMD5E5 DMA enabled but CONFIG_ARCH_DMA disabled"
#endif

/* Number of additional DMA descriptors in LPRAM */

#ifndef CONFIG_SAMD5E5_DMAC_NDESC
#  define CONFIG_SAMD5E5_DMAC_NDESC 0
#endif

/****************************************************************************
 * Private Types
 ****************************************************************************/

/* The direction of the transfer */

enum sam_dmadir_e
{
  DMADIR_UNKOWN = 0,              /* We don't know the direction of the
                                   * transfer yet */
  DMADIR_TX,                      /* Transmit: Memory to peripheral */
  DMADIR_RX                       /* Receive: Peripheral to memory */
};

/* This structure describes one DMA channel */

struct sam_dmach_s
{
  bool               dc_inuse;    /* TRUE: The DMA channel is in use */
  uint8_t            dc_chan;     /* DMA channel number (0-31) */
  uint8_t            dc_dir;      /* See enum sam_dmadir_e */
  uint32_t           dc_txflags;  /* DMA channel flags for Tx transfers */
  uint32_t           dc_rxflags;  /* DMA channel flags for Rx transfers */
  dma_callback_t     dc_callback; /* Callback invoked when the DMA completes */
  void              *dc_arg;      /* Argument passed to callback function */
#if CONFIG_SAMD5E5_DMAC_NDESC > 0
  struct dma_desc_s *dc_head;     /* First allocated DMA descriptor */
  struct dma_desc_s *dc_tail;     /* DMA descriptor list tail */
#endif
};

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

static void   sam_dmaterminate(struct sam_dmach_s *dmach, int result);
static int    sam_dmainterrupt(int irq, void *context, void *arg);
static struct dma_desc_s *sam_alloc_desc(struct sam_dmach_s *dmach);
static struct dma_desc_s *sam_append_desc(struct sam_dmach_s *dmach,
                uint16_t btctrl, uint16_t btcnt,
                uint32_t srcaddr, uint32_t dstaddr);
static void   sam_free_desc(struct sam_dmach_s *dmach);
static size_t sam_maxtransfer(struct sam_dmach_s *dmach, uint32_t dmaflags);
static uint16_t sam_bytes2beats(struct sam_dmach_s *dmach, uint32_t dmaflags,
                size_t nbytes);
static int    sam_txbuffer(struct sam_dmach_s *dmach, uint32_t paddr,
                uint32_t maddr, uint32_t dmaflags, size_t nbytes);
static int    sam_rxbuffer(struct sam_dmach_s *dmach, uint32_t paddr,
                uint32_t maddr, uint32_t dmaflagsr, size_t nbytes);

/****************************************************************************
 * Private Data
 ****************************************************************************/

/* These mutex protect the DMA channel and descriptor tables */

static mutex_t g_chlock = NXMUTEX_INITIALIZER;
#if CONFIG_SAMD5E5_DMAC_NDESC > 0
static sem_t g_dsem = SEM_INITIALIZER(CONFIG_SAMD5E5_DMAC_NDESC);
#endif

/* This array describes the state of each DMA channel */

static struct sam_dmach_s g_dmach[SAMD5E5_NDMACHAN];

/* NOTE: Using the same address as the base descriptors for writeback
 * descriptors causes TERR and FERR interrupts to be raised immediately after
 * starting DMA.
 */

static struct dma_desc_s g_base_desc[SAMD5E5_NDMACHAN]
  locate_data(".lpram"), aligned(16);
static struct dma_desc_s g_writeback_desc[SAMD5E5_NDMACHAN]
  locate_data(".lpram"), aligned(16);

#if CONFIG_SAMD5E5_DMAC_NDESC > 0
/* Additional DMA descriptors for (optional) multi-block transfer support.
 * Also positioned in LPRAM.
 */

static struct dma_desc_s g_dma_desc[CONFIG_SAMD5E5_DMAC_NDESC]
  locate_data(".lpram"), aligned(16);
#endif

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: sam_dmaterminate
 *
 * Description:
 *   Terminate the DMA transfer and disable the DMA channel
 *
 ****************************************************************************/

static void sam_dmaterminate(struct sam_dmach_s *dmach, int result)
{
  irqstate_t flags;
  int chan;

  /* Disable the DMA channel */

  flags = enter_critical_section();
  chan  = dmach->dc_chan;
  putreg8(0, SAM_DMAC_CHCTRLA(chan));

  /* Reset the DMA channel */

  putreg8(DMAC_CHCTRLA_SWRST, SAM_DMAC_CHCTRLA(chan));

  /* Disable all channel interrupts */

  putreg8(1 << dmach->dc_chan, SAM_DMAC_CHINTENCLR(chan));
  leave_critical_section(flags);

  /* Free the DMA descriptor list */

  sam_free_desc(dmach);

  /* Perform the DMA complete callback */

  if (dmach->dc_callback)
    {
      dmach->dc_callback((DMA_HANDLE)dmach, dmach->dc_arg, result);
    }

  dmach->dc_callback = NULL;
  dmach->dc_arg      = NULL;
  dmach->dc_dir      = DMADIR_UNKOWN;
  leave_critical_section(flags);
}

/****************************************************************************
 * Name: sam_dmainterrupt
 *
 * Description:
 *  DMA interrupt handler
 *
 ****************************************************************************/

static int sam_dmainterrupt(int irq, void *context, void *arg)
{
  struct sam_dmach_s *dmach;
  unsigned int chndx;
  uint16_t intpend;
  int chan;

  /* Process all pending channel interrupts */

  while ((intpend = getreg16(SAM_DMAC_INTPEND)) != 0)
    {
      /* Get the channel that generated the interrupt */

      chndx = (intpend & DMAC_INTPEND_ID_MASK) >> DMAC_INTPEND_ID_SHIFT;
      dmach = &g_dmach[chndx];
      chan  = dmach->dc_chan;

      /* Clear all pending channel interrupt */

      putreg8(DMAC_INT_ALL, SAM_DMAC_CHINTFLAG(chan));

      /* Check for transfer error interrupt */

      if ((intpend & DMAC_INTPEND_TERR) != 0)
        {
          /* Yes... Terminate the transfer with an error? */

          sam_dmaterminate(dmach, -EIO);
        }

      /* Check for channel transfer complete interrupt */

      else if ((intpend & DMAC_INTPEND_TCMPL) != 0)
        {
          /* Yes.. Terminate the transfer with success */

          sam_dmaterminate(dmach, OK);
        }

      else if ((intpend & DMAC_INTPEND_TERR) != 0)
        {
          dmaerr("Invalid descriptor. Channel %d\n", chndx);
          sam_dmaterminate(dmach, -EIO);
        }

      /* Check for channel suspend interrupt */

      else if ((intpend & DMAC_INTPEND_SUSP) != 0)
        {
          dmaerr("Channel suspended. Channel %d\n", chndx);

          /* REVISIT: Do we want to do anything here? */
        }
    }

  return OK;
}

/****************************************************************************
 * Name: sam_alloc_desc
 *
 * Description:
 *  Allocate one DMA descriptor.  If the base DMA descriptor list entry is
 *  unused, then that value structure will be returned.  Otherwise, this
 *  function will search for a free descriptor in the g_desc[] list.
 *
 *  NOTE: Descriptor list entries are freed by the DMA interrupt handler.
 *  However, since the setting/clearing of the 'in use' indication is atomic,
 *  no special actions need be performed.  It would be a good thing to add
 *  logic to handle the case where all of the entries are exhausted and we
 *  could wait for some to be freed by the interrupt handler.
 *
 ****************************************************************************/

static struct dma_desc_s *sam_alloc_desc(struct sam_dmach_s *dmach)
{
  struct dma_desc_s *desc;

  /* First check if the base descriptor for the DMA channel is available */

  desc = &g_base_desc[dmach->dc_chan];
  if (desc->srcaddr == 0)
    {
      /* Yes, return a pointer to the base descriptor */

      desc->srcaddr = (uint32_t)-1; /* Any non-zero value */
      return desc;
    }
#if CONFIG_SAMD5E5_DMAC_NDESC > 0
  else
    {
      int i;

      /* Wait if no descriptor is available.  When we get a semaphore count,
       * then there will be at least one free descriptor in the table and
       * it is ours.
       */

      nxsem_wait_uninterruptible(&g_dsem);

      /* Examine each list entry to find an available one -- i.e., one
       * with srcaddr == 0.  That srcaddr field is set to zero by the DMA
       * transfer complete interrupt handler.  The following should be safe
       * because that is an atomic operation.
       */

      for (i = 0; i < CONFIG_SAMD5E5_DMAC_NDESC; i++)
        {
          desc = &g_dma_desc[i];
          if (desc->srcaddr == 0)
            {
              /* Set the srcaddr to any non-zero value to reserve
               * the descriptor.
               */

              desc->srcaddr = (uint32_t)-1; /* Any non-zero value */

              /* Save a pointer to the first allocated DMA descriptor
               * (for sam_free_desc).  We have to do this because we cannot
               * use the link in the base descriptor; it will be overwritten
               * by the writeback.
               */

              if (dmach->dc_head == NULL)
                {
                  dmach->dc_head = desc;
                }

              return desc;
            }
        }

      /* Because we hold a count from the counting semaphore, the above
       * search loop should always be successful.
       */

      DEBUGPANIC();
    }
#endif

  return NULL;
}

/****************************************************************************
 * Name: sam_append_desc
 *
 * Description:
 *  Allocate and add one descriptor to the DMA channel's descriptor list.
 *
 ****************************************************************************/

static struct dma_desc_s *sam_append_desc(struct sam_dmach_s *dmach,
                                          uint16_t btctrl, uint16_t btcnt,
                                          uint32_t srcaddr, uint32_t dstaddr)
{
  struct dma_desc_s *desc;

  /* Sanity check -- srcaddr == 0 is the indication that the descriptor is
   * unused.  Obviously setting it to zero would break that usage.
   */

  DEBUGASSERT(srcaddr != 0);

  /* Allocate a DMA descriptor */

  desc = sam_alloc_desc(dmach);
  if (desc != NULL)
    {
      /* We have it.  Initialize the new descriptor list entry */

      desc->btctrl   = btctrl;   /* Block Transfer Control Register */
      desc->btcnt    = btcnt;    /* Block Transfer Count Register */
      desc->srcaddr  = srcaddr;  /* Block Transfer Source Address Register */
      desc->dstaddr  = dstaddr;  /* Block Transfer Destination Address Register */
      desc->descaddr = 0;        /* Next Address Descriptor Register */

      /* And then hook it at the tail of the descriptor list */

#if CONFIG_SAMD5E5_DMAC_NDESC > 0
      if (dmach->dc_tail != NULL)
        {
          struct dma_desc_s *prev;

          DEBUGASSERT(desc != g_base_desc[dmach->dc_chan]);

          /* Link the previous tail to the new tail */

          prev->descaddr = (uint32_t)desc;
        }
      else
#endif
        {
          /* There is no previous link.  This is the new head of the list */

          DEBUGASSERT(desc == &g_base_desc[dmach->dc_chan]);
        }

#if CONFIG_SAMD5E5_DMAC_NDESC > 0
      /* In either case, this is the new tail of the list. */

      dmach->dc_tail = desc;
#endif
    }
  else
    {
      dmaerr("Failed to allocate descriptor\n");
    }

  return desc;
}

/****************************************************************************
 * Name: sam_free_desc
 *
 * Description:
 *  Free all descriptors in the DMA channel's descriptor list.
 *
 *  NOTE: Called from the DMA interrupt handler.
 *
 ****************************************************************************/

static void sam_free_desc(struct sam_dmach_s *dmach)
{
  struct dma_desc_s *desc;
#if CONFIG_SAMD5E5_DMAC_NDESC > 0
  struct dma_desc_s *next;
#endif

  /* Get the base descriptor pointer */

  desc           = &g_base_desc[dmach->dc_chan];
#if CONFIG_SAMD5E5_DMAC_NDESC > 0
  next           = dmach->dc_head;

  dmach->dc_head = NULL;
  dmach->dc_tail = NULL;
#endif

  /* Nullify the base descriptor */

  memset(desc, 0, sizeof(struct dma_desc_s));

#if CONFIG_SAMD5E5_DMAC_NDESC > 0
  /* Reset each additional descriptor in the descriptor list (thereby
   * freeing them)
   */

  while (next != NULL)
    {
      desc = next;
      DEBUGASSERT(desc->srcaddr != 0);

      next = (struct dma_desc_s *)desc->descaddr;
      memset(desc, 0, sizeof(struct dma_desc_s));
      nxsem_post(&g_dsem);
    }
#endif
}

/****************************************************************************
 * Name: sam_maxtransfer
 *
 * Description:
 *   Maximum number of bytes that can be sent/received in one transfer
 *
 ****************************************************************************/

static size_t sam_maxtransfer(struct sam_dmach_s *dmach, uint32_t dmaflags)
{
  int beatsize;

  /* The number of bytes per beat is 2**BEATSIZE */

  beatsize = (dmaflags & DMACH_FLAG_BEATSIZE_MASK) >>
              LPSRAM_BTCTRL_STEPSIZE_SHIFT;

  /* Maximum beats is UINT16_MAX */

  return (size_t)UINT16_MAX << beatsize;
}

/****************************************************************************
 * Name: sam_bytes2beats
 *
 * Description:
 *   Convert a count of bytes into a count of beats
 *
 ****************************************************************************/

static uint16_t sam_bytes2beats(struct sam_dmach_s *dmach, uint32_t dmaflags,
                                size_t nbytes)
{
  size_t mask;
  int beatsize;
  size_t nbeats;

  /* The number of bytes per beat is 2**BEATSIZE */

  beatsize = (dmaflags & DMACH_FLAG_BEATSIZE_MASK) >>
              LPSRAM_BTCTRL_STEPSIZE_SHIFT;

  /* The number of beats is then the ceiling of the division */

  mask     = (1 << beatsize) - 1;
  nbeats   = (nbytes + mask) >> beatsize;
  DEBUGASSERT(nbeats <= UINT16_MAX);
  return (uint16_t)nbeats;
}

/****************************************************************************
 * Name: sam_txbuffer
 *
 * Description:
 *   Configure DMA for transmit of one buffer (memory to peripheral).  This
 *   function may be called multiple times to handle large and/or dis-
 *   continuous transfers.
 *
 ****************************************************************************/

static int sam_txbuffer(struct sam_dmach_s *dmach, uint32_t paddr,
                        uint32_t maddr, uint32_t dmaflags, size_t nbytes)
{
  uint16_t btctrl;
  uint16_t btcnt;
  uint16_t tmp;

  DEBUGASSERT(dmach->dc_dir == DMADIR_UNKOWN || dmach->dc_dir == DMADIR_TX);

  /* Set up the Block Transfer Control Register configuration:
   *
   * This are fixed register selections:
   *
   *   LPSRAM_BTCTRL_VALID          - Descriptor is valid
   *   LPSRAM_BTCTRL_EVOSEL_DISABLE - No event output
   *   LPSRAM_BTCTRL_BLOCKACT_INT   - Disable channel and generate interrupt
   *                                  when the last block transfer completes.
   *
   * Other settings come from the channel configuration:
   *
   *   LPSRAM_BTCTRL_BEATSIZE     - Determined by DMACH_FLAG_BEATSIZE
   *   LPSRAM_BTCTRL_SRCINC       - Determined by DMACH_FLAG_MEM_INCREMENT
   *   LPSRAM_BTCTRL_DSTINC       - Determined by DMACH_FLAG_PERIPH_INCREMENT
   *   LPSRAM_BTCTRL_STEPSEL      - Determined by DMACH_FLAG_STEPSEL
   *   LPSRAM_BTCTRL_STEPSIZE     - Determined by DMACH_FLAG_STEPSIZE
   */

  btctrl  = LPSRAM_BTCTRL_VALID | LPSRAM_BTCTRL_EVOSEL_DISABLE |
            LPSRAM_BTCTRL_BLOCKACT_INT;

  tmp     = (dmaflags & DMACH_FLAG_BEATSIZE_MASK) >>
             DMACH_FLAG_BEATSIZE_SHIFT;
  btctrl |= tmp << LPSRAM_BTCTRL_BEATSIZE_SHIFT;

  /* See Addressing on page 264 of the datasheet.
   * When increment is used, we have to adjust the address in the descriptor
   * based on the beat count remaining in the block
   */

  /* Set up the Block Transfer Count Register configuration */

  btcnt = sam_bytes2beats(dmach, dmaflags, nbytes);

  if ((dmaflags & DMACH_FLAG_MEM_INCREMENT) != 0)
    {
      btctrl |= LPSRAM_BTCTRL_SRCINC;
      maddr += nbytes;
    }

  if ((dmaflags & DMACH_FLAG_PERIPH_INCREMENT) != 0)
    {
      btctrl |= LPSRAM_BTCTRL_DSTINC;
      paddr += nbytes;
    }

  if ((dmaflags & DMACH_FLAG_STEPSEL) == DMACH_FLAG_STEPSEL_PERIPH)
    {
      btctrl |= LPSRAM_BTCTRL_STEPSEL;
    }

  tmp     = (dmaflags & DMACH_FLAG_STEPSIZE_MASK) >>
             LPSRAM_BTCTRL_STEPSIZE_SHIFT;
  btctrl |= tmp << LPSRAM_BTCTRL_STEPSIZE_SHIFT;

  /* Add the new descriptor list entry */

  if (!sam_append_desc(dmach, btctrl, btcnt, maddr, paddr))
    {
      return -ENOMEM;
    }

  dmach->dc_dir = DMADIR_TX;
  return OK;
}

/****************************************************************************
 * Name: sam_rxbuffer
 *
 * Description:
 *   Configure DMA for receipt of one buffer (peripheral to memory).  This
 *   function may be called multiple times to handle large and/or dis-
 *   continuous transfers.
 *
 ****************************************************************************/

static int sam_rxbuffer(struct sam_dmach_s *dmach, uint32_t paddr,
                        uint32_t maddr, uint32_t dmaflags, size_t nbytes)
{
  uint16_t btctrl;
  uint16_t btcnt;
  uint16_t tmp;

  DEBUGASSERT(dmach->dc_dir == DMADIR_UNKOWN || dmach->dc_dir == DMADIR_RX);

  /* Set up the Block Transfer Control Register configuration:
   *
   * This are fixed register selections:
   *
   *   LPSRAM_BTCTRL_VALID          - Descriptor is valid
   *   LPSRAM_BTCTRL_EVOSEL_DISABLE - No event output
   *   LPSRAM_BTCTRL_BLOCKACT_INT   - Disable channel and generate interrupt
   *                                  when the last block transfer completes.
   *
   * Other settings come from the channel configuration:
   *
   *   LPSRAM_BTCTRL_BEATSIZE     - Determined by DMACH_FLAG_BEATSIZE
   *   LPSRAM_BTCTRL_SRCINC       - Determined by DMACH_FLAG_PERIPH_INCREMENT
   *   LPSRAM_BTCTRL_DSTINC       - Determined by DMACH_FLAG_MEM_INCREMENT
   *   LPSRAM_BTCTRL_STEPSEL      - Determined by DMACH_FLAG_STEPSEL
   *   LPSRAM_BTCTRL_STEPSIZE     - Determined by DMACH_FLAG_STEPSIZE
   */

  btctrl  = LPSRAM_BTCTRL_VALID | LPSRAM_BTCTRL_EVOSEL_DISABLE |
            LPSRAM_BTCTRL_BLOCKACT_INT;

  tmp     = (dmaflags & DMACH_FLAG_BEATSIZE_MASK) >>
             DMACH_FLAG_BEATSIZE_SHIFT;
  btctrl |= tmp << LPSRAM_BTCTRL_BEATSIZE_SHIFT;

  /* Set up the Block Transfer Count Register configuration */

  btcnt   = sam_bytes2beats(dmach, dmaflags, nbytes);

  if ((dmaflags & DMACH_FLAG_PERIPH_INCREMENT) != 0)
    {
      btctrl |= LPSRAM_BTCTRL_SRCINC;
      paddr += nbytes;
    }

  if ((dmaflags & DMACH_FLAG_MEM_INCREMENT) != 0)
    {
      btctrl |= LPSRAM_BTCTRL_DSTINC;
      maddr += nbytes;
    }

  if ((dmaflags & DMACH_FLAG_STEPSEL) == DMACH_FLAG_STEPSEL_MEM)
    {
      btctrl |= LPSRAM_BTCTRL_STEPSEL;
    }

  tmp     = (dmaflags & DMACH_FLAG_STEPSIZE_MASK) >>
             LPSRAM_BTCTRL_STEPSIZE_SHIFT;
  btctrl |= tmp << LPSRAM_BTCTRL_STEPSIZE_SHIFT;

  /* Add the new descriptor list entry */

  if (!sam_append_desc(dmach, btctrl, btcnt, paddr, maddr))
    {
      return -ENOMEM;
    }

  dmach->dc_dir = DMADIR_RX;
  return OK;
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: arm_dma_initialize
 *
 * Description:
 *   Initialize the DMA subsystem
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

void weak_function arm_dma_initialize(void)
{
  dmainfo("Initialize DMAC\n");
  int i;

  /* Initialized the DMA channel table */

  for (i = 0; i < SAMD5E5_NDMACHAN; i++)
    {
      g_dmach[i].dc_chan = i;
    }

  /* Clear descriptors (this will not be done automatically because they are
   * not in .bss).
   */

  memset(g_base_desc, 0, sizeof(struct dma_desc_s)*SAMD5E5_NDMACHAN);
  memset(g_writeback_desc, 0, sizeof(struct dma_desc_s)*SAMD5E5_NDMACHAN);
#if CONFIG_SAMD5E5_DMAC_NDESC > 0
  memset(g_dma_desc, 0, sizeof(struct dma_desc_s)*CONFIG_SAMD5E5_DMAC_NDESC);
#endif

  /* Enable DMA clocking */

  sam_ahb_dmac_enableperiph();

  /* Disable and reset the DMAC */

  putreg16(0, SAM_DMAC_CTRL);
  putreg16(DMAC_CTRL_SWRST, SAM_DMAC_CTRL);

  /* Attach DMA interrupt vectors */

  irq_attach(SAM_IRQ_DMACH0, sam_dmainterrupt, NULL);
  irq_attach(SAM_IRQ_DMACH1, sam_dmainterrupt, NULL);
  irq_attach(SAM_IRQ_DMACH2, sam_dmainterrupt, NULL);
  irq_attach(SAM_IRQ_DMACH3, sam_dmainterrupt, NULL);
  irq_attach(SAM_IRQ_DMACH4_31, sam_dmainterrupt, NULL);

  /* Set the LPRAM DMA descriptor table addresses.  These can only be
   * written when the DMAC is disabled.
   */

  putreg32((uint32_t)g_base_desc, SAM_DMAC_BASEADDR);
  putreg32((uint32_t)g_writeback_desc, SAM_DMAC_WRBADDR);

  /* Setup the Priority control register for each priority level */
#warning Missing logic

  /* Enable the DMA controller and all priority levels */

  putreg16(DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN0 | DMAC_CTRL_LVLEN1 |
           DMAC_CTRL_LVLEN2, SAM_DMAC_CTRL);

  /* Enable the IRQs at the NVIC (still disabled at the DMA controller) */

  up_enable_irq(SAM_IRQ_DMACH0);
  up_enable_irq(SAM_IRQ_DMACH1);
  up_enable_irq(SAM_IRQ_DMACH2);
  up_enable_irq(SAM_IRQ_DMACH3);
  up_enable_irq(SAM_IRQ_DMACH4_31);
}

/****************************************************************************
 * Name: sam_dmachannel
 *
 * Description:
 *   Allocate a DMA channel.  This function sets aside a DMA channel and
 *   gives the caller exclusive access to the DMA channel.
 *
 *   The naming convention in all of the DMA interfaces is that one side is
 *   the 'peripheral' and the other is 'memory'.  However, the interface
 *   could still be used if, for example, both sides were memory although
 *   the naming would be awkward.
 *
 * Returned Value:
 *   If a DMA channel if the required FIFO size is available, this function
 *   returns a non-NULL, void* DMA channel handle.  NULL is returned on any
 *   failure.
 *
 ****************************************************************************/

DMA_HANDLE sam_dmachannel(uint32_t txflags, uint32_t rxflags)
{
  struct sam_dmach_s *dmach;
  irqstate_t flags;
  unsigned int chndx;
  int ret;

  /* Search for an available DMA channel */

  dmach = NULL;
  ret = nxmutex_lock(&g_chlock);
  if (ret < 0)
    {
      return NULL;
    }

  for (chndx = 0; chndx < SAMD5E5_NDMACHAN; chndx++)
    {
      struct sam_dmach_s *candidate = &g_dmach[chndx];
      if (!candidate->dc_inuse)
        {
          dmach           = candidate;
          dmach->dc_inuse = true;

          /* Set the DMA channel flags */

          dmach->dc_txflags = txflags;
          dmach->dc_rxflags = rxflags;

          /* Disable the DMA channel */

          flags = enter_critical_section();
          putreg8(0, SAM_DMAC_CHCTRLA(chndx));

          /* Reset the channel */

          putreg8(DMAC_CHCTRLA_SWRST, SAM_DMAC_CHCTRLA(chndx));

          /* Disable all channel interrupts */

          putreg8(1 << chndx, SAM_DMAC_CHINTENCLR(chndx));
          leave_critical_section(flags);
          break;
        }
    }

  nxmutex_unlock(&g_chlock);

  dmainfo("chflags: %08x returning dmach: %p\n",  (int)chflags, dmach);
  return (DMA_HANDLE)dmach;
}

/****************************************************************************
 * Name: sam_dmaconfig
 *
 * Description:
 *   There are two channel usage models:  (1) The channel is allocated and
 *   configured in one step.  This is the typical case where a DMA channel
 *   performs a constant role.  The alternative is (2) where the DMA channel
 *   is reconfigured on the fly.  In this case, the chflags provided to
 *   sam_dmachannel are not usedand sam_dmaconfig() is called before each DMA
 *   to configure the DMA channel appropriately.
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

void sam_dmaconfig(DMA_HANDLE handle, uint32_t txflags, uint32_t rxflags)
{
  struct sam_dmach_s *dmach = (struct sam_dmach_s *)handle;

  /* Set the new DMA channel flags. */

  dmainfo("txflags: %08lx  rxflags: %08lx\n",
          (unsigned long)txflags, (unsigned long)rxflags);

  dmach->dc_txflags = txflags;
  dmach->dc_rxflags = rxflags;
}

/****************************************************************************
 * Name: sam_dmafree
 *
 * Description:
 *   Release a DMA channel.  NOTE:  The 'handle' used in this argument must
 *   NEVER be used again until sam_dmachannel() is called again to re-gain
 *   a valid handle.
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

void sam_dmafree(DMA_HANDLE handle)
{
  struct sam_dmach_s *dmach = (struct sam_dmach_s *)handle;

  dmainfo("dmach: %p\n", dmach);
  DEBUGASSERT((dmach != NULL) && (dmach->dc_inuse));

  /* Mark the channel no longer in use.  Clearing the inuse flag is an atomic
   * operation and so should be safe.
   */

  dmach->dc_txflags = 0;
  dmach->dc_rxflags = 0;
  dmach->dc_inuse   = false;
}

/****************************************************************************
 * Name: sam_dmatxsetup
 *
 * Description:
 *   Configure DMA for transmit of one buffer (memory to peripheral).  This
 *   function may be called multiple times to handle large and/or dis-
 *   continuous transfers.  Calls to sam_dmatxsetup() and sam_dmarxsetup()
 *   must not be intermixed on the same transfer, however.
 *
 ****************************************************************************/

int sam_dmatxsetup(DMA_HANDLE handle, uint32_t paddr, uint32_t maddr,
                   size_t nbytes)
{
  struct sam_dmach_s *dmach = (struct sam_dmach_s *)handle;
  ssize_t remaining = (ssize_t)nbytes;
  size_t maxtransfer;
  int ret = OK;

  dmainfo("dmach: %p paddr: %08x maddr: %08x nbytes: %d\n",
          dmach, (int)paddr, (int)maddr, (int)nbytes);
  DEBUGASSERT(dmach);

#if CONFIG_SAMD5E5_DMAC_NDESC > 0
  dmainfo("dc_head: %p dc_tail: %p\n", dmach->dc_head, dmach->dc_tail);
#endif

  /* The maximum transfer size in bytes depends upon the maximum number of
   * transfers and the number of bytes per transfer.
   */

  maxtransfer = sam_maxtransfer(dmach, dmach->dc_txflags);

  /* If this is a large transfer, break it up into smaller buffers */

  while (remaining > maxtransfer)
    {
      /* Set up the maximum size transfer */

      ret = sam_txbuffer(dmach, paddr, maddr, dmach->dc_txflags,
                         maxtransfer);
      if (ret == OK)
        {
          /* Decrement the number of bytes left to transfer */

          remaining -= maxtransfer;

          /* Increment the memory & peripheral address (if it is appropriate
           * do do).
           *
           * REVISIT: What if stepsize is not 1?
           */

          if ((dmach->dc_txflags & DMACH_FLAG_PERIPH_INCREMENT) != 0)
            {
              paddr += maxtransfer;
            }

          if ((dmach->dc_txflags & DMACH_FLAG_MEM_INCREMENT) != 0)
            {
              maddr += maxtransfer;
            }
        }
    }

  /* Then set up the final buffer transfer */

  if (ret == OK && remaining > 0)
    {
      ret = sam_txbuffer(dmach, paddr, maddr, dmach->dc_txflags, remaining);
    }

  return ret;
}

/****************************************************************************
 * Name: sam_dmarxsetup
 *
 * Description:
 *   Configure DMA for receipt of one buffer (peripheral to memory).  This
 *   function may be called multiple times to handle large and/or dis-
 *   continuous transfers.  Calls to sam_dmatxsetup() and sam_dmarxsetup()
 *   must not be intermixed on the same transfer, however.
 *
 ****************************************************************************/

int sam_dmarxsetup(DMA_HANDLE handle, uint32_t paddr, uint32_t maddr,
                   size_t nbytes)
{
  struct sam_dmach_s *dmach = (struct sam_dmach_s *)handle;
  ssize_t remaining = (ssize_t)nbytes;
  size_t maxtransfer;
  int ret = OK;

  dmainfo("dmach: %p paddr: %08x maddr: %08x nbytes: %d\n",
          dmach, (int)paddr, (int)maddr, (int)nbytes);
  DEBUGASSERT(dmach);

#if CONFIG_SAMD5E5_DMAC_NDESC > 0
  dmainfo("dc_head: %p dc_tail: %p\n", dmach->dc_head, dmach->dc_tail);
#endif

  /* The maximum transfer size in bytes depends upon the maximum number of
   * transfers and the number of bytes per transfer.
   */

  maxtransfer = sam_maxtransfer(dmach, dmach->dc_rxflags);

  /* If this is a large transfer, break it up into smaller buffers */

  while (remaining > maxtransfer)
    {
      /* Set up the maximum size transfer */

      ret = sam_rxbuffer(dmach, paddr, maddr, dmach->dc_rxflags,
                         maxtransfer);
      if (ret == OK)
        {
          /* Decrement the number of bytes left to transfer */

          remaining -= maxtransfer;

          /* Increment the memory & peripheral address (if it is appropriate
           * to do so).
           *
           * REVISIT: What if stepsize is not 1?
           */

          if ((dmach->dc_rxflags & DMACH_FLAG_PERIPH_INCREMENT) != 0)
            {
              paddr += maxtransfer;
            }

          if ((dmach->dc_rxflags & DMACH_FLAG_MEM_INCREMENT) != 0)
            {
              maddr += maxtransfer;
            }
        }
    }

  /* Then set up the final buffer transfer */

  if (ret == OK && remaining > 0)
    {
      ret = sam_rxbuffer(dmach, paddr, maddr, dmach->dc_rxflags, remaining);
    }

  return ret;
}

/****************************************************************************
 * Name: sam_dmastart
 *
 * Description:
 *   Start the DMA transfer
 *
 ****************************************************************************/

int sam_dmastart(DMA_HANDLE handle, dma_callback_t callback, void *arg)
{
  struct sam_dmach_s *dmach = (struct sam_dmach_s *)handle;
  struct dma_desc_s *head;
  irqstate_t flags;
  uint32_t dmaflags;
  uint32_t ctrla;
  uint32_t regval32;
  uint8_t regval8;
  int chan;
  int ret = -EINVAL;

  dmainfo("dmach: %p callback: %p arg: %p\n", dmach, callback, arg);
  DEBUGASSERT(dmach != NULL && dmach->dc_chan < SAMD5E5_NDMACHAN);
  head = &g_base_desc[dmach->dc_chan];
  chan = dmach->dc_chan;

  /* Verify that the DMA has been setup (i.e., at least one entry in the
   * descriptor list, the base entry).
   */

  if (head->srcaddr != 0)
    {
      /* Save the callback info.  This will be invoked when the DMA
       * completes.
       */

      dmach->dc_callback = callback;
      dmach->dc_arg      = arg;

      if (dmach->dc_dir == DMADIR_TX)
        {
          /* Memory to peripheral */

          dmaflags = dmach->dc_txflags;
        }
      else
        {
          /* Peripheral to memory */

          dmaflags = dmach->dc_rxflags;
        }

      /* Clear any pending interrupts from any previous DMAC transfer. */

      flags = enter_critical_section();
      putreg8(0, SAM_DMAC_CHCTRLA(chan));

      /* Setup the Channel Control A Register
       *
       * DMAC_CHCTRLA_ENABLE       - Set later
       * DMAC_CHCTRLA_RUNSTDBY     - Determined by DMACH_FLAG_RUNINSTDBY
       * DMAC_CHCTRLA_TRIGSRC      - Determined by DMACH_FLAG_PERIPH_TRIGSRC
       * DMAC_CHCTRLA_TRIGACT      - Determined by DMACH_FLAG_PERIPH_TRIGACT
       * DMAC_CHCTRLA_BURSTLEN     - Determined by DMACH_FLAG_BURSTLEN
       * DMAC_CHCTRLA_THRESHOLD    - Determined by DMACH_FLAG_THRESHOLD
       */

      ctrla = 0;

      /* Run in STANDBY option */

      if (dmaflags & DMACH_FLAG_RUNINSTDBY)
        {
          ctrla |= DMAC_CHCTRLA_RUNSTDBY;
        }

      /* Trigger source */

      regval32 = (dmaflags & DMACH_FLAG_PERIPH_TRIGSRC_MASK) >>
                  DMACH_FLAG_PERIPH_TRIGSRC_SHIFT;
      ctrla   |= DMAC_CHCTRLA_TRIGSRC(regval32) ;

      /* Trigger action */

      regval32 = (dmaflags & DMAC_CHCTRLA_TRIGACT_MASK) >>
                  DMAC_CHCTRLA_TRIGACT_SHIFT;
      ctrla   |= DMAC_CHCTRLA_TRIGACT(regval32) ;

      /* Burst length */

      regval32 = ((dmaflags & DMACH_FLAG_BURSTLEN_MASK) >>
                  DMACH_FLAG_BURSTLEN_SHIFT) + 1;
      ctrla   |= DMAC_CHCTRLA_BURSTLEN(regval32) ;

      /* Threshold */

      regval32 = ((dmaflags & DMACH_FLAG_BURSTLEN_MASK) >>
                  DMACH_FLAG_BURSTLEN_SHIFT) + 1;
      ctrla   |= DMAC_CHCTRLA_THRESHOLD(regval32) ;

      putreg32(ctrla, SAM_DMAC_CHCTRLA(chan));

      /* Setup the Channel Control B Register
       *
       *   DMAC_CHCTRLB_EVIE=0       - No channel input actions
       *   DMAC_CHCTRLB_EVOE=0       - Channel event output disabled
       *   DMAC_CHCTRLB_TRIGACT_BEAT - One trigger required for beat transfer
       *   DMAC_CHCTRLB_CMD_NOACTION - No action
       */

      putreg32(DMAC_CHCTRLB_CMD_NOACTION, SAM_DMAC_CHCTRLB(chan));

      /* Set up the channel priority register */

      regval8 = (dmaflags & DMACH_FLAG_PRIORITY_MASK) >>
                 DMACH_FLAG_PRIORITY_SHIFT;
      putreg8(DMAC_CHPRILVL(regval8), SAM_DMAC_CHPRILVL(chan));

      /* Setup channel event control register
       *
       *   DMAC_CHEVCTRL_EVACT      - Normal transfer and trigger
       *   DMAC_CHEVCTRL_EVMODE     - Default, block event output selection
       *   DMAC_CHEVCTRL_EVIE       - No channel input actions
       *   DMAC_CHEVCTRL_EVOE       - Channel event output disabled/
       */

      regval8 = DMAC_CHEVCTRL_EVACT_TRIG | DMAC_CHEVCTRL_EVOMODE_DEFAULT;
      putreg8(DMAC_CHPRILVL(regval8), SAM_DMAC_CHEVCTRL(chan));

      /* Enable the channel */

      ctrla  = getreg8(SAM_DMAC_CHCTRLA(chan));
      ctrla |= DMAC_CHCTRLA_ENABLE;
      putreg8(ctrla, SAM_DMAC_CHCTRLA(chan));

      /* Enable DMA channel interrupts */

      putreg8(DMAC_INT_TERR | DMAC_INT_TCMPL, SAM_DMAC_CHINTENSET(chan));
      leave_critical_section(flags);
      ret = OK;
    }

  return ret;
}

/****************************************************************************
 * Name: sam_dmastop
 *
 * Description:
 *   Cancel the DMA.  After sam_dmastop() is called, the DMA channel is
 *   reset and sam_dmarx/txsetup() must be called before sam_dmastart() can
 *   be called again
 *
 ****************************************************************************/

void sam_dmastop(DMA_HANDLE handle)
{
  struct sam_dmach_s *dmach = (struct sam_dmach_s *)handle;
  irqstate_t flags;

  dmainfo("dmach: %p\n", dmach);
  DEBUGASSERT(dmach != NULL);

  flags = enter_critical_section();
  sam_dmaterminate(dmach, -EINTR);
  leave_critical_section(flags);
}

/****************************************************************************
 * Name: sam_dmasample
 *
 * Description:
 *   Sample DMA register contents
 *
 * Assumptions:
 *   - DMA handle allocated by sam_dmachannel()
 *
 ****************************************************************************/

#ifdef CONFIG_DEBUG_DMA_INFO
void sam_dmasample(DMA_HANDLE handle, struct sam_dmaregs_s *regs)
{
  struct sam_dmach_s *dmach = (struct sam_dmach_s *)handle;
  uintptr_t base;
  irqstate_t flags;
  int chan;

  /* Sample DMAC registers. */

  flags            = enter_critical_section();
  chan             = dmach->dc_chan;
  base             = SAM_DMAC_CHAN_BASE(chan);
  regs->chan       = chan;

  regs->ctrl       = getreg16(SAM_DMAC_CTRL);       /* Control Register */
  regs->crcctrl    = getreg16(SAM_DMAC_CRCCTRL);    /* CRC Control Register */
  regs->crcdatain  = getreg32(SAM_DMAC_CRCDATAIN);  /* CRC Data Input Register */
  regs->crcchksum  = getreg32(SAM_DMAC_CRCCHKSUM);  /* CRC Checksum Register */
  regs->crcstatus  = getreg8(SAM_DMAC_CRCSTATUS);   /* CRC Status Register */
  regs->dbgctrl    = getreg8(SAM_DMAC_DBGCTRL);     /* Debug Control Register */
  regs->swtrigctrl = getreg32(SAM_DMAC_SWTRIGCTRL); /* Software Trigger Control Register */
  regs->prictrl0   = getreg32(SAM_DMAC_PRICTRL0);   /* Priority Control 0 Register */
  regs->intpend    = getreg16(SAM_DMAC_INTPEND);    /* Interrupt Pending Register */
  regs->intstatus  = getreg32(SAM_DMAC_INTSTATUS);  /* Interrupt Status Register */
  regs->busych     = getreg32(SAM_DMAC_BUSYCH);     /* Busy Channels Register */
  regs->pendch     = getreg32(SAM_DMAC_PENDCH);     /* Pending Channels Register */
  regs->active     = getreg32(SAM_DMAC_ACTIVE);     /* Active Channels and Levels Register */
  regs->baseaddr   = getreg32(SAM_DMAC_BASEADDR);   /* Descriptor Memory Section Base Address Register */
  regs->wrbaddr    = getreg32(SAM_DMAC_WRBADDR);    /* Write-Back Memory Section Base Address Register */

  regs->chctrla    = getreg32(base + SAM_DMAC_CHCTRLA_OFFSET);  /* Channel Control A Register */
  regs->chctrlb    = getreg8(base + SAM_DMAC_CHCTRLB_OFFSET);   /* Channel Control B Register */
  regs->chprilvl   = getreg8(base + SAM_DMAC_CHINTFLAG_OFFSET); /* Channel Priority Level */
  regs->chevctrl   = getreg8(base + SAM_DMAC_CHINTFLAG_OFFSET); /* Channel Event Control Register */
  regs->chintflag  = getreg8(base + SAM_DMAC_CHINTFLAG_OFFSET); /* Channel Interrupt Flag Status and Clear Register */
  regs->chstatus   = getreg8(base + SAM_DMAC_CHSTATU_OFFSETS);  /* Channel Status Register */

  leave_critical_section(flags);
}
#endif /* CONFIG_DEBUG_DMA_INFO */

/****************************************************************************
 * Name: sam_dmadump
 *
 * Description:
 *   Dump previously sampled DMA register contents
 *
 * Assumptions:
 *   - DMA handle allocated by sam_dmachannel()
 *
 ****************************************************************************/

#ifdef CONFIG_DEBUG_DMA_INFO
void sam_dmadump(DMA_HANDLE handle, const struct sam_dmaregs_s *regs,
                 const char *msg)
{
  struct sam_dmach_s *dmach = (struct sam_dmach_s *)handle;
  irqstate_t flags;

  flags            = enter_critical_section();
  dmainfo("%s\n", msg);
  dmainfo("  DMAC Global Registers:\n");
  dmainfo("    CTRL: %04x      CRCCTRL: %04x      "
          "CRCDATAIN: %08x  CRCCHKSUM: %08x\n",
          regs->ctrl, regs->crcctrl, regs->crcdatain, regs->crcchksum);
  dmainfo("    CRCSTATUS: %02x        DBGCTRL: %02x        "
          "SWTRIGCTRL: %08x  PRICTRL0: %08x\n",
          regs->crcstatus, regs->dbgctrl, regs->swtrigctrl, regs->prictrl0);
  dmainfo("    INTPEND: %04x      INTSTATUS: %08x  "
          "BUSYCH: %08x  PENDCH: %08x\n",
          regs->intpend, regs->intstatus, regs->busych, regs->pendch);
  dmainfo("    ACTIVE: %08x  BASEADDR: %08x  WRBADDR: %08x\n",
          regs->active, regs->baseaddr, regs->wrbaddr);

  dmainfo("  DMAC Channel %u Registers:\n", regs->chan);
  dmainfo("     CHCRTRLA: %08x  CHCRTRLB: %02x       "
          "CHPRILVL: %02x        CHPRILVL: %02x\n",
          regs->chctrla, regs->chctrlb, regs->chprilvl, priv->chprilvl);
  dmainfo("     CHINFLAG: %02x        CHSTATUS: %02x\n",
          regs->chintflag, regs->chstatus);
}
#endif /* CONFIG_DEBUG_DMA_INFO */
#endif /* CONFIG_SAMD5E5_DMAC */