1708 lines
50 KiB
C
1708 lines
50 KiB
C
/****************************************************************************
|
|
* 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;
|
|
}
|