MAX7456 is a nifty chip that lets you insert fixed-width characters and sprites into a TV signal. Sparkfun has a very useful breakout board for $40, which ends up being much cheaper than the MAX7456 eval board from Maxim priced around $100. You can find Youtube examples of DIY projects that have used the MAX7456, and to get a quick idea of how the display looks, take a peek at the MAX7456 datasheet. In this article, we will look at how to interface an MAX7456 to a PC using a Bus Pirate board.

Max7456Sample

MAX7456 Overview

MAX7456 can be used to inexpensively show text overlaid on a video feed. You can do minimal graphics using sprites. The total display area is 13×30 characters in NTSC and 16×30 characters in PAL feeds. You have a total of 256 characters to choose from. The chip comes pre-programmed with the character set below, but you can reporgram any of the characters as needed. The size of each character is 12 pixels by 18 pixels. You can set each pixel to black or white, or leave it transparent. You can also selectively display just the text or the text overlaid on the video feed. All control communication with the MAX7456 happens over an SPI bus.

Max7456DefaultCharacters

MAX7456 and Bus Pirate : Not Quite Friends

If you look at the MAX7456 datasheets, you will notice that it uses 5V logic, where as the Bus Pirate uses 3.3V logic for communications. The MAX7456 can read SPI communications as long as the High level on the bus is above 2V. But the 5V swing gets completely wiped out by the Bus Pirate when the MAX7456 writes to the MISO line. Anyone with an explanation of the underlying cause, please chime in. It took me the better part of a day with an oscilloscope to figure out that the Bus Pirate was eating up all signals on the MISO line.

The solution is to use a voltage divider on the MISO line. The proper scaling would be 66% of the 5V swing, but even if you use 50%, you will be fine. So, the MISO line hookup looks like this:

MAX7456_BusPirateInterface

Also note the pullup resistor on the Reset line. Without that, the MAX7456 stays powered down. I found that the 5V supply of the Bus Pirate was not delivering enough current to the MAX7456, and so the voltage was dropping to 4.74V, just below the 4.75V minimum the MAX7456 needs to run. So, I had to provide it a separate 5V source.

Configuring MAX7456

The chip doesn’t require any configuration at power up. You can simply write characters to its Display Memory, and turn on OSD using the VM0 SPI register, and you will instantly have something showing up on your screen.

Writing Characters

The Max7456 datasheet is a very well written document. One should have no problems following along to understand how to use the chip. I’m giving a brief outline here and skipping the details. Look up the datasheet to make sense of the register names (VM0, DMM, etc.)

The sequence to follow to write characters is:

  1. Disable OSD (VM0)
  2. Set Display Memory Mode (DMM)
  3. Write most significant bit of display location to DMAH
  4. Write lower 8 bits of display location to DMAL
  5. Write character code to DMDI
  6. Enable OSD (VM0)

The chip allows you to set the attributes for each character location. You can choose the states for Background Control, Blink and Invert. You can write to each location in 2 write modes – 8 bit and 16 bit.
In 16 bit write mode, you only write the character attributes once to DMM and write as many character codes as needed to DMDI (after setting DMAH and DMAL as needed). The attribute in DMM is used for all of the characters written in 16 bit mode.
In 8 bit mode, you write both the character code and attribute to DMDI, but you choose the meaning of the DMDI register using bit 1 of DMAH. When DMAH[1] = 0, DMDI is getting the character code and when DMAH[1] = 1, it’s getting the character attribute.
Below is a Python script that uses the pyBusPirateLite library to write characters to screen. The COM port used for talking to the Bus Pirate is hard coded as COM3. You should change it or make it a command line parameter if you use a different port. Look at the script below for an example of reading the COM port from the command line.

import sys
from pyBusPirateLite.SPI import *
 
import logging
 
class OSDDisplay:
    def __init__(self):
        self.spiPort = None
        self.mapCode = {' ':0, '1':1, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9,
            '0':10, 'A':11, 'B':12, 'C':13, 'D':14, 'E':15, 'F':16, 'G':17, 'H':18, 'I':19,
            'J':20, 'K':21, 'L':22, 'M':23, 'N':24, 'O':25, 'P':26, 'Q':27, 'R':28, 'S':29,
            'T':30, 'U':31, 'V':32, 'W':33, 'X':34, 'Y':35, 'Z':36, 'a':37, 'b':38, 'c':39,
            'd':40, 'e':41, 'f':42, 'g':43, 'h':44, 'i':45, 'j':46, 'k':47, 'l':48, 'm':49,
            'n':50, 'o':51, 'p':52, 'q':53, 'r':54, 's':55, 't':56, 'u':57, 'v':58, 'x':59,
            'y':60, 'z':61, '(':62, ')':63, '.':64, '?':65, ';':66, ':':67, ',':68, '\'':69,
            '/':70, '"':71, '-':72, '<':73, '>':74, '@':75, '\xa9':76
            }
    def InitMax7456(self, comPortName="COM3"):
        # Open Buspirate Port
        if not self.spiPort:
            self.spiPort = SPI(comPortName, 115200)
        else:
            logging.debug("Port already open")
        # endif
        # Enter binmode
        if not self.spiPort.BBmode():
            logging.error("Couldn't start Bitbang Mode")
            #self.spiPort.port.close()
            #self.spiPort = None
            return False
        # endif
        # Enter SPI mode
        if not self.spiPort.enter_SPI():
            logging.error("Couldn't start SPI Mode")
            #self.spiPort.port.close()
            #self.spiPort = None
            return False
        # endif
        # Set speed (125 KHz)
        #self.spiPort.set_speed(SPISpeed._125KHZ)
        #self.spiPort.set_speed(SPISpeed._250KHZ)
        self.spiPort.set_speed(SPISpeed._1MHZ)
        # Configure SPI mode for Max7456
        self.spiPort.cfg_spi(SPICfg.OUT_TYPE) # Output mode Normal (3.3V) rather than HiZ
        # Set SPI timeout
        self.spiPort.timeout(0.05)
        return True
 
    def ResetBP(self):
        self.spiPort.resetBP()
        self.spiPort.port.close()
        self.spiPort = None
 
    def ShowString(self, strText, posX, posY, enableDisplay = True):
        """ Prints strText at location (posX, posY), with starting location being (0,0)
        """
        if self.spiPort is None:
            logging.error("Max7456 communication not initialized")
            return False
        # endif
        # Convert string to character code
        arrCode = []
        for ch in strText:
            if self.mapCode.has_key(ch):
                arrCode.append(self.mapCode[ch])
            else:
                arrCode.append(0x00)
            # endif
        # end for
        arrCode.append(0xff)
        # Turn off OSD
        self.spiPort.CS_Low()
        self.spiPort.bulk_trans(2, [0x00, 0x00])
        self.spiPort.CS_High()
        # Write characters to display memory
        startIndex = posY * 30 + posX
        self.spiPort.CS_Low()
        self.spiPort.bulk_trans(2, [0x05, 0x00]) # Position MSB = 0
        self.spiPort.CS_High()
        self.spiPort.CS_Low()
        self.spiPort.bulk_trans(2, [0x06, startIndex]) # Position to start writing at
        self.spiPort.CS_High()
        self.spiPort.CS_Low()
        self.spiPort.bulk_trans(2, [0x04, 0x01]) # 16 bit operation, autoincrement
        self.spiPort.CS_High()
        logging.debug("Array: %s" % repr(arrCode))
        for code in arrCode:
            # self.spiPort.CS_Low()
            # self.spiPort.bulk_trans(2, [0x06, startIndex]) # Position to start writing at
            # self.spiPort.CS_High()
            # startIndex += 1
            self.spiPort.CS_Low()
            self.spiPort.bulk_trans(1, [code]) # character code
            self.spiPort.CS_High()
        # end for
        # Turn on OSD if needed
        if enableDisplay:
            self.spiPort.CS_Low()
            self.spiPort.bulk_trans(2, [0x00, 0x08]) # Enable OSD
            self.spiPort.CS_High()
        else:
            self.spiPort.CS_Low()
            response = self.spiPort.bulk_trans(2, [0x81, 0x00]) # Read VM1
            self.spiPort.CS_High()
            logging.info("VM1 = {0:08b}".format(ord(response[1])))
        # endif
 
    def ShowChars(self, arrChars, posX, posY, enableDisplay = True):
        """ Prints strText at location (posX, posY), with starting location being (0,0)
        """
        if self.spiPort is None:
            logging.error("Max7456 communication not initialized")
            return False
        # endif
        # Convert string to character code
        arrCode = [char for char in arrChars]
        arrCode.append(0xff)
        # Turn off OSD
        self.spiPort.CS_Low()
        self.spiPort.bulk_trans(2, [0x00, 0x00])
        self.spiPort.CS_High()
        # Write characters to display memory
        startIndex = posY * 30 + posX
        self.spiPort.CS_Low()
        self.spiPort.bulk_trans(2, [0x05, 0x00]) # Position MSB = 0
        self.spiPort.CS_High()
        self.spiPort.CS_Low()
        self.spiPort.bulk_trans(2, [0x06, startIndex]) # Position to start writing at
        self.spiPort.CS_High()
        self.spiPort.CS_Low()
        self.spiPort.bulk_trans(2, [0x04, 0x01]) # 16 bit operation, autoincrement
        self.spiPort.CS_High()
        logging.debug("Array: %s" % repr(arrCode))
        for code in arrCode:
            self.spiPort.CS_Low()
            self.spiPort.bulk_trans(1, [code]) # character code
            self.spiPort.CS_High()
        # end for
        # Turn on OSD
        if enableDisplay:
            self.spiPort.CS_Low()
            self.spiPort.bulk_trans(2, [0x00, 0x08]) # Enable OSD
            self.spiPort.CS_High()
        else:
            self.spiPort.CS_Low()
            response = self.spiPort.bulk_trans(2, [0x81, 0x00]) # Read VM1
            self.spiPort.CS_High()
            logging.info("VM1 = {0:08b}".format(ord(response[1])))
        # endif
 
if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
 
    displayHandler = OSDDisplay()
    if not displayHandler.InitMax7456("COM3"):
        # Reset and try again
        displayHandler.ResetBP()
        logging.error("BusPirate not responding")
    else:
        displayHandler.ShowString("Bus Pirate is the Dude!!", posX = 2, posY = 1, enableDisplay = True)
    # endif

Programming Character Bitmaps

You program the character bitmaps over the same SPI bus by writing 54 bytes of data to a shadow RAM, and then committing it to flash. To build the character bitmaps, you can use the very handy MAX7456EVKit software. The software is available for free download from Maxim’s website. You can edit the existing character set from the file Defaultcm.mcm and save it to a different filename. Then, you can use the script below to upload the character map to the chip. Mind you, the communication is very slow, so it may take you 15 minutes to transfer all the characters. The script doesn’t have much error handling, so it assumes that you have a valid MCM file generated by the EVKit software, with all 256 characters available in the file. You have to supply the MCM filename and the COM port at the command line to run the script.

import sys, optparse
from pyBusPirateLite.SPI import *
 
import logging
 
class MAX7456Writer:
    def __init__(self, comPortName = "COM3"):
        self.spiPort = None
        self.comPortName = comPortName
 
    def InitMax7456(self):
        # Open Buspirate Port
        if not self.spiPort:
            self.spiPort = SPI(self.comPortName, 115200)
        else:
            logging.debug("Port already open")
        # endif
        # Enter binmode
        if not self.spiPort.BBmode():
            logging.error("Couldn't start Bitbang Mode")
            #self.spiPort.port.close()
            #self.spiPort = None
            return False
        # endif
        # Enter SPI mode
        if not self.spiPort.enter_SPI():
            logging.error("Couldn't start SPI Mode")
            #self.spiPort.port.close()
            #self.spiPort = None
            return False
        # endif
        # Set speed (125 KHz)
        #self.spiPort.set_speed(SPISpeed._125KHZ)
        self.spiPort.set_speed(SPISpeed._1MHZ)
        # Configure SPI mode for Max7456
        self.spiPort.cfg_spi(SPICfg.OUT_TYPE) # Output mode Normal (3.3V) rather than HiZ
        # Set SPI timeout
        self.spiPort.timeout(0.3)
        return True
 
    def ResetBP(self):
        self.spiPort.resetBP()
        self.spiPort.port.close()
        self.spiPort = None
 
    def WriteCharactersToMemory(self, inputFile):
        """ Writes characters to MAX7456 character memory from given MCM file
        """
        # Open file
        try:
            input = open(inputFile, 'r')
        except IOError:
            logging.error("Cannot open file [{0}]".format(inputFile))
            return
        # end try
        # Read first line to verify it's MCM
        line = input.readline()
        line = line.strip()
        if line != "MAX7456":
            logging.error("{0} not an MCM file".format(inputFile))
            input.close()
            return
        # endif
        self.spiPort.CS_Low()
        self.spiPort.bulk_trans(2, [0x00, 0x00]) # disable OSD
        self.spiPort.CS_High()
        for i in range(256):
            sys.stdout.write(".")
            self.spiPort.CS_Low()
            self.spiPort.bulk_trans(2, [0x09, i]) # select character number
            self.spiPort.CS_High()
            # Read 54 lines for character data
            for j in range(54):
                line = input.readline().strip()
                val = int(line, 2) # convert binary string to integer
                self.spiPort.CS_Low()
                self.spiPort.bulk_trans(2, [0x0A, j]) # write pixel group number
                self.spiPort.CS_High()
                self.spiPort.CS_Low()
                self.spiPort.bulk_trans(2, [0xB, val]) # write pixel data
                self.spiPort.CS_High()
            # end for
            # Read 10 dummy lines
            for j in range(10):
                input.readline()
            # end for
            self.spiPort.CS_Low()
            self.spiPort.bulk_trans(2, [0x08, 0xA0]) # commit to NVM
            self.spiPort.CS_High()
        # end for
 
if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG)
 
    if len(sys.argv) < 2:
         logging.error("No character file provided")
     else:
         inputFile = sys.argv[1]
         if len(sys.argv) > 2:
            comPort = sys.argv[2]
        else:
            comPort = "COM3"
        # endif
    #endif
    charWriter = MAX7456Writer(comPort)
    if not charWriter.InitMax7456():
        # Reset and try again
        charWriter.ResetBP()
        logging.error("BusPirate not responding")
    else:
        charWriter.WriteCharactersToMemory(inputFile)
    # endif
Note: Refer to my previous post “Bus Pirate Interfacing using pyBusPirateLite – A Tutorial” for more details on using pyBusPirateLite. The scripts above need to be in the same directory where you have pyBusPirateLite (look for the file spi_test.py – these scripts go in the same directory).