Adafruit provides an Arduino library to drive the LED Display Matrix they sell. The library code does an impressive job of using the limited clock speed and RAM of the Arduino platform to get great performance out of the LED matrix. While the code contains lots of comments that describe technical specifics of the code, it doesn’t give much of a conceptual overview of how the display is driven. It took me several reads to figure out what was going on. I’m going to try to explain generally how the code works to help others who want to try to modify it or drive the display themselves. If you haven’t read my previous post about how the display works, I’d recommend it.
The Adafruit code uses a technique called Binary Code Modulation to control the colors of the display. Batsocks provides a good description of this, which I recommend reading get a full understanding of it. Here is a quick summary. Driving an LED with Pulse Width Modulation (PWM) would turn the LED on for an amount of time proportional to the desired brightness, then turn it off for the rest of the period. BCM divides the period into binary-length segments (1x, 2x, 4x, etc), and turns the LED on or off for each of these segments according to the bits of the LED’s desired brightness. Instead of using lots of equally-sized interrupts like PWM, BCM achieves the same color resolution with fewer varying-length interrupts, using less CPU time overall. The total on/off time for the LEDs is the same.
To use BCM, the pixel data must be divided up into bitplanes. A bitplane is an array in which each element is a single bit from a color value in the displayed image. The least significant bits form one plane, the next most significant bits form the next plane, and so on up to the most significant bitplane. Conceptually, a bitplane is an array of single bits, so each pixel would require three separate bitplanes, one for each color element. However to drive the display, all three colors are written out for two different pixels at the same time. The Adafruit code stores six bits, one from each bitplane, together in a byte. This way the bytes are immediately ready to be written to the GPIO port during the interrupt. The Adafruit code referes to the arrays of ready-to-write bytes as “planes,” presumably taken from “bitplanes.” Since the images displayed by the Adafruit code use 4-bit values for each color, there are four total “planes.”
In each interrupt (RGBmatrixPanel::updateDisplay) a byte from the appropriate plane is written out to the GPIO port and the delay until the next interrupt is set. Because of the way BCM works, the display interval for the second plane is twice as long as the first, the third is twice as long as the second, and so on. The actual interrupt behavior is a little trickier than it seems at first. As mentioned in the earlier post about the display matirx, data can be shifted into the display shift register while other data is being displayed. The shifted-in data does not drive the pixels until it is latched in. To maximize the amount of time that the display is on, the display is only turned off long enough to latch in the new data and calculate the correct row to power, then is turned back on. New data is then shifted in. However, this means that while the row to power and the delay until the next interrupt are calculated for the current interrupt period, the data shifted in is for the next interrupt period. This data is in a different bitplane and may be for a different row than the one currently driven.
Each data write to the matrix uses 6 bits: red, green, and blue for each of two pixels. The data bytes are stored so that these bits are ready to write. However, storing six bits per 8-bit byte is an inefficient use of the Arduino’s limited RAM. The six bits per byte required for each of four planes hold a total of 24 bits. 24 bits can be more efficiently held in 3 fully-used bytes, and the Adafruit code does exactly this. Three planes are stored with six ready-to-write bits, leaving 2 unused bits per byte. The two extra bits from three bytes are enough to represent the six bits needed for the fourth plane. Three interrupts write their bytes directly to the output port, while the fourth needs to assemble its data from the extra bits of the other three planes. It takes more processor time to assemble these bytes, so this is done during the interrupt that has the longest display time. This makes sure that the extra time spent in this interrupt does not delay the start of the next interrupt.
The display data is stored in ready-to-write bytes, but users of a display want to be able to control the display by saying “This pixel is this color,” so at some point this RGB pixel data needs to be broken down into “planes.” When the user specifies an RGB value for a pixel, the non-interrupt code immediately does all the bit-shifting work to update the bitplanes with the new pixel. (RGBmatrixPanel::drawPixel) The display data is never stored as an RGB pixel matrix, and instead is only stored as ready-to-write “planes”.
The rest of the code is all support code, including initialization, debugging, translation between the color resolution of the Adafruit graphics library (16-bit) and the LED matrix color resolution (12-bit), etc. The optimization of the interrupt code is interesting, and I intend to take a closer look at this in a future post.