Maxim Integrated DS3231M - ±5ppm, I2C 실시간 시계 (RTC)
1
MAX7219 드라이버 내장 8×8 LED 매트릭스
1
RGB 확산형 공통 캐소드 LED
1
220옴 저항
2
푸시버튼 내장 로터리 인코더
1
필요 도구 및 장비
납땜 인두 (일반용)
앱 및 온라인 서비스
아두이노 IDE
프로젝트 소개
이 8×8 LED 매트릭스 모듈 시계는 Adrian Jones가 개발한 것을 기반으로 제작했습니다. 시간은 좌우로 스크롤하며 표시되거나, 소프트웨어 제어 하에 상하로 플립되며 표시됩니다. 각 시간 숫자가 오른쪽에서부터 차례로 슬라이드되어 중앙에 도달하면 잠시 멈추고 약간 밝아졌다가 왼쪽으로 스크롤하면서 다음 숫자가 나타나는 방식입니다. 이 주기가 짧은 지연 후 반복됩니다.
상하 플립(vertical flipping) 모드로 변경하려면 코드에서 다음 부분을 수정하세요:
static boolean top2bottom = false;
를
static boolean top2bottom = true;
로 변경하면 됩니다.
코드 주석에 나와 있듯이 스크롤 속도, 문자 간 공백 줄 수, 12/24시간 표시, 문자 간 지연 시간, 밝기 등 다양한 파라미터를 조정할 수 있습니다.
로터리 인코더를 사용해 시간을 쉽게 설정할 수 있습니다. 투컬러 다이오드가 설정 모드(분, 시, 일반)를 표시합니다. 시계를 두 개의 기본 부품으로 구성된 상자에 넣어 내부 접근이 매우 편리하고 실용적입니다.
코드
code (아두이노)
//*****************************************************************************************//
// MATRIX CLOCK
// Adrian Jones, March 2014
//
// - 좌우 스크롤 및 상하 스크롤 지원
//
//*****************************************************************************************//
#include <Wire.h> // I2C-WIRE 라이브러리
#include <RTClib.h> // RTC 라이브러리
// MAX7219 레지스터 및 제어 핀 정의
#define max7219_reg_noop 0x00
#define max7219_reg_digit0 0x01
#define max7219_reg_digit1 0x02
#define max7219_reg_digit2 0x03
#define max7219_reg_digit3 0x04
#define max7219_reg_digit4 0x05
#define max7219_reg_digit5 0x06
#define max7219_reg_digit6 0x07
#define max7219_reg_digit7 0x08
#define max7219_reg_decodeMode 0x09
#define max7219_reg_intensity 0x0a
#define max7219_reg_scanLimit 0x0b
#define max7219_reg_shutdown 0x0c
#define max7219_reg_displayTest 0x0f
#define dataIn 12 // DIN
#define load 10 // CS
#define clock 11 // CLK
char alphanum[][8] = {
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // 공백
{0x10,0x10,0x10,0x10,0x10,0x10,0x00,0x10}, // !
{0x00,0x28,0x28,0x28,0x00,0x00,0x00,0x00}, // "
{0x00,0x28,0x7C,0x28,0x7C,0x28,0x00,0x00}, // #
{0x10,0x38,0x50,0x38,0x14,0x54,0x38,0x10}, // $
{0x41,0xA2,0x44,0x08,0x10,0x22,0x45,0x82}, // %
{0x38,0x44,0x44,0x38,0x50,0x4A,0x44,0x3A}, // &
{0x08,0x08,0x00,0x00,0x00,0x00,0x00,0x00}, // '
{0x30,0x40,0x80,0x80,0x80,0x80,0x40,0x30}, // (
{0xC0,0x20,0x10,0x10,0x10,0x10,0x20,0xC0}, // )
{0x28,0x10,0xAA,0x54,0xAA,0x10,0x28,0x00}, // *
{0x00,0x10,0x10,0x10,0xFE,0x10,0x10,0x10}, // +
{0x00,0x00,0x00,0x00,0x00,0x08,0x08,0x10}, // ,
{0x00,0x00,0x00,0x00,0x7E,0x00,0x00,0x00}, // -
{0x00,0x00,0x00,0x00,0x00,0x00,0x18,0x18}, // .
{0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80}, // /
{0x7E,0xC1,0xA1,0x91,0x89,0x85,0x83,0x7E}, // 0
{0x10,0x30,0x10,0x10,0x10,0x10,0x10,0x7C}, // 1
{0x38,0x44,0x82,0x04,0x18,0x20,0x40,0xFE}, // 2
{0x7C,0x82,0x02,0x3C,0x02,0x02,0x82,0x7C}, // 3
{0x08,0x18,0x28,0x48,0xFE,0x08,0x08,0x08}, // 4
{0xFE,0x80,0xF8,0x04,0x02,0x82,0x44,0x38}, // 5
{0x38,0x44,0x80,0xB8,0xC4,0x82,0x44,0x38}, // 6
{0xFE,0x02,0x04,0x08,0x10,0x20,0x20,0x20}, // 7
{0x7C,0x82,0x82,0x7C,0x82,0x82,0x82,0x7C}, // 8
{0x7C,0x82,0x82,0x7E,0x02,0x82,0x44,0x38}, // 9
{0x00,0x00,0x18,0x18,0x00,0x18,0x18,0x00}, // :
{0x00,0x00,0x18,0x18,0x00,0x18,0x18,0x30}, // ;
{0x00,0x10,0x20,0x40,0x80,0x40,0x20,0x10}, // <
{0x00,0x00,0x00,0x7E,0x00,0x7E,0x00,0x00}, // =
{0x00,0x80,0x40,0x20,0x10,0x20,0x40,0x80}, // >
{0x70,0x88,0x88,0x10,0x20,0x20,0x00,0x20}, // ?
{0x7E,0x81,0x99,0xA1,0xA1,0x9E,0x80,0x7E}, // @
{0x3C,0x42,0x81,0x81,0xFF,0x81,0x81,0x81}, // A
{0xFC,0x82,0x81,0xFE,0x81,0x81,0x82,0xFC}, // B
{0x3C,0x42,0x81,0x80,0x80,0x81,0x42,0x3C}, // C
{0xFC,0x82,0x81,0x81,0x81,0x81,0x82,0xFC}, // D
{0xFE,0x80,0x80,0xFC,0x80,0x80,0x80,0xFE}, // E
{0xFE,0x80,0x80,0xFC,0x80,0x80,0x80,0x80}, // F
{0x3C,0x42,0x81,0x80,0x87,0x81,0x42,0x3C}, // G
{0x81,0x81,0x81,0xFF,0x81,0x81,0x81,0x81}, // H
{0xFE,0x10,0x10,0x10,0x10,0x10,0x10,0xFE}, // I
{0xFF,0x08,0x08,0x08,0x08,0x88,0x88,0x70}, // J
{0x88,0x90,0xA0,0xC0,0xA0,0x90,0x88,0x84}, // K
{0x80,0x80,0x80,0x80,0x80,0x80,0x80,0xFE}, // L
{0x81,0xC3,0xA5,0x99,0x81,0x81,0x81,0x81}, // M
{0x81,0xC1,0xA1,0x91,0x89,0x85,0x83,0x81}, // N
{0x3C,0x42,0x81,0x81,0x81,0x81,0x42,0x3C}, // O
{0xFC,0x82,0x81,0x82,0xFC,0x80,0x80,0x80}, // P
{0x3C,0x42,0x81,0x81,0x81,0x85,0x42,0x3D}, // Q
{0xFC,0x82,0x81,0x82,0xFC,0x84,0x82,0x81}, // R
{0x3C,0x42,0x81,0x40,0x3E,0x81,0x42,0x3C}, // S
{0xFE,0x10,0x10,0x10,0x10,0x10,0x10,0x10}, // T
{0x82,0x82,0x82,0x82,0x82,0x82,0x44,0x38}, // U
{0x82,0x82,0x82,0x82,0x82,0x44,0x28,0x10}, // V
{0x81,0x81,0x81,0x81,0x99,0xA5,0xC3,0x81}, // W
{0x81,0x42,0x24,0x18,0x18,0x24,0x42,0x81}, // X
{0x82,0x44,0x28,0x10,0x10,0x10,0x10,0x10}, // Y
{0xFF,0x02,0x04,0x08,0x10,0x20,0x40,0xFF}, // Z
{0xE0,0x80,0x80,0x80,0x80,0x80,0x80,0xE0}, // [
{0x80,0x40,0x20,0x10,0x08,0x04,0x02,0x01}, // 백슬래시
{0x07,0x01,0x01,0x01,0x01,0x01,0x01,0x07}, // ]
{0xE0,0xA0,0xE0,0xA0,0xAA,0x15,0x15,0x11}, // am ('^'로 코딩)
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x7E}, // _
{0x10,0x08,0x00,0x00,0x00,0x00,0x00,0x00}, // '
{0x00,0x00,0x38,0x04,0x3C,0x44,0x48,0x34}, // a
{0x00,0x40,0x40,0x40,0x78,0x44,0x44,0x38}, // b
{0x00,0x00,0x18,0x24,0x40,0x40,0x24,0x18}, // c
{0x00,0x04,0x04,0x04,0x3C,0x44,0x44,0x38}, // d
{0x00,0x00,0x38,0x44,0x7C,0x40,0x44,0x38}, // e
{0x00,0x18,0x20,0x20,0x78,0x20,0x20,0x20}, // f
{0x00,0x38,0x44,0x44,0x38,0x04,0x44,0x38}, // g
{0x00,0x40,0x40,0x40,0x78,0x44,0x44,0x44}, // h
{0x00,0x00,0x40,0x00,0x40,0x40,0x40,0x40}, // i
{0x00,0x08,0x00,0x08,0x08,0x08,0x48,0x30}, // j
{0x00,0x40,0x40,0x48,0x50,0x60,0x50,0x48}, // k
{0x00,0x40,0x40,0x40,0x40,0x40,0x40,0x20}, // l
{0x00,0x00,0x00,0x28,0x54,0x44,0x44,0x44}, // m
{0x00,0x00,0x00,0x38,0x44,0x44,0x44,0x44}, // n
{0x00,0x00,0x00,0x38,0x44,0x44,0x44,0x38}, // o
{0x00,0x00,0x70,0x48,0x48,0x70,0x40,0x40}, // p
{0x00,0x00,0x30,0x48,0x48,0x38,0x08,0x08}, // q
{0x00,0x00,0x00,0x30,0x48,0x40,0x40,0x40}, // r
{0x00,0x30,0x48,0x40,0x30,0x08,0x48,0x30}, // s
{0x00,0x20,0x70,0x20,0x20,0x20,0x28,0x10}, // t
{0x00,0x00,0x44,0x44,0x44,0x44,0x44,0x38}, // u
{0x00,0x00,0x44,0x44,0x44,0x44,0x28,0x10}, // v
{0x00,0x00,0x82,0x82,0x82,0x92,0x54,0x28}, // w
{0x00,0x00,0x84,0x48,0x30,0x30,0x48,0x84}, // x
{0x00,0x48,0x48,0x48,0x38,0x08,0x48,0x30}, // y
{0x00,0x00,0x00,0x7C,0x08,0x10,0x20,0x7C}, // z
{0x00,0x30,0x40,0x40,0x80,0x40,0x40,0x30}, // {
{0x00,0x20,0x20,0x20,0x20,0x20,0x20,0x20}, // |
{0x00,0x60,0x10,0x10,0x08,0x10,0x10,0x60}, // }
{0xE0,0xA0,0xE0,0x80,0x8A,0x15,0x15,0x11} // pm ('~'로 코딩)
};
// RTC 동작 정의
RTC_DS1307 RTC; // Tiny RTC (DS1307) 모듈 (SDA - A4, SCL - A5)
// 로터리 인코더, 스위치, LED 제어
#define enc_PinA 2 // 인코더 A를 핀 2에 연결 (인터럽트 0)
#define enc_PinB 4 // 인코더 B를 핀 4에 연결
#define enc_Switch 3 // 인코더 스위치를 핀 3에 연결 (인터럽트 1)
#define mode_Pin 8 // 모드 LED 핀
#define min_Pin 9 // 분 LED 핀
unsigned char enc_A, enc_B, enc_A_prev=0;
static boolean rotating = false;
static boolean clockwise = false;
static boolean updateFlag= false;
static int mode = 0; // 0 - 없음, 1 - 시 설정, 2 - 분 설정
// 디스플레이 문자열 정의
#define max_array_size 100
char ac[max_array_size] = {};
byte rc[8] = {};
String display_message = "";
int arraylen;
// 동작 파라미터
#define delay_line 75 // 줄 전환 간 ms
#define delay_char 400 // 문자 간 ms
#define delay_mess 500 // 메시지 간 ms
#define cblanks 1 // 문자 간 공백 줄 수
#define eblanks 0 // 메시지 끝 추가 공백 줄 수 (8줄 초과분)
// 디스플레이 기능
static boolean top2bottom = false; // 표시 방향 (상하 또는 좌우)
static boolean hour24 = false; // 24시간 표시?
static boolean charHI = true; // 전체 문자 하이라이트?
static boolean doSerial = true; // 시리얼 출력?
//*****************************************************************************************//
// 초기 설정
//*****************************************************************************************//
void setup () {
Wire.begin();
Serial.begin(57600);
if(doSerial) Serial.print("MATRIX Clock - Adrian Jones, Mar. 2014");
// 8×8 LED 매트릭스 제어 핀
pinMode(dataIn, OUTPUT);
pinMode(clock, OUTPUT);
pinMode(load, OUTPUT);
initMatrix(); // LED 매트릭스 초기화
// LED 핀
pinMode(mode_Pin, OUTPUT);
digitalWrite(mode_Pin, 1);
pinMode(min_Pin, OUTPUT);
digitalWrite(min_Pin, 1);
// 인코더 제어
pinMode(enc_PinA, INPUT_PULLUP);
digitalWrite(enc_PinA, HIGH);
pinMode(enc_PinB, INPUT_PULLUP);
digitalWrite(enc_PinB, HIGH);
pinMode(enc_Switch, INPUT_PULLUP);
digitalWrite(enc_Switch, HIGH);
attachInterrupt(0, rotEncoder, CHANGE); // 시간 설정
attachInterrupt(1, swEncoder, CHANGE); // 분/시 설정
// RTC
RTC.begin();
if (! RTC.isrunning()) {
RTC.adjust(DateTime(__DATE__, __TIME__));
if(doSerial) Serial.println(" (RTC reset)");
} else {
if(doSerial) Serial.println(" (RTC running)");
}
}
//*****************************************************************************************//
// 메인 루프
//*****************************************************************************************//
void loop () {
DateTime now = RTC.now();
display_message = createMessage(now);
arraylen = initDisplayString(display_message);
if(updateFlag) {
show_time_and_date(now);
updateFlag = false;
}
while(rotating) {
delay(1); // 디바운스
adjTime(now, clockwise);
show_time_and_date( RTC.now() );
display_message = createMessage(now);
arraylen = initDisplayString(display_message);
delay(1);
rotating = false;
}
delay(5);
for (int i = 0; i < (arraylen-7); i++) {
for (int j = 1; j < 9; j++) {
maxSingle(j,ac[i+8-j]);
}
if(i%(8+cblanks) == 0) {
if(charHI) maxSingle(max7219_reg_intensity, 0x01);
newDelay(delay_char);
} else {
maxSingle(max7219_reg_intensity, 0x00);
newDelay(delay_line);
}
}
if(mode == 0) newDelay(delay_mess);
}
//*****************************************************************************************//
// 인터럽트 루틴
//*****************************************************************************************//
// rotEncoder(): 인코더 회전 시 호출되는 ISR
void rotEncoder(){
delay(1);
enc_A = digitalRead(enc_PinA);
enc_B = digitalRead(enc_PinB);
if(!enc_A && enc_A_prev){
clockwise = (!enc_A && enc_B)? true : false;
if(mode != 0) rotating = true;
}
enc_A_prev = enc_A;
}
// swEncoder(): 인코더 버튼 눌림 시 호출되는 ISR
void swEncoder(){
delay(1);
if(digitalRead (enc_Switch) != LOW) return;
delay(1);
if(digitalRead (enc_Switch) != LOW) return;
mode++;
mode = mode % 3;
digitalWrite(mode_Pin, !(mode == 1));
digitalWrite(min_Pin, !(mode == 2));
updateFlag = true;
}
//*****************************************************************************************//
// 동작 루틴
//*****************************************************************************************//
// newDelay 함수
void newDelay (int dly) {
for (int z=1; z< dly; z++) {
delay(1);
if(rotating || updateFlag) break;
}
}
// initMatrix(): MAX7219 레지스터 초기화
void initMatrix() {
maxSingle(max7219_reg_scanLimit, 0x07);
maxSingle(max7219_reg_decodeMode, 0x00);
maxSingle(max7219_reg_shutdown, 0x01);
maxSingle(max7219_reg_displayTest, 0x00);
for (int e=1; e<=8; e++) {maxSingle(e,0); }
maxSingle(max7219_reg_intensity, 0x00);
}
// adjTime(): 인코더 방향에 따라 시/분 증감
void adjTime(DateTime now, boolean dir) {
if(mode == 1) {
int adj_hrs = now.hour();
if(dir) {
if(++adj_hrs >= 25) adj_hrs = 1;
} else {
if(adj_hrs == 0) adj_hrs = 24;
if(--adj_hrs <= 0) adj_hrs = 24;
}
RTC.adjust(DateTime(now.year(), now.month(), now.day(), adj_hrs, now.minute(), now.second() ));
}
if(mode == 2) {
int adj_mins = now.minute();
if(dir) {
if(++adj_mins >= 60) adj_mins = 0;
} else {
if(--adj_mins < 0) adj_mins = 59;
}
RTC.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), adj_mins, now.second() ));
}
}
// rotChar(): 문자를 90도 회전 (상하 → 좌우)
byte rotChar(char inLetter) {
int ind = int(inLetter) - 0x20;
for (int col = 0; col < 8; col++) {
byte mask = 0x01 << (7-col);
for (int row = 0; row < 8 ; row++) {
bitWrite(rc[col], 7-row,(alphanum[ind][row] & mask));
}
}
}
// show_time_and_date: 시간 문자열 및 바이트 출력
void show_time_and_date(DateTime datetime){
if(doSerial) {
int minutes = datetime.minute();
int hours = datetime.hour();
if(hours==0) hours=24;
int seconds = datetime.second();
char delim = '/';
char dend = ' ';
String te = "Current date/time: ";
te = te + datetime.year() + delim + datetime.month() + delim + datetime.day() + dend;
Serial.print(te);
if(hours<10) Serial.print(0);
Serial.print(hours,DEC);
Serial.print(":");
if(minutes<10) Serial.print(0);
Serial.print(minutes,DEC);
Serial.print(":");
if(seconds < 10) Serial.print(0);
Serial.print(seconds,DEC);
Serial.println("");
}
}
String createMessage(DateTime datetime) {
String new_mess = " ";
int hr = datetime.hour()%24;
if(hr == 0) hr = 24;
int mn = datetime.minute();
if(mode == 0) {
if(hour24) {
new_mess += hr;
} else {
new_mess += (hr > 12)? hr - 12 : hr;
}
new_mess += ':';
if(mn < 10) new_mess += '0';
new_mess += mn;
if(!hour24) new_mess += (hr > 12)? "~" : "^";
}
if(mode == 1) {
new_mess += hr;
}
if(mode == 2) {
if(mn < 10) new_mess += '0';
new_mess += mn;
}
return new_mess;
}
// initDisplayString(): 메시지 문자열을 블랭크 포함 배열로 변환
int initDisplayString(String message) {
int x = 0;
for (int y = 0; y < message.length(); y++ ){
char thisCh = message.charAt(y);
int ind = int(thisCh) - 0x20;
if(!top2bottom) rotChar(thisCh);
for (int row = 0; row < 8 + cblanks; row++) {
if (row <= 7) {
ac[x] = (top2bottom)? alphanum[ind][7-row] : rc[row];
} else {
ac[x] = 0;
}
x++;
}
}
for(int y = 0; y < 7+eblanks; y++) {ac[x] = 0; x++; }
return x;
}
// maxSingle(): 레지스터에 데이터 로드
void maxSingle(byte reg, byte col) {
digitalWrite(load, LOW);
putByte(reg);
putByte(col);
digitalWrite(load,HIGH);
}
// putByte(): 매트릭스에 데이터 전송 (MSB to LSB)
void putByte(byte data) {
byte i = 8;
byte mask;
while(i > 0) {
mask = 0x01 << (i - 1);
digitalWrite(clock, LOW);
if (data & mask){
digitalWrite(dataIn, HIGH);
} else {
digitalWrite(dataIn, LOW);
}
digitalWrite(clock, HIGH);
--i;
}
}