Add notes, tweak doublebuffer example, move convert565 to arch.h

This commit is contained in:
Phillip Burgess 2020-03-05 22:14:54 -08:00
parent be78b3d42b
commit 20d8bc80b6
4 changed files with 250 additions and 166 deletions

116
README.md
View file

@ -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
View file

@ -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
View file

@ -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

View file

@ -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