A sand-hourglass simulator built on two daisy-chained MAX7219 8×8 LED matrices.
Each lit LED is a grain of sand that falls, slides, and settles under real gravity — detected by a GY-521 / MPU6050 I2C accelerometer. Flip the device and the sand flows the other way.
| Feature | Details |
|---|---|
| Sand Physics | Diagonal fall → lateral slide → random drift (no directional bias) |
| Gravity Sensing | GY-521 / MPU6050 accelerometer detects 4 cardinal orientations (0°, 90°, 180°, 270°) |
| Auto-Rotation | Display rotates with the device — sand always falls "downward" |
| Configurable Timer | Set hours + minutes in include/config.h; default is 1 minute |
| Non-Blocking Loop | Custom NonBlockDelay class keeps animation running while timing events |
| Debug Mode | Optional Serial ASCII dump of the matrix state (9600 baud) |
| # | Component | Notes |
|---|---|---|
| 1 | Arduino Nano | ATmega328P (old or new bootloader) |
| 2 | MAX7219 8×8 LED Matrix (×2) | Daisy-chained; common modules available |
| 3 | GY-521 Accelerometer Module | MPU6050 I2C accelerometer; default address 0x68 |
| 4 | Jumper Wires | – |
| 5 | Breadboard / PCB | – |
Arduino Nano MAX7219 (Module 1 — near end)
────────────── ──────────────────────────────
D5 (PIN_DATAIN) ──► DIN (Data In)
D4 (PIN_CLK) ──► CLK (Clock)
D6 (PIN_LOAD) ──► CS/LOAD (Chip Select)
5V ──► VCC
GND ──► GND
MAX7219 Module 1 MAX7219 (Module 2 — far end)
───────────── ────────────────────────────
DOUT ──────► DIN
CLK ) ──────► SCL
5V ──────► VCC
GND ──────► GND
Note: The code uses the default MPU6050 I2C address
0x68, which is used when the GY-521AD0pin is low or unconnected. If you wireAD0high, updateMPU_ADDRininclude/config.hto0x69.
| Arduino Nano Pin | Direction | Connected To | Purpose |
|---|---|---|---|
| D4 | OUT | MAX7219 CLK | SPI clock signal |
| D5 | OUT | MAX7219 DIN | SPI data to LED matrices |
| D6 | OUT | MAX7219 LOAD | SPI chip select |
| A0 | IN | Floating (no connection) | randomSeed() at startup only |
| A4 / SDA | IN/OUT | GY-521 SDA | I2C data |
| A5 / SCL | OUT | GY-521 SCL | I2C clock |
| 5V | OUT | GY-521 VCC | Module power |
| GND | — | GY-521 GND | Common ground |
┌─────────────┐
┌────┤ MAX7219 #1 │
Arduino Nano │ │ (near) │
┌──────────┐ │ └──┬──────────┘
│ D5 ──────┼──DATA──┘ DOUT│
│ D4 ──────┼──CLK───────────┤ CLK
│ D6 ──────┼──CS────────────┤ CS
│ 5V ──────┼────────────────┤ VCC ┌─────────────┐
│ GND──────┼────────────────┤ GND │ MAX7219 #2 │
│ │ DOUT─┼──DIN──►│ (far) │
│ A4 ──────┼──SDA───────────┤ └─────────────┘
│ A5 ──────┼──SCL──┐ (GY-521 / MPU6050)
│ 5V ──────┼──VCC──┘
│ GND──────┼──GND
│ │
└──────────┘
hourglass_instructables/
├── platformio.ini # PlatformIO project configuration (board, framework)
├── include/
│ └── config.h # ⚙️ All user-configurable parameters
├── src/
│ └── main.cpp # Main sketch — physics engine, setup/loop
├── lib/
│ ├── LedControl/
│ │ ├── LedControl.h # MAX7219 driver — API & documentation
│ │ └── LedControl.cpp # MAX7219 driver — implementation
│ └── Delay/
│ ├── Delay.h # Non-blocking delay — API & documentation
│ └── Delay.cpp # Non-blocking delay — implementation
├── backup/ # 📦 Original Arduino IDE flat-file backup
│ ├── hourglass.ino
│ ├── LedControl.h / .cpp
│ └── Delay.h / .cpp
└── README.md
All user-tunable settings are in include/config.h.
No other file needs to be edited for common adjustments.
#define TIMER_HOURS 0 // Add hours to the countdown
#define TIMER_MINUTES 1 // Add minutes to the countdownThe total drain time = (TIMER_HOURS × TIMER_MINUTES_PER_HOUR + TIMER_MINUTES) minutes.
Each of the SAND_PARTICLE_COUNT sand particles is scheduled using TIMER_MILLISECONDS_PER_SECOND.
Examples:
| TIMER_HOURS | TIMER_MINUTES | Total Duration |
|---|---|---|
| 0 | 1 | 1 minute |
| 0 | 5 | 5 minutes |
| 0 | 30 | 30 minutes |
| 1 | 0 | 1 hour |
#define ROTATION_OFFSET 90 // Physical matrix mounting offset (degrees)
#define DELAY_FRAME 100 // Animation frame period (ms)
#define DISPLAY_INTENSITY 1 // MAX7219 LED brightness, 0-15#define MPU_ADDR 0x68 // Default GY-521 / MPU6050 I2C address
#define ACC_THRESHOLD_LOW -10000 // Raw MPU6050 value below this = axis tilted "low"
#define ACC_THRESHOLD_HIGH 10000 // Raw MPU6050 value above this = axis tilted "high"The MPU6050 defaults to its +/-2g accelerometer range, where -1g is roughly -16384 and +1g is roughly +16384. Adjust the thresholds if orientation detection is too sensitive or not sensitive enough for your module and mounting.
#define DEBUG_OUTPUT 1 // 1 = enable Serial ASCII matrix dump, 0 = disableConnect to Serial monitor at SERIAL_BAUD_RATE to see the matrix state printed as ASCII art.
# 1. Open the project folder in VS Code
# 2. PlatformIO will auto-detect platformio.ini and install the toolchain
# Build only (verify compilation)
pio run
# Build and upload to connected Arduino Nano
pio run --target upload
# Open Serial monitor (SERIAL_BAUD_RATE, default 9600 baud)
pio device monitorBootloader note: The
platformio.initargetsnanoatmega328(old bootloader).
If your Nano uses the new bootloader, change the board tonanoatmega328new.
Each loop iteration calls updateMatrix(), which sweeps every cell of the 8×8 grid in a randomised diagonal order and applies the following priority rules to each particle:
1. Can fall diagonally (both L and R cells free)? → fall diagonally
2. Only left is free? → slide left
3. Only right is free? → slide right
4. Both free but diagonal blocked? → drift randomly L or R
5. Both blocked? → particle settled, no move
The randomised sweep direction per stripe prevents particles from always drifting the same way, producing a natural-looking simulation.
getGravity() reads the GY-521 / MPU6050 over I2C. It requests the X and Y acceleration registers in a single transaction, then maps the raw signed values to the nearest cardinal angle:
Y-axis low → 0° (upright)
X-axis high → 90° (rotated right)
Y-axis high → 180° (upside-down)
X-axis low → 270° (rotated left)
The dead-band between ACC_THRESHOLD_LOW and ACC_THRESHOLD_HIGH prevents jitter near boundaries. With the default wiring, the sensor uses address 0x68; wiring AD0 high changes it to 0x69.
dropParticle() fires once per timer period and transfers one grain from the raw corner cell [0,0] of MATRIX_A to raw corner cell [7,7] of MATRIX_B — the physical junction between the two daisy-chained displays.
The LedControl library maintains a rotation state. Every frame, loop() calls:
lc.setRotation((ROTATION_OFFSET + gravity) % GRAVITY_FULL_ROTATION_DEGREES);All subsequent LED coordinate writes are automatically rotated, so the user always sees "down" as down, regardless of which way the device is held.
This version matches the current hardware build and keeps the code easier to tune:
- Removed all passive buzzer, tick, and alarm code because the current hardware does not include a buzzer.
- Removed the blocking end-of-timer alarm path, so the animation loop no longer pauses for alarm beeps.
- Moved repeated hardware, display, timer, gravity, matrix, random, and serial values into
include/config.h. - Cached the gravity reading once per loop and reused that value when choosing the top and bottom matrices.
- Seeded the cached gravity value once during
setup()before the initial hourglass fill. - Cleaned the README and code comments so text is stored and displayed as valid UTF-8.
- Verified the project still builds successfully with
pio run.
The LedControl library is based on the original work by Eberhard Fahle (© 2007) and is distributed under the MIT Licence.
The hourglass application code (main.cpp, Delay.*, config.h) is released under the MIT Licence.
- LedControl library — Eberhard Fahle (original MIT-licensed MAX7219 driver)
- Hourglass concept — Based on the Instructables LED hourglass project
- PlatformIO migration & documentation — This repository
