Add notes, tweak doublebuffer example, move convert565 to arch.h
This commit is contained in:
parent
be78b3d42b
commit
20d8bc80b6
4 changed files with 250 additions and 166 deletions
116
README.md
116
README.md
|
|
@ -2,25 +2,78 @@
|
|||
|
||||
"I used protomatter in the Genesis matrix." - David Marcus, Star Trek III
|
||||
|
||||
An Arduino library for HUB75-style RGB LED matrices, targeted at 32-bit
|
||||
microcontrollers using "brute force" GPIO (that is, not relying on DMA or
|
||||
any specialized peripherals beyond timer interrupts, goal being portability).
|
||||
It does assume the existence of 32-bit GPIO ports with bit SET and CLEAR
|
||||
registers. A bit TOGGLE register (if available) can improve performance.
|
||||
Underlying code might be adaptable to other (non-Arduino) runtime
|
||||
environments (see notes in file arch.h).
|
||||
Code for driving HUB75-style RGB LED matrices, targeted at 32-bit MCUs
|
||||
using brute-force GPIO (that is, not relying on DMA or other specialized
|
||||
peripherals beyond a timer interrupt, goal being portability).
|
||||
|
||||
This might supersede the RGBmatrixPanel library on non-AVR devices, as the
|
||||
Name might change as it's nondescriptive and tedious to type in code.
|
||||
|
||||
# Matrix Concepts and Jargon
|
||||
|
||||
HUB75 RGB LED matrices are basically a set of six concurrent shift register
|
||||
chains, each with one output bit per column, the six chains being red, green
|
||||
and blue bits for two non-adjacent rows, plus a set of row drivers (each
|
||||
driving the aforementioned two rows) selected by a combination of address
|
||||
lines. The number of address lines determines the overall matrix height
|
||||
(3 to 5 bits is common...as an example, 3 address lines = 2^3 = 8 distinct
|
||||
address line combinations, each driving two rows = 16 pixels high). Address
|
||||
0 enables rows 0 and height/2, address 1 enables rows 1 and height/2+1, etc.
|
||||
Shift register chain length determines matrix width...32 and 64 pixels are
|
||||
common...matrices can be chained to increase width, a 64-pixel wide matrix
|
||||
is equivalent to two 32-pixel chained matrices, and so forth.
|
||||
|
||||
These matrices render only ONE BIT each for red, green and blue, they DO NOT
|
||||
natively display full color and must be quickly refreshed by the driving
|
||||
microcontroller, basically PWM-ing the intermediate shades (this in addition
|
||||
to the row scanning that must be performed).
|
||||
|
||||
There are a few peculiar RGB LED matrices that have the same physical
|
||||
connection but work a bit differently -- they might have only have three
|
||||
shift register chains rather than six, or might use a shift register for
|
||||
the row selection rather than a set of address lines. The code presented
|
||||
here DOES NOT support these matrix variants. Aim is to provide support for
|
||||
all HUB75 matrices in the Adafruit shop. Please don't submit pull requests
|
||||
for these other matrices as we have no means to test them. If you require
|
||||
this functionality, it's OK to create a fork of the code, which Git can
|
||||
help keep up-to-date with any future changes here!
|
||||
|
||||
# Hardware Requirements and Jargon
|
||||
|
||||
The common ground for architectures to support this library:
|
||||
|
||||
* 32-bit device (e.g. ARM core, but potentially ESP32 and others in future)
|
||||
* One or more 32-bit GPIO PORTs with atomic (single-cycle, not
|
||||
read-modify-write) bitmask SET and CLEAR registers. A bitmask TOGGLE
|
||||
register, if present, may improve performance but is NOT required.
|
||||
* Tolerate 8-bit or word-aligned 16-bit accesses within the 32-bit PORT
|
||||
registers (e.g. writing just one of four bytes, rather than the whole
|
||||
32 bits). The library does not use any unaligned accesses (i.e. the
|
||||
"middle word" of a 32-bit register), even if a device tolerates such.
|
||||
|
||||
# Software Components
|
||||
|
||||
This repository currently consists of:
|
||||
|
||||
* An Arduino C++ library (files Adafruit_Protomatter.cpp and
|
||||
Adafruit_Protomatter.h, plus the "examples" directory). The Arduino code
|
||||
is dependent on the Adafruit_GFX library.
|
||||
|
||||
* An underlying C library (files core.c, core.h and arch.h) that might be
|
||||
adaptable to other runtime environments (e.g. CircuitPython).
|
||||
|
||||
# Arduino Library
|
||||
|
||||
This *might* supersede the RGBmatrixPanel library on non-AVR devices, as the
|
||||
older library has painted itself into a few corners. The newer library uses
|
||||
a single constructor for all matrix setups, handling parallel chains,
|
||||
various matrix sizes and chain lengths, and variable bit depths from 1 to 6
|
||||
(refresh rate is a function of all of these). Note however that it is
|
||||
NOT A DROP-IN REPLACEMENT for RGBmatrixPanel. The constructor is entirely
|
||||
different, and there are several changes in the available functions. Also,
|
||||
all colors in the new library are specified as 5/6/5-bit RGB (as this is
|
||||
what the GFX library GFXcanvas16 type uses, aimed at low-cost color LCD
|
||||
displays), even if the matrix is configured for a lower bit depth (colors
|
||||
will be decimated/quantized in this case).
|
||||
a single constructor for all matrix setups, potentially handling parallel
|
||||
chains (not yet fully implemented), various matrix sizes and chain lengths,
|
||||
and variable bit depths from 1 to 6 (refresh rate is a function of all of
|
||||
these). Note however that it is NOT A DROP-IN REPLACEMENT for RGBmatrixPanel.
|
||||
The constructor is entirely different, and there are several changes in the
|
||||
available functions. Also, all colors in the new library are specified as
|
||||
5/6/5-bit RGB (as this is what the GFX library GFXcanvas16 type uses, being
|
||||
aimed at low-cost color LCD displays), even if the matrix is configured for
|
||||
a lower bit depth (colors will be decimated/quantized in this case).
|
||||
|
||||
It does have some new limitations, mostly significant RAM overhead (hence
|
||||
no plans for AVR port) and that all RGB data pins and the clock pin MUST be
|
||||
|
|
@ -31,4 +84,31 @@ sequential within this byte, if for instance it makes PCB routing easier,
|
|||
but they should all aim for a single byte). Other pins (matrix address lines,
|
||||
latch and output enable) can reside on any PORT or bit.
|
||||
|
||||
Name might change as it's nondescriptive and tedious to type in code.
|
||||
# C Library
|
||||
|
||||
The underlying C library is focused on *driving* the matrix and does not
|
||||
provide any drawing operations of its own. That must be handled by
|
||||
higher-level code, as in the Arduino wrapper which uses the Adafruit_GFX
|
||||
drawing functions.
|
||||
|
||||
The C code has the same limitations as the Arduino library: all RGB data
|
||||
pins and the clock pin MUST be on the same PORT register, and it's most
|
||||
memory efficient (though still a bit gluttonous) if those pins are all
|
||||
within the same 8-bit byte within the PORT (they do not need to be
|
||||
contiguous or sequential within that byte). Other pins (matrix address lines,
|
||||
latch and output enable) can reside on any PORT or bit.
|
||||
|
||||
When adapting this code to new devices (e.g. nRF52, ESP32) or new runtime
|
||||
environments (e.g. CircuitPython), goal is to put all the device- or
|
||||
platform-specific code into the arch.h file (or completely separate source
|
||||
files, as in the Arduino library .cpp and .h). core.c contains only the
|
||||
device-neutral bitbang code and should not have any "#ifdef DEVICE"- or
|
||||
"#ifdef ENVIRONMENT"-like lines. Macros for things like getting a PORT
|
||||
register address from a pin, or setting up a timer peripheral, all occur
|
||||
in arch.h, which is ONLY #included by core.c (to prevent problems like
|
||||
multiple instances of ISR functions, which must be singularly declared at
|
||||
compile-time).
|
||||
|
||||
Most macros and functions begin with the prefix **\_PM\_** in order to
|
||||
avoid naming collisions with other code (exception being static functions,
|
||||
which can't be seen outside their source file).
|
||||
|
|
|
|||
144
arch.h
144
arch.h
|
|
@ -496,4 +496,148 @@ _PM_minMinPeriod: Mininum value for the "minPeriod" class member,
|
|||
#define _PM_minMinPeriod 100
|
||||
#endif
|
||||
|
||||
// ARDUINO SPECIFIC CODE ---------------------------------------------------
|
||||
|
||||
#if defined(ARDUINO)
|
||||
|
||||
// 16-bit (565) color conversion functions go here (rather than in the
|
||||
// Arduino lib .cpp) because knowledge is required of chunksize and the
|
||||
// toggle register (or lack thereof), which are only known to this file,
|
||||
// not the .cpp or anywhere else
|
||||
// However...this file knows nothing of the GFXcanvas16 type (from
|
||||
// Adafruit_GFX...another C++ lib), so the .cpp just passes down some
|
||||
// pointers and minimal info about the canvas buffer.
|
||||
// It's probably not ideal but this is my life now, oh well.
|
||||
|
||||
// Different runtime environments (which might not use the 565 canvas
|
||||
// format) will need their own conversion functions.
|
||||
|
||||
// There are THREE COPIES of the following function -- one each for byte,
|
||||
// word and long. If changes are made in any one of them, the others MUST
|
||||
// be updated to match! Note that they are not simple duplicates of each
|
||||
// other. The byte case, for example, doesn't need to handle parallel
|
||||
// matrix chains (matrix data can only be byte-sized if one chain).
|
||||
|
||||
// width argument comes from GFX canvas width, which may be less than
|
||||
// core's bitWidth (due to padding). height isn't needed, it can be
|
||||
// inferred from core->numRowPairs.
|
||||
void _PM_convert_565_byte(Protomatter_core *core, uint16_t *source,
|
||||
uint16_t width) {
|
||||
uint16_t *upperSrc = source; // Canvas top half
|
||||
uint16_t *lowerSrc = source + width * core->numRowPairs; // " bottom half
|
||||
uint8_t *pinMask = (uint8_t *)core->rgbMask; // Pin bitmasks
|
||||
uint8_t *dest = (uint8_t *)core->screenData;
|
||||
if(core->doubleBuffer) {
|
||||
dest += core->bufferSize * (1 - core->activeBuffer);
|
||||
}
|
||||
|
||||
// No need to clear matrix buffer, loops below do a full overwrite
|
||||
// (except for any scanline pad, which was already initialized in the
|
||||
// begin() function and won't be touched here).
|
||||
|
||||
// Determine matrix bytes per bitplane & row (row pair really):
|
||||
|
||||
uint32_t bitplaneSize = _PM_chunkSize *
|
||||
((width + (_PM_chunkSize - 1)) / _PM_chunkSize); // 1 plane of row pair
|
||||
uint8_t pad = bitplaneSize - width; // Start-of-plane pad
|
||||
|
||||
// Skip initial scanline padding if present (HUB75 matrices shift data
|
||||
// in from right-to-left, so if we need scanline padding it occurs at
|
||||
// the start of a line, rather than the usual end). Destination pointer
|
||||
// passed in already handles double-buffer math, so we don't need to
|
||||
// handle that here, just the pad...
|
||||
dest += pad;
|
||||
|
||||
uint32_t initialRedBit, initialGreenBit, initialBlueBit;
|
||||
if(core->numPlanes == 6) {
|
||||
// If numPlanes is 6, red and blue are expanded from 5 to 6 bits.
|
||||
// This involves duplicating the MSB of the 5-bit value to the LSB
|
||||
// of its corresponding 6-bit value...or in this case, bitmasks for
|
||||
// red and blue are initially assigned to canvas MSBs, while green
|
||||
// starts at LSB (because it's already 6-bit). Inner loop below then
|
||||
// wraps red & blue after the first bitplane.
|
||||
initialRedBit = 0b1000000000000000; // MSB red
|
||||
initialGreenBit = 0b0000000000100000; // LSB green
|
||||
initialBlueBit = 0b0000000000010000; // MSB blue
|
||||
} else {
|
||||
// If numPlanes is 1 to 5, no expansion is needed, and one or all
|
||||
// three color components might be decimated by some number of bits.
|
||||
// The initial bitmasks are set to the components' numPlanesth bit
|
||||
// (e.g. for 5 planes, start at red & blue bit #0, green bit #1,
|
||||
// for 4 planes, everything starts at the next bit up, etc.).
|
||||
uint8_t shiftLeft = 5 - core->numPlanes;
|
||||
initialRedBit = 0b0000100000000000 << shiftLeft;
|
||||
initialGreenBit = 0b0000000001000000 << shiftLeft;
|
||||
initialBlueBit = 0b0000000000000001 << shiftLeft;
|
||||
}
|
||||
|
||||
// This works sequentially-ish through the destination buffer,
|
||||
// reading from the canvas source pixels in repeated passes,
|
||||
// beginning from the least bit.
|
||||
for(uint8_t row=0; row<core->numRowPairs; row++) {
|
||||
uint32_t redBit = initialRedBit;
|
||||
uint32_t greenBit = initialGreenBit;
|
||||
uint32_t blueBit = initialBlueBit;
|
||||
for(uint8_t plane=0; plane<core->numPlanes; plane++) {
|
||||
#if defined(_PM_portToggleRegister)
|
||||
uint8_t prior = core->clockMask; // Set clock bit on 1st out
|
||||
#endif
|
||||
for(uint16_t x=0; x<width; x++) {
|
||||
uint16_t upperRGB = upperSrc[x]; // Pixel in upper half
|
||||
uint16_t lowerRGB = lowerSrc[x]; // Pixel in lower half
|
||||
uint8_t result = 0;
|
||||
if(upperRGB & redBit) result |= pinMask[0];
|
||||
if(upperRGB & greenBit) result |= pinMask[1];
|
||||
if(upperRGB & blueBit) result |= pinMask[2];
|
||||
if(lowerRGB & redBit) result |= pinMask[3];
|
||||
if(lowerRGB & greenBit) result |= pinMask[4];
|
||||
if(lowerRGB & blueBit) result |= pinMask[5];
|
||||
#if defined(_PM_portToggleRegister)
|
||||
dest[x] = result ^ prior;
|
||||
prior = result | core->clockMask; // Set clock bit on next out
|
||||
#else
|
||||
dest[x] = result;
|
||||
#endif
|
||||
} // end x
|
||||
greenBit <<= 1;
|
||||
if(plane || (core->numPlanes < 6)) {
|
||||
// In most cases red & blue bit scoot 1 left...
|
||||
redBit <<= 1;
|
||||
blueBit <<= 1;
|
||||
} else {
|
||||
// Exception being after bit 0 with 6-plane display,
|
||||
// in which case they're reset to red & blue LSBs
|
||||
// (so 5-bit colors are expanded to 6 bits).
|
||||
redBit = 0b0000100000000000;
|
||||
blueBit = 0b0000000000000001;
|
||||
}
|
||||
#if defined(_PM_portToggleRegister)
|
||||
// If using bit-toggle register, erase the toggle bit on the
|
||||
// first element of each bitplane & row pair. The matrix-driving
|
||||
// interrupt functions correspondingly set the clock low before
|
||||
// finishing. This is all done for legibility on oscilloscope --
|
||||
// so idle clock appears LOW -- but really the matrix samples on
|
||||
// a rising edge and we could leave it high, but at this stage
|
||||
// in development just want the scope "readable."
|
||||
dest[-pad] &= ~core->clockMask; // Negative index is legal & intentional
|
||||
#endif
|
||||
dest += bitplaneSize; // Advance one scanline in dest buffer
|
||||
} // end plane
|
||||
upperSrc += width; // Advance one scanline in source buffer
|
||||
lowerSrc += width;
|
||||
} // end row
|
||||
}
|
||||
|
||||
void _PM_convert_565_word(Protomatter_core *core, uint16_t *source,
|
||||
uint16_t width) {
|
||||
// TO DO
|
||||
}
|
||||
|
||||
void _PM_convert_565_long(Protomatter_core *core, uint16_t *source,
|
||||
uint16_t width) {
|
||||
// TO DO
|
||||
}
|
||||
|
||||
#endif // ARDUINO
|
||||
|
||||
#endif // _PROTOMATTER_ARCH_H_
|
||||
|
|
|
|||
143
core.c
143
core.c
|
|
@ -635,146 +635,3 @@ uint32_t _PM_getFrameCount(Protomatter_core *core) {
|
|||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
#if defined(ARDUINO)
|
||||
|
||||
// 16-bit (565) color conversion functions go here (rather than in the
|
||||
// Arduino lib .cpp) because knowledge is required of chunksize and the
|
||||
// toggle register (or lack thereof), which are only known to this file,
|
||||
// not the .cpp or anywhere else
|
||||
// However...this file knows nothing of the GFXcanvas16 type (from
|
||||
// Adafruit_GFX...another C++ lib), so the .cpp just passes down some
|
||||
// pointers and info about the canvas buffer.
|
||||
// It's probably not ideal but this is my life now, oh well.
|
||||
|
||||
// Different runtime environments (which might not use the 565 canvas
|
||||
// format) will need their own conversion functions.
|
||||
|
||||
// There are THREE COPIES of the following function -- one each for byte,
|
||||
// word and long. If changes are made in any one of them, the others MUST
|
||||
// be updated to match! Note that they are not simple duplicates of each
|
||||
// other. The byte case, for example, doesn't need to handle parallel
|
||||
// matrix chains (matrix data can only be byte-sized if one chain).
|
||||
|
||||
// width argument comes from GFX canvas width, which may be less than
|
||||
// core's bitWidth (due to padding). height isn't needed, it can be
|
||||
// inferred from core->numRowPairs.
|
||||
void _PM_convert_565_byte(Protomatter_core *core, uint16_t *source,
|
||||
uint16_t width) {
|
||||
uint16_t *upperSrc = source; // Canvas top half
|
||||
uint16_t *lowerSrc = source + width * core->numRowPairs; // " bottom half
|
||||
uint8_t *pinMask = (uint8_t *)core->rgbMask; // Pin bitmasks
|
||||
uint8_t *dest = (uint8_t *)core->screenData;
|
||||
if(core->doubleBuffer) {
|
||||
dest += core->bufferSize * (1 - core->activeBuffer);
|
||||
}
|
||||
|
||||
// No need to clear matrix buffer, loops below do a full overwrite
|
||||
// (except for any scanline pad, which was already initialized in the
|
||||
// begin() function and won't be touched here).
|
||||
|
||||
// Determine matrix bytes per bitplane & row (row pair really):
|
||||
|
||||
uint32_t bitplaneSize = _PM_chunkSize *
|
||||
((width + (_PM_chunkSize - 1)) / _PM_chunkSize); // 1 plane of row pair
|
||||
uint8_t pad = bitplaneSize - width; // Start-of-plane pad
|
||||
|
||||
// Skip initial scanline padding if present (HUB75 matrices shift data
|
||||
// in from right-to-left, so if we need scanline padding it occurs at
|
||||
// the start of a line, rather than the usual end). Destination pointer
|
||||
// passed in already handles double-buffer math, so we don't need to
|
||||
// handle that here, just the pad...
|
||||
dest += pad;
|
||||
|
||||
uint32_t initialRedBit, initialGreenBit, initialBlueBit;
|
||||
if(core->numPlanes == 6) {
|
||||
// If numPlanes is 6, red and blue are expanded from 5 to 6 bits.
|
||||
// This involves duplicating the MSB of the 5-bit value to the LSB
|
||||
// of its corresponding 6-bit value...or in this case, bitmasks for
|
||||
// red and blue are initially assigned to canvas MSBs, while green
|
||||
// starts at LSB (because it's already 6-bit). Inner loop below then
|
||||
// wraps red & blue after the first bitplane.
|
||||
initialRedBit = 0b1000000000000000; // MSB red
|
||||
initialGreenBit = 0b0000000000100000; // LSB green
|
||||
initialBlueBit = 0b0000000000010000; // MSB blue
|
||||
} else {
|
||||
// If numPlanes is 1 to 5, no expansion is needed, and one or all
|
||||
// three color components might be decimated by some number of bits.
|
||||
// The initial bitmasks are set to the components' numPlanesth bit
|
||||
// (e.g. for 5 planes, start at red & blue bit #0, green bit #1,
|
||||
// for 4 planes, everything starts at the next bit up, etc.).
|
||||
uint8_t shiftLeft = 5 - core->numPlanes;
|
||||
initialRedBit = 0b0000100000000000 << shiftLeft;
|
||||
initialGreenBit = 0b0000000001000000 << shiftLeft;
|
||||
initialBlueBit = 0b0000000000000001 << shiftLeft;
|
||||
}
|
||||
|
||||
// This works sequentially-ish through the destination buffer,
|
||||
// reading from the canvas source pixels in repeated passes,
|
||||
// beginning from the least bit.
|
||||
for(uint8_t row=0; row<core->numRowPairs; row++) {
|
||||
uint32_t redBit = initialRedBit;
|
||||
uint32_t greenBit = initialGreenBit;
|
||||
uint32_t blueBit = initialBlueBit;
|
||||
for(uint8_t plane=0; plane<core->numPlanes; plane++) {
|
||||
#if defined(_PM_portToggleRegister)
|
||||
uint8_t prior = core->clockMask; // Set clock bit on 1st out
|
||||
#endif
|
||||
for(uint16_t x=0; x<width; x++) {
|
||||
uint16_t upperRGB = upperSrc[x]; // Pixel in upper half
|
||||
uint16_t lowerRGB = lowerSrc[x]; // Pixel in lower half
|
||||
uint8_t result = 0;
|
||||
if(upperRGB & redBit) result |= pinMask[0];
|
||||
if(upperRGB & greenBit) result |= pinMask[1];
|
||||
if(upperRGB & blueBit) result |= pinMask[2];
|
||||
if(lowerRGB & redBit) result |= pinMask[3];
|
||||
if(lowerRGB & greenBit) result |= pinMask[4];
|
||||
if(lowerRGB & blueBit) result |= pinMask[5];
|
||||
#if defined(_PM_portToggleRegister)
|
||||
dest[x] = result ^ prior;
|
||||
prior = result | core->clockMask; // Set clock bit on next out
|
||||
#else
|
||||
dest[x] = result;
|
||||
#endif
|
||||
} // end x
|
||||
greenBit <<= 1;
|
||||
if(plane || (core->numPlanes < 6)) {
|
||||
// In most cases red & blue bit scoot 1 left...
|
||||
redBit <<= 1;
|
||||
blueBit <<= 1;
|
||||
} else {
|
||||
// Exception being after bit 0 with 6-plane display,
|
||||
// in which case they're reset to red & blue LSBs
|
||||
// (so 5-bit colors are expanded to 6 bits).
|
||||
redBit = 0b0000100000000000;
|
||||
blueBit = 0b0000000000000001;
|
||||
}
|
||||
#if defined(_PM_portToggleRegister)
|
||||
// If using bit-toggle register, erase the toggle bit on the
|
||||
// first element of each bitplane & row pair. The matrix-driving
|
||||
// interrupt functions correspondingly set the clock low before
|
||||
// finishing. This is all done for legibility on oscilloscope --
|
||||
// so idle clock appears LOW -- but really the matrix samples on
|
||||
// a rising edge and we could leave it high, but at this stage
|
||||
// in development just want the scope "readable."
|
||||
dest[-pad] &= ~core->clockMask; // Negative index is legal & intentional
|
||||
#endif
|
||||
dest += bitplaneSize; // Advance one scanline in dest buffer
|
||||
} // end plane
|
||||
upperSrc += width; // Advance one scanline in source buffer
|
||||
lowerSrc += width;
|
||||
} // end row
|
||||
}
|
||||
|
||||
void _PM_convert_565_word(Protomatter_core *core, uint16_t *source,
|
||||
uint16_t width) {
|
||||
// TO DO
|
||||
}
|
||||
|
||||
void _PM_convert_565_long(Protomatter_core *core, uint16_t *source,
|
||||
uint16_t width) {
|
||||
// TO DO
|
||||
}
|
||||
|
||||
#endif // ARDUINO
|
||||
|
||||
|
|
|
|||
|
|
@ -76,11 +76,11 @@ byte of PORT bits.
|
|||
Adafruit_Protomatter matrix(
|
||||
64, 6, 1, rgbPins, 4, addrPins, clockPin, latchPin, oePin, true);
|
||||
|
||||
const char str[] = "Adafruit 16x32 RGB LED Matrix";
|
||||
int16_t textX = matrix.width(),
|
||||
textMin = sizeof(str) * -12,
|
||||
hue = 0;
|
||||
int8_t ball[3][4] = {
|
||||
int16_t textMin,
|
||||
textX = matrix.width(),
|
||||
hue = 0;
|
||||
char str[40];
|
||||
int8_t ball[3][4] = {
|
||||
{ 3, 0, 1, 1 }, // Initial X,Y pos & velocity for 3 bouncy balls
|
||||
{ 17, 15, 1, -1 },
|
||||
{ 27, 4, -1, 1 }
|
||||
|
|
@ -99,6 +99,9 @@ void setup(void) {
|
|||
Serial.print("Protomatter begin() status: ");
|
||||
Serial.println((int)status);
|
||||
|
||||
sprintf(str, "Adafruit %dx%d RGB LED Matrix",
|
||||
matrix.width(), matrix.height());
|
||||
textMin = strlen(str) * -12;
|
||||
matrix.setTextWrap(false);
|
||||
matrix.setTextSize(2);
|
||||
matrix.setTextColor(0xFFFF); // White
|
||||
|
|
|
|||
Loading…
Reference in a new issue