├── README.md └── fallingSand ├── fallingSand.ino └── imgBufferGFX.h /README.md: -------------------------------------------------------------------------------- 1 | # Falling-Sand-Matrix 2 | A falling sand simulation using an ESP32, RGB Led Matrix and an accelerometer 3 | -------------------------------------------------------------------------------- /fallingSand/fallingSand.ino: -------------------------------------------------------------------------------- 1 | /******************************************************************* 2 | Falling sand animation using a 64x64 RGB Led Matrix,an ESP32 and 3 | a MPU6050 module. 4 | * * 5 | Built using an ESP32 and using my own ESP32 Matrix Shield 6 | https://www.tindie.com/products/brianlough/esp32-matrix-shield-mini-32/ 7 | 8 | Adapted for the Matrix and MPU6050 by Brian Lough 9 | YouTube: https://www.youtube.com/brianlough 10 | Tindie: https://www.tindie.com/stores/brianlough/ 11 | Twitter: https://twitter.com/witnessmenow 12 | *******************************************************************/ 13 | 14 | // This was the original header from the Adafruit Learn guide this project 15 | // was based on 16 | // https://learn.adafruit.com/matrix-led-sand/overview 17 | // -------------------------------------------------------------------------- 18 | // Animated 'sand' for Adafruit Feather. Uses the following parts: 19 | // - Feather 32u4 Basic Proto (adafruit.com/product/2771) 20 | // - Charlieplex FeatherWing (adafruit.com/product/2965 - any color!) 21 | // - LIS3DH accelerometer (2809) 22 | // - 350 mAh LiPoly battery (2750) 23 | // - SPDT Slide Switch (805) 24 | // 25 | // This is NOT good "learn from" code for the IS31FL3731; it is "squeeze 26 | // every last byte from the microcontroller" code. If you're starting out, 27 | // download the Adafruit_IS31FL3731 and Adafruit_GFX libraries, which 28 | // provide functions for drawing pixels, lines, etc. 29 | //-------------------------------------------------------------------------- 30 | 31 | #include // For I2C communication 32 | 33 | 34 | //Adtional Libraries to install 35 | 36 | #include 37 | // For communicating with the MPU6050 38 | // 39 | // You need to install my version from GitHub 40 | // https://github.com/witnessmenow/Arduino-MPU6050 41 | 42 | #define double_buffer // this must be enabled to stop flickering 43 | #include 44 | // The library for controlling the LED Matrix 45 | // 46 | // Can be installed from the library manager 47 | // https://github.com/2dom/PxMatrix 48 | 49 | // Adafruit GFX library is a dependancy for the PxMatrix Library 50 | // Can be installed from the library manager 51 | // https://github.com/adafruit/Adafruit-GFX-Library 52 | 53 | #include "imgBufferGFX.h" 54 | 55 | #define N_GRAINS 1000 // Number of grains of sand 56 | #define WIDTH 64 // Display width in pixels 57 | #define HEIGHT 64 // Display height in pixels 58 | #define MAX_FPS 45 // Maximum redraw rate, frames/second 59 | 60 | // The 'sand' grains exist in an integer coordinate space that's 256X 61 | // the scale of the pixel grid, allowing them to move and interact at 62 | // less than whole-pixel increments. 63 | #define MAX_X (WIDTH * 256 - 1) // Maximum X coordinate in grain space 64 | #define MAX_Y (HEIGHT * 256 - 1) // Maximum Y coordinate 65 | struct Grain { 66 | int16_t x, y; // Position 67 | int16_t vx, vy; // Velocity 68 | uint16_t pos; 69 | uint16_t colour; 70 | } grain[N_GRAINS]; 71 | 72 | // ---------------------------- 73 | // Wiring and Display setup 74 | // ---------------------------- 75 | 76 | #define P_LAT 22 77 | #define P_A 19 78 | #define P_B 23 79 | #define P_C 18 80 | #define P_D 5 81 | #define P_E 15 82 | // #define P_OE 2 83 | #define P_OE 21 // Feather Huzzah 84 | hw_timer_t * timer = NULL; 85 | portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED; 86 | 87 | // This defines the 'on' time of the display is us. The larger this number, 88 | // the brighter the display. If too large the ESP will crash 89 | uint8_t display_draw_time = 10; //10-50 is usually fine 90 | 91 | //PxMATRIX display(matrix_width, matrix_height, P_LAT, P_OE, P_A, P_B, P_C); 92 | //PxMATRIX display(64,32,P_LAT, P_OE,P_A,P_B,P_C,P_D); 93 | PxMATRIX display(64, 64, P_LAT, P_OE, P_A, P_B, P_C, P_D, P_E); 94 | 95 | #define NUM_COLOURS 5 96 | 97 | uint16_t myRED = display.color565(255, 0, 0); 98 | uint16_t myGREEN = display.color565(0, 255, 0); 99 | uint16_t myBLUE = display.color565(0, 0, 255); 100 | uint16_t myMAGENTA = display.color565(255, 0, 255); 101 | uint16_t myYELLOW = display.color565(255, 255, 0); 102 | uint16_t myCYAN = display.color565(0, 255, 255); 103 | 104 | uint16_t myCOLORS[6]={myRED,myGREEN,myCYAN,myMAGENTA,myYELLOW,myBLUE}; 105 | 106 | MPU6050 mpu; 107 | uint32_t prevTime = 0; // Used for frames-per-second throttle 108 | uint16_t backbuffer = 0, // Index for double-buffered animation 109 | img[WIDTH * HEIGHT]; // Internal 'map' of pixels 110 | 111 | ImgBufferGFX imgWrapper(img, WIDTH, HEIGHT); 112 | 113 | float xOffset = -1350; 114 | float yOffset = -2590; 115 | 116 | void pixelTask(void *param) { 117 | 118 | while (!mpu.begin(MPU6050_SCALE_2000DPS, MPU6050_RANGE_4G, MPU6050_ADDRESS, 27, 33)) 119 | { 120 | //Serial.println("Could not find a valid MPU6050 sensor, check wiring!"); 121 | delay(500); 122 | } 123 | 124 | Vector accelVector = mpu.readRawAccel(); 125 | 126 | float xOffset = (accelVector.XAxis * -1) * -1; 127 | float yOffset = (accelVector.YAxis * -1) * -1; 128 | 129 | 130 | while (true) { 131 | // Limit the animation frame rate to MAX_FPS. Because the subsequent sand 132 | // calculations are non-deterministic (don't always take the same amount 133 | // of time, depending on their current states), this helps ensure that 134 | // things like gravity appear constant in the simulation. 135 | uint32_t t; 136 | while (((t = micros()) - prevTime) < (1000000L / MAX_FPS)); 137 | prevTime = t; 138 | 139 | // // Display frame rendered on prior pass. It's done immediately after the 140 | // // FPS sync (rather than after rendering) for consistent animation timing. 141 | // pageSelect(0x0B); // Function registers 142 | // writeRegister(0x01); // Picture Display reg 143 | // Wire.write(backbuffer); // Page # to display 144 | // Wire.endTransmission(); 145 | // backbuffer = 1 - backbuffer; // Swap front/back buffer index 146 | 147 | // Read accelerometer... 148 | //Vector accelVector = mpu.readNormalizeAccel(); 149 | Vector accelVector = mpu.readRawAccel(); 150 | 151 | float accelX = (accelVector.XAxis * -1) + xOffset; 152 | float accelY = (accelVector.YAxis * -1) + yOffset; 153 | float accelZ = accelVector.ZAxis; 154 | 155 | int16_t ax = -accelY / 256, // Transform accelerometer axes 156 | ay = accelX / 256, // to grain coordinate space 157 | az = abs(accelZ) / 2048; // Random motion factor 158 | az = (az >= 3) ? 1 : 4 - az; // Clip & invert 159 | ax -= az; // Subtract motion factor from X, Y 160 | ay -= az; 161 | int16_t az2 = az * 2 + 1; // Range of random motion to add back in 162 | 163 | // ...and apply 2D accel vector to grain velocities... 164 | int32_t v2; // Velocity squared 165 | float v; // Absolute velocity 166 | for (int i = 0; i < N_GRAINS; i++) { 167 | grain[i].vx += ax + random(az2); // A little randomness makes 168 | grain[i].vy += ay + random(az2); // tall stacks topple better! 169 | // Terminal velocity (in any direction) is 256 units -- equal to 170 | // 1 pixel -- which keeps moving grains from passing through each other 171 | // and other such mayhem. Though it takes some extra math, velocity is 172 | // clipped as a 2D vector (not separately-limited X & Y) so that 173 | // diagonal movement isn't faster 174 | v2 = (int32_t)grain[i].vx * grain[i].vx + (int32_t)grain[i].vy * grain[i].vy; 175 | if (v2 > 65536) { // If v^2 > 65536, then v > 256 176 | v = sqrt((float)v2); // Velocity vector magnitude 177 | grain[i].vx = (int)(256.0 * (float)grain[i].vx / v); // Maintain heading 178 | grain[i].vy = (int)(256.0 * (float)grain[i].vy / v); // Limit magnitude 179 | } 180 | } 181 | 182 | // ...then update position of each grain, one at a time, checking for 183 | // collisions and having them react. This really seems like it shouldn't 184 | // work, as only one grain is considered at a time while the rest are 185 | // regarded as stationary. Yet this naive algorithm, taking many not- 186 | // technically-quite-correct steps, and repeated quickly enough, 187 | // visually integrates into something that somewhat resembles physics. 188 | // (I'd initially tried implementing this as a bunch of concurrent and 189 | // "realistic" elastic collisions among circular grains, but the 190 | // calculations and volument of code quickly got out of hand for both 191 | // the tiny 8-bit AVR microcontroller and my tiny dinosaur brain.) 192 | 193 | uint16_t i, bytes, oldidx, newidx, delta; 194 | int16_t newx, newy; 195 | 196 | for (i = 0; i < N_GRAINS; i++) { 197 | newx = grain[i].x + grain[i].vx; // New position in grain space 198 | newy = grain[i].y + grain[i].vy; 199 | if (newx > MAX_X) { // If grain would go out of bounds 200 | newx = MAX_X; // keep it inside, and 201 | grain[i].vx /= -2; // give a slight bounce off the wall 202 | } else if (newx < 0) { 203 | newx = 0; 204 | grain[i].vx /= -2; 205 | } 206 | if (newy > MAX_Y) { 207 | newy = MAX_Y; 208 | grain[i].vy /= -2; 209 | } else if (newy < 0) { 210 | newy = 0; 211 | grain[i].vy /= -2; 212 | } 213 | 214 | oldidx = (grain[i].y / 256) * WIDTH + (grain[i].x / 256); // Prior pixel # 215 | newidx = (newy / 256) * WIDTH + (newx / 256); // New pixel # 216 | if ((oldidx != newidx) && // If grain is moving to a new pixel... 217 | img[newidx]) { // but if that pixel is already occupied... 218 | delta = abs(newidx - oldidx); // What direction when blocked? 219 | if (delta == 1) { // 1 pixel left or right) 220 | newx = grain[i].x; // Cancel X motion 221 | grain[i].vx /= -2; // and bounce X velocity (Y is OK) 222 | newidx = oldidx; // No pixel change 223 | } else if (delta == WIDTH) { // 1 pixel up or down 224 | newy = grain[i].y; // Cancel Y motion 225 | grain[i].vy /= -2; // and bounce Y velocity (X is OK) 226 | newidx = oldidx; // No pixel change 227 | } else { // Diagonal intersection is more tricky... 228 | // Try skidding along just one axis of motion if possible (start w/ 229 | // faster axis). Because we've already established that diagonal 230 | // (both-axis) motion is occurring, moving on either axis alone WILL 231 | // change the pixel index, no need to check that again. 232 | if ((abs(grain[i].vx) - abs(grain[i].vy)) >= 0) { // X axis is faster 233 | newidx = (grain[i].y / 256) * WIDTH + (newx / 256); 234 | if (!img[newidx]) { // That pixel's free! Take it! But... 235 | newy = grain[i].y; // Cancel Y motion 236 | grain[i].vy /= -2; // and bounce Y velocity 237 | } else { // X pixel is taken, so try Y... 238 | newidx = (newy / 256) * WIDTH + (grain[i].x / 256); 239 | if (!img[newidx]) { // Pixel is free, take it, but first... 240 | newx = grain[i].x; // Cancel X motion 241 | grain[i].vx /= -2; // and bounce X velocity 242 | } else { // Both spots are occupied 243 | newx = grain[i].x; // Cancel X & Y motion 244 | newy = grain[i].y; 245 | grain[i].vx /= -2; // Bounce X & Y velocity 246 | grain[i].vy /= -2; 247 | newidx = oldidx; // Not moving 248 | } 249 | } 250 | } else { // Y axis is faster, start there 251 | newidx = (newy / 256) * WIDTH + (grain[i].x / 256); 252 | if (!img[newidx]) { // Pixel's free! Take it! But... 253 | newx = grain[i].x; // Cancel X motion 254 | grain[i].vy /= -2; // and bounce X velocity 255 | } else { // Y pixel is taken, so try X... 256 | newidx = (grain[i].y / 256) * WIDTH + (newx / 256); 257 | if (!img[newidx]) { // Pixel is free, take it, but first... 258 | newy = grain[i].y; // Cancel Y motion 259 | grain[i].vy /= -2; // and bounce Y velocity 260 | } else { // Both spots are occupied 261 | newx = grain[i].x; // Cancel X & Y motion 262 | newy = grain[i].y; 263 | grain[i].vx /= -2; // Bounce X & Y velocity 264 | grain[i].vy /= -2; 265 | newidx = oldidx; // Not moving 266 | } 267 | } 268 | } 269 | } 270 | } 271 | grain[i].x = newx; // Update grain position 272 | grain[i].y = newy; 273 | img[oldidx] = 0; // Clear old spot (might be same as new, that's OK) 274 | img[newidx] = 255; // Set new spot 275 | grain[i].pos = newidx; 276 | //Serial.println(newidx); 277 | } 278 | } 279 | } 280 | 281 | void IRAM_ATTR display_updater() { 282 | display.display(display_draw_time); 283 | } 284 | 285 | void display_update_enable(bool is_enable) 286 | { 287 | if (is_enable) 288 | { 289 | timer = timerBegin(0, 80, true); 290 | timerAttachInterrupt(timer, &display_updater, true); 291 | timerAlarmWrite(timer, 2000, true); 292 | timerAlarmEnable(timer); 293 | } 294 | else 295 | { 296 | timerDetachInterrupt(timer); 297 | timerAlarmDisable(timer); 298 | } 299 | } 300 | 301 | // SETUP - RUNS ONCE AT PROGRAM START -------------------------------------- 302 | 303 | void setup(void) { 304 | int i, j, bytes; 305 | 306 | 307 | //Serial.begin(115200); 308 | 309 | //Serial.println("Initialize MPU6050"); 310 | 311 | // Define your display layout here, e.g. 1/8 step 312 | display.begin(32); 313 | 314 | display.setFastUpdate(true); 315 | display.clearDisplay(); 316 | display_update_enable(true); 317 | display.showBuffer(); 318 | 319 | memset(img, 0, sizeof(img)); // Clear the img[] array 320 | 321 | imgWrapper.setCursor(12, 18); 322 | imgWrapper.setTextColor(myBLUE); 323 | imgWrapper.setTextSize(4); 324 | imgWrapper.print("HI"); 325 | 326 | for (i = 0; i < N_GRAINS; i++) { // For each sand grain... 327 | 328 | int imgIndex = 0; 329 | do { 330 | grain[i].x = random(WIDTH * 256); // Assign random position within 331 | grain[i].y = random(HEIGHT * 256); // the 'grain' coordinate space 332 | // Check if corresponding pixel position is already occupied... 333 | for (j = 0; (j < i) && (((grain[i].x / 256) != (grain[j].x / 256)) || 334 | ((grain[i].y / 256) != (grain[j].y / 256))); j++); 335 | imgIndex = (grain[i].y / 256) * WIDTH + (grain[i].x / 256); 336 | } while (img[imgIndex] != 0); // Keep retrying until a clear spot is found 337 | img[imgIndex] = 255; // Mark it 338 | grain[i].pos = (grain[i].y / 256) * WIDTH + (grain[i].x / 256); 339 | grain[i].vx = grain[i].vy = 0; // Initial velocity is zero 340 | 341 | grain[i].colour = myCOLORS[i%NUM_COLOURS]; 342 | } 343 | 344 | //imgWrapper.drawChar(24,24, 'u', myBLUE, 0, 3); 345 | 346 | 347 | //imgWrapper.fillRect(24, 24, 16, 16, myBLUE); 348 | 349 | TaskHandle_t xHandle = NULL; 350 | xTaskCreatePinnedToCore(pixelTask, "PixelTask1", 5000, 0, (2 | portPRIVILEGE_BIT), &xHandle, 0); 351 | } 352 | 353 | // MAIN LOOP - RUNS ONCE PER FRAME OF ANIMATION ---------------------------- 354 | 355 | void loop() { 356 | 357 | display.clearDisplay(); 358 | //display.drawChar(24,24, 'u', myBLUE, 0, 3); 359 | for (int i = 0; i < N_GRAINS; i++) { 360 | int yPos = grain[i].pos / WIDTH; 361 | int xPos = grain[i].pos % WIDTH; 362 | display.drawPixel(xPos , yPos, grain[i].colour); 363 | } 364 | //display.drawChar(24,24, 'u', myBLUE, 0, 3); 365 | //display.fillRect(24, 24, 16, 16, myBLUE); 366 | 367 | display.setCursor(12, 18); 368 | display.setTextColor(myBLUE); 369 | display.setTextSize(4); 370 | display.print("HI"); 371 | display.showBuffer(); 372 | } 373 | 374 | -------------------------------------------------------------------------------- /fallingSand/imgBufferGFX.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Adafruit_GFX.h" 3 | 4 | class ImgBufferGFX : public Adafruit_GFX 5 | { 6 | public: 7 | uint16_t *imgBuffer; 8 | int imgBufferSize; 9 | int resX; 10 | int resY; 11 | 12 | ImgBufferGFX(uint16_t *img, const int xres, const int yres) 13 | :Adafruit_GFX(xres, yres) 14 | { 15 | imgBuffer = img; 16 | imgBufferSize = xres * yres; 17 | resX = xres; 18 | resY = yres; 19 | } 20 | 21 | virtual void drawPixel(int16_t x, int16_t y, uint16_t color) 22 | { 23 | int16_t index = (y * resX) + x; 24 | if(index < imgBufferSize){ 25 | imgBuffer[index] = color; 26 | } 27 | } 28 | }; 29 | --------------------------------------------------------------------------------