While waiting for some parts to arrive to complete the assembly of my quadcopter, I started assembly of a few Max7219 display kits I had bought previously.
Once I had a few displays assembled, I wanted to test if they were working. I used a few push buttons and pull-up resistors and simulated Max7219 serial protocol by hand.
Obviously this is not the best and fastest way to test, but since putting max7219 into display test mode is easy, just push 2 x 'FF' bytes, and it's in test mode with all LEDs on.
As soon as I had confirmed that display were ok and working, it was time to step up the game.
So I decided to see if ATtiny85, would have enought punch to handle a dew displays with the internal 8Mhz.
I have never used Arduino before, but it seemed pretty simple, when compared to ASM firmware that I have done some time ago. At least it's in C/C++, so it should be easy.
In one day, I did the following:
- Installed my hardware programmer
- Setup Arduino development environment
- Setup ATtiny support (not using any arduino board)
- Started developing some code
At the end of the day, using an already available Max7219 lib (LedControl), I had an example working, that was filling several matrix displays, pixel by pixel or by full rows (faster).
During the night I added some more features, namelly to support a basic bitmap font (just a dozen chars) and support scroll left and scroll right (default lib font does not work with my configuration, and it's only 5x8).
However, looking at the way the LedControl library was implemented (looked at source code), I see two major problems:
- Writes to the sequence of displays one at a time (forcing NO_OP, on other devices)
- Mix & matches display processing with device access.
What I mean is, this thing should be split into two or more parts, since it doesn't make sense (for example) to have an hardcoded font inside the library code used to access devices, because some people may not need it, or may want to use some other mapping.
This is specially important, when you consider that each hardware implementation (schematic) can have different configurations for matrices (row vs col orientation), and also how they serially connect adjacent modules (horizontaly or vertically) or a composed lines x columns of devices, making any assumed font invalid for these configurations.
Another factor, is program space, which seems large for its current functions, mostly due to internal buffers and font bitmap and some Arduino bloat.
So interfacing, should be distinct of how it is actually used as a whole (as a screen).
After adding a few more displays (gone from 2, to 3 and then 4), it become painfully apparent that the library was slow, because of the way it's writting to each display, and it gets exponencially worse each time a display is added.
Max7219, suports 8x8 matrix display, or 8 x 7 segment displays (plus '.'), which gives you control of 64 LEDs (8x8=64).
But the most it can do, is to write one row (one byte, 8 bits) to each display at a time.
So to completly update a single display we need 8 writes, one for each row. Since each Max7219 command uses 2 bytes, we transfer a total of 16 bytes (2*8).
However, when several displays are serially connected, the command that just went through the first display, enters the next display, untouched. So they provide a NO_OP command, that allows a display to do nothing.
Resuming, if we have for example a screen composed of 3x1 displays in series, if we want to update display 2, we can do the following:
- Write 1 command (2 bytes) with NO_OP for display [3]
- Write 1 command (2 bytes) to update a single row of display [2]
- Write 1 command (2 bytes) with NO_OP for display [1]
Since we want to update all rows we have to repeat this process for all 8 rows of display 2.
In total, we have to write 3 commands x 8 rows = 24 commands (48 bytes), just to update a single display device.
This is how LedControl library works, and it makes sense since it is the correct way to do it, when you want to update only one of the display devices, without afecting the others.
However, you tipically want to update the entire screen (all 3 display devices) at once.
But LedControl, doesn't allow you to to this, you have to write one display at a time, in sequence, which equates to 3 x 24 commands = 72 commands in a total of 144 bytes to be transfered.
But the problem is that it gets worse, when you start to add displays.
For 4 displays in sequence you get:
- Update one display row = 4 commands
- Update all display rows = 8 x 4 commands = 32 commands
- Update entire screen (4 displays) = 4 * 32 commands = 128 commands (256 bytes).
So to refresh one full screen frame we have gone from 72 commands (for 3 displays) to 128 commands (for 4 displays), and from 144 bytes to 256 bytes respectively. For 5 displays and up, it only gets worse.
With 3 display devices and running ATtiny85 at 8MHz, some form of wobbling is clearly apparent when screen is animated. i.e. updates are not fast enought.
But there is a better way, which is to allow the library user to control the max7219 latching mechanism (Load/CS), so that the screen controller can decide what to do, including optimizing the writting to all display devices at once, row by row.
Worst case cenario, writing a full screen frame, will take the following number of commands:
{num_displays} * 8 rows * 1 command/row
So for a screen using 4 displays in sequence, we get:
4 * 8 * 1 = 32 commands
This grows linearly with number of displays. Compare that to 128 commands we had before!
After all this babeling and technical mumbo-jumbo, there is another issue that can be improved, which is to swap the bit-banging style of the library, and instead use the SPI support of the chip.
NOTE: bit-banging can be useful when we are out of pins (it's more flexible) or when the SPI is already being used.
Hence 2 versions of the library should exist, one bit-banged and another with native SPI support.
I will probably implement my own library(ies), taking into account all these concerns and optimizations and then release it to the public.