Sunday 27 May 2018

Using an 8 x 7-segment SPI display with the BBC micro:bit


Introduction
I got sent ten (!) of these MAX7219 displays by accident when I had ordered something completely different on Aliexpress, so I thought I'd better learn how to use them. As usual, there are some Arduino libraries for these, and also for the Raspberry Pi but I could only find one example for the BBC micro:bit. That example is a mashup of two different drivers but, when I tried it, I found that quite a lot of things didn't work properly. In any case, I don't really like just plugging in someone else's driver because I prefer to understand what is going on "under the hood". Also, using modules with the micro:bit is a bit of a pain as it's a two-stage process which slows down program development and debugging.

So, armed with the MAX7219 data sheet, I decided to work things out for myself.

Connecting things up
This is very straightforward. The display has five pins. Apart from VCC and GND (it's 3.3v-friendly), there is DIN (data in), CS (chip select) and CLK (clock). As it is an SPI device, the default connections on the micro:bit are CLK=P13 and DIN=P14 (although these are just the defaults - you can use any pins). In addition, you'll need to connect CS to any micro:bit pin (I used P0).

Micropython code
The good news is that testing your connections is easy because the MAX7219 has a test mode which overrides any other settings so it should work every time. The photo above shows "test mode", where each segment of each digit is lit up at full intensity. The MAX7219 is programmed by sending 16 bits, of which the 4 "most significant bits" are ignored, the next 4 bits are the register addresses and the 8 "least significant bits" are the data to send to the registers. Referring to page 10 of the data sheet, to enable the "display test" mode, we need to send the register address 0xXf (where X is "don't care), followed by 0x01. Try this (using the Mu editor):

from microbit import * 
spi.init(baudrate=1000000,bits=8,mode=0, sclk=pin13, mosi=pin15, miso=pin14) #setup SPI
spi.write(b'\x0f\x01')#enable test mode

Whoa! That doesn't work though. The reason is that (to send anything to the display) you need to take the chip select pin (P0) low, then send the data, then take the chip select pin high again. So, this should work:

from microbit import * 
spi.init(baudrate=1000000,bits=8,mode=0, sclk=pin13, mosi=pin15, miso=pin14) # setup SPI
pin0.write_digital(0) # take CS pin low
spi.write(b'\x0f\x01')# enable test mode 
pin0.write_digital(1) # take CS high to latch data

That's the easy bit! The tricky thing I found about using this display is that (apart from "display test" mode), nothing will work as you want it to, unless you make some essential settings. The data sheet explains what each of the registers is for, but it took me a while to realise that nothing much works unless you configure the following registers as below:
  • Test mode (0x0f) set to off (0x00)
  • Decode mode (0x09) set to off (0x00)
  • Set "scan limit" (0x0b) for 8 columns (0x07)
  • Shutdown register (0x0c) enabled (0x01)
  • Intensity register (0x0a) set to something between 1 (0x01 - dim) and 15 (0x0f - very bright)

In addition to that problem, some of the settings are persistent after a power-down, so you also need to clear all the registers to start with (because there is no way to find out what the existing settings are). So, the things to remember are that you need:

  1. Before sending commands/data, to take CS low, then send your commands/data, then take CS high.
  2. To clear all the registers so you start with a "clean slate".
  3. To make the essential settings.
  4. Then to send the data you want to display.
Here's an example program showing how to put it all together. In the example, I have used a python dictionary to hold the bit patterns for all the numbers and most of the letters of the alphabet.




from microbit import *
spi.init(baudrate=1000000,bits=8,mode=0, sclk=pin13, mosi=pin15, miso=pin14) #setup SPI
# initialisation code

pin0.write_digital(0)
spi.write(b'\x0f\x00')#enable normal mode (disable test mode)
pin0.write_digital(1) #latch data
sleep(300)

# zero-out all registers
for cmd in range(16):
    pin0.write_digital(0)
    packet = cmd << 8
    # e.g., if cmd is 0101, 0101 << 8 becomes 010100000000
    spi.write(bytearray(packet))
    pin0.write_digital(1)

# set some essential parameters
pin0.write_digital(0)
spi.write(bytearray([0x09,0x00])) #enable no decode
pin0.write_digital(1)

pin0.write_digital(0)
spi.write(bytearray([0x0b,0x07])) #enable 8 cols
pin0.write_digital(1)

pin0.write_digital(0)
spi.write(bytearray([0x0c,0x01])) # enable shutdown register
pin0.write_digital(1)


# set intensity
pin0.write_digital(0)
spi.write(b'\x0a\x0f')# set intensity to 15 (max)
pin0.write_digital(1) #latch data

# dictionary of bit patterns
# n.b. decimal point can be added to any character by adding 128 to its decimal value
DIGITS = {
    ' ': 0,
    '-': 1,
    '_': 8,
    '\'': 2,
    '0': 126,
    '1': 48,
    '2': 109,
    '3': 121,
    '4': 51,
    '5': 91,
    '6': 95,
    '7': 112,
    '8': 127,
    '9': 123,
    'a': 125,
    'b': 31,
    'c': 13,
    'd': 61,
    'e': 111,
    'f': 71,
    'g': 123,
    'h': 23,
    'i': 16,
    'j' : 24,
    # 'k' Can't represent
    'l': 6,
    # 'm' Can't represent
    'n': 21,
    'o': 29,
    'p': 103,
    'q': 115,
    'r': 5,
    's': 91,
    't': 15,
    'u': 28,
    'v': 28,
    # 'w' Can't represent
    # 'x' Can't represent
    'y': 59,
    'z': 109,
    'A': 119,
    'B': 127,
    'C': 78,
    'D': 126,
    'E': 79,
    'F': 71,
    'G': 94,
    'H': 55,
    'I': 48,
    'J': 56,
    # 'K' Can't represent
    'L': 14,
    # 'M' Can't represent
    'N': 118,
    'O': 126,
    'P': 103,
    'Q': 115,
    'R': 70,
    'S': 91,
    'T': 15,
    'U': 62,
    'V': 62,
    # 'W' Can't represent
    # 'X' Can't represent
    'Y': 59,
    'Z': 109,
    ',': 128,
    '.': 128,
    '!': 176,
}

def write_segs(col,char): # turn on the relevant segments
    pin0.write_digital(0)
    spi.write(bytearray([col,char]))
    pin0.write_digital(1) #latch data

def letter(charx): # Look up character in DIGITS dictionary & return
    value = DIGITS.get(str(charx))
    return value

def blank_cols(): # blank out all columns
for col in range(8):
pin0.write_digital(0)
spi.write(bytearray([col+1,0x00])) # range is 0-7, cols are 1-8!
pin0.write_digital(1)

def write_str(disp_str): #
len_str=len(disp_str) # find length of string
if len_str>8:
len_str=8 #truncate if too long for display
c=len_str #start column
for x in range(len_str):
n=disp_str[x]
write_segs(c,letter(n))
c-=1 #Next (i.e. previous) COLUMN

################################################################ now send some stuff to the display!
while True:
    blank_cols() # blank all columns
    write_str('Hello  ') # write another string
    sleep(2000)
    write_str('there  ') #write a string
    sleep(2000)
    for displ_count in range(1,500): #count up from 0 to 500
        write_str("{0:0=8d}".format(displ_count)) #turn into 8-digit str with leading spaces# 

No comments:

Post a Comment