Hi everyone
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!