Need help simulating hourglass on WS2812B matrix using ESP32 (not Arduino Uno)

Hi everyone :waving_hand:

I'm working on a fun project where I'm trying to simulate a digital hourglass using an 8x32 WS2812B LED matrix and an Arduino UNO (currently powered through an ESP32 board only for stable 5V power). The idea is to display a visual falling sand effect, just like in a real hourglass.

Right now, I'm only focusing on the simulation part — I want the LEDs to show particles falling from the top to the bottom over time. Eventually, I plan to add an MPU6050 accelerometer to detect when the hourglass is flipped and make the sand fall in the other direction.

Here's my current problem:
I wrote the code to simulate the sand falling effect. At the beginning, a few yellow "grains" fall from the top, but after just a few drops, everything stops — it looks like the sand doesn't continue falling as expected. I'm not sure why the animation halts early, and would really appreciate your insights.

Here is the code I'm using:


#include <FastLED.h>

// LED matrix configuration
#define LED_PIN 21       // The pin connected to the matrix DIN
#define NUM_LEDS 256     // Total number of LEDs in the matrix (8x32)
#define LED_TYPE WS2812B
#define COLOR_ORDER GRB
#define BRIGHTNESS 100   // Brightness (0-255)

// Matrix size
#define MATRIX_WIDTH 8   // Width
#define MATRIX_HEIGHT 32 // Height

// Simulation speed
#define SAND_FALL_DELAY 50 // Milliseconds between updates
#define DROP_RATE 2        // Number of iterations before generating a new drop

// Colors
#define SAND_COLOR CRGB(255, 220, 100)     // Sand color (light yellow)
#define BACKGROUND_COLOR CRGB(0, 0, 15)    // Background color (very dark blue)

// Height of the initial filled sand section
#define INITIAL_SAND_HEIGHT 10 // Number of top rows initially filled with sand

// LED array
CRGB leds[NUM_LEDS];

// Logical matrix to track sand positions
bool sandMatrix[MATRIX_HEIGHT][MATRIX_WIDTH] = {0};

// Global variables for simulation state
int dropCounter = 0;         // Iteration counter for creating drops
int topSandLevel = 0;        // Number of full sand rows left on top
int bottomSandLevel = 0;     // Number of full sand rows accumulated at the bottom
bool isFlipped = false;      // Whether the hourglass is flipped
bool flipInProgress = false; // Whether a flip is currently in progress
unsigned long lastFlipTime = 0; // Time of the last flip

// Map XY coordinates to a 1D LED array index
uint16_t XY(uint8_t x, uint8_t y) {
  if (x >= MATRIX_WIDTH || y >= MATRIX_HEIGHT) {
    return 0;
  }

  uint16_t i;

  // The matrix is arranged in zigzag format
  // Even rows: left to right, odd rows: right to left
  if (y & 0x01) {
    i = (y * MATRIX_WIDTH) + (MATRIX_WIDTH - 1 - x);
  } else {
    i = (y * MATRIX_WIDTH) + x;
  }

  return i;
}

// Update LEDs based on the sand matrix
void updateLEDs() {
  for (int y = 0; y < MATRIX_HEIGHT; y++) {
    for (int x = 0; x < MATRIX_WIDTH; x++) {
      leds[XY(x, y)] = sandMatrix[y][x] ? SAND_COLOR : BACKGROUND_COLOR;
    }
  }
  FastLED.show();
}

// Initialize the hourglass state
void initHourglass() {
  // Clear the matrix
  for (int y = 0; y < MATRIX_HEIGHT; y++) {
    for (int x = 0; x < MATRIX_WIDTH; x++) {
      sandMatrix[y][x] = false;
    }
  }
  
  if (!isFlipped) {
    // Fill top rows with sand
    for (int y = 0; y < INITIAL_SAND_HEIGHT; y++) {
      for (int x = 0; x < MATRIX_WIDTH; x++) {
        sandMatrix[y][x] = true;
      }
    }
    topSandLevel = INITIAL_SAND_HEIGHT;
    bottomSandLevel = 0;
  } else {
    // Fill bottom rows with sand
    for (int y = MATRIX_HEIGHT - INITIAL_SAND_HEIGHT; y < MATRIX_HEIGHT; y++) {
      for (int x = 0; x < MATRIX_WIDTH; x++) {
        sandMatrix[y][x] = true;
      }
    }
    topSandLevel = 0;
    bottomSandLevel = INITIAL_SAND_HEIGHT;
  }
  
  dropCounter = 0;
  updateLEDs();
}

// Randomly remove a drop of sand from the topmost full row
void dropSandFromTop() {
  if (topSandLevel <= 0) return;

  int lastFullRow = -1;
  for (int y = 0; y < MATRIX_HEIGHT / 2; y++) {
    bool isRowFull = true;
    for (int x = 0; x < MATRIX_WIDTH; x++) {
      if (!sandMatrix[y][x]) {
        isRowFull = false;
        break;
      }
    }
    if (isRowFull) {
      lastFullRow = y;
    } else {
      break;
    }
  }

  if (lastFullRow >= 0) {
    int column = random(MATRIX_WIDTH);
    sandMatrix[lastFullRow][column] = false;

    bool isRowEmpty = true;
    for (int x = 0; x < MATRIX_WIDTH; x++) {
      if (sandMatrix[lastFullRow][x]) {
        isRowEmpty = false;
        break;
      }
    }

    if (isRowEmpty) {
      topSandLevel--;
    }
  }
}

// Randomly remove a drop of sand from the bottommost full row (when flipped)
void dropSandFromBottom() {
  if (bottomSandLevel <= 0) return;

  int firstFullRow = -1;
  for (int y = MATRIX_HEIGHT - 1; y >= MATRIX_HEIGHT / 2; y--) {
    bool isRowFull = true;
    for (int x = 0; x < MATRIX_WIDTH; x++) {
      if (!sandMatrix[y][x]) {
        isRowFull = false;
        break;
      }
    }
    if (isRowFull) {
      firstFullRow = y;
    } else {
      break;
    }
  }

  if (firstFullRow >= 0) {
    int column = random(MATRIX_WIDTH);
    sandMatrix[firstFullRow][column] = false;

    bool isRowEmpty = true;
    for (int x = 0; x < MATRIX_WIDTH; x++) {
      if (sandMatrix[firstFullRow][x]) {
        isRowEmpty = false;
        break;
      }
    }

    if (isRowEmpty) {
      bottomSandLevel--;
    }
  }
}

// Simulate one step of sand falling
void updateSand() {
  if (flipInProgress) return;

  dropCounter++;
  if (dropCounter >= DROP_RATE) {
    if (!isFlipped) {
      dropSandFromTop();
    } else {
      dropSandFromBottom();
    }
    dropCounter = 0;
  }

  if (!isFlipped) {
    for (int y = MATRIX_HEIGHT - 2; y >= 0; y--) {
      for (int x = 0; x < MATRIX_WIDTH; x++) {
        if (sandMatrix[y][x] && y >= topSandLevel) {
          if (!sandMatrix[y + 1][x]) {
            sandMatrix[y + 1][x] = true;
            sandMatrix[y][x] = false;
          } else if (x > 0 && !sandMatrix[y + 1][x - 1]) {
            sandMatrix[y + 1][x - 1] = true;
            sandMatrix[y][x] = false;
          } else if (x < MATRIX_WIDTH - 1 && !sandMatrix[y + 1][x + 1]) {
            sandMatrix[y + 1][x + 1] = true;
            sandMatrix[y][x] = false;
          }

          if (y >= MATRIX_HEIGHT / 2) {
            bool isBottomRowFull = true;
            int bottomRow = MATRIX_HEIGHT - bottomSandLevel - 1;
            if (bottomRow >= MATRIX_HEIGHT / 2) {
              for (int bx = 0; bx < MATRIX_WIDTH; bx++) {
                if (!sandMatrix[bottomRow][bx]) {
                  isBottomRowFull = false;
                  break;
                }
              }
              if (isBottomRowFull) {
                bottomSandLevel++;
              }
            }
          }
        }
      }
    }
  } else {
    for (int y = 1; y < MATRIX_HEIGHT; y++) {
      for (int x = 0; x < MATRIX_WIDTH; x++) {
        if (sandMatrix[y][x] && y < MATRIX_HEIGHT - bottomSandLevel) {
          if (!sandMatrix[y - 1][x]) {
            sandMatrix[y - 1][x] = true;
            sandMatrix[y][x] = false;
          } else if (x > 0 && !sandMatrix[y - 1][x - 1]) {
            sandMatrix[y - 1][x - 1] = true;
            sandMatrix[y][x] = false;
          } else if (x < MATRIX_WIDTH - 1 && !sandMatrix[y - 1][x + 1]) {
            sandMatrix[y - 1][x + 1] = true;
            sandMatrix[y][x] = false;
          }

          if (y <= MATRIX_HEIGHT / 2) {
            bool isTopRowFull = true;
            int topRow = topSandLevel;
            if (topRow < MATRIX_HEIGHT / 2) {
              for (int tx = 0; tx < MATRIX_WIDTH; tx++) {
                if (!sandMatrix[topRow][tx]) {
                  isTopRowFull = false;
                  break;
                }
              }
              if (isTopRowFull) {
                topSandLevel++;
              }
            }
          }
        }
      }
    }
  }
}

// Check if the hourglass should be flipped
bool shouldFlipHourglass() {
  if (!isFlipped) {
    return (topSandLevel <= 0 && bottomSandLevel >= INITIAL_SAND_HEIGHT);
  } else {
    return (bottomSandLevel <= 0 && topSandLevel >= INITIAL_SAND_HEIGHT);
  }
}

// Perform the hourglass flip
void flipHourglass() {
  flipInProgress = true;

  isFlipped = !isFlipped;

  for (int i = 0; i < 3; i++) {
    fill_solid(leds, NUM_LEDS, SAND_COLOR);
    FastLED.show();
    delay(200);
    fill_solid(leds, NUM_LEDS, BACKGROUND_COLOR);
    FastLED.show();
    delay(200);
  }

  delay(500); // Additional delay before reinitializing

  initHourglass();

  lastFlipTime = millis();
  flipInProgress = false;
}

void setup() {
  randomSeed(analogRead(0));

  FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
  FastLED.setBrightness(BRIGHTNESS);

  fill_solid(leds, NUM_LEDS, BACKGROUND_COLOR);
  FastLED.show();

  isFlipped = false;
  initHourglass();

  delay(1000);
}

void loop() {
  if (shouldFlipHourglass() && millis() - lastFlipTime > 3000) {
    flipHourglass();
  }

  updateSand();
  updateLEDs();
  delay(SAND_FALL_DELAY);
}

If you have any suggestions or improvements to help the animation run continuously, or if you have tips for integrating the MPU6050 later, I'd love to hear them!

Thanks in advance! :folded_hands:

Why? What makes you think the power from the ESP32 is more stable than the Arduino Uno's power?

How is the matrix powered? Those things can suck a lot of juice.

1 Like

Welcome to the forum

The ESP32 board is not designed to be used as a power supply. As a matter of interest, how is the ESP32 powered ?

1 Like

Thank you so much for your replies – it really helps me a lot! :folded_hands:
I'm just a beginner student and still learning the basics, so your explanations are very valuable to me.

This project is part of my academic course, and I must use the ESP32 because it's part of the required hardware that was provided to us. I understand that it's not ideal to use the ESP32 only as a power source, but for now I'm using it this way just to supply stable 5V to the WS2812B matrix (since I already have the board and it's convenient).

If there’s a better or safer way to power the matrix without using the ESP32, I’d be happy to hear suggestions – but due to course constraints, I need to stick with this hardware setup.

Thanks again for the support!

And the answer is ...

So why are you using the Uno when you could program the ESP32 ?

1 Like

It's quite a lot of code to absorb.

This is the first thing that struck me:

The leds array will consume 768 byes of SRAM (dynamic) memory. The sandMatrix array will consume another 256 bytes. That's 50% of the Uno's memory used. If you get close to 80%, wierd, unexpected things start to happen.

I don't think you realy need the sandMatrix array anyway:

Instead of

if (!sandMatrix[y][x]) {

you can say

if (LEDs[XY(y, x)] == BACKGROUND_COLOR) {
1 Like

Sorry, I wasn’t clear enough.
I have the ESP32 connected to my computer via a regular USB cable.
I'm uploading code to it using the Arduino IDE, where I select the appropriate board type ("ESP32 Dev Module") and the correct COM port that matches the ESP32.

In my project, I’m not using the Arduino Uno to program the ESP32 or control anything. I mentioned it earlier only because I also have one, and I used it at some point just to help understand how things work.

So to clarify:

The ESP32 is connected directly to my PC via USB.

It gets power from the USB port (5V).

I upload code to it using the Arduino IDE.

I hope this makes it clearer! :blush:

Much clearer than your original post

Where exactly is the LED matrix getting its power from ? Is it from a pin on the ESP32 ?

1 Like

Ah, so this is some meaning of the words "not clear enough" I was previously unaware of! (HHGTG)

Please tell the forum now if anything else you have said is "not clear enough".

1 Like

Not stable enough, unless you consider "it's red hot and there's smoke coming out of it" to be stable.

You need an external 5V power supply rated for about 10~15 Amps for that matrix. Each led can consume 50~60mA and you have over 250 of them. USB can provide only about 0.5A. This power supply can power the ESP board also.

1 Like

Oops :sweat_smile:
I guess I’ve said “UNO” so many times in my head that it sneaked into my post without permission!
To set the record straight: I’m using only the ESP32 in this project – the UNO was just hanging out in my drawer feeling left out. :grinning_face_with_smiling_eyes:

Thanks for catching that – and sorry for the mixed signals! I’ll update the title so it doesn’t mislead anyone else.

Currently, the WS2812B matrix is getting 5V directly from the 5V pin on the ESP32 board, which is in turn powered via USB from my computer. GND is also connected, of course.

So – I’ll look into getting a proper external 5V power supply (10–15A) as recommended. I’ll also make sure it powers both the matrix and the ESP32 safely.

Seriously, I really appreciate the advice – I’m learning a ton here! :folded_hands:

How exactly do you want the animation to look? Printing out the contents of sandMatrix looks like a rectangular section of LEDs at the top, with a single LED being removed from each row until it reaches the top, at which time the animation stalls. Does not look at all like an hourglass shape. No dropping effect, no LEDS turning on at the bottom.

1 Like

Thank you for the feedback! You're absolutely right - the current code doesn't create a proper hourglass effect.
What I'm trying to achieve:

Start with several full rows of "sand" (LEDs) at the top of the matrix
Individual sand particles should "drop" from the bottom edge of the filled area and fall down pixel by pixel, creating a realistic falling sand effect
As particles fall and reach the bottom, they should accumulate there, gradually building up full rows from the bottom
The top section should gradually empty out as particles drop, while the bottom section fills up
When the top is completely empty and bottom is full, the whole thing should flip/reset and start over

The issues with my current code:
No proper particle physics - sand doesn't fall naturally
No accumulation effect at the bottom
No visual "dropping" animation
The hourglass shape/behavior isn't working correctly

I need help with Implementing proper falling particle simulation where individual pixels fall from the filled area
Making particles accumulate naturally at the bottom
Creating the visual effect of sand flowing from a solid block above into an empty space below
Ensuring particles respect boundaries and stack properly when they hit the bottom or other particles

Would anyone be able to help me fix the particle physics and falling animation?

Yes, the forum will help. But we look at the project holistically: the circuit, the components, the power supplies etc. as well as the code. No point spending a lot of effort on amazing code that models particle physics if the whole circuit dies within hours or minutes because something important hasn't been considered.

If you want to model an hourglass shape, I guess you will lose about half the pixels in the two centre 8x8 squares of pixels, leaving about 192 out of 256 featuring in the animation. The other ~64 pixels will remain black. That's not a lot of pixels to model physics with!

Let's imagine that the particles emerge from the constriction at one per second.

The first particle drops vertically down, accelerating at some rate, and hits the base at some velocity. Should it bounce? If so, because sand particles have irregular shapes, it will bounce off in a random direction and velocity, having lost some kinetic energy in the collision. Gravity will slow down and reverse the vertical component of the velocity, causing it to fall and bounce a few times before settling.

Then the next sand particles drop and could hit the base or another sand particle, bounce off and so on.

How good this will look on a pixel matrix of only 8x32, I don't know.

The other aspect to model is what is happening in the upper part of the hourglass. As each particle drops through the constriction, the other particles move around to take up the spaces. A conical dip or hole would probably form.

1 Like

@rik-sys the following links answer pretty much everything you need to know about neopixels and the second link gives an example of falling "particles" with code.

1 Like

Wow – thank you all so much for the truly outstanding help and feedback! :folded_hands:
Thanks so much for all the explanations and info you’ve shared – it really helped me see things more clearly, both on the hardware and animation sides.
I'll rethink the hourglass shape and animation strategy to work within the pixel limitations, and simplify things where needed. I’ll also look into proper power supply options and make sure everything is safe and solid before continuing.

Thanks again for the warm support and clear insights – this forum is amazing :yellow_heart: