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

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

#include <nuttx/config.h>

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

#include <nuttx/arch.h>
#include <nuttx/spi/spi.h>
#include <nuttx/lcd/lcd.h>
#include <nuttx/lcd/ssd1351.h>

#ifdef CONFIG_LCD_SSD1351

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

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

/* SSD1351 configuration settings:
 * CONFIG_SSD1351_PARALLEL8BIT - 8-bit parallel interface
 * CONFIG_SSD1351_SPI3WIRE     - 3-wire SPI interface
 * CONFIG_SSD1351_SPI4WIRE     - 4-wire SPI interface
 * CONFIG_SSD1351_SPIMODE      - SPI mode
 * CONFIG_SSD1351_SPIFREQ      - SPI frequency
 * CONFIG_SSD1351_NINTERFACES  - number of physical devices supported
 * CONFIG_SSD1351_XRES         - X resolution
 * CONFIG_SSD1351_YRES         - Y resolution
 * CONFIG_SSD1351_MIRRORX      - mirror along the X axis
 * CONFIG_SSD1351_MIRRORY      - mirror along the Y axis
 * CONFIG_SSD1351_INVERT       - invert the display
 * CONFIG_SSD1351_VDDEXT       - external VDD
 * CONFIG_SSD1351_TRST         - reset period
 * CONFIG_SSD1351_TPRECHG1     - first pre-charge period
 * CONFIG_SSD1351_PERFENHANCE  - enhace display performance
 * CONFIG_SSD1351_CLKDIV       - clock divider
 * CONFIG_SSD1351_OSCFREQ      - oscillator frequency
 * CONFIG_SSD1351_TPRECHG2     - second pre-charge period
 * CONFIG_SSD1351_VPRECHG      - pre-charge voltage level
 * CONFIG_SSD1351_VCOMH        - COM deselect voltage level
 * CONFIG_SSD1351_CONTRASTA    - color A contrast
 * CONFIG_SSD1351_CONTRASTB    - color B contrast
 * CONFIG_SSD1351_CONTRASTC    - color C contrast
 * CONFIG_SSD1351_MSTRCONTRAST - master contrast ratio
 *
 * Required LCD driver settings:
 * CONFIG_LCD_SSD1351          - enables SSD1351 support
 * CONFIG_LCD_MAXPOWER         - maximum power, must be 1
 *
 * Additional LCD driver settings:
 * CONFIG_LCD_LANDSCAPE        - landscape
 * CONFIG_LCD_RLANDSCAPE       - reverse landscape
 * CONFIG_LCD_PORTRAIT         - portrait
 * CONFIG_LCD_RPORTRAIT        - reverse portrait
 *
 * Required SPI driver settings:
 * CONFIG_SPI                  - enables support for SPI
 * CONFIG_SPI_CMDDATA          - enables support for cmd/data selection
 *                               (if using 4-wire SPI)
 *
 * NX settings that must be undefined:
 * CONFIG_NX_DISABLE_16BPP     - disables 16 bpp support
 */

/* Verify that all configuration requirements have been met */

/* Number of bits per pixel */

#ifdef CONFIG_NX_DISABLE_16BPP
#  error "Requires support for 16 bits per pixel"
#endif

/* Max power */

#if CONFIG_LCD_MAXPOWER != 1
#  error "CONFIG_LCD_MAXPOWER should be 1"
#endif

/* 9-bit SPI */

#ifdef CONFIG_SSD1351_SPI3WIRE
#  define SSD1351_SPICMD  0
#  define SSD1351_SPIDATA (1 << 8)
#  define SSD1351_SPIBITS 9
#else
#  define SSD1351_SPIBITS 8
#endif

/* Macro Helpers ************************************************************/

#define SSD1351_MAX(a, b)        ((a) > (b) ? (a) : (b))
#define SSD1351_MIN(a, b)        ((a) < (b) ? (a) : (b))
#define SSD1351_CLAMP(n, a, b)   SSD1351_MIN(SSD1351_MAX(n, a), b)

/* Fundamental Commands *****************************************************/

/* Set column address.  Two data bytes.
 *   Data 1: start address (0-127)
 *   Data 2: end address (0-127)
 */

#define SSD1351_CMD_COLADDR      0x15

/* Set row address.  Two data bytes.
 *   Data 1: start address (0-127)
 *   Data 2: end address (0-127)
 */

#define SSD1351_CMD_ROWADDR      0x75

/* Write data bytes to RAM. */

#define SSD1351_CMD_RAMWRITE     0x5c

/* Read data bytes from RAM. */

#define SSD1351_CMD_RAMREAD      0x5d

/* Set address increment, column address mapping, color sequence,
 * scan direction, COM split, and color depth.
 * One data byte.
 */

#define SSD1351_CMD_ORIENTATION  0xa0
#define SSD1351_ADDRINCHORIZ     0x00 /* Horizontal address increment */
#define SSD1351_ADDRINCVERT      0x01 /* Vertical address increment */
#define SSD1351_REMAPCOL0        0x00 /* Column address 0 mapped to SEG0 */
#define SSD1351_REMAPCOL127      0x02 /* Column address 127 mapped to SEG0 */
#define SSD1351_COLORABC         0x00 /* Color sequence ABC */
#define SSD1351_COLORCBA         0x04 /* Color sequence CBA */
#define SSD1351_SCANFROMCOM0     0x00 /* Scan from COM0 */
#define SSD1351_SCANTOCOM0       0x10 /* Scan to COM0 */
#define SSD1351_SPLITDIS         0x00 /* Disable COM split odd even */
#define SSD1351_SPLITEN          0x20 /* Enable COM split odd even */
#define SSD1351_DEPTH65K         0x00 /* 65k color depth */
#define SSD1351_DEPTH262K1       0x80 /* 262k color depth format 1 */
#define SSD1351_DEPTH262K2       0xc0 /* 262k color depth format 2 */

/* Set vertical scroll by RAM (0-128).  One data byte. */

#define SSD1351_CMD_STARTLINE    0xa1
#define SSD1351_STARTLINE(n)     SSD1351_CLAMP(n, 0, 128)

/* Set vertical scroll by row (0-128).  One data byte. */

#define SSD1351_CMD_OFFSET       0xa2
#define SSD1351_OFFSET(n)        SSD1351_CLAMP(n, 0, 128)

/* Set all pixels off.  No data bytes. */

#define SSD1351_CMD_ALLOFF       0xa4

/* Set all pixels on.  No data bytes. */

#define SSD1351_CMD_ALLON        0xa5

/* Set normal display.  No data bytes. */

#define SSD1351_CMD_NORMAL       0xa6

/* Set inverse display.  No data bytes. */

#define SSD1351_CMD_INVERSE      0xa7

/* Set VDD and interface.  One data byte. */

#define SSD1351_CMD_VDDIFACE     0xab
#define SSD1351_VDDEXT           0x00 /* external VDD */
#define SSD1351_VDDINT           0x01 /* internal VDD */
#define SSD1351_IFACE8BIT        0x00 /* 8-bit parallel */
#define SSD1351_IFACE16BIT       0x40 /* 16-bit parallel */
#define SSD1351_IFACE18BIT       0xc0 /* 18-bit parallel */

/* Set display off (sleep mode on).  No data bytes. */

#define SSD1351_CMD_DISPOFF      0xae

/* Set display on (sleep mode off).  No data bytes. */

#define SSD1351_CMD_DISPON       0xaf

/* Set reset period in DCLKs (5-31) and first pre-charge period
 * in DCLKs (3-15).  One data byte.
 */

#define SSD1351_CMD_TRSTTPRECHG1 0xb1
#define SSD1351_TRST(n)          (SSD1351_CLAMP(n, 5, 31) / 2)
#define SSD1351_TPRECHG1(n)      (SSD1351_CLAMP(n, 3, 15) << 4)

/* Set display performance.  Three data bytes. */

#define SSD1351_CMD_PERF         0xb2
#define SSD1351_PERFNORMAL       0x00 /* Data 1: normal performance */
#define SSD1351_PERFENHANCED     0xa4 /* Data 1: enhanced performance */
#define SSD1351_PERFDATA2        0x00
#define SSD1351_PERFDATA3        0x00

/* Set clock divider (0-10) and oscillator frequency (0-15).
 * One data byte.
 */

#define SSD1351_CMD_DIVFREQ      0xb3
#define SSD1351_CLKDIV(r)        (SSD1351_CLAMP(r, 0, 10) << 0)
#define SSD1351_OSCFREQ(r)       (SSD1351_CLAMP(r, 0, 15) << 4)

/* Set segment low voltage.  Three data bytes. */

#define SSD1351_CMD_VSL          0xb4
#define SSD1351_VSLEXT           0xa0 /* Data 1: external VSL */
#define SSD1351_VSLDATA2         0xb5
#define SSD1351_VSLDATA3         0x55

/* Set GPIO pins.  One data byte. */

#define SSD1351_CMD_GPIO         0xb5
#define SSD1351_GPIODIS          0x00 /* High impedance, disabled */
#define SSD1351_GPIOEN           0x01 /* High impedance, enabled */
#define SSD1351_GPIOLOW          0x02 /* Output low */
#define SSD1351_GPIOHIGH         0x03 /* Output high */
#define SSD1351_GPIO0(n)         (((n) & 3) << 0)
#define SSD1351_GPIO1(n)         (((n) & 3) << 2)

/* Set second pre-charge period in DCLKs (1-15).  One data byte. */

#define SSD1351_CMD_TPRECHG2     0xb6
#define SSD1351_TPRECHG2(n)      SSD1351_CLAMP(n, 1, 15)

/* Set lookup table for grayscale pulse width.  63 data bytes. */

#define SSD1351_CMD_LUT              0xb8

/* Use built-in linear lookup table.  No data bytes. */

#define SSD1351_CMD_LINEARLUT        0xb9

/* Set pre-charge voltage level as a percentage of VCC (20-60).
 * One data byte.
 */

#define SSD1351_CMD_VPRECHG      0xbb
#define SSD1351_VPRECHG(n)       (100 * (SSD1351_CLAMP(n, 20, 60) - 20) / \
                                  (100 * (60 - 20) / 31))

/* Set COM deselect voltage level as a percentage of VCC (72-86).
 * One data byte.
 */

#define SSD1351_CMD_VCOMH        0xbe
#define SSD1351_VCOMH(n)         ((SSD1351_CLAMP(n, 72, 86) - 72) / 2)

/* Set contrast for colors A (0-255), B (0-255), and C (0-255).
 * Three data bytes.
 *   Data 1: color A
 *   Data 2: color B
 *   Data 3: color C
 */

#define SSD1351_CMD_CONTRAST     0xc1
#define SSD1351_CONTRAST(n)      SSD1351_CLAMP(n, 0, 255)

/* Set master contrast ratio in sixteenths (1-16).  One data byte. */

#define SSD1351_CMD_MSTRCONTRAST 0xc7
#define SSD1351_MSTRCONTRAST(n)  (SSD1351_CLAMP(n, 1, 16) - 1)

/* Set multiplex ratio (16-128).  One data byte. */

#define SSD1351_CMD_MUXRATIO     0xca
#define SSD1351_MUXRATIO(n)      (SSD1351_CLAMP(n, 16, 128) - 1)

/* Set command lock.  One data byte. */

#define SSD1351_CMD_LOCK         0xfd
#define SSD1351_UNLOCK           0x12 /* Unlock commands */
#define SSD1351_LOCK             0x16 /* Lock commands */
#define SSD1351_INACCESSIBLE     0xb0 /* Make some commands inaccessible */
#define SSD1351_ACCESSIBLE       0xb1 /* Make some commands accessible */

/* Graphic Acceleration Commands ********************************************/

/* Set horizontal scroll.  Five data bytes.
 *   Data 1: 0x00:      no scrolling
 *           0x01-0x3f: scroll towards SEG127 with 1 column offset
 *           0x40-0xff: scroll towards SEG0 with 1 column offset
 *   Data 2: start row address
 *   Data 3: number of rows to scroll
 */

#define SSD1351_CMD_HSCROLL      0x96
#define SSD1351_HSCROLLDATA4     0x00 /* Data 4 */
#define SSD1351_HSCROLLTEST      0x00 /* Data 5: test mode */
#define SSD1351_HSCROLLNORMAL    0x01 /* Data 5: normal */
#define SSD1351_HSCROLLSLOW      0x02 /* Data 5: slow */
#define SSD1351_HSCROLLSLOWEST   0x03 /* Data 5: slowest */

/* Start horizontal scroll.  No data bytes. */

#define SSD1351_CMD_STARTHSCROLL 0x9e

/* Stop horizontal scroll.  No data bytes. */

#define SSD1351_CMD_STOPHSCROLL  0x9f

/* Color Properties *********************************************************/

/* Display resolution */

#if defined(CONFIG_LCD_LANDSCAPE) || defined(CONFIG_LCD_RLANDSCAPE)
#define SSD1351_XRES         CONFIG_SSD1351_XRES
#define SSD1351_YRES         CONFIG_SSD1351_YRES
#else
#define SSD1351_XRES         CONFIG_SSD1351_YRES
#define SSD1351_YRES         CONFIG_SSD1351_XRES
#endif

/* Color depth and format */

#define SSD1351_BPP          16
#define SSD1351_COLORFMT     FB_FMT_RGB16_565
#define SSD1351_STRIDE       (2 * SSD1351_XRES)
#define SSD1351_PIX2BYTES(p) (2 * (p))

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

/* This structure describes the state of this driver */

struct ssd1351_dev_s
{
  /* Publically visible device structure */

  struct lcd_dev_s          dev;

  /* Private LCD-specific information follows */

#ifdef CONFIG_SSD1351_PARALLEL8BIT
  FAR struct ssd1351_lcd_s *lcd;   /* Contained platform-specific interface */
#elif defined(CONFIG_SSD1351_SPI3WIRE) || defined(CONFIG_SSD1351_SPI4WIRE)
  FAR struct spi_dev_s     *spi;   /* Contained SPI driver instance */
#endif
  uint8_t                   power; /* Current power (backlight) setting */

  /* This is working memory allocated by the LCD driver for each LCD device
   * and for each color plane.  This memory will hold one raster line of
   * data.  The size of the allocated run buffer must therefore be at least
   * (bpp * xres / 8).  Actual alignment of the buffer must conform to the
   * bitwidth of the underlying pixel type.
   *
   * If there are multiple planes, they may share the same working buffer
   * because different planes will not be operate on concurrently.  However,
   * if there are multiple LCD devices, they must each have unique run
   * buffers.
   */

  uint16_t                  runbuffer[SSD1351_XRES];

  /* This is another buffer, but used internally by the LCD driver in order
   * to expand the pixel data into 9-bit data needed by the LCD.  There are
   * some customizations that would eliminate the need for this extra buffer
   * and for the extra expansion/copy, but those customizations would require
   * a special, non-standard SPI driver that could expand 8- to 9-bit data on
   * the fly.
   */

#ifdef CONFIG_SSD1351_SPI3WIRE
  uint16_t                  rowbuffer[SSD1351_STRIDE+1];
#endif
};

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

/* Helpers */

#ifdef CONFIG_SSD1351_PARALLEL8BIT
#define ssd1351_select(priv)
#define ssd1351_deselect(priv)
#elif defined(CONFIG_SSD1351_SPI3WIRE) || defined(CONFIG_SSD1351_SPI4WIRE)
static void ssd1351_select(FAR struct ssd1351_dev_s *priv);
static void ssd1351_deselect(FAR struct ssd1351_dev_s *priv);
#endif

#if defined(CONFIG_SSD1351_PARALLEL8BIT) && !defined(CONFIG_LCD_NOGETRUN)
static void ssd1351_read(FAR struct ssd1351_dev_s *priv, uint8_t cmd,
                         FAR uint8_t *data, size_t datlen);
#endif
static void ssd1351_write(FAR struct ssd1351_dev_s *priv, uint8_t cmd,
                          FAR const uint8_t *data, size_t datlen);

/* LCD Data Transfer Methods */

static int ssd1351_putrun(fb_coord_t row, fb_coord_t col,
                          FAR const uint8_t *buffer, size_t npixels);
static int ssd1351_getrun(fb_coord_t row, fb_coord_t col,
                          FAR uint8_t *buffer, size_t npixels);

/* LCD Configuration */

static int ssd1351_getvideoinfo(FAR struct lcd_dev_s *dev,
                                FAR struct fb_videoinfo_s *vinfo);
static int ssd1351_getplaneinfo(FAR struct lcd_dev_s *dev,
                                unsigned int planeno,
                                FAR struct lcd_planeinfo_s *pinfo);

/* LCD RGB Mapping */

#ifdef CONFIG_FB_CMAP
#  error "RGB color mapping not supported by this driver"
#endif

/* Cursor Controls */

#ifdef CONFIG_FB_HWCURSOR
#  error "Cursor control not supported by this driver"
#endif

/* LCD Specific Controls */

static int ssd1351_getpower(struct lcd_dev_s *dev);
static int ssd1351_setpower(struct lcd_dev_s *dev, int power);
static int ssd1351_getcontrast(struct lcd_dev_s *dev);
static int ssd1351_setcontrast(struct lcd_dev_s *dev, unsigned int contrast);

/* Initialization */

static inline void ssd1351_hwinitialize(FAR struct ssd1351_dev_s *priv);

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

/* This is the standard, NuttX LCD driver object */

static struct ssd1351_dev_s g_lcddev;

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

/****************************************************************************
 * Name: ssd1351_select
 *
 * Description:
 *   Select the SPI, locking and re-configuring if necessary.
 *
 ****************************************************************************/

#if defined(CONFIG_SSD1351_SPI3WIRE) || defined(CONFIG_SSD1351_SPI4WIRE)
static void ssd1351_select(FAR struct ssd1351_dev_s *priv)
{
  FAR struct spi_dev_s *spi = priv->spi;

  /* Select the chip, locking the SPI bus in case there are multiple devices
   * competing for the SPI bus
   */

  gdbg("SELECTED\n");
  SPI_LOCK(spi, true);
  SPI_SELECT(spi, SPIDEV_DISPLAY, true);

  /* Now make sure that the SPI bus is configured for this device (it might
   * have gotten configured for a different device while unlocked)
   */

  SPI_SETMODE(spi, CONFIG_SSD1351_SPIMODE);
  SPI_SETBITS(spi, SSD1351_SPIBITS);
  (void)SPI_HWFEATURES(spi, 0);
  (void)SPI_SETFREQUENCY(spi, CONFIG_SSD1351_SPIFREQ);
}
#endif

/****************************************************************************
 * Name: ssd1351_deselect
 *
 * Description:
 *   De-select the SPI.
 *
 ****************************************************************************/

#if defined(CONFIG_SSD1351_SPI3WIRE) || defined(CONFIG_SSD1351_SPI4WIRE)
static void ssd1351_deselect(FAR struct ssd1351_dev_s *priv)
{
  FAR struct spi_dev_s *spi = priv->spi;

  /* De-select the chip and relinquish the SPI bus */

  gdbg("DE-SELECTED\n");
  SPI_SELECT(spi, SPIDEV_DISPLAY, false);
  SPI_LOCK(spi, false);
}
#endif

/****************************************************************************
 * Name: ssd1351_read
 *
 * Description:
 *   Send a 1-byte command and read datlen data bytes.
 *
 ****************************************************************************/

#if defined(CONFIG_SSD1351_PARALLEL8BIT) && !defined(CONFIG_LCD_NOGETRUN)
static void ssd1351_read(FAR struct ssd1351_dev_s *priv, uint8_t cmd,
                         FAR uint8_t *data, size_t datlen)
{
  FAR struct ssd1351_lcd_s *lcd = priv->lcd;
  size_t i;

  /* Sanity check */

  DEBUGASSERT(priv != NULL);
  DEBUGASSERT((data == NULL && datlen == 0) || (data != NULL && datlen > 0));

  /* Send the command */

  lcd->cmd(lcd, cmd);

  /* Discard the first data read if reading from the display */

  if (cmd == SSD1351_CMD_RAMREAD)
    {
      (void)lcd->read(lcd);
    }

  /* Read all of the data */

  for (i = 0; i < datlen; i++)
    {
      data[i] = lcd->read(lcd);
    }
}
#endif

/****************************************************************************
 * Name: ssd1351_write
 *
 * Description:
 *   Send a 1-byte command followed by datlen data bytes.
 *
 ****************************************************************************/

#ifdef CONFIG_SSD1351_PARALLEL8BIT
static void ssd1351_write(FAR struct ssd1351_dev_s *priv, uint8_t cmd,
                          FAR const uint8_t *data, size_t datlen)
{
  FAR struct ssd1351_lcd_s *lcd = priv->lcd;
  size_t i;

  /* Sanity check */

  DEBUGASSERT(priv != NULL);
  DEBUGASSERT((data == NULL && datlen == 0) || (data != NULL && datlen > 0));

  /* Send the command */

  lcd->cmd(lcd, cmd);

  /* Write all of the data */

  for (i = 0; i < datlen; i++)
    {
      lcd->write(lcd, data[i]);
    }
}
#elif defined(CONFIG_SSD1351_SPI3WIRE)
static void ssd1351_write(FAR struct ssd1351_dev_s *priv, uint8_t cmd,
                          FAR const uint8_t *data, size_t datlen)
{
  size_t i;

  /* Sanity check */

  DEBUGASSERT(priv != NULL);
  DEBUGASSERT((data == NULL && datlen == 0) || (data != NULL && datlen > 0));
  DEBUGASSERT(datlen <= SSD1351_STRIDE);

  /* Copy the command into the line buffer */

  priv->rowbuffer[0] = (uint16_t)cmd | SSD1351_SPICMD;

  /* Copy any data after the command into the line buffer */

  for (i = 0; i < datlen; i++)
    {
      priv->rowbuffer[i+1] = (uint16_t)data[i] | SSD1351_SPIDATA;
    }

  /* Send the line buffer */

  (void)SPI_SNDBLOCK(priv->spi, priv->rowbuffer, datlen+1);
}
#elif defined(CONFIG_SSD1351_SPI4WIRE)
static void ssd1351_write(FAR struct ssd1351_dev_s *priv, uint8_t cmd,
                          FAR const uint8_t *data, size_t datlen)
{
  FAR struct spi_dev_s *spi = priv->spi;

  /* Sanity check */

  DEBUGASSERT(priv != NULL);
  DEBUGASSERT((data == NULL && datlen == 0) || (data != NULL && datlen > 0));

  /* Select command transfer */

  (void)SPI_CMDDATA(spi, SPIDEV_DISPLAY, true);

  /* Send the command */

  (void)SPI_SEND(spi, cmd);

  /* Do we have any data to send? */

  if (datlen > 0)
    {
      /* Yes, select data transfer */

      (void)SPI_CMDDATA(spi, SPIDEV_DISPLAY, false);

      /* Transfer all of the data */

      (void)SPI_SNDBLOCK(spi, data, datlen);
    }
}
#endif

/****************************************************************************
 * Name: ssd1351_setcursor
 *
 * Description:
 *   Set the cursor position.
 *
 ****************************************************************************/

static void ssd1351_setcursor(FAR struct ssd1351_dev_s *priv, uint8_t col,
                              uint8_t row)
{
  uint8_t buf[2];

#if defined(CONFIG_LCD_LANDSCAPE) || defined(CONFIG_LCD_RLANDSCAPE)
  /* Set the column address to the column */

  buf[0] = col;
  buf[1] = SSD1351_XRES - 1;
  ssd1351_write(priv, SSD1351_CMD_COLADDR, buf, 2);

  /* Set the row address to the row */

  buf[0] = row;
  buf[1] = SSD1351_YRES - 1;
  ssd1351_write(priv, SSD1351_CMD_ROWADDR, buf, 2);
#elif defined(CONFIG_LCD_PORTRAIT) || defined(CONFIG_LCD_RPORTRAIT)
  /* Set the column address to the row */

  buf[0] = row;
  buf[1] = SSD1351_YRES - 1;
  ssd1351_write(priv, SSD1351_CMD_COLADDR, buf, 2);

  /* Set the row address to the column */

  buf[0] = col;
  buf[1] = SSD1351_XRES - 1;
  ssd1351_write(priv, SSD1351_CMD_ROWADDR, buf, 2);
#endif
}

/****************************************************************************
 * Name: ssd1351_putrun
 *
 * Description:
 *   This method can be used to write a partial raster line to the LCD:
 *
 * Input Parameters:
 *   row     - Starting row to write to (range: 0 <= row < yres)
 *   col     - Starting column to write to (range: 0 <= col <= xres-npixels
 *   buffer  - The buffer containing the run to be written to the LCD
 *   npixels - The number of pixels to write to the LCD
 *             (range: 0 < npixels <= xres-col)
 *
 ****************************************************************************/

static int ssd1351_putrun(fb_coord_t row, fb_coord_t col,
                          FAR const uint8_t *buffer, size_t npixels)
{
  FAR struct ssd1351_dev_s *priv = &g_lcddev;

  /* Sanity check */

  DEBUGASSERT(buffer != NULL && ((uintptr_t)buffer & 1) == 0 &&
              col >= 0 && col+npixels <= SSD1351_XRES &&
              row >= 0 && row < SSD1351_YRES);

  /* Select and lock the device */

  ssd1351_select(priv);

  /* Set the starting position for the run */

  ssd1351_setcursor(priv, col, row);

  /* Write all of the data */

  ssd1351_write(priv, SSD1351_CMD_RAMWRITE, buffer,
                SSD1351_PIX2BYTES(npixels));

  /* Unlock and de-select the device */

  ssd1351_deselect(priv);

  return OK;
}

/****************************************************************************
 * Name: ssd1351_getrun
 *
 * Description:
 *   This method can be used to read a partial raster line from the LCD.
 *
 * Input Parameters:
 *   row     - Starting row to read from (range: 0 <= row < yres)
 *   col     - Starting column to read from (range: 0 <= col <= xres-npixels)
 *   buffer  - The buffer in which to return the run read from the LCD
 *   npixels - The number of pixels to read from the LCD
 *             (range: 0 < npixels <= xres-col)
 *
 ****************************************************************************/

static int ssd1351_getrun(fb_coord_t row, fb_coord_t col,
                          FAR uint8_t *buffer, size_t npixels)
{
#if defined(CONFIG_SSD1351_PARALLEL8BIT) && !defined(CONFIG_LCD_NOGETRUN)
  FAR struct ssd1351_dev_s *priv = &g_lcddev;

  /* Sanity check */

  DEBUGASSERT(buffer != NULL && ((uintptr_t)buffer & 1) == 0 &&
              col >= 0 && col+npixels <= SSD1351_XRES &&
              row >= 0 && row < SSD1351_YRES);

  /* Select and lock the device */

  ssd1351_select(priv);

  /* Set the starting position for the run */

  ssd1351_setcursor(priv, col, row);

  /* Read all of the data */

  ssd1351_read(priv, SSD1351_CMD_RAMREAD, buffer,
               SSD1351_PIX2BYTES(npixels));

  /* Unlock and de-select the device */

  ssd1351_deselect(priv);

  return OK;
#else
  return -ENOSYS;
#endif
}

/****************************************************************************
 * Name: ssd1351_getvideoinfo
 *
 * Description:
 *   Get information about the LCD video controller configuration.
 *
 ****************************************************************************/

static int ssd1351_getvideoinfo(FAR struct lcd_dev_s *dev,
                                FAR struct fb_videoinfo_s *vinfo)
{
  DEBUGASSERT(dev != NULL && vinfo != NULL);

  vinfo->fmt     = SSD1351_COLORFMT;
  vinfo->xres    = SSD1351_XRES;
  vinfo->yres    = SSD1351_YRES;
  vinfo->nplanes = 1;

  gvdbg("fmt: %u xres: %u yres: %u nplanes: %u\n",
        vinfo->fmt, vinfo->xres, vinfo->yres, vinfo->nplanes);
  return OK;
}

/****************************************************************************
 * Name: ssd1351_getplaneinfo
 *
 * Description:
 *   Get information about the configuration of each LCD color plane.
 *
 ****************************************************************************/

static int ssd1351_getplaneinfo(FAR struct lcd_dev_s *dev,
                                unsigned int planeno,
                                FAR struct lcd_planeinfo_s *pinfo)
{
  FAR struct ssd1351_dev_s *priv = (FAR struct ssd1351_dev_s *)dev;

  DEBUGASSERT(dev != NULL && pinfo != NULL && planeno == 0);

  pinfo->putrun = ssd1351_putrun;
  pinfo->getrun = ssd1351_getrun;
  pinfo->buffer = (uint8_t *)priv->runbuffer;
  pinfo->bpp    = SSD1351_BPP;

  gvdbg("planeno: %u bpp: %u\n", planeno, pinfo->bpp);
  return OK;
}

/****************************************************************************
 * Name: ssd1351_getpower
 *
 * Description:
 *   Get the LCD panel power status
 *   (0: full off - CONFIG_LCD_MAXPOWER: full on).
 *   On backlit LCDs, this setting may correspond to the backlight setting.
 *
 ****************************************************************************/

static int ssd1351_getpower(FAR struct lcd_dev_s *dev)
{
  FAR struct ssd1351_dev_s *priv = (FAR struct ssd1351_dev_s *)dev;

  /* Sanity check */

  DEBUGASSERT(priv != NULL);
  gvdbg("power: %d\n", priv->power);

  return priv->power;
}

/****************************************************************************
 * Name: ssd1351_setpower
 *
 * Description:
 *   Enable/disable LCD panel power
 *   (0: full off - CONFIG_LCD_MAXPOWER: full on).
 *   On backlit LCDs, this setting may correspond to the backlight setting.
 *
 ****************************************************************************/

static int ssd1351_setpower(FAR struct lcd_dev_s *dev, int power)
{
  FAR struct ssd1351_dev_s *priv = (FAR struct ssd1351_dev_s *)dev;

  /* Sanity check */

  DEBUGASSERT(priv != NULL && (unsigned int)power <= LCD_FULL_ON);
  gvdbg("power: %d\n", power);

  /* Select and lock the device */

  ssd1351_select(priv);

  if (power > LCD_FULL_OFF)
    {
      /* Turn the display on */

      ssd1351_write(priv, SSD1351_CMD_DISPON, NULL, 0);
      priv->power = LCD_FULL_ON;
    }
  else
    {
      /* Turn the display off */

      ssd1351_write(priv, SSD1351_CMD_DISPOFF, NULL, 0);
      priv->power = LCD_FULL_OFF;
    }

  /* Unlock and de-select the device */

  ssd1351_deselect(priv);

  return OK;
}

/****************************************************************************
 * Name: ssd1351_getcontrast
 *
 * Description:
 *   Get the current contrast setting (0-CONFIG_LCD_MAXCONTRAST).
 *
 ****************************************************************************/

static int ssd1351_getcontrast(FAR struct lcd_dev_s *dev)
{
  return -ENOSYS;
}

/****************************************************************************
 * Name: ssd1351_setcontrast
 *
 * Description:
 *   Set LCD panel contrast (0-CONFIG_LCD_MAXCONTRAST).
 *
 ****************************************************************************/

static int ssd1351_setcontrast(FAR struct lcd_dev_s *dev,
                               unsigned int contrast)
{
  return -ENOSYS;
}

/****************************************************************************
 * Name: ssd1351_hwinitialize
 *
 * Description:
 *   Initialize the video hardware.
 *
 ****************************************************************************/

static inline void ssd1351_hwinitialize(FAR struct ssd1351_dev_s *priv)
{
  size_t i;
  uint8_t buf[3];

  /* Select and lock the device */

  ssd1351_select(priv);

  /* Unlock most commands */

  buf[0] = SSD1351_UNLOCK;
  ssd1351_write(priv, SSD1351_CMD_LOCK, buf, 1);

  /* Unlock the rest of the commands */

  buf[0] = SSD1351_ACCESSIBLE;
  ssd1351_write(priv, SSD1351_CMD_LOCK, buf, 1);

  /* Turn the display off */

  ssd1351_write(priv, SSD1351_CMD_DISPOFF, NULL, 0);

  /* Set the address increment, the column address mapping, the color
   * sequence, the scan direction, the COM split, and the color depth
   */

  buf[0] = SSD1351_COLORABC | SSD1351_SPLITEN | SSD1351_DEPTH65K;
#if defined(CONFIG_LCD_LANDSCAPE) || defined(CONFIG_LCD_RLANDSCAPE)
  buf[0] |= SSD1351_ADDRINCHORIZ;
#else
  buf[0] |= SSD1351_ADDRINCVERT;
#endif
#if (defined(CONFIG_LCD_LANDSCAPE)  && !defined(CONFIG_SSD1351_MIRRORX)) || \
    (defined(CONFIG_LCD_RLANDSCAPE) &&  defined(CONFIG_SSD1351_MIRRORX)) || \
    (defined(CONFIG_LCD_PORTRAIT)   && !defined(CONFIG_SSD1351_MIRRORY)) || \
    (defined(CONFIG_LCD_RPORTRAIT)  &&  defined(CONFIG_SSD1351_MIRRORY))
  buf[0] |= SSD1351_REMAPCOL0;
#else
  buf[0] |= SSD1351_REMAPCOL127;
#endif
#if (defined(CONFIG_LCD_LANDSCAPE)  && !defined(CONFIG_SSD1351_MIRRORY)) || \
    (defined(CONFIG_LCD_RLANDSCAPE) &&  defined(CONFIG_SSD1351_MIRRORY)) || \
    (defined(CONFIG_LCD_PORTRAIT)   &&  defined(CONFIG_SSD1351_MIRRORX)) || \
    (defined(CONFIG_LCD_RPORTRAIT)  && !defined(CONFIG_SSD1351_MIRRORX))
  buf[0] |= SSD1351_SCANTOCOM0;
#else
  buf[0] |= SSD1351_SCANFROMCOM0;
#endif
  ssd1351_write(priv, SSD1351_CMD_ORIENTATION, buf, 1);

  /* Set the vertical scroll by RAM */

#if (defined(CONFIG_LCD_LANDSCAPE)  && !defined(CONFIG_SSD1351_MIRRORY)) || \
    (defined(CONFIG_LCD_RLANDSCAPE) &&  defined(CONFIG_SSD1351_MIRRORY)) || \
    (defined(CONFIG_LCD_PORTRAIT)   &&  defined(CONFIG_SSD1351_MIRRORX)) || \
    (defined(CONFIG_LCD_RPORTRAIT)  && !defined(CONFIG_SSD1351_MIRRORX))
  buf[0] = SSD1351_STARTLINE(CONFIG_SSD1351_YRES);
#else
  buf[0] = SSD1351_STARTLINE(0);
#endif
  ssd1351_write(priv, SSD1351_CMD_STARTLINE, buf, 1);

  /* Set the vertical scroll by row */

  buf[0] = SSD1351_OFFSET(0);
  ssd1351_write(priv, SSD1351_CMD_OFFSET, buf, 1);

  /* Set the display to normal or inverse */

#ifdef CONFIG_SSD1351_INVERT
  ssd1351_write(priv, SSD1351_CMD_INVERSE, NULL, 0);
#else
  ssd1351_write(priv, SSD1351_CMD_NORMAL, NULL, 0);
#endif

  /* Set the VDD and the interface */

#ifdef CONFIG_SSD1351_VDDEXT
  buf[0] = SSD1351_VDDEXT;
#else
  buf[0] = SSD1351_VDDINT;
#endif
  buf[0] |= SSD1351_IFACE8BIT;
  ssd1351_write(priv, SSD1351_CMD_VDDIFACE, buf, 1);

  /* Set the reset period and the first pre-charge period */

  buf[0] = SSD1351_TRST(CONFIG_SSD1351_TRST) |
           SSD1351_TPRECHG1(CONFIG_SSD1351_TPRECHG1);
  ssd1351_write(priv, SSD1351_CMD_TRSTTPRECHG1, buf, 1);

  /* Set the display performance */

#ifdef CONFIG_SSD1351_PERFENHANCE
  buf[0] = SSD1351_PERFENHANCE;
#else
  buf[0] = SSD1351_PERFNORMAL;
#endif
  buf[1] = SSD1351_PERFDATA2;
  buf[2] = SSD1351_PERFDATA3;
  ssd1351_write(priv, SSD1351_CMD_PERF, buf, 3);

  /* Set the clock divider and the oscillator frequency */

  buf[0] = SSD1351_CLKDIV(CONFIG_SSD1351_CLKDIV) |
           SSD1351_OSCFREQ(CONFIG_SSD1351_OSCFREQ);
  ssd1351_write(priv, SSD1351_CMD_DIVFREQ, buf, 1);

  /* Set the segment low voltage */

  buf[0] = SSD1351_VSLEXT;
  buf[1] = SSD1351_VSLDATA2;
  buf[2] = SSD1351_VSLDATA3;
  ssd1351_write(priv, SSD1351_CMD_VSL, buf, 3);

  /* Set the GPIO pins */

  buf[0] = SSD1351_GPIO0(SSD1351_GPIOLOW) | SSD1351_GPIO1(SSD1351_GPIOLOW);
  ssd1351_write(priv, SSD1351_CMD_GPIO, buf, 1);

  /* Set the second pre-charge period */

  buf[0] = SSD1351_TPRECHG2(CONFIG_SSD1351_TPRECHG2);
  ssd1351_write(priv, SSD1351_CMD_TPRECHG2, buf, 1);

  /* Use the built-in linear lookup table */

  ssd1351_write(priv, SSD1351_CMD_LINEARLUT, NULL, 0);

  /* Set the pre-charge voltage level */

  buf[0] = SSD1351_VPRECHG(CONFIG_SSD1351_VPRECHG);
  ssd1351_write(priv, SSD1351_CMD_VPRECHG, buf, 1);

  /* Set the COM deselect voltage level */

  buf[0] = SSD1351_VCOMH(CONFIG_SSD1351_VCOMH);
  ssd1351_write(priv, SSD1351_CMD_VCOMH, buf, 1);

  /* Set the contrast */

  buf[0] = SSD1351_CONTRAST(CONFIG_SSD1351_CONTRASTA);
  buf[1] = SSD1351_CONTRAST(CONFIG_SSD1351_CONTRASTB);
  buf[2] = SSD1351_CONTRAST(CONFIG_SSD1351_CONTRASTC);
  ssd1351_write(priv, SSD1351_CMD_CONTRAST, buf, 3);

  /* Set the master contrast ratio */

  buf[0] = SSD1351_MSTRCONTRAST(CONFIG_SSD1351_MSTRCONTRAST);
  ssd1351_write(priv, SSD1351_CMD_MSTRCONTRAST, buf, 1);

  /* Set the multiplex ratio */

  buf[0] = SSD1351_MUXRATIO(128);
  ssd1351_write(priv, SSD1351_CMD_MUXRATIO, buf, 1);

  /* Lock some of the commands */

  buf[0] = SSD1351_INACCESSIBLE;
  ssd1351_write(priv, SSD1351_CMD_LOCK, buf, 1);

  /* Set the cursor position */

  ssd1351_setcursor(priv, 0, 0);

  /* Clear the display memory */

  buf[0] = 0;
  buf[1] = 0;
  for (i = 0; i < SSD1351_XRES * SSD1351_YRES; i++)
    {
      ssd1351_write(priv, SSD1351_CMD_RAMWRITE, buf, 2);
    }

  /* Unlock and de-select the device */

  ssd1351_deselect(priv);
}

/****************************************************************************
 * Name: ssd1351_initialize
 *
 * Description:
 *   Initialize the video hardware.  The initial state of the device
 *   is fully initialized, display memory cleared, and ready to use,
 *   but with the power setting at 0 (full off == sleep mode).
 *
 * Input Parameters:
 *   lcd   - A reference to the platform-specific interface.
 *   spi   - A reference to the SPI driver instance.
 *   devno - A value in the range of 0 through CONFIG_SSD1351_NINTERFACES-1.
 *           This allows support for multiple devices.
 *
 * Returned Value:
 *   On success, this function returns a reference to the LCD object for the
 *   specified device.  NULL is returned on failure.
 *
 ****************************************************************************/

#ifdef CONFIG_SSD1351_PARALLEL8BIT
FAR struct lcd_dev_s *ssd1351_initialize(FAR struct ssd1351_lcd_s *lcd,
                                         unsigned int devno)
#elif defined(CONFIG_SSD1351_SPI3WIRE) || defined(CONFIG_SSD1351_SPI4WIRE)
FAR struct lcd_dev_s *ssd1351_initialize(FAR struct spi_dev_s *spi,
                                         unsigned int devno)
#endif
{
  FAR struct ssd1351_dev_s *priv = &g_lcddev;

  /* Sanity check */

#ifdef CONFIG_SSD1351_PARALLEL8BIT
  DEBUGASSERT(lcd != NULL);
#elif defined(CONFIG_SSD1351_SPI3WIRE) || defined(CONFIG_SSD1351_SPI4WIRE)
  DEBUGASSERT(spi != NULL);
#endif
  DEBUGASSERT(devno == 0);

  /* Initialize the driver data structure */

  priv->dev.getvideoinfo = ssd1351_getvideoinfo;
  priv->dev.getplaneinfo = ssd1351_getplaneinfo;
  priv->dev.getpower     = ssd1351_getpower;
  priv->dev.setpower     = ssd1351_setpower;
  priv->dev.getcontrast  = ssd1351_getcontrast;
  priv->dev.setcontrast  = ssd1351_setcontrast;
#ifdef CONFIG_SSD1351_PARALLEL8BIT
  priv->lcd              = lcd;
#elif defined(CONFIG_SSD1351_SPI3WIRE) || defined(CONFIG_SSD1351_SPI4WIRE)
  priv->spi              = spi;
#endif
  priv->power            = LCD_FULL_OFF;

  /* Configure the device */

  ssd1351_hwinitialize(priv);

  return &priv->dev;
}

#endif /* CONFIG_LCD_SSD1351 */
