/****************************************************************************
 * drivers/ioexpander/mcp23x08.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 <assert.h>
#include <errno.h>
#include <debug.h>

#include <nuttx/irq.h>
#include <nuttx/i2c/i2c_master.h>
#include <nuttx/kmalloc.h>
#include <nuttx/ioexpander/ioexpander.h>

#include "mcp23x08.h"

#if defined(CONFIG_IOEXPANDER_MCP23X08)

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

#ifndef CONFIG_I2C
#  warning I2C support is required (CONFIG_I2C)
#endif

#if (!defined CONFIG_SCHED_WORKQUEUE) && (defined CONFIG_MCP23X08_INT_ENABLE)
#  error Work queue support is required (CONFIG_SCHED_WORKQUEUE)
#endif

#if (!defined CONFIG_SCHED_HPWORK) && (defined CONFIG_MCP23X08_INT_ENABLE)
#  error High-Priority Work support is required (CONFIG_SCHED_HPWORK)
#endif

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

static inline int mcp23x08_write(FAR struct mcp23x08_dev_s *priv,
             FAR const uint8_t *wbuffer, int wbuflen);
static inline int mcp23x08_writeread(FAR struct mcp23x08_dev_s *priv,
             FAR const uint8_t *wbuffer, int wbuflen, FAR uint8_t *rbuffer,
             int rbuflen);
static int mcp23x08_direction(FAR struct ioexpander_dev_s *dev, uint8_t pin,
             int dir);
static int mcp23x08_option(FAR struct ioexpander_dev_s *dev, uint8_t pin,
             int opt, void *val);
static int mcp23x08_writepin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
             bool value);
static int mcp23x08_readpin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
             FAR bool *value);
static int mcp23x08_readbuf(FAR struct ioexpander_dev_s *dev, uint8_t pin,
             FAR bool *value);
#ifdef CONFIG_IOEXPANDER_MULTIPIN
static int mcp23x08_multiwritepin(FAR struct ioexpander_dev_s *dev,
             FAR const uint8_t *pins, FAR const bool *values, int count);
static int mcp23x08_multireadpin(FAR struct ioexpander_dev_s *dev,
             FAR const uint8_t *pins, FAR bool *values, int count);
static int mcp23x08_multireadbuf(FAR struct ioexpander_dev_s *dev,
             FAR uint8_t *pins, FAR bool *values, int count);
#endif
#ifdef CONFIG_IOEXPANDER_INT_ENABLE
static FAR void *mcp23x08_attach(FAR struct ioexpander_dev_s *dev,
             ioe_pinset_t pinset, ioe_callback_t callback, FAR void *arg);
static int mcp23x08_detach(FAR struct ioexpander_dev_s *dev,
             FAR void *handle);
#endif

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

#ifndef CONFIG_MCP23X08_MULTIPLE
/* If only a single MCP23X08 device is supported, then the driver state
 * structure may as well be pre-allocated.
 */

static struct mcp23x08_dev_s g_mcp23x08;

/* Otherwise, we will need to maintain allocated driver instances in a list */

#else
static struct mcp23x08_dev_s *g_mcp23x08list;
#endif

/* I/O expander vtable */

static const struct ioexpander_ops_s g_mcp23x08_ops =
{
  mcp23x08_direction,
  mcp23x08_option,
  mcp23x08_writepin,
  mcp23x08_readpin,
  mcp23x08_readbuf
#ifdef CONFIG_IOEXPANDER_MULTIPIN
  , mcp23x08_multiwritepin
  , mcp23x08_multireadpin
  , mcp23x08_multireadbuf
#endif
#ifdef CONFIG_IOEXPANDER_INT_ENABLE
  , mcp23x08_attach
  , mcp23x08_detach
#endif
};

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

/****************************************************************************
 * Name: mcp23x08_write
 *
 * Description:
 *   Write to the I2C device.
 *
 ****************************************************************************/

static inline int mcp23x08_write(FAR struct mcp23x08_dev_s *priv,
                                 FAR const uint8_t *wbuffer, int wbuflen)
{
  struct i2c_msg_s msg;
  int ret;

  /* Setup for the transfer */

  msg.frequency = priv->config->frequency;
  msg.addr      = priv->config->address;
  msg.flags     = 0;
  msg.buffer    = (FAR uint8_t *)wbuffer;  /* Override const */
  msg.length    = wbuflen;

  /* Then perform the transfer. */

  ret = I2C_TRANSFER(priv->i2c, &msg, 1);
  return (ret >= 0) ? OK : ret;
}

/****************************************************************************
 * Name: mcp23x08_writeread
 *
 * Description:
 *   Write to then read from the I2C device.
 *
 ****************************************************************************/

static inline int mcp23x08_writeread(FAR struct mcp23x08_dev_s *priv,
                                     FAR const uint8_t *wbuffer, int wbuflen,
                                     FAR uint8_t *rbuffer, int rbuflen)
{
  struct i2c_config_s config;

  /* Set up the configuration and perform the write-read operation */

  config.frequency = priv->config->frequency;
  config.address   = priv->config->address;
  config.addrlen   = 7;

  return i2c_writeread(priv->i2c, &config, wbuffer, wbuflen,
                       rbuffer, rbuflen);
}

/****************************************************************************
 * Name: mcp23x08_setbit
 *
 * Description:
 *  Write a bit in a register pair
 *
 ****************************************************************************/

static int mcp23x08_setbit(FAR struct mcp23x08_dev_s *priv, uint8_t addr,
                           uint8_t pin, bool bitval)
{
  uint8_t buf[2];
  int ret;

  if (pin > 7)
    {
      return -ENXIO;
    }

  buf[0] = addr;

#ifdef CONFIG_MCP23X08_SHADOW_MODE
  /* Get the shadowed register value */

  buf[1] = priv->sreg[addr];

#else
  /* Get the register value from the IO-Expander */

  ret = mcp23x08_writeread(priv, &buf[0], 1, &buf[1], 1);
  if (ret < 0)
    {
      return ret;
    }
#endif

  if (bitval)
    {
      buf[1] |= (1 << pin);
    }
  else
    {
      buf[1] &= ~(1 << pin);
    }

#ifdef CONFIG_MCP23X08_SHADOW_MODE
  /* Save the new register value in the shadow register */

  priv->sreg[addr] = buf[1];
#endif

  ret = mcp23x08_write(priv, buf, 2);
#ifdef CONFIG_MCP23X08_RETRY
  if (ret != OK)
    {
      /* Try again (only once) */

      ret = mcp23x08_write(priv, buf, 2);
    }
#endif

  return ret;
}

/****************************************************************************
 * Name: mcp23x08_getbit
 *
 * Description:
 *  Get a bit from a register pair
 *
 ****************************************************************************/

static int mcp23x08_getbit(FAR struct mcp23x08_dev_s *priv, uint8_t addr,
                           uint8_t pin, FAR bool *val)
{
  uint8_t buf;
  int ret;

  if (pin > 7)
    {
      return -ENXIO;
    }

  ret = mcp23x08_writeread(priv, &addr, 1, &buf, 1);
  if (ret < 0)
    {
      return ret;
    }

#ifdef CONFIG_MCP23X08_SHADOW_MODE
  /* Save the new register value in the shadow register */

  priv->sreg[addr] = buf;
#endif

  *val = (buf >> pin) & 1;
  return OK;
}

/****************************************************************************
 * Name: mcp23x08_direction
 *
 * Description:
 *   Set the direction of an ioexpander pin. Required.
 *
 * Input Parameters:
 *   dev - Device-specific state data
 *   pin - The index of the pin to alter in this call
 *   dir - One of the IOEXPANDER_DIRECTION_ macros
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int mcp23x08_direction(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                             int direction)
{
  FAR struct mcp23x08_dev_s *priv = (FAR struct mcp23x08_dev_s *)dev;
  int ret;

  if (direction != IOEXPANDER_DIRECTION_IN &&
      direction != IOEXPANDER_DIRECTION_IN_PULLUP &&
      direction != IOEXPANDER_DIRECTION_OUT)
    {
      return -EINVAL;
    }

  /* Get exclusive access to the MCP23X08 */

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

  ret = mcp23x08_setbit(priv, MCP23X08_IODIR, pin,
                        (direction == IOEXPANDER_DIRECTION_IN) ||
                        (direction == IOEXPANDER_DIRECTION_IN_PULLUP));
  if (ret < 0)
    {
      nxmutex_unlock(&priv->lock);
      return ret;
    }

  ret = mcp23x08_setbit(priv, MCP23X08_GPPU, pin,
                        (direction == IOEXPANDER_DIRECTION_IN_PULLUP));

  nxmutex_unlock(&priv->lock);
  return ret;
}

/****************************************************************************
 * Name: mcp23x08_option
 *
 * Description:
 *   Set pin options. Required.
 *   Since all IO expanders have various pin options, this API allows setting
 *     pin options in a flexible way.
 *
 * Input Parameters:
 *   dev - Device-specific state data
 *   pin - The index of the pin to alter in this call
 *   opt - One of the IOEXPANDER_OPTION_ macros
 *   val - The option's value
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int mcp23x08_option(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                           int opt, FAR void *value)
{
  FAR struct mcp23x08_dev_s *priv = (FAR struct mcp23x08_dev_s *)dev;
  int ret = -EINVAL;

  if (opt == IOEXPANDER_OPTION_INVERT)
    {
      /* Get exclusive access to the MCP23X08 */

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

      ret = mcp23x08_setbit(priv, MCP23X08_IPOL, pin,
                            ((uintptr_t)value == IOEXPANDER_VAL_INVERT));
      nxmutex_unlock(&priv->lock);
    }

  return ret;
}

/****************************************************************************
 * Name: mcp23x08_writepin
 *
 * Description:
 *   Set the pin level. Required.
 *
 * Input Parameters:
 *   dev - Device-specific state data
 *   pin - The index of the pin to alter in this call
 *   val - The pin level. Usually TRUE will set the pin high,
 *         except if OPTION_INVERT has been set on this pin.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int mcp23x08_writepin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                             bool value)
{
  FAR struct mcp23x08_dev_s *priv = (FAR struct mcp23x08_dev_s *)dev;
  int ret;

  /* Get exclusive access to the MCP23X08 */

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

  ret = mcp23x08_setbit(priv, MCP23X08_GPIO, pin, value);
  nxmutex_unlock(&priv->lock);
  return ret;
}

/****************************************************************************
 * Name: mcp23x08_readpin
 *
 * Description:
 *   Read the actual PIN level. This can be different from the last value
 *      written to this pin. Required.
 *
 * Input Parameters:
 *   dev    - Device-specific state data
 *   pin    - The index of the pin
 *   valptr - Pointer to a buffer where the pin level is stored. Usually TRUE
 *            if the pin is high, except if OPTION_INVERT has been set on
 *            this pin.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int mcp23x08_readpin(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                            FAR bool *value)
{
  FAR struct mcp23x08_dev_s *priv = (FAR struct mcp23x08_dev_s *)dev;
  int ret;

  /* Get exclusive access to the MCP23X08 */

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

  ret = mcp23x08_getbit(priv, MCP23X08_GPIO, pin, value);

  nxmutex_unlock(&priv->lock);
  return ret;
}

/****************************************************************************
 * Name: mcp23x08_readbuf
 *
 * Description:
 *   Read the buffered pin level.
 *   This can be different from the actual pin state. Required.
 *
 * Input Parameters:
 *   dev    - Device-specific state data
 *   pin    - The index of the pin
 *   valptr - Pointer to a buffer where the level is stored.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int mcp23x08_readbuf(FAR struct ioexpander_dev_s *dev, uint8_t pin,
                            FAR bool *value)
{
  FAR struct mcp23x08_dev_s *priv = (FAR struct mcp23x08_dev_s *)dev;
  int ret;

  /* Get exclusive access to the MCP23X08 */

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

  ret = mcp23x08_getbit(priv, MCP23X08_GPIO, pin, value);
  nxmutex_unlock(&priv->lock);
  return ret;
}

#ifdef CONFIG_IOEXPANDER_MULTIPIN

/****************************************************************************
 * Name: mcp23x08_getmultibits
 *
 * Description:
 *  Read multiple bits from MCP23X08 registers.
 *
 ****************************************************************************/

static int mcp23x08_getmultibits(FAR struct mcp23x08_dev_s *priv,
                                 uint8_t addr,
                                 FAR uint8_t *pins,
                                 FAR bool *values,
                                 int count)
{
  uint8_t buf[2];
  int ret = OK;
  int i;
  int index;
  int pin;

  ret = mcp23x08_writeread(priv, &addr, 1, buf, 2);
  if (ret < 0)
    {
      return ret;
    }

#ifdef CONFIG_MCP23X08_SHADOW_MODE
  /* Save the new register value in the shadow register */

  priv->sreg[addr]     = buf[0];
  priv->sreg[addr + 1] = buf[1];
#endif

  /* Read the requested bits */

  for (i = 0; i < count; i++)
    {
      index = 0;
      pin   = pins[i];
      if (pin > 15)
        {
          return -ENXIO;
        }
      else if (pin > 7)
        {
          index = 1;
          pin  -= 8;
        }

      values[i] = (buf[index] >> pin) & 1;
    }

  return OK;
}

/****************************************************************************
 * Name: mcp23x08_multiwritepin
 *
 * Description:
 *   Set the pin level for multiple pins. This routine may be faster than
 *   individual pin accesses. Optional.
 *
 * Input Parameters:
 *   dev - Device-specific state data
 *   pins - The list of pin indexes to alter in this call
 *   val - The list of pin levels.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int mcp23x08_multiwritepin(FAR struct ioexpander_dev_s *dev,
                                  FAR const uint8_t *pins,
                                  FAR const bool *values, int count)
{
  FAR struct mcp23x08_dev_s *priv = (FAR struct mcp23x08_dev_s *)dev;
  uint8_t addr = MCP23X08_GPIOA;
  uint8_t buf[3];
  int ret;
  int i;
  int index;
  int pin;

  /* Get exclusive access to the MCP23X08 */

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

  /* Start by reading both registers, whatever the pins to change. We could
   * attempt to read one port only if all pins were on the same port, but
   * this would not save much.
   */

#ifndef CONFIG_MCP23X08_SHADOW_MODE
  ret = mcp23x08_writeread(priv, &addr, 1, &buf[1], 2);
  if (ret < 0)
    {
      nxmutex_unlock(&priv->lock);
      return ret;
    }
#else
  /* In Shadow-Mode we "read" the pin status from the shadow registers */

  buf[1] = priv->sreg[addr];
  buf[2] = priv->sreg[addr + 1];
#endif

  /* Apply the user defined changes */

  for (i = 0; i < count; i++)
    {
      index = 1;
      pin = pins[i];
      if (pin > 15)
        {
          nxmutex_unlock(&priv->lock);
          return -ENXIO;
        }
      else if (pin > 7)
        {
          index = 2;
          pin  -= 8;
        }

      if (values[i])
        {
          buf[index] |= (1 << pin);
        }
      else
        {
          buf[index] &= ~(1 << pin);
        }
    }

  /* Now write back the new pins states */

  buf[0] = addr;
#ifdef CONFIG_MCP23X08_SHADOW_MODE
  /* Save the new register values in the shadow register */

  priv->sreg[addr]     = buf[1];
  priv->sreg[addr + 1] = buf[2];
#endif
  ret = mcp23x08_write(priv, buf, 3);

  nxmutex_unlock(&priv->lock);
  return ret;
}

/****************************************************************************
 * Name: mcp23x08_multireadpin
 *
 * Description:
 *   Read the actual level for multiple pins. This routine may be faster than
 *   individual pin accesses. Optional.
 *
 * Input Parameters:
 *   dev    - Device-specific state data
 *   pin    - The list of pin indexes to read
 *   valptr - Pointer to a buffer where the pin levels are stored.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int mcp23x08_multireadpin(FAR struct ioexpander_dev_s *dev,
                                 FAR const uint8_t *pins,
                                 FAR bool *values, int count)
{
  FAR struct mcp23x08_dev_s *priv = (FAR struct mcp23x08_dev_s *)dev;
  int ret;

  /* Get exclusive access to the MCP23X08 */

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

  ret = mcp23x08_getmultibits(priv, MCP23X08_GPIO,
                              pins, values, count);
  nxmutex_unlock(&priv->lock);
  return ret;
}

/****************************************************************************
 * Name: mcp23x08_multireadbuf
 *
 * Description:
 *   Read the buffered level of multiple pins. This routine may be faster
 *   than individual pin accesses. Optional.
 *
 * Input Parameters:
 *   dev    - Device-specific state data
 *   pin    - The index of the pin
 *   valptr - Pointer to a buffer where the buffered levels are stored.
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int mcp23x08_multireadbuf(FAR struct ioexpander_dev_s *dev,
                                 FAR const uint8_t *pins,
                                 FAR bool *values, int count)
{
  FAR struct mcp23x08_dev_s *priv = (FAR struct mcp23x08_dev_s *)dev;
  int ret;

  /* Get exclusive access to the MCP23X08 */

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

  ret = mcp23x08_getmultibits(priv, MCP23X08_GPIO,
                              pins, values, count);
  nxmutex_unlock(&priv->lock);
  return ret;
}

#endif

#ifdef CONFIG_MCP23X08_INT_ENABLE

/****************************************************************************
 * Name: mcp23x08_attach
 *
 * Description:
 *   Attach and enable a pin interrupt callback function.
 *
 * Input Parameters:
 *   dev      - Device-specific state data
 *   pinset   - The set of pin events that will generate the callback
 *   callback - The pointer to callback function.  NULL will detach the
 *              callback.
 *   arg      - User-provided callback argument
 *
 * Returned Value:
 *   A non-NULL handle value is returned on success.  This handle may be
 *   used later to detach and disable the pin interrupt.
 *
 ****************************************************************************/

static FAR void *mcp23x08_attach(FAR struct ioexpander_dev_s *dev,
                                 ioe_pinset_t pinset,
                                 ioe_callback_t callback,
                                 FAR void *arg)
{
  FAR struct mcp23x08_dev_s *priv = (FAR struct mcp23x08_dev_s *)dev;
  FAR void *handle = NULL;
  uint8_t addr = MCP23X08_GPINTENA;
  uint8_t buf[2];
  int i;
  int ret;

  /* Get exclusive access to the MCP23X08 */

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

  ret = mcp23x08_writeread(priv, &addr, 1, &buf[1], 1);
  if (ret < 0)
    {
      nxmutex_unlock(&priv->lock);
      return NULL;
    }

  buf[0]  = addr;
  buf[1] |= pinset;

  ret = mcp23x08_write(priv, buf, 2);
  if (ret < 0)
    {
      nxmutex_unlock(&priv->lock);
      return NULL;
    }

  /* Find and available in entry in the callback table */

  for (i = 0; i < CONFIG_MCP23X08_INT_NCALLBACKS; i++)
    {
      /* Is this entry available (i.e., no callback attached) */

      if (priv->cb[i].cbfunc == NULL)
        {
          /* Yes.. use this entry */

          priv->cb[i].pinset = pinset;
          priv->cb[i].cbfunc = callback;
          priv->cb[i].cbarg  = arg;
          handle             = &priv->cb[i];
          break;
        }
    }

  /* Add this callback to the table */

  nxmutex_unlock(&priv->lock);
  return handle;
}

/****************************************************************************
 * Name: mcp23x08_detach
 *
 * Description:
 *   Detach and disable a pin interrupt callback function.
 *
 * Input Parameters:
 *   dev      - Device-specific state data
 *   handle   - The non-NULL opaque value return by mcp23x08_attch()
 *
 * Returned Value:
 *   0 on success, else a negative error code
 *
 ****************************************************************************/

static int mcp23x08_detach(FAR struct ioexpander_dev_s *dev,
                           FAR void *handle)
{
  FAR struct mcp23x08_dev_s *priv = (FAR struct mcp23x08_dev_s *)dev;
  FAR struct mcp23x08_callback_s *cb =
    (FAR struct mcp23x08_callback_s *)handle;

  DEBUGASSERT(priv != NULL && cb != NULL);
  DEBUGASSERT((uintptr_t)cb >= (uintptr_t)&priv->cb[0] &&
              (uintptr_t)cb <=
              (uintptr_t)&priv->cb[CONFIG_MCP23X08_INT_NCALLBACKS - 1]);
  UNUSED(priv);

  cb->pinset = 0;
  cb->cbfunc = NULL;
  cb->cbarg  = NULL;
  return OK;
}

/****************************************************************************
 * Name: mcp23x08_irqworker
 *
 * Description:
 *   Handle GPIO interrupt events (this function actually executes in the
 *   context of the worker thread).
 *
 ****************************************************************************/

static void mcp23x08_irqworker(void *arg)
{
  FAR struct mcp23x08_dev_s *priv = (FAR struct mcp23x08_dev_s *)arg;
  uint8_t addr = MCP23X08_INTFA;
  uint8_t buf[2];
  ioe_pinset_t pinset;
  int ret;
  int i;

  /* Read interrupt flags */

  ret = mcp23x08_writeread(priv, &addr, 1, buf, 1);
  if (ret == OK)
    {
      /* Create a 16-bit pinset */

      pinset = ((unsigned int)buf[1] << 8) | buf[0];

      /* Perform pin interrupt callbacks */

      for (i = 0; i < CONFIG_MCP23X08_INT_NCALLBACKS; i++)
        {
          /* Is this entry valid (i.e., callback attached)?  If so, did
           * any of the requested pin interrupts occur?
           */

          if (priv->cb[i].cbfunc != NULL)
            {
              /* Did any of the requested pin interrupts occur? */

              ioe_pinset_t match = pinset & priv->cb[i].pinset;
              if (match != 0)
                {
                  /* Yes.. perform the callback */

                  priv->cb[i].cbfunc(&priv->dev, match,
                                    priv->cb[i].cbarg);
                }
            }
        }

      /* Read GPIOs to clear interrupt condition */

      addr = MCP23X08_INTCAP;

      mcp23x08_writeread(priv, &addr, 1, buf, 1);

#ifdef CONFIG_MCP23X08_SHADOW_MODE
      /* Don't forget to update the shadow registers at this point */

      priv->sreg[addr]     = buf[0];
#endif
    }

  /* Re-enable interrupts */

  priv->config->enable(priv->config, TRUE);
}

/****************************************************************************
 * Name: mcp23x08_interrupt
 *
 * Description:
 *   Handle GPIO interrupt events (this function executes in the
 *   context of the interrupt).
 *
 ****************************************************************************/

static int mcp23x08_interrupt(int irq, FAR void *context, FAR void *arg)
{
  FAR struct mcp23x08_dev_s *priv = (FAR struct mcp23x08_dev_s *)arg;

  /* In complex environments, we cannot do I2C transfers from the interrupt
   * handler because semaphores are probably used to lock the I2C bus.  In
   * this case, we will defer processing to the worker thread.  This is also
   * much kinder in the use of system resources and is, therefore, probably
   * a good thing to do in any event.
   */

  /* Notice that further GPIO interrupts are disabled until the work is
   * actually performed.  This is to prevent overrun of the worker thread.
   * Interrupts are re-enabled in mcp23x08_irqworker() when the work is
   * completed.
   */

  if (work_available(&priv->work))
    {
      priv->config->enable(priv->config, FALSE);
      work_queue(HPWORK, &priv->work, mcp23x08_irqworker,
                 (FAR void *)priv, 0);
    }

  return OK;
}

#endif

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

/****************************************************************************
 * Name: mcp23x08_initialize
 *
 * Description:
 *   Initialize a MCP23X08 I2C device.
 *
 * TODO: Add support for more than one device.
 *
 ****************************************************************************/

FAR struct ioexpander_dev_s *mcp23x08_initialize(
                              FAR struct i2c_master_s *i2cdev,
                              FAR struct mcp23x08_config_s *config)
{
  FAR struct mcp23x08_dev_s *priv;
#ifdef CONFIG_MCP23X08_INT_MIRROR
  uint8_t buf[3];
#endif

  DEBUGASSERT(i2cdev != NULL && config != NULL);

#ifdef CONFIG_MCP23X08_MULTIPLE
  /* Allocate the device state structure */

  priv = (FAR struct mcp23x08_dev_s *)
    kmm_zalloc(sizeof(struct mcp23x08_dev_s));
  if (!priv)
    {
      return NULL;
    }

  /* And save the device structure in the list of MCP23X08 so that we can
   * find it later.
   */

  priv->flink = g_mcp23x08list;
  g_mcp23x08list = priv;

#else
  /* Use the global MCP23X08 driver instance */

  priv = &g_mcp23x08;
#endif

  /* Initialize the device state structure */

  priv->i2c     = i2cdev;
  priv->dev.ops = &g_mcp23x08_ops;
  priv->config  = config;

#ifdef CONFIG_MCP23X08_INT_ENABLE
  priv->config->attach(priv->config, mcp23x08_interrupt, priv);
  priv->config->enable(priv->config, TRUE);
#endif

  nxmutex_init(&priv->lock);
  return &priv->dev;
}

#endif /* CONFIG_IOEXPANDER_MCP23X08 */