Skip to content

Instantly share code, notes, and snippets.

@mohitbhoite
Created January 25, 2018 20:48
Show Gist options
  • Save mohitbhoite/8ad928f820a8a8ec590a02e42b7cd7ea to your computer and use it in GitHub Desktop.
Save mohitbhoite/8ad928f820a8a8ec590a02e42b7cd7ea to your computer and use it in GitHub Desktop.
Modified version of Adafruit's led sand animation
#include <SPI.h>
#include <ADXL362.h>
#include <Wire.h> // For I2C communication
#include <Adafruit_IS31FL3731.h>
//#include <Adafruit_LIS3DH.h> // For accelerometer
#define DISP_ADDR 0x74 // Charlieplex FeatherWing I2C address
//#define ACCEL_ADDR 0x18 // Accelerometer I2C address
#define N_GRAINS 20 // Number of grains of sand
#define WIDTH 15 // Display width in pixels
#define HEIGHT 7 // Display height in pixels
#define MAX_FPS 200 // 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
} grain[N_GRAINS];
ADXL362 xl;
int16_t temp;
int16_t XValue, YValue, ZValue, Temperature;
//Adafruit_LIS3DH accel = Adafruit_LIS3DH();
uint32_t prevTime = 0; // Used for frames-per-second throttle
uint8_t backbuffer = 0, // Index for double-buffered animation
img[WIDTH * HEIGHT]; // Internal 'map' of pixels
const uint8_t PROGMEM remap[] = { // In order to redraw the screen super
0, 90, 75, 60, 45, 30, 15, 0, // fast, this sketch bypasses the
0, 0, 0, 0, 0, 0, 0, 0, // Adafruit_IS31FL3731 library and
0, 91, 76, 61, 46, 31, 16, 1, // writes to the LED driver directly.
14, 29, 44, 59, 74, 89,104, 0, // But this means we need to do our
0, 92, 77, 62, 47, 32, 17, 2, // own coordinate management, and the
13, 28, 43, 58, 73, 88,103, 0, // layout of pixels on the Charlieplex
0, 93, 78, 63, 48, 33, 18, 3, // Featherwing is strange! This table
12, 27, 42, 57, 72, 87,102, 0, // remaps LED register indices in
0, 94, 79, 64, 49, 34, 19, 4, // sequence to the corresponding pixel
11, 26, 41, 56, 71, 86,101, 0, // indices in the img[] array.
0, 95, 80, 65, 50, 35, 20, 5,
10, 25, 40, 55, 70, 85,100, 0,
0, 96, 81, 66, 51, 36, 21, 6,
9, 24, 39, 54, 69, 84, 99, 0,
0, 97, 82, 67, 52, 37, 22, 7,
8, 23, 38, 53, 68, 83, 98
};
// IS31FL3731-RELATED FUNCTIONS --------------------------------------------
// Begin I2C transmission and write register address (data then follows)
uint8_t writeRegister(uint8_t n) {
Wire.beginTransmission(DISP_ADDR);
Wire.write(n); // No endTransmission() - left open for add'l writes
return 2; // Always returns 2; count of I2C address + register byte n
}
// Select one of eight IS31FL3731 pages, or the Function Registers
void pageSelect(uint8_t n) {
writeRegister(0xFD); // Command Register
Wire.write(n); // Page number (or 0xB = Function Registers)
Wire.endTransmission();
}
// SETUP - RUNS ONCE AT PROGRAM START --------------------------------------
void setup(void) {
Serial.begin(9600);
uint8_t i, j, bytes;
xl.begin(11); // Setup SPI protocol, issue device soft reset. CS pin is 11
xl.beginMeasure(); // Switch ADXL362 to measure mode
delay(500);
// if(!accel.begin(ACCEL_ADDR)) { // Init accelerometer. If it fails...
// pinMode(LED_BUILTIN, OUTPUT); // Using onboard LED
// digitalWrite(LED_BUILTIN, 0);
// for(i=1;;i++) { // Loop forever...
// digitalWrite(LED_BUILTIN, i & 1); // LED on/off blink to alert user
// delay(250); // 1/4 second
// }
// }
// accel.setRange(LIS3DH_RANGE_4_G); // Select accelerometer +/- 4G range
// you need to call Wire.begin() here since we have commented out the code for accel.begin()
Wire.begin();
Wire.setClock(400000); // Run I2C at 400 KHz for faster screen updates
// Initialize IS31FL3731 Charlieplex LED driver "manually"...
pageSelect(0x0B); // Access the Function Registers
writeRegister(0); // Starting from first...
for(i=0; i<13; i++) Wire.write(10 == i); // Clear all except Shutdown
Wire.endTransmission();
for(j=0; j<2; j++) { // For each page used (0 & 1)...
pageSelect(j); // Access the Frame Registers
for(bytes=i=0; i<180; i++) { // For each register...
if(!bytes) bytes = writeRegister(i); // Buf empty? Start xfer @ reg i
Wire.write(0xFF * (i < 18)); // 0-17 = enable, 18+ = blink+PWM
if(++bytes >= 32) bytes = Wire.endTransmission();
}
if(bytes) Wire.endTransmission(); // Write any data left in buffer
}
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].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...
xl.readXYZTData(XValue, YValue, ZValue, Temperature);
// Serial.println(XValue);
// Serial.println(YValue);
// Serial.println(ZValue);
// delay(100);
// accel.read();
int16_t ax = YValue / 256, // Transform accelerometer axes
ay = XValue / 256, // to grain coordinate space
az = abs(ZValue) / 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.)
uint8_t i, bytes, oldidx, newidx, delta;
int16_t newx, newy;
const uint8_t *ptr = remap;
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
}
// Update pixel data in LED driver
pageSelect(backbuffer); // Select background buffer
for(i=bytes=0; i<sizeof(remap); i++) {
if(!bytes) bytes = writeRegister(0x24 + i);
Wire.write(img[pgm_read_byte(ptr++)] / 3); // Write each byte to matrix
if(++bytes >= 32) bytes = Wire.endTransmission();
}
if(bytes) Wire.endTransmission();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment