Can anyone point me to Teensy 3.5 code using SmartMatrix to make the “Digital Sand” display as shown in nearly every Adafruit panel product? They seem to usually run a Raspberry Pi and there is similar code in the Adafruit PixelDust library (Snow.cpp) but don’t have the chops to convert that to Arduinoish code. I have most of the SmartMatrix library demo programs working so my system is good. This is the panel (and demo) I have.
https://www.adafruit.com/product/5362
This is from the Matrixportal demo Examples → Adafruit Protomatter → pixeldust (which uses Arduino libraries), and I’ve modified it slightly for my uses.
You will need to modify the Adafruit_Protomatter library for the Teensy, or replace its use with the SmartMatrix libraries. Unfortunately, there are some missing board specific dependencies for Teensy 3.2/3.5/3.6. It does build for the Teensy 4.1. Note, the pin assignments are for the Matrix Portal, and you will need to modify them for the Teensy that you use. Note, that you need to use a different Smart Matrix shield (and different pins) between the Teensy 3.x and Teensy 4.x processors.
Obviously, if you use a different accelerometer, you would need to modify that code as well.
/* ----------------------------------------------------------------------
"Pixel dust" Protomatter library example. As written, this is
SPECIFICALLY FOR THE ADAFRUIT MATRIXPORTAL M4 with 64x32 pixel matrix.
Change "HEIGHT" below for 64x64 matrix. Could also 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.
The original code was an Ardunio example for the Adafruit Matrix Portal.
Examples -> Adafruit Protomatter -> pixeldust
I (Michael Meissner, arduino@the-meissners.org) have made several changes to
this file to add a non-interactive random mode, and to reformat some of the
code to meet my personal coding style. If you include my changes, please at
least include the attribution of those changes.
------------------------------------------------------------------------- */
#include <Wire.h> // For I2C communication
#include <Adafruit_LIS3DH.h> // For accelerometer
#include <Adafruit_PixelDust.h> // For sand simulation
#include <Adafruit_Protomatter.h> // For RGB matrix
#include <Bounce2.h> // For reading up/down buttons.
#define HEIGHT 32 // Matrix height (pixels) - SET TO 64 FOR 64x64 MATRIX!
#define WIDTH 64 // Matrix width (pixels)
#define MAX_FPS 45 // Maximum redraw rate, frames/second
#if HEIGHT == 64 // 64-pixel tall matrices have 5 address lines:
uint8_t addrPins[] = {17, 18, 19, 20, 21};
#else // 32-pixel tall matrices have 4 address lines:
uint8_t addrPins[] = {17, 18, 19, 20};
#endif
// Remaining pins are the same for all matrix sizes. These values
// are for MatrixPortal M4. See "simple" example for other boards.
uint8_t rgbPins[] = {7, 8, 9, 10, 11, 12};
const uint8_t clockPin = 14;
const uint8_t latchPin = 15;
const uint8_t oePin = 16;
// Pins on the Matrix portal
const uint8_t upPin = 2;
const uint8_t downPin = 3;
// Pin to switch between using the accelerometer and using random.
const uint8_t switchPin = upPin;
const uint32_t switchInterval = 25; // use a debounce interval of 25ms
Bounce switchBounce;
bool do_random = true;
Adafruit_Protomatter matrix (WIDTH, // Matrix width in pixels,
4, // bit depth
1, rgbPins, // # of matrix chains, array of 6 RGB pins
sizeof(addrPins), addrPins, // # of address pins (height is inferred), array of pins
clockPin, latchPin, oePin, true); // other matrix control pins.
Adafruit_LIS3DH accel = Adafruit_LIS3DH();
#define N_COLORS 8
#define BOX_HEIGHT 8
#define N_GRAINS (BOX_HEIGHT*N_COLORS*8)
uint16_t colors[N_COLORS];
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 --------------------------------------
void err(int x) {
uint8_t i;
pinMode(LED_BUILTIN, OUTPUT); // Using onboard LED
for(i=1;;i++) { // Loop forever...
digitalWrite(LED_BUILTIN, i & 1); // LED on/off blink to alert user
delay(x);
}
}
void setup(void) {
Serial.begin (115200);
while (!Serial && millis () < 3000UL) {
delay (10);
}
// Attach bounce pin
switchBounce.attach (switchPin, INPUT_PULLUP);
switchBounce.interval (switchInterval);
ProtomatterStatus status = matrix.begin();
Serial.printf("Protomatter begin() status: %d\n", status);
if (!sand.begin()) {
Serial.println("Couldn't start sand");
err(1000); // Slow blink = malloc error
}
if (!accel.begin(0x19)) {
Serial.println("Couldn't find accelerometer");
err(250); // Fast bink = I2C error
}
accel.setRange(LIS3DH_RANGE_4_G); // 2, 4, 8 or 16 G!
//sand.randomize(); // Initialize random sand positions
// Set up initial sand coordinates, in 8x8 blocks
int n = 0;
for(int i=0; i<N_COLORS; i++) {
int xx = i * WIDTH / N_COLORS;
int yy = HEIGHT - BOX_HEIGHT;
for(int y=0; y<BOX_HEIGHT; y++) {
for(int x=0; x < WIDTH / N_COLORS; x++) {
//Serial.printf("#%d -> (%d, %d)\n", n, xx + x, yy + y);
sand.setPosition(n++, xx + x, yy + y);
}
}
}
Serial.printf("%d total pixels\n", n);
colors[0] = matrix.color565 (64, 64, 64); // Dark Gray
colors[1] = matrix.color565 (120, 79, 23); // Brown
colors[2] = matrix.color565 (228, 3, 3); // Red
colors[3] = matrix.color565 (255, 140, 0); // Orange
colors[4] = matrix.color565 (255, 237, 0); // Yellow
colors[5] = matrix.color565 ( 0, 128, 38); // Green
colors[6] = matrix.color565 ( 0, 77, 255); // Blue
colors[7] = matrix.color565 (117, 7, 135); // Purple
}
// MAIN LOOP - RUNS ONCE PER FRAME OF ANIMATION ----------------------------
void loop() {
const int change = 700;
static int counter = change;
static int xx_i = 0;
static int yy_i = 0;
static int zz_i = 0;
double xx;
double yy;
double zz;
uint32_t t;
// Limit the animation frame rate to MAX_FPS. Because the subsequent sand
// calculations are non-deterministic (don't always take the same amount
// of time, depending on their current states), this helps ensure that
// things like gravity appear constant in the simulation.
while(((t = micros()) - prevTime) < (1000000L / MAX_FPS))
;
prevTime = t;
// See if we pressed the button to switch between modes.
switchBounce.update ();
if (switchBounce.fell ()) {
if (do_random) {
Serial.println ("Switching to using the accelerometer");
do_random = false;
} else {
Serial.println ("Switching to random mode");
do_random = true;
counter = change;
}
}
// Either do random mode or use the accelerometer.
if (do_random) {
if (counter++ > change) {
counter = 0;
int xx_j;
int yy_j;
int zz_j;
int num = 0;
do {
xx_j = random (-4000, 4000);
yy_j = random ( -400, 400);
zz_j = random ( -400, 400);
} while ((xx_i == xx_j) && (yy_i == yy_j) && (zz_i == zz_j) && (num++ < 5));
xx_i = xx_j;
yy_i = yy_j;
zz_i = zz_j;
Serial.printf ("xx = %5d, yy = %5d, zz = %5d\n", xx_i, yy_i, zz_i);
}
xx = xx_i;
yy = yy_i;
zz = zz_i;
} else {
// Read accelerometer...
sensors_event_t event;
accel.getEvent(&event);
xx = event.acceleration.x * 1000;
yy = event.acceleration.y * 1000;
zz = event.acceleration.z * 1000;
{
long xx_l = (long) xx;
long yy_l = (long) yy;
long zz_l = (long) zz;
long xx_f = (long) (((fabs (xx) - (fabs ((double) xx_l))) * 10.0) + 0.5);
long yy_f = (long) (((fabs (yy) - (fabs ((double) yy_l))) * 10.0) + 0.5);
long zz_f = (long) (((fabs (zz) - (fabs ((double) zz_l))) * 10.0) + 0.5);
Serial.printf("xx = %8ld.%ld, yy = %8ld.%ld, zz = %8ld.%ld\n",
xx_l, xx_f,
yy_l, yy_f,
zz_l, zz_f);
}
}
// Run one frame of the simulation
sand.iterate (xx, yy, zz);
//sand.iterate(-accel.y, accel.x, accel.z);
// Update pixel data in LED driver
dimension_t x, y;
matrix.fillScreen(0x0);
for(int i=0; i<N_GRAINS ; i++) {
sand.getPosition(i, &x, &y);
int n = i / ((WIDTH / N_COLORS) * BOX_HEIGHT); // Color index
uint16_t flakeColor = colors[n];
matrix.drawPixel(x, y, flakeColor);
//Serial.printf("(%d, %d)\n", x, y);
}
matrix.show(); // Copy data to matrix buffers
}
// HISTORY
// $Log: Meissner_pixeldust_64x32.ino,v $
// Revision 1.13 2020/12/07 03:19:41 michaelmeissner
// Add message about changes.
//
// Revision 1.12 2020/12/07 01:52:23 michaelmeissner
// Iterate on random settings.
//
// Revision 1.11 2020/12/07 01:37:50 michaelmeissner
// Spacing.
//
// Revision 1.10 2020/12/05 23:21:49 michaelmeissner
// Bump up sizes for printing accel. information.
//
// Revision 1.9 2020/12/05 23:19:56 michaelmeissner
// Fix thinko in reporting accel. positions.
//
// Revision 1.8 2020/12/05 22:56:21 michaelmeissner
// Spacing.
//
// Revision 1.7 2020/12/05 22:55:09 michaelmeissner
// Comment parameters to the Adafruit_Protomatter setup.
//
// Revision 1.6 2020/12/05 22:46:46 michaelmeissner
// Spacing; Reset random counter when using button; Lower random change time to 700.
//
// Revision 1.5 2020/12/05 22:38:47 michaelmeissner
// Make things const; Spacing; Switch between random and accel. via the UP button.
//
// Revision 1.4 2020/12/05 22:22:29 michaelmeissner
// Spacing.
//
// Revision 1.3 2020/12/05 22:20:49 michaelmeissner
// Eliminate carriage returns.
//
// Revision 1.2 2020/12/05 22:12:33 michaelmeissner
// Spacing; switch to using random display instead of accelerometer.
//
// Revision 1.1 2020/12/05 20:56:46 michaelmeissner
// Initial version.
//
Thank you Michael. Using bits of your code and some from Overview | Shake Away 2021 with MatrixPortal | Adafruit Learning System and a lot of trial and error I got it working. Comments will be appreciated, I only pretend to be a programmer. This is a digital clock that melts into a sand display when you shake it a few times. Will be a cool Christmas present for some one. See Clock01 - YouTube
I am setting the “sand” to random colors. What I would like to do though, is randomize the pixel colors once, then maintain those colors for each pixel as it moves. I have not found a way to retrieve the pixel color from before the sand iteration. Apparently the way it works is the sand library only moves the pixel positions, It does not repaint the pixel in the buffer, that is left to the SmartMatrix library. I may have to make my own local rgb24 copy of the background buffer or maybe a local copy of the sand buffer or maybe both. I do not see a way to readpixel from the active matrix screen which I think would give me the “before” information.
The hardware for this sketch is Teensy 3.6, SmartMatrix adapter V4 (Adafruit #1902), LIS3DH accelerometer (Adafruit 2809) and 2mm LED panel (Adafruit 5362).
-------------------------------------------------------------------------
// SPDX-FileCopyrightText: 2020 Limor Fried for Adafrudit Industries
// https://learn.adafruit.com/matrixportal-shake-away-2020
// SPDX-License-Identifier: MIT
//
#include <MatrixHardware_Teensy3_ShieldV4.h> // SmartLED Shield for Teensy 3 (V4)
#include <SmartMatrix.h>
#include <Adafruit_LIS3DH.h> // For accelerometer
#include <Adafruit_PixelDust.h> // For simulation
#include <TimeLib.h>
#define COLOR_DEPTH 24 // Choose the color depth used for storing pixels in the layers: 24 or 48 (24 is good for most sketches - If the sketch uses type `rgb24` directly, COLOR_DEPTH must be 24)
const uint16_t kMatrixWidth = 64; // Set to the width of your display, must be a multiple of 8
const uint16_t kMatrixHeight = 64; // Set to the height of your display
const uint8_t kRefreshDepth = 36; // Tradeoff of color quality vs refresh rate, max brightness, and RAM usage. 36 is typically good, drop down to 24 if you need to. On Teensy, multiples of 3, up to 48: 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48. On ESP32: 24, 36, 48
const uint8_t kDmaBufferRows = 4; // known working: 2-4, use 2 to save RAM, more to keep from dropping frames and automatically lowering refresh rate. (This isn't used on ESP32, leave as default)
const uint8_t kPanelType = SM_PANELTYPE_HUB75_64ROW_MOD32SCAN; // Choose the configuration that matches your panels. See more details in MatrixCommonHub75.h and the docs: https://github.com/pixelmatix/SmartMatrix/wiki
const uint32_t kMatrixOptions = (SM_HUB75_OPTIONS_NONE); // see docs for options: https://github.com/pixelmatix/SmartMatrix/wiki
const uint8_t kBackgroundLayerOptions = (SM_BACKGROUND_OPTIONS_NONE);
//const uint8_t kIndexedLayerOptions = (SM_INDEXED_OPTIONS_NONE);
//const uint8_t kScrollingLayerOptions = (SM_SCROLLING_OPTIONS_NONE);
const int displayCenter = (kMatrixHeight / 2);
SMARTMATRIX_ALLOCATE_BUFFERS(matrix, kMatrixWidth, kMatrixHeight, kRefreshDepth, kDmaBufferRows, kPanelType, kMatrixOptions);
SMARTMATRIX_ALLOCATE_BACKGROUND_LAYER(backgroundLayer, kMatrixWidth, kMatrixHeight, COLOR_DEPTH, kBackgroundLayerOptions);
const int defaultBrightness = (80 * 255) / 100; // 80% brightness
const rgb24 Black = {0x00, 0x00, 0x00};
const rgb24 White = {0xFF, 0xFF, 0xFF};
#define SHAKE_ACCEL_G 2.9 // Force (in Gs) to trigger shake
#define SHAKE_ACCEL_MS2 (SHAKE_ACCEL_G * 9.8) // Convert to m/s^2
#define SHAKE_ACCEL_SQ (SHAKE_ACCEL_MS2 * SHAKE_ACCEL_MS2) // Avoid sqrt() in accel check
#define SHAKE_EVENTS 5 // Number of accel readings to trigger sand
#define SHAKE_PERIOD 3000 // Period (in ms) when SHAKE_EVENTS must happen
#define SAND_TIME 30000 // Time (in ms) to run simulation before restarting
#define MAX_FPS 60 // Maximum redraw rate, frames/second
uint32_t prevTime = 0; // For frames-per-second throttle
uint16_t n_grains = 0; // Number of sand grains (counted on sand trigger)
Adafruit_PixelDust *sand; // Sand object (allocated in setup())
Adafruit_LIS3DH accel = Adafruit_LIS3DH(); // Accelerometer
sensors_event_t event; // Accelerometer result
/////////////////////////////////////////////////////////////////
void setup(void) {
// set the Time library to use Teensy 3.0's RTC
setSyncProvider(getTeensy3Time);
Serial.begin(115200);
delay(100); // Wait for serial to start
// Get clock time from the connected PC (if available)
// Mandatory only if clock battery is not provided
if (timeStatus() != timeSet) {
Serial.println("Unable to sync with the RTC");
Serial.println("Defaulting to stored clock");
} else {
Serial.println("RTC has set from PC system time");
}
// Setup SmartMatrix
matrix.addLayer(&backgroundLayer);
matrix.begin();
matrix.setBrightness(defaultBrightness);
Serial.println("Matrix begin()");
backgroundLayer.setFont(gohufont11b); // Font is 11 pixels high
// PixelDust set up is deferred until sandSimulation()
if (!accel.begin(0x18)) {
Serial.println("Couldn't Find Accelerometer");
err(250); // Fast blink = I2C error
}
Serial.println("Accelerometer OK");
accel.setRange(LIS3DH_RANGE_8_G);
}
/////////////////////////////////////////////////////////////////
void loop() {
char timeBuffer[10]; // Holds formatted time string
int indent; // Left margin for prints to screen
static int lastsec; // To trigger a time update
static int numGrains; // # of pixels lit
static bool notStirred = false; // Shake counter entry flag
time_t t; // Used to record current time in MS
static time_t last_event_time; // Calculated end of allowed shake period
static int num_events; // Counts shake events
if (second() != lastsec) { // Update the clock display once per second
lastsec = second();
backgroundLayer.fillScreen(Black);
Serial.println(second());
// Draw Clock to SmartMatrix using AM/PM 12 hour time
// Font position reference point is top left of first char
// Pixel zero is top left of display
uint8_t Hour = hourFormat12();
static char AMPM = 'A';
if (isPM()) AMPM = 'P';
// Draw time
indent = 5; // Left margin allow for 2 digit time
if (Hour < 10) indent = 9; // Left margin allow for 3 digits
sprintf(timeBuffer, "%d:%02d:%02d%c", Hour, minute(), second(), AMPM);
backgroundLayer.drawString(indent, displayCenter - 18, White, timeBuffer);
// Draw date
indent = 4; // Left margin allow for 2 digit month
if (month() < 10) indent = 6; // Left margin allow for 3 digits month
sprintf(timeBuffer, "%d/%02d/%02d", month(), day(), year());
backgroundLayer.drawString(indent, displayCenter, White, timeBuffer);
}
// Look for shakes
t = millis(); // Current time
if ((t > last_event_time)) { // Timed out!
notStirred = false; // Reset for another try
// num_events = 0;
}
accel.getEvent(&event);
// Square the accelerometer readings (why?)
// x**2 + y**2 + z**2
float mag2 = event.acceleration.x * event.acceleration.x +
event.acceleration.y * event.acceleration.y +
event.acceleration.z * event.acceleration.z;
if (mag2 >= SHAKE_ACCEL_SQ) { // A Shake has been detected
delay(5); // Debounce the accelerometer
// notStirred is true if already counting shakes, false if first one
if (notStirred) { // Count additional shakes
if (++num_events >= SHAKE_EVENTS) { // Enough shakes detected?
// Cue the digital sand
sandSimulation();
num_events = 0; // Reset for next shakes interval
}
} else { // notStirred is false, initial shake detected
notStirred = true; // In a shake interval,flag to look for more
num_events = 1;
last_event_time = t + SHAKE_PERIOD; // Allowed interval for shakes
}
} // End of shake detect
// OK to swap now
backgroundLayer.swapBuffers();
} // End of Loop()
/* Function to create a random color pixel in rgb24 form */
rgb24 randomPixel() {
uint8_t Red = random (255);
uint8_t Grn = random (255);
uint8_t Blu = random (255);
return {Red, Grn, Blu};
}
/* function to retrieve time from Teensy RTC */
time_t getTeensy3Time()
{
return Teensy3Clock.get();
}
/* code to process initial time sync messages from the serial port */
#define TIME_HEADER "T" // Header tag for serial time sync message
unsigned long processSyncMessage() {
unsigned long pctime = 0L;
const unsigned long DEFAULT_TIME = 1357041600; // Jan 1 2013
if (Serial.find(TIME_HEADER)) {
pctime = Serial.parseInt();
return pctime;
if ( pctime < DEFAULT_TIME) { // check the value is a valid time (greater than Jan 1 2013)
pctime = 0L; // return 0 to indicate that the time is not valid
}
}
return pctime;
}
/* Error handler used by setup() */
void err(int x) {
uint8_t i;
pinMode(LED_BUILTIN, OUTPUT); // Using onboard LED
for (i = 1;; i++) { // Loop forever...
digitalWrite(LED_BUILTIN, i & 1); // LED on/off blink to alert user
delay(x);
}
}
/* Run sand simulation for a few seconds
At this point the background layer has not been swapped
*/
void sandSimulation() {
int n_grains = 0;
time_t t, elapsed;
time_t prevTime, sandStartTime = millis();
rgb24 thisPixel;
int i, j = 0, b;
// Count number of 'on' pixels (sand grains) in background buffer
// This must be done *before* swapping buffers
// as readPixel doesn't work in matrix layer.
for (i = 0; i < kMatrixHeight; i++) { // Row index (y coordinate)
for (b = 0; b < kMatrixWidth; b++) { // Column index (x coordinate)
thisPixel = backgroundLayer.readPixel(i, b);
if (thisPixel.red | thisPixel.green | thisPixel.blue) {
n_grains++;
}
}
}
// Set initial sand pixel positions and draw initial matrix state
// Allocate sand object based on matrix size and bitmap 'on' pixels
sand = new Adafruit_PixelDust(kMatrixWidth, kMatrixHeight, n_grains, 1);
if (!sand->begin()) {
Serial.println("PixelDust init failed");
return;
}
sand->clear();
// Rescan the display and attach lit pixels to the sand grains
for (i = 0; i < kMatrixHeight; i++) { // Row index
for (b = 0; b < kMatrixWidth; b++) { // Column index
thisPixel = backgroundLayer.readPixel(b, i);
if (thisPixel.red | thisPixel.green | thisPixel.blue) {
sand->setPosition(j++, b, i);
}
}
}
while (true) { // Hijack loop()
// Only run for specified time
while ((elapsed = (millis() - sandStartTime)) < SAND_TIME) {
// Limit the animation frame rate to MAX_FPS.
while (((t = micros()) - prevTime) < (1000000L / MAX_FPS));
prevTime = t;
// Read accelerometer...
sensors_event_t event;
accel.getEvent(&event);
// Run one frame of the simulation
sand->iterate(event.acceleration.x * 1024, event.acceleration.y * 1024, event.acceleration.z * 1024);
// Update pixel data in LED driver
backgroundLayer.fillScreen(Black);
dimension_t x, y;
for (i = 0; i < n_grains ; i++) {
sand->getPosition(i, &x, &y);
// Would be nice to getPixel color here and reuse in the drawPixel
// so individual grains don't shimmer
// backgroundLayer.drawPixel(x, y, Blue); // works
backgroundLayer.drawPixel(x, y, randomPixel()); // works
}
backgroundLayer.swapBuffers(false);
} // End of elapsed time test
return; // Exit back to displaying clock
} // end of while true loop
}
I really like the effect, great job!
I find the transition from sand to the clock again to be abrupt. Some suggestions:
- Simple: fade out the sand and fade in the clock
- Advanced: make the clock pixels “sticky” so that sand that hits one of the clock pixels gets stuck there.
- If the user isn’t actively moving sand when you want the clock to be shown, dump a load of sand from the top whatever doesn’t stick drops off the bottom
I do not see a way to readpixel from the active matrix screen
I may have to make my own local rgb24 copy of the background buffer or maybe a local copy of the sand buffer or maybe both
I’m not exactly sure what you need to do with an additional layer, but you can use multiple layers in SmartMatrix Library. The background layer is opaque, so if you’re adding multiple background layers, the last one you add will be the only one visible. You can create layers that aren’t refreshed to the display (by not calling addLayer()
, but there are some things to look out for there. swapBuffers
won’t work if the layer isn’t being refreshed. You can draw to a layer, get the pixel value, and clear the layer, so if you want to use a layer as a “scratchpad” for seeing where text is for example, you could do that. For text, using indexedLayer (see MatrixClock example) will only use one bit per pixels instead of 24 bits.
Happy to chat about this project more but I’m out of time now
Thank you Louis. I have the clock working as I hoped. The digital sand feature assigns random colors and maintains that color for each pixel as the sand plays out. I like this a lot better than continuously random generated colors. I did this by declaring an rgb24[4096] array which records the initial randomly selected color for each grain of sand. Then for each sand iteration I look up the color and paint the pixels. I made a nice Walnut box for it.
Short video at:
Boy am I glad I posted this code here. Had a hard drive die Thursday and lost the working version. Was able to recreate from this post in a few hours.
Louis: I was able to get fade in/out working as you suggested. It looks nice.
Louis, could you have a look at this thread? I’m seeing a weird interaction with EEPROM writes and the clock used by Smart Matrix.
Another effort using the same hardware. Because the world needs another digital hourglass.
Parts: Adafruit 5362 display
Adafruit 2809 accelerometer
SmartMatrix V4 shield
PJRC Teensy 3.6
/*
* Hourglass simulation
* Uses SmartMatrix 64x64 display, Teensy 3.6 by Jim Harvey https://wb8nbs.wordpress.com
*/
#include <MatrixHardware_Teensy3_ShieldV4.h> // SmartLED Shield for Teensy 3 (V4)
#include <SmartMatrix.h>
#include <Adafruit_PixelDust.h> // For simulation
#include <Adafruit_LIS3DH.h> // For accelerometer
#define COLOR_DEPTH 24 // Choose the color depth used for storing pixels in the layers: 24 or 48 (24 is good for most sketches - If the sketch uses type `rgb24` directly, COLOR_DEPTH must be 24)
const uint16_t kMatrixWidth = 64; // Set to the width of your display, must be a multiple of 8
const uint16_t kMatrixHeight = 64; // Set to the height of your display
const uint8_t kRefreshDepth = 36; // Tradeoff of color quality vs refresh rate, max brightness, and RAM usage. 36 is typically good, drop down to 24 if you need to. On Teensy, multiples of 3, up to 48: 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48. On ESP32: 24, 36, 48
const uint8_t kDmaBufferRows = 4; // known working: 2-4, use 2 to save RAM, more to keep from dropping frames and automatically lowering refresh rate. (This isn't used on ESP32, leave as default)
const uint8_t kPanelType = SM_PANELTYPE_HUB75_64ROW_MOD32SCAN; // Choose the configuration that matches your panels. See more details in MatrixCommonHub75.h and the docs: https://github.com/pixelmatix/SmartMatrix/wiki
const uint32_t kMatrixOptions = (SM_HUB75_OPTIONS_NONE); // see docs for options: https://github.com/pixelmatix/SmartMatrix/wiki
const uint8_t kBackgroundLayerOptions = (SM_BACKGROUND_OPTIONS_NONE);
const int displayCenter = (kMatrixHeight / 2);
const rgb24 Black = {0x00, 0x00, 0x00};
const rgb24 White = {0xFF, 0xFF, 0xFF};
const rgb24 Gray = {0x64, 0x64, 0x64};
const rgb24 Red = {0xFF, 0x00, 0x00};
const rgb24 Green = {0x00, 0xFF, 0x00};
const rgb24 Blue = {0x50, 0x50, 0xFF};
SMARTMATRIX_ALLOCATE_BUFFERS(matrix, kMatrixWidth, kMatrixHeight, kRefreshDepth, kDmaBufferRows, kPanelType, kMatrixOptions);
SMARTMATRIX_ALLOCATE_BACKGROUND_LAYER(backgroundLayer, kMatrixWidth, kMatrixHeight, COLOR_DEPTH, kBackgroundLayerOptions);
float defaultBrightness = 100.0;
Adafruit_PixelDust *sand; // Sand object (allocated in setup())
Adafruit_LIS3DH accel = Adafruit_LIS3DH(); // Accelerometer
sensors_event_t event; // Accelerometer result
void setup(void) {
Serial.begin(115200);
// Setup SmartMatrix
matrix.addLayer(&backgroundLayer);
matrix.begin();
backgroundLayer.enableColorCorrection(true);
matrix.setBrightness(180);
if (!accel.begin(0x18)) {
Serial.println("Couldn't Find Accelerometer");
}
Serial.println("Accelerometer OK");
accel.setRange(LIS3DH_RANGE_8_G);
} // End setup()
void loop() {
backgroundLayer.fillScreen(Black); // Erase the screen
backgroundLayer.swapBuffers(true);
// sand argument is delay to slow the grains. With Teensy set to 120 MHz
// 0 is about 30 seconds
// 38 is about 2 minutes
// 88 is about 88 minutes
sandSimulation(38 );
Serial.println("resetting in loop()");
delay (3000);
} // end loop()
/* Run sand simulation
Arg sets a delay between pixel frames to adjust the time to complete
*/
void sandSimulation(int procrastinate) {
int n_grains = 0;
int glassWidth = 40; // Pixels
int glassHeight = 62; // Pixels
int pixelBlack[20] { // Pixels to erase
32, 33,
32, 31,
12, 1,
13, 1,
51, 1,
52, 1,
12, 62,
13, 62,
51, 62,
52, 62
};
rgb24 thisPixel; // Temp variable
rgb24 grainColor[4096]; // Records initial color
int i, j = 0, b;
dimension_t x, y;
// backgroundLayer.fillScreen(Black); // Erase the screen
backgroundLayer.fillTriangle(15, 3, 49, 3, 32, 30, Gray); // Fill the top triangle with gray sand
// Outline the hour glass
backgroundLayer.drawTriangle(12, 1, 52, 1, 32, 33, White);
backgroundLayer.drawTriangle(12, 62, 52, 62, 32, 31, White);
// Round off the sharp corners and create narrow center channel
for (int i = 0; i < 20; i += 2) {
backgroundLayer.drawPixel(pixelBlack[i], pixelBlack[i + 1], Black);
}
// Count number of 'on' pixels (marked Gray) in background buffer
// This must be done *before* swapping buffers
// as readPixel doesn't work in matrix layer.
for (i = 0; i < kMatrixHeight; i++) { // Row index (y coordinate)
for (b = 0; b < kMatrixWidth; b++) { // Column index (x coordinate)
thisPixel = backgroundLayer.readPixel(i, b);
if (thisPixel.red == 100) { // Is this Gray?
thisPixel = randomPixel(); // Generate random color
grainColor[n_grains] = thisPixel; // And remember that color
n_grains++; // Count the grains
}
}
}
// Set initial sand pixel positions and draw initial matrix state
// Allocate sand object based on matrix size and bitmap 'on' pixels
sand = new Adafruit_PixelDust(kMatrixWidth, kMatrixHeight, n_grains, 1);
if (!sand->begin()) {
Serial.println("PixelDust init failed");
return;
}
sand->clear();
// Rescan the display and attach random colored pixels to the sand grains
for (i = 1; i < (1 + glassHeight); i++) { // Row index
for (b = 12; b < (12 + glassWidth); b++) { // Column index
thisPixel = backgroundLayer.readPixel(b, i);
if (thisPixel.red | thisPixel.green | thisPixel.blue) { // is anything there?
if (thisPixel.red == 0xFF) { // Full White is outline
sand->setPixel(b, i); // Define outline pixels as obstacles
// Must add blocking to the matrix else the outline leaks
if ((b < 32)) sand->setPixel(b - 1, i); // Blocks diagonal pixels on left side
if ((b > 32)) sand->setPixel(b + 1, i); // Blocks diagonal pixels on right side
} else {
backgroundLayer.drawPixel(b, i, grainColor[j]); // Not outline, Paint saved color
sand->setPosition(j++, b, i);
}
}
}
}
backgroundLayer.swapBuffers(true); // Ready to go
delay (500); // Pause to admire the colors
while (true) { // Main Spin loop
delay(procrastinate); // Limit the sand speed
sensors_event_t event; // Read accelerometer...
accel.getEvent(&event);
// Run one frame of the simulation
sand->iterate(event.acceleration.x * 1024,
event.acceleration.y * 1024,
event.acceleration.z * 1024);
// Update pixel data in LED driver
// First, erase the inside area
for (i = 1; i < (1 + glassHeight); i++) { // Row index
for (b = 12; b < (12 + glassWidth); b++) { // Column index
thisPixel = backgroundLayer.readPixel(b, i);
// If not black and not part of the outline --
if ((thisPixel.red > 0) && (thisPixel.red < 255)) {
backgroundLayer.drawPixel(b, i, Black); // Erase the pixel
}
}
}
// Then paint the repositioned pixels
for (i = 0; i < n_grains ; i++) {
sand->getPosition(i, &x, &y);
// Recall that grain color for the new position
backgroundLayer.drawPixel(x, y, grainColor[i]);
}
backgroundLayer.swapBuffers(false);
} // End of fade spin loop
backgroundLayer.fillScreen(Black); // Erase the screen
backgroundLayer.swapBuffers(true);
return; // Exit back to displaying clock
}
/* Function to create a random color pixel in rgb24 form
+ 3 ensures no pixels will be completely dark
but will never be 255 - to distinguish between hour
glass outline and pixels.
*/
rgb24 randomPixel() {
uint8_t Red = random(220) + 34;
uint8_t Grn = random(220) + 34;
uint8_t Blu = random(220) + 34;
return {Red, Grn, Blu};
}
I’m considering the clock project done after adding one final option - a digital “Magic 8 Ball”. Still having problems with clock speed after an EEPROM write but I will add a reset button to get around that. Just to recap these are the parts
Adafruit 5362 display
Adafruit 2809 accelerometer
SmartMatrix V4 shield
PJRC Teensy 3.6
I’m reluctant to spend a lot of time doing drawings as the Teensy 3.6 is obsolete and most of the connections are described in the SmartMatrix documents. Here is a series of YouTubes showing the clock’s evolution
[Clock01 - YouTube]
[Clock02 - YouTube]
[Clock04 - YouTube]
[Hour Glass - YouTube]
[Magic Eight Ball simulation - YouTube]
Thank you Louis for the library.
I documented the build on my wordpress page
Discussion of the clock itself
[url]https://wb8nbs.wordpress.com/2022/07/14/a-clock-with-benefits/[/url]
Explanation of the Setup Menus
[url]https://wb8nbs.wordpress.com/2022/07/15/sand-clock-settings/[/url]
Very nicely done.
The programmer in me only has 2 suggestions
- 24H time, AM/PM must die
- 2022/08/14, programmer dates are ISO and sortable by having year first, this fixes the mess of english vs non english dates (month first or day first)
Thanks for the suggestions but this project will likely end up as a gift to some non-technical relative for whom 24 hour time is alien.
I have given up on getting the teensy clock problem fixed. Installed a reset button, have to reset after any mode change (which writes EEPROM) to stop the display from flickering.