incubator-nuttx/drivers/video/max7456.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;
}