There oughta be an LED cube.

28 September 2020

Inspired by many LED cube projects before me, I decided to build my own version, which eventually turned into a CPU status indicator on my desk. Is there a more over the top way to monitor your CPU temperature and usage than 12,288 individually addressable RGB LEDs arranged as a cube?

Thumbnail of a youtube video about my LED cube, showing three images of the cube with dominantly blue, then green and finally red color.
Click the image to see the video on youtube.com.

In this case, I strongly suggest to check out the video first as it conveys the looks of the cube much better than static images. Of course, the blog article will provide all the technical details.

Why?

Why building an LED cube? Well, because it looks bloody amazing. Last year I had the honor to give a talk at the 36th Chaos Communication Congress (36C3) and of course had the opportunity to stroll through the assemblies to see all the amazing hacker projects. It is like traveling to another planet1 and of course there were many of those cubes made out of LED video panels. I just knew: I have to have one too.

Photo of an LED cube with a color gradient across all sides.
Color gradient test pattern on an LED cube.

All cubes I have seen so far run on battery and can be held in your hand, which allows for amazing animations with particle systems reacting to gravity as you rotate the cube. But I knew, that while those designs have a mildly superior coolness, I would be better off with a stationary design: No battery to deal with and the cube would simply have a set of animations that work independently. If I could get living-room clearance from my wife, it would serve as a mood light, like a lava lamp.

Photo of the back of the cube, showing that there is not a panel on every side.
As it is designed to be stationary, this cube only has LED panels on three sides and is AC-powered.

Such stationary design also makes it a bit cheaper. If it is sitting on a shelf, some panels would face the ground or the walls anyway, so I would get away with only three LED panels. After all, I found that I was able to build the entire cube for less than 150€2.

Overview

So, I started building the cube, but the living-room clearance from my wife was not forthcoming. Therefore, I pivoted3 to placing the cube on my desk, where it would serve to monitor the CPU status of my desktop PC.

Photo of my desk. Three screens and the LED cube at one side.
As the cube did not find a place in our living-room, it had to be repurposed as a CPU status indicator on my desk (bottom right corner).

The cube automatically turns on when I start my PC and I have a simple Python script runnning in the background of the PC which reports the current temperature and usage statistics of the CPU to the cube. The colorful animation on the cube is designed to be not distracting (slow and calming) and changes such that I can see the state of the CPU at a single glance.

Three photo of my LED cube in different states from left to right. The left-most has a thin white ring on top of a blue-patterned background. The ring of the center image has a bulge at one side and the background is dominated by green and yellow colors. The photo on the right-hand side shows a cube with a very thick ring on a red background.
The LED cube showing different states of my PC's CPU. Left: CPU is at a cool temperature and all threads are idle. Center: CPU is at a higher temperature and there is one thread under heavy load with the others being idle. Right: CPU temperature is very high and all twelve CPU threads are being used at full capacity.

If the CPU is cool and idle, the animation runs on a blue background. With increasing temperature, the background changes from blue to green, yellow and eventually a fiery red. In the center of the animation is a circle that spans across the three visible sides of the cube. This circle consists of twelve segments representing the twelve threads of my Ryzen 5 CPU. Each segment changes width according to the usage of a thread, so if the CPU is idle, the entire ring is thin. If few threads are running at full capacity, there is a bulge in few segments4 of the ring and if the entire CPU is occupied, the entire circle swells up.

Hardware

If you break down the cube, the hardware is surprisingly simple as every part is an integrated system designed to be a component of a larger system.

Photo of the disassembled cube with active LED panels.
The cube without its 3d-printed case.

Of course, we have three LED matrix panels and at the cube’s heart, there is an old Raspberry Pi 2 Model B and a LED matrix driver board by Adafruit. That’s almost the entire thing. We only need a 3d printed case and a power supply and are ready to shine.

LED panels

I ordered the LED video panels directly from aliexpress.com where I found the best deal for this type of panel. Unfortunately, this always is a bit risky because I find it extremely difficult to get the exact specs from the product description and there are some variants on how these displays are driven. In the end, you will have to figure out, which display type you have and how to drive it with the matrix library (see below) and there will always be some room for little surprises like new revisions of the same panel.

Photo of the front and back of the LED panel.
Front and back of an LED panel.

At least the description was clear about the stats for the panels as seen from the outside. Each panel has 64x64 RGB LEDs at a “P2” pitch, meaning that they are on a 2mm grid leading to a side length of 128mm for the square panels. They came mounted on metal mounts, which probably mean something to someone who actually uses these panels in a large video display as they are supposed to be used, but I removed those mounts immediately by removing some tiny screws.

It is also noteworthy, that these displays usually run at 5V and that I also found it rather difficult to find out the nominal power consumption for these panels. In this case, the description was OK-ish in that regard and I came to the conclusion that a 50W power supply (providing 10A at 5V) would be enough to drive the panels and the Pi.

Photo of the power supply.
The cube is powered by a 50W power supply, providing 10A at 5V.

To me, this usually seems rather tricky. As the panels run at 5V, you need very high currents to get the power to them, so you can imagine that you need some well designed power distribution if you want to supply many panels. In fact, the 10A are not sufficient for my system in every situation. If I turn all LEDs to full brightness white, the Pi becomes a bit unstable and sometimes reboots.

I have to admit that I was a bit lazy here. The power supply is simply plugged into Adafruit’s matrix driver (see below) which supplies the Pi5 and the three panels. If you want to copy this project, you might want to use an even stronger power supply and/or separate the power for the Pi and the panels.

However, in practice you only need to be aware of the limits of your power supply. Most of the time, your LEDs will not all be on full white6 and if you are running a preset animation like me, your power supply only needs to deal with the “brightest situation”. This of course limits what your animation can do as for example showing short intense full-white flashes might not be a good idea.

Driving the panels

So, as already outlined above, the LED cube is driven by a Raspberry Pi 2. This is mostly the case, because I had a spare one, but it also presents some advantages over other models. Newer and faster Pis (rPi 3 and 4) become much hotter than the older rPi 2, which may or may not be a problem in a completely closed cube. On the other hand, you do not want to go to the rPi 1 as this is a single core system and it is highly recommended to dedicate an entire core to driving the panels to avoid flickering.

Photo of the Raspberry Pi 2 with the Adafruit RGB Matrix Bonnet and a Wifi dongle.
The LED cube is driven by a Raspberry Pi 2 with the Adafruit RGB Matrix Bonnet on top. You can also see the Wifi dongle in one of the USB ports.

I have added an USB Wifi dongle to the Pi, to get easy network access without additional cables7, but more importantly I have equipped it with the Adafruit RGB Matrix Bonnet which takes care of distributing the power and driving the LED panels. Note, that minimal soldering was required on the RGB Matrix Bonnet as my panels have an “E” address, but this is properly explained in the Bonnet’s documentation.

Photo of the Raspberry Pi 2 with the Adafruit RGB Matrix Bonnet connected to the three LED panels.
The LED panels are connected as a single daisy-chain. The power is supplied by the red/black cables which have been disconnected from the second and third panel for the photo.

In this setup, all three panels can simply be daisy-chained to the RGB Matrix Bonnet. Each panel has a data in and data out port, which can connect to the next one and each panel needs a separate power supply.

3d printing

The case of the cube was 3d printed in black PLA. It (obviously) consists of six sides that snap into each other in an arbitrary configuration. In the end I added some plastic glue to increase stability, but I did not glue the panels as they are a nice tight fit and I can easily take them out if I need to do maintenance on the inside. Of the six sides in my version of the cube, three are identical frames to hold a panel each, two are designed to hold the Raspberry Pi (pins to hold it and cutouts for power supply and SD card) and one is just a solid wall.

Photo of cube without panels.
The panels are held by 3d-printed parts. Of the six sides, three hold displays, two are designed to hold the Pi and one side is just solid.

I have also designed a base that allows to present the cube at a slight tilt, but on my desk I found that the viewing angle was better without the base.

Photo the 3d-printed base and the cube.
Usually, I do not use the optional base to tilt the cube.

The parts were designed in Blender and you can find the corresponding blend files as well as exported STL files for your slicer of choice in the github repository for this project.

Software

That’s it for the hardware. Now we have to actually send something to the panels, which mostly is software running on the cube including the OpenGL shader that creates the actual animation. But there is also a script on my PC to publish the CPU stats and the Raspberry Pi’s OS needs to be fortified such that I do not have to deal with booting and shutting down in order to avoid corrupting the file system.

Controlling the LED panels

The most important piece of software is a little C++ program running on the cube8. This uses the rpi-rgb-led-matrix library by Henner Zeller, opens a UDP port to receive the CPU status from the PC and uses OpenGL to render the animation. It is mostly a mash-up of the rpi-rgb-led-matrix library’s examples and an example by Matus Novak of how to use OpenGL without X on a Raspberry Pi. If you want to understand my code-mash-up, you should check out their resources first.

To get the rpi-rgb-led-matrix library I used the install script by Adafruit which is part of their instructions for their Bonnet. However, I had some trouble to properly run my LED panels with the version their install script got, so I switched to a more recent commit of the library by replacing the line COMMIT=4f6fd9a5354f44180a16d767a80915b265191c9c in the script by COMMIT=21410d2b0bac006b4a1661594926af347b3ce334. Note, that this commit is also a bit old by now, so you might want to try the newest version first.

Much of this stuff is rather specific to your LED panels. In most examples of the library, you can provide these parameters at the command line. For my code on the cube I found that setting the correct defaults makes life a bit easier. This also means that it may make sense for you to adapt those as well if your panels differ:

1
2
3
4
5
6
7
8
9
10
11
12
//LED Matrix settings
RGBMatrix::Options defaults;
rgb_matrix::RuntimeOptions runtime;
defaults.hardware_mapping = "adafruit-hat-pwm";
defaults.led_rgb_sequence = "RGB";
defaults.pwm_bits = 11;
defaults.pwm_lsb_nanoseconds = 50;
defaults.panel_type = "FM6126A";
defaults.rows = 64;
defaults.cols = 192;
defaults.chain_length = 1;
defaults.parallel = 1;

Note, that pwm_bits and pwm_lsb_nanoseconds may not seem to have such a huge impact at first, but they can be very important for the image quality. Specifically, the pwm_bits determines the number of bits of the pulse width modulation which determines how many color steps can be represented. The downside of increasing this value is that it reduces the refresh rate9 of the LED panel, which can be improved by reducing the pwm_lsb_nanoseconds settings - if your panels support such low values. In the end, you want a fairly high refresh rate, especially if you intent to film your cube as flickering that is not apparent to the eye will easily be seen in combination with the frame rate of a camera.

Also, it is extremely important that the Pi can drive the RGB Bonnet without interruption or you will see artifacts. For this, it is highly recommended to reserve an entire CPU core to this task as described in the library’s documentation.

You can find the complete cpu-stats-gl.cpp in this project’s github repository. Note that I do not provide a make file, so you need to take care of linking to all the required libraries like this: g++ -g -o cpu-stats-gl cpu-stats-gl.cpp -std=c++11 -lbrcmEGL -lbrcmGLESv2 -I/opt/vc/include -L/opt/vc/lib -Lrpi-rgb-led-matrix/lib -lrgbmatrix -lrt -lm -lpthread -lstdc++ -Irpi-rgb-led-matrix/include/. Don’t forget to also get your Pi ready to properly support OpenGL by following the instructions by Matus Novak.

The OpenGL shader

Ok, so at this point we have the hardware ready and some code to drive our panels. With the examples from the rpi-rgb-led-matrix library we should be able to display text, images and even animated GIFs on the cube. However, we want something dynamically generated and prepared OpenGL just for that.

The animation that represents the CPU state is mostly implemented as a fragment shader, i.e. a little piece of code that can run independently in parallel for every pixel of the image. The code has evolved a bit and is hard to follow10 and it is embedded as a string constant in the c++ code11.

In order to correctly project the image onto the cube, I render three pairs of triangles, each covering one face of the cube. The thing is, that if you look at the cube as a three dimensional object and want to show a two dimensional shape like a circle, you can assign coordinates of an imaginary two dimensional canvas in front of your face onto each edge of the cube.

Schematic of the coordinates as seen when looking at the cube.
The coordinates of the projected image as seen from the viewer when looking at the cube. Note that the local coordinate systems all are aligned differently while the coordinates in white form a proper coordinate system from this perspective.

If we now “unfold” the cube to the rectangular array of pixels that we actually address, we can cover that array by a few triangles such that there is a vertex12 at the edge of every LED panel. We can also match the “virtual canvas” coordinates to each vertex to get a mapping of our canvas coordinates to the actual pixels on the panel array.

Schematic of the coordinates in 2d following the arrangement of the pixels.
This is the pixel layout of the three panels as addressed in software. The cyan triangles form the rendering surface with vertices mapped to the edges of the individual panels. The white coordinates of the projected image are not continuous anymore, but can still be used by the fragment shader as they can be interpolated between the vertices.

In the shader, this can then be done very easily. We can simply supply the canvas coordinates for each vertex as an additional array buffer to the GPU and let it interpolate those coordinates for each pixel. We can then directly use the correct canvas coordinates for each pixel in the fragment shader without any additional thought.

This way, the circle looks circular when looking at the cube from the common corner of the three LED panels.13

Pushing CPU stats from my PC

Finally, we need to push our CPU stats to the cube via UDP, which is done by a very simple Python 3 script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/python3
import psutil
import socket
import time

TARGET_IP="192.168.2.45"
TARGET_PORT=1234

while True:
  temperature = 0.0
  time.sleep(0.5)
  temperature += psutil.sensors_temperatures()["k10temp"][0].current
  time.sleep(0.5)
  temperature += psutil.sensors_temperatures()["k10temp"][0].current
  time.sleep(0.5)
  temperature += psutil.sensors_temperatures()["k10temp"][0].current
  time.sleep(0.5)
  temperature += psutil.sensors_temperatures()["k10temp"][0].current
  time.sleep(0.5)
  temperature += psutil.sensors_temperatures()["k10temp"][0].current
  temperature /= 5.0

  cores = psutil.cpu_percent(percpu=True)

  out = str(temperature) + "," + ",".join(map(str, sorted(cores, reverse=True)))
  socket.socket(socket.AF_INET, socket.SOCK_DGRAM).sendto(out.encode("utf-8"), (TARGET_IP, TARGET_PORT))

On my PC this runs automatically as a systemd service and as you can see, it targets a fixed IP that I have set up for the cube. The longest part is simply a naive repetition14 and averaging of temperature readings as the temperature readings from the Ryzen are very jumpy and the cube should show a smoother reaction to a slower temperature trend.

Read-only filesystem

So, at this point the cube should be working as seen in the video. What is there left to do? Well, we still have a regular Raspbian15 installation on a regular Raspberry Pi, which means that if you do not want your nice setup to get corrupted, you need to properly shut it down every time you want to unplug the cube. I do not know about you, but I prefer my self-build devices to be entirely cut from power if I am not using them, so I want to hook the cube up to a power socket that simply follows the state of the PC. It is simply plugged into a socket strip along with the screens and the speakers that are all disconnected if no power is drawn on the socket to which the PC is connected16.

This is not a good idea for a normal Raspbian installation as write processes to the SD card may (and will!) fail and your file system will eventually get corrupted. This can be prevented by making the file system read-only. We do not need to store anything on the cube, we do not need any status files and we have set up everything we need, right?

Making the file system read only is not a simple thing to do. There is a lot that has to be changed in the system and it should not be done lightly. This has to be the very last step when everything is ready and you absolutely have to do a backup17 before attempting it. Luckily, there again is a script and instructions by Adafruit for this step.

Conclusion

Not sure how to call this last chapter. I did not reinvent the wheel and there is not much to conclude here except that LED cubes look extremely bodacious and I am happy to have one too.

Photo of the LED cube in idle state.
Conclusion: One of the most extravagant methods to monitor your CPU.

I am wondering whether I should by a few more of those LED panels to find other uses for them…

  1. Silly analogy. As if I’d ever been to another planet. 

  2. Not counting consumables like 3d printer filament or the old Raspberry Pi 2 that I revived from retirement for this project. 

  3. No, I do not start using entrepreneur speech. Therefore, whenever I use the word “pivot”, I mean it ironically. 

  4. Actually, I sort the threads by usage before applying them to the segments, so it is always the same segment that get thicker first. I found it easier to understand, especially as the system dynamically assigns a task/job to varying threads. 

  5. Actually not recommended by Adafruit. 

  6. Otherwise you would not need a video panel, would you? 

  7. More relevant for the original mood light plan. On my desk an enthernet cable would not be that much of a problem. 

  8. By “running on the cube” I mean running on the Raspberry Pi inside the cube. 

  9. Not to be confused with the frame rate at which a new image can be rendered. 

  10. As in my experience is rather common for shader code. I probably have not yet found how to document and structure it properly. 

  11. I find it interesting, that the shader code is usually provided as source code and compiled for the specific graphics card at runtime. Maybe, this is obvious and clear to the more experienced OpenGL coders out there, but I always find that fascinating. 

  12. Vertices (plural of vertex) are the corner points of each line of a polygon. 

  13. Of course, this only works for one point of view. If you look onto only one side of the cube, the circle looks distorted. 

  14. Yes, that could be a loop. 

  15. Yeah, it is now called “Raspberry Pi OS”, but I cannot get used to it. It sounds so generic… 

  16. When I bought this, it was called a “Master-Slave socket strip”, but I think that this term is not exactly appropriate. 

  17. Yes! Spend those 10 minutes to backup the entire SD before doing it. Seriously!