LED_Sand.ino
1 // SPDX-FileCopyrightText: 2017 Phillip Burgess for Adafruit Industries 2 // 3 // SPDX-License-Identifier: MIT 4 5 //-------------------------------------------------------------------------- 6 // Animated 'sand' for Adafruit Feather. Uses the following parts: 7 // - Feather 32u4 Basic Proto (adafruit.com/product/2771) 8 // - Charlieplex FeatherWing (adafruit.com/product/2965 - any color!) 9 // - LIS3DH accelerometer (2809) 10 // - 350 mAh LiPoly battery (2750) 11 // - SPDT Slide Switch (805) 12 // 13 // This is NOT good "learn from" code for the IS31FL3731; it is "squeeze 14 // every last byte from the microcontroller" code. If you're starting out, 15 // download the Adafruit_IS31FL3731 and Adafruit_GFX libraries, which 16 // provide functions for drawing pixels, lines, etc. 17 //-------------------------------------------------------------------------- 18 19 #include <Wire.h> // For I2C communication 20 #include <Adafruit_LIS3DH.h> // For accelerometer 21 22 #define DISP_ADDR 0x74 // Charlieplex FeatherWing I2C address 23 #define ACCEL_ADDR 0x18 // Accelerometer I2C address 24 #define N_GRAINS 20 // Number of grains of sand 25 #define WIDTH 15 // Display width in pixels 26 #define HEIGHT 7 // Display height in pixels 27 #define MAX_FPS 45 // Maximum redraw rate, frames/second 28 29 // The 'sand' grains exist in an integer coordinate space that's 256X 30 // the scale of the pixel grid, allowing them to move and interact at 31 // less than whole-pixel increments. 32 #define MAX_X (WIDTH * 256 - 1) // Maximum X coordinate in grain space 33 #define MAX_Y (HEIGHT * 256 - 1) // Maximum Y coordinate 34 struct Grain { 35 int16_t x, y; // Position 36 int16_t vx, vy; // Velocity 37 } grain[N_GRAINS]; 38 39 Adafruit_LIS3DH accel = Adafruit_LIS3DH(); 40 uint32_t prevTime = 0; // Used for frames-per-second throttle 41 uint8_t backbuffer = 0, // Index for double-buffered animation 42 img[WIDTH * HEIGHT]; // Internal 'map' of pixels 43 44 const uint8_t PROGMEM remap[] = { // In order to redraw the screen super 45 0, 90, 75, 60, 45, 30, 15, 0, // fast, this sketch bypasses the 46 0, 0, 0, 0, 0, 0, 0, 0, // Adafruit_IS31FL3731 library and 47 0, 91, 76, 61, 46, 31, 16, 1, // writes to the LED driver directly. 48 14, 29, 44, 59, 74, 89,104, 0, // But this means we need to do our 49 0, 92, 77, 62, 47, 32, 17, 2, // own coordinate management, and the 50 13, 28, 43, 58, 73, 88,103, 0, // layout of pixels on the Charlieplex 51 0, 93, 78, 63, 48, 33, 18, 3, // Featherwing is strange! This table 52 12, 27, 42, 57, 72, 87,102, 0, // remaps LED register indices in 53 0, 94, 79, 64, 49, 34, 19, 4, // sequence to the corresponding pixel 54 11, 26, 41, 56, 71, 86,101, 0, // indices in the img[] array. 55 0, 95, 80, 65, 50, 35, 20, 5, 56 10, 25, 40, 55, 70, 85,100, 0, 57 0, 96, 81, 66, 51, 36, 21, 6, 58 9, 24, 39, 54, 69, 84, 99, 0, 59 0, 97, 82, 67, 52, 37, 22, 7, 60 8, 23, 38, 53, 68, 83, 98 61 }; 62 63 // IS31FL3731-RELATED FUNCTIONS -------------------------------------------- 64 65 // Begin I2C transmission and write register address (data then follows) 66 uint8_t writeRegister(uint8_t n) { 67 Wire.beginTransmission(DISP_ADDR); 68 Wire.write(n); // No endTransmission() - left open for add'l writes 69 return 2; // Always returns 2; count of I2C address + register byte n 70 } 71 72 // Select one of eight IS31FL3731 pages, or the Function Registers 73 void pageSelect(uint8_t n) { 74 writeRegister(0xFD); // Command Register 75 Wire.write(n); // Page number (or 0xB = Function Registers) 76 Wire.endTransmission(); 77 } 78 79 // SETUP - RUNS ONCE AT PROGRAM START -------------------------------------- 80 81 void setup(void) { 82 uint8_t i, j, bytes; 83 84 if(!accel.begin(ACCEL_ADDR)) { // Init accelerometer. If it fails... 85 pinMode(LED_BUILTIN, OUTPUT); // Using onboard LED 86 for(i=1;;i++) { // Loop forever... 87 digitalWrite(LED_BUILTIN, i & 1); // LED on/off blink to alert user 88 delay(250); // 1/4 second 89 } 90 } 91 accel.setRange(LIS3DH_RANGE_4_G); // Select accelerometer +/- 4G range 92 93 Wire.setClock(400000); // Run I2C at 400 KHz for faster screen updates 94 95 // Initialize IS31FL3731 Charlieplex LED driver "manually"... 96 pageSelect(0x0B); // Access the Function Registers 97 writeRegister(0); // Starting from first... 98 for(i=0; i<13; i++) Wire.write(10 == i); // Clear all except Shutdown 99 Wire.endTransmission(); 100 for(j=0; j<2; j++) { // For each page used (0 & 1)... 101 pageSelect(j); // Access the Frame Registers 102 for(bytes=i=0; i<180; i++) { // For each register... 103 if(!bytes) bytes = writeRegister(i); // Buf empty? Start xfer @ reg i 104 Wire.write(0xFF * (i < 18)); // 0-17 = enable, 18+ = blink+PWM 105 if(++bytes >= 32) bytes = Wire.endTransmission(); 106 } 107 if(bytes) Wire.endTransmission(); // Write any data left in buffer 108 } 109 110 memset(img, 0, sizeof(img)); // Clear the img[] array 111 for(i=0; i<N_GRAINS; i++) { // For each sand grain... 112 do { 113 grain[i].x = random(WIDTH * 256); // Assign random position within 114 grain[i].y = random(HEIGHT * 256); // the 'grain' coordinate space 115 // Check if corresponding pixel position is already occupied... 116 for(j=0; (j<i) && (((grain[i].x / 256) != (grain[j].x / 256)) || 117 ((grain[i].y / 256) != (grain[j].y / 256))); j++); 118 } while(j < i); // Keep retrying until a clear spot is found 119 img[(grain[i].y / 256) * WIDTH + (grain[i].x / 256)] = 255; // Mark it 120 grain[i].vx = grain[i].vy = 0; // Initial velocity is zero 121 } 122 } 123 124 // MAIN LOOP - RUNS ONCE PER FRAME OF ANIMATION ---------------------------- 125 126 void loop() { 127 // Limit the animation frame rate to MAX_FPS. Because the subsequent sand 128 // calculations are non-deterministic (don't always take the same amount 129 // of time, depending on their current states), this helps ensure that 130 // things like gravity appear constant in the simulation. 131 uint32_t t; 132 while(((t = micros()) - prevTime) < (1000000L / MAX_FPS)); 133 prevTime = t; 134 135 // Display frame rendered on prior pass. It's done immediately after the 136 // FPS sync (rather than after rendering) for consistent animation timing. 137 pageSelect(0x0B); // Function registers 138 writeRegister(0x01); // Picture Display reg 139 Wire.write(backbuffer); // Page # to display 140 Wire.endTransmission(); 141 backbuffer = 1 - backbuffer; // Swap front/back buffer index 142 143 // Read accelerometer... 144 accel.read(); 145 int16_t ax = -accel.y / 256, // Transform accelerometer axes 146 ay = accel.x / 256, // to grain coordinate space 147 az = abs(accel.z) / 2048; // Random motion factor 148 az = (az >= 3) ? 1 : 4 - az; // Clip & invert 149 ax -= az; // Subtract motion factor from X, Y 150 ay -= az; 151 int16_t az2 = az * 2 + 1; // Range of random motion to add back in 152 153 // ...and apply 2D accel vector to grain velocities... 154 int32_t v2; // Velocity squared 155 float v; // Absolute velocity 156 for(int i=0; i<N_GRAINS; i++) { 157 grain[i].vx += ax + random(az2); // A little randomness makes 158 grain[i].vy += ay + random(az2); // tall stacks topple better! 159 // Terminal velocity (in any direction) is 256 units -- equal to 160 // 1 pixel -- which keeps moving grains from passing through each other 161 // and other such mayhem. Though it takes some extra math, velocity is 162 // clipped as a 2D vector (not separately-limited X & Y) so that 163 // diagonal movement isn't faster 164 v2 = (int32_t)grain[i].vx*grain[i].vx+(int32_t)grain[i].vy*grain[i].vy; 165 if(v2 > 65536) { // If v^2 > 65536, then v > 256 166 v = sqrt((float)v2); // Velocity vector magnitude 167 grain[i].vx = (int)(256.0*(float)grain[i].vx/v); // Maintain heading 168 grain[i].vy = (int)(256.0*(float)grain[i].vy/v); // Limit magnitude 169 } 170 } 171 172 // ...then update position of each grain, one at a time, checking for 173 // collisions and having them react. This really seems like it shouldn't 174 // work, as only one grain is considered at a time while the rest are 175 // regarded as stationary. Yet this naive algorithm, taking many not- 176 // technically-quite-correct steps, and repeated quickly enough, 177 // visually integrates into something that somewhat resembles physics. 178 // (I'd initially tried implementing this as a bunch of concurrent and 179 // "realistic" elastic collisions among circular grains, but the 180 // calculations and volument of code quickly got out of hand for both 181 // the tiny 8-bit AVR microcontroller and my tiny dinosaur brain.) 182 183 uint8_t i, bytes, oldidx, newidx, delta; 184 int16_t newx, newy; 185 const uint8_t *ptr = remap; 186 187 for(i=0; i<N_GRAINS; i++) { 188 newx = grain[i].x + grain[i].vx; // New position in grain space 189 newy = grain[i].y + grain[i].vy; 190 if(newx > MAX_X) { // If grain would go out of bounds 191 newx = MAX_X; // keep it inside, and 192 grain[i].vx /= -2; // give a slight bounce off the wall 193 } else if(newx < 0) { 194 newx = 0; 195 grain[i].vx /= -2; 196 } 197 if(newy > MAX_Y) { 198 newy = MAX_Y; 199 grain[i].vy /= -2; 200 } else if(newy < 0) { 201 newy = 0; 202 grain[i].vy /= -2; 203 } 204 205 oldidx = (grain[i].y/256) * WIDTH + (grain[i].x/256); // Prior pixel # 206 newidx = (newy /256) * WIDTH + (newx /256); // New pixel # 207 if((oldidx != newidx) && // If grain is moving to a new pixel... 208 img[newidx]) { // but if that pixel is already occupied... 209 delta = abs(newidx - oldidx); // What direction when blocked? 210 if(delta == 1) { // 1 pixel left or right) 211 newx = grain[i].x; // Cancel X motion 212 grain[i].vx /= -2; // and bounce X velocity (Y is OK) 213 newidx = oldidx; // No pixel change 214 } else if(delta == WIDTH) { // 1 pixel up or down 215 newy = grain[i].y; // Cancel Y motion 216 grain[i].vy /= -2; // and bounce Y velocity (X is OK) 217 newidx = oldidx; // No pixel change 218 } else { // Diagonal intersection is more tricky... 219 // Try skidding along just one axis of motion if possible (start w/ 220 // faster axis). Because we've already established that diagonal 221 // (both-axis) motion is occurring, moving on either axis alone WILL 222 // change the pixel index, no need to check that again. 223 if((abs(grain[i].vx) - abs(grain[i].vy)) >= 0) { // X axis is faster 224 newidx = (grain[i].y / 256) * WIDTH + (newx / 256); 225 if(!img[newidx]) { // That pixel's free! Take it! But... 226 newy = grain[i].y; // Cancel Y motion 227 grain[i].vy /= -2; // and bounce Y velocity 228 } else { // X pixel is taken, so try Y... 229 newidx = (newy / 256) * WIDTH + (grain[i].x / 256); 230 if(!img[newidx]) { // Pixel is free, take it, but first... 231 newx = grain[i].x; // Cancel X motion 232 grain[i].vx /= -2; // and bounce X velocity 233 } else { // Both spots are occupied 234 newx = grain[i].x; // Cancel X & Y motion 235 newy = grain[i].y; 236 grain[i].vx /= -2; // Bounce X & Y velocity 237 grain[i].vy /= -2; 238 newidx = oldidx; // Not moving 239 } 240 } 241 } else { // Y axis is faster, start there 242 newidx = (newy / 256) * WIDTH + (grain[i].x / 256); 243 if(!img[newidx]) { // Pixel's free! Take it! But... 244 newx = grain[i].x; // Cancel X motion 245 grain[i].vy /= -2; // and bounce X velocity 246 } else { // Y pixel is taken, so try X... 247 newidx = (grain[i].y / 256) * WIDTH + (newx / 256); 248 if(!img[newidx]) { // Pixel is free, take it, but first... 249 newy = grain[i].y; // Cancel Y motion 250 grain[i].vy /= -2; // and bounce Y velocity 251 } else { // Both spots are occupied 252 newx = grain[i].x; // Cancel X & Y motion 253 newy = grain[i].y; 254 grain[i].vx /= -2; // Bounce X & Y velocity 255 grain[i].vy /= -2; 256 newidx = oldidx; // Not moving 257 } 258 } 259 } 260 } 261 } 262 grain[i].x = newx; // Update grain position 263 grain[i].y = newy; 264 img[oldidx] = 0; // Clear old spot (might be same as new, that's OK) 265 img[newidx] = 255; // Set new spot 266 } 267 268 // Update pixel data in LED driver 269 pageSelect(backbuffer); // Select background buffer 270 for(i=bytes=0; i<sizeof(remap); i++) { 271 if(!bytes) bytes = writeRegister(0x24 + i); 272 Wire.write(img[pgm_read_byte(ptr++)] / 3); // Write each byte to matrix 273 if(++bytes >= 32) bytes = Wire.endTransmission(); 274 } 275 if(bytes) Wire.endTransmission(); 276 } 277