From 20d8bc80b608313402c0ace6ac0edd001b25658c Mon Sep 17 00:00:00 2001 From: Phillip Burgess Date: Thu, 5 Mar 2020 22:14:54 -0800 Subject: [PATCH] Add notes, tweak doublebuffer example, move convert565 to arch.h --- README.md | 116 ++++++++++++++++---- arch.h | 144 +++++++++++++++++++++++++ core.c | 143 ------------------------ examples/doublebuffer/doublebuffer.ino | 13 ++- 4 files changed, 250 insertions(+), 166 deletions(-) diff --git a/README.md b/README.md index 5b08afd..1a289b7 100644 --- a/README.md +++ b/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). diff --git a/arch.h b/arch.h index 5d6107e..8df03ef 100644 --- a/arch.h +++ b/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; rownumRowPairs; row++) { + uint32_t redBit = initialRedBit; + uint32_t greenBit = initialGreenBit; + uint32_t blueBit = initialBlueBit; + for(uint8_t plane=0; planenumPlanes; plane++) { +#if defined(_PM_portToggleRegister) + uint8_t prior = core->clockMask; // Set clock bit on 1st out +#endif + for(uint16_t x=0; xclockMask; // 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_ diff --git a/core.c b/core.c index 537111b..cbf1a04 100644 --- a/core.c +++ b/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; rownumRowPairs; row++) { - uint32_t redBit = initialRedBit; - uint32_t greenBit = initialGreenBit; - uint32_t blueBit = initialBlueBit; - for(uint8_t plane=0; planenumPlanes; plane++) { -#if defined(_PM_portToggleRegister) - uint8_t prior = core->clockMask; // Set clock bit on 1st out -#endif - for(uint16_t x=0; xclockMask; // 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 - diff --git a/examples/doublebuffer/doublebuffer.ino b/examples/doublebuffer/doublebuffer.ino index 65ca6bc..6d2e7c4 100644 --- a/examples/doublebuffer/doublebuffer.ino +++ b/examples/doublebuffer/doublebuffer.ino @@ -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