/****************************************************************************
 * arch/arm/src/rp2040/rp2040_i2s.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 <sys/types.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#include <debug.h>

#include <nuttx/arch.h>
#include <nuttx/mutex.h>
#include <nuttx/semaphore.h>
#include <nuttx/kmalloc.h>
#include <nuttx/queue.h>
#include <nuttx/wdog.h>
#include <nuttx/wqueue.h>
#include <nuttx/spi/spi.h>
#include <nuttx/audio/audio.h>
#include <nuttx/audio/i2s.h>

#include <arch/board/board.h>

#include "arm_internal.h"
#include "rp2040_gpio.h"
#include "rp2040_dmac.h"
#include "rp2040_i2s_pio.h"

#ifdef CONFIG_RP2040_I2S

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

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

#ifndef CONFIG_SCHED_WORKQUEUE
#  error Work queue support is required (CONFIG_SCHED_WORKQUEUE)
#endif

#ifndef CONFIG_AUDIO
#  error CONFIG_AUDIO required by this driver
#endif

#ifndef CONFIG_RP2040_I2S_MAXINFLIGHT
#  define CONFIG_RP2040_I2S_MAXINFLIGHT 16
#endif

/* Debug ********************************************************************/

/* Check if SSC debug is enabled (non-standard.. no support in
 * include/debug.h
 */

#ifndef CONFIG_DEBUG_I2S_INFO
#  undef CONFIG_RP2040_I2S_DUMPBUFFERS
#endif

/* The I2S can handle most any bit width from 8 to 32.  However, the DMA
 * logic here is constrained to byte, half-word, and word sizes.
 */

#ifndef CONFIG_RP2040_I2S_DATALEN
#  define CONFIG_RP2040_I2S_DATALEN 16
#endif

#if CONFIG_RP2040_I2S_DATALEN == 8
#  define RP2040_I2S_DATAMASK  0
#elif CONFIG_RP2040_I2S_DATALEN == 16
#  define RP2040_I2S_DATAMASK  1
#elif  CONFIG_RP2040_I2S_DATALEN < 8 || CONFIG_RP2040_I2S_DATALEN > 16
#  error Invalid value for CONFIG_RP2040_I2S_DATALEN
#else
#  error Valid but supported value for CONFIG_RP2040_I2S_DATALEN
#endif

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

/* I2S buffer container */

struct rp2040_buffer_s
{
  struct rp2040_buffer_s *flink; /* Supports a singly linked list */
  i2s_callback_t callback;       /* Function to call when the transfer
                                  * completes */
  uint32_t timeout;              /* The timeout value to use with DMA
                                  * transfers */
  void *arg;                     /* The argument to be returned with the
                                  * callback */
  struct ap_buffer_s *apb;       /* The audio buffer */
  int result;                    /* The result of the transfer */
};

/* This structure describes the state of one receiver or transmitter
 * transport.
 */

struct rp2040_transport_s
{
  DMA_HANDLE dma;               /* I2S DMA handle */
  struct wdog_s dog;            /* Watchdog that handles DMA timeouts */
  sq_queue_t pend;              /* A queue of pending transfers */
  sq_queue_t act;               /* A queue of active transfers */
  sq_queue_t done;              /* A queue of completed transfers */
  struct work_s work;           /* Supports worker thread operations */
  uint32_t timeout;             /* Current DMA timeout value */
};

/* The state of the one I2S peripheral */

struct rp2040_i2s_s
{
  struct i2s_dev_s  dev;            /* Externally visible I2S interface */
  mutex_t           lock;           /* Assures mutually exclusive access to I2S */
  bool              initialized;    /* Has I2S interface been initialized */
  uint8_t           datalen;        /* Data width (8 or 16) */
#ifdef CONFIG_DEBUG_FEATURES
  uint8_t           align;          /* Log2 of data width (0 or 1) */
#endif
  uint32_t          samplerate;     /* Data sample rate */
  uint32_t          channels;       /* Audio channels (1:mono or 2:stereo) */
  dma_config_t      txconfig;       /* TX DMA configuration */
  struct rp2040_transport_s tx;     /* TX transport state */

  /* Pre-allocated pool of buffer containers */

  sem_t bufsem;                     /* Buffer wait semaphore */
  struct rp2040_buffer_s *freelist; /* A list a free buffer containers */
  struct rp2040_buffer_s containers[CONFIG_RP2040_I2S_MAXINFLIGHT];
};

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

/* Register helpers */

#ifdef CONFIG_RP2040_I2S_DUMPBUFFERS
#  define       i2s_dump_buffer(m,b,s) lib_dumpbuffer(m,b,s)
#else
#  define       i2s_dump_buffer(m,b,s)
#endif

/* Buffer container helpers */

static struct rp2040_buffer_s *
                i2s_buf_allocate(struct rp2040_i2s_s *priv);
static void     i2s_buf_free(struct rp2040_i2s_s *priv,
                             struct rp2040_buffer_s *bfcontainer);
static void     i2s_buf_initialize(struct rp2040_i2s_s *priv);

/* DMA support */

static void     i2s_txdma_timeout(wdparm_t arg);
static int      i2s_txdma_setup(struct rp2040_i2s_s *priv);
static void     i2s_tx_worker(void *arg);
static void     i2s_tx_schedule(struct rp2040_i2s_s *priv, int result);
static void     i2s_txdma_callback(DMA_HANDLE handle, uint8_t result,
                                   void *arg);

/* I2S methods (and close friends) */

static int      i2s_checkwidth(struct rp2040_i2s_s *priv, int bits);

static int      rp2040_i2s_txchannels(struct i2s_dev_s *dev,
                                      uint8_t channels);
static uint32_t rp2040_i2s_txsamplerate(struct i2s_dev_s *dev,
                                        uint32_t rate);
static uint32_t rp2040_i2s_txdatawidth(struct i2s_dev_s *dev, int bits);
static int      rp2040_i2s_send(struct i2s_dev_s *dev,
                                struct ap_buffer_s *apb,
                                i2s_callback_t callback, void *arg,
                                uint32_t timeout);
static int      rp2040_i2s_ioctl(struct i2s_dev_s *dev, int cmd,
                                 unsigned long arg);

/* Initialization */

static int      i2s_dma_flags(struct rp2040_i2s_s *priv);
static int      i2s_dma_allocate(struct rp2040_i2s_s *priv);
static void     i2s_dma_free(struct rp2040_i2s_s *priv);
static void     i2s_configure(struct rp2040_i2s_s *priv);

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

/* I2S device operations */

static const struct i2s_ops_s g_i2sops =
{
  .i2s_txchannels   = rp2040_i2s_txchannels,
  .i2s_txsamplerate = rp2040_i2s_txsamplerate,
  .i2s_txdatawidth  = rp2040_i2s_txdatawidth,
  .i2s_send         = rp2040_i2s_send,
  .i2s_ioctl        = rp2040_i2s_ioctl,
};

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

/****************************************************************************
 * Name: i2s_buf_allocate
 *
 * Description:
 *   Allocate a buffer container by removing the one at the head of the
 *   free list
 *
 * Input Parameters:
 *   priv - I2S state instance
 *
 * Returned Value:
 *   A non-NULL pointer to the allocate buffer container on success; NULL if
 *   there are no available buffer containers.
 *
 * Assumptions:
 *   The caller does NOT have exclusive access to the I2S state structure.
 *   That would result in a deadlock!
 *
 ****************************************************************************/

static struct rp2040_buffer_s *i2s_buf_allocate(struct rp2040_i2s_s *priv)
{
  struct rp2040_buffer_s *bfcontainer;
  irqstate_t flags;
  int ret;

  /* Set aside a buffer container.  By doing this, we guarantee that we will
   * have at least one free buffer container.
   */

  ret = nxsem_wait_uninterruptible(&priv->bufsem);
  if (ret < 0)
    {
      return NULL;
    }

  /* Get the buffer from the head of the free list */

  flags = enter_critical_section();
  bfcontainer = priv->freelist;
  DEBUGASSERT(bfcontainer);

  /* Unlink the buffer from the freelist */

  priv->freelist = bfcontainer->flink;
  leave_critical_section(flags);
  return bfcontainer;
}

/****************************************************************************
 * Name: i2s_buf_free
 *
 * Description:
 *   Free buffer container by adding it to the head of the free list
 *
 * Input Parameters:
 *   priv - I2S state instance
 *   bfcontainer - The buffer container to be freed
 *
 * Returned Value:
 *   None
 *
 * Assumptions:
 *   The caller has exclusive access to the I2S state structure
 *
 ****************************************************************************/

static void i2s_buf_free(struct rp2040_i2s_s *priv,
                         struct rp2040_buffer_s *bfcontainer)
{
  irqstate_t flags;

  /* Put the buffer container back on the free list */

  flags = enter_critical_section();
  bfcontainer->flink  = priv->freelist;
  priv->freelist = bfcontainer;
  leave_critical_section(flags);

  /* Wake up any threads waiting for a buffer container */

  nxsem_post(&priv->bufsem);
}

/****************************************************************************
 * Name: i2s_buf_initialize
 *
 * Description:
 *   Initialize the buffer container allocator by adding all of the
 *   pre-allocated buffer containers to the free list
 *
 * Input Parameters:
 *   priv - I2S state instance
 *
 * Returned Value:
 *   None
 *
 * Assumptions:
 *   Called early in I2S initialization so that there are no issues with
 *   concurrency.
 *
 ****************************************************************************/

static void i2s_buf_initialize(struct rp2040_i2s_s *priv)
{
  int i;

  priv->freelist = NULL;
  nxsem_init(&priv->bufsem, 0, CONFIG_RP2040_I2S_MAXINFLIGHT);

  for (i = 0; i < CONFIG_RP2040_I2S_MAXINFLIGHT; i++)
    {
      i2s_buf_free(priv, &priv->containers[i]);
    }
}

/****************************************************************************
 * Name: i2s_txdma_timeout
 *
 * Description:
 *   The TX watchdog timeout without completion of the TX DMA.
 *
 * Input Parameters:
 *   arg - The argument
 *
 * Returned Value:
 *   None
 *
 * Assumptions:
 *   Always called from the interrupt level with interrupts disabled.
 *
 ****************************************************************************/

static void i2s_txdma_timeout(wdparm_t arg)
{
  struct rp2040_i2s_s *priv = (struct rp2040_i2s_s *)arg;
  DEBUGASSERT(priv != NULL);

  /* Cancel the DMA */

  rp2040_dmastop(priv->tx.dma);

  /* Then schedule completion of the transfer to occur on the worker thread.
   */

  i2s_tx_schedule(priv, -ETIMEDOUT);
}

/****************************************************************************
 * Name: i2s_txdma_setup
 *
 * Description:
 *   Setup and initiate the next TX DMA transfer
 *
 * Input Parameters:
 *   priv - I2S state instance
 *
 * Returned Value:
 *   OK on success; a negated errno value on failure
 *
 * Assumptions:
 *   Interrupts are disabled
 *
 ****************************************************************************/

static int i2s_txdma_setup(struct rp2040_i2s_s *priv)
{
  struct rp2040_buffer_s *bfcontainer;
  struct ap_buffer_s *apb;
  uintptr_t samp;
  uint32_t timeout;
  apb_samp_t nbytes;
  int ret;

  /* If there is already an active transmission in progress, then bail
   * returning success.
   */

  if (!sq_empty(&priv->tx.act))
    {
      return OK;
    }

  /* If there are no pending transfer, then bail returning success */

  if (sq_empty(&priv->tx.pend))
    {
      return OK;
    }

  /* Adding the pending DMA */

  /* Remove the pending TX transfer at the head of the TX pending
   * queue.
   */

  bfcontainer = (struct rp2040_buffer_s *)sq_remfirst(&priv->tx.pend);
  DEBUGASSERT(bfcontainer && bfcontainer->apb);

  apb = bfcontainer->apb;

  /* Get the transfer information, accounting for any data offset */

  samp   = (uintptr_t)&apb->samp[apb->curbyte];
  nbytes = apb->nbytes - apb->curbyte;
#ifdef CONFIG_DEBUG_FEATURES
  DEBUGASSERT((samp & priv->align) == 0 && (nbytes & priv->align) == 0);
#endif

  /* Configure DMA stream */

  rp2040_txdmasetup(priv->tx.dma,
                    rp2040_i2s_pio_getdmaaddr(),
                    (uint32_t)samp, nbytes,
                    priv->txconfig);

  timeout = bfcontainer->timeout;

  /* Add the container to the list of active DMAs */

  sq_addlast((sq_entry_t *)bfcontainer, &priv->tx.act);

  /* Start the DMA, saving the container as the current active transfer */

  rp2040_dmastart(priv->tx.dma, i2s_txdma_callback, priv);
  rp2040_i2s_pio_enable(true);

  /* Start a watchdog to catch DMA timeouts */

  if (timeout > 0)
    {
      ret = wd_start(&priv->tx.dog, timeout,
                     i2s_txdma_timeout, (wdparm_t)priv);

      priv->tx.timeout = timeout;

      /* Check if we have successfully started the watchdog timer.  Note
       * that we do nothing in the case of failure to start the timer.  We
       * are already committed to the DMA anyway.  Let's just hope that the
       * DMA does not hang.
       */

      if (ret < 0)
        {
          i2serr("ERROR: wd_start failed: %d\n", ret);
        }
    }

  return OK;
}

/****************************************************************************
 * Name: i2s_tx_worker
 *
 * Description:
 *   TX transfer done worker
 *
 * Input Parameters:
 *   arg - the I2S device instance cast to void*
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

static void i2s_tx_worker(void *arg)
{
  struct rp2040_i2s_s *priv = (struct rp2040_i2s_s *)arg;
  struct rp2040_buffer_s *bfcontainer;
  irqstate_t flags;

  DEBUGASSERT(priv);

  /* When the transfer was started, the active buffer containers were removed
   * from the tx.pend queue and saved in the tx.act queue.  We get here when
   * the DMA is finished... either successfully, with a DMA error, or with a
   * DMA timeout.
   *
   * In any case, the buffer containers in tx.act will be moved to the end
   * of the tx.done queue and tx.act will be emptied before this worker is
   * started.
   */

  i2sinfo("tx.act.head=%p tx.done.head=%p\n",
           priv->tx.act.head, priv->tx.done.head);

  /* Check if the DMA is IDLE */

  if (sq_empty(&priv->tx.act))
    {
      /* Then start the next DMA.  This must be done with interrupts
       * disabled.
       */

      flags = enter_critical_section();
      i2s_txdma_setup(priv);
      leave_critical_section(flags);
    }

  /* Process each buffer in the tx.done queue */

  while (sq_peek(&priv->tx.done) != NULL)
    {
      /* Remove the buffer container from the tx.done queue.  NOTE that
       * interrupts must be enabled to do this because the tx.done queue is
       * also modified from the interrupt level.
       */

      flags = enter_critical_section();
      bfcontainer = (struct rp2040_buffer_s *)sq_remfirst(&priv->tx.done);
      leave_critical_section(flags);

      /* Perform the TX transfer done callback */

      DEBUGASSERT(bfcontainer && bfcontainer->callback);
      bfcontainer->callback(&priv->dev, bfcontainer->apb,
                            bfcontainer->arg, bfcontainer->result);

      /* Release our reference on the audio buffer.  This may very likely
       * cause the audio buffer to be freed.
       */

      apb_free(bfcontainer->apb);

      /* And release the buffer container */

      i2s_buf_free(priv, bfcontainer);
    }
}

/****************************************************************************
 * Name: i2s_tx_schedule
 *
 * Description:
 *   An TX DMA completion or timeout has occurred.  Schedule processing on
 *   the working thread.
 *
 * Input Parameters:
 *   priv - I2S state instance
 *   result - The result of the DMA transfer
 *
 * Returned Value:
 *   None
 *
 * Assumptions:
 *   - Interrupts are disabled
 *   - The TX timeout has been canceled.
 *
 ****************************************************************************/

static void i2s_tx_schedule(struct rp2040_i2s_s *priv, int result)
{
  struct rp2040_buffer_s *bfcontainer;
  int ret;

  /* Upon entry, the transfer(s) that just completed are the ones in the
   * priv->tx.act queue.
   */

  /* Move all entries from the tx.act queue to the tx.done queue */

  while (!sq_empty(&priv->tx.act))
    {
      /* Remove the next buffer container from the tx.act list */

      bfcontainer = (struct rp2040_buffer_s *)sq_remfirst(&priv->tx.act);

      /* Report the result of the transfer */

      bfcontainer->result = result;

      /* Add the completed buffer container to the tail of the tx.done
       * queue
       */

      sq_addlast((sq_entry_t *)bfcontainer, &priv->tx.done);
    }

  /* If the worker has completed running, then reschedule the working thread.
   * REVISIT:  There may be a race condition here.  So we do nothing is the
   * worker is not available.
   */

  if (work_available(&priv->tx.work))
    {
      /* Schedule the TX DMA done processing to occur on the worker thread. */

      ret = work_queue(HPWORK, &priv->tx.work, i2s_tx_worker, priv, 0);
      if (ret != 0)
        {
          i2serr("ERROR: Failed to queue TX work: %d\n", ret);
        }
    }
}

/****************************************************************************
 * Name: i2s_txdma_callback
 *
 * Description:
 *   This callback function is invoked at the completion of the I2S TX DMA.
 *
 * Input Parameters:
 *   handle - The DMA handler
 *   result - The result of the DMA transfer
 *   arg - A pointer to the chip select struction
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

static void i2s_txdma_callback(DMA_HANDLE handle, uint8_t result, void *arg)
{
  struct rp2040_i2s_s *priv = (struct rp2040_i2s_s *)arg;
  DEBUGASSERT(priv != NULL);

  /* Cancel the watchdog timeout */

  if (priv->tx.timeout > 0)
    {
      wd_cancel(&priv->tx.dog);
    }

  /* Then schedule completion of the transfer to occur on the worker thread */

  i2s_tx_schedule(priv, result);
}

/****************************************************************************
 * Name: i2s_checkwidth
 *
 * Description:
 *   Check for a valid bit width.  The I2S is capable of handling most any
 *   bit width from 8 to 16, but the DMA logic in this driver is constrained
 *   to 8- and 16-bit data widths
 *
 * Input Parameters:
 *   dev  - Device-specific state data
 *   bits - The I2S data with in bits.
 *
 * Returned Value:
 *   OK on success; a negated errno value on failure.
 *
 ****************************************************************************/

static int i2s_checkwidth(struct rp2040_i2s_s *priv, int bits)
{
  /* The I2S can handle most any bit width from 8 to 32.  However, the DMA
   * logic here is constrained to byte, half-word, and word sizes.
   */

  switch (bits)
    {
    case 8:
#ifdef CONFIG_DEBUG
      priv->align = 0;
#endif
      break;

    case 16:
#ifdef CONFIG_DEBUG
      priv->align = 1;
#endif
      break;

    default:
      i2serr("ERROR: Unsupported or invalid data width: %d\n", bits);
      return (bits < 8 || bits > 16) ? -EINVAL : -ENOSYS;
    }

  /* Save the new data width */

  priv->datalen = bits;
  return OK;
}

/****************************************************************************
 * Name: rp2040_i2s_txchannels
 *
 * Description:
 *   Set the I2S TX number of channels.
 *
 * Input Parameters:
 *   dev  - Device-specific state data
 *   channels - The I2S numbers of channels
 *
 * Returned Value:
 *   OK on success; a negated errno value on failure.
 *
 ****************************************************************************/

static int rp2040_i2s_txchannels(struct i2s_dev_s *dev, uint8_t channels)
{
  struct rp2040_i2s_s *priv = (struct rp2040_i2s_s *)dev;

  if (channels != 1 && channels != 2)
    {
      return -EINVAL;
    }

  priv->channels = channels;
  return OK;
}

/****************************************************************************
 * Name: rp2040_i2s_txsamplerate
 *
 * Description:
 *   Set the I2S TX sample rate.
 *
 * Input Parameters:
 *   dev  - Device-specific state data
 *   rate - The I2S sample rate in samples (not bits) per second
 *
 * Returned Value:
 *   OK on success; a negated errno value on failure.
 *
 ****************************************************************************/

static uint32_t rp2040_i2s_txsamplerate(struct i2s_dev_s *dev, uint32_t rate)
{
  struct rp2040_i2s_s *priv = (struct rp2040_i2s_s *)dev;

  DEBUGASSERT(priv && priv->samplerate >= 0 && rate > 0);

  if (rate < 8000)
    {
      return -EINVAL;
    }

  priv->samplerate = rate;
  return 0;
}

/****************************************************************************
 * Name: rp2040_i2s_txdatawidth
 *
 * Description:
 *   Set the I2S TX data width.  The TX bitrate is determined by
 *   sample_rate * data_width.
 *
 * Input Parameters:
 *   dev   - Device-specific state data
 *   bits - The I2S data with in bits.
 *
 * Returned Value:
 *   OK on success; a negated errno value on failure.
 *
 ****************************************************************************/

static uint32_t rp2040_i2s_txdatawidth(struct i2s_dev_s *dev, int bits)
{
  struct rp2040_i2s_s *priv = (struct rp2040_i2s_s *)dev;
  int ret;

  i2sinfo("Data width bits of tx = %d\n", bits);
  DEBUGASSERT(priv && bits > 1);

  /* Check if this is a bit width that we are configured to handle */

  ret = i2s_checkwidth(priv, bits);
  if (ret < 0)
    {
      i2serr("ERROR: i2s_checkwidth failed: %d\n", ret);
      return 0;
    }

  /* Update the DMA flags */

  ret = i2s_dma_flags(priv);
  if (ret < 0)
    {
      i2serr("ERROR: i2s_dma_flags failed: %d\n", ret);
      return 0;
    }

  return 0;
}

/****************************************************************************
 * Name: rp2040_i2s_send
 *
 * Description:
 *   Send a block of data on I2S.
 *
 * Input Parameters:
 *   dev      - Device-specific state data
 *   apb      - A pointer to the audio buffer from which to send data
 *   callback - A user provided callback function that will be called at
 *              the completion of the transfer.  The callback will be
 *              performed in the context of the worker thread.
 *   arg      - An opaque argument that will be provided to the callback
 *              when the transfer complete
 *   timeout  - The timeout value to use.  The transfer will be canceled
 *              and an ETIMEDOUT error will be reported if this timeout
 *              elapsed without completion of the DMA transfer.  Units
 *              are system clock ticks.  Zero means no timeout.
 *
 * Returned Value:
 *   OK on success; a negated errno value on failure.  NOTE:  This function
 *   only enqueues the transfer and returns immediately.  Success here only
 *   means that the transfer was enqueued correctly.
 *
 *   When the transfer is complete, a 'result' value will be provided as
 *   an argument to the callback function that will indicate if the transfer
 *   failed.
 *
 ****************************************************************************/

static int rp2040_i2s_send(struct i2s_dev_s *dev, struct ap_buffer_s *apb,
                    i2s_callback_t callback, void *arg, uint32_t timeout)
{
  struct rp2040_i2s_s *priv = (struct rp2040_i2s_s *)dev;
  struct rp2040_buffer_s *bfcontainer;
  irqstate_t flags;
  int ret;

  /* Make sure that we have valid pointers that that the data has uint32_t
   * alignment.
   */

  DEBUGASSERT(priv && apb);
  i2sinfo("apb=%p nbytes=%d arg=%p timeout=%" PRId32 "\n",
          apb, apb->nbytes - apb->curbyte, arg, timeout);

  i2s_dump_buffer("Sending", &apb->samp[apb->curbyte],
                  apb->nbytes - apb->curbyte);
#ifdef CONFIG_DEBUG_FEATURES
  DEBUGASSERT(((uintptr_t)&apb->samp[apb->curbyte] & priv->align) == 0);
#endif

  /* Allocate a buffer container in advance */

  bfcontainer = i2s_buf_allocate(priv);
  DEBUGASSERT(bfcontainer);

  /* Get exclusive access to the I2S driver data */

  ret = nxmutex_lock(&priv->lock);
  if (ret < 0)
    {
      goto errout_with_buf;
    }

  /* Add a reference to the audio buffer */

  apb_reference(apb);

  /* Initialize the buffer container structure */

  bfcontainer->callback = (void *)callback;
  bfcontainer->timeout  = timeout;
  bfcontainer->arg      = arg;
  bfcontainer->apb      = apb;
  bfcontainer->result   = -EBUSY;

  /* Add the buffer container to the end of the TX pending queue */

  flags = enter_critical_section();
  sq_addlast((sq_entry_t *)bfcontainer, &priv->tx.pend);

  leave_critical_section(flags);
  nxmutex_unlock(&priv->lock);
  return OK;

errout_with_buf:
  i2s_buf_free(priv, bfcontainer);
  return ret;
}

/****************************************************************************
 * Name: rp2040_i2s_cleanup_queues
 *
 * Description:
 *   Clean up the all buffers in the queues.
 *
 * Input Parameters:
 *   priv - I2S state instance
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

static void i2s_cleanup_queues(struct rp2040_i2s_s *priv)
{
  irqstate_t flags;
  struct rp2040_buffer_s *bfcontainer;

  while (sq_peek(&priv->tx.done) != NULL)
    {
      flags = enter_critical_section();
      bfcontainer = (struct rp2040_buffer_s *)sq_remfirst(&priv->tx.done);
      leave_critical_section(flags);
      bfcontainer->callback(&priv->dev, bfcontainer->apb,
                            bfcontainer->arg, OK);
      apb_free(bfcontainer->apb);
      i2s_buf_free(priv, bfcontainer);
    }

  while (sq_peek(&priv->tx.act) != NULL)
    {
      flags = enter_critical_section();
      bfcontainer = (struct rp2040_buffer_s *)sq_remfirst(&priv->tx.act);
      leave_critical_section(flags);
      bfcontainer->callback(&priv->dev, bfcontainer->apb,
                            bfcontainer->arg, OK);
      apb_free(bfcontainer->apb);
      i2s_buf_free(priv, bfcontainer);
    }

  while (sq_peek(&priv->tx.pend) != NULL)
    {
      flags = enter_critical_section();
      bfcontainer = (struct rp2040_buffer_s *)sq_remfirst(&priv->tx.pend);
      leave_critical_section(flags);
      bfcontainer->apb->flags |= AUDIO_APB_FINAL;
      bfcontainer->callback(&priv->dev, bfcontainer->apb,
                            bfcontainer->arg, OK);
      apb_free(bfcontainer->apb);
      i2s_buf_free(priv, bfcontainer);
    }
}

/****************************************************************************
 * Name: rp2040_i2s_ioctl
 *
 * Description:
 *   Perform a device ioctl
 *
 ****************************************************************************/

static int rp2040_i2s_ioctl(struct i2s_dev_s *dev, int cmd,
                            unsigned long arg)
{
  struct rp2040_i2s_s *priv = (struct rp2040_i2s_s *)dev;
  struct audio_buf_desc_s *bufdesc;
  int ret = -ENOTTY;

  switch (cmd)
    {
      /* AUDIOIOC_START - Start the audio stream.
       *
       *   ioctl argument:  Audio session
       */

      case AUDIOIOC_START:
        {
          irqstate_t flags;
          int mode;

          i2sinfo("AUDIOIOC_START\n");

          if (priv->channels == 1)
            {
              if (priv->datalen == 16)
                mode = RP2040_I2S_PIO_16BIT_MONO;
              else
                mode = RP2040_I2S_PIO_8BIT_MONO;
            }
          else
            {
              if (priv->datalen == 16)
                mode = RP2040_I2S_PIO_16BIT_STEREO;
              else
                mode = RP2040_I2S_PIO_8BIT_STEREO;
           }

          rp2040_i2s_pio_configure(mode, priv->samplerate);

          flags = enter_critical_section();
          ret = i2s_txdma_setup(priv);
          leave_critical_section(flags);
        }
        break;

      /* AUDIOIOC_STOP - Stop the audio stream.
       *
       *   ioctl argument:  Audio session
       */

#ifndef CONFIG_AUDIO_EXCLUDE_STOP
      case AUDIOIOC_STOP:
        {
          irqstate_t flags;

          i2sinfo("AUDIOIOC_STOP\n");

          flags = enter_critical_section();
          if (priv->tx.timeout > 0)
            {
              wd_cancel(&priv->tx.dog);
            }

          rp2040_dmastop(priv->tx.dma);
          leave_critical_section(flags);

          i2s_cleanup_queues(priv);

          ret = 0;
        }
        break;
#endif /* CONFIG_AUDIO_EXCLUDE_STOP */

      /* AUDIOIOC_PAUSE - Pause the audio stream.
       *
       *   ioctl argument:  Audio session
       */

#ifndef CONFIG_AUDIO_EXCLUDE_PAUSE_RESUME

      case AUDIOIOC_PAUSE:
        {
          irqstate_t flags;

          i2sinfo("AUDIOIOC_PAUSE\n");

          flags = enter_critical_section();
          if (priv->tx.timeout > 0)
            {
              priv->tx.timeout = wd_gettime(&priv->tx.dog);
              wd_cancel(&priv->tx.dog);
            }

          rp2040_i2s_pio_enable(false);
          leave_critical_section(flags);

          ret = 0;
        }
        break;

      /* AUDIOIOC_RESUME - Resume the audio stream.
       *
       *   ioctl argument:  Audio session
       */

      case AUDIOIOC_RESUME:
        {
          irqstate_t flags;

          i2sinfo("AUDIOIOC_RESUME\n");

          flags = enter_critical_section();
          if (priv->tx.timeout > 0)
            {
              wd_start(&priv->tx.dog, priv->tx.timeout,
                       i2s_txdma_timeout, (wdparm_t)priv);
            }

          rp2040_i2s_pio_enable(true);
          leave_critical_section(flags);

          ret = 0;
        }
        break;

#endif /* CONFIG_AUDIO_EXCLUDE_PAUSE_RESUME */

      /* AUDIOIOC_ALLOCBUFFER - Allocate an audio buffer
       *
       *   ioctl argument:  pointer to an audio_buf_desc_s structure
       */

      case AUDIOIOC_ALLOCBUFFER:
        {
          i2sinfo("AUDIOIOC_ALLOCBUFFER\n");

          bufdesc = (struct audio_buf_desc_s *)arg;
          ret = apb_alloc(bufdesc);
        }
        break;

      /* AUDIOIOC_FREEBUFFER - Free an audio buffer
       *
       *   ioctl argument:  pointer to an audio_buf_desc_s structure
       */

      case AUDIOIOC_FREEBUFFER:
        {
          i2sinfo("AUDIOIOC_FREEBUFFER\n");

          bufdesc = (struct audio_buf_desc_s *)arg;
          DEBUGASSERT(bufdesc->u.buffer != NULL);
          apb_free(bufdesc->u.buffer);
          ret = sizeof(struct audio_buf_desc_s);
        }
        break;

      default:
        break;
    }

  return ret;
}

/****************************************************************************
 * Name: i2s_dma_flags
 *
 * Description:
 *   Determine DMA FLAGS based on PID and data width
 *
 * Input Parameters:
 *   priv - Partially initialized I2S device structure.
 *
 * Returned Value:
 *   OK on success; a negated errno value on failure
 *
 ****************************************************************************/

static int i2s_dma_flags(struct rp2040_i2s_s *priv)
{
  switch (priv->datalen)
    {
    case 8:
      priv->txconfig.size = RP2040_DMA_SIZE_BYTE;
      break;

    case 16:
      priv->txconfig.size = RP2040_DMA_SIZE_HALFWORD;
      break;

    default:
      i2serr("ERROR: Unsupported data width: %d\n", priv->datalen);
      return -ENOSYS;
    }

  priv->txconfig.noincr = false;
  priv->txconfig.dreq = rp2040_i2s_pio_getdreq();

  return OK;
}

/****************************************************************************
 * Name: i2s_dma_allocate
 *
 * Description:
 *   Allocate I2S DMA channels
 *
 * Input Parameters:
 *   priv - Partially initialized I2S device structure.  This function
 *          will complete the DMA specific portions of the initialization
 *
 * Returned Value:
 *   OK on success; A negated errno value on failure.
 *
 ****************************************************************************/

static int i2s_dma_allocate(struct rp2040_i2s_s *priv)
{
  /* Allocate a TX DMA channel */

  priv->tx.dma = rp2040_dmachannel();
  if (!priv->tx.dma)
    {
      i2serr("ERROR: Failed to allocate the TX DMA channel\n");
      goto errout;
    }

  /* Success exit */

  return OK;

  /* Error exit */

errout:
  i2s_dma_free(priv);
  return -ENOMEM;
}

/****************************************************************************
 * Name: i2s_dma_free
 *
 * Description:
 *   Release DMA-related resources allocated by i2s_dma_allocate()
 *
 * Input Parameters:
 *   priv - Partially initialized I2S device structure.
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

static void i2s_dma_free(struct rp2040_i2s_s *priv)
{
  if (priv->tx.timeout > 0)
    {
      wd_cancel(&priv->tx.dog);
    }

  if (priv->tx.dma)
    {
      rp2040_dmafree(priv->tx.dma);
    }
}

/****************************************************************************
 * Name: i2s_configure
 *
 * Description:
 *   Configure I2S
 *
 * Input Parameters:
 *   priv - Partially initialized I2S device structure.  These functions
 *          will complete the I2S specific portions of the initialization
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

static void i2s_configure(struct rp2040_i2s_s *priv)
{
  /* Only configure if the port is not already configured */

  if (!priv->initialized)
    {
      rp2040_gpio_set_function(CONFIG_RP2040_I2S_DATA,
                               RP2040_GPIO_FUNC_PIO0);
      rp2040_gpio_set_function(CONFIG_RP2040_I2S_CLOCK,
                               RP2040_GPIO_FUNC_PIO0);
      rp2040_gpio_set_function(CONFIG_RP2040_I2S_CLOCK + 1,
                               RP2040_GPIO_FUNC_PIO0);

      priv->initialized = true;
    }

  /* Configure driver state specific to this I2S peripheral */

  priv->channels = 2;
  priv->samplerate = 44100;
  priv->datalen = CONFIG_RP2040_I2S_DATALEN;
#ifdef CONFIG_DEBUG
  priv->align   = RP2040_I2S_DATAMASK;
#endif
}

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

/****************************************************************************
 * Name: rp2040_i2sbus_initialize
 *
 * Description:
 *   Initialize the selected i2S port
 *
 * Input Parameters:
 *   Port number (for hardware that has multiple I2S interfaces)
 *
 * Returned Value:
 *   Valid I2S device structure reference on success; a NULL on failure
 *
 ****************************************************************************/

struct i2s_dev_s *rp2040_i2sbus_initialize(int port)
{
  struct rp2040_i2s_s *priv = NULL;
  irqstate_t flags;
  int ret;

  i2sinfo("port: %d\n", port);

  priv = (struct rp2040_i2s_s *)kmm_zalloc(sizeof(struct rp2040_i2s_s));
  if (!priv)
    {
      i2serr("ERROR: Failed to allocate a chip select structure\n");
      return NULL;
    }

  /* Set up the initial state for this chip select structure.  Other fields
   * were zeroed by kmm_zalloc().
   */

  /* Initialize the common parts for the I2S device structure */

  nxmutex_init(&priv->lock);
  priv->dev.ops = &g_i2sops;

  /* Initialize buffering */

  i2s_buf_initialize(priv);

  flags = enter_critical_section();

  i2s_configure(priv);

  /* Allocate DMA channels */

  ret = i2s_dma_allocate(priv);
  if (ret < 0)
    {
      goto errout_with_alloc;
    }

  leave_critical_section(flags);

  /* Success exit */

  return &priv->dev;

  /* Failure exits */

errout_with_alloc:
  nxmutex_destroy(&priv->lock);
  nxsem_destroy(&priv->bufsem);
  kmm_free(priv);
  return NULL;
}

#endif /* CONFIG_RP2040_I2S */
