/ LED_Sand / LED_Sand.ino
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