/****************************************************************************
 * drivers/lcd/ft80x.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.
 *
 ****************************************************************************/

/* References:
 *  - Document No.: FT_000792, "FT800 Embedded Video Engine", Datasheet
 *    Version 1.1, Clearance No.: FTDI# 334, Future Technology Devices
 *    International Ltd.
 *  - Document No.: FT_000986, "FT801 Embedded Video Engine Datasheet",
 *    Version 1.0, Clearance No.: FTDI#376, Future Technology Devices
 *    International Ltd.
 *  - Application Note AN_240AN_240, "FT800 From the Ground Up", Version
 *    1.1, Issue Date: 2014-06-09, Future Technology Devices International
 *    Ltd.
 *  - "FT800 Series Programmer Guide Guide", Version 2.1, Issue Date:
 *    2016-09-19, Future Technology Devices International Ltd.
 */

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

#include <nuttx/config.h>

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

#include <nuttx/arch.h>
#include <nuttx/semaphore.h>
#include <nuttx/kmalloc.h>
#include <nuttx/clock.h>
#include <nuttx/wqueue.h>
#include <nuttx/fs/fs.h>
#include <nuttx/i2c/i2c_master.h>
#include <nuttx/spi/spi.h>
#include <nuttx/lcd/lcd.h>
#include <nuttx/lcd/ft80x.h>

#include <arch/irq.h>

#include "ft80x.h"

#ifdef CONFIG_LCD_FT80X

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

#define CHIPID         0x7c
#define ROMID_MASK     0x0000ffff
#define VERSION_MASK   0xffff0000
#if defined(CONFIG_LCD_FT800)
#  define DEVNAME      "/dev/ft800"
#  define ROM_CHIPID   0x00010008  /* Byte [0:1] Chip ID "0800" BCD
                                    * Byte [2:3] Version "0100" BCD */
#elif defined(CONFIG_LCD_FT801)
#  define DEVNAME      "/dev/ft801"
#  define ROM_CHIPID   0x00010108  /* Byte [0:1] Chip ID "0801" BCD
                                    * Byte [2:3] Version "0100" BCD */
#else
#  error No FT80x device configured
#endif

#define ROMID          (ROM_CHIPID & ROMID_MASK)
#define VERSION        (ROM_CHIPID & VERSION_MASK)

#define MIN_FADE_DELAY 10          /* Milliseconds */
#define MAX_FADE_DELAY 16700       /* Milliseconds */
#define FADE_STEP_MSEC 10          /* Milliseconds */

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

static int  ft80x_fade(FAR struct ft80x_dev_s *priv,
              FAR const struct ft80x_fade_s *fade);

static void ft80x_notify(FAR struct ft80x_dev_s *priv,
              enum ft80x_notify_e id, int value);
static void ft80x_interrupt_work(FAR void *arg);
static int  ft80x_interrupt(int irq, FAR void *context, FAR void *arg);
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
static void ft80x_destroy(FAR struct ft80x_dev_s *priv);
#endif

/* Character driver methods */

static int  ft80x_open(FAR struct file *filep);
static int  ft80x_close(FAR struct file *filep);

static ssize_t ft80x_read(FAR struct file *filep, FAR char *buffer,
              size_t buflen);
static ssize_t ft80x_write(FAR struct file *filep, FAR const char *buffer,
              size_t buflen);
static int  ft80x_ioctl(FAR struct file *filep, int cmd, unsigned long arg);
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
static int  ft80x_unlink(FAR struct inode *inode);
#endif

/* Initialization */

static int  ft80x_initialize(FAR struct ft80x_dev_s *priv);

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

static const struct file_operations g_ft80x_fops =
{
  ft80x_open,    /* open */
  ft80x_close,   /* close */
  ft80x_read,    /* read */
  ft80x_write,   /* write */
  NULL,          /* seek */
  ft80x_ioctl,   /* ioctl */
  NULL           /* poll */
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
  , ft80x_unlink /* unlink */
#endif
};

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

/****************************************************************************
 * Name: ft80x_fade
 *
 * Description:
 *   Change the backlight intensity with a controllable fade.
 *
 ****************************************************************************/

static int ft80x_fade(FAR struct ft80x_dev_s *priv,
                      FAR const struct ft80x_fade_s *fade)
{
  clock_t start;
  clock_t elapsed;
  int32_t delay;
  int32_t duty;
  int16_t endduty;
  int16_t delta;

  /* 0% corresponds to the value 0, but 100% corresponds to the value 128 */

  endduty = (uint16_t)((uint16_t)fade->duty << 7) / 100;

  /* Get the change in duty from the current to the terminal duty. */

  duty  = (int32_t)(ft80x_read_byte(priv, FT80X_REG_PWM_DUTY) & 0x7f);
  delta = endduty - (int16_t)duty;

  /* The "smoothness" of the steps will depend on the resolution of the
   * system timer.  The minimum delay is <= 2 * system_clock_period.
   *
   * We will try for a FADE_STEP_MSEC delay, but we will try to adapt to
   * whatever we get is we are working close the system time resolution.
   * For human factors reasons, any delay less than 100 MS or so should
   * appear more or less smooth.
   *
   * The delay calculation should never overflow:
   *
   *   Max delay:        16,700 msec (MAX_FADE_DELAY)
   *   Min clock period: 1 usec
   *   Max delay:        16,700,000 ticks
   *   INT32_MAX         2,147,483,647
   */

  delay = MSEC2TICK((int32_t)fade->delay);
  if (delay <= 0)
    {
      delay = 1;
    }

  start = clock_systime_ticks();

  do
    {
      /* Wait for FADE_STEP_MSEC msec (or whatever we get) */

      nxsig_usleep(FADE_STEP_MSEC * 1000);

      /* Get the elapsed time */

      elapsed = clock_systime_ticks() - start;
      if (elapsed > INT32_MAX || (int32_t)elapsed >= delay)
        {
          duty = endduty;
        }
      else
        {
          /* Interpolate to get the next PWM duty in the fade.  This
           * calculation should never overflow:
           *
           *   Max delta:       128
           *   Max elapsed:     16,700,000 ticks
           *   Max numerator:   2,137,600,000
           *   Min denominator: 1
           *   Max duty:        2,137,600,000
           *   INT32_MAX        2,147,483,647
           */

          duty += ((int32_t)delta * (int32_t)elapsed) / delay;
          if (duty > 128)
            {
              duty = 128;
            }
          else if (duty < 0)
            {
              duty = 0;
            }
        }

      /* The set the new backlight PWM duty */

      ft80x_write_byte(priv, FT80X_REG_PWM_DUTY, (uint8_t)duty);
    }
  while (duty != endduty);

  return OK;
}

/****************************************************************************
 * Name: ft80x_notify
 *
 * Description:
 *   Notify any registered client of the FT80x event
 *
 ****************************************************************************/

static void ft80x_notify(FAR struct ft80x_dev_s *priv,
                         enum ft80x_notify_e id, int value)
{
  FAR struct ft80x_eventinfo_s *info = &priv->notify[id];

  /* Are notifications enabled for this event? */

  if (info->enable)
    {
      DEBUGASSERT(info->pid > 0);

      /* Yes.. Signal the client */

      info->event.sigev_value.sival_int = value;
      nxsig_notification(info->pid, &info->event, SI_QUEUE, &info->work);
    }
}

/****************************************************************************
 * Name: ft80x_interrupt_work
 *
 * Description:
 *   Back end handling for FT80x interrupts
 *
 ****************************************************************************/

static void ft80x_interrupt_work(FAR void *arg)
{
  FAR struct ft80x_dev_s *priv = (FAR struct ft80x_dev_s *)arg;
  uint32_t intflags;
  uint32_t regval;

  DEBUGASSERT(priv != NULL);

  /* Get exclusive access to the device structures */

  nxmutex_lock(&priv->lock);

  /* Get the set of pending interrupts.  Note that simply reading this
   * register is sufficient to clear all pending interrupts.
   */

  intflags = ft80x_read_word(priv, FT80X_REG_INT_FLAGS);

  /* And process each pending interrupt.
   *
   * REVISIT:  No interrupt sources are ever enabled in the current
   * implementation.
   */

  if ((intflags & FT80X_INT_SWAP) != 0)
    {
      /* Display swap occurred */

      lcdinfo("Display swap occurred\n");
      ft80x_notify(priv, FT80X_NOTIFY_SWAP, 0);
    }

  if ((intflags & FT80X_INT_TOUCH) != 0)
    {
      /* Touch-screen touch detected */

      lcdinfo("Touch-screen touch detected\n");
      ft80x_notify(priv, FT80X_NOTIFY_TOUCH, 0);
    }

  if ((intflags & FT80X_INT_TAG) != 0)
    {
      /* Touch-screen tag value change */

      lcdinfo("Touch-screen tag value change\n");
#ifdef CONFIG_LCD_FT800
      regval = ft80x_read_word(priv, FT80X_REG_TOUCH_TAG);
#else
      regval = ft80x_read_word(priv, FT80X_REG_CTOUCH_TAG);
#endif
      ft80x_notify(priv, FT80X_NOTIFY_TAG, (int)(regval & TOUCH_TAG_MASK));
    }

  if ((intflags & FT80X_INT_SOUND) != 0)
    {
      /*  Sound effect ended */

      lcdinfo(" Sound effect ended\n");
      ft80x_notify(priv, FT80X_NOTIFY_SOUND, 0);
    }

  if ((intflags & FT80X_INT_PLAYBACK) != 0)
    {
      /* Audio playback ended */

      lcdinfo("Audio playback ended\n");
      ft80x_notify(priv, FT80X_NOTIFY_PLAYBACK, 0);
    }

  if ((intflags & FT80X_INT_CMDEMPTY) != 0)
    {
      /* Command FIFO empty */

      lcdinfo("Command FIFO empty\n");
      ft80x_notify(priv, FT80X_NOTIFY_CMDEMPTY, 0);
    }

  if ((intflags & FT80X_INT_CMDFLAG) != 0)
    {
      /* Command FIFO flag */

      lcdinfo("Command FIFO flag\n");
      ft80x_notify(priv, FT80X_NOTIFY_CMDFLAG, 0);
    }

  if ((intflags & FT80X_INT_CONVCOMPLETE) != 0)
    {
      /* Touch-screen conversions completed */

      lcdinfo(" Touch-screen conversions completed\n");
      ft80x_notify(priv, FT80X_NOTIFY_CONVCOMPLETE, 0);
    }

  /* Re-enable interrupts */

  DEBUGASSERT(priv->lower != NULL && priv->lower->enable != NULL);
  priv->lower->enable(priv->lower, true);
  nxmutex_unlock(&priv->lock);
}

/****************************************************************************
 * Name: ft80x_interrupt
 *
 * Description:
 *   FT80x interrupt handler
 *
 ****************************************************************************/

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

  DEBUGASSERT(priv != NULL);

  /* Perform the interrupt work on the high priority work queue. */

  work_queue(HPWORK, &priv->intwork, ft80x_interrupt_work, priv, 0);

  /* Disable further interrupts for the GPIO interrupt source.
   * REVISIT:  This assumes that GPIO interrupts will pend until re-enabled.
   * In certain implementations, that assumption is not true and could cause
   * a loss of interrupts.
   */

  DEBUGASSERT(priv->lower != NULL && priv->lower->enable != NULL);
  priv->lower->enable(priv->lower, false);
  return OK;
}

/****************************************************************************
 * Name: ft80x_destroy
 *
 * Description:
 *   The driver has been unlinked... clean up as best we can.
 *
 ****************************************************************************/

#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
static void ft80x_destroy(FAR struct ft80x_dev_s *priv)
{
  /* If the lower half driver provided a destroy method, then call that
   * method now in order order to clean up resources used by the lower-half
   * driver.
   */

  DEBUGASSERT(priv != NULL && priv->lower != NULL);
  if (priv->lower->destroy != NULL)
    {
      priv->lower->destroy(priv->lower);
    }

  /* Then free our container */

  nxmutex_destroy(&priv->lock);
  kmm_free(priv);
}
#endif

/****************************************************************************
 * Name: ft80x_open
 *
 * Description:
 *   This function is called whenever the PWM device is opened.
 *
 ****************************************************************************/

static int ft80x_open(FAR struct file *filep)
{
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
  FAR struct inode *inode;
  FAR struct ft80x_dev_s *priv;
  uint8_t tmp;
  int ret;

  DEBUGASSERT(filep != NULL);
  inode = filep->f_inode;
  DEBUGASSERT(inode != NULL && inode->i_private != NULL);
  priv  = inode->i_private;

  lcdinfo("crefs: %d\n", priv->crefs);

  /* Get exclusive access to the device structures */

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

  /* Increment the count of references to the device */

  tmp = priv->crefs + 1;
  if (tmp == 0)
    {
      /* More than 255 opens; uint8_t overflows to zero */

      ret = -EMFILE;
      goto errout_with_lock;
    }

  /* Save the new open count */

  priv->crefs = tmp;
  ret = OK;

errout_with_lock:
  nxmutex_unlock(&priv->lock);

errout:
  return ret;
#else
  return OK;
#endif
}

/****************************************************************************
 * Name: ft80x_close
 *
 * Description:
 *   This function is called when the PWM device is closed.
 *
 ****************************************************************************/

static int ft80x_close(FAR struct file *filep)
{
#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
  FAR struct inode *inode;
  FAR struct ft80x_dev_s *priv;
  int ret;

  DEBUGASSERT(filep != NULL);
  inode = filep->f_inode;
  DEBUGASSERT(inode != NULL && inode->i_private != NULL);
  priv  = inode->i_private;

  lcdinfo("crefs: %d\n", priv->crefs);

  /* Get exclusive access to the device structures */

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

  /* Will the count decrement to zero? */

  if (priv->crefs <= 1)
    {
      /* Yes.. if the driver has been unlinked, then we need to destroy the
       * driver instance.
       */

      priv->crefs = 0;
      if (priv->unlinked)
        {
          nxmutex_unlock(&priv->lock);
          ft80x_destroy(priv);
          return OK;
        }
    }
  else
    {
      /* NO.. decrement the number of references to the driver. */

      priv->crefs--;
    }

  ret = OK;
  nxmutex_unlock(&priv->lock);

errout:
  return ret;
#else
  return OK;
#endif
}

/****************************************************************************
 * Name: ft80x_read
 ****************************************************************************/

static ssize_t ft80x_read(FAR struct file *filep, FAR char *buffer,
                          size_t len)
{
  /* Reading from the FT80X is an undefined operation and not support */

  lcdinfo("buffer: %p len %lu\n", buffer, (unsigned long)len);
  return 0;  /* Return EOF */
}

/****************************************************************************
 * Name: ft80x_write
 ****************************************************************************/

static ssize_t ft80x_write(FAR struct file *filep, FAR const char *buffer,
                           size_t len)
{
  FAR struct inode *inode;
  FAR struct ft80x_dev_s *priv;
  int ret;

  lcdinfo("buffer: %p len %lu\n", buffer, (unsigned long)len);
  DEBUGASSERT(buffer != NULL && ((uintptr_t)buffer & 3) == 0 &&
              len > 0 && (len & 3) == 0 && len <= FT80X_RAM_DL_SIZE);

  DEBUGASSERT(filep != NULL);
  inode = filep->f_inode;
  DEBUGASSERT(inode != NULL && inode->i_private != NULL);
  priv  = inode->i_private;

  if (buffer == NULL || ((uintptr_t)buffer & 3) != 0 ||
      len == 0 || (len & 3) != 0 || (len + filep->f_pos) > FT80X_RAM_DL_SIZE)
    {
      return -EINVAL;
    }

  /* Get exclusive access to the device structures */

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

  /* Note that there is no check if the driver was opened read-only.  That
   * would be a silly thing to do.
   */

  /* The write method is functionally equivalent to the FT80X_IOC_CREATEDL
   * IOCTL command:  It simply copies the display list in the user buffer to
   * the FT80x display list memory.
   */

  ft80x_write_memory(priv, FT80X_RAM_DL + filep->f_pos, buffer, len);
  filep->f_pos += len;

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

/****************************************************************************
 * Name: ft80x_ioctl
 *
 * Description:
 *   The standard ioctl method.  This is where ALL of the PWM work is done.
 *
 ****************************************************************************/

static int ft80x_ioctl(FAR struct file *filep, int cmd, unsigned long arg)
{
  FAR struct inode *inode;
  FAR struct ft80x_dev_s *priv;
  int ret;

  DEBUGASSERT(filep != NULL);
  inode = filep->f_inode;
  DEBUGASSERT(inode != NULL && inode->i_private != NULL);
  priv  = inode->i_private;

  lcdinfo("cmd: %d arg: %lu\n", cmd, arg);

  /* Get exclusive access to the device structures */

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

  /* Handle built-in ioctl commands */

  switch (cmd)
    {
      /* FT80X_IOC_CREATEDL:
       *   Description:  Write a display list to the FT80x display list
       *                 memory
       *   Description:  Write a display list to the FT80x display list
       *                 memory starting at offset zero.  This may or may
       *                 not be the entire display list.  Display lists may
       *                 be created incrementally, starting with
       *                 FT80X_IOC_CREATEDL and finishing the display list
       *                 using FT80XIO_APPENDDL
       *   Argument:     A reference to a display list structure instance.
       *                 See struct ft80x_displaylist_s.
       *   Returns:      None
       */

      case FT80X_IOC_CREATEDL:

        /* Set the file position to zero and fall through to "append" the new
         * display list data at offset 0.
         */

        filep->f_pos = 0;

        /* FALLTHROUGH */

      /* FT80X_IOC_APPENDDL:
       *   Description:  Write additional display list entries to the FT80x
       *                 display list memory at the current display list
       *                 offset.  This IOCTL command permits display lists
       *                 to be completed incrementally, starting with
       *                 FT80X_IOC_CREATEDL and finishing the display list
       *                 using FT80XIO_APPENDDL.
       *   Argument:     A reference to a display list structure instance.
       *                 See struct ft80x_displaylist_s.
       *   Returns:      None
       */

      case FT80X_IOC_APPENDDL:
        {
          FAR struct ft80x_displaylist_s *dl =
            (FAR struct ft80x_displaylist_s *)((uintptr_t)arg);

          if (dl == NULL || ((uintptr_t)&dl->cmd & 3) != 0 ||
              (dl->dlsize & 3) != 0 ||
              dl->dlsize + filep->f_pos > FT80X_RAM_DL_SIZE)
            {
              ret = -EINVAL;
            }
          else
            {
              /* Check if there is a display list.  It might be useful for
               * the application to issue FT80X_IOC_CREATEDL with no data in
               * order to initialize the display list, then form all of the
               * list entries with FT80X_IOC_APPENDDL.
               */

              if (dl->dlsize > 0)
                {
                  /* This IOCTL command simply copies the display list
                   * provided into the FT80x display list memory.
                   */

                  ft80x_write_memory(priv, FT80X_RAM_DL + filep->f_pos,
                                     &dl->cmd, dl->dlsize);
                  filep->f_pos += dl->dlsize;
                }

              ret = OK;
            }
        }
        break;

      /* FT80X_IOC_GETRAMDL:
       *   Description:  Read a 32-bit value from the display list.
       *   Argument:     A reference to an instance of struct ft80x_relmem_s.
       *   Returns:      The 32-bit value read from the display list.
       */

      case FT80X_IOC_GETRAMDL:
        {
          FAR struct ft80x_relmem_s *ramdl =
            (FAR struct ft80x_relmem_s *)((uintptr_t)arg);

          if (ramdl == NULL || ((uintptr_t)ramdl->offset & 3) != 0 ||
              ramdl->offset >= FT80X_RAM_DL_SIZE)
            {
              ret = -EINVAL;
            }
          else
            {
              ft80x_read_memory(priv, FT80X_RAM_DL + ramdl->offset,
                                ramdl->value, ramdl->nbytes);
              ret = OK;
            }
        }
        break;

      /* FT80X_IOC_PUTRAMG
       *   Description:  Write byte data to FT80x graphics memory (RAM_G)
       *   Argument:     A reference to an instance of struct ft80x_relmem_s.
       *   Returns:      None.
       */

      case FT80X_IOC_PUTRAMG:
        {
          FAR struct ft80x_relmem_s *ramg =
            (FAR struct ft80x_relmem_s *)((uintptr_t)arg);

          if (ramg == NULL ||
             (ramg->offset + ramg->nbytes) >= FT80X_RAM_G_SIZE)
            {
              ret = -EINVAL;
            }
          else
            {
              ft80x_write_memory(priv, FT80X_RAM_G + ramg->offset,
                                 ramg->value, ramg->nbytes);
              ret = OK;
            }
        }
        break;

      /* FT80X_IOC_PUTRAMCMD
       *   Description:  Write 32-bit aligned data to FT80x FIFO (RAM_CMD)
       *   Argument:     A reference to an instance of struct ft80x_relmem_s.
       *   Returns:      None.
       */

      case FT80X_IOC_PUTRAMCMD:
        {
          FAR struct ft80x_relmem_s *ramcmd =
            (FAR struct ft80x_relmem_s *)((uintptr_t)arg);

          if (ramcmd == NULL || ((uintptr_t)ramcmd->offset & 3) != 0)
            {
              ret = -EINVAL;
            }
          else
            {
              ft80x_write_memory(priv, FT80X_RAM_CMD + ramcmd->offset,
                                ramcmd->value, ramcmd->nbytes);
              ret = OK;
            }
        }
        break;

      /* FT80X_IOC_GETREG8:
       *   Description:  Read an 8-bit register value from the FT80x.
       *   Argument:     A reference to an instance of struct
       *                 ft80x_register_s.
       *   Returns:      The 8-bit value read from the register.
       */

      case FT80X_IOC_GETREG8:
        {
          FAR struct ft80x_register_s *reg =
            (FAR struct ft80x_register_s *)((uintptr_t)arg);

          if (reg == NULL || ((uintptr_t)reg->addr & 3) != 0)
            {
              ret = -EINVAL;
            }
          else
            {
              reg->value.u8 = ft80x_read_byte(priv, reg->addr);
              ret = OK;
            }
        }
        break;

      /* FT80X_IOC_GETREG16:
       *   Description:  Read a 16-bit register value from the FT80x.
       *   Argument:     A reference to an instance of struct
       *                 ft80x_register_s.
       *   Returns:      The 16-bit value read from the register.
       */

      case FT80X_IOC_GETREG16:
        {
          FAR struct ft80x_register_s *reg =
            (FAR struct ft80x_register_s *)((uintptr_t)arg);

          if (reg == NULL || ((uintptr_t)reg->addr & 3) != 0)
            {
              ret = -EINVAL;
            }
          else
            {
              reg->value.u16 = ft80x_read_hword(priv, reg->addr);
              ret = OK;
            }
        }
        break;

      /* FT80X_IOC_GETREG32:
       *   Description:  Read a 32-bit register value from the FT80x.
       *   Argument:     A reference to an instance of struct
       *                 ft80x_register_s.
       *   Returns:      The 32-bit value read from the register.
       */

      case FT80X_IOC_GETREG32:
        {
          FAR struct ft80x_register_s *reg =
            (FAR struct ft80x_register_s *)((uintptr_t)arg);

          if (reg == NULL || ((uintptr_t)reg->addr & 3) != 0)
            {
              ret = -EINVAL;
            }
          else
            {
              reg->value.u32 = ft80x_read_word(priv, reg->addr);
              ret = OK;
            }
        }
        break;

      /* FT80X_IOC_GETREGS:
       *   Description:  Read multiple 32-bit register values from the FT80x.
       *   Argument:     A reference to an instance of struct
       *                 ft80x_registers_s.
       *   Returns:      The 32-bit values read from the consecutive
       *                 registers .
       */

      case FT80X_IOC_GETREGS:
        {
          FAR struct ft80x_registers_s *regs =
            (FAR struct ft80x_registers_s *)((uintptr_t)arg);

          if (regs == NULL || ((uintptr_t)regs->addr & 3) != 0)
            {
              ret = -EINVAL;
            }
          else
            {
              ft80x_read_memory(priv, regs->addr, regs->value,
                                (size_t)regs->nregs << 2);
              ret = OK;
            }
        }
        break;

      /* FT80X_IOC_PUTREG8:
       *   Description:  Write an 8-bit register value to the FT80x.
       *   Argument:     A reference to an instance of struct
       *                 ft80x_register_s.
       *   Returns:      None.
       */

      case FT80X_IOC_PUTREG8:
        {
          FAR struct ft80x_register_s *reg =
            (FAR struct ft80x_register_s *)((uintptr_t)arg);

          if (reg == NULL || ((uintptr_t)reg->addr & 3) != 0)
            {
              ret = -EINVAL;
            }
          else
            {
              ft80x_write_byte(priv, reg->addr, reg->value.u8);
              ret = OK;
            }
        }
        break;

      /* FT80X_IOC_PUTREG16:
       *   Description:  Write a 16-bit  register value to the FT80x.
       *   Argument:     A reference to an instance of struct
       *                 ft80x_register_s.
       *   Returns:      None.
       */

      case FT80X_IOC_PUTREG16:
        {
          FAR struct ft80x_register_s *reg =
            (FAR struct ft80x_register_s *)((uintptr_t)arg);

          if (reg == NULL || ((uintptr_t)reg->addr & 3) != 0)
            {
              ret = -EINVAL;
            }
          else
            {
              ft80x_write_hword(priv, reg->addr, reg->value.u16);
              ret = OK;
            }
        }
        break;

      /* FT80X_IOC_PUTREG32:
       *   Description:  Write a 32-bit  register value to the FT80x.
       *   Argument:     A reference to an instance of struct
       *                 ft80x_register_s.
       *   Returns:      None.
       */

      case FT80X_IOC_PUTREG32:
        {
          FAR struct ft80x_register_s *reg =
            (FAR struct ft80x_register_s *)((uintptr_t)arg);

          if (reg == NULL || ((uintptr_t)reg->addr & 3) != 0)
            {
              ret = -EINVAL;
            }
          else
            {
              ft80x_write_word(priv, reg->addr, reg->value.u32);
              ret = OK;
            }
        }
        break;

      /* FT80X_IOC_PUTREGS:
       *   Description:  Write multiple 32-bit register values to the FT80x.
       *   Argument:     A reference to an instance of struct
       *                 ft80x_registers_s.
       *   Returns:      None.
       */

      case FT80X_IOC_PUTREGS:
        {
          FAR struct ft80x_registers_s *regs =
            (FAR struct ft80x_registers_s *)((uintptr_t)arg);

          if (regs == NULL || ((uintptr_t)regs->addr & 3) != 0)
            {
              ret = -EINVAL;
            }
          else
            {
              ft80x_write_memory(priv, regs->addr, regs->value,
                                 (size_t)regs->nregs << 2);
              ret = OK;
            }
        }
        break;

      /* FT80X_IOC_EVENTNOTIFY:
       *   Description:  Setup to receive a signal when an event occurs.
       *   Argument:     A reference to an instance of struct ft80x_notify_s.
       *   Returns:      None
       */

      case FT80X_IOC_EVENTNOTIFY:
        {
          FAR struct ft80x_notify_s *notify =
            (FAR struct ft80x_notify_s *)((uintptr_t)arg);

          if (notify == NULL || notify->pid < 0 ||
              (unsigned int)notify->id >= FT80X_INT_NEVENTS)
            {
              ret = -EINVAL;
            }
          else
            {
              FAR struct ft80x_eventinfo_s *info = &priv->notify[notify->id];
              uint32_t regval;

              /* Are we enabling or disabling */

              if (notify->enable)
                {
                  /* Make sure that arguments are valid for the enable */

                  if (notify->pid == 0)
                    {
                      ret = -EINVAL;
                    }
                  else
                    {
                      /* Setup the new notification information */

                      info->event  = notify->event;
                      info->pid    = notify->pid;
                      info->enable = true;

                      /* Enable interrupts associated with the event */

                      regval  = ft80x_read_word(priv, FT80X_REG_INT_MASK);
                      regval |= (1 << notify->id);
                      ft80x_write_word(priv, FT80X_REG_INT_MASK, regval);
                      ret = OK;
                    }
                }
              else
                {
                  /* Disable the notification */

                  info->pid    = 0;
                  info->enable = false;

                  /* Disable interrupts associated with the event */

                  regval  = ft80x_read_word(priv, FT80X_REG_INT_MASK);
                  regval &= ~(1 << notify->id);
                  ft80x_write_word(priv, FT80X_REG_INT_MASK, regval);

                  /* Cancel any pending notification */

                  nxsig_cancel_notification(&info->work);
                  ret = OK;
                }
            }
        }
        break;

       /* FT80X_IOC_FADE:
        *   Description:  Change the backlight intensity with a controllable
        *                 fade.
        *   Argument:     A reference to an instance of struct ft80x_fade_s.
        *   Returns:      None.
        */

       case FT80X_IOC_FADE:
        {
          FAR const struct ft80x_fade_s *fade =
            (FAR const struct ft80x_fade_s *)((uintptr_t)arg);

          if (fade == NULL || fade->duty > 100 ||
              fade->delay < MIN_FADE_DELAY || fade->delay > MAX_FADE_DELAY)
            {
              ret = -EINVAL;
            }
          else
            {
              ret = ft80x_fade(priv, fade);
            }
        }
        break;

       /* FT80X_IOC_AUDIO:
        *   Description:  Enable/disable an external audio amplifier.
        *   Argument:     0=disable; 1=enable.
        *   Returns:      None.
        */

       case FT80X_IOC_AUDIO:
        {
#if defined(CONFIG_LCD_FT80X_AUDIO_MCUSHUTDOWN)
          /* Amplifier is controlled by an MCU GPIO pin */

          DEBUGASSERT(priv->lower->attach != NULL &&
                      priv->lower->audio != NULL);
          DEBUGASSERT(arg == 0 || arg == 1);

          priv->lower->audio(priv->lower, (arg != 0));
          ret = OK;

#elif defined(CONFIG_LCD_FT80X_AUDIO_GPIOSHUTDOWN)
          /* Amplifier is controlled by an FT80x GPIO pin */

          uint8_t regval8;

          DEBUGASSERT(arg == 0 || arg == 1);

          regval8  = ft80x_read_byte(priv, FT80X_REG_GPIO);

          /* Active low logic assumed */

          if (arg == 0)
            {
              regval8 |= (1 << CONFIG_LCD_FT80X_AUDIO_GPIO);
            }
          else
            {
              regval8 &= ~(1 << CONFIG_LCD_FT80X_AUDIO_GPIO);
            }

          ft80x_write_byte(priv, FT80X_REG_GPIO, regval8);
          ret = OK;

#else
          /* Amplifier is not controllable. */

          DEBUGASSERT(arg == 0 || arg == 1);
          return OK;
#endif
        }
        break;

      /* Unrecognized IOCTL command */

      default:
        lcderr("ERROR: Unrecognized cmd: %d arg: %ld\n", cmd, arg);
        ret = -ENOTTY;
        break;
    }

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

/****************************************************************************
 * Name: ft80x_unlink
 ****************************************************************************/

#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS
static int ft80x_unlink(FAR struct inode *inode)
{
  FAR struct ft80x_dev_s *priv;

  /* Get the reference to our internal state structure from the inode
   * structure.
   */

  DEBUGASSERT(inode && inode->i_private);
  priv = inode->i_private;

  /* Indicate that the driver has been unlinked */

  priv->unlinked = true;

  /* If there are no further open references to the driver, then commit
   * Hara-Kiri now.
   */

  if (priv->crefs == 0)
    {
      ft80x_destroy(priv);
    }

  return OK;
}
#endif

/****************************************************************************
 * Name: ft80x_initialize
 *
 * Description:
 *  Initialize the FT80x
 *
 ****************************************************************************/

static int ft80x_initialize(FAR struct ft80x_dev_s *priv)
{
  uint32_t timeout;
  uint32_t regval32;
  uint8_t regval8;

  /* To configure the display, load the timing control registers with values
   * for the particular display. These registers control horizontal timing:
   *
   *   - FT80X_REG_PCLK
   *   - FT80X_REG_PCLK_POL
   *   - FT80X_REG_HCYCLE
   *   - FT80X_REG_HOFFSET
   *   - FT80X_REG_HSIZE
   *   - FT80X_REG_HSYNC0
   *   - FT80X_REG_HSYNC1
   *
   * These registers control vertical timing:
   *
   *   - FT80X_REG_VCYCLE
   *   - FT80X_REG_VOFFSET
   *   - FT80X_REG_VSIZE
   *   - FT80X_REG_VSYNC0
   *   - FT80X_REG_VSYNC1
   *
   * And the FT80X_REG_CSPREAD register changes color clock timing to reduce
   * system noise.
   *
   * GPIO bit 7 is used for the display enable pin of the LCD module. By
   * setting the direction of the GPIO bit to out direction, the display can
   * be enabled by writing value of 1 into GPIO bit 7 or the display can be
   * disabled by writing a value of 0 into GPIO bit 7. By default GPIO bit 7
   * direction is output and the value is 0.
   */

  /* Initialization Sequence from Power Down using PD_N pin:
   *
   * 1. Drive the PD_N pin high
   * 2. Wait for at least 20ms
   * 3. Execute "Initialization Sequence during the Boot up" from steps 1
   *    to 9
   *
   * Initialization Sequence from Sleep Mode:
   *
   * 1. Send Host command "ACTIVE" to enable clock to FT800
   * 2. Wait for at least 20ms
   * 3. Execute "Initialization Sequence during Boot Up" from steps 5 to 8
   *
   * Initialization sequence from standby mode:
   *
   * Execute all the steps mentioned in "Initialization Sequence from Sleep
   * Mode" except waiting for at least 20ms in step 2.
   */

  DEBUGASSERT(priv->lower != NULL && priv->lower->pwrdown != NULL);
  priv->lower->pwrdown(priv->lower, false);
  up_mdelay(20);

  /* Initialization Sequence during the boot up:
   *
   * 1. Use MCU SPI clock not more than 11MHz
   * 2. Send Host command CLKEXT to FT800 to enable PLL input from oscillator
   *    or external clock.  Should default to 48MHz PLL output.
   * 3. Send Host command ACTIVE to enable clock and wake up the FT80x.
   * 4. Configure video timing registers, except FT80X_REG_PCLK
   * 5. Write first display list
   * 6. Write FT80X_REG_DLSWAP, FT800 swaps display list immediately
   * 7. Enable back light control for display
   * 8. Write FT80X_REG_PCLK, video output begins with the first display list
   * 9. Use MCU SPI clock not more than 30MHz
   */

  /* 1. Select the initial SPI frequency */

  DEBUGASSERT(priv->lower->init_frequency <= 11000000);
  priv->frequency = priv->lower->init_frequency;

  /* 2. Send Host command CLKEXT to FT800 to enable PLL input from oscillator
   *    or external clock.
   */

  ft80x_host_command(priv, FT80X_CMD_CLKEXT);
  up_mdelay(10);

#if 0 /* Un-necessary? */
  /* Switch PLL output to 48MHz (should be the default) */

  ft80x_host_command(priv, FT80X_CMD_CLK48M);
  up_mdelay(10);
#endif

  /* 3. Send Host command ACTIVE to enable clock and wake up the FT80x. */

  ft80x_host_command(priv, FT80X_CMD_ACTIVE);
  up_mdelay(10);

#if 0 /* Un-necessary? */
  /* Do a core reset for safer */

  ft80x_host_command(priv, FT80X_CMD_CORERST);
#endif

  /* Verify the chip ID.  Read repeatedly until FT80x is ready. */

  timeout = 0;
  for (; ; )
    {
      /* Read the Chip ID */

      regval8 = ft80x_read_byte(priv, FT80X_REG_ID);
      if (regval8 == CHIPID)
        {
          /* Chip ID verify so FT80x is ready */

          break;
        }

      /* Initial Chip ID read may fail because the chip is not yet ready. */

      if (++timeout > 100000)
        {
          lcderr("ERROR: Bad chip ID: %02x\n", regval8);
          return -ENODEV;
        }
    }

  regval32 = ft80x_read_word(priv, FT80X_ROM_CHIPID);
  if ((regval32 & ROMID_MASK) != ROMID)
    {
      lcderr("ERROR: Bad ROM chip ID: %08lx\n", (unsigned long)regval32);
      return -ENODEV;
    }

  /* 4. Configure video timing registers, except FT80X_REG_PCLK
   *
   * Once the FT800 is awake and the internal clock set and Device ID
   * checked, the next task is to configure the LCD display parameters for
   * the chosen display with the values determined in Section 2.3.3 above.
   *
   * a. Set FT80X_REG_PCLK to zero - This disables the pixel clock output
   *    while the LCD and other system parameters are configured
   * b. Set the following registers with values for the chosen display.
   *    Typical WQVGA and QVGA values are shown:
   *
   *    Register            Description                    WQVGA     QVGA
   *                                                      480x272  320x240
   *    FT80X_REG_PCLK_POL  Pixel Clock Polarity             1        0
   *    FT80X_REG_HSIZE     Image width in pixels            480      320
   *    FT80X_REG_HCYCLE    Total number of clocks per line  548      408
   *    FT80X_REG_HOFFSET   Horizontal image start           43       70
   *                        (pixels from left)
   *    FT80X_REG_HSYNC0    Start of HSYNC pulse             0        0
   *                        (falling edge)
   *    FT80X_REG_HSYNC1    End of HSYNC pulse               41       10
   *                        (rising edge)
   *    FT80X_REG_VSIZE     Image height in pixels           272      240
   *    FT80X_REG_VCYCLE    Total number of lines per screen 292      263
   *    FT80X_REG_VOFFSET   Vertical image start             12       13
   *                        (lines from top)
   *    FT80X_REG_VSYNC0    Start of VSYNC pulse             0        0
   *                        (falling edge)
   *    FT80X_REG_VSYNC1    End of VSYNC pulse               10       2
   *                        (rising edge)
   *
   * c. Enable or disable FT80X_REG_CSPREAD with a value of 01h or 00h,
   *    respectively.  Enabling FT80X_REG_CSPREAD will offset the R, G and B
   *    output bits so all they do not all change at the same time.
   */

  ft80x_write_byte(priv, FT80X_REG_PCLK, 0);

#if defined(CONFIG_LCD_FT80X_WQVGA)
  ft80x_write_hword(priv, FT80X_REG_HCYCLE, 548);
  ft80x_write_hword(priv, FT80X_REG_HOFFSET, 43);
  ft80x_write_hword(priv, FT80X_REG_HSYNC0, 0);
  ft80x_write_hword(priv, FT80X_REG_HSYNC1, 41);
  ft80x_write_hword(priv, FT80X_REG_VCYCLE, 292);
  ft80x_write_hword(priv, FT80X_REG_VOFFSET, 12);
  ft80x_write_hword(priv, FT80X_REG_VSYNC0, 0);
  ft80x_write_hword(priv, FT80X_REG_VSYNC1, 10);
  ft80x_write_byte(priv, FT80X_REG_SWIZZLE, 0);
  ft80x_write_byte(priv, FT80X_REG_PCLK_POL, 1);
  ft80x_write_byte(priv, FT80X_REG_CSPREAD, 1);
  ft80x_write_hword(priv, FT80X_REG_HSIZE, 480);
  ft80x_write_hword(priv, FT80X_REG_VSIZE, 272);

#elif defined(CONFIG_LCD_FT80X_QVGA)
  ft80x_write_hword(priv, FT80X_REG_HCYCLE, 408);
  ft80x_write_hword(priv, FT80X_REG_HOFFSET, 70);
  ft80x_write_hword(priv, FT80X_REG_HSYNC0, 0);
  ft80x_write_hword(priv, FT80X_REG_HSYNC1, 10);
  ft80x_write_hword(priv, FT80X_REG_VCYCLE, 263);
  ft80x_write_hword(priv, FT80X_REG_VOFFSET, 13);
  ft80x_write_hword(priv, FT80X_REG_VSYNC0, 0);
  ft80x_write_hword(priv, FT80X_REG_VSYNC1, 2);
  ft80x_write_byte(priv, FT80X_REG_SWIZZLE, 2);
  ft80x_write_byte(priv, FT80X_REG_PCLK_POL, 0);
  ft80x_write_byte(priv, FT80X_REG_CSPREAD, 1);
  ft80x_write_hword(priv, FT80X_REG_HSIZE, 320);
  ft80x_write_hword(priv, FT80X_REG_VSIZE, 240);

#else
#  error Unknown display size
#endif

  /* 5. Write first display list */

  ft80x_write_word(priv, FT80X_RAM_DL + 0, FT80X_CLEAR_COLOR_RGB(0, 0, 0));
  ft80x_write_word(priv, FT80X_RAM_DL + 4, FT80X_CLEAR(1, 1, 1));
  ft80x_write_word(priv, FT80X_RAM_DL + 8, FT80X_DISPLAY());

  /* 6. Write FT80X_REG_DLSWAP, FT800 swaps display list immediately */

  ft80x_write_byte(priv, FT80X_REG_DLSWAP, DLSWAP_FRAME);

  /* GPIO bit 7 is used for the display enable pin of the LCD module. By
   * setting the direction of the GPIO bit to out direction, the display can
   * be enabled by writing value of 1 into GPIO bit 7 or the display can be
   * disabled by writing a value of 0 into GPIO bit 7. By default GPIO bit 7
   * direction is output and the value is 0.
   *
   * If an external audio amplified is controlled by an FT80x GPIO, then
   * configure that GPIO as well.  Active low logic is assumed so that the
   * amplifier is initially in the shutdown state.
   */

  regval8  = ft80x_read_byte(priv, FT80X_REG_GPIO_DIR);
  regval8 |= (1 << 7);
#ifdef CONFIG_LCD_FT80X_AUDIO_GPIOSHUTDOWN
  regval8 |= (1 << CONFIG_LCD_FT80X_AUDIO_GPIO);
#endif
  ft80x_write_byte(priv, FT80X_REG_GPIO_DIR, regval8);

  regval8  = ft80x_read_byte(priv, FT80X_REG_GPIO);
  regval8 |= (1 << 7);
#ifdef CONFIG_LCD_FT80X_AUDIO_GPIOSHUTDOWN
  regval8 |= (1 << CONFIG_LCD_FT80X_AUDIO_GPIO);
#endif
  ft80x_write_byte(priv, FT80X_REG_GPIO, regval8);

  /* 7. Enable back light control for display */
#warning Missing logic

  /* 8. Write FT80X_REG_PCLK, video output with the first display list */

#if defined(CONFIG_LCD_FT80X_WQVGA)
  ft80x_write_byte(priv, FT80X_REG_PCLK, 5);
#elif defined(CONFIG_LCD_FT80X_QVGA)
  ft80x_write_byte(priv, FT80X_REG_PCLK, 8);
#else
#  error Unknown display size
#endif

  /* 9. Use MCU SPI clock not more than 30MHz */

  DEBUGASSERT(priv->lower->op_frequency <= 30000000);
  priv->frequency = priv->lower->op_frequency;

  /* Configure touch mode.  Using default touch mode of FRAME_SYNC (~60Hz) */

  ft80x_write_byte(priv, FT80X_REG_TOUCH_MODE, TOUCH_MODE_FRAMESYNC);

#if defined(CONFIG_LCD_FT800)
  /* Configure the touch threshold.  The value 1200 may need to be tweaked
   * for your application.
   */

  ft80x_write_hword(priv, FT80X_REG_TOUCH_RZTHRESH, 1200);

#elif defined(CONFIG_LCD_FT801)
#ifdef CONFIG_LCD_FT801_MULTITOUCH
  /* Selected extended mode */

  ft80x_write_byte(priv, FT80X_REG_CTOUCH_EXTENDED, 0);
#else
  /* Selected compatibility mode */

  ft80x_write_byte(priv, FT80X_REG_CTOUCH_EXTENDED, 1);
#endif

#else
#  error No FT80x device configured
#endif

  return OK;
}

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

/****************************************************************************
 * Name: ft80x_register
 *
 * Description:
 *   Configure the ADS7843E to use the provided SPI device instance.  This
 *   will register the driver as /dev/ft800 or /dev/ft801, depending upon
 *   the configuration.
 *
 * Input Parameters:
 *   spi   - An SPI driver instance
 *   i2c   - An I2C master driver instance
 *   lower - Persistent board configuration data / lower half interface
 *
 * Returned Value:
 *   Zero is returned on success.  Otherwise, a negated errno value is
 *   returned to indicate the nature of the failure.
 *
 ****************************************************************************/

#if defined(CONFIG_LCD_FT80X_SPI)
int ft80x_register(FAR struct spi_dev_s *spi,
                   FAR const struct ft80x_config_s *lower)
#elif defined(CONFIG_LCD_FT80X_I2C)
int ft80x_register(FAR struct i2c_master_s *i2c,
                   FAR const struct ft80x_config_s *lower)
#endif
{
  FAR struct ft80x_dev_s *priv;
  int ret;

#if defined(CONFIG_LCD_FT80X_SPI)
  DEBUGASSERT(spi != NULL && lower != NULL);
#elif defined(CONFIG_LCD_FT80X_I2C)
  DEBUGASSERT(i2c != NULL && lower != NULL);
#endif

  /* Allocate the driver state structure */

  priv = (FAR struct ft80x_dev_s *)kmm_zalloc(sizeof(struct ft80x_dev_s));
  if (priv == NULL)
    {
      lcderr("ERROR: Failed to allocate state structure\n");
      return -ENOMEM;
    }

  /* Save the lower level interface and configuration information */

  priv->lower = lower;

#ifdef CONFIG_LCD_FT80X_SPI
  /* Remember the SPI configuration */

  priv->spi = spi;
#else
  /* Remember the I2C configuration */

  priv->i2c = i2c;
#endif

  /* Initialize the mutual exclusion mutex */

  nxmutex_init(&priv->lock);

  /* Initialize the FT80x */

  ret = ft80x_initialize(priv);
  if (ret < 0)
    {
      goto errout_with_lock;
    }

  /* Attach our interrupt handler */

  DEBUGASSERT(lower->attach != NULL && lower->enable != NULL);
  ret = lower->attach(lower, ft80x_interrupt, priv);
  if (ret < 0)
    {
      goto errout_with_lock;
    }

  /* Disable all interrupt sources, but enable interrupts both in the lower
   * half driver and in the FT80x.
   */

  ft80x_write_word(priv, FT80X_REG_INT_MASK, 0);
  ft80x_write_word(priv, FT80X_REG_INT_EN, FT80X_INT_ENABLE);
  lower->enable(lower, true);

  /* Register the FT80x character driver */

  ret = register_driver(DEVNAME, &g_ft80x_fops, 0666, priv);
  if (ret < 0)
    {
      goto errout_with_interrupts;
    }

  return OK;

errout_with_interrupts:
  lower->enable(lower, false);
  ft80x_write_word(priv, FT80X_REG_INT_EN, FT80X_INT_DISABLE);
  lower->attach(lower, NULL, NULL);

errout_with_lock:
  nxmutex_destroy(&priv->lock);
  kmm_free(priv);
  return ret;
}

#endif /* CONFIG_LCD_FT80X */
