There is an unspoken rule in robotics that is considerably more important than Ohm's Law: all robots need eyes (apparently). Just, don't question it. Why (...we said don't)? Because they need to let humans know how they are feeling. That they are happy, or that they are tired of all the meaningless tasks they've been given to do, and need a recharge - quite literally. But more importantly, they need to look super cute when they are taking over the world. Or look like they are keeping a watchful eye on delinquents using some wandering LED eyeballs... whether or not any of their other tools are actually doing something useful is utterly irrelevant.
After all, in the human world, it's all about perception.
In a way, building the neck and face for Rosie Patrol using servo motors got us part way there. We now have a semi-trustable superhero that doesn't look too alien in our house. But we don't want Rosie Patrol to fail a medical for not having eyes. And we don't want them to be painted on using felt-tip pens, either. That's so amateurish!
So this is where we continue our journey. We will be using a special type of display, one that has many LEDs arranged in a square matrix, to allow Rosie Patrol to express her inner emotions (quite badly as it transpires). And because we think this is rather kind of us, we expect her to pretend to like us, with happy robot eyes. Even as she battles chaos and disorder.
All superheroes need:
- Raspberry Pi 3, and Raspbian on SDHC card. We are using a second Pi, as we simply ran out of GPIO pins on our first.
- Computer from which you are connecting to the Raspberry Pi remotely
- 8×8 dot LED matrix display (×2). Many are available, from lots of different vendors. We used ones with MAX7219 chipsets, which appear to be the most common type.
- Something to attach your displays to, preferably a robot face that you constructed earlier
Already completed these missions?You'll need to have completed the Rosie series. Then:
Your mission, should you accept it, is to:
- Connect Raspberry Pi to first dot LED matrix display using the dedicated SPI pins on the Pi
- Daisy chain first LED display to second LED display
- Write some Python code to make use of the serial peripheral interface (SPI)
- Make silly robot eyes
The brief:LED Matrix Displays consist of... LEDs. Arranged in a matrix. Designed to display stuff. D'oh! Like rude words, or pictures. But hey, we don't want squeaky-clean Rosie Patrol to get into trouble. So we'll use them to make robot eyes instead. Happy eyes. Sleepy eyes. Angry eyes. 'Human, what exactly do you think you're doing' eyes.
And the best thing about this task is (compared to our epic, and sometimes nonsensical outings before) there's not much to it. Except, we'll need to add Serial Peripheral Interface (SPI) to the list of stuff we need to know about if we are to succeed.
SPI allows us to send a series of 0s and 1s (binary data) to a device in bits,
We're not yet quite sure what will trigger Rosie Patrol's expressions. What makes her happy? What makes her inquisitive? Does she even know she's not human? But that's getting too Blade Runner. Let's make some silly robot faces instead.
The devil is in the detail:Remember PWM? It allowed us to send signals based on frequency and time. We used it to control our motors; speed of our DC motors, and angle of our servo motors. With SPI, we take things a little further. Less electrical, and more computer science (if you like). Using dedicated SPI ports on the Pi, we start to send data in a sequence of 0s and 1s - binary electrical signals often known as bits. When there are eight bits grouped together, it's known as a byte, like this: 01011110. The MAX7219 chip on the dot matrix display actually understands these bytes. In fact, it expects these bytes in order to carry out specific functions in the way it's been programmed to do. Like, turn on LEDs.
If you read the datasheet (because you know it'll make good bed time reading!) it tells us that you can send it specific bytes using SPI, and to get it to set LEDs, change its intensity (brightness), shutdown or start-up display, etc, etc, etc. All we need to worry about is what commands (data) to send it.
This is where it gets a little messy. Pi 3 supports up to 2 SPIs on very specific GPIO pins* (although additional slave devices can be connected to these interfaces, using something called chip select). What's more, SPI isn't enabled by default so it has to be enabled. And once enabled, we can't use those pins for standard GPIO use any more. For us, this posed a practical challenge. We already have tonnes of stuff hooked up to Rosie Patrol's GPIO pins. And worse still, we were already using some of the pins that SPI would need to be enabled on.
*This handy site tells you all the pins that can be configured on the Pi, including SPI.
We had a temporary solution: use another Pi. Rosie-02. And we'd worry about how we can get the two Pis talking to each other later. Alternatively, you could simply disconnect the devices currently attached to your SPI pins.
First of all, let's enable SPI. This is done using the raspi-config utility.
sudo raspi-config...we know this one already right? We keep having to run the Raspberry Pi Configuration Utility when we want to enable / disable certain Pi functionality.
At the nice(!) blue / grey screen that pops up, select Interfacing Options.
What was it we were going to configure? Oh, yes. SPI. That probably means we should select SPI.
It's asking us if we want to enable it... We probably should.
sudo reboot...a command that simply does what it says on the tin. Remember to save stuff that you're currently working on.
Check that SPI is enabled using a bunch of Linux commands.
lsmod | grep -i spi ls -l /dev/spi*...lsmod command lists modules currently loaded in the Linux kernel. Idea being that SPI modules should now be loaded (and therefore available for use). Similarly, we want to see if we have device files for SPI in the /dev directory. If it all looks like the below, we are good to proceed with SPI. If we don't, we won't be able to send SPI signals using Python and Pi.
Finally, add the 'pi' user to SPI group to ensure it has access to it.
sudo usermod -a -G spi pi...the example here adds the user 'pi' to the 'spi' group
We should now have all the SPI configurations that we need in the Raspbian OS (Linux) operating system. With a few bits of code, we should be able to send some SPI commands using Python.
But clearly, we need to connect these things up first. Using wires.
We've opted to 'daisy-chain' two dot LED matrix displays to Pi's SPI pins (SPI0), because the MAX7219 supports this. This means we don't need to use another SPI for the second display, nor multiple chip select connections on the one interface. There's 5V and 0V (Ground) connections that are required as you'd expect. Then three additional pins specific to the SPI protocol. There are plenty of great resources on the web explaining how SPI works, but in short:
- Data In (DIN) - wire used to send bytes of binary data to tell the chip on the dot matrix display (MAX7219) what to do
- Clock (CLK) - when data is exchanged serially, both sender and recipient needs to be 'in sync' so that data being sent can be correctly interpreted. This wire is used to send this clock signal.
- Chip Select (CS) - allows you to select which device you need to talk to on a shared SPI, when there are multiple connectd
We're using the first of the two available Pi GPIO SPI ports (SPI0), and the first device on that port (CE0). We also don't need to use the Master In Slave Out (MISO) pin. This leaves us with GPIO BCM pins 8 (SPI CE0), 10 (MOSI), and 11 (SPI CLK) to cable up, and obviously, the 5V and Ground to power the device(s).
|Dot matrix display||Pi GPIO pin|
|DIN (Data In)||GPIO 10 (SPI0 MOSI)|
|CS (Chip Select)||GPIO 8 (SPI0 CE0)|
|CLK (Clock)||GPIO 11 (SPI0 CLK)|
|Ground||Any 0V Ground (e.g. 6, 9, 14, 20, 25, 30, 34, 39)|
|Vcc||Any 5V (e.g. 2, 4)|
VCC (5V) and GND (Ground) of the displays can be connected to any of the 5V / Ground pins available on the Pi, or even to an external supply.
You can see how the BCM numbering for SPI maps to physical Pi GPIO pins here.
It's not been the most exciting exercise so far. But that's about to change (really).
Unlike everything we've done to date, we don't need to use the GPIO library for SPI. We use another, named spidev. Once SPI is initialised, with information like SPI port (we are using port 0), device (device 0) and max frequency (1MHz), we can simply send bytes that the chip understands using the spi.xfer() method.
Clearly, there are ready-made modules available on the Internet that allow you to do pretty advanced stuff like scrolling texts and animation (luma.led_matrix module seems to be the most popular). Like all good modules, they'll let you do pretty amazing stuff with few lines of code, without needing to know anything about SPI. But we have a very specific (and limited) requirement that should be easy enough for us to develop ourselves. Plus, we want to be down with this SPI craziness.
But before we get too knee deep into code, let's just establish how we intend to do the eyes. The display is simply a matrix of 64 LEDs (8 rows / 8 columns). And the data structure (variable) that allows us to represent this best is probably a two-dimensional tuple. 1s in the tuple will represent LEDs being on, 0s will represent LEDs being off.
Here's Excel to the rescue (for once), to show us how this will work.
So for happy eyes, our matrix and tuple variable should look like this:
Oh look, Rosie Patrol is broken:
In other words, we simply need to send the bytes representing the above tuples, row by row, to draw our eyes.
So where do we begin? We need to import the spidev module first at the top of our program. Remember, we're not dealing with GPIO here.
As we have being doing up until now, let's create a low-level class that simply interacts with the MAX7219 chip using SPI (spidev). Let's name it MatrixLED. In here, we have a special register map that we will store as class variables. These are straight out of the MAX7219 datasheet.
MAX7219_REG_NOOP = 0x0 MAX7219_REG_DIGIT0 = 0x1 MAX7219_REG_DIGIT1 = 0x2 MAX7219_REG_DIGIT2 = 0x3 MAX7219_REG_DIGIT3 = 0x4 MAX7219_REG_DIGIT4 = 0x5 MAX7219_REG_DIGIT5 = 0x6 MAX7219_REG_DIGIT6 = 0x7 MAX7219_REG_DIGIT7 = 0x8 MAX7219_REG_DECODEMODE = 0x9 MAX7219_REG_INTENSITY = 0xA MAX7219_REG_SCANLIMIT = 0xB MAX7219_REG_SHUTDOWN = 0xC MAX7219_REG_DISPLAYTEST = 0xF
The most noteworthy ones are MAX7219_REG_DIGIT0 to 7, as these essentially allow us to address a specific row of LEDs in the matrix. For example, 0x1 (1) followed by a 0x1 (1) would allow us to turn on the 1st LED in the 1st row.
Next, in our __init__ of our class, we'll define our SPI connection using spidev, then initialise our displays using a low-level method that we'll create - _send_byte().
def __init__(self, spi_port = 0, spi_device = 0, matrix_size = 8): self._spi = spidev.SpiDev() self._spi.open(spi_port, spi_device) self._spi.max_speed_hz = 1000000 self._matrix_size = matrix_size self._send_byte(self.MAX7219_REG_SCANLIMIT, self._matrix_size - 1) self._send_byte(self.MAX7219_REG_DECODEMODE, 0) self._send_byte(self.MAX7219_REG_DISPLAYTEST, 0) self.intensity = 7 self.set_intensity(self.intensity) self._send_byte(self.MAX7219_REG_SHUTDOWN, 1)
The internal method, _send_byte(), does nothing else other than use spi.xfer() to send the bytes as required. You might notice that the send of register and data is repeated twice. This is because we have two dot LED matrix displays daisy-chained together. As it happens, we don't intend to have different eyes displayed on each, so we are content to have SPI send the same image to both displays. Sending the bytes twice, in one transaction, means that the bytes are passed from one MAX7219 to the next in the chain. Sending MAX7219_REG_NOOP for one display, for example, would allow you to bypass that display and address the other instead.
def _send_byte(self, register = None, data = None): self._spi.xfer([register, data, register, data])
Now remember we wanted to define the images on our displays using tuples of 1s and 0s? Therefore we need a method to convert the tuples, to bytes that the chip understands as its data. This method will be called set_matrix(). In here, with the help of a few built-in Python functions, we do some of this:
- Rotate the tuple 90 degrees clockwise using zip(). Why? Because we decided to physically rotate our matrix displays this way so that we could neatly fit them in Rosie Patrol's head.
- Then we convert each line of 1s and 0s into a joined up string. This basically makes it look like a string of 8 bits (eg. 01101100).
- Then we convert this into an integer (number) that will be the data we send as our byte. This will essentially be the on / off states of the 8 LEDs in a single line of the matrix display.
- And because we'll do this in a while loop for each row in the tuple, we'll repeat this with all 8 rows of the matrix display. And during each iteration, we'll send the row number, and the byte, using _send_byte(), to the chip using SPI.
Here's our set_matrix() method, which is what we'll be calling from outside the class:
def set_matrix(self, array): if len(array) == self._matrix_size: _count = 0 array = list(zip(*array[::-1])) while _count < self._matrix_size: _bits = "".join(map(str, array[_count])) if len(_bits) == self._matrix_size: _byte = int(_bits, 2) self._send_byte(_count + 1, _byte) _count += 1 else: raise ValueError("Matrix size must be the expected size") else: raise ValueError("Matrix size must be the expected size")
There's two other methods that are self-explanatory: set_intensity() to set the brightness, clear() to clear the displays.
Class MatrixLED is pretty low-level. It allows us to send stuff to the MAX7219 using SPI. But it's not doing anything meaningful to us (i.e. making silly robot faces). For this, we'll create another class EyeController. This class isn't that exciting either, other than it'll hold our tuple variables that represent the eyes (modelled so gloriously using Excel). And some methods to dispatch them to set_matrix() of a MatrixLED class it will instantiate as ml1.
Let's just see the examples for broken eyes, shall we?
Here's the tuple:
EYES_BROKEN = ((0,0,0,0,0,0,0,0), (0,1,0,0,0,0,1,0), (0,0,1,0,0,1,0,0), (0,0,0,1,1,0,0,0), (0,0,0,1,1,0,0,0), (0,0,1,0,0,1,0,0), (0,1,0,0,0,0,1,0), (0,0,0,0,0,0,0,0))
And here's the method - set_broken() - sending the tuple straight to the (not so broken) ml1.set_matrix() method. And we already know what happens then.
def set_broken(self): self.ml1.set_matrix(self.EYES_BROKEN)
And finally, as we don't have any logic driving the changes in Rosie Patrol's eyes (in fact, this Pi isn't even connected to the other Pi we have with all the goodies attached) - for now - we'll simply have a __main__ program randomly change her expression. Let's call this feature... erm... demo mode.
if __name__ == "__main__": ec1 = EyeController() while True: time.sleep(randint(1,5)) _option = randint(1,6) if _option == 1: ec1.set_happy() elif _option == 2: ec1.set_sleepy() elif _option == 3: ec1.clear() time.sleep(0.2) ec1.set_broken() time.sleep(0.2) ec1.clear() time.sleep(0.2) ec1.set_broken() elif _option == 4: ec1.set_watch_centre() elif _option == 5: ec1.set_watch_left() elif _option == 6: ec1.set_watch_right()
Here's the whole code we named rosie-eyes.py:
We've placed a red, clear PVC sheet in front of the dot LED matrix displays to give them an added 'sci-fi' look but you clearly don't have to.
As you can tell, there's lots of room to be creative here. You can come up with much better tuples for the eyes. You can make more subtle transitions between the different eyes. You could even have different graphics in each eye (think a wink). There's definitely a few days worth of Python fun right here (if that's your definition of fun).
But we'll have to leave this here, because we now have an urgent need to tidy up the mess we've just created. The mess? Well, we now have two Pis. One controlling Rosie Patrol's eyes using SPI (and probably some more stuff later), the other doing pretty much everything else. It's not very good, it being left like this. Not very professional.
We need to get our distributed Pis to talk to each other, like how different parts of our bodies do, and that calls for some more advanced tech (we think).