멀티 디스플레이 LCD 알람 시계: 커스터마이징 가능한 10가지 시계 페이스
부품 및 소모품
아두이노 우노
×1
포함된 PCB 사용 시 ATmega328 DIP 마이크로프로세서, 16MHz 크리스탈, 2×22pF 커패시터, 7805 레귤레이터, 100µF 16V 커패시터, 3×0.1µF 커패시터, 1µF 커패시터 필요
10k옴 저항
×4
470옴 관통형 저항
×1
Adafruit 표준 LCD - 16x2 흰색/파란색
×1
부저
×1
10µF 커패시터
×1
실시간 시계 (RTC)
×1
DS1302 RTC
32.768kHz 크리스탈
×1
코인 셀 배터리 CR2032
×1
홀더 포함. PCB 사용 시 CR1220 배터리와 홀더 사용
틸트 스위치 (캡슐형)
×1
수은 스위치
트리머 포텐셔미터 10k옴
×1
필요한 도구 및 기계
앱 및 온라인 서비스
프로젝트 개요
10가지 시계 페이스 버전은 여기 에서 확인 가능.
만들 거리를 찾다가 LCD 1602 시계를 만들기로 했습니다. 웹을 둘러보며 다양한 구현을 보고, 왜 이 모든 걸 하나로 합친 시계를 만들지 않을까 생각했습니다.
시계 기능
8가지 다른 디스플레이 스타일 (최신 버전 10가지)
시간, 현재 날짜, 생일, 알람 설정
알람 끄기용 수은 스위치 (틸트 스위치)
백라이트 제어
DHT21 온습도 센서 지원
브레드보드 버전
실제 전자회로는 꽤 단순합니다.
배선 완료 후 스케치를 아두이노 IDE에 불러와 아두이노 우노에 업로드. (표시 안 됨: DHT21은 D9에 연결)
시계 사용법
시계에는 SETUP, INCREMENT, DECREMENT 세 버튼과 틸트 스위치가 있습니다.
백라이트가 꺼져 있을 때 아무 버튼이나 누르면 백라이트 켜짐. 백라이트 켜진 상태에서 버튼을 누르지 않으면 5초(최신 버전 10초) 후 자동 꺼짐. 백라이트 켜진 상태에서 버튼 기능:
SETUP – 설정 화면 진입. 오른쪽 각괄호 문자(>)가 커서. INCREMENT/DECREMENT 버튼으로 커서 위치 값 증감. SETUP 다시 누르면 시 → 분 → 일 → 월 → 년 → 생일 일 → 생일 월 → 생일 년 → 알람 시 → 알람 분 → 시계 모드 순으로 커서 이동.
INCREMENT – 설정 화면이 아닐 때 시계 스타일 전환.
DECREMENT – 설정 화면이 아닐 때 알람 켜기/끄기 토글.
틸트 스위치 – 알람 울릴 때 시계 기울이거나 아무 버튼 누르면 알람 끄기.
완성된 시계 제작
브레드보드에서 완성품으로 가려면 인쇄 회로 기판(PCB)과 추가 부품 필요. 이글 파일 첨부되어 있으니 상업 제작하거나 직접 만드세요. 저는 토너 전사법 사용.
주의: LCD 1602 디스플레이가 메인 PCB에 직각 핀 헤더로 연결되어 있어, 이미 납땜된 상태로는 케이스에 넣기 매우 어려울 수 있습니다. 양면 스루홀 도금 보드면 디스플레이를 제자리에서 납땜 가능.
작업장 부품으로 아두이노 우노를 ATmega328 DIP 칩, 16MHz 크리스탈, 2×22pF 세라믹 커패시터로 대체. 5V 레귤레이터는 7805 TO-220 타입, 100µF 16V 커패시터로 평활. RTC는 DS1302에 32.768kHz 워치 크리스탈. 스피커는 패시브 부저, 10µF 16V 커패시터로 DC 절연. 0.1µF와 1µF 커패시터는 모놀리식 세라믹 (5mm 홀 간격). 저항은 5% 1/8와트 (1/4와트도 가능). 수은 스위치는 아무 크기나 가능 (제건 5mm 직경, 더 작은 것도 됨). 보드 뒷면 장착 3개 택타일 버튼은 6×6mm에 13mm 샤프트.
케이스는 0.2mm 레이어 높이, 서포트 없이 3D 프린팅. PCB 마운트 홀 2.5mm 드릴로 뚫고 3mm 탭으로 나사산 생성. M3 6mm 나사로 보드 고정. PCB 마운트 홀도 4mm로 뚫어 보드 고정 시 버튼이 케이스에 걸리지 않게 조정 가능.
크레딧
이 시계는 여러 메이커들이 수년간 만든 다양한 시계들의 매시업입니다.
베이스는 Arduino Digital Clock With Alarm Function (custom PCB) . 케이스를 4조각이 아닌 2조각으로 프린트되게 수정.
표준 화면 디자인: Michalis Vasilakis
듀얼 두꺼운 폰트: Arduino World
듀얼 베벨 폰트: Arduino Forum
듀얼 트렉 폰트: Carrie Sundra
듀얼 얇은 폰트: Arduino World
워드 컨셉: LAGSILVA
바이오리듬 시계: John Bradnam
날씨 시계: ARDUinoautoMOTIVE
업데이트 내역
2020/06/29
- 워드 시계 철자 오류 수정
- 백라이트 제어용 #define 추가
- 백라이트 타임아웃 5초 → 10초 증가
2020/11/23
- 생일 설정 및 EEPROM 저장 추가
- 바이오리듬 시계 페이스 추가
- 설정 화면 코드 정리
생일을 기준으로 바이오리듬이 인생의 고점과 저점을 결정한다는 가설. 바이오리듬은 23일 신체 주기, 28일 감정 주기, 33일 지성 주기 세 가지로 구성. 바이오리듬 시계는 각 상태를 바로 그래프로 표시.
바가 바이오리듬이 양의 주기(윗쪽 바)인지 음의 주기(아랫쪽 바)인지 표시. 바 길이가 주기 내 얼마나 양/음인지 나타냄.
2020/12/06
- DHT21 지원 추가
- 온도계/습도계 시계 페이스 추가
새로운 날씨 시계 페이스 추가. 케이스 뒷면에 부착된 DHT21 온습도 센서 읽기. PCB에 DHT21 센서용 3핀 커넥터 추가.
코드
DigitalClockAlarmV7.ino
/* 1602 LCD 알람 시계
* by John Bradnam (jbrad2089@gmail.com)
*
* 디스플레이 16x2: 설정: 알람 설정
* +----------------+ +----------------+ +----------------+
* |HH:MM:SS | HH:MM| | >HH :>MM | | Set Alarm |
* |DD/MM/YY | ALARM| | >DD />MM />YYYY | | >HH :>MM |
* +----------------+ +----------------+ +----------------+
*
* 2020/06/25
* - Michalis Vasilakis의 시계를 코드 베이스 (https://www.instructables.com/id/Arduino-Digital-Clock-With-Alarm-Function-custom-P/)
* - 하드웨어 맞춤 수정 - DS1302 RTC 및 LCD 백라이트
* - 다양한 디스플레이 스타일 지원 추가
* - 표준 화면 디자인: Michalis Vasilakis
* - 듀얼 두꺼운 폰트: Arduino World (https://www.hackster.io/thearduinoworld/arduino-digital-clock-version-1-b1a328)
* - 듀얼 베벨 폰트: Arduino Forum (https://forum.arduino.cc/index.php/topic,8882.0.html)
* - 듀얼 트렉 폰트: Carrie Sundra (https://www.alpenglowindustries.com/blog/the-big-numbers-go-marching-2x2)
* - 듀얼 얇은 폰트: Arduino World (https://www.hackster.io/thearduinoworld/arduino-digital-clock-version-2-5bab65)
* - 워드 컨셉: LAGSILVA (https://www.hackster.io/lagsilva/text-clock-bilingual-en-pt-with-arduino-881a6e)
* 2020/06/29
* - 워드 시계 철자 오류 수정
* - 백라이트 제어용 #define 추가
* - 백라이트 타임아웃 5→10초 증가
* 2020/11/22
* - 생일 설정 및 EEPROM 저장 추가
* - 바이오리듬 시계 페이스 추가
* - 설정 화면 코드 정리
* 2021/xx/xx
* - DHT21 지원 추가
* - 온도계/습도계 시계 페이스 추가 */
// 라이브러리
#include <Wire.h>
#include <TimeLib.h>
#include <DS1302RTC.h>
#include <LiquidCrystal.h>
#include <EEPROM.h>
#include <dht.h>
// 듀얼 두꺼운/얇은 디스플레이에서 12시간 형식 표시하려면 주석 해제
//#define DUAL_THICK_12HR
//#define DUAL_THIN_12HR
// 백라이트 제어용 주석 해제
//#define NO_BACKLIGHT
//#define BACKLIGHT_ALWAYS_ON
#define BACKLIGHT_TIMEOUT 10000
// 바이오리듬 그래프 테스트용 주석 해제
//#define TEST_BIO_GRAPHS
#define LIGHT 2 // PD2
#define LCD_D7 3 // PD3
#define LCD_D6 4 // PD4
#define LCD_D5 5 // PD5
#define LCD_D4 6 // PD6
#define LCD_E 7 // PD7
#define LCD_RS 8 // PB0
#define BTN_SET A0 // PC0
#define BTN_ADJUST A1 // PC1
#define BTN_ALARM A2 // PC2
#define BTN_TILT A3 // PC3
#define SPEAKER 11 // PB3
#define DHT21 9 // PB1
#define RTC_CE 10 // PB2
#define RTC_IO 12 // PB4
#define RTC_SCLK 13 // PB5
// 연결 및 상수
LiquidCrystal lcd(LCD_RS, LCD_E, LCD_D4, LCD_D5, LCD_D6, LCD_D7);
DS1302RTC rtc(RTC_CE, RTC_IO, RTC_SCLK);
dht DHT;
char daysOfTheWeek[7][12] = {"Sunday","Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
const int monthDays[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
long interval = 300;
int melody[] = { 600, 800, 1000, 1200 };
// 변수
int DD, MM, YY, H, M, S, temp, hum, set_state, adjust_state, alarm_state, AH, AM, shake_state, BY, BM, BD;
int shakeTimes = 0;
int i = 0;
String sDD, sMM, sYY, sH, sM, sS, sBD, sBM, sBY, aH="12", aM="00", sTMP, sHUM;
long prevAlarmMillis = 0;
long prevDhtMillis = 0;
// 불린 플래그
boolean setupScreen = false;
boolean alarmON = false;
boolean turnItOn = false;
enum STYLE { STANDARD, DUAL_THICK, DUAL_BEVEL, DUAL_TREK, DUAL_THIN, WORD, BIO, THERMO };
STYLE currentStyle = STANDARD;
enum SETUP { CLOCK, TIME_HOUR, TIME_MIN, TIME_DAY, TIME_MONTH, TIME_YEAR, BIRTH_DAY, BIRTH_MONTH, BIRTH_YEAR, ALARM_HOUR, ALARM_MIN };
SETUP setupMode = CLOCK;
bool backlightOn = false;
long backlightTimeout = 0;
byte customChar[8];
//--------------------- EEPROM ------------------------------------------
#define EEPROM_AH 0 // 알람 시
#define EEPROM_AM 1 // 알람 분
#define EEPROM_AO 2 // 알람 켬/끔
#define EEPROM_CS 3 // 현재 스타일
#define EEPROM_BY 4 // 생일 년 (2바이트)
#define EEPROM_BM 6 // 생일 월
#define EEPROM_BD 7 // 생일 일
//--------------------- 워드 시계 --------------------------------------
String units[] = {"HUNDRED", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE"};
String teens[] = {"TEN", "ELEVEN", "TWELVE", "THIRTEEN", "FOURTEEN", "FIFTEEN", "SIXTEEN", "SEVENTEEN", "EIGHTEEN", "NINETEEN"};
String tens[] = {"", "", "TWENTY", "THIRTY", "FORTY", "FIFTY"};
//---------------------- 모래시계 애니메이션 ----------------------------
#define HOURGLASS_FRAMES 8
#define HOURGLASS_CHAR 0
#define FRAME_TIMEOUT 200
int nextFrame = 0;
long frameTimeout = 0;
const byte hourglass[HOURGLASS_FRAMES][8] PROGMEM = {
{ B11111, B11111, B01010, B01010, B01010, B01010, B10001, B11111 },
{ B11111, B11011, B01110, B01010, B01010, B01010, B10001, B11111 },
{ B11111, B10001, B01110, B01110, B01010, B01010, B10001, B11111 },
{ B11111, B10001, B01010, B01110, B01110, B01010, B10001, B11111 },
{ B11111, B10001, B01010, B01010, B01110, B01110, B10001, B11111 },
{ B11111, B10001, B01010, B01010, B01010, B01110, B10101, B11111 },
{ B11111, B10001, B01010, B01010, B01010, B01110, B11011, B11111 },
{ B11111, B10001, B01010, B01010, B01010, B01010, B11111, B11111 }
};
//---------------------- 알람, 시계, DHT 커스텀 문자 ----------------------------
#define BELL_CHAR 1
#define CLOCK_CHAR 2
#define THERMOMETER_CHAR 3
#define DROPLET_CHAR 4
const byte bell[8] PROGMEM = {0x4, 0xe, 0xe, 0xe, 0x1f, 0x0, 0x4};
const byte clock[8] PROGMEM = {0x0, 0xe, 0x15, 0x17, 0x11, 0xe, 0x0};
const byte thermometer[8] PROGMEM = {0x4, 0xa, 0xa, 0xe, 0xe, 0x1f, 0x1f, 0xe};
const byte droplet[8] PROGMEM = {0x4, 0x4, 0xa, 0xa, 0x11, 0x11, 0x11, 0xe};
#define DHT_UPDATE_INTERVAL 6000
//---------------------- 바이오리듬 시계 ----------------------------
// 커스텀 문자 상수 (M은 MSB/윗바, L은 LSB/아랫바)
#define PHYSICAL_M 3
#define PHYSICAL_L 4
#define EMOTIONAL_M 5
#define EMOTIONAL_L 6
#define INTELLECTUAL_M 7
#define INTELLECTUAL_L 8
//---------------------- 두꺼운 정사각 폰트 ----------------------------
#define C0 3
#define C1 4
#define C2 5
#define C3 6
const byte C_0[8] PROGMEM = {0x1F,0x1F,0x1F,0x00,0x00,0x00,0x00,0x00};
const byte C_1[8] PROGMEM = {0x1F,0x1F,0x1F,0x00,0x00,0x1F,0x1F,0x1F};
const byte C_2[8] PROGMEM = {0x00,0x00,0x00,0x00,0x00,0x1F,0x1F,0x1F};
const byte C_3[8] PROGMEM = {0x00,0x00,0x0E,0x0A,0x0A,0x0E,0x00,0x00};
const byte blockChar[11][2][3] = {
{{ 255, C0, 255}, {255, C2, 255}}, // 0
{{ C0, 255, 32}, {C2, 255, C2}}, // 1
{{ C0, C0, 255}, {255, C1, C2}}, // 2
{{ C1, C1, 255}, {C1, C1, 255}}, // 3
{{ 255, C2, 255}, {32, 32, 255}}, // 4
{{ 255, C1, C1}, {C2, C2, 255}}, // 5
{{ 255, C0, C0}, {255, C1, 255}}, // 6
{{ C0, C1, 255}, {32, C0, 255}}, // 7
{{ 255, C1, 255}, {255, C1, 255}}, // 8
{{ 255, C1, 255}, {C2, C2, 255}}, // 9
{{ 32, 32, 32}, {32, 32, 32}} // 빈칸
};
//---------------------- 두꺼운 베벨 폰트 ----------------------------
#define LT 0
#define UB 1
#define RT 2
#define LL 3
#define LB 4
#define LR 5
#define UMB 6
#define LMB 7
const byte _LT[8] PROGMEM = { B00111, B01111, B11111, B11111, B11111, B11111, B11111, B11111};
const byte _UB[8] PROGMEM = { B11111, B11111, B11111, B00000, B00000, B00000, B00000, B00000};
const byte _RT[8] PROGMEM = { B11100, B11110, B11111, B11111, B11111, B11111, B11111, B11111};
const byte _LL[8] PROGMEM = { B11111, B11111, B11111, B11111, B11111, B11111, B01111, B00111};
const byte _LB[8] PROGMEM = { B00000, B00000, B00000, B00000, B00000, B11111, B11111, B11111};
const byte _LR[8] PROGMEM = { B11111, B11111, B11111, B11111, B11111, B11111, B11110, B11100};
const byte _UMB[8] PROGMEM = { B11111, B11111, B11111, B00000, B00000, B00000, B11111, B11111};
const byte _LMB[8] PROGMEM = { B11111, B11111, B11111, B11111, B11111, B11111, B11111, B11111};
const byte bevelChar[11][2][3] = {
{{LT, UB, RT}, {LL, LB, LR}}, // 0
{{UB, RT, 32}, {LB, LMB, LB}}, // 1
{{UMB, UMB, RT}, {LL, LB, LB}}, // 2
{{UMB, UMB, RT}, {LB, LB, LR}}, // 3
{{LL, LB, LMB}, {32, 32, LMB}}, // 4
{{LT, UMB, UMB}, {LB, LB, LR}}, // 5
{{LT, UMB, UMB}, {LL, LB, LR}}, // 6
{{UB, UB, RT}, {32, 32, LT}}, // 7
{{LT, UMB, RT}, {LL, LB, LR}}, // 8
{{LT, UMB, RT}, {32, 32, LR}}, // 9
{{ 32, 32, 32}, {32, 32, 32}} // 빈칸
};
//---------------------- 트렉 폰트 ----------------------------
#define K0 0
#define K1 1
#define K2 2
#define K3 3
#define K4 4
#define K5 5
#define K6 6
#define K7 7
const byte K_0[8] PROGMEM = {0x1F,0x1F,0x00,0x00,0x00,0x00,0x00,0x00};
const byte K_1[8] PROGMEM = {0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18};
const byte K_2[8] PROGMEM = {0x00,0x00,0x00,0x00,0x00,0x00,0x1F,0x1F};
const byte K_3[8] PROGMEM = {0x1F,0x1F,0x03,0x03,0x03,0x03,0x1F,0x1F};
const byte K_4[8] PROGMEM = {0x1F,0x1F,0x18,0x18,0x18,0x18,0x1F,0x1F};
const byte K_5[8] PROGMEM = {0x1F,0x1F,0x18,0x18,0x18,0x18,0x18,0x18};
const byte K_6[8] PROGMEM = {0x03,0x03,0x03,0x03,0x03,0x03,0x1F,0x1F};
const byte K_7[8] PROGMEM = {0x1F,0x1F,0x03,0x03,0x03,0x03,0x03,0x03};
const byte trekChar[11][2][2] = {
{{ K5, K7}, {255, K6}}, // 0
{{ K0, K1}, {K2, 255}}, // 1
{{ K0, K3}, {255, K2}}, // 2
{{ K0, K3}, {K2, 255}}, // 3
{{ K1, 255}, {K0, K1}}, // 4
{{ K4, K0}, {K2, 255}}, // 5
{{ K5, K0}, {K4, 255}}, // 6
{{ K0, 255}, {32, K1}}, // 7
{{ 255, K3}, {K4, 255}}, // 8
{{ 255, K3}, {K2, K6}}, // 9
{{ 32, 32}, {32, 32}} // 빈칸
};
//---------------------- 얇은 폰트 ----------------------------
#define T0 0
#define T1 1
#define T2 2
#define T3 3
#define T4 4
#define T5 5
#define T6 6
#define T7 7
const byte T_0[8] PROGMEM = {0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02};
const byte T_1[8] PROGMEM = {0x0E,0x02,0x02,0x02,0x02,0x02,0x02,0x0E};
const byte T_2[8] PROGMEM = {0x0E,0x08,0x08,0x08,0x08,0x08,0x08,0x0E};
const byte T_3[8] PROGMEM = {0x0E,0x0A,0x0A,0x0A,0x0A,0x0A,0x0A,0x0E};
const byte T_5[8] PROGMEM = {0x0A,0x0A,0x0A,0x0A,0x0A,0x0A,0x0A,0x0E};
const byte T_4[8] PROGMEM = {0x0E,0x0A,0x0A,0x0A,0x0A,0x0A,0x0A,0x0A};
const byte T_6[8] PROGMEM = {0x0E,0x02,0x02,0x02,0x02,0x02,0x02,0x02};
const byte T_7[8] PROGMEM = {0x18,0x18,0x18,0x18,0x18,0x1E,0x1F,0x1F};
const byte thinChar[11][2] = {
{T4, T5}, // 0
{T0, T0}, // 1
{T1, T2}, // 2
{T1, T1}, // 3
{T5, T6}, // 4
{T2, T1}, // 5
{T2, T3}, // 6
{T6, T0}, // 7
{T3, T3}, // 8
{T3, T1}, // 9
{32, 32} // 빈칸
};
//---------------------- 일반 초기화 ----------------------------
void setup() {
Serial.begin(115200);
// 입출력 설정
pinMode(BTN_SET, INPUT);
pinMode(BTN_ADJUST, INPUT);
pinMode(BTN_ALARM, INPUT);
pinMode(BTN_TILT, INPUT);
pinMode(SPEAKER, OUTPUT);
pinMode(LIGHT, OUTPUT);
// RTC에 유효한 시간/날짜 있는지 확인, 없으면 2018-01-01 00:00:00으로 설정
// 최초 실행이나 코인 배터리 방전 시만 실행
// setSyncProvider()로 Time 라이브러리가 5분마다 RTC.get() 호출하여 외부 RTC와 동기화
setSyncProvider(rtc.get);
if (timeStatus() != timeSet) {
Serial.println("기본 시간 설정");
tmElements_t tm;
tm.Year = CalendarYrToTm(2020);
tm.Month = 6;
tm.Day = 26;
tm.Hour = 7;
tm.Minute = 52;
tm.Second = 0;
time_t t = makeTime(tm);
if (rtc.set(t) == 0) {
setTime(t);
} else {
Serial.println("RTC 설정 실패!");
}
}
delay(100);
// EEPROM에서 알람 시간 읽기
AH = EEPROM.read(EEPROM_AH);
AM = EEPROM.read(EEPROM_AM);
byte ao = EEPROM.read(EEPROM_AO);
alarmON = (ao != 0);
byte cs = EEPROM.read(EEPROM_CS);
// 읽은 숫자 유효성 검사 (시:0-23, 분:0-59)
if (AH > 23) AH = 0;
if (AM > 59) AM = 0;
// EEPROM에서 생일 읽기
BY = (EEPROM.read(EEPROM_BY + 0) << 8) | EEPROM.read(EEPROM_BY + 1);
if (BY < 1900 || BY > 2099) BY = 2000;
BM = EEPROM.read(EEPROM_BM);
if (BM < 0 || BM > 12) BM = 1;
BD = EEPROM.read(EEPROM_BD);
if (BD < 0 || BD > 31) BD = 1;
// 현재 스타일 설정
lcd.begin(16,2);
currentStyle = (cs > (uint8_t)THERMO) ? STANDARD : (STYLE)cs;
switch (currentStyle) {
case STANDARD: lcdStandardSetup(); break;
case DUAL_THICK: lcdDualThickSetup(); break;
case DUAL_BEVEL: lcdDualBevelSetup(); break;
case DUAL_TREK: lcdDualTrekSetup(); break;
case DUAL_THIN: lcdDualThinSetup(); break;
case WORD: lcdWordSetup(); break;
case BIO: lcdBioRhythmSetup(); break;
case THERMO: lcdThermometerSetup(); break;
}
#ifdef BACKLIGHT_ALWAYS_ON
switchBacklight(true);
#endif
}
//---------------------- 메인 프로그램 루프 ----------------------------
void loop() {
readBtns(); // 버튼 읽기
getTimeDate(); // RTC에서 시간/날짜 읽기
getTempHum(); // 온습도 읽기
if (!setupScreen) {
lcdPrint(); // 정상적으로 현재 시간/날짜/알람 LCD 출력
if (alarmON) {
callAlarm(); // 알람 설정 시 알람 체크
if (turnItOn) switchBacklight(true);
}
#ifdef BACKLIGHT_TIMEOUT
if (backlightOn && (millis() > backlightTimeout)) {
switchBacklight(false);
}
#endif
} else {
timeSetup(); // SET 버튼 눌리면 시간 설정 함수 호출
switchBacklight(true);
}
}
//--------------------------------------------------
// 버튼 상태 읽기
void readBtns() {
set_state = digitalRead(BTN_SET);
adjust_state = digitalRead(BTN_ADJUST);
alarm_state = digitalRead(BTN_ALARM);
if (!backlightOn && !setupScreen) {
if (set_state == LOW || adjust_state == LOW || alarm_state == LOW) {
switchBacklight(true);
delay(500); // 최소 0.5초 누르고 있어야 함
}
} else {
if (!setupScreen) {
if (alarm_state == LOW) {
alarmON = !alarmON;
EEPROM.write(EEPROM_AO, alarmON ? 1 : 0);
delay(500);
switchBacklight(true);
} else if (adjust_state == LOW) {
currentStyle = (currentStyle == THERMO) ? STANDARD : (STYLE)((int)currentStyle + 1);
EEPROM.write(EEPROM_CS, (byte)currentStyle);
switch (currentStyle) {
case STANDARD: lcdStandardSetup(); break;
case DUAL_THICK: lcdDualThickSetup(); break;
case DUAL_BEVEL: lcdDualBevelSetup(); break;
case DUAL_TREK: lcdDualTrekSetup(); break;
case DUAL_THIN: lcdDualThinSetup(); break;
case WORD: lcdWordSetup(); break;
case BIO: lcdBioRhythmSetup(); break;
case THERMO: lcdThermometerSetup(); break;
}
lcd.clear();
lcdPrint();
delay(500);
switchBacklight(true);
}
}
if (set_state == LOW) {
setupMode = (setupMode == ALARM_MIN) ? CLOCK : (SETUP)((int)setupMode + 1);
if (setupMode != CLOCK) {
setupScreen = true;
if (setupMode == TIME_HOUR) {
lcd.clear();
lcd.setCursor(0,0);
lcd.print("------SET------");
lcd.setCursor(0,1);
lcd.print("-TIME and DATE-");
delay(2000);
lcd.clear();
}
} else {
lcd.clear();
// RTC 설정
tmElements_t tm;
tm.Year = CalendarYrToTm(YY);
tm.Month = MM;
tm.Day = DD;
tm.Hour = H;
tm.Minute = M;
tm.Second = 0;
time_t t = makeTime(tm);
if (rtc.set(t) == 0) {
setTime(t);
} else {
Serial.println("RTC 설정 실패!");
}
// 알람, 생일 EEPROM 저장
EEPROM.write(EEPROM_AH, AH);
EEPROM.write(EEPROM_AM, AM);
EEPROM.write(EEPROM_BY + 0, BY >> 8);
EEPROM.write(EEPROM_BY + 1, BY & 0xFF);
EEPROM.write(EEPROM_BM, BM);
EEPROM.write(EEPROM_BD, BD);
lcd.print("Saving....");
delay(2000);
lcd.clear();
setupScreen = false;
setupMode = CLOCK;
switchBacklight(true);
}
delay(500);
}
}
}
//--------------------------------------------------
// RTC에서 시간/날짜 읽기
void getTimeDate() {
if (!setupScreen) {
time_t t = now();
DD = day(t);
MM = month(t);
YY = year(t);
H = hour(t);
M = minute(t);
S = second(t);
}
// 문자열 포맷팅
sDD = (DD < 10 ? "0" : "") + String(DD);
sMM = (MM < 10 ? "0" : "") + String(MM);
sYY = String(YY - 2000);
sH = (H < 10 ? "0" : "") + String(H);
sM = (M < 10 ? "0" : "") + String(M);
sS = (S < 10 ? "0" : "") + String(S);
sBD = (BD < 10 ? "0" : "") + String(BD);
sBM = (BM < 10 ? "0" : "") + String(BM);
sBY = String(BY);
aH = (AH < 10 ? "0" : "") + String(AH);
aM = (AM < 10 ? "0" : "") + String(AM);
}
//--------------------------------------------------
// DHT 센서에서 6초마다 온습도 읽기
void getTempHum() {
unsigned long currentMillis = millis();
if (currentMillis - prevDhtMillis >= DHT_UPDATE_INTERVAL) {
int chk = DHT.read21(DHT21);
prevDhtMillis = currentMillis;
hum = min(round(DHT.humidity), 99);
temp = min(round(DHT.temperature), 99);
sTMP = (temp > 9 ? "" : " ") + String(temp);
sHUM = (hum > 9 ? "" : " ") + String(hum);
}
}
//--------------------------------------------------
// 백라이트 켜기/끄기
void switchBacklight(bool on) {
#ifdef NO_BACKLIGHT
digitalWrite(LIGHT, LOW);
backlightOn = true;
#elif defined(BACKLIGHT_ALWAYS_ON)
digitalWrite(LIGHT, HIGH);
backlightOn = true;
#else
digitalWrite(LIGHT, on ? HIGH : LOW);
backlightOn = on;
backlightTimeout = millis() + BACKLIGHT_TIMEOUT;
#endif
}
//--------------------------------------------------
// 디스플레이에 값 출력
void lcdPrint() {
switch (currentStyle) {
case STANDARD: lcdStandardLayout(); break;
case DUAL_THICK: lcdDualThickLayout(); break;
case DUAL_BEVEL: lcdDualBevelLayout(); break;
case DUAL_TREK: lcdDualTrekLayout(); break;
case DUAL_THIN: lcdDualThinLayout(); break;
case WORD: lcdWordLayout(); break;
case BIO: lcdBioRhythmLayout(); break;
case THERMO: lcdThermometerLayout(); break;
}
}
// PROGMEM에서 커스텀 문자 생성
void createCharP(byte slot, byte* p) {
for (int i = 0; i < 8; i++) {
customChar[i] = pgm_read_byte(p++);
}
lcd.createChar(slot, customChar);
}
//------------------------------------------------ 표준 레이아웃 ---------------------------------------------------------------------
void lcdStandardSetup() { }
void lcdStandardLayout() {
String line1 = sH + ":" + sM + ":" + sS + " | " + aH + ":" + aM;
String line2 = sDD + "/" + sMM + "/" + sYY + " | " + ((alarmON && (S & 0x01)) ? "ALARM" : " ");
lcd.setCursor(0, 0);
lcd.print(line1);
lcd.setCursor(0, 1);
lcd.print(line2);
}
//------------------------------------------------ 듀얼 두꺼운 레이아웃 ---------------------------------------------------------------------
void lcdDualThickSetup() {
createCharP(C0, C_0);
createCharP(C1, C_1);
createCharP(C2, C_2);
createCharP(C3, C_3);
createCharP(BELL_CHAR, bell);
}
void lcdDualThickLayout() {
#ifdef DUAL_THICK_12HR
int h = (H >= 12) ? H - 12 : H;
if (h == 0) h = 12;
lcdDualThickPrintNumber(8, M, true);
lcdDualThickPrintNumber(0, h, false);
lcd.setCursor(15, 0); lcd.print(H >= 12 ? "p" : "a");
lcd.setCursor(15, 1); lcd.print("m");
#else
lcdDualThickPrintNumber(8, M, true);
lcdDualThickPrintNumber(0, H, true);
bool alarm = (S & 0x01);
lcdWordShowBell(15, 0, alarm, BELL_CHAR);
lcdWordShowBell(15, 1, !alarm, BELL_CHAR);
#endif
byte c = (S & 1) ? C3 : 32;
lcd.setCursor(7, 0); lcd.write(c);
lcd.setCursor(7, 1); lcd.write(c);
}
void lcdDualThickPrintNumber(int pos, int number, int leadingZero) {
int t = number / 10;
int u = number % 10;
if (t == 0 && !leadingZero) t = 11;
lcdDualThickPrintDigit(pos, t);
lcdDualThickPrintDigit(pos + 4, u);
}
void lcdDualThickPrintDigit(int pos, int number) {
for (int y = 0; y < 2; y++) {
lcd.setCursor(pos, y);
for (int x = 0; x < 3; x++) {
lcd.write(blockChar[number][y][x]);
}
}
}
//------------------------------------------------ 듀얼 베벨 레이아웃 ---------------------------------------------------------------------
void lcdDualBevelSetup() {
createCharP(LT, _LT);
createCharP(UB, _UB);
createCharP(RT, _RT);
createCharP(LL, _LL);
createCharP(LB, _LB);
createCharP(LR, _LR);
createCharP(UMB, _UMB);
createCharP(LMB, _LMB);
}
void lcdDualBevelLayout() {
#ifdef DUAL_THICK_12HR
int h = (H >= 12) ? H - 12 : H;
if (h == 0) h = 12;
lcdDualBevelPrintNumber(8, M, true);
lcdDualBevelPrintNumber(0, h, false);
lcd.setCursor(15, 0); lcd.print(H >= 12 ? "p" : "a");
lcd.setCursor(15, 1); lcd.print("m");
#else
lcdDualBevelPrintNumber(8, M, true);
lcdDualBevelPrintNumber(0, H, true);
bool alarm = (S & 0x01);
lcdWordShowBell(15, 0, alarm, 65);
lcdWordShowBell(15, 1, !alarm, 65);
#endif
byte c = (S & 1) ? 58 : 32;
lcd.setCursor(7, 0); lcd.write(c);
lcd.setCursor(7, 1); lcd.write(c);
}
void lcdDualBevelPrintNumber(int pos, int number, int leadingZero) {
int t = number / 10;
int u = number % 10;
if (t == 0 && !leadingZero) t = 11;
lcdDualBevelPrintDigit(pos, t);
lcdDualBevelPrintDigit(pos + 4, u);
}
void lcdDualBevelPrintDigit(int pos, int number) {
for (int y = 0; y < 2; y++) {
lcd.setCursor(pos, y);
for (int x = 0; x < 3; x++) {
lcd.write(bevelChar[number][y][x]);
}
}
}
//------------------------------------------------ 듀얼 트렉 레이아웃 ---------------------------------------------------------------------
void lcdDualTrekSetup() {
createCharP(K0, K_0);
createCharP(K1, K_1);
createCharP(K2, K_2);
createCharP(K3, K_3);
createCharP(K4, K_4);
createCharP(K5, K_5);
createCharP(K6, K_6);
createCharP(K7, K_7);
}
void lcdDualTrekLayout() {
lcdDualTrekPrintNumber(10, S, true);
lcdDualTrekPrintNumber(5, M, true);
lcdDualTrekPrintNumber(0, H, true);
byte c = (S & 1) ? 165 : 32;
lcd.setCursor(4, 0); lcd.write(c);
lcd.setCursor(4, 1); lcd.write(c);
lcd.setCursor(9, 0); lcd.write(c);
lcd.setCursor(9, 1); lcd.write(c);
bool alarm = (S & 0x01);
lcdWordShowBell(15, 0, alarm, 65);
lcdWordShowBell(15, 1, !alarm, 65);
}
void lcdDualTrekPrintNumber(int pos, int number, int leadingZero) {
int t = number / 10;
int u = number % 10;
if (t == 0 && !leadingZero) t = 11;
lcdDualTrekPrintDigit(pos, t);
lcdDualTrekPrintDigit(pos + 2, u);
}
void lcdDualTrekPrintDigit(int pos, int number) {
for (int y = 0; y < 2; y++) {
lcd.setCursor(pos, y);
for (int x = 0; x < 2; x++) {
lcd.write(trekChar[number][y][x]);
}
}
}
//------------------------------------------------ 듀얼 얇은 레이아웃 ---------------------------------------------------------------------
void lcdDualThinSetup() {
createCharP(T0, T_0);
createCharP(T1, T_1);
createCharP(T2, T_2);
createCharP(T3, T_3);
createCharP(T4, T_4);
createCharP(T5, T_5);
createCharP(T6, T_6);
createCharP(T7, T_7);
}
void lcdDualThinLayout() {
#ifdef DUAL_THIN_12HR
int h = (H >= 12) ? H - 12 : H;
if (h == 0) h = 12;
lcdDualThinPrintNumber(6, S, true);
lcdDualThinPrintNumber(3, M, true);
lcdDualThinPrintNumber(0, h, false);
lcd.setCursor(9, 0); lcd.print(H >= 12 ? "p" : "a");
lcd.setCursor(9, 1); lcd.print("m");
#else
lcdDualThinPrintNumber(6, S, true);
lcdDualThinPrintNumber(3, M, true);
lcdDualThinPrintNumber(0, H, true);
#endif
byte c = (S & 1) ? 165 : 32;
lcd.setCursor(2, 0); lcd.write(c);
lcd.setCursor(2, 1); lcd.write(c);
lcd.setCursor(5, 0); lcd.write(c);
lcd.setCursor(5, 1); lcd.write(c);
String line1 = aH + ":" + aM;
String line2 = (alarmON && (S & 0x01)) ? "ALARM" : " ";
lcd.setCursor(11, 0); lcd.print(line1);
lcd.setCursor(11, 1); lcd.print(line2);
}
void lcdDualThinPrintNumber(int pos, int number, int leadingZero) {
int t = number / 10;
int u = number % 10;
if (t == 0 && !leadingZero) t = 11;
lcdDualThinPrintDigit(pos, t);
lcdDualThinPrintDigit(pos + 1, u);
}
void lcdDualThinPrintDigit(int pos, int number) {
for (int y = 0; y < 2; y++) {
lcd.setCursor(pos, y);
lcd.write(thinChar[number][y]);
}
}
//------------------------------------------------ 워드 레이아웃 ---------------------------------------------------------------------
void lcdWordSetup() {
createCharP(BELL_CHAR, &bell[0]);
}
void lcdWordLayout() {
String line1 = numberToWord(H, false);
String line2 = numberToWord(M, true);
lcd.setCursor(0, 0);
printClear(line1, 13);
lcd.setCursor(0, 1);
printClear(line2, 14);
if (millis() > frameTimeout) {
frameTimeout = millis() + FRAME_TIMEOUT;
createCharP(HOURGLASS_CHAR, &hourglass[nextFrame][0]);
nextFrame = (nextFrame + 1) % HOURGLASS_FRAMES;
lcd.setCursor(13, 0);
lcd.write((int)HOURGLASS_CHAR);
lcd.print(sS);
}
bool alarm = (S & 0x01);
lcdWordShowBell(14, 1, alarm, BELL_CHAR);
lcdWordShowBell(15, 1, !alarm, BELL_CHAR);
}
// 알람 켜져 있으면 종 모양 표시
void lcdWordShowBell(int x, int y, bool show, byte chr) {
lcd.setCursor(x, y);
lcd.print(" ");
if (alarmON && show) {
lcd.setCursor(x, y);
lcd.write(chr);
}
}
// 문자열 출력하고 오른쪽 지우기
void printClear(String s, int maxLen) {
lcd.print(s);
for (int i = s.length(); i < maxLen; i++) lcd.print(" ");
}
// 숫자를 영단어로 변환 (워드 시계용)
String numberToWord(int num, bool isMinute) {
// 구현부 생략 (원본 코드 참조)
return "";
}
// ... 이하 바이오리듬, 온도계 레이아웃 등 나머지 함수들 생략 ...
// 전체 코드는 첨부 파일 다운로드 참조
커스텀 파트 및 인클로저
stl_files_ZuDXHCHZCl.zip
회로도
이글 파일 포함: eagle_files_ZN59zdeNf5.zip