Friday 7 April 2017

Driving an Adafruit 4x14-segment I2C display with the BBC micro:bit

Introduction

I have previously posted about using  Adafruit 8x8 displays with the BBC micro:bit. I also recently bought an Adafruit 4x14-segment display so I could experiment with displaying text as well as just numbers. Like the 8x8 LED matrix, the 4x14-segment display is soldered onto an I2C "backpack" that contains an HT16K33 LED driver/controller.

The I2C protocol is a serial protocol that allows different data to be sent to different devices along a "bus". Each device must have its own address on the bus. The Adafruit backpack has a default address of 0x70 but this can be changed to one of seven other addresses by soldering across one or more of the three jumpers on the backpack. This would allow you to have several 4x14-segment displays running from the same micro:bit,and controlled separately.

My post about the 8x8 LED matrix provided instructions for using a Micropython library for the HT16K33 to simplify the Python code. However, that library doesn't support the various bit-patterns needed to form each of the 60-plus characters that can be displayed on this particular display, so it doesn't help much. In any case, I find it a slow process using libraries with the micro:bit as you have to upload the Python code first (which generates an error message that takes a long time to disappear) THEN copy the library to the micro:bit and reboot. If you just want to make a small change to your code, this gets quite tedious.

The micro:bit Micropython reference page shows there are only three micro:bit I2C functions. Sending data to an I2C device is not complicated as there is only really one function that you need to know about. Assuming you are not changing the default configuration, you don't need to initialise the I2C device; neither do you normally need to read from a 4x14-segment display, which only leaves the function:
microbit.i2c.write(addr, buf, repeat=False) 
In our case, we can also omit the "repeat=False" parameter. So, all we need to do is call:
i2c.write(0x70, bytearray(buf))
... where 0x70 is the I2C address of the HT16K33 and bytearray(buf) contains the data that you want to send to the HT16K33 LED controller.

The HT16K33 uses a number of command codes to control the display, and an area of RAM to store the segment display data. The main command codes we need are:

0x21  Start oscillator
0x80  Blank the display
0x81  Send the RAM data to the display
0x83  Blink the display @2Hz
0xE0  Dim the display
0xEF  Set brightness to maximum

To display characters, we need to write the data to the HT16K33 RAM and then send the data to the display itself. Since we are always going to send a whole set of data for each of the four segments, we will always preface the data for which LED segments to switch on by the starting address of the RAM on the HT16K33, i.e. 0x0. This is then followed by four two-byte pairs (one pair for each "digit", working from left to right). The second byte for each digit is essentially the traditional 7-segment bit pattern (except that the middle segment is split into two). The first byte consists of the extra segments (e.g. the diagonal ones) needed to form the letters of the alphabet. Then you send the control byte $81 to "latch" the data to the display.
The bit mapping shown above can be represented as two eight-bit binary numbers where A is the least significant bit, as per the sequence below (the first bit isn't used; you can make it 0 or 1). Bytes 1 and 2 are sent in reverse:
    --- byte 2 ---    ,   --- byte 1 ---
0 DP N M L K J H , G2 G1 F E D C B A

To turn on just the A segment, use binary 00000000,00000001 and reverse-> hex 0x1, 0x0
To turn on just the G1 segment, use binary 00000000,01000000 and reverse-> hex 0x40, 0x0
To make "0", turn on segments F E D C B A - use binary 00000000,00111111 and reverse-> hex 0x3F, 0x0
To make "K", turn on segments N K , G1 F E - use binary 00100100,01110000 and reverse-> hex 0x70,0x24

 

Connecting things up

This assumes you have already assembled the display by soldering the two pairs of 14-segment displays to the backpack. If not, refer to this Adafruit page.

The first step is to connect the display to the micro:bit. Compared with the other Adafruit I2C backpack displays, this device has an extra pin which connects to the logic level voltage of the processor you are using (i.e. 3.3v). The display works best at 5v, so a power supply that provides both 5v and 3.3v is ideal. I used a YwRobot MB102 USB breadboard power supply module (pictured). This allows you to power either side of a solderless breadboard with a different regulated voltage by plugging it into a USB port.

You'll need to use an edge-connector board to access the I2C pins (P19 and P20). Some ready-made edge-connectors don't have those two pins soldered in place so you might need to get the soldering iron out. Assuming you have them, connecting is easy (SDA=P20, SCL=P19).  Since I was using solderless breadboard, I used the very neat bread:bit edge connector board from Proto-Pic. Combined with the USB breadboard power supply, this makes a great prototyping setup.

Micropython on the micro:bit automatically enables the internal pullup resistors on the I2C lines, so the usual 5.1k resistors on the SCL and SDA connections are not needed.

Python code

There are two types of command used:
1. Send a control byte to the display to turn it on/off/blink etc. This takes the form
i2c.write(0x70, bytearray([control byte])) 
2 Send character data to the display controller RAM
i2c.write(0x70,([0x0,digit1,digit2,digit3,digit4]))
Here's some code I used to test the various functions that the display uses. It clears the display, then shows all the printable characters in groups of four (some of the lower case letters are not that convincing to be honest). Finally it displays a message that dims and then flashes, followed by succession of "wheel spokes" giving the appearance that they are spinning around. I have included virtually the whole character set for reference, but in practice you would just include the characters that you are going to use.

from microbit import i2c, sleep
# address of HT16K33 is 0x70

sleep(1) # allow time for display to settle after power on/reset
# start oscillator
i2c.write(0x70, bytearray([0x21]))


# To display something:
# send     (i2c device address,([RAM start addr,    four pairs of bytes    ]))
# i2c.write(      0x70        ,([0x0           ,digit1,digit2,digit3,digit4]))
# turn on the display


# Clear the display
i2c.write(0x70, bytearray([0x80]))
sleep(1000)


# data for each character
#   byte1,byte2
#0 = 0x3f,0x0
#1 = 0x6,0x0
#2 = 0xdb,0x0
#3 = 0xcf,0x0
#4 = 0xe6,0x0
#5 = 0xed,0x0
#6 = 0xfd,0x0
#7 = 0x1,0xc
#8 = 0xff,0x0
#9 = 0xc7,0x8
#. = 0x0,0x40
#+ = 0xc0,0x12
#- = 0xc0,0x0
#/ = 0x0,0xc
#| = 0x0,0x10
#\ = 0x0,0x21
#0x = 0x8d,0xc
#all segments = 0x3f,0x3f
#A 0xF7,0x0
#B 0x8F,0x12
#C 0x39,0x0
#D 0xF,0x12
#E 0xF9,0x0
#F 0xF1,0x0
#G 0xBD,0x0
#H 0xF6,0x0
#I 0x9,0x12
#J 0x1E,0x0
#K 0x70,0x24
#L 0x38,0x0
#M 0x36,0x5
#N 0x36,0x21
#O 0x3F,0x0
#P 0xF3,0x0
#Q 0x3F,0x20
#R 0xF3,0x20
#S 0xED,0x0
#T 0x0,0x12
#U 0x3E,0x0
#V 0x30,0xC
#W 0x36,0x28
#X 0x0,0x2D
#Y 0x0,0x15
#Z 0x9,0xC
#a 0x58,0x10
#b 0x78,0x20
#c 0xD8,0x0
#d 0x8E,0x8
#e 0x58,0x8
#f 0x71,0x0
#g 0x8E,0x4
#h 0x70,0x10
#i 0x0,0x10
#j 0xE,0x0
#k 0x0,0x36
#l 0x30,0x0
#m 0xD4,0x10
#n 0x50,0x10
#o 0xDC,0x0
#p 0x70,0x1
#q 0x86,0x4
#r 0x50,0x0
#s 0x88,0x20
#t 0x78,0x0
#u 0x1C,0x0
#v 0x4,0x20
#w 0x14,0x28
#x 0xC0,0x28
#y 0xC,0x28
#z 0x48,0x8

while True:

# Turn on all segments
    i2c.write(0x70,bytearray([0x0,0x3f,0x3f,0x3f,0x3f,0x3f,0x3f,0x3f,0x3f]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep(1000)

# print out the character set four at a time
#0 1 2 3
    i2c.write(0x70, bytearray([0x0,0x3f,0x0,0x6,0x0,0xdb,0x0,0xcf,0x0]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#4 5 6 7
    i2c.write(0x70, bytearray([0x0,0xe6,0x0,0xed,0x0,0xfd,0x0,0x1,0xc]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#8 9 0x all
    i2c.write(0x70, bytearray([0x0,0xff,0x0,0xe7,0x0,0xed,0x12,0x3f,0x3f]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#- / . +
    i2c.write(0x70, bytearray([0x0,0x0,0x40,0xc0,0x12,0xc0,0x0,0x0,0xc]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#A B C D
    i2c.write(0x70, bytearray([0x0,0xF7,0x0,0x8F,0x12,0x39,0x0,0xF,0x12]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#E F G H
    i2c.write(0x70, bytearray([0x0,0xF9,0x0,0xF1,0x0,0xBD,0x0,0xF6,0x0]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#I J K L
    i2c.write(0x70, bytearray([0x0,0x9,0x12,0x1E,0x0,0x70,0x24,0x38,0x0]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#M N O P
    i2c.write(0x70, bytearray([0x0,0x36,0x5,0x36,0x21,0x3F,0x0,0xF3,0x0]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#Q R S T
    i2c.write(0x70, bytearray([0x0,0x3F,0x20,0xF3,0x20,0xED,0x0,0x1,0x12]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#U V W X
    i2c.write(0x70, bytearray([0x0,0x3E,0x0,0x30,0xC,0x36,0x28,0x0,0x2D]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#Y Z all all
    i2c.write(0x70, bytearray([0x0,0x0,0x15,0x9,0xC,0x3f,0x3f,0x3f,0x3f]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#a b c d
    i2c.write(0x70, bytearray([0x0,0x58,0x10,0x78,0x20,0xD8,0x0,0x8E,0x8]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#e f g h
    i2c.write(0x70, bytearray([0x0,0x58,0x8,0x71,0x0,0x8E,0x4,0x70,0x10]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#i j k l
    i2c.write(0x70, bytearray([0x0,0x0,0x10,0xE,0x0,0x0,0x36,0x30,0x0]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#m n o p
    i2c.write(0x70, bytearray([0x0,0xD4,0x10,0x50,0x10,0xDC,0x0,0x70,0x1]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#q r s t
    i2c.write(0x70, bytearray([0x0,0x86,0x4,0x50,0x0,0x88,0x20,0x78,0x0]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#u v w x
    i2c.write(0x70, bytearray([0x0,0x1C,0x0,0x4,0x20,0x14,0x28,0xC0,0x28]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)
#y z
    i2c.write(0x70, bytearray([0x0,0xC,0x20,0x48,0x8,0x0,0x0,0x0,0x0]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep (1000)

# display a message
#                             |   L    |    O   |    V     |   E  
    i2c.write(0x70,bytearray([0x00,0x38,0x0,0x3f,0x0,0x30,0xC,0xf9,0x0]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep(1000)

#                             |   B    |    E    |     E   |   R  
    i2c.write(0x70,bytearray([0x00,0x8F,0x12,0xf9,0x00,0xf9,0x00,0xf3,0x20]))
    i2c.write(0x70, bytearray([0x81])) # display on
    sleep(1000)


    i2c.write(0x70, bytearray([0xE0])) # dim the display (range 0xE0 to 0xEF)
    sleep (1000)
    i2c.write(0x70, bytearray([0xEF])) # full brightness again
    sleep (1000)
    i2c.write(0x70, bytearray([0x83])) # blink the display (0x83=2Hz blink, 0x85=1Hz, 0x87=0.5Hz)
    sleep (3000)

# Clear the display
    i2c.write(0x70, bytearray([0x80]))
    sleep(1000)

# make a spinney-round thing!
    for iterations in range(20):
        i2c.write(0x70, bytearray([0x0,0x0,0x12,0x0,0x12,0x0,0x12,0x0,0x12]))
        i2c.write(0x70, bytearray([0x81])) # display on
        sleep (21)
   
        i2c.write(0x70, bytearray([0x0,0x0,0xc,0x0,0xc,0x0,0xc,0x0,0xc]))
        i2c.write(0x70, bytearray([0x81])) # display on
        sleep (20)
   
        i2c.write(0x70, bytearray([0x0,0xc0,0x0,0xc0,0x0,0xc0,0x0,0xc0,0x0]))
        i2c.write(0x70, bytearray([0x81])) # display on
        sleep (20)
   
        i2c.write(0x70, bytearray([0x0,0x0,0x21,0x0,0x21,0x0,0x21,0x0,0x21]))
        i2c.write(0x70, bytearray([0x81])) # display on
        sleep (20)

# turn off the display
    i2c.write(0x70, bytearray([0x80]))
    sleep(1000)