Compare commits

...

10 commits

Author SHA1 Message Date
Phillip Burgess
32407dd631 Overhaul examples - more comments etc., rename "protomatter" example to "simple" 2020-10-19 13:24:57 -07:00
Phillip Burgess
316af7345d Copious notes on gamma handling, as things might change big soon 2020-10-14 10:50:31 -07:00
Phillip Burgess
a53a33ef1a Comment fix 2020-10-13 20:34:57 -07:00
Phillip Burgess
4869e64850 Tweak timer interrupt, more efficient? 2020-10-13 20:17:28 -07:00
Phillip Burgess
1615dc4c1d SAMD51 brightness working a little better 2020-10-13 18:58:52 -07:00
Phillip Burgess
6f312c642f Brightness starting to semi-work-ish on SAMD51 2020-10-13 17:08:48 -07:00
Phillip Burgess
c9ecf7c735 More SAMD juggling things around 2020-10-13 11:46:27 -07:00
Phillip Burgess
0a75527d30 Overflow interrupt setup fix 2020-10-12 16:54:12 -07:00
Phillip Burgess
a413580aa2 Gamma shenanigans underway 2020-10-12 15:44:28 -07:00
Phillip Burgess
da1c4fcc68 Starting to bring in bits of the gamma code 2020-10-11 20:02:05 -07:00
19 changed files with 929 additions and 605 deletions

View file

@ -1,8 +1,9 @@
// Play GIFs from CIRCUITPY drive (USB-accessible filesystem) to LED matrix.
// Designed for Adafruit MatrixPortal M4, but may run on some other M4 & M0
// and nRF52 boards (relies on TinyUSB stack). As written, runs on 64x32 pixel
// matrix, this can be changed by editing the addrPins[] array (height) and/or
// matrix constructor call (width).
// ***DESIGNED FOR ADAFRUIT MATRIXPORTAL M4***, but may run on some other
// M4 & M0 and nRF52 boards (relies on TinyUSB stack). As written, runs on
// 64x32 pixel matrix, this can be changed by editing the addrPins[] array
// (height) and/or matrix constructor call (width). See the "simple" example
// for a run-down on matrix configuration.
// Adapted from examples from Larry Bank's AnimatedGIF library and
// msc_external_flash example in Adafruit_TinyUSB_Arduino.
// Prerequisite libraries:

View file

@ -1,173 +1,164 @@
#include "Adafruit_Protomatter.h"
/* ----------------------------------------------------------------------
Double-buffering (smooth animation) Protomatter library example.
PLEASE SEE THE "simple" EXAMPLE FOR AN INTRODUCTORY SKETCH.
Comments here pare down many of the basics and focus on the new concepts.
/*
METRO M0 PORT-TO-PIN ASSIGNMENTS BY BYTE:
PA00 PA08 D4 PA16 D11 PB00 PB08 A1
PA01 PA09 D3 PA17 D13 PB01 PB09 A2
PA02 A0 PA10 D1 PA18 D10 PB02 A5 PB10 MOSI
PA03 PA11 D0 PA19 D12 PB03 PB11 SCK
PA04 A3 PA12 MISO PA20 D6 PB04 PB12
PA05 A4 PA13 PA21 D7 PB05 PB13
PA06 D8 PA14 D2 PA22 SDA PB06 PB14
PA07 D9 PA15 D5 PA23 SCL PB07 PB15
This example is written for a 64x32 matrix but can be adapted to others.
------------------------------------------------------------------------- */
SAME, METRO M4:
PA00 PA08 PA16 D13 PB00 PB08 A4 PB16 D3
PA01 PA09 PA17 D12 PB01 PB09 A5 PB17 D2
PA02 A0 PA10 PA18 D10 PB02 SDA PB10 PB18
PA03 PA11 PA19 D11 PB03 SCL PB11 PB19
PA04 A3 PA12 MISO PA20 D9 PB04 PB12 D7 PB20
PA05 A1 PA13 SCK PA21 D8 PB05 PB13 D4 PB21
PA06 A2 PA14 MISO PA22 D1 PB06 PB14 D5 PB22
PA07 PA15 PA23 D0 PB07 PB15 D6 PB23
#include <Adafruit_Protomatter.h>
#include <Fonts/FreeSansBold18pt7b.h> // Large friendly font
FEATHER M4:
PA00 PA08 PA16 D5 PB08 A2 PB16 D1/TX
PA01 PA09 PA17 SCK PB09 A3 PB17 D0/RX
PA02 A0 PA10 PA18 D6 PB10 PB18
PA03 PA11 PA19 D9 PB11 PB19
PA04 A4 PA12 SDA PA20 D10 PB12 PB20
PA05 A1 PA13 SCL PA21 D11 PB13 PB21
PA06 A5 PA14 D4 PA22 D12 PB14 PB22 MISO
PA07 PA15 PA23 D13 PB15 PB23 MOSI
/* ----------------------------------------------------------------------
The RGB matrix must be wired to VERY SPECIFIC pins, different for each
microcontroller board. This first section sets that up for a number of
supported boards.
------------------------------------------------------------------------- */
FEATHER M0:
PA00 PA08 PA16 D11 PB00 PB08 A1
PA01 PA09 PA17 D13 PB01 PB09 A2
PA02 A0 PA10 TX/D1 PA18 D10 PB02 A5 PB10 MOSI
PA03 PA11 RX/D0 PA19 D12 PB03 PB11 SCK
PA04 A3 PA12 MISO PA20 D6 PB04 PB12
PA05 A4 PA13 PA21 D7 PB05 PB13
PA06 PA14 PA22 SDA PB06 PB14
PA07 D9 PA15 D5 PA23 SCL PB07 PB15
FEATHER nRF52840:
P0.00 P0.08 D12 P0.24 RXD P1.08 D5
P0.01 P0.09 P0.25 TXD P1.09 D13
P0.02 A4 P0.10 D2 (NFC) P0.26 D9 P1.10
P0.03 A5 P0.11 SCL P0.27 D10 P1.11
P0.04 A0 P0.12 SDA P0.28 A3 P1.12
P0.05 A1 P0.13 MOSI P0.29 P1.13
P0.06 D11 P0.14 SCK P0.30 A2 P1.14
P0.07 D6 P0.15 MISO P0.31 P1.15
FEATHER ESP32:
P0.00 P0.08 P0.16 16/RX P0.24 P1.00 32/A7
P0.01 P0.09 P0.17 17/TX P0.25 25/A1 P1.01 33/A9/SS
P0.02 P0.10 P0.18 18/MOSI P0.26 26/A0 P1.02 34/A2 (in)
P0.03 P0.11 P0.19 19/MISO P0.27 27/A10 P1.03
P0.04 4/A5 P0.12 12/A11 P0.20 P0.28 P1.04 36/A4 (in)
P0.05 5/SCK P0.13 13/A12 P0.21 21 P0.29 P1.05
P0.06 P0.14 14/A6 P0.22 22/SCL P0.30 P1.06
P0.07 P0.15 15/A8 P0.23 23/SDA P0.31 P1.07 39/A3 (in)
RGB Matrix FeatherWing:
R1 D6 A A5
G1 D5 B A4
B1 D9 C A3
R2 D11 D A2
G2 D10 LAT D0/RX
B2 D12 OE D1/TX
CLK D13
RGB+clock in one PORT byte on Feather M4!
RGB+clock are on same PORT but not within same byte on Feather M0 --
the code could run there (with some work to be done in the convert_*
functions), but would be super RAM-inefficient. Should be fine on other
M0 devices like a Metro, if wiring manually so one can pick a contiguous
byte of PORT bits.
RGB+clock are on different PORTs on nRF52840.
*/
#if defined(__SAMD51__)
// Use FeatherWing pinout
#if defined(_VARIANT_MATRIXPORTAL_M4_) // MatrixPortal M4
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;
#elif defined(_VARIANT_FEATHER_M4_) // Feather M4 + RGB Matrix FeatherWing
uint8_t rgbPins[] = {6, 5, 9, 11, 10, 12};
uint8_t addrPins[] = {A5, A4, A3, A2};
uint8_t clockPin = 13;
uint8_t latchPin = 0;
uint8_t oePin = 1;
#elif defined(_SAMD21_)
#elif defined(__SAMD51__) // M4 Metro Variants (Express, AirLift)
uint8_t rgbPins[] = {6, 5, 9, 11, 10, 12};
uint8_t addrPins[] = {A5, A4, A3, A2};
uint8_t clockPin = 13;
uint8_t latchPin = 0;
uint8_t oePin = 1;
#elif defined(_SAMD21_) // Feather M0 variants
uint8_t rgbPins[] = {6, 7, 10, 11, 12, 13};
uint8_t addrPins[] = {0, 1, 2, 3};
uint8_t clockPin = SDA;
uint8_t latchPin = 4;
uint8_t oePin = 5;
#elif defined(NRF52_SERIES)
// Special nRF52840 FeatherWing pinout
#elif defined(NRF52_SERIES) // Special nRF52840 FeatherWing pinout
uint8_t rgbPins[] = {6, A5, A1, A0, A4, 11};
uint8_t addrPins[] = {10, 5, 13, 9};
uint8_t clockPin = 12;
uint8_t latchPin = PIN_SERIAL1_RX;
uint8_t oePin = PIN_SERIAL1_TX;
#elif defined(ESP32)
// 'Safe' pins (not overlapping any peripherals):
// 'Safe' pins, not overlapping any peripherals:
// GPIO.out: 4, 12, 13, 14, 15, 21, 27, GPIO.out1: 32, 33
// Peripheral-overlapping pins, sorted from 'most expendible':
// 16, 17 (RX, TX), 25, 26 (A0, A1), 18, 5, 9 (MOSI, SCK, MISO), 22, 23 (SCL, SDA)
// 16, 17 (RX, TX)
// 25, 26 (A0, A1)
// 18, 5, 9 (MOSI, SCK, MISO)
// 22, 23 (SCL, SDA)
uint8_t rgbPins[] = {4, 12, 13, 14, 15, 21};
uint8_t addrPins[] = {16, 17, 25, 26};
uint8_t clockPin = 27; // Must be on same port as rgbPins
uint8_t latchPin = 32;
uint8_t oePin = 33;
#elif defined(ARDUINO_TEENSY40)
uint8_t rgbPins[] = {15, 16, 17, 20, 21, 22}; // A1-A3, A6-A8, skips SDA,SCL
uint8_t rgbPins[] = {15, 16, 17, 20, 21, 22}; // A1-A3, A6-A8, skip SDA,SCL
uint8_t addrPins[] = {2, 3, 4, 5};
uint8_t clockPin = 23; // A9
uint8_t latchPin = 6;
uint8_t oePin = 9;
#elif defined(ARDUINO_TEENSY41)
uint8_t rgbPins[] = {26, 27, 38, 20, 21, 22}; // A12-14, A6-A8 (yes that's a 38, NOT 28!)
uint8_t rgbPins[] = {26, 27, 38, 20, 21, 22}; // A12-14, A6-A8
uint8_t addrPins[] = {2, 3, 4, 5};
uint8_t clockPin = 23; // A9
uint8_t latchPin = 6;
uint8_t oePin = 9;
#endif
// Last arg here enables double-buffering
Adafruit_Protomatter matrix(
64, 6, 1, rgbPins, 4, addrPins, clockPin, latchPin, oePin, true);
/* ----------------------------------------------------------------------
Matrix initialization is explained EXTENSIVELY in "simple" example sketch!
It's very similar here, but we're passing "true" for the last argument,
enabling double-buffering -- this permits smooth animation by having us
draw in a second "off screen" buffer while the other is being shown.
------------------------------------------------------------------------- */
int16_t textMin,
textX = matrix.width(),
Adafruit_Protomatter matrix(
64, // Matrix width in pixels
6, // Bit depth -- 6 here provides maximum color options
1, rgbPins, // # of matrix chains, array of 6 RGB pins for each
4, addrPins, // # of address pins (height is inferred), array of pins
clockPin, latchPin, oePin, // Other matrix control pins
true); // HERE IS THE MAGIG FOR DOUBLE-BUFFERING!
// Sundry globals used for animation ---------------------------------------
int16_t textX = matrix.width(), // Current text position (X)
textY, // Current text position (Y)
textMin, // Text pos. (X) when scrolled off left edge
hue = 0;
char str[40];
char str[50]; // Buffer to hold scrolling message text
int8_t ball[3][4] = {
{ 3, 0, 1, 1 }, // Initial X,Y pos & velocity for 3 bouncy balls
{ 3, 0, 1, 1 }, // Initial X,Y pos+velocity of 3 bouncy balls
{ 17, 15, 1, -1 },
{ 27, 4, -1, 1 }
};
uint16_t ballcolor[3]; // Colors for bouncy balls (init in setup())
const uint16_t ballcolor[3] = {
0b0000000001000000, // Dark green
0b0000000000000001, // Dark blue
0b0000100000000000 // Dark red
};
// SETUP - RUNS ONCE AT PROGRAM START --------------------------------------
void setup(void) {
Serial.begin(9600);
// Initialize matrix...
ProtomatterStatus status = matrix.begin();
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
if(status != PROTOMATTER_OK) {
// DO NOT CONTINUE if matrix setup encountered an error.
for(;;);
}
// Unlike the "simple" example, we don't do any drawing in setup().
// But we DO initialize some things we plan to animate...
// Set up the scrolling message...
sprintf(str, "Adafruit %dx%d RGB LED Matrix",
matrix.width(), matrix.height());
matrix.setFont(&FreeSansBold18pt7b); // Use nice bitmap font
matrix.setTextWrap(false); // Allow text off edge
matrix.setTextColor(0xFFFF); // White
int16_t x1, y1;
uint16_t w, h;
matrix.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); // How big is it?
textMin = -w; // All text is off left edge when it reaches this point
textY = matrix.height() / 2 - (y1 + h / 2); // Center text vertically
// Set up the colors of the bouncy balls.
ballcolor[0] = color565(0, 20, 0); // Dark green
ballcolor[1] = color565(0, 0, 20); // Dark blue
ballcolor[2] = color565(20, 0, 0); // ark red
}
// LOOP - RUNS REPEATEDLY AFTER SETUP --------------------------------------
void loop(void) {
byte i;
// Every frame, we clear the background and draw everything anew.
// This happens "in the background" with double buffering, that's
// why you don't see everything flicker. It requires double the RAM,
// so it's not practical for every situation.
// Clear background
matrix.fillScreen(0);
matrix.fillScreen(0); // Fill background black
// Bounce three balls around
for(i=0; i<3; i++) {
// Draw the big scrolly text
matrix.setCursor(textX, textY);
matrix.print(str);
// Update text position for next frame. If text goes off the
// left edge, reset its position to be off the right edge.
if((--textX) < textMin) textX = matrix.width();
// Draw the three bouncy balls on top of the text...
for(byte i=0; i<3; i++) {
// Draw 'ball'
matrix.fillCircle(ball[i][0], ball[i][1], 5, ballcolor[i]);
// Update X, Y position
// Update ball's X,Y position for next frame
ball[i][0] += ball[i][2];
ball[i][1] += ball[i][3];
// Bounce off edges
@ -177,16 +168,19 @@ void loop(void) {
ball[i][3] *= -1;
}
// Draw big scrolly text on top
matrix.setCursor(textX, 1);
matrix.print(str);
// Move text left (w/wrap), increase hue
if((--textX) < textMin) textX = matrix.width();
hue += 7;
if(hue >= 1536) hue -= 1536;
// AFTER DRAWING, A show() CALL IS REQUIRED TO UPDATE THE MATRIX!
matrix.show();
delay(20);
delay(20); // 20 milliseconds = ~50 frames/second
}
// Utility function converts 24-bit color (8 bits red, green, blue) used in
// a lot of existing graphics code down to the "565" color format used by
// Adafruit_GFX. Might get further quantized by matrix if using less than
// 6-bit depth.
uint16_t color565(uint8_t red, uint8_t green, uint8_t blue) {
return ((red & 0b11111000) << 8) |
((green & 0b11111100) << 3) |
( blue >> 3);
}

View file

@ -1,7 +1,21 @@
/* ----------------------------------------------------------------------
"Pixel dust" Protomatter library example. As written, this is
SPECIFICALLY FOR THE ADAFRUIT MATRIXPORTAL M4 with 64x32 pixel matrix.
(see "pixeldust_64x64" example for a 64x64 matrix), but could be adapted
to other Protomatter-capable boards with an attached LIS3DH accelerometer.
PLEASE SEE THE "simple" EXAMPLE FOR AN INTRODUCTORY SKETCH,
or "doublebuffer" for animation basics.
------------------------------------------------------------------------- */
#include <Wire.h> // For I2C communication
#include <Adafruit_LIS3DH.h>
#include <Adafruit_PixelDust.h> // For simulation
#include "Adafruit_Protomatter.h"
#include <Adafruit_Protomatter.h>
#define WIDTH 64 // Display width in pixels
#define HEIGHT 32 // Display height in pixels
#define MAX_FPS 45 // Maximum redraw rate, frames/second
uint8_t rgbPins[] = {7, 8, 9, 10, 11, 12};
uint8_t addrPins[] = {17, 18, 19, 20};
@ -12,12 +26,7 @@ uint8_t oePin = 16;
Adafruit_LIS3DH accel = Adafruit_LIS3DH();
Adafruit_Protomatter matrix(
64, 4, 1, rgbPins, 4, addrPins, clockPin, latchPin, oePin, true);
#define WIDTH 64 // Display width in pixels
#define HEIGHT 32 // Display height in pixels
#define MAX_FPS 45 // Maximum redraw rate, frames/second
WIDTH, 4, 1, rgbPins, 4, addrPins, clockPin, latchPin, oePin, true);
#define N_COLORS 8
#define BOX_HEIGHT 8
@ -27,7 +36,6 @@ uint16_t colors[N_COLORS];
// Sand object, last 2 args are accelerometer scaling and grain elasticity
Adafruit_PixelDust sand(WIDTH, HEIGHT, N_GRAINS, 1, 128, false);
uint32_t prevTime = 0; // Used for frames-per-second throttle
// SETUP - RUNS ONCE AT PROGRAM START --------------------------------------
@ -89,6 +97,7 @@ void setup(void) {
uint16_t color565(uint8_t red, uint8_t green, uint8_t blue) {
return ((red & 0xF8) << 8) | ((green & 0xFC) << 3) | (blue >> 3);
}
// MAIN LOOP - RUNS ONCE PER FRAME OF ANIMATION ----------------------------
void loop() {
@ -126,6 +135,4 @@ void loop() {
//Serial.printf("(%d, %d)\n", x, y);
}
matrix.show(); // Copy data to matrix buffers
}

View file

@ -1,7 +1,21 @@
/* ----------------------------------------------------------------------
"Pixel dust" Protomatter library example. As written, this is
SPECIFICALLY FOR THE ADAFRUIT MATRIXPORTAL M4 with 64x32 pixel matrix.
(see "pixeldust_64x64" example for a 64x64 matrix), but could be adapted
to other Protomatter-capable boards with an attached LIS3DH accelerometer.
PLEASE SEE THE "simple" EXAMPLE FOR AN INTRODUCTORY SKETCH,
or "doublebuffer" for animation basics.
------------------------------------------------------------------------- */
#include <Wire.h> // For I2C communication
#include <Adafruit_LIS3DH.h>
#include <Adafruit_PixelDust.h> // For simulation
#include "Adafruit_Protomatter.h"
#include <Adafruit_Protomatter.h>
#define WIDTH 64 // Display width in pixels
#define HEIGHT 64 // Display height in pixels
#define MAX_FPS 45 // Maximum redraw rate, frames/second
uint8_t rgbPins[] = {7, 8, 9, 10, 11, 12};
uint8_t addrPins[] = {17, 18, 19, 20, 21};
@ -12,12 +26,7 @@ uint8_t oePin = 16;
Adafruit_LIS3DH accel = Adafruit_LIS3DH();
Adafruit_Protomatter matrix(
64, 4, 1, rgbPins, 5, addrPins, clockPin, latchPin, oePin, true);
#define WIDTH 64 // Display width in pixels
#define HEIGHT 64 // Display height in pixels
#define MAX_FPS 45 // Maximum redraw rate, frames/second
WIDTH, 4, 1, rgbPins, 5, addrPins, clockPin, latchPin, oePin, true);
#define N_COLORS 8
#define BOX_HEIGHT 8
@ -27,7 +36,6 @@ uint16_t colors[N_COLORS];
// Sand object, last 2 args are accelerometer scaling and grain elasticity
Adafruit_PixelDust sand(WIDTH, HEIGHT, N_GRAINS, 1, 128, false);
uint32_t prevTime = 0; // Used for frames-per-second throttle
// SETUP - RUNS ONCE AT PROGRAM START --------------------------------------
@ -89,6 +97,7 @@ void setup(void) {
uint16_t color565(uint8_t red, uint8_t green, uint8_t blue) {
return ((red & 0xF8) << 8) | ((green & 0xFC) << 3) | (blue >> 3);
}
// MAIN LOOP - RUNS ONCE PER FRAME OF ANIMATION ----------------------------
void loop() {
@ -126,6 +135,4 @@ void loop() {
//Serial.printf("(%d, %d)\n", x, y);
}
matrix.show(); // Copy data to matrix buffers
}

View file

@ -1,155 +0,0 @@
#include "Adafruit_Protomatter.h"
/*
METRO M0 PORT-TO-PIN ASSIGNMENTS BY BYTE:
PA00 PA08 D4 PA16 D11 PB00 PB08 A1
PA01 PA09 D3 PA17 D13 PB01 PB09 A2
PA02 A0 PA10 D1 PA18 D10 PB02 A5 PB10 MOSI
PA03 PA11 D0 PA19 D12 PB03 PB11 SCK
PA04 A3 PA12 MISO PA20 D6 PB04 PB12
PA05 A4 PA13 PA21 D7 PB05 PB13
PA06 D8 PA14 D2 PA22 SDA PB06 PB14
PA07 D9 PA15 D5 PA23 SCL PB07 PB15
SAME, METRO M4:
PA00 PA08 PA16 D13 PB00 PB08 A4 PB16 D3
PA01 PA09 PA17 D12 PB01 PB09 A5 PB17 D2
PA02 A0 PA10 PA18 D10 PB02 SDA PB10 PB18
PA03 PA11 PA19 D11 PB03 SCL PB11 PB19
PA04 A3 PA12 MISO PA20 D9 PB04 PB12 D7 PB20
PA05 A1 PA13 SCK PA21 D8 PB05 PB13 D4 PB21
PA06 A2 PA14 MISO PA22 D1 PB06 PB14 D5 PB22
PA07 PA15 PA23 D0 PB07 PB15 D6 PB23
FEATHER M4:
PA00 PA08 PA16 D5 PB08 A2 PB16 D1/TX
PA01 PA09 PA17 SCK PB09 A3 PB17 D0/RX
PA02 A0 PA10 PA18 D6 PB10 PB18
PA03 PA11 PA19 D9 PB11 PB19
PA04 A4 PA12 SDA PA20 D10 PB12 PB20
PA05 A1 PA13 SCL PA21 D11 PB13 PB21
PA06 A5 PA14 D4 PA22 D12 PB14 PB22 MISO
PA07 PA15 PA23 D13 PB15 PB23 MOSI
FEATHER M0:
PA00 PA08 PA16 D11 PB00 PB08 A1
PA01 PA09 PA17 D13 PB01 PB09 A2
PA02 A0 PA10 TX/D1 PA18 D10 PB02 A5 PB10 MOSI
PA03 PA11 RX/D0 PA19 D12 PB03 PB11 SCK
PA04 A3 PA12 MISO PA20 D6 PB04 PB12
PA05 A4 PA13 PA21 D7 PB05 PB13
PA06 PA14 PA22 SDA PB06 PB14
PA07 D9 PA15 D5 PA23 SCL PB07 PB15
FEATHER nRF52840:
P0.00 P0.08 D12 P0.24 RXD P1.08 D5
P0.01 P0.09 P0.25 TXD P1.09 D13
P0.02 A4 P0.10 D2 (NFC) P0.26 D9 P1.10
P0.03 A5 P0.11 SCL P0.27 D10 P1.11
P0.04 A0 P0.12 SDA P0.28 A3 P1.12
P0.05 A1 P0.13 MOSI P0.29 P1.13
P0.06 D11 P0.14 SCK P0.30 A2 P1.14
P0.07 D6 P0.15 MISO P0.31 P1.15
FEATHER ESP32:
P0.00 P0.08 P0.16 16/RX P0.24 P1.00 32/A7
P0.01 P0.09 P0.17 17/TX P0.25 25/A1 P1.01 33/A9/SS
P0.02 P0.10 P0.18 18/MOSI P0.26 26/A0 P1.02 34/A2 (in)
P0.03 P0.11 P0.19 19/MISO P0.27 27/A10 P1.03
P0.04 4/A5 P0.12 12/A11 P0.20 P0.28 P1.04 36/A4 (in)
P0.05 5/SCK P0.13 13/A12 P0.21 21 P0.29 P1.05
P0.06 P0.14 14/A6 P0.22 22/SCL P0.30 P1.06
P0.07 P0.15 15/A8 P0.23 23/SDA P0.31 P1.07 39/A3 (in)
RGB Matrix FeatherWing:
R1 D6 A A5
G1 D5 B A4
B1 D9 C A3
R2 D11 D A2
G2 D10 LAT D0/RX
B2 D12 OE D1/TX
CLK D13
RGB+clock in one PORT byte on Feather M4!
RGB+clock are on same PORT but not within same byte on Feather M0 --
the code could run there (with some work to be done in the convert_*
functions), but would be super RAM-inefficient. Should be fine on other
M0 devices like a Metro, if wiring manually so one can pick a contiguous
byte of PORT bits.
RGB+clock are on different PORTs on nRF52840.
*/
#if defined(__SAMD51__)
// Use FeatherWing pinout
uint8_t rgbPins[] = {6, 5, 9, 11, 10, 12};
uint8_t addrPins[] = {A5, A4, A3, A2};
uint8_t clockPin = 13;
uint8_t latchPin = 0;
uint8_t oePin = 1;
#elif defined(_SAMD21_)
uint8_t rgbPins[] = {6, 7, 10, 11, 12, 13};
uint8_t addrPins[] = {0, 1, 2, 3};
uint8_t clockPin = SDA;
uint8_t latchPin = 4;
uint8_t oePin = 5;
#elif defined(NRF52_SERIES)
// Special nRF52840 FeatherWing pinout
uint8_t rgbPins[] = {6, A5, A1, A0, A4, 11};
uint8_t addrPins[] = {10, 5, 13, 9};
uint8_t clockPin = 12;
uint8_t latchPin = PIN_SERIAL1_RX;
uint8_t oePin = PIN_SERIAL1_TX;
#elif defined(ESP32)
// 'Safe' pins (not overlapping any peripherals):
// GPIO.out: 4, 12, 13, 14, 15, 21, 27, GPIO.out1: 32, 33
// Peripheral-overlapping pins, sorted from 'most expendible':
// 16, 17 (RX, TX), 25, 26 (A0, A1), 18, 5, 9 (MOSI, SCK, MISO), 22, 23 (SCL, SDA)
uint8_t rgbPins[] = {4, 12, 13, 14, 15, 21};
uint8_t addrPins[] = {16, 17, 25, 26};
uint8_t clockPin = 27; // Must be on same port as rgbPins
uint8_t latchPin = 32;
uint8_t oePin = 33;
#elif defined(ARDUINO_TEENSY40)
uint8_t rgbPins[] = {15, 16, 17, 20, 21, 22}; // A1-A3, A6-A8, skips SDA,SCL
uint8_t addrPins[] = {2, 3, 4, 5};
uint8_t clockPin = 23; // A9
uint8_t latchPin = 6;
uint8_t oePin = 9;
#elif defined(ARDUINO_TEENSY41)
uint8_t rgbPins[] = {26, 27, 38, 20, 21, 22}; // A12-14, A6-A8 (yes that's a 38, NOT 28!)
uint8_t addrPins[] = {2, 3, 4, 5};
uint8_t clockPin = 23; // A9
uint8_t latchPin = 6;
uint8_t oePin = 9;
#endif
Adafruit_Protomatter matrix(
64, 4, 1, rgbPins, 4, addrPins, clockPin, latchPin, oePin, false);
void setup(void) {
Serial.begin(9600);
ProtomatterStatus status = matrix.begin();
Serial.print("Protomatter begin() status: ");
Serial.println((int)status);
for(int x=0; x<matrix.width(); x++) {
uint8_t rb = x * 32 / matrix.width();
uint8_t g = x * 64 / matrix.width();
matrix.drawPixel(x, matrix.height() - 4, rb << 11);
matrix.drawPixel(x, matrix.height() - 3, g << 5);
matrix.drawPixel(x, matrix.height() - 2, rb);
matrix.drawPixel(x, matrix.height() - 1, (rb << 11) | (g << 5) | rb);
}
matrix.drawCircle(12, 10, 9, 0b1111100000000000); // Red
matrix.drawCircle(22, 14, 9, 0b0000011111100000); // Green
matrix.drawCircle(32, 18, 9, 0b0000000000011111); // Blue
matrix.println("ADAFRUIT");
matrix.show(); // Copy data to matrix buffers
}
void loop(void) {
Serial.print("Refresh FPS = ~");
Serial.println(matrix.getFrameCount());
delay(1000);
}

273
examples/simple/simple.ino Normal file
View file

@ -0,0 +1,273 @@
/* ----------------------------------------------------------------------
"Simple" Protomatter library example sketch (once you get past all
the various pin configurations at the top, and all the comments).
Shows basic use of Adafruit_Protomatter library with different devices.
This example is written for a 64x32 matrix but can be adapted to others.
Once the RGB matrix is initialized, most functions of the Adafruit_GFX
library are available for drawing -- code from other projects that use
LCDs or OLEDs can be easily adapted, or may be insightful for reference.
GFX library is documented here:
https://learn.adafruit.com/adafruit-gfx-graphics-library
------------------------------------------------------------------------- */
#include <Adafruit_Protomatter.h>
/* ----------------------------------------------------------------------
The RGB matrix must be wired to VERY SPECIFIC pins, different for each
microcontroller board. This first section sets that up for a number of
supported boards. Notes have been moved to the bottom of the code.
------------------------------------------------------------------------- */
#if defined(_VARIANT_MATRIXPORTAL_M4_) // MatrixPortal M4
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;
#elif defined(_VARIANT_FEATHER_M4_) // Feather M4 + RGB Matrix FeatherWing
uint8_t rgbPins[] = {6, 5, 9, 11, 10, 12};
uint8_t addrPins[] = {A5, A4, A3, A2};
uint8_t clockPin = 13;
uint8_t latchPin = 0;
uint8_t oePin = 1;
#elif defined(__SAMD51__) // M4 Metro Variants (Express, AirLift)
uint8_t rgbPins[] = {6, 5, 9, 11, 10, 12};
uint8_t addrPins[] = {A5, A4, A3, A2};
uint8_t clockPin = 13;
uint8_t latchPin = 0;
uint8_t oePin = 1;
#elif defined(_SAMD21_) // Feather M0 variants
uint8_t rgbPins[] = {6, 7, 10, 11, 12, 13};
uint8_t addrPins[] = {0, 1, 2, 3};
uint8_t clockPin = SDA;
uint8_t latchPin = 4;
uint8_t oePin = 5;
#elif defined(NRF52_SERIES) // Special nRF52840 FeatherWing pinout
uint8_t rgbPins[] = {6, A5, A1, A0, A4, 11};
uint8_t addrPins[] = {10, 5, 13, 9};
uint8_t clockPin = 12;
uint8_t latchPin = PIN_SERIAL1_RX;
uint8_t oePin = PIN_SERIAL1_TX;
#elif defined(ESP32)
// 'Safe' pins, not overlapping any peripherals:
// GPIO.out: 4, 12, 13, 14, 15, 21, 27, GPIO.out1: 32, 33
// Peripheral-overlapping pins, sorted from 'most expendible':
// 16, 17 (RX, TX)
// 25, 26 (A0, A1)
// 18, 5, 9 (MOSI, SCK, MISO)
// 22, 23 (SCL, SDA)
uint8_t rgbPins[] = {4, 12, 13, 14, 15, 21};
uint8_t addrPins[] = {16, 17, 25, 26};
uint8_t clockPin = 27; // Must be on same port as rgbPins
uint8_t latchPin = 32;
uint8_t oePin = 33;
#elif defined(ARDUINO_TEENSY40)
uint8_t rgbPins[] = {15, 16, 17, 20, 21, 22}; // A1-A3, A6-A8, skip SDA,SCL
uint8_t addrPins[] = {2, 3, 4, 5};
uint8_t clockPin = 23; // A9
uint8_t latchPin = 6;
uint8_t oePin = 9;
#elif defined(ARDUINO_TEENSY41)
uint8_t rgbPins[] = {26, 27, 38, 20, 21, 22}; // A12-14, A6-A8
uint8_t addrPins[] = {2, 3, 4, 5};
uint8_t clockPin = 23; // A9
uint8_t latchPin = 6;
uint8_t oePin = 9;
#endif
/* ----------------------------------------------------------------------
Okay, here's where the RGB LED matrix is actually declared...
First argument is the matrix width, in pixels. Usually 32 or
64, but might go larger if you're chaining multiple matrices.
Second argument is the "bit depth," which determines color
fidelity, applied to red, green and blue (e.g. "4" here means
4 bits red, 4 green, 4 blue = 2^4 x 2^4 x 2^4 = 4096 colors).
There is a trade-off between bit depth and RAM usage. Most
programs will tend to use either 1 (R,G,B on/off, 8 colors,
best for text, LED sand, etc.) or the maximum of 6 (best for
shaded images...though, because the GFX library was designed
for LCDs, only 5 of those bits are available for red and blue.
Third argument is the number of concurrent (parallel) matrix
outputs. THIS SHOULD ALWAYS BE "1" FOR NOW. Fourth is a uint8_t
array listing six pins: red, green and blue data out for the
top half of the display, and same for bottom half. There are
hard constraints as to which pins can be used -- they must all
be on the same PORT register, ideally all within the same byte
of that PORT.
Fifth argument is the number of "address" (aka row select) pins,
from which the matrix height is inferred. "4" here means four
address lines, matrix height is then (2 x 2^4) = 32 pixels.
16-pixel-tall matrices will have 3 pins here, 32-pixel will have
4, 64-pixel will have 5. Sixth argument is a uint8_t array
listing those pin numbers. No PORT constraints here.
Next three arguments are pin numbers for other RGB matrix
control lines: clock, latch and output enable (active low).
Clock pin MUST be on the same PORT register as RGB data pins
(and ideally in same byte). Other pins have no special rules.
Last argument is a boolean (true/false) to enable double-
buffering for smooth animation (requires 2X the RAM). See the
"doublebuffer" example for a demonstration.
------------------------------------------------------------------------- */
Adafruit_Protomatter matrix(
64, // Width of matrix (or matrix chain) in pixels
4, // Bit depth, 1-6
1, rgbPins, // # of matrix chains, array of 6 RGB pins for each
4, addrPins, // # of address pins (height is inferred), array of pins
clockPin, latchPin, oePin, // Other matrix control pins
false); // No double-buffering here (see "doublebuffer" example)
// SETUP - RUNS ONCE AT PROGRAM START --------------------------------------
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) {
// DO NOT CONTINUE if matrix setup encountered an error.
for(;;);
}
// Since this is a simple program with no animation, all the
// drawing can be done here in setup() rather than loop():
// 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, color565(level, 0, 0));
matrix.drawPixel(x, matrix.height() - 3, color565(0, level, 0));
matrix.drawPixel(x, matrix.height() - 2, color565(0, 0, level));
matrix.drawPixel(x, matrix.height() - 1, color565(level, level, level));
}
// You'll notice the ramp looks smoother as bit depth increases
// (second argument to the matrix constructor call above setup()).
// Simple shapes and text, showing GFX library calls:
matrix.drawCircle(12, 10, 9, color565(255, 0, 0)); // Red
matrix.drawRect(14, 6, 17, 17, color565(0, 255, 0)); // Green
matrix.drawTriangle(32, 9, 41, 27, 23, 27, 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
}
// Utility function converts 24-bit color (8 bits red, green, blue) used in
// a lot of existing graphics code down to the "565" color format used by
// Adafruit_GFX. Might get further quantized by matrix if using less than
// 6-bit depth.
uint16_t color565(uint8_t red, uint8_t green, uint8_t blue) {
return ((red & 0b11111000) << 8) |
((green & 0b11111100) << 3) |
( blue >> 3);
}
// LOOP - RUNS REPEATEDLY AFTER SETUP --------------------------------------
void loop(void) {
// Since there's nothing more to be drawn, this loop() function just
// shows the approximate refresh rate of the matrix at current settings.
Serial.print("Refresh FPS = ~");
Serial.println(matrix.getFrameCount());
delay(1000);
}
// MORE NOTES --------------------------------------------------------------
/*
The "RGB and clock bits on same PORT register" constraint requires
considerable planning and knowledge of the underlying microcontroller
hardware. These are some earlier notes on various devices' PORT registers
and bits and their corresponding Arduino pin numbers. You probably won't
need this -- it's all codified in the #if defined() sections at the top
of this sketch now -- but keeping it around for reference if needed.
METRO M0 PORT-TO-PIN ASSIGNMENTS BY BYTE:
PA00 PA08 D4 PA16 D11 PB00 PB08 A1
PA01 PA09 D3 PA17 D13 PB01 PB09 A2
PA02 A0 PA10 D1 PA18 D10 PB02 A5 PB10 MOSI
PA03 PA11 D0 PA19 D12 PB03 PB11 SCK
PA04 A3 PA12 MISO PA20 D6 PB04 PB12
PA05 A4 PA13 PA21 D7 PB05 PB13
PA06 D8 PA14 D2 PA22 SDA PB06 PB14
PA07 D9 PA15 D5 PA23 SCL PB07 PB15
SAME, METRO M4:
PA00 PA08 PA16 D13 PB00 PB08 A4 PB16 D3
PA01 PA09 PA17 D12 PB01 PB09 A5 PB17 D2
PA02 A0 PA10 PA18 D10 PB02 SDA PB10 PB18
PA03 PA11 PA19 D11 PB03 SCL PB11 PB19
PA04 A3 PA12 MISO PA20 D9 PB04 PB12 D7 PB20
PA05 A1 PA13 SCK PA21 D8 PB05 PB13 D4 PB21
PA06 A2 PA14 MISO PA22 D1 PB06 PB14 D5 PB22
PA07 PA15 PA23 D0 PB07 PB15 D6 PB23
FEATHER M4:
PA00 PA08 PA16 D5 PB08 A2 PB16 D1/TX
PA01 PA09 PA17 SCK PB09 A3 PB17 D0/RX
PA02 A0 PA10 PA18 D6 PB10 PB18
PA03 PA11 PA19 D9 PB11 PB19
PA04 A4 PA12 SDA PA20 D10 PB12 PB20
PA05 A1 PA13 SCL PA21 D11 PB13 PB21
PA06 A5 PA14 D4 PA22 D12 PB14 PB22 MISO
PA07 PA15 PA23 D13 PB15 PB23 MOSI
FEATHER M0:
PA00 PA08 PA16 D11 PB00 PB08 A1
PA01 PA09 PA17 D13 PB01 PB09 A2
PA02 A0 PA10 TX/D1 PA18 D10 PB02 A5 PB10 MOSI
PA03 PA11 RX/D0 PA19 D12 PB03 PB11 SCK
PA04 A3 PA12 MISO PA20 D6 PB04 PB12
PA05 A4 PA13 PA21 D7 PB05 PB13
PA06 PA14 PA22 SDA PB06 PB14
PA07 D9 PA15 D5 PA23 SCL PB07 PB15
FEATHER nRF52840:
P0.00 P0.08 D12 P0.24 RXD P1.08 D5
P0.01 P0.09 P0.25 TXD P1.09 D13
P0.02 A4 P0.10 D2 (NFC) P0.26 D9 P1.10
P0.03 A5 P0.11 SCL P0.27 D10 P1.11
P0.04 A0 P0.12 SDA P0.28 A3 P1.12
P0.05 A1 P0.13 MOSI P0.29 P1.13
P0.06 D11 P0.14 SCK P0.30 A2 P1.14
P0.07 D6 P0.15 MISO P0.31 P1.15
FEATHER ESP32:
P0.00 P0.08 P0.16 16/RX P0.24 P1.00 32/A7
P0.01 P0.09 P0.17 17/TX P0.25 25/A1 P1.01 33/A9/SS
P0.02 P0.10 P0.18 18/MOSI P0.26 26/A0 P1.02 34/A2 (in)
P0.03 P0.11 P0.19 19/MISO P0.27 27/A10 P1.03
P0.04 4/A5 P0.12 12/A11 P0.20 P0.28 P1.04 36/A4 (in)
P0.05 5/SCK P0.13 13/A12 P0.21 21 P0.29 P1.05
P0.06 P0.14 14/A6 P0.22 22/SCL P0.30 P1.06
P0.07 P0.15 15/A8 P0.23 23/SDA P0.31 P1.07 39/A3 (in)
RGB MATRIX FEATHERWING NOTES:
R1 D6 A A5
G1 D5 B A4
B1 D9 C A3
R2 D11 D A2
G2 D10 LAT D0/RX
B2 D12 OE D1/TX
CLK D13
RGB+clock fit in one PORT byte on Feather M4!
RGB+clock are on same PORT but not within same byte on Feather M0 --
the code could run there, but would be super RAM-inefficient. Avoid.
Should be fine on other M0 devices like a Metro, if wiring manually
so one can pick a contiguous byte of PORT bits.
Original RGB Matrix FeatherWing will NOT work on Feather nRF52840
because RGB+clock are on different PORTs. This was resolved by making
a unique version of the FeatherWing that works with that board!
*/

22
notes.txt Normal file
View file

@ -0,0 +1,22 @@
Code is now in the 'src' folder.
arch.h has been split out into multiple files (one per architecture),
in the 'arch' folder. There's still an arch.h file, but it mostly
#includes the device-specific headers and then defaults any undefined
items at the end.
A few functions have been renamed:
_PM_free(Protomatter_core *) is now _PM_deallocate(Protomatter_core *)
_PM_setReg(bit) is now _PM_setBit(bit)
_PM_clearReg(bit) is now _PM_clearBit(bit)
_PM_ALLOCATOR(size) is now _PM_allocate(size)
_PM_FREE(addr) is now _PM_free(addr)
_PM_timerStart() now accepts two tick intervals. First is the overflow
interval (when the per-bitplane switchover occurs), as before. Second is
when the matrix OE line is de-asserted, if this needs to occur before the
overflow interval (e.g. when brightness scaling or for gamma correction).
Most architectures will ignore the second value for now because the
necessary interrupt changes have not been made yet. Only SAMD51 uses it.
If the second interval arg is omitted, the early OE de-assert does not
occur, and the normal brief OE de-assert occurs in the bitplane switchover.

View file

@ -50,8 +50,6 @@ Adafruit_Protomatter::Adafruit_Protomatter(uint16_t bitWidth, uint8_t bitDepth,
void *timer)
: GFXcanvas16(bitWidth,
(2 << min((int)addrCount, 5)) * min((int)rgbCount, 5)) {
if (bitDepth > 6)
bitDepth = 6; // GFXcanvas16 color limit (565)
// Arguments are passed through to the C _PM_init() function which does
// some input validation and minor allocation. Return value is ignored
@ -67,8 +65,9 @@ Adafruit_Protomatter::~Adafruit_Protomatter(void) {
_PM_protoPtr = NULL;
}
ProtomatterStatus Adafruit_Protomatter::begin(void) {
ProtomatterStatus Adafruit_Protomatter::begin(uint8_t brightness) {
_PM_protoPtr = &core;
core.brightness = brightness;
return _PM_begin(&core);
}

View file

@ -67,6 +67,10 @@ public:
/*!
@brief Start a Protomatter matrix display running -- initialize
pins, timer and interrupt into existence.
@param brightness Initial matrix brightness setting,
0 = minimum (off), 255 = maximum (default if unset).
May not be supported by all architectures.
See setBrightness() notes.
@return A ProtomatterStatus status, one of:
PROTOMATTER_OK if everything is good.
PROTOMATTER_ERR_PINS if data and/or clock pins are split
@ -76,7 +80,19 @@ public:
PROTOMATTER_ERR_ARG if a bad value was passed to the
constructor.
*/
ProtomatterStatus begin(void);
ProtomatterStatus begin(uint8_t brightness = 255);
/*!
@brief Change brightness of an already-running matrix.
May not be supported by all architectures.
Brightness value is a linear scaling of the duty cycle,
NOT perceptual brightness -- a ~50% value here (127) will
halve the LEDs' duty cycle, but will likely appear more
than half as bright as 100%.
@param brightness Brightness setting, 0 = minimum (off),
255 = maximum (default if unset).
*/
void setBrightness(uint8_t brightness = 255) { core.brightness = brightness; }
/*!
@brief Process data from GFXcanvas16 to the matrix framebuffer's

10
src/arch/arch.h Normal file → Executable file
View file

@ -83,7 +83,11 @@ Timer-related macros/functions:
_PM_timerFreq: A numerical constant - the source clock rate
(in Hz) that's fed to the timer peripheral.
_PM_timerInit(void*): Initialize (but do not start) timer.
_PM_timerStart(void*,count): (Re)start timer for a given timer-tick interval.
_PM_timerStart(void*,a,b): (Re)start timer for a given timer-tick interval,
'a' is the top/overflow period (when the timer
rolls over to zero), 'b' is a second shorter
period that can be used to de-assert the OE
line early (pass 0xFFFF to ignore).
_PM_timerStop(void*): Stop timer, return current timer counter value.
_PM_timerGetCount(void*): Get current timer counter value (whether timer
is running or stopped).
@ -206,3 +210,7 @@ _PM_free: Corresponding deallocator for _PM_allocate().
#if !defined(_PM_PORT_TYPE)
#define _PM_PORT_TYPE uint32_t ///< PORT register size/type
#endif
#if !defined(_PM_maxBitplanes)
#define _PM_maxBitplanes 6 ///< RGB bit depth handled by architecture
#endif

5
src/arch/esp32.h Normal file → Executable file
View file

@ -90,9 +90,10 @@ void _PM_timerInit(void *tptr) {
}
// Set timer period, initialize count value to zero, enable timer.
IRAM_ATTR inline void _PM_timerStart(void *tptr, uint32_t period) {
IRAM_ATTR inline void _PM_timerStart(void *tptr, uint32_t top,
uint32_t match) {
hw_timer_t *timer = *(hw_timer_t **)tptr;
timerAlarmWrite(timer, period, true);
timerAlarmWrite(timer, top, true);
timerAlarmEnable(timer);
timerStart(timer);
}

4
src/arch/nrf52.h Normal file → Executable file
View file

@ -183,11 +183,11 @@ void _PM_timerInit(void *tptr) {
NVIC_EnableIRQ(timer[timerNum].IRQn);
}
inline void _PM_timerStart(void *tptr, uint32_t period) {
inline void _PM_timerStart(void *tptr, uint32_t top, uint32_t match) {
volatile NRF_TIMER_Type *tc = (volatile NRF_TIMER_Type *)tptr;
tc->TASKS_STOP = 1; // Stop timer
tc->TASKS_CLEAR = 1; // Reset to 0
tc->CC[0] = period;
tc->CC[0] = top;
tc->TASKS_START = 1; // Start timer
}

75
src/arch/samd-common.h Normal file → Executable file
View file

@ -31,7 +31,10 @@
#define _PM_wordOffset(pin) (1 - (g_APinDescription[pin].ulPin / 16))
#endif
// Arduino implementation is tied to a specific timer/counter & freq:
// Arduino implementation is tied to a specific timer/counter & freq.
// Partly because IRQs must be declared at compile-time, and partly
// because we know Arduino's already set up one of the GCLK sources
// for 48 MHz (declared above in the environment-neutral code).
#if defined(TC4)
#define _PM_TIMER_DEFAULT TC4
#define _PM_IRQ_HANDLER TC4_Handler
@ -39,22 +42,6 @@
#define _PM_TIMER_DEFAULT TC3
#define _PM_IRQ_HANDLER TC3_Handler
#endif
#define _PM_timerFreq 48000000
// Partly because IRQs must be declared at compile-time, and partly
// because we know Arduino's already set up one of the GCLK sources
// for 48 MHz.
// Because it's tied to a specific timer right now, there can be only
// one instance of the Protomatter_core struct. The Arduino library
// sets up this pointer when calling begin().
void *_PM_protoPtr = NULL;
// Timer interrupt service routine
void _PM_IRQ_HANDLER(void) {
// Clear overflow flag:
_PM_TIMER_DEFAULT->COUNT16.INTFLAG.reg = TC_INTFLAG_OVF;
_PM_row_handler(_PM_protoPtr); // In core.c
}
#elif defined(CIRCUITPY) // COMPILING FOR CIRCUITPYTHON --------------------
@ -74,25 +61,49 @@ void _PM_IRQ_HANDLER(void) {
#define _PM_wordOffset(pin) (1 - ((pin & 31) / 16))
#endif
// CircuitPython implementation is tied to a specific freq (but the counter
// is dynamically allocated):
#define _PM_timerFreq 48000000
// As currently implemented, there can be only one instance of the
// Protomatter_core struct. This pointer is set up when starting the matrix.
void *_PM_protoPtr = NULL;
// Timer interrupt service routine
void _PM_IRQ_HANDLER(void) {
((Tc *)(((Protomatter_core *)_PM_protoPtr)->timer))->COUNT16.INTFLAG.reg =
TC_INTFLAG_OVF;
_PM_row_handler(_PM_protoPtr); // In core.c
}
#else // END CIRCUITPYTHON -------------------------------------------------
// Byte offset macros, timer and ISR work for other environments go here.
#endif
// SETTINGS COMMON TO ALL ENVIRONMENTS -------------------------------------
// Keep this down here because there's environment-specific defines above.
#define _PM_maxBitplanes 16 ///< RGB bit depth handled by architecture
// Arduino and CircuitPython implementations both rely on a 48 MHz timer.
// In Arduino the timer # is fixed, in CircuitPython it's dynamically
// allocated from a timer pool. If that's not the case in other
// enviromnents, this will need to be broken out into each section.
#define _PM_timerFreq 48000000
// As currently implemented, there can be only one instance of the
// Protomatter_core struct. This pointer is initialized when starting
// the matrix (in the environment-specific code -- e.g. matrix.begin()
// in the Arduino library).
void *_PM_protoPtr = NULL;
// Timer interrupt service routine. _PM_IRQ_HANDLER *must* be defined by
// environment (as with Arduino above) to trigger as an interrupt, else
// this is a function that must be invoked by interrupt function elsewhere
// (as with CircuitPython).
void _PM_IRQ_HANDLER(void) {
Protomatter_core *core = (Protomatter_core *)_PM_protoPtr;
Tc *timer = core->timer;
// If overflow flag is set, clear both the overflow AND match-compare
// bits (sometimes both are set if the two trigger in close proximity),
// handle the overflow case (load new row in core.c), don't bother with
// the match compare now, it's unneeded.
if (timer->COUNT16.INTFLAG.bit.OVF) {
timer->COUNT16.INTFLAG.reg = TC_INTFLAG_OVF | TC_INTFLAG_MC1;
_PM_row_handler(core);
} else {
// Otherwise, must be the match-compare bit only. Clear just the MC1
// flag (but not OVF) and disable the LEDs early (in core.c).
timer->COUNT16.INTFLAG.reg = TC_INTFLAG_MC1;
_PM_matrix_oe_off(core);
}
}
#endif // END SAMD51/SAMD21

21
src/arch/samd21.h Normal file → Executable file
View file

@ -43,6 +43,11 @@
// CODE COMMON TO ALL ENVIRONMENTS -----------------------------------------
// Note that the SAMD21 and SAMD51 functions are nearly but not actually
// identical -- the registers involved are different between the two.
// If any additions or bug fixes are made in one, check the other to see if
// similar changes should be made there.
// Initialize, but do not start, timer
void _PM_timerInit(void *tptr) {
static const struct {
@ -99,8 +104,8 @@ void _PM_timerInit(void *tptr) {
while (tc->COUNT16.STATUS.bit.SYNCBUSY)
;
// Overflow interrupt
tc->COUNT16.INTENSET.reg = TC_INTENSET_OVF;
// Enable overflow and match-compare-1 interrupts
tc->COUNT16.INTENSET.reg = TC_INTENSET_OVF | TC_INTENSET_MC1;
NVIC_DisableIRQ(timer[timerNum].IRQn);
NVIC_ClearPendingIRQ(timer[timerNum].IRQn);
@ -113,12 +118,20 @@ void _PM_timerInit(void *tptr) {
// Set timer period, initialize count value to zero, enable timer.
// Timer must be initialized to 16-bit mode using the init function
// above, but must be inactive before calling this.
inline void _PM_timerStart(void *tptr, uint32_t period) {
// 'top' is the timer rollover period, 'match' is a compare-match value
// that can be used to de-assert the matrix OE line early (e.g. for
// brightness or gamma correction).
// This does not test the args for validity! Calling func should handle.
// It's OK if match exceeds top; interrupt simply doesn't happen then.
inline void _PM_timerStart(void *tptr, uint32_t top, uint32_t match) {
Tc *tc = (Tc *)tptr; // Cast peripheral address passed in
tc->COUNT16.COUNT.reg = 0;
while (tc->COUNT16.STATUS.bit.SYNCBUSY)
;
tc->COUNT16.CC[0].reg = period;
tc->COUNT16.CC[0].reg = top;
while (tc->COUNT16.STATUS.bit.SYNCBUSY)
;
tc->COUNT16.CC[1].reg = match;
while (tc->COUNT16.STATUS.bit.SYNCBUSY)
;
tc->COUNT16.CTRLA.bit.ENABLE = 1;

21
src/arch/samd51.h Normal file → Executable file
View file

@ -54,6 +54,11 @@
// CODE COMMON TO ALL ENVIRONMENTS -----------------------------------------
// Note that the SAMD21 and SAMD51 functions are nearly but not actually
// identical -- the registers involved are different between the two.
// If any additions or bug fixes are made in one, check the other to see if
// similar changes should be made there.
// Initialize, but do not start, timer
void _PM_timerInit(void *tptr) {
static const struct {
@ -147,8 +152,8 @@ void _PM_timerInit(void *tptr) {
while (tc->COUNT16.SYNCBUSY.bit.CTRLB)
;
// Overflow interrupt
tc->COUNT16.INTENSET.reg = TC_INTENSET_OVF;
// Enable overflow and match-compare-1 interrupts
tc->COUNT16.INTENSET.reg = TC_INTENSET_OVF | TC_INTENSET_MC1;
NVIC_DisableIRQ(timer[timerNum].IRQn);
NVIC_ClearPendingIRQ(timer[timerNum].IRQn);
@ -161,14 +166,22 @@ void _PM_timerInit(void *tptr) {
// Set timer period, initialize count value to zero, enable timer.
// Timer must be initialized to 16-bit mode using the init function
// above, but must be inactive before calling this.
inline void _PM_timerStart(void *tptr, uint32_t period) {
// 'top' is the timer rollover period, 'match' is a compare-match value
// that can be used to de-assert the matrix OE line early (e.g. for
// brightness or gamma correction), or pass 0xFFFFFFFF to ignore.
// This does not test the args for validity! Calling func should handle.
// It's OK if match exceeds top; interrupt simply doesn't happen then.
inline void _PM_timerStart(void *tptr, uint32_t top, uint32_t match) {
Tc *tc = (Tc *)tptr; // Cast peripheral address passed in
tc->COUNT16.COUNT.reg = 0;
while (tc->COUNT16.SYNCBUSY.bit.COUNT)
;
tc->COUNT16.CC[0].reg = period;
tc->COUNT16.CC[0].reg = top;
while (tc->COUNT16.SYNCBUSY.bit.CC0)
;
tc->COUNT16.CC[1].reg = match;
while (tc->COUNT16.SYNCBUSY.bit.CC1)
;
tc->COUNT16.CTRLA.bit.ENABLE = 1;
while (tc->COUNT16.SYNCBUSY.bit.STATUS)
;

4
src/arch/stm32.h Normal file → Executable file
View file

@ -114,10 +114,10 @@ void _PM_timerInit(void *tptr) {
NVIC_SetPriority(tim_irq, 0); // Top priority
}
inline void _PM_timerStart(void *tptr, uint32_t period) {
inline void _PM_timerStart(void *tptr, uint32_t top, uint32_t match) {
TIM_TypeDef *tim = tptr;
tim->SR = 0;
tim->ARR = period;
tim->ARR = top;
tim->CR1 |= TIM_CR1_CEN;
tim->DIER |= TIM_DIER_UIE;
HAL_NVIC_EnableIRQ(stm_peripherals_timer_get_irqnum(tim));

6
src/arch/teensy4.h Normal file → Executable file
View file

@ -130,11 +130,11 @@ void _PM_timerInit(void *tptr) {
}
// Set timer period, initialize count value to zero, enable timer.
inline void _PM_timerStart(void *tptr, uint32_t period) {
inline void _PM_timerStart(void *tptr, uint32_t top, uint32_t match) {
IMXRT_PIT_CHANNEL_t *timer = (IMXRT_PIT_CHANNEL_t *)tptr;
timer->TCTRL = 0; // Disable timer and interrupt
timer->LDVAL = period; // Set load value
// timer->CVAL = period; // And current value (just in case?)
timer->LDVAL = top; // Set load value
// timer->CVAL = top; // And current value (just in case?)
timer->TFLG = 1; // Clear timer interrupt
timer->TCTRL = 3; // Enable timer and interrupt
}

View file

@ -1,4 +1,5 @@
/*!
p
* @file core.c
*
* Part of Adafruit's Protomatter library for HUB75-style RGB LED matrices.
@ -56,6 +57,12 @@
// arch.h) because it's not architecture-specific.
#define _PM_ROW_DELAY 8 ///< Delay time between row address line changes (ms)
// Gamma correction is provided if the number of bitplanes requested
// exceeds 6 bits (the limit of RGB565 color fidelity). Gamma correction
// makes intermediate shades more perceptually linear, but incurs more RAM
// use and processor load.
#define _PM_GAMMA 2.6 ///< Exponent for pow() function in gamma setting
// These are the lowest-level functions for issing data to matrices.
// There are three versions because it depends on how the six RGB data bits
// (and clock bit) are arranged within a 32-bit PORT register. If all six
@ -72,10 +79,10 @@ static void blast_byte(Protomatter_core *core, uint8_t *data);
static void blast_word(Protomatter_core *core, uint16_t *data);
static void blast_long(Protomatter_core *core, uint32_t *data);
#define _PM_clearReg(x) \
#define _PM_clearBit(x) \
(*(volatile _PM_PORT_TYPE *)((x).clearReg) = \
((x).bit)) ///< Clear non-RGB-data-or-clock control line (_PM_pin type)
#define _PM_setReg(x) \
#define _PM_setBit(x) \
(*(volatile _PM_PORT_TYPE *)((x).setReg) = \
((x).bit)) ///< Set non-RGB-data-or-clock control line (_PM_pin type)
@ -94,9 +101,8 @@ ProtomatterStatus _PM_init(Protomatter_core *core, uint16_t bitWidth,
rgbCount = 5; // Max 5 in parallel (32-bit PORT)
if (addrCount > 5)
addrCount = 5; // Max 5 address lines (A-E)
// bitDepth is NOT constrained here, handle in calling function
// (varies with implementation, e.g. GFX lib is max 6 bitplanes,
// but might be more or less elsewhere)
if (bitDepth > _PM_maxBitplanes)
bitDepth = _PM_maxBitplanes; // Varies with architecture & environment
#if defined(_PM_TIMER_DEFAULT)
// If NULL timer was passed in (the default case for the constructor),
@ -105,6 +111,8 @@ ProtomatterStatus _PM_init(Protomatter_core *core, uint16_t bitWidth,
if (timer == NULL)
timer = _PM_TIMER_DEFAULT;
#else
// Some environments (e.g. CircuitPython) don't use a default timer --
// it's allocated from a pool, period.
if (timer == NULL)
return PROTOMATTER_ERR_ARG;
#endif
@ -303,13 +311,67 @@ ProtomatterStatus _PM_begin(Protomatter_core *core) {
}
}
// Set up remap_rb and remap_g tables, which assist in quickly converting
// RGB565 pixel values (from the image canvas) to the number of bitplanes
// allocated to the matrix (not always a simple shift).
if (core->numPlanes < 6) {
// 5 or fewer bitplanes, decimate 5-bit red+blue and 6-bit green to
// that many planes. Shift right, in-to-out conversion is linear.
uint8_t shift = 5 - core->numPlanes; // Might be zero, that's OK
for (uint8_t i = 0; i < 32; i++) {
core->remap_rb[i] = i >> shift;
}
shift = 6 - core->numPlanes;
for (uint8_t i = 0; i < 64; i++) {
core->remap_g[i] = i >> shift;
}
} else if (core->numPlanes == 6) {
// 6 bitplanes exactly, 6-bit green is preserved, 5-bit red+blue
// is expanded to 6 bits, in-to-out conversion is still linear.
for (uint8_t i = 0; i < 32; i++) {
core->remap_rb[i] = (i << 1) | (i >> 4); // Copy msb to lsb
}
for (uint8_t i = 0; i < 64; i++) {
core->remap_g[i] = i;
}
} else {
// Above 6 bitplanes, gamma correction kicks in, in-to-out conversion
// is no longer linear, aiming for perceptual linearity instead. 5-bit
// red+blue and 6-bit green are expanded to the number of bitplanes
// requested (10 should be ample, but you can use more or less to
// balance accuracy vs RAM & processor load).
float top = (float)((1 << core->numPlanes) - 1);
for (uint8_t i = 0; i < 32; i++) { // 5 bits red, blue
core->remap_rb[i] =
(uint16_t)(pow((float)i / 31.0, _PM_GAMMA) * top + 0.5);
}
for (uint8_t i = 0; i < 64; i++) { // 6 bits green
core->remap_g[i] =
(uint16_t)(pow((float)i / 63.0, _PM_GAMMA) * top + 0.5);
}
}
// TO DO: this now needs to evolve for gamma correction.
// There's now two periods, the 'top' and the 'match' value for
// each plane. top can't be less than minMinPeriod, but match
// can. There may be several planes (least bits) that all have
// the same top value, but different match values.
// Match will always be powers-of-two, top will be whatever it
// takes to get the frame rate.
// Estimate minimum bitplane #0 period for _PM_MAX_REFRESH_HZ rate.
uint32_t minPeriodPerFrame = _PM_timerFreq / _PM_MAX_REFRESH_HZ;
uint32_t minPeriodPerLine = minPeriodPerFrame / core->numRowPairs;
core->minPeriod = minPeriodPerLine / ((1 << core->numPlanes) - 1);
#if 0
if (core->minPeriod < _PM_minMinPeriod) {
core->minPeriod = _PM_minMinPeriod;
}
#else
if (core->minPeriod < 1) {
core->minPeriod = 1;
}
#endif
// Actual frame rate may be lower than this...it's only an estimate
// and does not factor in things like address line selection delays
// or interrupt overhead. That's OK, just don't want to exceed this
@ -317,7 +379,11 @@ ProtomatterStatus _PM_begin(Protomatter_core *core) {
// Make a wild guess for the initial bit-zero interval. It's okay
// that this is off, code adapts to actual timer results pretty quick.
#if 0
core->bitZeroPeriod = core->width * 5; // Initial guesstimate
#else
core->bitZeroPeriod = core->minPeriod;
#endif
core->activeBuffer = 0;
@ -391,7 +457,7 @@ void _PM_stop(Protomatter_core *core) {
while (core->swapBuffers)
; // Wait for any pending buffer swap
_PM_timerStop(core->timer); // Halt timer
_PM_setReg(core->oe); // Set OE HIGH (disable output)
_PM_setBit(core->oe); // Set OE HIGH (disable output)
// So, in PRINCIPLE, setting OE high would be sufficient...
// but in case that pin is shared with another function such
// as the onloard LED (which pulses during bootloading) let's
@ -408,8 +474,8 @@ void _PM_stop(Protomatter_core *core) {
_PM_clockHoldLow;
}
// Latch data
_PM_setReg(core->latch);
_PM_clearReg(core->latch);
_PM_setBit(core->latch);
_PM_clearBit(core->latch);
}
}
@ -423,7 +489,7 @@ void _PM_resume(Protomatter_core *core) {
core->frameCount = 0;
_PM_timerInit(core->timer); // Configure timer
_PM_timerStart(core->timer, 1000); // Start timer
_PM_timerStart(core->timer, 1000, 0xFFFF); // Start timer
}
}
@ -443,35 +509,35 @@ void _PM_deallocate(Protomatter_core *core) {
}
}
// ISR function (in arch.h) calls this function which it extern'd.
// Profuse apologies for the ESP32-specific IRAM_ATTR here -- the goal was
// for all architecture-specific detauls to be in arch.h -- but the need
// for one here caught me off guard. So, in arch.h, for all non-ESP32
// devices, IRAM_ATTR is defined to nothing and is ignored here. If any
// future architectures have their own attribute for making a function
// RAM-resident, #define IRAM_ATTR to that in the corresponding device-
// specific section of arch.h. Sorry. :/
// ISR function (in arch-specific header) calls this function which it
// extern'd. Profuse apologies for the ESP32-specific IRAM_ATTR here --
// the goal was for all architecture-specific detauls to be in the
// aforementioned header -- but the need for one here caught me off guard.
// So, in those headers, for all non-ESP32 devices, IRAM_ATTR is defined to
// nothing and is ignored here. If any future architectures have their own
// attribute for making a function RAM-resident, #define IRAM_ATTR to that
// in the corresponding device-specific file. Sorry. :/
// Any functions called by this function should also be IRAM_ATTR'd.
IRAM_ATTR void _PM_row_handler(Protomatter_core *core) {
_PM_setReg(core->oe); // Disable LED output
_PM_setBit(core->oe); // Disable LED output
// ESP32 requires this next line, but not wanting to put arch-specific
// ifdefs in this code...it's a trivial operation so just do it.
// Latch is already clear at this point, but we go through the motions
// to clear it again in order to sync up the setReg(OE) above with the
// setReg(latch) that follows. Reason being, bit set/clear operations
// to clear it again in order to sync up the setBit(OE) above with the
// setBit(latch) that follows. Reason being, bit set/clear operations
// on ESP32 aren't truly atomic, and if those two pins are on the same
// port (quite common) the second setReg will be ignored. The nonsense
// clearReg is used to sync up the two setReg operations. See also the
// port (quite common) the second setBit will be ignored. The nonsense
// clearBit is used to sync up the two setBit operations. See also the
// ESP32-specific PEW define in arch.h, same deal.
_PM_clearReg(core->latch);
_PM_clearBit(core->latch);
_PM_setReg(core->latch);
_PM_setBit(core->latch);
// Stop timer, save count value at stop
uint32_t elapsed = _PM_timerStop(core->timer);
uint8_t prevPlane = core->plane; // Save that plane # for later timing
_PM_clearReg(core->latch); // (split to add a few cycles)
_PM_clearBit(core->latch); // (split to add a few cycles)
// If plane 0 just finished being displayed (plane 1 was loaded on prior
// pass, or there's only one plane...I know, it's confusing), take note
@ -513,9 +579,9 @@ IRAM_ATTR void _PM_row_handler(Protomatter_core *core) {
line++, bit <<= 1) {
if ((core->row & bit) != (core->prevRow & bit)) {
if (core->row & bit) { // Set addr line high
_PM_setReg(core->addr[line]);
_PM_setBit(core->addr[line]);
} else { // Set addr line low
_PM_clearReg(core->addr[line]);
_PM_clearBit(core->addr[line]);
}
_PM_delayMicroseconds(_PM_ROW_DELAY);
}
@ -545,9 +611,81 @@ IRAM_ATTR void _PM_row_handler(Protomatter_core *core) {
// now while the next plane data is loaded.
// Set timer and enable LED output for data loaded on PRIOR pass:
_PM_timerStart(core->timer, core->bitZeroPeriod << prevPlane);
uint32_t top = core->bitZeroPeriod << prevPlane;
uint32_t match = top * (core->brightness + 1) / 256;
if (top == match) { // Don't need match interrupt
match = 0xFFFF;
}
if (match > 0) {
_PM_timerStart(core->timer, top, match);
_PM_delayMicroseconds(1); // Appease Teensy4
_PM_clearReg(core->oe); // Enable LED output
_PM_clearBit(core->oe); // Enable LED output
} else {
// If match time is 0, don't enable LEDs, just run for bitplane time.
_PM_timerStart(core->timer, top, 0xFFFF);
}
/* --------------------------------------------------------------------
So, I'm at an impasse here. Going to push the code and take a moment to
regroup. At present, the library WILL allow more than 6 bitplanes and
WILL attempt to apply gamma correction and brightness, but this WILL
NOT LOOK CORRECT on the matrix (though maybe a marginal improvement vs.
the prior 6-bit limit).
Issue at the moment is timer/counter interrupt handling on SAMD. This
function we're in is called from a timer ISR. The single timer ISR
handles two different situations -- timer overflow (in which case this
function is called, loading new data to the matrix) and timer match
(disabling the matrix OE, different function). It used to ONLY handle
the overflow case, because we had few enough bitplanes that the timer
match wasn't required ... but gamma correction and brightness will
both require it.
On SAMD, timer/counters have a single interrupt handler. There are
multiple interrupt flags for different situations (overflow vs match),
but both go to a single interrupt handler...and, therefore, it can't
interrupt itself. Thus the compare-match CAN'T trigger until the
overflow interrupt (which is issuing data to the matrix) completes...
and this totally throws off the least-bit timing, and this ruins gamma
correction.
There are several possible workarounds:
1) Use a TCC rather than a TC peripheral. These have distinct
interrupts for overflow vs. compare match, and one should be able
to interrupt the other. This would require identifying a "safe" TCC
to use for Arduino, and I don't know how CircuitPython doles out
TCCs as opposed to TCs. Also, the timer setup and interrupt code
would need to change as the peripheral registers are different.
Possible, but messy. Also, unsure of applicability to other devices.
2) Use hardware PWM for the matrix OE line, and don't use the compare
match interrupt. Since the WHOLE GOAL of Protomatter was to avoid
tying ourselves to specific pin numbers and use mundane GPIO
wherever possible (exception for the RGB+clock constrtaints), this
option is OFF THE TABLE.
3) Do the matrix data-loading while OE is OFF, rather than the current
strategy of loading the next bitplane while the prior plane is ON
(which is done to maximize matrix brightness -- it's off as little
as possible). Larger matrices, or longer matrix chains, will incur
more off time, with a corresponding impact on brightness. We're
realistically talking some small percentage though...maybe 10%,
amd given how bright these are at full-blast, that's probably
acceptable. Getting this to work WELL though -- balancing a desired
refresh rate against an acceptable CPU load -- this will require an
empirical step of determining how many timer cycles are needed to
shift out a known number of bits (or alternately, number of bits per
cycle), which will need to be repeated for each architecture (and on
SAMD, each clock speed). This is the MOST LIKELY approach to be
used, just accepting the slight brightness hit and the collecting
timing data step. (A "golden option" here would be to issue data
during OE off on least bitplanes, switching to during OE on for
higher (longer duration) bitplanes...but this would require some
extra math to figure out the refresh rate throttling, weird handling
of the current-bitplane counter, and all-around hurts my brain, so
I'm skipping it for now.)
----------------------------------------------------------------------- */
uint32_t elementsPerLine =
_PM_chunkSize * ((core->width + (_PM_chunkSize - 1)) / _PM_chunkSize);
@ -569,6 +707,12 @@ IRAM_ATTR void _PM_row_handler(Protomatter_core *core) {
// 'plane' data is now loaded, will be shown on NEXT pass
}
// ISR function (in arch-specific header) calls this function which it
// extern'd. Same apologies as above.
IRAM_ATTR void _PM_matrix_oe_off(Protomatter_core *core) {
_PM_setBit(core->oe); // Disable LED output
}
// Innermost data-stuffing loop functions
// The presence of a bit-toggle register can make the data-stuffing loop a
@ -824,7 +968,8 @@ void _PM_swapbuffer_maybe(Protomatter_core *core) {
#if defined(ARDUINO) || defined(CIRCUITPY)
// Arduino and CircuitPython happen to use the same internal canvas
// representation.
// representation. This won't necessarily be true for other environments,
// can't really say.
// 16-bit (565) color conversion functions go here (rather than in the
// Arduino lib .cpp) because knowledge is required of chunksize and the
@ -859,26 +1004,24 @@ __attribute__((noinline)) void _PM_convert_565_byte(Protomatter_core *core,
dest += core->bufferSize * (1 - core->activeBuffer);
}
#if defined(_PM_portToggleRegister)
#if !defined(_PM_STRICT_32BIT_IO)
#if defined(_PM_portToggleRegister) && !defined(_PM_STRICT_32BIT_IO)
// core->clockMask mask is already an 8-bit value
uint8_t clockMask = core->clockMask;
#else
// core->clockMask mask is 32-bit, shift down to 8-bit for this func.
uint8_t clockMask = core->clockMask >> (core->portOffset * 8);
#endif
#endif
// 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):
// Determine matrix bytes per bitplane & row pair
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
uint32_t rowSize = bitplaneSize * core->numPlanes;
// Skip initial scanline padding if present (HUB75 matrices shift data
// in from right-to-left, so if we need scanline padding it occurs at
@ -887,87 +1030,71 @@ __attribute__((noinline)) void _PM_convert_565_byte(Protomatter_core *core,
// 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 = clockMask; // Set clock bit on 1st out
uint8_t prior[16]; // Initial bit state for each plane
// At row start, set 'prior' value for each bitplane to clock mask
memset(prior, clockMask, core->numPlanes);
#endif
// This has been juggled around from prior code (loop hierarchy was
// row.plane.column, now row.column.plane) to reduce the amount of
// shifting and masking needed in the column loop, while the addition
// of the remap_rb[] and remap_g[] tables allow all bit depths to be
// handled similarly, no special bit checks. It does require a bit of
// jumping around with the dest offset, but still saves much math.
uint32_t offset;
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
uint16_t rgb565 = upperSrc[x]; // Pixel in upper half
uint16_t upperRed = core->remap_rb[rgb565 >> 11];
uint16_t upperGreen = core->remap_g[(rgb565 >> 5) & 0x3F];
uint16_t upperBlue = core->remap_rb[rgb565 & 0x1F];
rgb565 = lowerSrc[x]; // Pixel in lower half
uint16_t lowerRed = core->remap_rb[rgb565 >> 11];
uint16_t lowerGreen = core->remap_g[(rgb565 >> 5) & 0x3F];
uint16_t lowerBlue = core->remap_rb[rgb565 & 0x1F];
uint16_t planeMask = 1; // Start from LSB, work up
offset = x; // Offset into current dest pointer
for (uint8_t plane = 0; plane < core->numPlanes; plane++) {
uint8_t result = 0;
if (upperRGB & redBit)
if (upperRed & planeMask)
result |= pinMask[0];
if (upperRGB & greenBit)
if (upperGreen & planeMask)
result |= pinMask[1];
if (upperRGB & blueBit)
if (upperBlue & planeMask)
result |= pinMask[2];
if (lowerRGB & redBit)
if (lowerRed & planeMask)
result |= pinMask[3];
if (lowerRGB & greenBit)
if (lowerGreen & planeMask)
result |= pinMask[4];
if (lowerRGB & blueBit)
if (lowerBlue & planeMask)
result |= pinMask[5];
#if defined(_PM_portToggleRegister)
dest[x] = result ^ prior;
prior = result | clockMask; // Set clock bit on next out
dest[offset] = result ^ prior[plane];
prior[plane] = result | clockMask; // Set clock bit on next out
#else
dest[x] = result;
dest[offset] = 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] &= ~clockMask; // Negative index is legal & intentional
#endif
dest += bitplaneSize; // Advance one scanline in dest buffer
offset += bitplaneSize; // Same column, next bitplane
planeMask <<= 1;
} // end plane
} // end x
#if defined(_PM_portToggleRegister)
// If using bit-toggle register, erase clock toggle bit on 1st element
// of each bitplane & row pair. The matrix-driving interrupt functions
// correspondingly set the clock low before finishing. This is just
// 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 dev stage just want the scope readable.
offset = -pad; // Negative index is legal & intentional
for (uint8_t plane = 0; plane < core->numPlanes; plane++) {
dest[offset] &= ~clockMask;
offset += bitplaneSize;
}
#endif
dest += rowSize; // Advance one scanline in dest buffer
upperSrc += width; // Advance one scanline in source buffer
lowerSrc += width;
} // end row
@ -991,40 +1118,23 @@ void _PM_convert_565_word(Protomatter_core *core, uint16_t *source,
_PM_chunkSize *
((width + (_PM_chunkSize - 1)) / _PM_chunkSize); // 1 plane of row pair
uint8_t pad = bitplaneSize - width; // Start-of-plane pad
uint32_t rowSize = bitplaneSize * core->numPlanes;
uint32_t initialRedBit, initialGreenBit, initialBlueBit;
if (core->numPlanes == 6) {
initialRedBit = 0b1000000000000000; // MSB red
initialGreenBit = 0b0000000000100000; // LSB green
initialBlueBit = 0b0000000000010000; // MSB blue
} else {
uint8_t shiftLeft = 5 - core->numPlanes;
initialRedBit = 0b0000100000000000 << shiftLeft;
initialGreenBit = 0b0000000001000000 << shiftLeft;
initialBlueBit = 0b0000000000000001 << shiftLeft;
}
uint32_t offset; // Current position in the 'dest' buffer
// Unlike the 565 byte converter, the word converter DOES clear out the
// matrix buffer (because each chain is OR'd into place). If a toggle
// register exists, "clear" really means the clock mask is set in all
// but the first element on a scanline (per bitplane). If no toggle
// register, can just zero everything out.
#if defined(_PM_portToggleRegister)
// No per-chain loop is required; one clock bit handles all chains
uint32_t offset = 0; // Current position in the 'dest' buffer
uint16_t mask = core->clockMask >> (core->portOffset * 16);
for (uint8_t row = 0; row < core->numRowPairs; row++) {
for (uint8_t plane = 0; plane < core->numPlanes; plane++) {
dest[offset++] = 0; // First element of each plane
for (uint16_t x = 1; x < bitplaneSize; x++) { // All subsequent items
dest[offset++] = mask;
}
}
}
#if defined(_PM_portToggleRegister) && !defined(_PM_STRICT_32BIT_IO)
// core->clockMask mask is already an 16-bit value
uint16_t clockMask = core->clockMask;
#else
memset(dest, 0, core->bufferSize);
// core->clockMask mask is 32-bit, shift down to 16-bit for this func.
uint16_t clockMask = core->clockMask >> (core->portOffset * 16);
#endif
// Clear matrix buffer (because each chain's data is OR'd into place).
// If _PM_portToggleRegister is used, no need to specially handle the
// clock bit, the 'prior' var takes care of that across each row/plane.
memset(dest, 0, core->bufferSize);
dest += pad; // Pad value is in 'elements,' not bytes, so this is OK
// After a set of rows+bitplanes are processed, upperSrc and lowerSrc
@ -1034,57 +1144,60 @@ void _PM_convert_565_word(Protomatter_core *core, uint16_t *source,
for (uint8_t chain = 0; chain < core->parallel; chain++) {
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)
uint16_t prior[16]; // Initial bit state for each plane
// Since we're ORing in bits over an existing clock bit,
// prior is 0 rather than clockMask as in the byte case.
uint16_t prior = 0;
memset(prior, 0, core->numPlanes * sizeof(uint16_t));
#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
uint16_t rgb565 = upperSrc[x]; // Pixel in upper half
uint16_t upperRed = core->remap_rb[rgb565 >> 11];
uint16_t upperGreen = core->remap_g[(rgb565 >> 5) & 0x3F];
uint16_t upperBlue = core->remap_rb[rgb565 & 0x1F];
rgb565 = lowerSrc[x]; // Pixel in lower half
uint16_t lowerRed = core->remap_rb[rgb565 >> 11];
uint16_t lowerGreen = core->remap_g[(rgb565 >> 5) & 0x3F];
uint16_t lowerBlue = core->remap_rb[rgb565 & 0x1F];
uint16_t planeMask = 1; // Start from LSB, work up
offset = x; // Offset into current dest pointer
for (uint8_t plane = 0; plane < core->numPlanes; plane++) {
uint16_t result = 0;
if (upperRGB & redBit)
if (upperRed & planeMask)
result |= pinMask[0];
if (upperRGB & greenBit)
if (upperGreen & planeMask)
result |= pinMask[1];
if (upperRGB & blueBit)
if (upperBlue & planeMask)
result |= pinMask[2];
if (lowerRGB & redBit)
if (lowerRed & planeMask)
result |= pinMask[3];
if (lowerRGB & greenBit)
if (lowerGreen & planeMask)
result |= pinMask[4];
if (lowerRGB & blueBit)
if (lowerBlue & planeMask)
result |= pinMask[5];
#if defined(_PM_portToggleRegister)
// Main difference here vs byte converter is each chain
// ORs new bits into place (vs single-pass overwrite).
#if defined(_PM_portToggleRegister)
dest[x] |= result ^ prior; // Bitwise OR
prior = result;
dest[offset] |= result ^ prior[plane]; // Bitwise OR
prior[plane] = result | clockMask; // Set clock on next out
#else
dest[x] |= result; // Bitwise OR
dest[offset] |= result; // Bitwise OR
#endif
} // end x
greenBit <<= 1;
if (plane || (core->numPlanes < 6)) {
redBit <<= 1;
blueBit <<= 1;
} else {
redBit = 0b0000100000000000;
blueBit = 0b0000000000000001;
}
dest += bitplaneSize; // Advance one scanline in dest buffer
offset += bitplaneSize; // Same column, next bitplane
planeMask <<= 1;
} // end plane
} // end x
dest += rowSize; // Advance one scanline in dest buffer
upperSrc += width; // Advance one scanline in source buffer
lowerSrc += width;
} // end row
pinMask += 6; // Next chain's RGB pin masks
upperSrc += halfMatrixOffset; // Advance to next matrix start pos
lowerSrc += halfMatrixOffset;
}
dest -= rowSize * core->numRowPairs; // Reset dest to initial value
} // end chain
}
// Corresponding function for long output -- either several parallel chains
@ -1104,33 +1217,14 @@ void _PM_convert_565_long(Protomatter_core *core, uint16_t *source,
_PM_chunkSize *
((width + (_PM_chunkSize - 1)) / _PM_chunkSize); // 1 plane of row pair
uint8_t pad = bitplaneSize - width; // Start-of-plane pad
uint32_t rowSize = bitplaneSize * core->numPlanes;
uint32_t initialRedBit, initialGreenBit, initialBlueBit;
if (core->numPlanes == 6) {
initialRedBit = 0b1000000000000000; // MSB red
initialGreenBit = 0b0000000000100000; // LSB green
initialBlueBit = 0b0000000000010000; // MSB blue
} else {
uint8_t shiftLeft = 5 - core->numPlanes;
initialRedBit = 0b0000100000000000 << shiftLeft;
initialGreenBit = 0b0000000001000000 << shiftLeft;
initialBlueBit = 0b0000000000000001 << shiftLeft;
}
uint32_t offset; // Current position in the 'dest' buffer
#if defined(_PM_portToggleRegister)
// No per-chain loop is required; one clock bit handles all chains
uint32_t offset = 0; // Current position in the 'dest' buffer
for (uint8_t row = 0; row < core->numRowPairs; row++) {
for (uint8_t plane = 0; plane < core->numPlanes; plane++) {
dest[offset++] = 0; // First element of each plane
for (uint16_t x = 1; x < bitplaneSize; x++) { // All subsequent items
dest[offset++] = core->clockMask;
}
}
}
#else
// Clear matrix buffer (because each chain's data is OR'd into place).
// If _PM_portToggleRegister is used, no need to specially handle the
// clock bit, the 'prior' var takes care of that across each row/plane.
memset(dest, 0, core->bufferSize);
#endif
dest += pad; // Pad value is in 'elements,' not bytes, so this is OK
@ -1138,55 +1232,60 @@ void _PM_convert_565_long(Protomatter_core *core, uint16_t *source,
for (uint8_t chain = 0; chain < core->parallel; chain++) {
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)
uint32_t prior = 0;
uint32_t prior[16]; // Initial bit state for each plane
// Since we're ORing in bits over an existing clock bit,
// prior is 0 rather than clockMask as in the byte case.
memset(prior, 0, core->numPlanes * sizeof(uint32_t));
#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
uint16_t rgb565 = upperSrc[x]; // Pixel in upper half
uint16_t upperRed = core->remap_rb[rgb565 >> 11];
uint16_t upperGreen = core->remap_g[(rgb565 >> 5) & 0x3F];
uint16_t upperBlue = core->remap_rb[rgb565 & 0x1F];
rgb565 = lowerSrc[x]; // Pixel in lower half
uint16_t lowerRed = core->remap_rb[rgb565 >> 11];
uint16_t lowerGreen = core->remap_g[(rgb565 >> 5) & 0x3F];
uint16_t lowerBlue = core->remap_rb[rgb565 & 0x1F];
uint16_t planeMask = 1; // Start from LSB, work up
offset = x; // Offset into current dest pointer
for (uint8_t plane = 0; plane < core->numPlanes; plane++) {
uint32_t result = 0;
if (upperRGB & redBit)
if (upperRed & planeMask)
result |= pinMask[0];
if (upperRGB & greenBit)
if (upperGreen & planeMask)
result |= pinMask[1];
if (upperRGB & blueBit)
if (upperBlue & planeMask)
result |= pinMask[2];
if (lowerRGB & redBit)
if (lowerRed & planeMask)
result |= pinMask[3];
if (lowerRGB & greenBit)
if (lowerGreen & planeMask)
result |= pinMask[4];
if (lowerRGB & blueBit)
if (lowerBlue & planeMask)
result |= pinMask[5];
#if defined(_PM_portToggleRegister)
// Main difference here vs byte converter is each chain
// ORs new bits into place (vs single-pass overwrite).
#if defined(_PM_portToggleRegister)
dest[x] |= result ^ prior; // Bitwise OR
prior = result;
dest[offset] |= result ^ prior[plane]; // Bitwise OR
prior[plane] = result | core->clockMask; // Set clock on next out
#else
dest[x] |= result; // Bitwise OR
dest[offset] |= result; // Bitwise OR
#endif
} // end x
greenBit <<= 1;
if (plane || (core->numPlanes < 6)) {
redBit <<= 1;
blueBit <<= 1;
} else {
redBit = 0b0000100000000000;
blueBit = 0b0000000000000001;
}
dest += bitplaneSize; // Advance one scanline in dest buffer
offset += bitplaneSize; // Same column, next bitplane
planeMask <<= 1;
} // end plane
} // end x
dest += rowSize; // Advance one scanline in dest buffer
upperSrc += width; // Advance one scanline in source buffer
lowerSrc += width;
} // end row
pinMask += 6; // Next chain's RGB pin masks
upperSrc += halfMatrixOffset; // Advance to next matrix start pos
lowerSrc += halfMatrixOffset;
}
dest -= rowSize * core->numRowPairs; // Reset dest to initial value
} // end chain
}
void _PM_convert_565(Protomatter_core *core, uint16_t *source, uint16_t width) {
@ -1216,7 +1315,7 @@ void _PM_convert_565(Protomatter_core *core, uint16_t *source, uint16_t width) {
// single matrix chain (doing parallel chains would require either an
// impractically large lookup table, or adding together multiple tables'
// worth of bitmasks, which would slow things down in the vital inner loop).
// Although parallel matrix chains aren't yet 100% implemented in this code
// right now, I wanted to leave that possibility for the future, as a way to
// handle larger matrix combos, because long chains will slow down the
// Although parallel matrix chains aren't yet 100% verified in this code
// right now, I wanted to leave that possibility for the future, as a way
// to handle larger matrix combos, because long chains will slow down the
// refresh rate.

View file

@ -65,6 +65,8 @@ typedef struct {
uint32_t rgbAndClockMask; ///< PORT bit mask for RGB data + clock
volatile void *addrPortToggle; ///< See singleAddrPort below
void *screenData; ///< Per-bitplane RGB data for matrix
uint16_t remap_rb[32]; ///< Gamma or decimation table for red+blue
uint16_t remap_g[64]; ///< Gamma or decimation table for green
_PM_pin latch; ///< RGB data latch
_PM_pin oe; ///< !OE (LOW out enable)
_PM_pin *addr; ///< Array of address pins
@ -80,6 +82,7 @@ typedef struct {
uint8_t portOffset; ///< Active 8- or 16-bit pos. in PORT
uint8_t numPlanes; ///< Display bitplanes (1 to 6)
uint8_t numRowPairs; ///< Addressable row pairs
uint8_t brightness; ///< Max brightness (if supported by arch)
bool doubleBuffer; ///< 2X buffers for clean switchover
bool singleAddrPort; ///< If 1, all addr lines on same PORT
volatile uint8_t activeBuffer; ///< Index of currently-displayed buf
@ -198,6 +201,15 @@ extern void _PM_deallocate(Protomatter_core *core);
*/
extern void _PM_row_handler(Protomatter_core *core);
/*!
@brief Sometimes called by a timer interrupt to disable matrix output
prior to the normal bitplane switchover time (handled by the
row handler above). Usually only occurs on the least bitplanes
of high-depth setups.
@param core Pointer to Protomatter_core structure.
*/
extern void _PM_matrix_oe_off(Protomatter_core *core);
/*!
@brief Returns current value of frame counter and resets its value to
zero. Two calls to this, timed one second apart (or use math with
@ -214,9 +226,12 @@ extern uint32_t _PM_getFrameCount(Protomatter_core *core);
@param tptr Pointer to timer/counter peripheral OR a struct
encapsulating information about a timer/counter
periph (architecture-dependent).
@param period Timer 'top' / rollover value.
@param top Timer 'top' / rollover value.
@param match Timer 'match' value, can be used to de-assert the matrix
OE line before the 'top' interval (e.g. for brightness or
gamma correction), or pass 0xFFFFFFFF to ignore.
*/
extern void _PM_timerStart(void *tptr, uint32_t period);
extern void _PM_timerStart(void *tptr, uint32_t top, uint32_t match);
/*!
@brief Stop timer/counter peripheral.