Skip to content

Instantly share code, notes, and snippets.

@witnessmenow
Created August 12, 2019 23:14
Show Gist options
  • Save witnessmenow/29192dd0e5dda61f50900e3422cedeab to your computer and use it in GitHub Desktop.
Save witnessmenow/29192dd0e5dda61f50900e3422cedeab to your computer and use it in GitHub Desktop.
Falling Sand project for RGB matrix with a MPU6050 acceloremeter
// WIP!!!
//--------------------------------------------------------------------------
// Animated 'sand' for Adafruit Feather. Uses the following parts:
// - Feather 32u4 Basic Proto (adafruit.com/product/2771)
// - Charlieplex FeatherWing (adafruit.com/product/2965 - any color!)
// - LIS3DH accelerometer (2809)
// - 350 mAh LiPoly battery (2750)
// - SPDT Slide Switch (805)
//
// This is NOT good "learn from" code for the IS31FL3731; it is "squeeze
// every last byte from the microcontroller" code. If you're starting out,
// download the Adafruit_IS31FL3731 and Adafruit_GFX libraries, which
// provide functions for drawing pixels, lines, etc.
//--------------------------------------------------------------------------
#include <Wire.h> // For I2C communication
#include <MPU6050.h>
#define double_buffer // this must be enabled to stop flickering
#include <PxMatrix.h>
// The library for controlling the LED Matrix
//
// At time of writing this the double_buffer
// Have been merged into the main PxMatrix library,
// but have not been released, so you will need to install
// from Github
//
// https://github.com/2dom/PxMatrix
// Adafruit GFX library is a dependancy for the PxMatrix Library
// Can be installed from the library manager
// https://github.com/adafruit/Adafruit-GFX-Library
#define N_GRAINS 40 // Number of grains of sand
#define WIDTH 64 // Display width in pixels
#define HEIGHT 64 // Display height in pixels
#define MAX_FPS 45 // Maximum redraw rate, frames/second
// The 'sand' grains exist in an integer coordinate space that's 256X
// the scale of the pixel grid, allowing them to move and interact at
// less than whole-pixel increments.
#define MAX_X (WIDTH * 256 - 1) // Maximum X coordinate in grain space
#define MAX_Y (HEIGHT * 256 - 1) // Maximum Y coordinate
struct Grain {
int16_t x, y; // Position
int16_t vx, vy; // Velocity
uint16_t pos;
} grain[N_GRAINS];
// ----------------------------
// Wiring and Display setup
// ----------------------------
#define P_LAT 22
#define P_A 19
#define P_B 23
#define P_C 18
#define P_D 5
#define P_E 15
// #define P_OE 2
#define P_OE 21 // Feather Huzzah
hw_timer_t * timer = NULL;
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
// This defines the 'on' time of the display is us. The larger this number,
// the brighter the display. If too large the ESP will crash
uint8_t display_draw_time = 10; //10-50 is usually fine
//PxMATRIX display(matrix_width, matrix_height, P_LAT, P_OE, P_A, P_B, P_C);
//PxMATRIX display(64,32,P_LAT, P_OE,P_A,P_B,P_C,P_D);
PxMATRIX display(64, 64, P_LAT, P_OE, P_A, P_B, P_C, P_D, P_E);
uint16_t myRED = display.color565(255, 0, 0);
void IRAM_ATTR display_updater() {
display.display(display_draw_time);
}
void display_update_enable(bool is_enable)
{
if (is_enable)
{
timer = timerBegin(0, 80, true);
timerAttachInterrupt(timer, &display_updater, true);
timerAlarmWrite(timer, 2000, true);
timerAlarmEnable(timer);
}
else
{
timerDetachInterrupt(timer);
timerAlarmDisable(timer);
}
}
MPU6050 mpu;
uint32_t prevTime = 0; // Used for frames-per-second throttle
uint16_t backbuffer = 0, // Index for double-buffered animation
img[WIDTH * HEIGHT]; // Internal 'map' of pixels
// SETUP - RUNS ONCE AT PROGRAM START --------------------------------------
void setup(void) {
uint8_t i, j, bytes;
Serial.begin(115200);
Serial.println("Initialize MPU6050");
while(!mpu.begin(MPU6050_SCALE_2000DPS, MPU6050_RANGE_4G, MPU6050_ADDRESS, 27, 33))
{
Serial.println("Could not find a valid MPU6050 sensor, check wiring!");
delay(500);
}
// Define your display layout here, e.g. 1/8 step
display.begin(32);
display.setFastUpdate(true);
display.clearDisplay();
display_update_enable(true);
display.showBuffer();
memset(img, 0, sizeof(img)); // Clear the img[] array
for(i=0; i<N_GRAINS; i++) { // For each sand grain...
do {
grain[i].x = random(WIDTH * 256); // Assign random position within
grain[i].y = random(HEIGHT * 256); // the 'grain' coordinate space
// Check if corresponding pixel position is already occupied...
for(j=0; (j<i) && (((grain[i].x / 256) != (grain[j].x / 256)) ||
((grain[i].y / 256) != (grain[j].y / 256))); j++);
} while(j < i); // Keep retrying until a clear spot is found
img[(grain[i].y / 256) * WIDTH + (grain[i].x / 256)] = 255; // Mark it
grain[i].pos = (grain[i].y / 256) * WIDTH + (grain[i].x / 256);
grain[i].vx = grain[i].vy = 0; // Initial velocity is zero
}
}
// MAIN LOOP - RUNS ONCE PER FRAME OF ANIMATION ----------------------------
void loop() {
// 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.
uint32_t t;
while(((t = micros()) - prevTime) < (1000000L / MAX_FPS));
prevTime = t;
// // Display frame rendered on prior pass. It's done immediately after the
// // FPS sync (rather than after rendering) for consistent animation timing.
// pageSelect(0x0B); // Function registers
// writeRegister(0x01); // Picture Display reg
// Wire.write(backbuffer); // Page # to display
// Wire.endTransmission();
// backbuffer = 1 - backbuffer; // Swap front/back buffer index
// Read accelerometer...
//Vector accelVector = mpu.readNormalizeAccel();
Vector accelVector = mpu.readRawAccel();
float accelX = accelVector.XAxis * -1;
float accelY = accelVector.YAxis * -1;
float accelZ = accelVector.ZAxis;
int16_t ax = -accelY / 256, // Transform accelerometer axes
ay = accelX / 256, // to grain coordinate space
az = abs(accelZ) / 2048; // Random motion factor
az = (az >= 3) ? 1 : 4 - az; // Clip & invert
ax -= az; // Subtract motion factor from X, Y
ay -= az;
int16_t az2 = az * 2 + 1; // Range of random motion to add back in
// ...and apply 2D accel vector to grain velocities...
int32_t v2; // Velocity squared
float v; // Absolute velocity
for(int i=0; i<N_GRAINS; i++) {
grain[i].vx += ax + random(az2); // A little randomness makes
grain[i].vy += ay + random(az2); // tall stacks topple better!
// Terminal velocity (in any direction) is 256 units -- equal to
// 1 pixel -- which keeps moving grains from passing through each other
// and other such mayhem. Though it takes some extra math, velocity is
// clipped as a 2D vector (not separately-limited X & Y) so that
// diagonal movement isn't faster
v2 = (int32_t)grain[i].vx*grain[i].vx+(int32_t)grain[i].vy*grain[i].vy;
if(v2 > 65536) { // If v^2 > 65536, then v > 256
v = sqrt((float)v2); // Velocity vector magnitude
grain[i].vx = (int)(256.0*(float)grain[i].vx/v); // Maintain heading
grain[i].vy = (int)(256.0*(float)grain[i].vy/v); // Limit magnitude
}
}
// ...then update position of each grain, one at a time, checking for
// collisions and having them react. This really seems like it shouldn't
// work, as only one grain is considered at a time while the rest are
// regarded as stationary. Yet this naive algorithm, taking many not-
// technically-quite-correct steps, and repeated quickly enough,
// visually integrates into something that somewhat resembles physics.
// (I'd initially tried implementing this as a bunch of concurrent and
// "realistic" elastic collisions among circular grains, but the
// calculations and volument of code quickly got out of hand for both
// the tiny 8-bit AVR microcontroller and my tiny dinosaur brain.)
uint16_t i, bytes, oldidx, newidx, delta;
int16_t newx, newy;
for(i=0; i<N_GRAINS; i++) {
newx = grain[i].x + grain[i].vx; // New position in grain space
newy = grain[i].y + grain[i].vy;
if(newx > MAX_X) { // If grain would go out of bounds
newx = MAX_X; // keep it inside, and
grain[i].vx /= -2; // give a slight bounce off the wall
} else if(newx < 0) {
newx = 0;
grain[i].vx /= -2;
}
if(newy > MAX_Y) {
newy = MAX_Y;
grain[i].vy /= -2;
} else if(newy < 0) {
newy = 0;
grain[i].vy /= -2;
}
oldidx = (grain[i].y/256) * WIDTH + (grain[i].x/256); // Prior pixel #
newidx = (newy /256) * WIDTH + (newx /256); // New pixel #
if((oldidx != newidx) && // If grain is moving to a new pixel...
img[newidx]) { // but if that pixel is already occupied...
delta = abs(newidx - oldidx); // What direction when blocked?
if(delta == 1) { // 1 pixel left or right)
newx = grain[i].x; // Cancel X motion
grain[i].vx /= -2; // and bounce X velocity (Y is OK)
newidx = oldidx; // No pixel change
} else if(delta == WIDTH) { // 1 pixel up or down
newy = grain[i].y; // Cancel Y motion
grain[i].vy /= -2; // and bounce Y velocity (X is OK)
newidx = oldidx; // No pixel change
} else { // Diagonal intersection is more tricky...
// Try skidding along just one axis of motion if possible (start w/
// faster axis). Because we've already established that diagonal
// (both-axis) motion is occurring, moving on either axis alone WILL
// change the pixel index, no need to check that again.
if((abs(grain[i].vx) - abs(grain[i].vy)) >= 0) { // X axis is faster
newidx = (grain[i].y / 256) * WIDTH + (newx / 256);
if(!img[newidx]) { // That pixel's free! Take it! But...
newy = grain[i].y; // Cancel Y motion
grain[i].vy /= -2; // and bounce Y velocity
} else { // X pixel is taken, so try Y...
newidx = (newy / 256) * WIDTH + (grain[i].x / 256);
if(!img[newidx]) { // Pixel is free, take it, but first...
newx = grain[i].x; // Cancel X motion
grain[i].vx /= -2; // and bounce X velocity
} else { // Both spots are occupied
newx = grain[i].x; // Cancel X & Y motion
newy = grain[i].y;
grain[i].vx /= -2; // Bounce X & Y velocity
grain[i].vy /= -2;
newidx = oldidx; // Not moving
}
}
} else { // Y axis is faster, start there
newidx = (newy / 256) * WIDTH + (grain[i].x / 256);
if(!img[newidx]) { // Pixel's free! Take it! But...
newx = grain[i].x; // Cancel X motion
grain[i].vy /= -2; // and bounce X velocity
} else { // Y pixel is taken, so try X...
newidx = (grain[i].y / 256) * WIDTH + (newx / 256);
if(!img[newidx]) { // Pixel is free, take it, but first...
newy = grain[i].y; // Cancel Y motion
grain[i].vy /= -2; // and bounce Y velocity
} else { // Both spots are occupied
newx = grain[i].x; // Cancel X & Y motion
newy = grain[i].y;
grain[i].vx /= -2; // Bounce X & Y velocity
grain[i].vy /= -2;
newidx = oldidx; // Not moving
}
}
}
}
}
grain[i].x = newx; // Update grain position
grain[i].y = newy;
img[oldidx] = 0; // Clear old spot (might be same as new, that's OK)
img[newidx] = 255; // Set new spot
grain[i].pos = newidx;
Serial.println(newidx);
}
display.clearDisplay();
for(i=0; i<N_GRAINS; i++) {
int yPos = grain[i].pos/WIDTH;
int xPos = grain[i].pos%WIDTH;
display.drawPixel(xPos , yPos, myRED);
}
display.showBuffer();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment