/****************************************************************************
 * drivers/video/max7456.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.
 *
 ****************************************************************************/

/****************************************************************************
 * Theory of Operation
 *
 * The MAX7456 is a single-channel, monochrome, on-screen-display generator
 * that accepts an NTSC or PAL video input signal, overlays user-defined
 * character data, and renders the combined stream to CVBS (analog) output.
 * The typical use case then forwards that CVBS output to a video
 * transmitter, analog display, recording device, and/or other external
 * components.
 *
 * The chip is fundamentally an SPI slave device with a register bank to
 * configure the chip's analog components, update values in the display frame
 * buffer, and modify the chip's onboard non-volatile character set.
 *
 * The MAX7456 must by necessity recover the video stream's hsync and vsync
 * signals, as part of its normal operations. These signals are also made
 * available at pins on the chip body, and may be used to synchronize updates
 * of frame buffer data with the vertical-blanking period.  Such
 * synchronization prevents "glitches" during OSD updates.
 *
 * Up to 480 user-definable characters can be displayed at one time. Each
 * 16-bit "character" is expressed an 8-bit index into the chip's onboard
 * character set, followed by an 8-bit character attribute that controls the
 * character's local background, blinking, and inversion.
 *
 * The overlaid characters may be distributed across 13 (NTSC) or 16 (PAL)
 * rows of the visible display area. The attributes of each of those lines
 * are also controllable on a line-by-line basis.
 *
 * OSD insertion is ultimately an analog process, and a few of the chip's
 * control registers are provided to adjust the OSD multiplexer's rise and
 * fall times. This is necessary to strike the user's preferred balance
 * between overlay sharpness and certain, undesirable display artifacts. The
 * defaults are probably good enough to start with, though.
 *
 * Note: Although we use the term "frame buffer", we cannot use the NuttX
 * standard /dev/fbN interface because our buffer memory is accessible only
 * across SPI. This is an inexpensive, slow, simple chip, and you wouldn't
 * use it for intensive work, but you WOULD use it on a memory-constrained
 * device. We keep our RAM footprint small by not keeping a local copy of the
 * framebuffer data.
 *
 ****************************************************************************/

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

#include <nuttx/config.h>

#include <errno.h>
#include <debug.h>
#include <string.h>
#include <limits.h>
#include <nuttx/mutex.h>

#include <nuttx/bits.h>
#include <nuttx/compiler.h>
#include <nuttx/kmalloc.h>
#include <nuttx/spi/spi.h>
#include <nuttx/fs/fs.h>
#include <nuttx/video/max7456.h>

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

/* Enables debug-related interfaces. Leave undefined otherwise. */

#define DEBUG 1

/* Creates a mask of @m bits, i.e. MASK(2) -> 00000011 */

#define MASK(m) (BIT((m) + 1) - 1)

/* Masks and shifts @v into bit field @m */

#define TO_BITFIELD(m,v) ((v) & MASK(m ##__WIDTH) << (m ##__SHIFT))

/* Un-masks and un-shifts bit field @m from @v */

#define FROM_BITFIELD(m,v) (((v) >> (m ##__SHIFT)) & MASK(m ##__WIDTH))

/* SPI read/write codes and speed */

#define SPI_REG_READ 0x80
#define SPI_REG_WRITE 0
#define SPI_FREQ 10000000UL
#define SPI_MODE SPIDEV_MODE0

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

/* Register file description. */

enum mx7_regaddr_e
{
  VM0                 = 0,                  /* video mode (config) 0 */
  VM0__PAL            = BIT(6),
  VM0__SYNCSEL__SHIFT = 4,
  VM0__SYNCSEL__WIDTH = 2,
  VM0__ENABLE         = BIT(3),
  VM0__VSYNC_EN       = BIT(2),
  VM0__RESET          = BIT(1),
  VM0__VBUF_EN        = BIT(0),

  VM1                 = 1,                  /* video mode (config) 1 */
  VM1__GRAY           = BIT(7),
  VM1__OSD_PCT__SHIFT = 4,
  VM1__OSD_PCT__WIDTH = 3,
  VM1__BT__SHIFT      = 2,
  VM1__BT__WIDTH      = 2,
  VM1__BD__SHIFT      = 0,
  VM1__BD__WIDTH      = 2,

  HOS                 = 2,                  /* horizontal position */
  HOS__HPOS__SHIFT    = 0,
  HOS__HPOS__WIDTH    = 6,

  VOS                 = 3,                  /* vertical position */
  VOS__VPOS__SHIFT    = 0,
  VOS__VPOS__WIDTH    = 5,

  DMM                 = 4,                  /* display memory mode */
  DMM__8BIT           = BIT(6),
  DMM__LBC            = BIT(5),
  DMM__BLK            = BIT(4),
  DMM__INV            = BIT(3),
  DMM__CA__SHIFT      = 3,                  /* character attr */
  DMM__CA__WIDTH      = 3,
  DMM__CLEAR          = BIT(2),
  DMM__VCLEAR         = BIT(1),
  DMM__AUTOINC        = BIT(0),

  DMAH                  = 5,                /* display mem addr, high */
  DMAH__ATTR            = BIT(1),
  DMAH__ADDRBIT8__SHIFT = 0,
  DMAH__ADDRBIT8__WIDTH = 1,

  DMAL                  = 6,                /* display mem addr, low */
  DMAL__ADDR__SHIFT     = 0,
  DMAL__ADDR__WIDTH     = 8,

  DMDI                  = 7,                /* display memory data in */
  DMDI__SHIFT           = 0,
  DMDI__WIDTH           = 8,

  CMM                   = 8,                /* character memory mode */
  CMM__READ_NVM         = BIT(6) | BIT(4),
  CMM__WRITE_NVM        = BIT(7) | BIT(5),

  CMAH                  = 9,                /* char memory addr, high */
  CMAH__SHIFT           = 0,
  CMAH__WIDTH           = 6,

  CMAL                  = 0xa,              /* char memory addr, low */
  CMAL__ADDR__SHIFT     = 0,
  CMAL__ADDR__WIDTH     = 6,

  CMDI                  = 0xb,              /* character memory data in */

  OSDM                  = 0xc,              /* osd insertion mux */
  OSDM__RISET__SHIFT    = 3,                /* rise time */
  OSDM__RISET__WIDTH    = 3,
  OSDM__SWITCHT__SHIFT  = 0,                /* switching time */
  OSDM__SWITCHT__WIDTH  = 3,

  RB0                   = 0x10,             /* row N brightness */
  RB1                   = (RB0 + 1),
  RB2                   = (RB0 + 2),
  RB3                   = (RB0 + 4),
  RB4                   = (RB0 + 5),
  RB6                   = (RB0 + 6),
  RB7                   = (RB0 + 7),
  RB8                   = (RB0 + 8),
  RB9                   = (RB0 + 9),
  RB10                  = (RB0 + 10),
  RB11                  = (RB0 + 11),
  RB12                  = (RB0 + 12),
  RB13                  = (RB0 + 13),
  RB14                  = (RB0 + 14),
  RB15                  = (RB0 + 15),

  OSDBL                 = 0x6c,             /* osd black level */
  OSDBL__DISABLE        = BIT(4),
  OSDBL__PRESET__SHIFT  = 0,
  OSDBL__PRESET__WIDTH  = 4,

  STAT                  = 0xa0,             /* status (ro) */
  STAT__INRESET         = BIT(6),           /* 1 = in power-on reset */
  STAT__CHARUNAVAIL     = BIT(5),           /* 1 = unavailable for writes */
  STAT__NVSYNC          = BIT(4),           /* 1 = in vertical sync time */
  STAT__NHSYNC          = BIT(3),           /* 1 = in horizontal sync time */
  STAT__LOS             = BIT(2),           /* 1 = lost sync */
  STAT__NTSC            = BIT(1),           /* 1 = ntsc video detected */
  STAT__PAL             = BIT(0),           /* 1 = pal video detected */

  DMDO                  = 0xb0,             /* data memory data out (ro) */
  CMDO                  = 0xc0,             /* char memory data out (ro) */
};

struct path_name_map_s
{
  uint8_t addr;
  FAR const char *path;
};

#define PATH_MAP_ENTRY(node) { .addr = (node), .path = "" #node "" }

enum mx7_interface_e
{
  FB,                         /* 8-bit read/write interface */
  RAW,                        /* 16-bit interface in chip's native format */
  VSYNC,                      /* blocks until vertical blanking interval */
  CM                          /* Character Memory, i.e. the character map */
};

static struct path_name_map_s node_map[] =
{
  PATH_MAP_ENTRY(FB),
  PATH_MAP_ENTRY(RAW),
  PATH_MAP_ENTRY(VSYNC),
  PATH_MAP_ENTRY(CM),
};

#define NODE_MAP_LEN (sizeof(node_map) / sizeof(*node_map))

#if defined(DEBUG)

/* Maps between register names and addresses */

static struct path_name_map_s reg_name_map[] =
{
  PATH_MAP_ENTRY(VM0),
  PATH_MAP_ENTRY(VM1),
  PATH_MAP_ENTRY(HOS),
  PATH_MAP_ENTRY(VOS),
  PATH_MAP_ENTRY(DMM),
  PATH_MAP_ENTRY(DMAH),
  PATH_MAP_ENTRY(DMAL),
  PATH_MAP_ENTRY(DMDI),
  PATH_MAP_ENTRY(CMM),
  PATH_MAP_ENTRY(CMAH),
  PATH_MAP_ENTRY(CMAL),
  PATH_MAP_ENTRY(CMDI),
  PATH_MAP_ENTRY(OSDM),
  PATH_MAP_ENTRY(RB0),
  PATH_MAP_ENTRY(RB1),
  PATH_MAP_ENTRY(RB2),
  PATH_MAP_ENTRY(RB3),
  PATH_MAP_ENTRY(RB4),
  PATH_MAP_ENTRY(RB6),
  PATH_MAP_ENTRY(RB7),
  PATH_MAP_ENTRY(RB8),
  PATH_MAP_ENTRY(RB9),
  PATH_MAP_ENTRY(RB10),
  PATH_MAP_ENTRY(RB11),
  PATH_MAP_ENTRY(RB12),
  PATH_MAP_ENTRY(RB13),
  PATH_MAP_ENTRY(RB14),
  PATH_MAP_ENTRY(RB15),
  PATH_MAP_ENTRY(OSDBL),
  PATH_MAP_ENTRY(STAT),
  PATH_MAP_ENTRY(DMDO),
  PATH_MAP_ENTRY(CMDO)
};
#endif

#define REG_NAME_MAP_LEN (sizeof(reg_name_map) / sizeof(*reg_name_map))

/* Used to manage the device. No user-serviceable parts inside. */

struct mx7_dev_s
{
  mutex_t lock;               /* mutex for this structure */
  struct mx7_config_s config; /* board-specific information */

  uint8_t ca;                 /* character attribute (lbc, blink, etc.) */

#if defined(DEBUG)
  char debug[2];              /* stash for debugging-related output */
#endif
};

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

static int mx7_open(FAR struct file *filep);
static ssize_t mx7_read(FAR struct file *filep,
                        FAR char *buf, size_t len);
static ssize_t mx7_write(FAR struct file *filep,
                         FAR const char *buf, size_t len);

#if defined(DEBUG)
static ssize_t mx7_debug_read(FAR struct file *filep,
                              FAR char *buf, size_t len);
static ssize_t mx7_debug_write(FAR struct file *filep,
                               FAR const char *buf, size_t len);
#endif

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

/* General user interface operations. */

static const struct file_operations g_mx7_fops =
{
  mx7_open,      /* open */
  NULL,          /* close */
  mx7_read,      /* read */
  mx7_write,     /* write */
  NULL,          /* seek */
  NULL,          /* ioctl */
  NULL           /* poll */
};

#if defined(DEBUG)

/* Debug-only interface, mostly for direct register access. */

static const struct file_operations g_mx7_debug_fops =
{
  NULL,                /* open */
  NULL,                /* close */
  mx7_debug_read,      /* read */
  mx7_debug_write,     /* write */
};
#endif

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

/* Translates an interface name name to its associated mx7_interface_e
 * enumerator.
 */

static int node_from_name(FAR const char *name)
{
  int n;

  for (n = 0; n < NODE_MAP_LEN; n++)
    {
      if (!strcmp(name, node_map[n].path))
        {
          return node_map[n].addr;
        }
    }

  return -1;
}

/* Translates a register name to its associated address. */

static int regaddr_from_name(FAR const char *name)
{
  int n;

  for (n = 0; n < REG_NAME_MAP_LEN; n++)
    {
      if (!strcmp(name, reg_name_map[n].path))
        {
          return reg_name_map[n].addr;
        }
    }

  return -1;
}

/* NOTE :
 *
 * In all of the following code, functions named with a double leading
 * underscore '__' must be invoked ONLY if the mx7_dev_s lock is
 * already held. Failure to do this might cause the transaction to get
 * interrupted, which will likely confuse the data you're trying to send.
 *
 * The mx7_dev_s lock is NOT the same thing as, i.e. the SPI master
 * interface lock: the latter protects the bus interface hardware
 * (which may have other SPI devices attached), the former protects
 * our chip and its associated data.
 */

/****************************************************************************
 * Name: __mx7_read_reg
 *
 * Description:
 *   Reads @len bytes into @buf from @dev, starting at register address
 *   @addr. This is a low-level function used for reading a sequence of one
 *   or more register values, and isn't usually called directly unless you
 *   REALLY know what you are doing. Consider one of the register-specific
 *   helper functions defined below whenever possible.
 *
 * Note: The caller must hold @dev->lock before calling this function.
 *
 * Input parameters:
 *   dev       - the target device's handle
 *   addr      - starting register address
 *   buf       - where to store the register values
 *   len       - number of registers to read
 *
 * Returned value:
 *   Returns number of bytes read, or a negative errno.
 *
 ****************************************************************************/

static int __mx7_read_reg(FAR struct mx7_dev_s *dev,
                          enum mx7_regaddr_e addr,
                          FAR uint8_t * buf, uint8_t len)
{
  int ret;
  FAR struct spi_dev_s *spi = dev->config.spi;
  int id = dev->config.spi_devid;

  /* We'll probably return the number of bytes asked for. */

  ret = len;

  /* Grab the SPI master controller, and set the mode. */

  SPI_LOCK(spi, true);
  SPI_SETMODE(spi, SPI_MODE);
  SPI_SETFREQUENCY(spi, SPI_FREQ);

  /* Select the chip. */

  SPI_SELECT(spi, id, true);

  /* Send the read request. */

  SPI_SEND(spi, addr | SPI_REG_READ);

  /* Clock in the data. */

  while (0 != len--)
    {
      *buf++ = (uint8_t) (SPI_SEND(spi, 0xff));
    }

  /* Deselect the chip, release the SPI master. */

  SPI_SELECT(spi, id, false);
  SPI_LOCK(spi, false);

  return ret;
}

/****************************************************************************
 * Name:  __mx7_write_reg
 *
 * Description:
 *   Writes @len bytes from @buf to @dev, starting at @addr. This is a
 *   low-level function used for updating a sequence of one or more register
 *   values, and it DOES NOT check that the register being requested is
 *   write-capable. This function isn't called directly unless you REALLY
 *   know what you are doing.
 *
 *   Consider one of the register-specific helper functions defined below
 *   whenever possible. If a helper function for the register you desire to
 *   write is not defined, it's probably because that register is read-only.
 *
 * Note: The caller must hold @dev->lock before calling this function.
 *
 * Input parameters:
 *   dev       - the target device's handle
 *   addr      - starting register address
 *   buf       - byte sequence to write
 *   len       - length of @buf, number of bytes to write
 *
 * Returned value:
 *   Returns number of bytes written, or a negative errno.
 *
 ****************************************************************************/

static int __mx7_write_reg(FAR struct mx7_dev_s *dev,
                           enum mx7_regaddr_e addr,
                           FAR const uint8_t * buf, uint8_t len)
{
  int ret = len;
  FAR struct spi_dev_s *spi = dev->config.spi;
  int id = dev->config.spi_devid;

  /* Grab and configure the SPI master device. */

  SPI_LOCK(spi, true);
  SPI_SETMODE(spi, SPI_MODE);
  SPI_SETFREQUENCY(spi, SPI_FREQ);

  /* Select the chip. */

  SPI_SELECT(spi, id, true);

  /* Send the write request. */

  SPI_SEND(spi, addr | SPI_REG_WRITE);

  /* Send the data. */

  while (0 != len--)
    {
      SPI_SEND(spi, *buf++);
    }

  /* Release the chip and SPI master. */

  SPI_SELECT(spi, id, false);
  SPI_LOCK(spi, false);

  return ret;
}

/****************************************************************************
 * Name: __mx7_read_reg__stat
 *
 * Description:
 *   Reads the contents of the STAT register.
 *
 * Returned value:
 *   Returns the value in STAT, or negative errno.
 *
 ****************************************************************************/

static inline int __mx7_read_reg__stat(FAR struct mx7_dev_s *dev)
{
  uint8_t val = 0xff;
  int ret;

  ret = __mx7_read_reg(dev, STAT, &val, sizeof(val));

  /* Return the error code, if an error occurred. */

  if (ret < 0)
    {
      return ret;
    }

  /* Return the register value. */

  return val;
}

/****************************************************************************
 * Name:  __mx7_read_reg__dmm
 *
 * Description:
 *   Reads the contents of the DMM register.
 *
 * Returned value:
 *   Returns the value held in in DMM, or negative errno.
 *
 ****************************************************************************/

static inline int __mx7_read_reg__dmm(FAR struct mx7_dev_s *dev)
{
  uint8_t val = 0xff;
  int ret;

  ret = __mx7_read_reg(dev, DMM, &val, sizeof(val));
  if (ret < 0)
    {
      return ret;
    }

  return val;
}

/****************************************************************************
 * Name: __mx7_write_reg__vm0
 *
 * Description:
 *   Writes @val to VM0. A simple helper around __mx7_write_reg().
 *
 * Returned value:
 *   Returns the number of bytes written (always 1), or a negative errno.
 *
 ****************************************************************************/

static inline int __mx7_write_reg__vm0(FAR struct mx7_dev_s *dev,
                                       uint8_t val)
{
  return __mx7_write_reg(dev, VM0, &val, sizeof(val));
}

/****************************************************************************
 * Name: __mx7_read_reg__vm0
 *
 * Description:
 *   Returns the contents of VM0.
 *
 * Returned value:
 *   Returns the register value, or a negative errno.
 *
 ****************************************************************************/

static inline int __mx7_read_reg__vm0(FAR struct mx7_dev_s *dev)
{
  uint8_t val = 0xff;
  int ret;

  ret = __mx7_read_reg(dev, VM0, &val, sizeof(val));

  if (ret < 0)
    {
      return ret;
    }

  return val;
}

/****************************************************************************
 * Name: __mx7_write_reg__cmah
 *
 * Description:
 *   Writes @val to CMAH.
 *
 * Returned value:
 *   Returns the number of bytes written (always 1), or a negative errno.
 *
 ****************************************************************************/

static inline int __mx7_write_reg__cmah(FAR struct mx7_dev_s *dev,
                                        uint8_t val)
{
  return __mx7_write_reg(dev, CMAH, &val, sizeof(val));
}

/****************************************************************************
 * Name: __mx7_write_reg__cmm
 *
 * Description:
 *   Writes @val to CMM.
 *
 * Returned value:
 *   Returns the number of bytes written (always 1), or a negative errno.
 *
 ****************************************************************************/

static inline int __mx7_write_reg__cmm(FAR struct mx7_dev_s *dev,
                                       uint8_t val)
{
  return __mx7_write_reg(dev, CMM, &val, sizeof(val));
}

/****************************************************************************
 * Name: __mx7_write_reg__cmal
 *
 * Description:
 *   Writes @val to CMAL.
 *
 * Returned value:
 *   Returns the number of bytes written (always 1), or a negative errno.
 *
 ****************************************************************************/

static inline int __mx7_write_reg__cmal(FAR struct mx7_dev_s *dev,
                                        uint8_t val)
{
  return __mx7_write_reg(dev, CMAL, &val, sizeof(val));
}

/****************************************************************************
 * Name: __mx7_write_reg__osdbl
 *
 * Description:
 *   Writes @val to OSDBL.
 *
 * Returned value:
 *   Returns the number of bytes written (always 1), or a negative errno.
 *
 ****************************************************************************/

static inline int __mx7_write_reg__osdbl(FAR struct mx7_dev_s *dev,
                                         uint8_t val)
{
  return __mx7_write_reg(dev, OSDBL, &val, sizeof(val));
}

/****************************************************************************
 * Name: __mx7_read_reg__osdbl
 *
 * Description:
 *   Returns the contents of OSDBL.
 *
 * Returned value:
 *   Returns the register value, or a negative errno.
 *
 ****************************************************************************/

static inline int __mx7_read_reg__osdbl(FAR struct mx7_dev_s *dev)
{
  uint8_t val = 0xff;
  int ret;

  ret = __mx7_read_reg(dev, OSDBL, &val, sizeof(val));

  if (ret < 0)
    {
      return ret;
    }

  return val;
}

/****************************************************************************
 * Name: __mx7_read_reg__cmdo
 *
 * Description:
 *   Returns the contents of CMDO.
 *
 * Returned value:
 *   Returns the register value, or a negative errno.
 *
 ****************************************************************************/

static inline int __mx7_read_reg__cmdo(FAR struct mx7_dev_s *dev)
{
  uint8_t val = 0xff;
  int ret;

  ret = __mx7_read_reg(dev, CMDO, &val, sizeof(val));

  if (ret < 0)
    {
      return ret;
    }

  return val;
}

/****************************************************************************
 * Name: __mx7_wait_reset
 *
 * Description:
 *   Waits until the chip finishes its reset activities.
 *
 ****************************************************************************/

static inline void __mx7_wait_reset(FAR struct mx7_dev_s *dev)
{
  int stat = 0;                 /* contents of STAT register */
  int dmm = 0;                  /* contents of DMM register */
  int vm0 = 0;                  /* contents of VM0 register */

  do
    {
      /* If we're here, a reset command has probably just been issued; wait
       * 100usec before checking, per the datasheet.
       */

      up_udelay(100);

      vm0 = __mx7_read_reg__vm0(dev);
      if (vm0 & VM0__RESET)
        {
          continue;
        }

      stat = __mx7_read_reg__stat(dev);
      dmm = __mx7_read_reg__dmm(dev);
    }
  while ((stat & STAT__INRESET) || (dmm & DMM__CLEAR));
}

/****************************************************************************
 * Name: __mx7_read_nvm
 *
 * Description:
 *   Commands the NVM to move the current CMAH:CMAL into shadow RAM.
 ****************************************************************************/

static inline void __mx7_read_nvm(FAR struct mx7_dev_s *dev)
{
  int stat = 0;

  /* Initiate the command. */

  __mx7_write_reg__cmm(dev, CMM__READ_NVM);

  do
    {
      /* Wait for it to finish. */

      up_udelay(10);
      stat = __mx7_read_reg__stat(dev);
    }
  while (stat & STAT__CHARUNAVAIL);
}

/****************************************************************************
 * Name: mx7_reset
 *
 * Description:
 *   Asserts a RESET command in the chip, and waits for it to finish. Except
 *   for NVM and the OSD brightness trim, this action restores all register
 *   values in the chip to their factory defaults.
 *
 ****************************************************************************/

static void mx7_reset(FAR struct mx7_dev_s *dev)
{
  nxmutex_lock(&dev->lock);

  /* Issue the reset command. */

  __mx7_write_reg__vm0(dev, VM0__RESET);

  /* Wait for all reset-related activities to finish. */

  __mx7_wait_reset(dev);

  /* All done. */

  nxmutex_unlock(&dev->lock);
}

/****************************************************************************
 * Name: __write_fb
 *
 * Description:
 *   Writes a stream of bytes to character address memory. We use the chip's
 *   "16-bit auto-increment mode", in order to make this operation as fast
 *   as possible. All of the bytes written are given the same attribute @ca.
 *
 *   This operation is best performed with the CS held, so we do all of the
 *   SPI heavy-lifting ourselves here. This function is comparable to a
 *   giant __write_reg_N().
 *
 * Input parameters:
 *   buf   -  character addresses (data) to write
 *   len   -  length of @buf
 *   ca    -  character attribute, see the DMM register for details
 *   pos   -  starting address, 0 = upper-left corner of the display
 *
 * Returned value:
 *   Returns the number of bytes written, or a negative errno.
 *
 ****************************************************************************/

static ssize_t __write_fb(FAR struct mx7_dev_s *dev,
                          FAR const uint8_t * buf, size_t len,
                          uint8_t ca, size_t pos)
{
  ssize_t ret = len;
  FAR struct spi_dev_s *spi = dev->config.spi;
  int id = dev->config.spi_devid;

  /* Configure the bus and grab the chip as usual. */

  SPI_LOCK(spi, true);
  SPI_SETMODE(spi, SPI_MODE);
  SPI_SETFREQUENCY(spi, SPI_FREQ);
  SPI_SELECT(spi, id, true);

  while (len != 0)
    {
      /* Thus sayeth the datasheet (pp. 39-40):
       *
       * "When in 16-Bit [Auto-Increment] Operating Mode:
       *
       * 1) Write DMAH[0] = X to select the MSB and DMAL[7:0] = XX to select
       *    the lower order address bits of the starting address for
       *    auto-increment operation.  This address determines the location
       *    of the first character on the display (see Figures 10 and 21)."
       */

      SPI_SEND(spi, DMAH | SPI_REG_WRITE);
      SPI_SEND(spi, TO_BITFIELD(DMAH__ADDRBIT8, (pos >> 8)));
      SPI_SEND(spi, DMAL | SPI_REG_WRITE);
      SPI_SEND(spi, TO_BITFIELD(DMAL__ADDR, pos));

      /* "2) Write DMM[0] = 1 to set the auto-increment mode.
       *
       *  3) Write DMM[6] = 0 to set the 16-bit operating mode.
       *
       *  4) Write DMM[5:3] = XXX to set the Local Background Control
       *     (LBC), Blink (BLK) and Invert (INV) attribute bits that
       *     will be applied to all characters."
       */

      SPI_SEND(spi, DMM | SPI_REG_WRITE);
      SPI_SEND(spi, DMM__AUTOINC | TO_BITFIELD(DMM__CA, ca));

      /* "5) Write CA [Character Address: the index into the chip's onboard
       *     NVM character map] data in the intended character order to
       *     display text on the screen. It will be stored along with a
       *     Character Attribute byte derived from DMM[5:3].  See Figure
       *     19. This is the single byte operation. The DMDI[7:0] address is
       *     automatically set by autoincrement mode. The display memory
       *     address is automatically incremented following the write
       *     operation until the final display memory address is reached."
       */

      while (len != 0)
        {
          /* Send the byte to the DMDI register. The "auto-increment" mode
           * will update DMAH and DMAL for us.
           */

          SPI_SEND(spi, DMDI | SPI_REG_WRITE);
          SPI_SEND(spi, *buf);

          /* Check if we just exited auto-increment mode. */

          if (*buf == 0xff)
            {
              /* An embedded 0xff terminates auto-increment mode, and since
               * we've already sent it, pause here to deal with
               * it. Betaflight, et. al just skip the byte and continue, and
               * then retrace their steps later. I think it's a better
               * workflow to just deal with it now. Plus, there's only a
               * 1/256 chance of there being such a byte anyway, and if
               * performance ends up being a problem then the user can move
               * the CA to a different index in their NVM map.
               */

              break;
            }
          else
            {
              /* It was an ordinary byte, so we're still in auto-increment
               * mode; count it and keep going.
               */

              buf++;
              pos++;
              len--;
            }
        }

      /* (Use of while() here instead of if() catches repeated 0xff's while
       * we're already out of auto-increment mode. Since you mustached, this
       * shaves a transaction or two when they occur.)
       */

      while (len != 0 && *buf == 0xff)
        {
          /* We're out of the auto-increment loop but still have data
           * remaining, which means there's an 0xff in the data stream. We
           * must send it the hard way, but we can still use the attribute
           * byte already stored in DMM.
           */

          SPI_SEND(spi, DMAH | SPI_REG_WRITE);
          SPI_SEND(spi, TO_BITFIELD(DMAH__ADDRBIT8, (pos >> 8)));
          SPI_SEND(spi, DMAL | SPI_REG_WRITE);
          SPI_SEND(spi, TO_BITFIELD(DMAL__ADDR, pos));
          SPI_SEND(spi, DMDI | SPI_REG_WRITE);
          SPI_SEND(spi, *buf);

          /* Now we can retire the byte. */

          buf++;
          pos++;
          len--;
        }
    }

  /* "6) Write CA = FFh to terminate the auto-increment mode." */

  SPI_SEND(spi, DMDI | SPI_REG_WRITE);
  SPI_SEND(spi, 0xff);

  /* The datasheet suggests that the chip will drop DMM[1] when it leaves
   * auto-increment mode, but I don't see that happening. Let's force it to
   * drop here just in case, so as to not not confuse future DMDI writes.
   */

  SPI_SEND(spi, DMM | SPI_REG_WRITE);
  SPI_SEND(spi, TO_BITFIELD(DMM__CA, ca));

  /* And, finally, we're all done. */

  SPI_SELECT(spi, id, false);
  SPI_LOCK(spi, false);

  return ret;
}

/****************************************************************************
 * Name: __read_cm
 *
 * Description:
 *    Reads the chip's Character Memory area.
 *
 *    Each entry in the Character Memory area is 3x18=54 bytes, so one would
 *    expect that the @len parameter would always be an integer multiple of
 *    that quantity. But we don't require that here, because the chip doesn't
 *    either.
 *
 *    Each row in the CA EEPROM is 64 bytes wide, but only the first 54 bytes
 *    are used. The rest are marked as "unused memory" in the datasheet. All
 *    64 bytes of each row are included in the data we return, if the user's
 *    request spans that area. We assume that the user understands the
 *    format.
 *
 *    In total, the chip has 64 bytes per row x 256 rows of EEPROM.
 *
 *    Finally, each pixel of a character requires two bits to define. Thus,
 *    there are four pixels per byte.
 *
 * Input parameters:
 *   dev  - device handle
 *   pos - starting address to read from, i.e. offset in bytes from the start
 *          of character memory
 *   buf  - buffer to return the character map data
 *   len  - number of bytes to return
 *
 * Returned value:
 *  Returns the number of bytes read on success, or negative errno.
 *
 ****************************************************************************/

static ssize_t __read_cm(FAR struct mx7_dev_s *dev,
                         size_t pos, FAR uint8_t * buf, size_t len)
{
  const size_t eeprom_rows = 256;
  const size_t eeprom_cols = 64;
  const size_t eeprom_bytes = eeprom_rows * eeprom_cols;
  ssize_t ret = len;
  int vm0 = 0;
  int cmah = 0;
  int cmal = 0;

  /* Does the request stay in-bounds? */

  if (pos + len >= eeprom_bytes)
    {
      if (pos >= eeprom_bytes)
        {
          /* They want to start out-of-bounds. No. */

          len = 0;
        }

      /* The starting position is in-bounds, but somewhere after that they
       * run out of bounds. Truncate the length of their request to what
       * will fit, per the usual read() semantics. Next time, they'll
       * probably call us with a position that's out of bounds. We'll catch
       * them above, and return 0.
       */

      len = eeprom_bytes - pos;
    }

  /* If we have nothing to do, do nothing. */

  if (len == 0)
    {
      return 0;
    }

  /* Thus sayeth the datasheet (p. 38):
   *
   * "Steps for Reading Character Bytes from Character Memory:
   *
   * 1) Write VM0[3] = 0 to disable the OSD image."
   */

  vm0 = __mx7_read_reg__vm0(dev);
  __mx7_write_reg__vm0(dev, vm0 & ~VM0__ENABLE);

  while (len != 0)
    {
      /* "2) Write CMAH[7:0] = xxH to select the character (0-255) to be
       *     read (Figures 10 and 13)."
       *
       * Put another way: CMAH is the row number in the EEPROM.
       */

      cmah = pos / eeprom_cols;
      __mx7_write_reg__cmah(dev, cmah);

      /* "3) Write CMM[7:0] = 0101xxxx to read the character data from the
       *     NVM to the shadow RAM (Figure 13)."
       *
       * They forgot to mention STAT[5], but we remembered it.
       */

      __mx7_read_nvm(dev);

      /* "4) Write CMAL[7:0] = xxH to select the 4-pixel byte (0-63) in
       *     the character to be read (Figures 10 and 13)."
       *
       * That means CMAL is the column number.
       */

      cmal = pos % eeprom_cols;

      /* The shadow RAM is large enough to hold an entire row, so we don't
       * need to go back for another until we've read all of this one.
       */

      do
        {
          __mx7_write_reg__cmal(dev, cmal);

          /* "5) Read CMDO[7:0] = xxH to read the selected 4-pixel byte of
           * data (Figures 11 and 13)."
           */

          *buf = __mx7_read_reg__cmdo(dev);

          /* "6) Repeat steps 4 and 5 to read other bytes of 4-pixel data." */

          buf++;
          pos++;
          len--;
          cmal++;
        }
      while (cmal < eeprom_cols);
    }

  /* "7) Write VM0[3] = 1 to enable the OSD image display." */

  __mx7_write_reg__vm0(dev, vm0);

  return ret;
}

/****************************************************************************
 * Name: mx7_open
 *
 * Description:
 *    The usual open() interface for user accesses.
 *
 * Note: we don't deal with multiple users trying to access this interface at
 * the same time. Until further notice, you probably should just not do that.
 *
 * It's not as simple as just prohibiting concurrent opens or reads with a
 * mutex: there are legit reasons for concurrent access, but they must be
 * treated carefully in this interface lest a partial reader end up with a
 * mixture of old and new side-effects. This will make some users unhappy.
 *
 ****************************************************************************/

static int mx7_open(FAR struct file *filep)
{
  FAR struct inode *inode = filep->f_inode;
  FAR struct mx7_dev_s *dev = inode->i_private;

  /* Reset any leftover CA from a previous operation. */

  dev->ca = 0;

  return 0;
}

/****************************************************************************
 * Name: mx7_read_cm
 *
 * Description:
 *   Reads from Character Memory, the chip's NVM character map.
 *
 ****************************************************************************/

static ssize_t mx7_read_cm(FAR struct file *filep, FAR char *buf, size_t len)
{
  FAR struct inode *inode = filep->f_inode;
  FAR struct mx7_dev_s *dev = inode->i_private;
  ssize_t ret;

  nxmutex_lock(&dev->lock);
  ret = __read_cm(dev, filep->f_pos, (FAR uint8_t *)buf, len);
  nxmutex_unlock(&dev->lock);

  return ret;
}

/****************************************************************************
 * Name: mx7_read
 *
 * Description:
 *   The usual file-operations read() method. I don't know what such an
 *   operation would mean in general, so we do nothing here.
 *
 *   TODO: One idea is to have interfaces allowing the user to discover
 *   details of our capabilities: display size, PAL vs. NTSC, etc., but I
 *   would want to have more experience with other chips before deciding how
 *   to best generalize those things.
 *
 ****************************************************************************/

static ssize_t mx7_read(FAR struct file *filep, FAR char *buf, size_t len)
{
  FAR struct inode *inode = filep->f_inode;
  ssize_t ret = 0;

  /* Which interface are they using? */

  switch (node_from_name(inode->i_name))
    {
    case CM:

      /* Reading from Character Memory (character map). */

      ret = mx7_read_cm(filep, buf, len);
      break;

    default:

      /* Someday we'll have others, I'm sure... */

      break;
    }

  if (ret > 0)
    {
      /* Successful read, so update the file position. */

      filep->f_pos += ret;
    }

  return ret;
}

/****************************************************************************
 * Name: mx7_write_fb
 *
 * Description:
 *   The usual file-operations write() method for the ".../fb" interface.
 *   The user is redirected here by the frontend write() helper defined
 *   below.
 *
 *   We send @len bytes from @buf to the chip's character address array,
 *   starting at the current file position as stored in @filep->f_pos. Users
 *   may adjust this value beforehand by calling seek() in the usual
 *   way. (Position 0 is the upper-left corner of the display window.)
 *
 *   Note: The contents of @buf aren't ASCII data, they're indices into the
 *   chip's onboard NVM character data. (It is possible to make those look
 *   like ASCII data, but that's not generally how the chip is used because
 *   it's a big waste of NVM.)
 *
 * Returned Value:
 *   Returns the number of bytes written, or negative errno.
 *
 ****************************************************************************/

static ssize_t mx7_write_fb(FAR struct file *filep, FAR const char *buf,
                            size_t len)
{
  FAR struct inode *inode = filep->f_inode;
  FAR struct mx7_dev_s *dev = inode->i_private;
  ssize_t ret;

  nxmutex_lock(&dev->lock);
  ret = __write_fb(dev, (FAR uint8_t *)buf, len, dev->ca, filep->f_pos);
  nxmutex_unlock(&dev->lock);

  return ret;
}

/****************************************************************************
 * Name: mx7_write
 *
 * Description:
 *   A "frontend write() helper" that redirects the user's write() request
 *   to the correct handler. We are otherwise an ordinary file-operations
 *   write() function.
 *
 *   We use the approach you see here so that we don't have to have one
 *   distinct function (and a separate file_operations structure) for each of
 *   the many interfaces we're likely to create for interacting with this
 *   chip in its various useful ways. This schema also lets us re-use the
 *   interface code internally (see the test-pattern generator at startup.)
 *
 *   In general, any function we call from here uses the combination of
 *   seek() and write() to implement a zero-copy frame buffer. The seek()
 *   parameter sets the current cursor position, and successive write()s
 *   provide the character data starting at that position.
 *
 *   TODO: At the moment, we have no mechanism for setting the character
 *   attribute (the LBC, BLK, and INV fields in DMM) for the data arriving
 *   here. Fortunately, the default value of '0', asserted in open(), works
 *   for the basic stuff.
 *
 *   The above isn't a hard problem to solve, I just don't need to solve it
 *   right now. And, I don't know what the most convenient solution would
 *   look like: the obvious choice is ioctl(), but I don't like ioctl()
 *   because I can't test it from the command line.
 *
 *   One idea is to have "fb", "blink", "inv", and other entry points for
 *   writing data with specific attributes. That has a nice feel to it,
 *   actually...
 *
 ****************************************************************************/

static ssize_t mx7_write(FAR struct file *filep,
                         FAR const char *buf, size_t len)
{
  FAR struct inode *inode = filep->f_inode;
  ssize_t ret = -EINVAL;

  /* Which interface are they using? */

  switch (node_from_name(inode->i_name))
    {
    case FB:

      /* The "here is some stuff to display" interface */

      ret = mx7_write_fb(filep, buf, len);
      break;

    default:

      /* Someday we'll have others, I'm sure... */

      break;
    }

  if (ret > 0)
    {
      /* Successful read, so update the file position. */

      filep->f_pos += ret;
    }

  return ret;
}

#if defined(DEBUG)

/****************************************************************************
 * Name: uint8_to_hex
 *
 * Description:
 *   Converts an 8-bit integer value to its ascii-hex representation. Values
 *   less than 16 are right-justified and padded with zero.
 *
 * Input parameters:
 *   @n    -  integer value to convert
 *   @buf  -  two-byte buffer to store the converted representation
 *
 * Returned value:
 *   Always returns 2.
 *
 ****************************************************************************/

static int uint8_to_hex(uint8_t n, FAR char *buf)
{
  static FAR const char *hexchar = "0123456789abcdef";

  buf[0] = hexchar[(n >> 4) & 0xf];
  buf[1] = hexchar[n & 0xf];
  return 2;
}

/****************************************************************************
 * Name: hex_to_uint8
 *
 * Description:
 *   Converts a two-byte, ascii-hex string to its integer value.
 *
 * Input parameters:
 *   @buf   -  nul-terminated sequence of ascii-hex string characters
 *
 * Returned value:
 *   Returns the converted value.
 *
 ****************************************************************************/

static int hex_to_uint8(FAR const char *buf)
{
  /* Interpret as hex even without the leading "0x". */

  return strtol(buf, NULL, 16);
}

/****************************************************************************
 * Name: mx7_debug_read
 *
 * Description:
 *   Semi-ordinary file-operations read() method. Returns the value in the
 *   eponymous register, formatted as ascii hex. This allows users to observe
 *   raw hardware register values, like this:
 *
 *      $ cat /dev/osd0/DMM
 *      e5
 *
 *   This same function is used for all registers, which are distinguished
 *   by @filep->f_inode->i_name, i.e. there is a "/dev/osd0/DMM",
 *   "/dev/osd0/VM0", etc., and reads from all of those interfaces arrive
 *   here.
 *
 *   Utilities like cat(1) will exit automatically at EOF, which can be
 *   tricky to deliver at the right time. We achieve this by reading the
 *   associated register value only once, when filep->f_pos is at the
 *   beginning of the "file" we're emulating. The value obtained is stored
 *   in dev->debug[], and we work our way through that and increment the
 *   "file position" accordingly to keep track (because the user may ask for
 *   only one byte at a time, and our register values require two bytes to
 *   express as ascii-hex text).
 *
 *   When we reach the end of dev->debug[], we return EOF. If the user wants
 *   a fresh copy, they can either close and reopen the interface, or move
 *   the file pointer back to 0 via a seek operation.
 *
 ****************************************************************************/

static ssize_t mx7_debug_read(FAR struct file *filep,
                              FAR char *buf, size_t len)
{
  FAR struct inode *inode = filep->f_inode;
  FAR struct mx7_dev_s *dev = inode->i_private;
  FAR const char *name = inode->i_name;
  FAR const char *orig_buf = buf;
  int ret = 0;
  int addr = 0;
  uint8_t val = 0;

  /* If we've already sent them a copy of the register value, don't re-send
   * it until they ask for a fresh one by either reopening the interface, or
   * doing a seek() to reset the cursor. This causes cat(1), etc. to exit
   * nicely.
   */

  if (filep->f_pos >= sizeof(dev->debug))
    {
      return 0;                 /* 0 == "eof" */
    }

  /* Populate the register value "cache" if needed. */

  if (filep->f_pos == 0)
    {
      /* Map the interface name to its associated register address. */

      addr = regaddr_from_name(name);

      /* Read the register. */

      nxmutex_lock(&dev->lock);
      ret = __mx7_read_reg(dev, addr, &val, 1);
      nxmutex_unlock(&dev->lock);

      if (ret != 1)
        {
          return ret;
        }

      /* Save the value to our local cache. */

      uint8_to_hex(val, dev->debug);
    }

  /* Return as many bytes as we have that will fit. */

  while (len-- != 0 && filep->f_pos < sizeof(dev->debug))
    {
      *buf++ = dev->debug[filep->f_pos++];
    }

  return buf - orig_buf;
}

/****************************************************************************
 * Name: mx7_debug_write
 *
 * Description:
 *   Semi-ordinary file-operations write() method, for all debugging
 *   interfaces.
 *
 *   Specifically, we allow users to assert new register values, by sending
 *   us ascii-hex strings:
 *
 *       $  echo 3e > /dev/osd0/VM0
 *
 ****************************************************************************/

static ssize_t mx7_debug_write(FAR struct file *filep, FAR const char *buf,
                               size_t len)
{
  FAR struct inode *inode = filep->f_inode;
  FAR struct mx7_dev_s *dev = inode->i_private;
  FAR const char *name = inode->i_name;

  /* Map the incoming interface name to the associated register address. */

  int addr = regaddr_from_name(name);

  /* Convert from ascii-hex to binary. */

  uint8_t val = hex_to_uint8(buf);

  /* Write the register value. */

  nxmutex_lock(&dev->lock);
  __mx7_write_reg(dev, addr, &val, 1);
  nxmutex_unlock(&dev->lock);

  return len;
}
#endif

/****************************************************************************
 * Name: add_interface
 *
 * Description:
 *   Creates an interface named "@path/@name", and registers it. If @name is
 *   NULL, the interface is named just "@path" instead.
 *
 * Input parameters:
 *   path    - The full path to the interface to register. E.g., "/dev/osd0"
 *   name    - Entry underneath @path (making the latter a directory)
 *   fops    - File operations for the interface
 *   mode    - Access permisisons
 *   private - Opaque pointer to forward to the file operation handlers
 *
 * Returned value:
 *   Zero on success, negative errno otherwise.
 *
 ****************************************************************************/

static int add_interface(FAR const char *path,
                         FAR const char *name,
                         FAR const struct file_operations *fops,
                         mode_t mode, FAR void *private)
{
  char buf[128];

  /* Start with calling @path the interface name. */

  strlcpy(buf, path, sizeof(buf));

  /* Is the interface actually in a directory named @path? */

  if (name != NULL)
    {
      /* Convert @path to a directory name. */

      strlcat(buf, "/", sizeof(buf));

      /* Append the real interface name. */

      strlcat(buf, name, sizeof(buf));
    }

  /* Register the interface in the usual way. NuttX will build the
   * (pseudo-)directory for us.
   */

  return register_driver(buf, fops, mode, private);
}

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

/****************************************************************************
 * Name: max7456_register
 *
 * Description:
 *   Creates awareness of a max7456 chip, and builds a user interface to it.
 *
 * Input parameters:
 *   path    - The full path to the interface to register. E.g., "/dev/osd0"
 *   config  - Configuration information
 *
 * Returned value:
 *   Zero on success, negative errno otherwise.
 *
 ****************************************************************************/

int max7456_register(FAR const char *path, FAR struct mx7_config_s *config)
{
  FAR struct mx7_dev_s *dev = NULL;
  int ret = 0;
  int osdbl = 0;
  int n;

  /* Without config info, we can't do anything. */

  if (config == NULL)
    {
      return -EINVAL;
    }

  /* Initialize the device structure. */

  dev = kmm_malloc(sizeof(struct mx7_dev_s));
  if (dev == NULL)
    {
      return -ENOMEM;
    }

  memset(dev, 0, sizeof(*dev));
  nxmutex_init(&dev->lock);

  /* Keep a copy of the config structure, in case the caller discards
   * theirs.
   */

  dev->config = *config;

  /* Reset the display, to give it a clean initial state. */

  mx7_reset(dev);

  /* Turn the display on. */

  /* Note: we don't _really_ need to lock this, because nobody can see our
   * device yet. But since we're using the lock-requiring functions below,
   * I'm doing it anyway for consistency.
   */

  nxmutex_lock(&dev->lock);

  /* Thus sayeth the datasheet (pp. 38):
   *
   * "The following two steps enable viewing of the OSD image. These steps
   *  are not required to read from or write to the display memory:
   *
   * 1) Write VM0[3] = 1 to enable the display of the OSD image."
   */

  __mx7_write_reg__vm0(dev, VM0__ENABLE);

  /* "2) Write OSDBL[4] = 0 to enable automatic OSD black level control
   *     [Note: there is no "manual" control]. This ensures the correct
   *     OSD image brightness. This register contains 4 factory-preset
   *     bits [3:0] that must not be changed. Therefore, when changing
   *     bit 4, first read OSDBL[7:0], modify bit 4, and then write back
   *     the updated byte."
   */

  osdbl = __mx7_read_reg__osdbl(dev);
  osdbl &= ~OSDBL__DISABLE;
  __mx7_write_reg__osdbl(dev, osdbl);

  /* Create device nodes for the ordinary user interfaces:
   *    /dev/osd0/fb
   *    /dev/osd0/raw
   *    /dev/osd0/vsync
   *    /dev/osd0/cm
   */

  for (n = 0; ret >= 0 && n < NODE_MAP_LEN; n++)
    {
      ret = add_interface(path, node_map[n].path, &g_mx7_fops, 0666, dev);
    }

#if defined(DEBUG)
  /* Add the register-debugging entries. These are device nodes with names
   * that match the associated register, which developers can read or write
   * through to see what the hardware is doing. Not useful in everyday
   * activities.
   */

  for (n = 0; ret >= 0 && n < REG_NAME_MAP_LEN; n++)
    {
      ret = add_interface(path, reg_name_map[n].path, &g_mx7_debug_fops,
                          0666, dev);
    }
#endif

  if (ret < 0)
    {
      snerr("ERROR: Failed to register max7456 interface: %d\n", ret);
      nxmutex_destroy(&dev->lock);
      kmm_free(dev);
      return ret;
    }

#if defined(DEBUG)
  /* For testing, display a test pattern of sorts. When this sequence is
   * longer than 254 bytes, we get a 0xff in the stream; this confirms that
   * __write_fb() can handle that situation properly.
   */

  uint8_t buf[300];
  for (n = 0; n < sizeof(buf); n++)
    {
      buf[n] = n;
    }

  __write_fb(dev, buf, sizeof(buf), 0, 0);
#endif

  /* Release the device to the world. */

  nxmutex_unlock(&dev->lock);

  return 0;
}