https://learn.adafruit.com/animated-led-sand/code
I didn't have a LIS3DH, but instead figured it wouldn't be too difficult to rework the code for a ADXL343. And actually, I have the Adafruit ADXL343 + ADT7410 Sensor FeatherWing (Product ID: 4147). I'm just not using the temperature section here.
I've made some tweaks to the original sketch, and the ADXL343 sensor is recognized, my code compiles, and I get grain motion, but no gravity or accelerometer interaction. Kind of like bugs running around, with no change when the device is tipped or turned around.
I'm using ADXL343 at address 0x53 (because 0x1D wouldn't compile). The biggest changes I made were in the accelerometer reading section. The original, which uses accel.read(); doesn't work for the ADXL343, nor does the accel.y, accel.x designations for the variables. So I reworked that based on my understanding of what the ADXL343 wants to see.
See full code below. I've commented the sections I modified. Any thoughts on how to get the ADXL343 to respond better?
Code: Select all
// 3/24/23 JJD - Modified for use with Adafruit ADXL343 + ADT7410 Sensor FeatherWing Product ID: 4147
// (instead of LIS3DH accelerometer)
// SPDX-FileCopyrightText: 2017 Phillip Burgess for Adafruit Industries
//
// SPDX-License-Identifier: MIT
// https://learn.adafruit.com/animated-led-sand/code
//--------------------------------------------------------------------------
// 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!)
// - 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 <Adafruit_Sensor.h>
// #include <Adafruit_LIS3DH.h> // For accelerometer // JJD replacing with Adafruit_ADXL343
#include <Adafruit_ADXL343.h> // JJD
//#include "Adafruit_ADT7410.h" // JJD ADT7410 Temp sensor (0x48 0x49 0x4A or 0x4B) Not used for now
#define DISP_ADDR 0x74 // Charlieplex FeatherWing I2C address // 0x74 is from original sketch
// IS31FL3731 144-LED CharliePlex driver (0x74 0x75 0x66 or 0x77)
//#define ACCEL_ADDR 0x18 // Accelerometer I2C address // 0x18 is LIS3DH accelerometer
#define ACCEL_ADDR 0x53 // Accelerometer I2C address // ADXL343 3-axis accelerometer (0x1D or 0x53)
#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 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
} grain[N_GRAINS];
// Adafruit_LIS3DH accel = Adafruit_LIS3DH(); // replace
// Create the ADXL343 accelerometer sensor object
Adafruit_ADXL343 accel = Adafruit_ADXL343(12345); // JJD
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) {
uint8_t i, j, bytes;
if(!accel.begin(ACCEL_ADDR)) { // Init accelerometer. If it fails... ACCEL_ADDR fails
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(250); // 1/4 second
}
}
//accel.setRange(LIS3DH_RANGE_4_G); // Select accelerometer +/- 4G range // replace
accel.setRange(ADXL343_RANGE_4_G); // Select accelerometer +/- 4G range // JJD
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
/* JJD reworked this section for ADXL343 */
// Read accelerometer...
sensors_event_t event; // accel.read(); from original sketch didn't work for ADXL343
accel.getEvent(&event);
int16_t ax = -event.acceleration.y / 256, // Transform accelerometer axes
ay = event.acceleration.x / 256, // to grain coordinate space
az = abs(event.acceleration.z) / 2048; // Random motion factor
// 3/24/23 Having issues getting accel to work properly; gravity effect not visible
/* End JJD tweaks */
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();
}