/**************************************************************************** * 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 necesssary 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 */ #define SPI_REG_READ 0x80 #define SPI_REG_WRITE 0 /**************************************************************************** * 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 attribute bit field */ DMM__CA__WIDTH = 3, /* (shorthand for lcb, blk, and inv) */ DMM__CLEAR = BIT(2), DMM__VCLEAR = BIT(1), DMM__AUTOINC = BIT(0), DMAH = 5, /* display memory address register, high */ DMAH__ATTR = BIT(1), DMAH__ADDRBIT8__SHIFT = 0, DMAH__ADDRBIT8__WIDTH = 1, DMAL = 6, /* display memory address register, 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 */ CMAH = 9, /* character memory address, high */ CMAH__SHIFT = 0, CMAH__WIDTH = 6, CMAL = 0xa, /* character memory address, 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, /* note: value must be preserved during writes */ OSDBL__PRESET__WIDTH = 4, STAT = 0xa0, /* status (ro) */ STAT__INRESET = BIT(6), /* 1 = chip is performing power-on reset */ STAT__CHARUNAVAIL = BIT(5), /* 1 = character memory unavailable for writes */ STAT__NVSYNC = BIT(4), /* 0 = "active during vertical sync time" */ STAT__NHSYNC = BIT(3), /* 0 = "active during horizontal sync time" */ STAT__LOS = BIT(2), /* 1 = no sync (after 32 missing input video lines) */ STAT__NTSC = BIT(1), /* 1 = ntsc video stream detected */ STAT__PAL = BIT(0), /* 1 = pal video stream detected */ DMDO = 0xb0, /* data memory data out */ /* ro */ CMD0 = 0xc0, /* character memolry 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 ASCII interface (or as best we can manage) */ RAW, /* 16-bit interface in chip's native format */ VSYNC /* blocks until vertical blanking interval */ }; static struct path_name_map_s node_map[] = { PATH_MAP_ENTRY(FB), PATH_MAP_ENTRY(RAW), PATH_MAP_ENTRY(VSYNC) }; #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(CMD0) }; #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; /* current 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 = { #ifndef CONFIG_DISABLE_POLL .poll = NULL, #endif #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 = { #ifndef CONFIG_DISABLE_POLL .poll = NULL, #endif #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) { for (int 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) { for (int 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 * * Return 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. * * Per datasheet, SCLK is always max 10MHz. */ SPI_LOCK(spi, true); SPI_SETMODE(spi, SPIDEV_MODE0); SPI_SETFREQUENCY(spi, 10000000); /* 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 * * Return 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, SPIDEV_MODE0); SPI_SETFREQUENCY(spi, 10000000); /* 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. * * Return 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. * * Return 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_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 */ do { /* If we're here, a reset command has probably just been issued; * wait 100usec before checking, per the datasheet. */ up_udelay(100); stat = __mx7_read_reg__stat(dev); dmm = __mx7_read_reg__dmm(dev); } while ((stat & STAT__INRESET) || (dmm & DMM__CLEAR)); } /**************************************************************************** * Name: __mx7_write_reg__vm0 * * Description: * Writes @val to VM0. A simple helper around __mx7_write_reg(). * * Return 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__osdbl * * Description: * Returns the contents of OSDBL. A simple helper around __mx7_read_reg(). * * Return 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__osdbl * * Description: * Returns the contents of OSDBL. A simple helper around __mx7_read_reg(). * * Return value: * Returns the register value, 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__dmm * * Description: * Returns the contents of DMM. A simple helper around __mx7_read_reg(). * * Return 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(). * * Return 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(). * * Return 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(). * * Return 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: __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); /* Make sure we aren't already in RESET. */ __mx7_wait_reset(dev); /* Issue the reset command. We do this regardless of what we found * above, because by calling this function the user is requesting us * to issue a reset; we don't care if/why the chip might already * have been in reset. */ __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, SPIDEV_MODE0); SPI_SETFREQUENCY(spi, 10000000); 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." * * (We can do this safely even if we aren't in auto-increment mode, so we * don't need to check.) */ 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, DMM__AUTOINC | TO_BITFIELD(DMM__CA, ca)); /* And, finally, we're all done. */ SPI_SELECT(spi, id, false); SPI_LOCK(spi, false); 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 * * 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; FAR struct mx7_dev_s *dev = inode->i_private; UNUSED(inode); UNUSED(dev); return 0; } /**************************************************************************** * 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; /* Which interface are they using? */ switch (node_from_name(inode->i_name)) { case FB : /* The "here is some stuff to display" interface */ return mx7_write_fb(filep, buf, len); case RAW : /* The "raw" interface (unimplemented) */ break; /* return mx7_write_raw(filep, buf, len); */ default: /* Someday we'll have others, I'm sure... */ break; } /* If you get here, we have no idea what you are asking for. */ return -EINVAL; } /**************************************************************************** * 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. */ __mx7_write_reg(dev, addr, &val, 1); 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; /* 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 */ for (int 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 (int 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 (int 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; }