Modifying an Adafruit CircuitPython library to support the Pimoroni 11×7 LED Matrix Breakout on the Raspberry Pi Pico

At the moment, I am trying to combine several different products – sensors, displays – into a project for the Raspberry Pi Pico. One of them is the Breakout Garden 11×7 Matrix from Pimoroni. I’ve chosen to use CircuitPython as there are a lot of libraries available from Adafruit. One of those available is the IS31FL3731 library which supports a variety of products (use the link to see which ones) but not the one I needed it to. I decided to see how difficult it was to add support for the 11×7 Matrix to it. Here is what I did to do just that.

Addendum

I’ve since been told (by David Glaude) that there are easier ways to do this (such as doing a library in regular, non-compiled Python). Of course, I obviously didn’t find this way, and I think that compiling to .mpy saves some space on the Pico. There’s also a pre-compiled version of mpy-cross for Windows that avoids all the Ubuntu/WSL stuff. However, having a Linux shell available on Windows does give me other advantages, so “swings-and-roundabouts”…

MPY files

The first thing I realised was that the library files provided by Adafruit are pre-built as .mpy files – compiled Python. So, I would need to figure out how to build from the source Python .py file into .mpy. Fortunately, being the nice people they are, Adafruit provide the source code file so that it is modifiable. So… How do I build everything?

Windows Subsystem for Linux

I’m using Windows at the moment (for better or worse) so there are some hoops to jump through! I found this thread on a Forum which contained some hints and mentioned something called the WSL – the Windows Subsystem for Linux – which gives you a Linux terminal prompt and lets you do things in a “Linux-y” way.

Adafruit provides instructions for installing the WSL here (which I won’t duplicate, but suffice to say, you use Windows Features to add it and then install the Ubuntu app through the Windows Store). Once I’d installed that and set-up a username for myself, I saw a typical Ubuntu terminal screen.

Then, I needed to install some build tools into the WSL. Again, Adafruit provides instructions for installing these. The “meat” of this is as follows (I added the pip3 install as it wasn’t there):

sudo apt update
# Try running `make` at the command line. If it's not installed, do:
# sudo apt install build-essential
sudo apt install git
sudo apt install gettext
sudo apt install python3-pip
pip3 install huffman

ARM gcc toolchain

I also needed the “ARM gcc toolchain” (which I think is the bits and pieces needed to compile for an ARM processor). I’m going to be using the latest CircuitPython firmware (which is hovering around v6.2 beta-3) so I needed the 10-2020-q4-major version. Now, I should probably have used curl or wget to get the file straight into WSL, but I didn’t, so it was then downloaded to my Windows Downloads folder.

However… the WSL mounts my C drive in /mnt, so first I moved the file to the C drive, then copied the file into the Linux area by using:

mkdir ~/bin
cp /mnt/c/<filename> ~/bin/

This created a bin folder in my home folder and then copied <filename> into it. To unpack the file I did this:

cd ~/bin
tar xvf <filename>

I now needed to add the path to my ~/.bash_profile file.

This is easier said than done if you’re not used to Linux. Hopefully, you are because you’re reading a Raspberry Pi blog! If you’re not, take a look at using the Nano editor as I don’t want to go into that here.

I got the bin folder path from the folder that I just unpacked. This will be something like…

/home/<user>/bin/gcc-arm-none-eabi-10-2020-q4-major/bin

I copied that to my clipboard, then edited the ~/.bash_profile file (which was a new file for me – make sure you include the full stop if you’re replicating) and added the following line (which you’ll need to change to match your own path).

export PATH=/home/recantha/bin/gcc-arm-none-eabi-10-2020-q4-major/bin:$PATH

I logged out from the terminal (CTRL-D normally does it) and then opened the Ubuntu terminal again to type:

which arm-none-eabi-gcc

This returned a path (as opposed to an empty line). If you’re following me and it returns an empty line, you’ve done something wrong and you should retrace your steps. You probably did the export command wrong in .bash_profile or you didn’t logout and log back in again!

I made sure I was in my home directory before going any further by typing:

cd ~

Building the compiler

I now needed to build the mpy-cross compiler. This is described in this tutorial from Adafruit. In short:

git clone https://github.com/adafruit/circuitpython.git
cd circuitpython
git submodule sync --quiet --recursive
git submodule update --init
make -C mpy-cross

didn’t build CircuitPython itself because I have no need to compile my own CircuitPython firmware (yet!). The last step (building mpy-cross) took a little while.

Compiling py to mpy

The next thing to do was to compile a .py file into .mpy just to see if it worked. To do this, I needed to know the path to the mpy-cross executable. This was, in my case: ~/circuitpython/mpy-cross/mpy-cross

I decided to fork from the Adafruit library to my own repository. That way, if I wanted to, I could then do a pull request to Adafruit to incorporate the new code. Whether I do this or not depends on me being brave! 🙂 So, I clicked the fork button and replicated the code to my own repo.

The next thing to do was to get that repository into the WSL environment on my Windows machine. I did this by:

git clone https://github.com/recantha/Adafruit_CircuitPython_IS31FL3731

I then went into the directory that I had cloned and used mpy-cross to compile the Python file into .mpy:

cd Adafruit_CircuitPython_IS31FL3731
~/circuitpython/mpy-cross/mpy-cross adafruit_is31fl3731.py

To my surprise, it worked without errors!

Connecting the Pico to WSL

Now, how do I get that library file onto the Pico. I could, of course, copy it from the Linux area back into the C: drive and do it that way, but there was an easier method.

First of all, I identified that my Pico was mounted onto Windows using E:.

So, I did the following to create a “mount point” and then mounted the Pico (the E drive) to it:

sudo mkdir /mnt/e
sudo mount -t drvfs E: /mnt/e
ls /mnt/e

This last command should list the files on the E: drive (one of which should, if you’ve already uploaded code to the Pico, be code.py).

I knew I needed to put the mpy file into the lib folder on the Pico, so I did this:

cp ~/Adafruit_CircuitPython_IS31FL3731/adafruit_is31fl3731.mpy /mnt/e/lib/

Running the code

I had (and still have) no idea how to use WSL as a programming environment for the Pico, so I swapped back to Windows and ran Mu. I uploaded my program to the Pico which used the IS31* library. I received the same “Unsupported operation” error that I got earlier, so all good so far. Now… let’s see about modifying that code…

Side note

Okay, so what actually happened is that Mu stopped showing me the REPL. So I had to download the PuTTY terminal software package and connect to it separately to see the output…

Now, back to our scheduled programme…

Getting some output

So, what does “Unsupported operation” actually mean? Well, I guessed that as I was running the following on the Pico:

i2c = busio.I2C(scl=board.GP21, sda=board.GP20, frequency=100000)
display = adafruit_is31fl3731.Matrix(i2c)

and getting that error around the following line:

File "adafruit_is31fl3731.py", line 102, in _i2c_write_reg

… that I was attempting to write over I2C to something I couldn’t. So, let’s look at the Matrix code… The Matrix code uses I2C address 0x74 as its default. This is wrong. On the back of the 11×7 module, it says:

…0x75 or 0x77. Now, I haven’t altered the board by cutting the trace at the top-right, so we’re going to assume it’s the 0x75 address. This is different, obviously, to the default that the library is using, so the error appears to be that I’m trying to write to an I2C address that isn’t there. Fair enough. Inside the library code, it looks like the Matrix class init() function accepts an address parameter, so let’s try using that and see what we get…

display = adafruit_is31fl3731.Matrix(i2c, address=0x75)

I saved it and this time I received no error message. Excellent. Now, I know that the Matrix class has a hardcoded width of 16 and height of 9, and this is, of course, different to the Breakout Garden module’s 11×7 dimensions. So, we will have problems with getting the pixels in the right place, but let’s add to the CircuitPython code on the Pico the following:

display.fill(127)

This ought to display a solid colour… Save it… Check the REPL (using PuTTY, now – see above). Ta-da!

It worked! Excellent. Now, to try and get some text scrolling across…

Unfortunately, the examples from Adafruit for scrolling text and displaying images require PIL (Python Image Library). I read somewhere that PIL doesn’t work in CircuitPython, just in normal Python. Umm… Okay, let’s leave that for now.

I decided, instead, to just try looping through each pixel in order as a test. This is where knowing a little bit about the Pimoroni product and where they keep their code comes in handy.

Looking at the Pimoroni library

The Pimoroni init() function for their 11×7 matrix is here. Now, this is written in normal Python, not CircuitPython, not even MicroPython, so I’ve tried to do a halfway house between their code and the Adafruit library.

I did a bit of looking around and found that the existing CircuitPython library supports the Scroll pHAT HD. This is 17×7, so not far off. So, I started with that function, copied it and pasted it back into the Adafruit library, changing the class name to Matrix11x7. I also added a new __init__() function that would set the 0x75 address by default.

Now, to re-compile using myp-cross again and upload… All good so far.

I then changed the CircuitPython code on the Pico to use Matrix11x7():

display = adafruit_is31fl3731.Matrix11x7(i2c)

It worked again – this must mean that it is using my new, modified library code! Exciting!

Getting the Matrix right

I added some Pico code to display pixels one at a time:

for y in range(7):
    for x in range(11):
        display.pixel(x, y, 100)
        time.sleep(0.1)
        display.fill(0)

What this code should do is to put a single pixel on the display, left to right, bottom to top and do it for every LED on the display. As you can see…

it didn’t quite work (although, getting the ranges wrong initially, and for this video, didn’t help!) so I knew I needed to do some translation in the library. This is the code I had to work with, from a different class:

def pixel_addr(x, y):
    if x <= 8:
        x = 8 - x
        y = 6 - y
    else:
        x = x - 8
        y = y - 8

    return x * 16 + y

I knew, just by looking at it, that it wasn’t quite right. I love maths. Honest I do…

Then, I wondered: have Pimoroni already done the hard work for me? And indeed they had! If you look at their normal Python library for it, you’ll see that their _pixel_addr() function has a mapping and some calculations:

mapping = [
    6, 22, 38, 54, 70, 86, 14, 30, 46, 62, 78,
    5, 21, 37, 53, 69, 85, 13, 29, 45, 61, 77,
    4, 20, 36, 52, 68, 84, 12, 28, 44, 60, 76,
    3, 19, 35, 51, 67, 83, 11, 27, 43, 59, 75,
    2, 18, 34, 50, 66, 82, 10, 26, 42, 58, 74,
    1, 17, 33, 49, 65, 81, 9, 25, 41, 57, 73,
    0, 16, 32, 48, 64, 80, 8, 24, 40, 56, 72
]

y = self.height - 1 - y
return mapping[(y * self.width) + x]

This is the “magic sauce” (or source! ha-ha… #tumbleweed) that makes the maths that makes it work. I converted it into something that would work in MicroPython/CircuitPython and…

Ta-da! A halfway house between Pimoroni’s code and Adafruit’s original library. I ran the code again and…

Hooray! A successfuly library mod and upload to the Pico, followed by a successful test of each and every pixel in the correct order.

I’ll do something about scrolling text later – this is enough for now!

Closing thoughts

I hope this is of some use to somebody – I know I’ll need to refer back to it at some point!

If I could manage this, you can too! The toolchain is a little complicated on Windows (I suspect if I’d have done it using a Raspberry Pi/native Linux environment it would have been simpler, but you use what you’ve got!)

I even plucked up the courage to do a Pull Request to merge it into the original Adafruit library!

3 comments for “Modifying an Adafruit CircuitPython library to support the Pimoroni 11×7 LED Matrix Breakout on the Raspberry Pi Pico

    • Thanks for that. Yeah… it was, at least, a learning experience. I also figure that compiling is good for the microcontroller 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.