Friday 31 March 2017

Driving Adafruit I2C 8x8 LED matrices with the BBC micro:bit

Introduction

I bought a couple of Adafruit 8x8 displays in a sale and tried them out with various microcontrollers. This post is about using them with the BBC micro:bit. It assumes you have already assembled the display by soldering the LED matrix to the backpack. If not, refer to this Adafruit guide.



They consist of an 8x8 LED matrix 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 LED matrices running from the same micro:bit,and controlled separately.

Connecting things up

The first step is to connect the display to the micro:bit. 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) but I used a separate power supply for the display as it will struggle to light up all the pixels if you draw power from the micro:bit supply. Don't forget to connect the earth (- supply) connections on the two power sources togther (i.e. connect an extra wire from -ve on the backpack to -ve on the micro:bit edge connector). 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

I was happy to find that deshipu from the MicroPython forum had already ported a Python library for the HT16K33 LED driver/controller to work with MicroPython. This library allows the same I2C backpack to be used with three different display types. You will need to copy the library to the users\mu_code folder on your PC.

Here's some code I used to test the various functions that the display uses. It clears the display, then fills it again, then dims the display in steps and clears it again. Then it draws a "smiley face" (using the byte array image() expressed in binary to make it more obvious which pixels should be lit up) before flashing the display on and off before finally clearing it again.

You can design your pattern on a piece of paper and put a '1 where you want the pixel lit and a '0' where you want a pixel off. Once you have got the pattern you want, you could convert the binary to hex to save space as Micropython on the micro:bit doesn't leave much space for programs/data.


from microbit import i2c, sleep
from ht16k33 import Matrix8x8
# address of HT16K33 is 0x70
display = Matrix8x8(i2c, address=0x70)

# Clear the display
display.fill(0x00)
display.show()
sleep(1000)

# Fill the display
display.fill(0xff)
display.show()
sleep(1000)

#step through some different brightness levels
display.brightness(1)
sleep(3000)
display.brightness(7)
sleep(3000)
display.brightness(15)
sleep(3000)


# Clear the display
display.fill(0x00)
display.show()
sleep(1000)

# define the bitmap image to be displayed  (in this case a smiley face)
image = (
    0b01111110,
    0b10000001,
    0b10100101,
    0b10000001,
    0b10100101,
    0b10011001,
    0b10000001,
    0b01111110,
)

#draw the image
for y, line in enumerate(image):
    for x in range(8):
        if line & (1 << x):
            display.pixel(y, x, 1)
display.show()
sleep(3000)
# Blink the display
display.blink_rate(0x83)
sleep(3000)
# Stop blinking?
display.blink_rate(0x81)
sleep(3000)

# Clear the display
display.fill(0x00)
display.show()
sleep(1000)


Getting it onto the micro:bit is a two-stage process. First "flash" the test code to the micro:bit in the usual way using Mu. This will result in an error as the required library is not on the micro:bit yet. Once the error message has finished scrolling on the micro:bit's display, press the "Files" button in Mu. If you don't see that button, you probably have an old version of Mu, so replace it with the current version. This will open up a new window below the code window. Provided you have put the library file in the mu_code folder on your PC, you should see it in the right-hand panel in Mu (along with any other files in your mu_code folder). Using the mouse, drag it and drop it into the left-hand window. This should copy it to the micro:bit. Now, if you disconnect the programming cable, power the micro:bit off and on again and the code should run.


Saturday 18 March 2017

Driving DotStar APA102 LED strings with the BBC micro:bit

Introduction


Although NeoPixels are very popular and relatively cheap nowadays, there is another type of individually-addressable LED string that offers some distinct advantages.



The APA102 and APA102C LEDs are small super-bright RGB LED lighting modules. Each LED is a single light source and LEDs can be chained to create light strips of varying length. Light strips are commonly available in 10, 20, 30 and 60 LED lengths (and also in other form factors such as the Pimoroni Blinkt). Strips can be cut and joined as desired. These LEDs are marketed by Adafruit under the name 'DotStar'. The APA102 and APA102C modules use an industry standard SPI interface. This is significantly easier to use than the single wire interface of the NeoPixel (WS2812) and other similar modules (all of which require specific interface timing which can be difficult to achieve).

By using SPI, the APA102 offers much faster data and PWM rates than NeoPixels - which allows "persistence of vision" effects. The higher data rate is particularly noticeable on long strings of LEDs.

Since I already had some APA102s, I decided to try them out with the BBC micro:bit. The only example I found on the Internet was a re-write of the Pimoroni Python library. However, it just uses "bit banging" to switch the LEDs on and off, and, whilst this was just about acceptable on a Blinkt with 8 LEDs, it was very slow on a long string of 30 LEDs. I decided to write my own Python code using the BBC micro:bit's built-in SPI support. I'm no Python expert so I expect the examples below could be improved but they should demonstrate the basic principles.

After a bit of research, I discovered what is needed to drive an APA102 device. Lighting data is sent to the LED strip by sending a 'start frame', brightness data for each LED and then an 'end frame'.

The 'start frame' is 32 zero bits sent as four zero value bytes.

00000000 00000000 00000000 00000000

The brightness data for each LED consists of four bytes, a start byte which has its three most significant bits set and the least significant five bits setting the overall brightness, then three bytes which set the intensity of the LED's blue, green and red brightness; 0 is off, 255 ($FF) is fully on.

111xxxxx bbbbbbbb gggggggg rrrrrrrr

The first byte of the LED's data, the overall brightness setting, has a byte value of 224 ($E0) to 255 ($FF); this allows for 32 levels of brightness from lowest to maximum respectively. It is reported that, on some devices, LED flicker may be more noticeable when the overall brightness is set less than maximum and, to avoid this, it is recommended to send $FF as the brightness setting and solely control the LED brightness by adjusting the blue, green and red byte values.

The 'end frame' is 32 one bits sent as four 255 ($FF) value bytes. Because of the way the LEDs pass data through themselves, it is necessary to send additional clock pulses to latch the sent data into the final LED. Sending the 'end frame' provides for this.

11111111 11111111 11111111 11111111

An 'end frame' needs to be sent for every set of 64 LEDs in the light strip. A light strip of 1 to 64 LEDs requires a single 'end frame', a light strip of 65 to 128 LEDs require two, and so on.

Note that if the LED count for a strip is incorrectly set the 'end frame' is equivalent to setting a LED fully on. It is therefore advised to ensure the 'end frame' is not sent before all data for the entire light strip is sent or else this will set a LED within the light strip fully on.

If the 'end frame' is not sent, the last LED in a light strip may not have its colour or brightness updated until the next set of brightness data is sent to the light strip.

Connecting things up

APA102s normally have four connections:
  • 5v
  • GND
  • Clock
  • Data
Note that the BBC micro:bit is a 3.3V device and, in any case, its outputs cannot provide enough current to light the LEDs so a separate 5v power source should be provided (with a common GND connection to the micro:bit). LED strips can be chained together so the clock and data signals should be provided to the correct end of the strip (this is normally marked using arrows on the strip itself).

In this situation, the micro:bit will be the SPI Master and the APA102 will be the SPI Slave. On the micro:bit, the SPI connections are therefore:
  • Clock (SCK) - pin P13
  • Data out (MOSI) - pin P15
I used a small breadboard to make the connections. In the case of the Blinkt (or other Raspberry Pi versions of the APA102, I plugged the Blinkt into a "cobbler" inserted into the breadboard and used wires to connect to the micro:bit's pins (you'll need an edge connector from Kitronik or elsewhere to access P13 and P15). After I had written this, deshipu on the MicroPython forum pointed out that you don't need to use P13 and P15 for the SPI connections - it's actually just a convention - and you can use P0, P1 or P2, so the edge connector would not be needed. [Don't forget to change the spi.init() parameters if you do this]



Python code

I used the excellent Mu editor to write the code and flash it to the micro:bit.

Example 1

The first snippet below shows how to switch on one LED in red (change the values of the variables r,g,b for other colours). For testing purposes I used a Blinkt with 8 LEDs (addressed as 0 to 7). You might need to "tweak" the indents if you copy and paste from here.

from microbit import *
# initialise SPI
spi.init(baudrate=1000000,bits=8,mode=0, sclk=pin13, mosi=pin15, miso=pin14) # setup SPI
# number of pixels in the chain
num_pixels = 8
on_pixel = 5 # define which pixel to turn on (e.g. 5)

x = 0xff # full brightness
r = 0xff # red fully on
g = 0x00 # green fully off
b = 0x00 # blue fully off

# start frame
spi.write(b'\x00\x00\x00\x00') #start frame
for i in range(num_pixels): # for each pixel
    spi.write(b'\xff\x00\x00\x00') # all colours off
spi.write(b'\xff\xff\xff\xff') #end frame

# light up LEDs
while True:
    buf=bytearray([x,b,g,r]) #send the colours in reverse order
    sleep(1)
    spi.write(b'\x00\x00\x00\x00') #start frame
    for i in range(num_pixels): #check each value of i
        if i==on_pixel:
           spi.write(buf)
        else:
            spi.write(b'\xff\x00\x00\x00') # off
# end frame
spi.write(b'\xff\xff\xff\xff')

Example 2

In this example, the first four LEDs are set to red, green, blue, white and the rest are set to purple then they are all blinked on and off every second.

from microbit import *
# init
spi.init(baudrate=1000000,bits=8,mode=0, sclk=pin13, mosi=pin15, miso=pin14) #setup SPI
# number of pixels in the chain
num_pixels = 8

#### turn all off ####
# start frame
spi.write(b'\x00\x00\x00\x00') #start frame
for i in range(num_pixels): # for each pixel
    spi.write(b'\xff\x00\x00\x00') # all colours off
spi.write(b'\xff\xff\xff\xff') # end frame

##### light up LEDs ####
while True:
    sleep(1000)
    spi.write(b'\x00\x00\x00\x00') # start frame
    spi.write(b'\xff\x00\x00\xff') # red n.b. colours sent in reverse order (b,g,r)
    spi.write(b'\xff\x00\xff\x00') # green
    spi.write(b'\xff\xff\x00\x00') # blue
    spi.write(b'\xff\xff\xff\xff') # white
    for i in range(4,8,1): #for the last 4 pixels purple
        spi.write(b'\xff\xff\x00\xff') # all colours off
    spi.write(b'\xff\xff\xff\xff') # end frame

#all off again
    sleep(1000)
    spi.write(b'\x00\x00\x00\x00') # start frame

    for i in range(num_pixels):
        spi.write(b'\xff\x00\x00\x00') # all off
    spi.write(b'\xff\xff\xff\xff') # end frame


Example3

This example is best demonstrated on a long strip of LEDs (I have used 30) as it "bounces" a coloured pixel up and down the whole length of the strip. It builds on Example 1 by turning on each pixel in turn, until it reaches the end of the strip, then comes back down again. The two sleep(15) commands can be changed to alter the speed with which the pixel appears to travel up and then back down again. It will obviously still work on a shorter strip but the best effect is on a long strip. If you are feeling adventurous, you might like to experiment with changing the colour as it goes up and down.

from microbit import *
# initialise SPI
spi.init(baudrate=1000000,bits=8,mode=0, sclk=pin13, mosi=pin15, miso=pin14) #setup SPI
# number of pixels in the chain
num_pixels = 30
x = 0xff # brightness control
r = 0xff # red value
g = 0x0f # green value
b = 0xab # blue value
buf=bytearray([x,b,g,r]) # colour mix

#### start with all pixels off ####
spi.write(b'\x00\x00\x00\x00') # start frame
for i in range(num_pixels): # for each pixel
    spi.write(b'\xff\x00\x00\x00') # all colours off
spi.write(b'\xff\xff\xff\xff') # tail

#### now loop up and down ####
while True:
    for j in range(num_pixels): # going up!
    # light up LEDs

        sleep(15)
        spi.write(b'\x00\x00\x00\x00') # start frame
        for i in range(num_pixels): # check each value of i
            if i==j:
                spi.write(buf) # colour
            else:
                spi.write(b'\xff\x00\x00\x00') # off
    spi.write(b'\xff\xff\xff\xff') # end frame
   
    for k in range(num_pixels-1,0,-1): # going down!
        sleep(15)
        spi.write(b'\x00\x00\x00\x00') # start frame
        for i in range(num_pixels): # check each value of i
            if i==k:
                spi.write(buf) # colour
            else:
                spi.write(b'\xff\x00\x00\x00') # off
    spi.write(b'\xff\xff\xff\xff') #end frame





Enjoy!