이 디지털 모래시계는 ESP32-C3 마이크로컨트롤러, MAX7219 8x8 LED 매트릭스 2개, TP4056 배터리 충전 모듈, 그리고 소형 500mAh 리튬 폴리머 배터리를 3D 프린팅 케이스에 담아 제작했습니다.
버튼을 눌러 1분, 3분, 5분, 또는 10분 중에서 타이머 시간을 선택할 수 있으며, 타이머가 종료되면 큰 소리의 부저가 울립니다.
💡 특징:
조절 가능한 타이머 (1 / 3 / 5 / 10분)
선명한 LED 모래시계 애니메이션
타이머 만료 시 큰 소리의 알림음
4시간 이상 사용 가능한 배터리
콤팩트한 3D 프린팅 하우징
USB-C 충전 포트
🔧 사용 부품:
ESP32-C3 마이크로컨트롤러
MAX7219 8x8 LED 매트릭스 2개
TP4056 배터리 충전 모듈
500mAh LiPo 배터리 (501240)
소형 스위치 + 푸시 버튼
USB-C 포트
📦 다운로드 내용:
회로도 (배선도)
Arduino 코드 (.ino)
조립 설명서
📜 이용 약관 / 라이선스
개인적인 용도로만 사용 가능하며, 상업적 용도는 금지됩니다.
본 코드, 3D 파일, 기타 프로젝트 데이터를 공유하지 마십시오.
파일을 리믹스하거나 수정하는 것은 허용되지 않습니다.
케이스에 제 로고를 인쇄해야 합니다.

출처는 위 사진 아래에 있어요.
다른 모래시계 참고자료 링크
4. 아두이노 Project Hub - LED Hourglass 자료
참고링크 2번의 소스코드
// Viral Science www.viralsciencecreativity.com www.youtube.com/c/viralscience
// Arduino Matrix 8x8 Display Hour Glass Project
#include "Arduino.h"
#include <MPU6050_tockn.h>
#include "LedControl.h"
#define MATRIX_A 1
#define MATRIX_B 0
MPU6050 mpu6050(Wire);
// Values are 260/330/400
#define ACC_THRESHOLD_LOW -25
#define ACC_THRESHOLD_HIGH 25
// Matrix
#define PIN_DATAIN 5
#define PIN_CLK 4
#define PIN_LOAD 6
// Accelerometer
#define PIN_X mpu6050.getAngleX()
#define PIN_Y mpu6050.getAngleY()
// Rotary Encoder
#define PIN_ENC_1 3
#define PIN_ENC_2 2
#define PIN_ENC_BUTTON 7
#define PIN_BUZZER 14
// This takes into account how the matrixes are mounted
#define ROTATION_OFFSET 90
// in milliseconds
#define DEBOUNCE_THRESHOLD 500
#define DELAY_FRAME 100
#define DEBUG_OUTPUT 1
#define MODE_HOURGLASS 0
#define MODE_SETMINUTES 1
#define MODE_SETHOURS 2
byte delayHours = 0;
byte delayMinutes = 1;
int mode = MODE_HOURGLASS;
int gravity;
LedControl lc = LedControl(PIN_DATAIN, PIN_CLK, PIN_LOAD, 2);
int resetCounter = 0;
bool alarmWentOff = false;
/**
* Get delay between particle drops (in seconds)
*/
long getDelayDrop() {
// since we have exactly 60 particles we don't have to multiply by 60 and then divide by the number of particles again :)
return delayMinutes + delayHours * 60;
}
#if DEBUG_OUTPUT
void printmatrix() {
Serial.println(" 0123-4567 ");
for (int y = 0; y<8; y++) {
if (y == 4) {
Serial.println("|----|----|");
}
Serial.print(y);
for (int x = 0; x<8; x++) {
if (x == 4) {
Serial.print("|");
}
Serial.print(lc.getXY(0,x,y) ? "X" :" ");
}
Serial.println("|");
}
Serial.println("-----------");
}
#endif
coord getDown(int x, int y) {
coord xy;
xy.x = x-1;
xy.y = y+1;
return xy;
}
coord getLeft(int x, int y) {
coord xy;
xy.x = x-1;
xy.y = y;
return xy;
}
coord getRight(int x, int y) {
coord xy;
xy.x = x;
xy.y = y+1;
return xy;
}
bool canGoLeft(int addr, int x, int y) {
if (x == 0) return false; // not available
return !lc.getXY(addr, getLeft(x, y)); // you can go there if this is empty
}
bool canGoRight(int addr, int x, int y) {
if (y == 7) return false; // not available
return !lc.getXY(addr, getRight(x, y)); // you can go there if this is empty
}
bool canGoDown(int addr, int x, int y) {
if (y == 7) return false; // not available
if (x == 0) return false; // not available
if (!canGoLeft(addr, x, y)) return false;
if (!canGoRight(addr, x, y)) return false;
return !lc.getXY(addr, getDown(x, y)); // you can go there if this is empty
}
void goDown(int addr, int x, int y) {
lc.setXY(addr, x, y, false);
lc.setXY(addr, getDown(x,y), true);
}
void goLeft(int addr, int x, int y) {
lc.setXY(addr, x, y, false);
lc.setXY(addr, getLeft(x,y), true);
}
void goRight(int addr, int x, int y) {
lc.setXY(addr, x, y, false);
lc.setXY(addr, getRight(x,y), true);
}
int countParticles(int addr) {
int c = 0;
for (byte y=0; y<8; y++) {
for (byte x=0; x<8; x++) {
if (lc.getXY(addr, x, y)) {
c++;
}
}
}
return c;
}
bool moveParticle(int addr, int x, int y) {
if (!lc.getXY(addr,x,y)) {
return false;
}
bool can_GoLeft = canGoLeft(addr, x, y);
bool can_GoRight = canGoRight(addr, x, y);
if (!can_GoLeft && !can_GoRight) {
return false; // we're stuck
}
bool can_GoDown = canGoDown(addr, x, y);
if (can_GoDown) {
goDown(addr, x, y);
} else if (can_GoLeft&& !can_GoRight) {
goLeft(addr, x, y);
} else if (can_GoRight && !can_GoLeft) {
goRight(addr, x, y);
} else if (random(2) == 1) { // we can go left and right, but not down
goLeft(addr, x, y);
} else {
goRight(addr, x, y);
}
return true;
}
void fill(int addr, int maxcount) {
int n = 8;
byte x,y;
int count = 0;
for (byte slice = 0; slice < 2*n-1; ++slice) {
byte z = slice<n ? 0 : slice-n + 1;
for (byte j = z; j <= slice-z; ++j) {
y = 7-j;
x = (slice-j);
lc.setXY(addr, x, y, (++count <= maxcount));
}
}
}
/**
* Detect orientation using the accelerometer
*
* | up | right | left | down |
* --------------------------------
* 400 | | | y | x |
* 330 | y | x | x | y |
* 260 | x | y | | |
*/
int getGravity() {
int x = mpu6050.getAngleX();
int y = mpu6050.getAngleY();
if (y < ACC_THRESHOLD_LOW) { return 90; }
if (x > ACC_THRESHOLD_HIGH) { return 0; }
if (y > ACC_THRESHOLD_HIGH) { return 270; }
if (x < ACC_THRESHOLD_LOW) { return 180; }
}
int getTopMatrix() {
return (getGravity() == 90) ? MATRIX_A : MATRIX_B;
}
int getBottomMatrix() {
return (getGravity() != 90) ? MATRIX_A : MATRIX_B;
}
void resetTime() {
for (byte i=0; i<2; i++) {
lc.clearDisplay(i);
}
fill(getTopMatrix(), 60);
d.Delay(getDelayDrop() * 1000);
}
/**
* Traverse matrix and check if particles need to be moved
*/
bool updateMatrix() {
int n = 8;
bool somethingMoved = false;
byte x,y;
bool direction;
for (byte slice = 0; slice < 2*n-1; ++slice) {
direction = (random(2) == 1); // randomize if we scan from left to right or from right to left, so the grain doesn't always fall the same direction
byte z = slice<n ? 0 : slice-n + 1;
for (byte j = z; j <= slice-z; ++j) {
y = direction ? (7-j) : (7-(slice-j));
x = direction ? (slice-j) : j;
// for (byte d=0; d<2; d++) { lc.invertXY(0, x, y); delay(50); }
if (moveParticle(MATRIX_B, x, y)) {
somethingMoved = true;
};
if (moveParticle(MATRIX_A, x, y)) {
somethingMoved = true;
}
}
}
return somethingMoved;
}
/**
* Let a particle go from one matrix to the other
*/
boolean dropParticle() {
if (d.Timeout()) {
d.Delay(getDelayDrop() * 1000);
if (gravity == 0 || gravity == 180) {
if ((lc.getRawXY(MATRIX_A, 0, 0) && !lc.getRawXY(MATRIX_B, 7, 7)) ||
(!lc.getRawXY(MATRIX_A, 0, 0) && lc.getRawXY(MATRIX_B, 7, 7))
) {
// for (byte d=0; d<8; d++) { lc.invertXY(0, 0, 7); delay(50); }
lc.invertRawXY(MATRIX_A, 0, 0);
lc.invertRawXY(MATRIX_B, 7, 7);
tone(PIN_BUZZER, 440, 10);
return true;
}
}
}
return false;
}
void alarm() {
for (int i=0; i<5; i++) {
tone(PIN_BUZZER, 440, 200);
delay(1000);
}
}
void resetCheck() {
int z = analogRead(A3);
if (z > ACC_THRESHOLD_HIGH || z < ACC_THRESHOLD_LOW) {
resetCounter++;
Serial.println(resetCounter);
} else {
resetCounter = 0;
}
if (resetCounter > 20) {
Serial.println("RESET!");
resetTime();
resetCounter = 0;
}
}
void displayLetter(char letter, int matrix) {
// Serial.print("Letter: ");
// Serial.println(letter);
lc.clearDisplay(matrix);
lc.setXY(matrix, 1,4, true);
lc.setXY(matrix, 2,3, true);
lc.setXY(matrix, 3,2, true);
lc.setXY(matrix, 4,1, true);
lc.setXY(matrix, 3,6, true);
lc.setXY(matrix, 4,5, true);
lc.setXY(matrix, 5,4, true);
lc.setXY(matrix, 6,3, true);
if (letter == 'M') {
lc.setXY(matrix, 4,2, true);
lc.setXY(matrix, 4,3, true);
lc.setXY(matrix, 5,3, true);
}
if (letter == 'H') {
lc.setXY(matrix, 3,3, true);
lc.setXY(matrix, 4,4, true);
}
}
void renderSetMinutes() {
fill(getTopMatrix(), delayMinutes);
displayLetter('M', getBottomMatrix());
}
void renderSetHours() {
fill(getTopMatrix(), delayHours);
displayLetter('H', getBottomMatrix());
}
void knobClockwise() {
Serial.println("Clockwise");
if (mode == MODE_SETHOURS) {
delayHours = constrain(delayHours+1, 0, 64);
renderSetHours();
} else if(mode == MODE_SETMINUTES) {
delayMinutes = constrain(delayMinutes+1, 0, 64);
renderSetMinutes();
}
Serial.print("Delay: ");
Serial.println(getDelayDrop());
}
void knobCounterClockwise() {
Serial.println("Counterclockwise");
if (mode == MODE_SETHOURS) {
delayHours = constrain(delayHours-1, 0, 64);
renderSetHours();
} else if (mode == MODE_SETMINUTES) {
delayMinutes = constrain(delayMinutes-1, 0, 64);
renderSetMinutes();
}
Serial.print("Delay: ");
Serial.println(getDelayDrop());
}
volatile int lastEncoded = 0;
volatile long encoderValue = 0;
long lastencoderValue = 0;
long lastValue = 0;
void updateEncoder() {
int MSB = digitalRead(PIN_ENC_1); //MSB = most significant bit
int LSB = digitalRead(PIN_ENC_2); //LSB = least significant bit
int encoded = (MSB << 1) |LSB; //converting the 2 pin value to single number
int sum = (lastEncoded << 2) | encoded; //adding it to the previous encoded value
if(sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) encoderValue--;
if(sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) encoderValue++;
// Serial.print("Value: ");
// Serial.println(encoderValue);
if ((encoderValue % 4) == 0) {
int value = encoderValue / 4;
if (value > lastValue) knobClockwise();
if (value < lastValue) knobCounterClockwise();
lastValue = value;
}
lastEncoded = encoded; //store this value for next time
}
/**
* Button callback (incl. software debouncer)
* This switches between the modes (normal, set minutes, set hours)
*/
volatile unsigned long lastButtonPushMillis;
void buttonPush() {
if((long)(millis() - lastButtonPushMillis) >= DEBOUNCE_THRESHOLD) {
mode = (mode+1) % 3;
Serial.print("Switched mode to: ");
Serial.println(mode);
lastButtonPushMillis = millis();
if (mode == MODE_SETMINUTES) {
lc.backup(); // we only need to back when switching from MODE_HOURGLASS->MODE_SETMINUTES
renderSetMinutes();
}
if (mode == MODE_SETHOURS) {
renderSetHours();
}
if (mode == MODE_HOURGLASS) {
lc.clearDisplay(0);
lc.clearDisplay(1);
lc.restore();
resetTime();
}
}
}
/**
* Setup
*/
void setup() {
mpu6050.calcGyroOffsets(true);
// Serial.begin(9600);
mpu6050.begin();
// while (!Serial) {
// ; // wait for serial port to connect. Needed for native USB
// }
// setup rotary encoder
pinMode(PIN_ENC_1, INPUT);
pinMode(PIN_ENC_2, INPUT);
pinMode(PIN_ENC_BUTTON, INPUT);
digitalWrite(PIN_ENC_1, HIGH); //turn pullup resistor on
digitalWrite(PIN_ENC_2, HIGH); //turn pullup resistor on
digitalWrite(PIN_ENC_BUTTON, HIGH); //turn pullup resistor on
attachInterrupt(digitalPinToInterrupt(PIN_ENC_1), updateEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC_2), updateEncoder, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_ENC_BUTTON), buttonPush, RISING);
// Serial.println(digitalPinToInterrupt(PIN_ENC_1));
// Serial.println(digitalPinToInterrupt(PIN_ENC_2));
// Serial.println(digitalPinToInterrupt(PIN_ENC_BUTTON));
randomSeed(analogRead(A0));
// init displays
for (byte i=0; i<2; i++) {
lc.shutdown(i,false);
lc.setIntensity(i,0);
}
resetTime();
}
/**
* Main loop
*/
void loop() {
mpu6050.update();
Serial.println("angleX : ");
Serial.println(mpu6050.getAngleX());
Serial.println("\tangleY : ");
Serial.println(mpu6050.getAngleY());
delay(DELAY_FRAME);
// update the driver's rotation setting. For the rest of the code we pretend "down" is still 0,0 and "up" is 7,7
gravity = getGravity();
lc.setRotation((ROTATION_OFFSET + gravity) % 360);
// handle special modes
if (mode == MODE_SETMINUTES) {
renderSetMinutes(); return;
} else if (mode == MODE_SETHOURS) {
renderSetHours(); return;
}
// resetCheck(); // reset now happens when pushing a button
bool moved = updateMatrix();
bool dropped = dropParticle();
// alarm when everything is in the bottom part
if (!moved && !dropped && !alarmWentOff && (countParticles(getTopMatrix()) == 0)) {
alarmWentOff = true;
alarm();
}
// reset alarm flag next time a particle was dropped
if (dropped) {
alarmWentOff = false;
}
}
'ESP32 Project' 카테고리의 다른 글
| ESP32와 Arduino 코어를 이용한 OLED 애니메이션 구현 (0) | 2026.02.24 |
|---|---|
| 영원한 메이커의 고향, 시계 ESPclock 스마트 시계 (1) | 2026.02.24 |
| ESP32-CAM 보드에서 YOLO를 구현 (0) | 2026.02.04 |
| ESP32-CAM 영상 물체 탐지 기능 구현 웨비나 (1) | 2026.01.28 |
| ESP32의 OTA 업데이트는 쉬워 보이지만 (1) | 2026.01.12 |
| ESP32 C3 Super Mini Pomodoro timer ESP-IDF (1) | 2026.01.06 |
| 와이파이를 동작 감지 센서로 바꾸는 방법 (2) | 2025.12.30 |
| ESP32를 사용한 간단한 GPS 추적기 제작 방법 및 지도 상의 데이터 시각화 (0) | 2025.11.17 |
취업, 창업의 막막함, 외주 관리, 제품 부재!
당신의 고민은 무엇입니까? 현실과 동떨어진 교육, 실패만 반복하는 외주 계약,
아이디어는 있지만 구현할 기술이 없는 막막함.
우리는 알고 있습니다. 문제의 원인은 '명확한 학습, 실전 경험과 신뢰할 수 있는 기술력의 부재'에서 시작됩니다.
이제 고민을 멈추고, 캐어랩을 만나세요!
코딩(펌웨어), 전자부품과 디지털 회로설계, PCB 설계 제작, 고객(시장/수출) 발굴과 마케팅 전략으로 당신을 지원합니다.
제품 설계의 고수는 성공이 만든 게 아니라 실패가 만듭니다. 아이디어를 양산 가능한 제품으로!
귀사의 제품을 만드세요. 교육과 개발 실적으로 신뢰할 수 있는 파트너를 확보하세요.
캐어랩