Raspberry Pi Pico Controlled 64x32 LED Matrix (CircuitPython)

by devincej in Circuits > Microcontrollers

6605 Views, 10 Favorites, 0 Comments

Raspberry Pi Pico Controlled 64x32 LED Matrix (CircuitPython)

IMG_0593.jpeg
IMG_0594.jpg

This is a simple way to control an LED matrix using a raspberry pi pico. While using this board is a more limited approach, it is a cheap way to make a basic display. This project is just a loose framework for what you can do with LED matrices, so it's limited to displaying simple images and text. If you want more memory, better animation, or network connection then I would recommend looking for an approach that uses a header to connect a microcontroller to the matrix (this also makes powering the device slightly easier). I chose to create a box to house the wiring and board, and used an mpr121 capacitive touch sensor to create "buttons".

Supplies

017E69A1-D801-46B2-9F3D-5F6AE69674BD_1_105_c.jpeg
424E710E-6CF9-42B1-9D97-4046DD3A4348_1_105_c.jpeg
5C83B03E-754C-4C50-A28C-66AF4B6B7ED1_1_105_c.jpeg
99DF9415-FB4B-4BD5-98BA-D23D1E762537_1_105_c.jpeg
IMG_0591.jpeg
IMG_0592.jpeg

Materials:

Tools:

  • Wire cutters
  • A screwdriver

*I purchased this one from Amazon which came with the indented cables. This is the simplest option for LED matrices, I think this is the basic component for higher functionality matrices sold by Adafruit or SparkFun. Those boards usually either come with a built in header, or have a recommended header board which essentially just makes it so that the matrix and microcontroller are easily connected and powered from the same source. These boards also may require soldering.

**I designed a simple box using MakerCase, but any other housing would work.

Powering the Matrix

FECA4979-4B7A-4A28-B9F8-5CB12483E8B7_1_105_c.jpeg
998827B0-6F93-4470-A5ED-AED33AB73F88_1_105_c.jpeg

The matrix I purchased from the link above comes with a power cable that has two four pin power connectors. You could connect it to any 5v source of power just like that, however it's probably easier to do what I did and clip off one of the 4 pin connectors. Clip each of the 4 wires to the desired length, strip the ends, and then twist the exposed ends of the same colored wires together. Using the screwdriver, loosen the terminals of the DC adaptor, insert the exposed wires, then retighten. Plug the 4 pin adaptor into the LED matrix, then all you need is to connect to a 5v power source. It's important that you make sure the power source is only 5v, more may damage the matrix.

Create a Box (optional)

5A9341B6-5607-4B93-83C4-52B741AE9AF9_1_105_c.jpeg
609D756E-2434-4228-806E-1FF153EA494F_1_105_c.jpeg

I used MakerCase to design a box to house the project and Adobe Illustrator to create holes in the top and side for wires to go through. I then used a laser cutter to cut the sides with finger joints and glued them together except for the top, so there's still access. This is just the way I chose to make a box, any other design or pre-made box you choose would work just as well.

Install CircuitPython

I used CircuitPython to make this project. Follow the steps on this link to install CircuitPython on the Pico. If you're using a different board just find that board under the "Downloads" tab of that link.

Connect Pico to Matrix

Pins.jpg
raspberry-pi-pico-pinout-featured-image.jpg
65EC752C-BCC5-402C-8A93-6792D7176041_1_105_c.jpeg
F0AD1DE9-B604-45E2-8AAC-034F42B65D57_1_105_c.jpeg

This step is slightly complicated because there are a lot of wires. It's helpful to find multicolored jumper wires to make it easier to keep track of the connections to the HUB75 cable. The attached table shows the color of the HUB75 wire in the included cable, the pin they connect to on the LED matrix, and then the pin I connected it to on the pico. The I2C connection is simple, you connect the red wire to one of the 3.3v pins, the other to a ground, then connect the SCL and SDA to any pair of pins with those labels in the pinout image below (I used GP17 for SCL and GP16 for SDA).

Chain Matrices (Optional)

The matrix comes with both HUB75 input and output ports. The output port can be connected to another matrix to create larger displays. I did not attempt to do this, but there were plenty of resources online if you'd like to try and figure out how to use this capability.

Import Modules

The code I used needed the following modules:

board, digitalio, adafruit_mpr121, busio, time, displayio, rgbmatrix, framebufferio, adafruit_imageload, terminalio, random, math, adafruit_display_text

If you use different code you may need different libraries. If your board is missing some of these (if it's new it will be) all you need to do to add them is to download the folder from this link, find the file or folder with the same name as the module in the folder, then copy and paste that file into the "lib" folder on your board.

Initialize I2C and Matrix

The code I created really only added capacitive touch capability to a number of animations I found online. There are plenty of resources on the CircuitPython website and other related forums to learn how to create your own displays, but this can be a bit advanced if you aren't experienced in programming. Also, it's not necessary to use the touch sensor, that's just what I wanted. You could use any sensor of your choosing to add functionality, such as a proximity sensor to make a motion detecting display. The most important components of the code are the I2C and matrix initialization, which on my pico were as followed.

displayio.release_displays()

matrix = rgbmatrix.RGBMatrix(

  width=64, bit_depth=2,

  rgb_pins=[board.GP0, board.GP1, board.GP2, board.GP3, board.GP4, board.GP5],

  addr_pins=[board.GP6, board.GP7, board.GP8, board.GP9],

  clock_pin=board.GP10, latch_pin=board.GP12, output_enable_pin=board.GP13)

display = framebufferio.FramebufferDisplay(matrix)


i2c = busio.I2C(board.GP17, board.GP16)

touch_pad = adafruit_mpr121.MPR121(i2c)


Make sure that the displayio.release_displays() command is the first line below the modules, as this prevents some errors from occurring.

For anyone using a different board here are some of the above lines with the corresponding HUB75 pins:

  • rgb_pins = [R0, G0, B0, R1, G1, B1]
  • addr_pins = [A, B, C, D, (E)]*
  • clock_pin = CLK
  • latch_pin = STB
  • output_enable_pin = OEn
  • busio.I2C(SCL, SDA)

*The matrices function by grouping LEDs into blocks of 8 (I think along the y-axis). The address pins correspond to these blocks, so depending on the length of your board you may need use more or less of the address pins than I did. Reading any documentation about your matrix or simply googling is helpful if you can't figure it out.

Creating Usable Images (optional)

adafruit.bmp
blinka.bmp
raspberrypi.bmp
nyan0.bmp
nyan1.bmp
nyan2.bmp
nyan3.bmp

The code I created is capable of displaying bmp images on the display. This is useful, as it allows you to display basically anything as long as you can convert it to a bmp file with dimensions smaller than your matrix. To do this, I dowloaded GIMP, an open source image editor. All you need to do to convert an image to bmp is the following (on Mac):

  • Open an image in GIMP
  • Go to the "Image" header
  • Select "Scale Image"
  • You can pick any resolution smaller than the dimensions of your matrix. I used a 64x32, so I stuck to 64x32 or 32x32, but if you are chaining matrices or using a smaller one adjust those values as necessary.
  • Click "Scale"
  • Go to the "Image" header again
  • Click the "Mode" drop down menu
  • Select "Indexed" *
  • Click "Convert"
  • Go to the "File" header
  • Click "Export As"
  • Select location and CHANGE ENDING TO .bmp

Depending on the display you're using, the images may not look great as you're effectively compressing a full image into a 64x32 pixel color grid. Because of this, you may want to focus on pixel art, as this comes across much cleaner. If you're interested in making your own pixel art, this guide on the Adafruit website does a much better job explaining than I could. All this being said, chaining together more matrices would theoretically allow you to display higher resolution images.

*This is the color mode necessary for CircuitPython. It encodes an index of colors in the file so the code knows which colors to light the LEDs.

Code

After you've initialized the matrix and any sensors or buttons you're using you can start to create displays. I plan on leaving this on my desk for decoration, so I just included a couple cool images and animations I found but there are plenty of ideas online if you're looking for different options. As I mentioned before, you can look into coding your own displays however this is somewhat advanced and would probably be easier on a better board than the Pico.

My code is primarily a series of if functions to add the capacitive touch control to multiple different display functions. These functions are mostly pulled together from the Adafruit website and CircuitPython forums/module documentations. If you'd like to learn more in depth how these displays work I'd recommend looking up the rgbmatrix, displayio, and framebufferio modules and trying to understand them. I'll give a brief description of some functions I (somewhat) wrote myself:

dsum: This function returns the sum of values in a list, you can see in the code that I structured the dummy variable to be a list of 1s and 0s, with either all values being zero or only one value being 1 at a time. The built in matrix.brightness function is not yet able to control brightness, all it can do is turn the LEDs all the way on or all the way off. By setting matrix.brightness equal to the dsum of my dummy list I can make it so the display turns off when the list is [0, 0, 0, 0, 0].

displaybmp: This function is just the generic way of displaying a bmp and was pulled from documentation, however the documentation assumes standard matrix and image dimensions, which I wasn't using, so I added code to recenter images. This will not work for bmps with dimensions larger than the LED matrix. The top left corner of the matrix is (0, 0), if you look in the code you'll see I wrote if blocks, one for the x coordinate and the other for the y. If the width of the image is less than 64, the function centers the x of that image as the width of the matrix minus the width of the image divided by two, which puts the image directly in the middle of your display. The y coordinate equivalent works the same.

For the rest of the sensors I just included simple animations, text, or images. The comments in the code explain them in more detail.

ALSO: I can't attach the folders of bmp images I used, but I've included some above that you can download. Just mess around with the code or create folders on your board to use those files.

import board, digitalio, adafruit_mpr121, busio, time, displayio, rgbmatrix, framebufferio
import adafruit_imageload, terminalio, random
import adafruit_display_text.label


displayio.release_displays()
matrix = rgbmatrix.RGBMatrix(
    width=64, bit_depth=2,
    rgb_pins=[board.GP0, board.GP1, board.GP2, board.GP3, board.GP4, board.GP5],
    addr_pins=[board.GP6, board.GP7, board.GP8, board.GP9],
    clock_pin=board.GP10, latch_pin=board.GP12, output_enable_pin=board.GP13)
display = framebufferio.FramebufferDisplay(matrix)


i2c = busio.I2C(board.GP17, board.GP16)
touch_pad = adafruit_mpr121.MPR121(i2c)


def scroll(line):
    line.x = line.x - 1
    line_width = line.bounding_box[2]
    if line.x < -line_width:
        line.x = display.width


def reverse_scroll(line):
    line.x = line.x + 1
    line_width = line.bounding_box[2]
    if line.x >= display.width:
        line.x = -line_width


def dsum(l): #Gets the sum of the dummy list, if 0 then the display is black
    nreturn = 0
    for i in l:
        nreturn = nreturn + i
    return nreturn
    
def displaybmp(filename): #Displays a bmp on your board
    g = displayio.Group()
    b, p = adafruit_imageload.load(filename)
    t = displayio.TileGrid(b, pixel_shader=p)
    if b.width >= 64:
        t.x = 0
    else: #Centers bmps smaller than 64 width
        t.x = round((64 - b.width)/2)
    if b.height >= 32:
        t.y = 0
    else: #Same but for height
        t.y = round((32 - b.height)/2)
    g.append(t)
    display.show(g)


def apply_life_rule(old, new):
    width = old.width
    height = old.height
    for y in range(height):
        yyy = y * width
        ym1 = ((y + height - 1) % height) * width
        yp1 = ((y + 1) % height) * width
        xm1 = width - 1
        for x in range(width):
            xp1 = (x + 1) % width
            neighbors = (
                old[xm1 + ym1] + old[xm1 + yyy] + old[xm1 + yp1] +
                old[x   + ym1] +                  old[x   + yp1] +
                old[xp1 + ym1] + old[xp1 + yyy] + old[xp1 + yp1])
            new[x+yyy] = neighbors == 3 or (neighbors == 2 and old[x+yyy])
            xm1 = x


def randomize(output, fraction=0.33):
    for i in range(output.height * output.width):
        output[i] = random.random() < fraction


def conway(output):
    conway_data = [
        b'  +++   ',
        b'  + +   ',
        b'  + +   ',
        b'   +    ',
        b'+ +++   ',
        b' + + +  ',
        b'   +  + ',
        b'  + +   ',
        b'  + +   ',
    ]
    for i in range(output.height * output.width):
        output[i] = 0
    for i, si in enumerate(conway_data):
        y = output.height - len(conway_data) - 2 + i
        for j, cj in enumerate(si):
            output[(output.width - 8)//2 + j, y] = cj & 1


SCALE = 1
b1 = displayio.Bitmap(display.width//SCALE, display.height//SCALE, 2)
b2 = displayio.Bitmap(display.width//SCALE, display.height//SCALE, 2)
palette = displayio.Palette(2)
tg1 = displayio.TileGrid(b1, pixel_shader=palette)
tg2 = displayio.TileGrid(b2, pixel_shader=palette)
g1 = displayio.Group(scale=SCALE)
g1.append(tg1)
display.show(g1)
g2 = displayio.Group(scale=SCALE)
g2.append(tg2)


#A list of bmp filenames I used
pixelart = ["lake.bmp", "moon.bmp", "painting.bmp", "starry_night.bmp"]
nyan = ["nyan0.bmp", "nyan1.bmp", "nyan2.bmp", "nyan3.bmp"]
bmps = ["adafruit.bmp", "bc.bmp", "blinka.bmp", "gimp.bmp", "muscle2.bmp", "raspberrypi.bmp"]
pixelpath = "pixelart/"
nyanpath = "nyan/"
bmpspath = "bmps/"
x = time.time()
dummy = [0, 0, 0, 0, 0]
firsttimeconway = True
randomindex = random.randint(0, len(bmps)-1)
newpress = True
lastindex = 0 #Used as a counter in the slideshow below
while True: #Because the Pico is a simpler board, you need to hold down on the cap touch sensors until your input is registered
    y = time.time()
    if touch_pad[0].value:
        print("10")
        dummy = [0, 0, 0, 0, 0]
    if touch_pad[1].value: #Triggers the first scroller
        dummy = [1, 0, 0, 0, 0]
    if touch_pad[2].value: #Triggers the game of life
        dummy = [0, 1, 0, 0, 0]
        firsttimeconway = True
    if touch_pad[3].value: #Triggers a random image from the bmp folder
        dummy = [0, 0, 1, 0, 0]
        newpress = True
    if touch_pad[4].value: #A nyan cat animation
        dummy = [0, 0, 0, 1, 0]
    if touch_pad[5].value: #Triggers a slideshow of images from the bmp folder
        dummy = [0, 0, 0, 0, 1]
    matrix.brightness = dsum(dummy)
    if dummy[0] == 1: #A text scroller
        scrollert = time.time()
        line1 = adafruit_display_text.label.Label(
            terminalio.FONT,
            color=0xff0000,
            text="CircuitPython")
        line1.x = display.width
        line1.y = 8
        line2 = adafruit_display_text.label.Label(
            terminalio.FONT,
            color=0x0080ff,
            text="On RP Pico")
        line2.x = display.width
        line2.y = 24
        g = displayio.Group()
        g.append(line1)
        g.append(line2)
        display.show(g)
        while time.time() < scrollert + 5:
            scroll(line1)
            reverse_scroll(line2)
            display.refresh(minimum_frames_per_second=0)
    if dummy[1] == 1: #Conway's game of life code by Jeff Epler https://learn.adafruit.com/rgb-led-matrices-matrix-panels-with-circuitpython/example-conways-game-of-life
        conwayt = time.time()
        if firsttimeconway:
            palette[1] = 0xffffff
            conway(b1)
            firsttimeconway = False
        display.auto_refresh = True
        time.sleep(3)
        n = 40
        while time.time() < conwayt + 5: #Change color and check for new input every 10 secs
            for _ in range(n):
                display.show(g1)
                apply_life_rule(b1, b2)
                display.show(g2)
                apply_life_rule(b2, b1)
            randomize(b1)
            palette[1] = (
                (0x0000ff if random.random() > .33 else 0) |
                (0x00ff00 if random.random() > .33 else 0) |
                (0xff0000 if random.random() > .33 else 0)) or 0xffffff
            n = 200
    if dummy[2] == 1: #Display a random bmp from the folder
        if newpress:
            randomindex = random.randint(0, len(bmps)-1)
            newpress = False
        filename2 = bmpspath + bmps[randomindex]
        displaybmp(filename2)
        time.sleep(1) #Pause so you don't hold down accidentally
    if dummy[3] == 1: #Nyan cat animation
        nyant = time.time()
        while time.time() < nyant + 5:
            for i in nyan:
                filename3 = nyanpath + i
                displaybmp(filename3)
                time.sleep(.01)
    if dummy[4] == 1: #A slideshow of images, t secs in between images
        slideshowt = time.time()
        t = 5
        while time.time() < slideshowt+5:
            if lastindex == len(pixelart)-1:
                filename4 = pixelpath + pixelart[lastindex]
                displaybmp(filename4)
                time.sleep(t)
                lastindex = 0
            else:
                filename4 = pixelpath + pixelart[lastindex]
                displaybmp(filename4)
                time.sleep(t)
                lastindex += 1

Connecting Tape to Touch Sensor

IMG_0586.jpeg
IMG_0587.jpeg
IMG_0588.jpeg
IMG_0589.jpeg

Once you have your images and code working, it's time to physically connect the sensor. To make controlling the device easier I used copper tape to extend the surface area of the capacitive sensor. This is pretty easy, just cut the tape to the desired length and tape it to the box so that one end can be touched from the outside and the other can be connected to the touchpads somehow. I used alligator clips to clip one end of the tape to a touchpad, but any other wire or just more copper tape would work too, just make sure you note which strip of tape corresponds to which touchpad. IMPORTANT: Make sure no bare metal is touching anything that it shouldn't be. The copper tape should connect to a clip then to the touchpad, it should not come into contact with any other tape, wires, or exposed metal.

Final Assembly Steps

All that's remaining at this point is securing the breadboard and touch sensor in the housing and running the cables through the holes in your box. I ran the power cable and data cable for the Pico through a small hole in the side and the HUB75 and power to the matrix through a hole in the top. You can then secure the matrix to the lid of the box (or not if you don't want to) and close the lid. Now all you need to do is connect the Pico and matrix to power. These steps don't have to be followed strictly, this is just supposed to be a model for any other projects using a matrix. By making simple changes to the code you can easily expand the functionality. Enjoy!