The Matrixclock project
This little project was born to build a present for my son’s birthday, I had a couple of components laying around and I decided to make something new.Goals
- build an easy-to-use alarm clock with a personal touch
- use only components I already had in stock
- use as few components as possible
- low power consumption
- learn to solder some SOIC components
Design decisions
- project a 3.3V circuit
- use a 64 led matrix for the display…
- a rotary encoder with integrated pushbutton for the controls
- an RGB led to change the clock color while the time passes (60′ cicle)
- sometimes print some random messages addressed to my son
- play a small song once every hour at his birthday
- play a custom alarm music
- an LDR sensor to adjust display and RGB brightness in the dark
- ATMEGA328P-PU for the MCU, clocked @ 8MHz using internal oscillator and running @ 3.3V
- DS1388 SOIC for the RTC (unfortunately DS1307 doesn’t run @ 3.3V)
- an 8OHM speaker for the sound
- an AS1107 SOIC to drive the matrix (MAX7219 does not officialy run @ 3.3V even if it seemed ok to me when I tested it on the breadboard)
And my beloved jungle-breadboard 🙂
Building the board
Placing components was the most difficult and tedious work, I had to keep the board small and I only had 100x70mm boards available. This was my first project using soic components, initially I was a bit scared about that, but it wasn’t too hard after all. I used the toner-transfer technique to prepare the PCB: … and HCL + H2O2 to etch the board: And finally: some more photos on Flickr and a short and ugly video showing the clock running.Follow the source Luke! (The code)
The code is very patchy but Just Works (TM) , I used a couple of Arduino libraries and I merged the Pong game I’ve already written as a standalone sketch./** * Matrix Clock */ #define DEBUG_CLOCK 0 #define ENABLE_RGB 1 /* rgb led */ // Pins #define ENCODER_PIN_1 2 // INT0 #define ENCODER_PIN_2 3 // INT1 #define SET_PIN 4 // digital pull up, connected to ENCODER push button #define LC_PIN_1 8 #define LC_PIN_2 7 #define LC_PIN_3 6 #define ALM_SWITCH_PIN A3 #define BUZZER_PIN 5 // digital (PWM?) -> BUZZER -> GND #define RED_PIN 9 #define GREEN_PIN 10 #define BLUE_PIN 11 #define LDR_PIN A2 // Includes #include "LedControl.h" #include "8x8Font.h" #include "3x5Font.h" #include <EEPROM.h> #include "EEPROMAnything.h" #include <Wire.h> #include "RTClib.h" #include "Timer.h" #include <Encoder.h> #include "note.h" #include "messages.h" #define NO_PORTB_PINCHANGES // to indicate that port b will not be used for pin change interrupts #define NO_PORTC_PINCHANGES // to indicate that port c will not be used for pin change interrupts // if there is only one PCInt vector in use the code can be inlined // reducing latency and code size // define DISABLE_PCINT_MULTI_SERVICE below to limit the handler to servicing a single interrupt per invocation. #define DISABLE_PCINT_MULTI_SERVICE #include <PinChangeInt.h> // FSM status #define CLK_IDLE 0 #define CLK_PLAY 1 #define CLK_SET_ALM_H 2 #define CLK_SET_ALM_M 3 #define CLK_SET_TIM_H 4 #define CLK_SET_TIM_M 5 #define CLK_SET_TIM_S 6 #define CLK_SET_DAT_D 7 #define CLK_SET_DAT_M 8 #define CLK_SET_DAT_Y 9 #define CLK_SET_SPEED 10 #define CLK_SET_LAST 10 // Must be the last: check main code for status changes. #define POS_CENTER 1 #define POS_LEFT 0 #define POS_RIGHT 2 #define SYM_ALM 0 #define SYM_DATE 1 #define SYM_TIME 2 #define SYM_SETUP 3 // Config #define BIRTHDAY_M 4 #define BIRTHDAY_D 12 #define CHAR_SPEED 20 // Random message every #define RANDOM_MSG_EVERY 6 * 60 * 1000 // 13 minutes #define PRINT_DATE_EVERY_CICLES 5 // Alarm FSM #define ALM_PLAY 1 #define ALM_STOP 0 #define ALM_CLEARED 2 // Pong #define PADSIZE 3 #define BALL_DELAY 100 #define GAME_DELAY 9 #define BOUNCE_VERTICAL 1 #define BOUNCE_HORIZONTAL -1 #define HIT_NONE 0 #define HIT_CENTER 1 #define HIT_LEFT 2 #define HIT_RIGHT 3 // Timers #define TIMEOUT 5000 // millis #define DELAYED_TIMEOUT 20000 // millis #define DEBOUNCE 200 // millis between pulses #define ALM_RESET_AFTER_MINUTES 2 // timer for alarm reset // Ugly globals... uint8_t timeH; uint8_t timeM; volatile uint8_t timeS; uint8_t dateD; uint8_t dateM; uint8_t dateY; uint8_t dateW; uint8_t almH; uint8_t almM; //int almDays; uint8_t brightness = 8; const uint8_t eeprom_id = 0x99; DateTime now; uint8_t print_date; uint8_t char_speed = CHAR_SPEED; // store clock status int clockStatus = CLK_IDLE; // Used in ISR, keep one-byte-long to avoid disabling INTs byte alarmStatus = ALM_STOP; volatile boolean setButtonPressed = false; volatile unsigned long setButtonBounceTime=0; // variable to hold ms count to debounce a pressed switch int oldStatus = CLK_IDLE; // Timers Timer timer; volatile int idleTimer = 1000; // set to out of range value volatile int alarmTimer = 1000; volatile int storeConfigTimer = 1000; volatile int ballTimer = 1000; // Pong byte direction; // Wind rose, 0 is north int xball; int yball; int yball_prev; int xpad; // Sound //char noteNames[] = {'C','D','E','F','G','a','b'}; //unsigned int frequencies[] = {262,294,330,349,392,440,494}; //const byte noteCount = sizeof(noteNames); // the number of notes // (7 in this example) //notes, a space represents a rest // char song[] PROGMEM = "CCGGaaGFFEEDDC GGFFEEDGGFFEED CCGGaaGFFEEDDC "; unsigned int alarm_song[] PROGMEM = {NOTE_C4, NOTE_C4, NOTE_G4, NOTE_G4, NOTE_A4, NOTE_A4, NOTE_G4, SILENCE, NOTE_F4, NOTE_F4, NOTE_E4, NOTE_E4, NOTE_D4, NOTE_D4, NOTE_C4, SILENCE, SILENCE, NOTE_G4, NOTE_G4, NOTE_F4, NOTE_F4, NOTE_E4, NOTE_E4, NOTE_D4, SILENCE, NOTE_G4, NOTE_G4, NOTE_F4, NOTE_F4, NOTE_E4, NOTE_E4, NOTE_D4, SILENCE, SILENCE, NOTE_C4, NOTE_C4, NOTE_G4, NOTE_G4, NOTE_A4, NOTE_A4, NOTE_G4, SILENCE, NOTE_F4, NOTE_F4, NOTE_E4, NOTE_E4, NOTE_D4, NOTE_D4, NOTE_C4, SILENCE, END_SONG}; //do do re do fa mi do do re do sol fa do do do# la fa mi re do do re do fa mi // # G G A G C B | G G A G D C | G G + G E C B A | F F E C D C unsigned int birthday_song[] PROGMEM = {NOTE_G4, NOTE_A4, NOTE_G4, NOTE_C5, NOTE_B4, SILENCE, NOTE_G4, NOTE_A4, NOTE_G4, NOTE_D5, NOTE_C5, SILENCE, NOTE_G4, NOTE_G5, NOTE_E5, NOTE_C5, NOTE_B4, NOTE_A4, SILENCE, NOTE_F4, NOTE_E4, NOTE_C4, NOTE_D4, NOTE_C4, SILENCE, END_SONG}; // Encoder int encoderCount = 0; Encoder myEnc(ENCODER_PIN_2, ENCODER_PIN_1); #if DEBUG_CLOCK RTC_DS1307 RTC; // variables created by the build process when compiling the sketch extern int __bss_end; extern void *__brkval; // function to return the amount of free RAM int memoryFree() { int freeValue; if((int)__brkval == 0) freeValue = ((int)&freeValue) - ((int)&__bss_end); else freeValue = ((int)&freeValue) - ((int)__brkval); return freeValue; } #else RTC_DS1388 RTC; #endif LedControl lc = LedControl(LC_PIN_1, LC_PIN_2, LC_PIN_3,1); char char_buffer[8]; char char_buffer2[8]; char buf[64]; #if ENABLE_RGB void setColor(int red, int green, int blue) { analogWrite(RED_PIN, 255-red); analogWrite(GREEN_PIN, 255-green); analogWrite(BLUE_PIN, 255-blue); } //Convert a given HSV (Hue Saturation Value) to RGB(Red Green Blue) and set the led to the color // h is hue value, integer between 0 and 360 // s is saturation value, double between 0 and 1 // v is value, double between 0 and 1 //http://splinter.com.au/blog/?p=29 void setColorHsv(int h, double s, double v) { byte rgb[3]; // Make sure our arguments stay in-range h = max(0, min(360, h)); s = max(0, min(1.0, s)); v = max(0, min(1.0, v)); if(s == 0) { // Achromatic (grey) rgb[0] = rgb[1] = rgb[2] = round(v * 255); } else { double hs = h / 60.0; // sector 0 to 5 int i = floor(hs); double f = hs - i; // factorial part of h double p = v * (1 - s); double q = v * (1 - s * f); double t = v * (1 - s * (1 - f)); double r, g, b; switch(i) { case 0: r = v; g = t; b = p; break; case 1: r = q; g = v; b = p; break; case 2: r = p; g = v; b = t; break; case 3: r = p; g = q; b = v; break; case 4: r = t; g = p; b = v; break; default: // case 5: r = v; g = p; b = q; } rgb[0] = round(r * 255.0); rgb[1] = round(g * 255.0); rgb[2] = round(b * 255.0); } setColor(rgb[0], rgb[1], rgb[2]); } void setColorHueValue(int hueValue, uint8_t brightness) { setColorHsv(hueValue, 1, constrain((double)brightness/15, 0.1, 1)); } #endif /* * Play a note for a given time * * void playNote(char note, int duration) { // play the tone corresponding to the note name for (int i = 0; i < noteCount; i++) { // try and find a match for the noteName to get the index to the note if (noteNames[i] == note) // find a matching note name in the array // play the note using the frequency: tone(BUZZER_PIN, frequencies[i], duration); } // if there is no match then the note is a rest, so just do the delay delay(duration); } */ /** * Play a note for a given time * */ void playFrequency(unsigned int note, int duration) { if(note != SILENCE){ tone(BUZZER_PIN, note, duration); } delay(duration); } /** * Set IDLE status */ void setIdle() { clockStatus = CLK_IDLE; } /** * Get encoder move */ void readEncoder(){ encoderCount = myEnc.read()/4; if(encoderCount){ myEnc.write(0); } } /** * String message handling */ char reverseByte(char b) { b = (b & 0xF0) >> 4 | (b & 0x0F) << 4; b = (b & 0xCC) >> 2 | (b & 0x33) << 2; b = (b & 0xAA) >> 1 | (b & 0x55) << 1; return b; } void setSprite(char* font){ for(int r = 0; r < 8; r++){ lc.setRow(0, r, reverseByte(*(font + r))); } } void load_char(char i, char* c){ for(int r = 0; r < 8; r++){ c[r] = pgm_read_byte((char*)font_data + (i * 8) + r); } } void merge_char(char* c1, char* c2, char pos){ for(int r = 0; r < 8; r++){ c1[r] = (c1[r] << 1) | (c2[r] >> (8 - pos)) ; } } void print_string(char* s){ load_char(32, char_buffer); bool valid = 1; while (valid){ setSprite(char_buffer); load_char(*s ? *s : 32, char_buffer2); for(int r = 1; r <= 8; r++){ if(setButtonPressed){ return; } merge_char(char_buffer, char_buffer2, r); setSprite(char_buffer); delay(char_speed); } valid = (char) *s; s++; } } void load_digits(char* c, uint8_t digits, uint8_t pos, uint8_t sym){ for(uint8_t i=0; i<5; i++){ c[i] = pgm_read_byte((char*)small_font_data + digits / 10 * 5 + i) << 4 | pgm_read_byte((char*)small_font_data + digits % 10 * 5 + i); } c[5] = 0; uint8_t shift = constrain(6 - 3 * pos, 0, 5); switch(sym){ case SYM_ALM: c[6] = B00000010 << shift; c[7] = B00000101 << shift; break; case SYM_TIME: c[6] = B00000111 << shift; c[7] = B00000010 << shift; break; case SYM_DATE: c[6] = B00000111 << shift; c[7] = B00000101 << shift; break; case SYM_SETUP: c[6] = 0; c[7] = B00000111 << shift; break; } } /** * Pong game */ void newGame() { lc.clearDisplay(0); // initial position xball = random(1, 7); yball = 1; direction = random(3, 6); // Go south load_char(1, char_buffer); setSprite(char_buffer); delay(700); lc.clearDisplay(0); } void setPad() { readEncoder(); xpad = constrain(xpad + encoderCount, 0, 8 - PADSIZE); } /** * Checks if the ball is in contact with the walls */ int checkBounce() { if(!xball || !yball || xball == 7 || yball == 6){ int bounce = (yball == 0 || yball == 6) ? BOUNCE_HORIZONTAL : BOUNCE_VERTICAL; return bounce; } return 0; } int getHit() { if(yball != 6 || xball < xpad || xball > xpad + PADSIZE){ return HIT_NONE; } if(xball == xpad + PADSIZE / 2){ return HIT_CENTER; } return xball < xpad + PADSIZE / 2 ? HIT_LEFT : HIT_RIGHT; } bool checkLoose() { return yball == 6 && getHit() == HIT_NONE; } void hitSound(){ playFrequency(NOTE_B4, 2); } void moveBall() { int bounce = checkBounce(); if(bounce) { switch(direction){ case 0: direction = 4; break; case 1: direction = (bounce == BOUNCE_VERTICAL) ? 7 : 3; break; case 2: direction = 6; break; case 6: direction = 2; break; case 7: direction = (bounce == BOUNCE_VERTICAL) ? 1 : 5; break; case 5: direction = (bounce == BOUNCE_VERTICAL) ? 3 : 7; break; case 3: direction = (bounce == BOUNCE_VERTICAL) ? 5 : 1; break; case 4: direction = 0; break; } } // Check hit switch(getHit()){ case HIT_LEFT: if(direction == 0){ direction = 7; } else if (direction == 1){ direction = 0; } hitSound(); break; case HIT_RIGHT: if(direction == 0){ direction = 1; } else if(direction == 7){ direction = 0; } hitSound(); break; case HIT_CENTER: hitSound(); break; } // Check orthogonal directions and borders ... // should never happen if((direction == 0 && xball == 0) || (direction == 4 && xball == 7)){ direction++; } if(direction == 0 && xball == 7){ direction = 7; } if(direction == 4 && xball == 0){ direction = 3; } if(direction == 2 && yball == 0){ direction = 3; } if(direction == 2 && yball == 6){ direction = 1; } if(direction == 6 && yball == 0){ direction = 5; } if(direction == 6 && yball == 6){ direction = 7; } // Corner case: if(xball == 0 && yball == 0){ direction = 3; } if(xball == 0 && yball == 6){ direction = 1; } if(xball == 7 && yball == 6){ direction = 7; } if(xball == 7 && yball == 0){ direction = 5; } yball_prev = yball; if(2 < direction && direction < 6) { yball++; } else if(direction != 6 && direction != 2) { yball--; } if(0 < direction && direction < 4) { xball++; } else if(direction != 0 && direction != 4) { xball--; } xball = max(0, min(7, xball)); yball = max(0, min(6, yball)); } void gameOver() { load_char(33, char_buffer); setSprite(char_buffer); delay(1500); lc.clearDisplay(0); } void drawGame() { if(yball_prev != yball){ lc.setRow(0, yball_prev, 0); } lc.setRow(0, yball, byte(1 << (xball))); byte padmap = byte(0xFF >> (8 - PADSIZE) << xpad) ; lc.setRow(0, 7, padmap); } void initGame(){ randomSeed(analogRead(0)); newGame(); ballTimer = timer.every(BALL_DELAY, moveBall); } void playGame(){ while(!setButtonPressed){ timer.update(); setPad(); drawGame(); if(checkLoose()) { gameOver(); newGame(); } delay(GAME_DELAY); } timer.stop(ballTimer); } /** * Wake up effect, this is not returning * check for status from button ISR * */ void wakeUp(const unsigned int* song) { while (pgm_read_word(song) != END_SONG) { if(alarmStatus != ALM_PLAY || setButtonPressed){ alarmStatus = ALM_CLEARED; setButtonPressed = false; return; } playFrequency(pgm_read_word(song++), 333); // play the note } delay(4000); // wait four seconds before repeating the song } /** * Birthday */ void check_birthday(){ unsigned int *song; song = birthday_song; if(dateD == BIRTHDAY_D && dateM == BIRTHDAY_M && timeH > 9 && timeH < 21){ load_char(3, char_buffer); setSprite(char_buffer); while (pgm_read_word(song) != END_SONG) { playFrequency(pgm_read_word(song++), 333); // play the note } delay(4000); } } /** * Turn off alarm */ void stopAlarm(){ alarmStatus = ALM_CLEARED; //digitalWrite(BUZZER_PIN, LOW); //digitalWrite(ALM_LED_PIN, LOW); } /** * ISR on FALLING push button */ void setSetButtonPressed(){ // this is the interrupt handler for button presses // it ignores presses that occur in intervals less then the bounce time if (abs(millis() - setButtonBounceTime) > DEBOUNCE) { setButtonPressed = true; setButtonBounceTime = millis(); // set whatever bounce time in ms is appropriate } } /** * Changes clock status */ void changeStatus() { clockStatus++; if(clockStatus > CLK_SET_LAST){ setIdle(); } // Start timer timer.stop(idleTimer); idleTimer = timer.after(TIMEOUT, setIdle); setButtonPressed = false; } /** * Store config to EEPROM */ void store_config(){ // Read EEPROM int write = 0; write += EEPROM_writeAnything(write, eeprom_id); write += EEPROM_writeAnything(write, char_speed); write += EEPROM_writeAnything(write, almH); EEPROM_writeAnything(write, almM); } /** * Delayed store config */ void store_config_delayed(){ timer.stop(storeConfigTimer); storeConfigTimer = timer.after(DELAYED_TIMEOUT, store_config); } /** * Adjust clock */ void storeDateTime(){ RTC.adjust(DateTime(dateY, dateM, dateD, timeH, timeM, timeS)); } /** * Adjust brightness */ void adjustBrightness(){ brightness = map(analogRead(LDR_PIN), 0, 1023, 0, 15); lc.setIntensity(0, brightness); #if ENABLE_RGB setColorHueValue((uint16_t)timeM * 6, brightness); #endif } /** * Random message */ void randomMessage(){ if(clockStatus == CLK_IDLE){ strcpy_P(buf, (char*)pgm_read_word(&(messages[random(0, sizeof(messages)/sizeof(char *))]))); print_string(buf); } } /** * Setup */ void setup() { // Read Alm from EEPROM // Read EEPROM if(EEPROM.read(0) == eeprom_id){ int read = 1; read += EEPROM_readAnything(read, char_speed); read += EEPROM_readAnything(read, almH); EEPROM_readAnything(read, almM); } else { store_config(); } pinMode(LDR_PIN, INPUT); adjustBrightness(); // LCD // The MAX72XX is in power-saving mode on startup, // we have to do a wakeup call lc.shutdown(0,false); /* Set the brightness to a medium values range (0-15) */ lc.setIntensity(0, brightness); /* and clear the display */ lc.clearDisplay(0); // RTC Wire.begin(); RTC.begin(); if (! RTC.isrunning()) { // following line sets the RTC to the date & time this sketch was compiled RTC.adjust(DateTime(__DATE__, __TIME__)); } // Encoder & push pinMode(SET_PIN, INPUT); digitalWrite(SET_PIN, HIGH); PCintPort::attachInterrupt(SET_PIN, &setSetButtonPressed, FALLING); // Alm enable pin pinMode(ALM_SWITCH_PIN, INPUT); digitalWrite(ALM_SWITCH_PIN, HIGH); // pull up pinMode(BUZZER_PIN, OUTPUT); timer.every(1000, adjustBrightness); timer.every(RANDOM_MSG_EVERY, randomMessage); print_date = 0; } /** * Loop */ void loop() { if(setButtonPressed && alarmStatus != ALM_PLAY){ changeStatus(); } // Read encoder readEncoder(); // Read clock now = RTC.now(); timeH = now.hour(); timeM = now.minute(); timeS = now.second(); dateD = now.day(); dateM = now.month(); dateY = now.yoff(); boolean alarmSwitchClosed; alarmSwitchClosed = (digitalRead(ALM_SWITCH_PIN) == LOW); // pull-up, closed is low // Check alarm, buttons pressed stop alarm int minutes_after_alarm; minutes_after_alarm = ((int)timeH * 60 + (int)timeM) - ((int)almH * 60 + (int)almM); if(clockStatus == CLK_IDLE && alarmSwitchClosed && alarmStatus == ALM_STOP && minutes_after_alarm == 0){ alarmStatus = ALM_PLAY; } if(alarmStatus == ALM_PLAY){ if(clockStatus == CLK_IDLE && alarmSwitchClosed && !setButtonPressed){ load_char(13, char_buffer); setSprite(char_buffer); wakeUp(alarm_song); } else { stopAlarm(); } } if(clockStatus == CLK_PLAY){ timer.stop(idleTimer); initGame(); playGame(); idleTimer = timer.after(TIMEOUT, setIdle); } if(clockStatus > CLK_PLAY){ uint8_t digits; uint8_t pos; uint8_t sym; switch(clockStatus) { case CLK_SET_ALM_H: pos = POS_LEFT; sym = SYM_ALM; digits = almH; break; case CLK_SET_ALM_M: pos = POS_CENTER; sym = SYM_ALM; digits = almM; break; case CLK_SET_TIM_H: pos = POS_LEFT; sym = SYM_TIME; digits = timeH; break; case CLK_SET_TIM_M: pos = POS_CENTER; sym = SYM_TIME; digits = timeM; break; case CLK_SET_TIM_S: pos = POS_RIGHT; sym = SYM_TIME; digits = timeS; break; case CLK_SET_DAT_D: pos = POS_LEFT; sym = SYM_DATE; digits = dateD; break; case CLK_SET_DAT_M: pos = POS_CENTER; sym = SYM_DATE; digits = dateM; break; case CLK_SET_DAT_Y: pos = POS_RIGHT; sym = SYM_DATE; pos = 2; digits = dateY; break; case CLK_SET_SPEED: digits = char_speed; pos = POS_CENTER; sym = SYM_SETUP; break; } if(encoderCount){ // reset timeout timer.stop(idleTimer); idleTimer = timer.after(TIMEOUT, setIdle); // Update... switch(clockStatus) { case CLK_SET_ALM_H: almH += encoderCount; if(almH > 23) almH = 0; store_config_delayed(); break; case CLK_SET_ALM_M: almM += encoderCount; if(almM > 59) almM = 0; store_config_delayed(); break; case CLK_SET_TIM_H: timeH += encoderCount; if(timeH > 23) timeH = 0; storeDateTime(); break; case CLK_SET_TIM_M: timeM += encoderCount; if(timeM > 59) timeM = 0; storeDateTime(); break; case CLK_SET_TIM_S: timeS += encoderCount; if(timeS > 59) timeS = 0; storeDateTime(); break; case CLK_SET_DAT_D: dateD += encoderCount; if(dateD > 31)dateD = 1; storeDateTime(); break; case CLK_SET_DAT_M: dateM += encoderCount; if(dateM > 12) dateM= 1; storeDateTime(); break; case CLK_SET_DAT_Y: dateY += encoderCount; if(dateY > 99) dateY = 0; storeDateTime(); break; case CLK_SET_SPEED: char_speed+= encoderCount; if(char_speed > 30) char_speed = 10; if(char_speed < 10) char_speed = 30; store_config_delayed(); break; } } load_digits(char_buffer, digits, pos, sym); setSprite(char_buffer); } else { //strcpy_P(buf, PSTR("Ciao Leo... sono le")); //print_string((char*) buf); #if DEBUG_CLOCK //sprintf(buf, "E: %d S: %d", encoderCount, clockStatus); //print_string(buf); //sprintf_P(buf, PSTR("F:%d"), memoryFree()); //print_string(buf); sprintf_P(buf, PSTR("B:%d"), brightness); print_string(buf); #endif sprintf_P(buf, PSTR("%02d:%02d:%02d"), now.hour(), now.minute(), now.second()); print_string(buf); if(print_date++ == PRINT_DATE_EVERY_CICLES){ print_date = 0; sprintf_P(buf, PSTR("%02d-%02d-%d"), now.day(), now.month(), now.year()); print_string(buf); } // Show alarm if(alarmSwitchClosed && alarmStatus == ALM_STOP){ sprintf_P(buf, PSTR("%c %02d:%02d"), 13, almH, almM); print_string(buf); } } if(timeM == 0){ check_birthday(); } // Hand timer if(alarmStatus != ALM_STOP && minutes_after_alarm >= ALM_RESET_AFTER_MINUTES){ alarmStatus = ALM_STOP; } timer.update(); }