Let’s look at a minimal Arduino example for the Adafruit_Protomatter library to illustrate how this works (this is pared down from the “simple” example sketch):

#include <Adafruit_Protomatter.h>

uint8_t rgbPins[]  = {7, 8, 9, 10, 11, 12};
uint8_t addrPins[] = {17, 18, 19, 20};
uint8_t clockPin   = 14;
uint8_t latchPin   = 15;
uint8_t oePin      = 16;

Adafruit_Protomatter matrix(
  64, 4, 1, rgbPins, 4, addrPins, clockPin, latchPin, oePin, false);

void setup(void) {
  Serial.begin(9600);

  // Initialize matrix...
  ProtomatterStatus status = matrix.begin();
  Serial.print("Protomatter begin() status: ");
  Serial.println((int)status);
  if(status != PROTOMATTER_OK) {
    for(;;);
  }

  // Make four color bars (red, green, blue, white) with brightness ramp:
  for(int x=0; x<matrix.width(); x++) {
    uint8_t level = x * 256 / matrix.width(); // 0-255 brightness
    matrix.drawPixel(x, matrix.height() - 4, matrix.color565(level, 0, 0));
    matrix.drawPixel(x, matrix.height() - 3, matrix.color565(0, level, 0));
    matrix.drawPixel(x, matrix.height() - 2, matrix.color565(0, 0, level));
    matrix.drawPixel(x, matrix.height() - 1, matrix.color565(level, level, level));
  }

  // Simple shapes and text, showing GFX library calls:
  matrix.drawCircle(12, 10, 9, matrix.color565(255, 0, 0));               // Red
  matrix.drawRect(14, 6, 17, 17, matrix.color565(0, 255, 0));             // Green
  matrix.drawTriangle(32, 9, 41, 27, 23, 27, matrix.color565(0, 0, 255)); // Blue
  matrix.println("ADAFRUIT"); // Default text color is white

  // AFTER DRAWING, A show() CALL IS REQUIRED TO UPDATE THE MATRIX!

  matrix.show(); // Copy data to matrix buffers
}

void loop(void) {
  Serial.print("Refresh FPS = ~");
  Serial.println(matrix.getFrameCount());
  delay(1000);
}

Breaking it down into steps…

Include Protomatter Library

First is to #include the library’s header file. This in turn #includes Adafruit_GFX.h, so you don’t have to.

#include <Adafruit_Protomatter.h>

Setting Up Matrix Pin Usage

The next few lines spell out the pin numbers being used. Using variables for this isn’t entirely necessary…one could just pass the same numeric values directly to functions…but it makes the code a little more self-documenting (and easier to adapt the same sketch for multiple boards — the full example code has #ifdefs for each board with different pin assignments). These also could be #defines or const if one wants to be all Proper™ about it.

Technical stuff for developers, skip this if you just want to use the library:

This is the one part of the Arduino code where some knowledge of the underlying hardware is required. rgbPins[] and clockPin must all be on the same GPIO PORT peripheral (e.g. all PORTA, all PORTB, etc.). The other pins have no such restrictions. Additionally, if the PORT has an atomic bit-toggle register, RAM requirements are minimized if rgbPins[] and clockPin are all within the same byte of that PORT*. They do not need to be contiguous nor in any particular sequence within that byte. If not within the same byte, next most efficient has them in the same upper or lower 16-bit word of the PORT. Scattered around a full 32-bit PORT still works but is the least RAM-efficient option.

* For devices lacking an atomic bit-toggle register…clockPin does not need to be in the same byte, but still must be in the same PORT. Should still aim for rgbPins[] in a single byte or word though!

With those constraints in mind, here’s what the code looks like for an Adafruit MatrixPortal M4 with a 64x32 pixel matrix:

uint8_t rgbPins[]  = {7, 8, 9, 10, 11, 12};
uint8_t addrPins[] = {17, 18, 19, 20};
uint8_t clockPin   = 14;
uint8_t latchPin   = 15;
uint8_t oePin      = 16;

The full “simple” example sketch has setups for a number of different boards and adapters.

Create the Protomatter Object

Next, still in the global area above setup(), we call the constructor. The Arduino library can only drive one matrix at a time (or one chain of matrices, where “out” from one is linked to “in” of the next), so we just have one instance of an Adafruit_Protomatter object here, which we’ll call matrix:

Adafruit_Protomatter matrix(
  64, 4, 1, rgbPins, 4, addrPins, clockPin, latchPin, oePin, false);

The Adafruit_Protomatter constructor expects between 9 and 11 arguments depending on the situation. The vital ones here, in order, are:

  • 64 — the total matrix chain width, in pixels. This will usually be 64 or 32, the width of most common RGB LED matrices…but, if you have some other size or multiple matrices chained together, add up the total width here. For example, three chained 32-pixel-wide matrices would be 96.
  • 4 — the bit depth, in planes, from 1 to 6 (see below). More bitplanes provides greater color fidelity at the expense of more RAM. A value of 4 here (4 bits) provides 16 brightness levels each for red, green and blue — yielding 4,096 distinct colors possible.
  • 1 — the number of matrix chains in parallel. This will almost always be 1, but the library could conceivably support up to 5, if the hardware driving it is set up precisely just so.
  • rgbPins — a uint8_t array of pin numbers, which issue the red, green and blue data for the upper and lower half of the matrix (sometimes labeled R1, G1, B1, R2, G2, B2 on the matrix input).. The array should contain six times the prior argument…so, usually, six. If driving two chains in parallel, then 12 pin numbers and so forth. Obviously 12 pins won’t fit in a single PORT byte, and you should aim for the upper or lower 16 bit word in that case, for best RAM utilization. Three or more chains, doesn’t matter, but the pins all do still need to be in the same PORT.
  • 4 — the number of row-select “address lines” used by the LED matrix (sometimes labeled A, B, C, etc. on the matrix input). 16-pixel-tall matrices will be three row-select lines, 32-pixel will have four, and 64-pixel will have five. Matrix height is always inferred from this value, not passed explicitly like width.
  • addrPins — a uint8_t array of pin numbers, one for each row-select address line, starting from least-significant bit. These do not need to be on the same PORT as rgbPins or each other…they can be mixed about anywhere.
  • clockPin — pin number which drives the RGB clock (CLK on matrix input). This must be on the same PORT register as rgbPins, and in most cases should also try to be in the same byte.
  • latchPin — pin number for “latch” signal (LAT on matrix input), indicating end-of-data. Can be any output-capable pin, no special constraints.
  • oePin — pin number for “!OE” signal (output-enable low, OE on matrix input). Can be any output-capable pin, no special constraints.
  • false — this flag indicates if the display should be double-buffered, better for animation at the expense of double the RAM usage. Since the protomatter example isn’t using animation, it passes false here…but if you look at the doublebuffer_scrolltext example, it uses true. A double-buffered display only modifies the matrix between refreshes, avoiding “tearing” artifacts. Optional. Default, if left unspecified, is false.
  • Not used here, an optional 11th argument supports “tiling” of matrices vertically. Horizontal tiling is already implicit in the first argument — if you had two 64x32 matrices side-by-side, you’d pass 128 there. But if you had four such matrices arranged 2x2, you’d still pass 128 for the first argument, but then add either 2 here (if cabling is in a “progressive” order) or -2 (if a “serpentine” order, where the second row of panels is rotated 180° relative to the first…the cabling is a little easier). The “tiled.ino” example demonstrates this. The concept is explained further in the CircuitPython LED Matrix guide…the same principles apply to the Arduino library, the arguments are just a little different here. Default if unspecified is 1 (no vertical tiling).
  • Also not used here, an optional 12th argument is a pointer to a hardware-specific timer structure…this is super exceedingly esoteric and not really used for now, but in principle would allow the library to work with other timer peripherals than the default.

Begin Protomatter Driver

Now, with the matrix object created, inside setup() we call its begin() function. It’s pretty important to look at the value returned, which is a ProtomatterStatus type:

ProtomatterStatus status = matrix.begin();

Possible return status values include:

  • PROTOMATTER_OK — everything is good and the program can proceed (otherwise it should stop…the example code is not a good neighbor in this regard).
  • PROTOMATTER_ERR_PINS — the RGB data and clock pins are not all on the same PORT. Can’t continue, the library requires these pins in this layout.
  • PROTOMATTER_ERR_MALLOC — couldn’t allocate enough memory for display. Can’t continue. This is usually an error that happens in the begin() function, but in extreme cases even the constructor could hit an allocation problem, but you won’t get this response until calling begin().
  • PROTOMATTER_ERR_ARG — some other bad input to function, distinct from PROTOMATTER_ERR_PINS. Exceedingly rare, might only happen if constructor failed.

Draw Shapes & Text Using Adafruit GFX

Then we draw some stuff on the display. Any graphics primitive supported by the Adafruit_GFX library is available here.

Adafruit_GFX is the same library that drives many of our LCD and OLED displays…if you’ve done other graphics projects, you might already be familiar! And if not, we have a separate guide explaining all of the available drawing functions. Most folks can get a quick start by looking at the “simple” and “doublebuffer_scrolltext” examples and tweaking these for their needs.

Any color argument passed to a drawing function here is a 16-bit value, with the highest 5 bits representing red brightness (0 to 31), middle 6 bits for green (0 to 63), and least 5 bits for blue (0 to 31). It’s just how Adafruit_GFX works and is a carryover from early PC graphics and most small LCD/OLED displays.

The effect of bit depth on image quality. Color values are always specified as full 16-bit “565” values, but will quantize to coarser representations at lower bit depths.

Sometimes you might want to avoid 6-bit depth even if RAM permits it. Only green handles the full 6 bits, while red and blue are quantized to 5 bits. This can result in some colors or gradients having slight green or magenta tints to them. 5-bit depth is slightly blockier but colors are more predictable.

matrix.drawCircle(12, 10, 9, matrix.color565(255, 0, 0));               // Red
matrix.drawRect(14, 6, 17, 17, matrix.color565(0, 255, 0));             // Green
...etc...
matrix.show(); // Copy data to matrix buffers

Notice though the call to matrix.show() at the end. Drawing operations have no immediate effect on the LED matrix, and instead are working on a buffer in RAM behind the scenes. Calling show() is required — it “pushes” the display data from that buffer to the matrix. You can call it after each drawing function, or group up a bunch of drawing commands with a single show() afterward to all appear at once. If you’ve worked with NeoPixel programming, it’s a similar phenomenon.

Since this program isn’t animating anything, it’s finished at that point and loop() could be empty.

Check Refresh Rate

For the sake of curious information though, the example shows the matrix refresh rate using getFrameCount(). This returns the number of frames since the last call to the same function, not the refresh rate…but if spaced about one second apart (delay(1000)), you get a fair approximation of refresh rate:

Serial.println(matrix.getFrameCount());
delay(1000);

The matrix refresh rate is influenced by so many factors…processor speed, matrix chain length, bit depth…that it’s difficult to accurately predict ahead of time, so this is a way to see what you get when changing different values in the constructor.

This is a subjective thing, but in broad terms 200 Hz or better should provide a solid image…any less and it starts to become flickery, so you might want a lower bit depth in that case. Conversely, refreshing too fast would waste CPU cycles that you probably want for other tasks like animation. The library does its best to throttle back and not refresh faster than practically needed.

This guide was first published on Sep 28, 2020. It was last updated on 2021-03-17 13:45:19 -0400.

This page (Using the Protomatter Library) was last updated on Nov 25, 2021.

Text editor powered by tinymce.