/**************************************************************************** * drivers/video/max7456.c * * Support for the Maxim MAX7456 Single-Channel Monochrome On-Screen * Display with Integrated EEPROM (datasheet 19-0576; Rev 1; 8/08). * * Copyright (C) 2019 Bill Gatliff. All rights reserved. * Author: Bill Gatliff * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * 3. Neither the name NuttX nor the names of its contributors may be * used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * ****************************************************************************/ /**************************************************************************** * 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 #include #include #include #include #include #include #include #include #include #include /**************************************************************************** * Pre-processor Definitions ****************************************************************************/ /* Enables debug-related interfaces. Leave undefined otherwise. */ #define DEBUG 1 /* Sets bit @n */ #define BIT(n) (1 << (n)) /* 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 int mx7_close(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); static int mx7_ioctl(FAR struct file *filep, int cmd, unsigned long arg); #if defined(DEBUG) static int mx7_debug_open(FAR struct file *filep); static int mx7_debug_close(FAR struct file *filep); 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 = { .poll = NULL, #ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS .unlink = NULL, #endif .open = mx7_open, .close = mx7_close, .read = mx7_read, .write = mx7_write, .ioctl = mx7_ioctl }; #if defined(DEBUG) /* Debug-only interface, mostly for direct register access. */ static const struct file_operations g_mx7_debug_fops = { .poll = NULL, #ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS .unlink = NULL, #endif .open = mx7_debug_open, .close = mx7_debug_close, .read = mx7_debug_read, .write = mx7_debug_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_read_reg__dmm * * Description: * Returns the contents of DMM. A simple helper around __mx7_read_reg(). * * Returned value: * Returns the register value, or a negative errno. * ****************************************************************************/ static inline int __mx7_write_reg__dmm(FAR struct mx7_dev_s *dev, uint8_t val) { return __mx7_write_reg(dev, DMM, &val, sizeof(val)); } /**************************************************************************** * Name: __mx7_read_reg__dmdi * * Description: * Returns the contents of DMDI. A simple helper around __mx7_read_reg(). * * Returned value: * Returns the register value, or a negative errno. * ****************************************************************************/ static inline int __mx7_write_reg__dmdi(FAR struct mx7_dev_s *dev, uint8_t val) { return __mx7_write_reg(dev, DMDI, &val, sizeof(val)); } /**************************************************************************** * Name: __mx7_read_reg__dmah * * Description: * Returns the contents of DMAH. A simple helper around __mx7_read_reg(). * * Returned value: * Returns the register value, or a negative errno. * ****************************************************************************/ static inline int __mx7_write_reg__dmah(FAR struct mx7_dev_s *dev, uint8_t val) { return __mx7_write_reg(dev, DMAH, &val, sizeof(val)); } /**************************************************************************** * Name: __mx7_read_reg__dmal * * Description: * Returns the contents of DMAL. A simple helper around __mx7_read_reg(). * * Returned value: * Returns the register value, or a negative errno. * ****************************************************************************/ static inline int __mx7_write_reg__dmal(FAR struct mx7_dev_s *dev, uint8_t val) { return __mx7_write_reg(dev, DMAL, &val, sizeof(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: __lock * * Description: * Locks the @dev data structure (mutex) to protect it against concurrent * access. This is necessary, because @dev has some state information in it * that has to be kept consistent with the chip. This lock also protects * operations that must not be interrupted by other access to the chip. * * Use this function before calling one of the lock-dependent helper * functions defined above (there are some defined below here, too). * ****************************************************************************/ static void inline __lock(FAR struct mx7_dev_s *dev) { nxmutex_lock(&dev->lock); } /**************************************************************************** * Name: __unlock * * Description: * Unlocks the @dev data structure (mutex). * * Use this function after calling one of the lock-dependent helper * functions defined above (there are some defined below here, too). * ****************************************************************************/ static void inline __unlock(FAR struct mx7_dev_s *dev) { nxmutex_unlock(&dev->lock); } /**************************************************************************** * 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) { __lock(dev); /* 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. */ __unlock(dev); } /**************************************************************************** * 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_close * * Description: * The usual file-operations close() method. ****************************************************************************/ static int mx7_close(FAR struct file *filep) { UNUSED(filep); 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; __lock(dev); ret = __read_cm(dev, filep->f_pos, (FAR uint8_t *) buf, len); __unlock(dev); 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; __lock(dev); ret = __write_fb(dev, (FAR uint8_t *) buf, len, dev->ca, filep->f_pos); __unlock(dev); 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; } /**************************************************************************** * Name: mx7_ioctl * * Description: * Does nothing, because I don't like ioctls. * ****************************************************************************/ static int mx7_ioctl(FAR struct file *filep, int cmd, unsigned long arg) { FAR struct inode *inode = filep->f_inode; FAR struct mx7_dev_s *dev = inode->i_private; UNUSED(inode); UNUSED(dev); return -ENOTTY; /* unsupported ioctl */ } #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_open * * Description: * Ordinary file-operations open() for debug-related interfaces. * ****************************************************************************/ static int mx7_debug_open(FAR struct file *filep) { FAR struct inode *inode = filep->f_inode; FAR struct mx7_dev_s *dev = inode->i_private; FAR const char *name = inode->i_name; UNUSED(inode); UNUSED(dev); UNUSED(name); return 0; } /**************************************************************************** * Name: mx7_debug_close * * Description: * Ordinary file-operations close() for debug-related interfaces. * ****************************************************************************/ static int mx7_debug_close(FAR struct file *filep) { return 0; } /**************************************************************************** * 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. */ __lock(dev); ret = __mx7_read_reg(dev, addr, &val, 1); __unlock(dev); 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. */ __lock(dev); __mx7_write_reg(dev, addr, &val, 1); __unlock(dev); 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. */ strcpy(buf, path); /* Is the interface actually in a directory named @path? */ if (name != NULL) { /* Convert @path to a directory name. */ strcat(buf, "/"); /* Append the real interface name. */ strcat(buf, name); } /* 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 = (FAR struct mx7_dev_s *)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. */ __lock(dev); /* 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. */ __unlock(dev); return 0; }