/****************************************************************************
 * arch/arm/src/samdl/sam_dmac.c
 *
 *   Copyright (C) 2015 Gregory Nutt. All rights reserved.
 *   Author: Gregory Nutt <gnutt@nuttx.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name NuttX nor the names of its contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/

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

#include <nuttx/config.h>

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

#include <nuttx/irq.h>
#include <nuttx/arch.h>
#include <arch/irq.h>

#include "up_arch.h"
#include "up_internal.h"
#include "sched/sched.h"
#include "chip.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_SAMDL_DMAC

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

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

/* Number of additional DMA descriptors in LPRAM */

#ifndef CONFIG_SAMDL_DMAC_NDESC
#  define CONFIG_SAMDL_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-15) */
  uint8_t            dc_dir;      /* See enum sam_dmadir_e */
  uint32_t           dc_flags;    /* DMA channel flags */
  dma_callback_t     dc_callback; /* Callback invoked when the DMA completes */
  void              *dc_arg;      /* Argument passed to callback function */
#if CONFIG_SAMDL_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_takechsem(void);
static inline void sam_givechsem(void);
#if CONFIG_SAMDL_DMAC_NDESC > 0
static void   sam_takedsem(void);
static inline void sam_givedsem(void);
#endif
static void   sam_dmaterminate(struct sam_dmach_s *dmach, int result);
static int    sam_dmainterrupt(int irq, void *context);
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);
static uint16_t sam_bytes2beats(struct sam_dmach_s *dmach, size_t nbytes);
static int    sam_txbuffer(struct sam_dmach_s *dmach, uint32_t paddr,
                uint32_t maddr, size_t nbytes);
static int    sam_rxbuffer(struct sam_dmach_s *dmach, uint32_t paddr,
                uint32_t maddr, size_t nbytes);

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

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

static sem_t g_chsem;
#if CONFIG_SAMDL_DMAC_NDESC > 0
static sem_t g_dsem;
#endif

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

static struct sam_dmach_s g_dmach[SAMDL_NDMACHAN];

/* DMA descriptor tables positioned in LPRAM.  In this use case, it is
 * acceptable for the writeback descriptors to overlap the base
 * descriptors since the base descriptors are always initialized prior
 * to starting each DMA transaction.
 */

static struct dma_desc_s g_base_desc[SAMDL_NDMACHAN]
  __attribute__ ((section(".lpram"), aligned(16)));
#define g_writeback_desc g_base_desc

#if CONFIG_SAMDL_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_SAMDL_DMAC_NDESC]
  __attribute__ ((section(".lpram"), aligned(16)));
#endif

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

/****************************************************************************
 * Name: sam_takechsem() and sam_givechsem()
 *
 * Description:
 *   Used to get exclusive access to the DMA channel table
 *
 ****************************************************************************/

static void sam_takechsem(void)
{
  /* Take the semaphore (perhaps waiting) */

  while (sem_wait(&g_chsem) != 0)
    {
      /* The only case that an error should occur here is if the wait was
       * awakened by a signal.
       */

      ASSERT(errno == EINTR);
    }
}

static inline void sam_givechsem(void)
{
  (void)sem_post(&g_chsem);
}

/****************************************************************************
 * Name: sam_takedsem() and sam_givedsem()
 *
 * Description:
 *   Used to wait for availability of descriptors in the descriptor table.
 *
 ****************************************************************************/

#if CONFIG_SAMDL_DMAC_NDESC > 0
static void sam_takedsem(void)
{
  /* Take the semaphore (perhaps waiting) */

  while (sem_wait(&g_dsem) != 0)
    {
      /* The only case that an error should occur here is if the wait was
       * awakened by a signal.
       */

      ASSERT(errno == EINTR);
    }
}

static inline void sam_givedsem(void)
{
  (void)sem_post(&g_dsem);
}
#endif

/****************************************************************************
 * 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;

  /* Disable the DMA channel */

  flags = irqsave();
  putreg8(dmach->dc_chan, SAM_DMAC_CHID);
  putreg8(0, SAM_DMAC_CHCTRLA);

  /* Reset the DMA channel */

  putreg8(DMAC_CHCTRLA_SWRST, SAM_DMAC_CHCTRLA);

  /* Disable all channel interrupts */

  putreg8(1 << dmach->dc_chan, SAM_DMAC_CHINTENCLR);
  irqrestore(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;
}

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

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

  /* Process all pending channel interrupts */

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

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

      /* Clear all pending channel interrupt */

      putreg8(DMAC_INT_ALL, SAM_DMAC_CHINTFLAG);

      /* 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);
        }

      /* Check for channel suspend interrupt */

      else if ((intpend & DMAC_INTPEND_SUSP) != 0)
        {
          /* REVISIT: Do we want to do anything here? */
        }
    }

  return OK;
}

/****************************************************************************
 * Name: sam_addrincr
 *
 * Description:
 *   Peripheral address increment for each beat.
 *
 ****************************************************************************/

#if 0 // Not used
static size_t sam_addrincr(struct sam_dmach_s *dmach)
{
  size_t beatsize;
  size_t stepsize;
  int shift

  /* How bit is one beat? {1,2,4} */

  shift    = (dmach->dc_flags & DMACH_FLAG_BEATSIZE_MASK) >>
             DMACH_FLAG_BEATSIZE_SHIFT;
  beatsize = (1 << shift);

  /* What is the address increment per beat? {1,4,6,...,128} */

  shift    = (dmach->dc_flags & DMACH_FLAG_STEPSIZE_MASK) >>
             DMACH_FLAG_STEPSIZE_SHIFT;
  stepsize = (1 << shift);

  return (beatsize * stepsize);
}
#endif

/****************************************************************************
 * 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_SAMDL_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.
       */

      sam_takedsem();

      /* 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_SAMDL_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_SAMDL_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_SAMDL_DMAC_NDESC > 0
      /* In either case, this is the new tail of the list. */

      dmach->dc_tail = desc;
#endif
    }

  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_SAMDL_DMAC_NDESC > 0
  struct dma_desc_s *next;
#endif

  /* Get the base descriptor pointer */

  desc           = &g_base_desc[dmach->dc_chan];
#if CONFIG_SAMDL_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_SAMDL_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));
      sam_givedsem();
    }
#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)
{
  int beatsize;

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

  beatsize = (dmach->dc_flags & 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, size_t nbytes)
{
  size_t mask;
  int beatsize;
  size_t nbeats;

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

  beatsize = (dmach->dc_flags & 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, size_t nbytes)
{
  uint16_t btctrl;
  uint16_t btcnt;
  uint16_t tmp;

  DEBUGASSERT(dmac->dc_dir == DMADIR_UNKOWN || dmac->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     = (dmach->dc_flags & DMACH_FLAG_BEATSIZE_MASK) >> DMACH_FLAG_BEATSIZE_SHIFT;
  btctrl |= tmp << LPSRAM_BTCTRL_BEATSIZE_SHIFT;

  if ((dmach->dc_flags & DMACH_FLAG_MEM_INCREMENT) != 0)
    {
      btctrl |= LPSRAM_BTCTRL_SRCINC;
    }

  if ((dmach->dc_flags & DMACH_FLAG_PERIPH_INCREMENT) != 0)
    {
      btctrl |= LPSRAM_BTCTRL_DSTINC;
    }

  if ((dmach->dc_flags & DMACH_FLAG_STEPSEL) == DMACH_FLAG_STEPSEL_PERIPH)
    {
      btctrl |= LPSRAM_BTCTRL_STEPSEL;
    }

  tmp     = (dmach->dc_flags & DMACH_FLAG_STEPSIZE_MASK) >> LPSRAM_BTCTRL_STEPSIZE_SHIFT;
  btctrl |= tmp << LPSRAM_BTCTRL_STEPSIZE_SHIFT;

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

  btcnt   = sam_bytes2beats(dmach, nbytes);

  /* 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, size_t nbytes)
{
  uint16_t btctrl;
  uint16_t btcnt;
  uint16_t tmp;

  DEBUGASSERT(dmac->dc_dir == DMADIR_UNKOWN || dmac->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     = (dmach->dc_flags & DMACH_FLAG_BEATSIZE_MASK) >> DMACH_FLAG_BEATSIZE_SHIFT;
  btctrl |= tmp << LPSRAM_BTCTRL_BEATSIZE_SHIFT;

  if ((dmach->dc_flags & DMACH_FLAG_PERIPH_INCREMENT) != 0)
    {
      btctrl |= LPSRAM_BTCTRL_SRCINC;
    }

  if ((dmach->dc_flags & DMACH_FLAG_MEM_INCREMENT) != 0)
    {
      btctrl |= LPSRAM_BTCTRL_DSTINC;
    }

  if ((dmach->dc_flags & DMACH_FLAG_STEPSEL) == DMACH_FLAG_STEPSEL_MEM)
    {
      btctrl |= LPSRAM_BTCTRL_STEPSEL;
    }

  tmp     = (dmach->dc_flags & DMACH_FLAG_STEPSIZE_MASK) >> LPSRAM_BTCTRL_STEPSIZE_SHIFT;
  btctrl |= tmp << LPSRAM_BTCTRL_STEPSIZE_SHIFT;

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

  btcnt   = sam_bytes2beats(dmach, nbytes);

  /* 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: up_dmainitialize
 *
 * Description:
 *   Initialize the DMA subsystem
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

void weak_function up_dmainitialize(void)
{
  dmallvdbg("Initialize DMAC\n");
  int i;

  /* Initialize global semaphores */

  sem_init(&g_chsem, 0, 1);
#if CONFIG_SAMDL_DMAC_NDESC > 0
  sem_init(&g_dsem, 0, CONFIG_SAMDL_DMAC_NDESC);
#endif

  /* Initialized the DMA channel table */

  for (i = 0; i < SAMDL_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)*SAMDL_NDMACHAN);
  memset(g_writeback_desc, 0, sizeof(struct dma_desc_s)*SAMDL_NDMACHAN);
#if CONFIG_SAMDL_DMAC_NDESC > 0
  memset(g_dma_desc, 0, sizeof(struct dma_desc_s)*CONFIG_SAMDL_DMAC_NDESC);
#endif

  /* Enable peripheral clock */

  sam_dmac_enableperiph();

  /* Disable and reset the DMAC */

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

  /* Attach DMA interrupt vector */

  (void)irq_attach(SAM_IRQ_DMAC, sam_dmainterrupt);

  /* 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);

  /* 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 IRQ at the NVIC (still disabled at the DMA controller) */

  up_enable_irq(SAM_IRQ_DMAC);
}

/****************************************************************************
 * 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 chflags)
{
  struct sam_dmach_s *dmach;
  irqstate_t flags;
  unsigned int chndx;

  /* Search for an available DMA channel */

  dmach = NULL;
  sam_takechsem();

  for (chndx = 0; chndx < SAMDL_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_flags = chflags;

          /* Disable the DMA channel */

          flags = irqsave();
          putreg8(chndx, SAM_DMAC_CHID);
          putreg8(0, SAM_DMAC_CHCTRLA);

          /* Reset the channel */

          putreg8(DMAC_CHCTRLA_SWRST, SAM_DMAC_CHCTRLA);

          /* Disable all channel interrupts */

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

  sam_givechsem();

  dmavdbg("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 used
 *   and sam_dmaconfig() is called before each DMA to configure the DMA channel
 *   appropriately.
 *
 * Returned Value:
 *   None
 *
 ************************************************************************************/

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

  /* Set the new DMA channel flags. */

  dmavdbg("chflags: %08x\n",  (int)chflags);
  dmach->dc_flags = chflags;
}

/****************************************************************************
 * 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;

  dmavdbg("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_flags = 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;

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

#if CONFIG_SAMDL_DMAC_NDESC > 0
  dmavdbg("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);

  /* 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, 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 do).
           *
           * REVISIT: What if stepsize is not 1?
           */

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

          if ((dmach->dc_flags & 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, 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;

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

#if CONFIG_SAMDL_DMAC_NDESC > 0
  dmavdbg("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);

  /* 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, 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_flags & DMACH_FLAG_PERIPH_INCREMENT) != 0)
            {
              paddr += maxtransfer;
            }

          if ((dmach->dc_flags & 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, 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;
  uint8_t ctrla;
  uint32_t chctrlb;
  uint32_t tmp;
  uint8_t qosctrl;
  uint8_t periphqos;
  uint8_t memqos;
  int ret = -EINVAL;

  dmavdbg("dmach: %p callback: %p arg: %p\n", dmach, callback, arg);
  DEBUGASSERT(dmach != NULL && dmach->dc_chan < SAMDL_NDMACHAN);
  head = &g_base_desc[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;

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

      flags = irqsave();
      putreg8(dmach->dc_chan, SAM_DMAC_CHID);
      putreg8(0, SAM_DMAC_CHCTRLA);

      /* Setup the Channel Control B Register
       *
       *   DMAC_CHCTRLB_EVACT_TRIG   - Normal transfer and trigger
       *   DMAC_CHCTRLB_EVIE=0       - No channel input actions
       *   DMAC_CHCTRLB_EVOE=0       - Channel event output disabled
       *   DMAC_CHCTRLB_LVL          - Determined by DMACH_FLAG_PRIORITY
       *   DMAC_CHCTRLB_TRIGSRC      - Determined by DMACH_FLAG_PERIPH_*XTRIG
       *   DMAC_CHCTRLB_TRIGACT_BEAT - One trigger required for beat transfer
       *   DMAC_CHCTRLB_CMD_NOACTION - No action
       */

      chctrlb = DMAC_CHCTRLB_EVACT_TRIG | DMAC_CHCTRLB_TRIGACT_BEAT |
                DMAC_CHCTRLB_CMD_NOACTION;

      tmp = (dmach->dc_flags & DMACH_FLAG_PRIORITY_MASK) >>
             DMACH_FLAG_PRIORITY_SHIFT;
      chctrlb |= tmp << DMAC_CHCTRLB_LVL_SHIFT;

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

          tmp = (dmach->dc_flags & DMACH_FLAG_PERIPH_TXTRIG_MASK) >>
                 DMACH_FLAG_PERIPH_TXTRIG_SHIFT;
        }
      else
        {
          /* Peripheral to memory */

          DEBUGASSERT(dmach->dc_dir == DMADIR_RX);
          tmp = (dmach->dc_flags & DMACH_FLAG_PERIPH_RXTRIG_MASK) >>
                 DMACH_FLAG_PERIPH_RXTRIG_SHIFT;
        }

      chctrlb |= tmp << DMAC_CHCTRLB_TRIGSRC_SHIFT;
      putreg8(chctrlb, SAM_DMAC_CHCTRLB);

      /* Setup the Quality of Service Control Register
       *
       *   DMAC_QOSCTRL_WRBQOS_DISABLE - Background
       *   DMAC_QOSCTRL_FQOS, DMAC_QOSCTRL_DQOS - Depend on DMACH_FLAG_PERIPH_QOS
       *     and DMACH_FLAG_MEM_QOS
       */

      periphqos = (dmach->dc_flags & DMACH_FLAG_PERIPH_QOS_MASK) >>
                  DMACH_FLAG_PERIPH_QOS_SHIFT;
      memqos    = (dmach->dc_flags & DMACH_FLAG_MEM_QOS_MASK) >>
                  DMACH_FLAG_MEM_QOS_SHIFT;

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

          qosctrl = (memqos << DMAC_QOSCTRL_FQOS_SHIFT) |
                    (periphqos << DMAC_QOSCTRL_DQOS_SHIFT);
        }
      else
        {
          /* Peripheral to memory */

          DEBUGASSERT(dmach->dc_dir == DMADIR_RX);
          qosctrl = (periphqos << DMAC_QOSCTRL_FQOS_SHIFT) |
                    (memqos << DMAC_QOSCTRL_DQOS_SHIFT);
        }

      putreg8(qosctrl | DMAC_QOSCTRL_WRBQOS_DISABLE, SAM_DMAC_QOSCTRL);

      /* Enable the channel */

      ctrla = DMAC_CHCTRLA_ENABLE;
      if (dmach->dc_flags & DMACH_FLAG_RUNINSTDBY)
        {
          ctrla |= DMAC_CHCTRLA_RUNSTDBY;
        }

      putreg8(ctrla, SAM_DMAC_CHCTRLA);

      /* Enable DMA channel interrupts */

      putreg8(DMAC_INT_TERR | DMAC_INT_TCMPL, SAM_DMAC_CHINTENSET);
      irqrestore(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;

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

  flags = irqsave();
  sam_dmaterminate(dmach, -EINTR);
  irqrestore(flags);
}

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

#ifdef CONFIG_DEBUG_DMA
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;

  /* Sample DMAC registers. */

  flags            = irqsave();
  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->qosctrl    = getreg8(SAM_DMAC_QOSCTRL);     /* Quality of Service 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->chid       = getreg8(SAM_DMAC_CHID);        /* Channel ID Register */
  regs->chctrla    = getreg8(SAM_DMAC_CHCTRLA);     /* Channel Control A Register */
  regs->chctrlb    = getreg32(SAM_DMAC_CHCTRLB);    /* Channel Control B Register */
  regs->chintflag  = getreg8(SAM_DMAC_CHINTFLAG);   /* Channel Interrupt Flag Status and Clear Register */
  regs->chstatus   = getreg8(SAM_DMAC_CHSTATUS);    /* Channel Status Register */
}
#endif /* CONFIG_DEBUG_DMA */

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

#ifdef CONFIG_DEBUG_DMA
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;

  dmadbg("%s\n", msg);
  dmadbg("  DMAC Registers:\n");
  dmadbg("         CTRL: %04x      CRCCTRL: %04x      CRCDATAIN: %08x  CRCCHKSUM: %08x\n",
         regs->ctrl, regs->crcctrl, regs->crcdatain, regs->crcchksum);
  dmadbg("    CRCSTATUS: %02x        DBGCTRL: %02x          QOSCTRL: %02x       SWTRIGCTRL: %08x\n",
         regs->crcstatus, regs->dbgctrl, regs->qosctrl, regs->swtrigctrl);
  dmadbg("     PRICTRL0: %08x  INTPEND: %04x     INSTSTATUS: %08x     BUSYCH: %08x\n",
         regs->prictrl0, regs->intpend, regs->intstatus, regs->busych);
  dmadbg("       PENDCH: %08x   ACTIVE: %08x   BASEADDR: %08x    WRBADDR: %08x\n",
         regs->pendch, regs->active, regs->baseaddr, regs->wrbaddr);
  dmadbg("         CHID: %02x       CHCRTRLA: %02x         CHCRTRLB: %08x   CHINFLAG: %02x\n",
         regs->chid, regs->chctrla, regs->chctrlb, regs->chintflag,
  dmadbg("     CHSTATUS: %02x\n",
         regs->chstatus);
}
#endif /* CONFIG_DEBUG_DMA */
#endif /* CONFIG_SAMDL_DMAC */
