With an Arduino, an MP3 shield, and a real-time clock one can make a cute alarm clock. I wanted to go one step further and add a pretty almost-graphical interface with an old Nokia LCD screen (that part was especially fun as it led to writing a simplistic interface event loop as described below).

Here are the main parts of the hardware: an Arduino with an MP3 shield in the center, the real-time clock to the left, and the Nokia screen to the right. Connecting them is straightforward and documented on various websites.

Functional Prototype

The clock was themed after Yale’s Harkness Tower. Below you can see the quickly put together, laser cut and engraved enclosure:

Inside Outside

Before we go into the code, here is the finished prototype:


Event loop

An event loop is one of the standard ways to implement graphical interfaces. Our case is especially simple: a loop is continuously checking for input (a button press); if there is no input it runs all the screen-updating, alarm-scheduling, time-keeping functions it needs to run; if there is input it acts on that input (changes a menu, resets an alarm, etc). Below is a barely-simplified version of the actual code:

enum button waitForInput(void)            // This is called inside Arduino's `loop`.
{                                         // It waits for button presses.
  while (NO_INPUT_EVENT_DETECTED) {       // It executes all screen-updating callbacks as it is waiting.
    doWhileWaitingForInput();             // When a button press is detected it returns and the rest of `loop`
    if (INPUT_HAPPENED) {                 // does whatever the user asked for through the buttons.

byte callbacksCount = 0;
void (*callbacks[CALLBACKS_MAX])(void);   // This array contains pointers to all the callback functions.

void addCallback(void (*callback)(void)) {
  callbacks[callbacksCount] = callback;

void removeCallback(void) {
  callbacks[callbacksCount] = 0;

void doWhileWaitingForInput(void) {       // This is called while waiting for input in order to 
  for (byte i=0; i<callbacksCount; i++) { // update the screen, update the time, and check for 
    (*callbacks[i])();                    // alarms that have to be activated. All "async" tasks
  }                                       // are implemented as callback functions added to the
  delay(GUI_PAUSE);                       // `callbacks` array. The alarm, for instance, is
}                                         // activated by adding the `checkAlarm` callback.

void setup() {
  addCallback(updateTimeAndDrawHeader);   // An example of adding one particular function (defined elsewhere).

void loop() {
  switch(waitForInput()) {
    case MENU_BUTTON:
      // act on button press
    case DOWN_BUTTON:
      // act on different button press
    // etc

LCD fonts (Memory constraints, Cyrillic)

Finally, I had two problems with the font for drawing text on the LCD screen. The font is just series of low resolution (just a few pixels per letter) bitmaps. Those use quite a few bytes of our very limited microcontroller memory, but thankfully we can just use the PROGMEM macro to move it to the program memory, instead of RAM.

The second issue was to find a Cyrillic font (it was a gift I wanted to make in the person’s native language instead of English). I ended up designing my own font (overwriting the usual ASCII characters), and here are the necessary bitmaps:

// The first 20 ASCII characters were skipped to save space.
const byte ASCII[] PROGMEM =  // ASCII code point, usual ASCII character, my Cyrillic font  
{0x00, 0x00, 0x00, 0x00, 0x00 // 20  
,0x00, 0x00, 0x5f, 0x00, 0x00 // 21 !
,0x00, 0x07, 0x00, 0x07, 0x00 // 22 "
,0x14, 0x7f, 0x14, 0x7f, 0x14 // 23 #
,0x24, 0x2a, 0x7f, 0x2a, 0x12 // 24 $
,0x23, 0x13, 0x08, 0x64, 0x62 // 25 %
,0x36, 0x49, 0x55, 0x22, 0x50 // 26 &
,0x00, 0x05, 0x03, 0x00, 0x00 // 27 '
,0x00, 0x1c, 0x22, 0x41, 0x00 // 28 (
,0x00, 0x41, 0x22, 0x1c, 0x00 // 29 )
,0x14, 0x08, 0x3e, 0x08, 0x14 // 2a *
,0x08, 0x08, 0x3e, 0x08, 0x08 // 2b +
,0x00, 0x50, 0x30, 0x00, 0x00 // 2c ,
,0x08, 0x08, 0x08, 0x08, 0x08 // 2d -
,0x00, 0x60, 0x60, 0x00, 0x00 // 2e .
,0x20, 0x10, 0x08, 0x04, 0x02 // 2f /
,0x3e, 0x51, 0x49, 0x45, 0x3e // 30 0
,0x00, 0x42, 0x7f, 0x40, 0x00 // 31 1
,0x42, 0x61, 0x51, 0x49, 0x46 // 32 2
,0x21, 0x41, 0x45, 0x4b, 0x31 // 33 3
,0x18, 0x14, 0x12, 0x7f, 0x10 // 34 4
,0x27, 0x45, 0x45, 0x45, 0x39 // 35 5
,0x3c, 0x4a, 0x49, 0x49, 0x30 // 36 6
,0x01, 0x71, 0x09, 0x05, 0x03 // 37 7
,0x36, 0x49, 0x49, 0x49, 0x36 // 38 8
,0x06, 0x49, 0x49, 0x29, 0x1e // 39 9
,0x00, 0x36, 0x36, 0x00, 0x00 // 3a :
,0x00, 0x56, 0x36, 0x00, 0x00 // 3b ;
,0x08, 0x14, 0x22, 0x41, 0x00 // 3c <
,0x14, 0x14, 0x14, 0x14, 0x14 // 3d =
,0x00, 0x41, 0x22, 0x14, 0x08 // 3e >
,0x02, 0x01, 0x51, 0x09, 0x06 // 3f ?
,0x32, 0x49, 0x79, 0x41, 0x3e // 40 @
,0x7e, 0x11, 0x11, 0x11, 0x7e // 41 A А
,0x7f, 0x49, 0x49, 0x49, 0x31 // 42 B Б
,0x7f, 0x40, 0x40, 0x7f, 0xc0 // 43 C Ц
,0xc0, 0x7f, 0x41, 0x7f, 0xc0 // 44 D Д
,0x7f, 0x49, 0x49, 0x49, 0x41 // 45 E Е
,0x1c, 0x22, 0x7f, 0x22, 0x1c // 46 F Ф
,0x7f, 0x01, 0x01, 0x01, 0x01 // 47 G Г
,0x63, 0x14, 0x08, 0x14, 0x63 // 48 H Х
,0x7f, 0x10, 0x08, 0x04, 0x7f // 49 I И
,0x7f, 0x10, 0x09, 0x04, 0x7f // 4a J Й
,0x7f, 0x08, 0x14, 0x22, 0x41 // 4b K К
,0x40, 0x3f, 0x01, 0x01, 0x7f // 4c L Л
,0x7f, 0x02, 0x0c, 0x02, 0x7f // 4d M М
,0x7f, 0x08, 0x08, 0x08, 0x7f // 4e N Н
,0x3e, 0x41, 0x41, 0x41, 0x3e // 4f O О
,0x7f, 0x01, 0x01, 0x01, 0x7f // 50 P П
,0x66, 0x19, 0x09, 0x09, 0x7f // 51 Q Я
,0x7f, 0x09, 0x09, 0x09, 0x06 // 52 R Р
,0x3e, 0x41, 0x41, 0x41, 0x22 // 53 S С
,0x01, 0x01, 0x7f, 0x01, 0x01 // 54 T Т
,0x47, 0x28, 0x10, 0x08, 0x07 // 55 U У
,0x77, 0x08, 0x7f, 0x08, 0x77 // 56 V Ж
,0x7f, 0x49, 0x49, 0x49, 0x36 // 57 W В
,0x7c, 0x21, 0x12, 0x08, 0x7c // 58 X ѝ
,0x01, 0x7f, 0x48, 0x48, 0x30 // 59 Y Ъ
,0x21, 0x41, 0x45, 0x4b, 0x31 // 5a Z З
,0x7c, 0x40, 0x7c, 0x40, 0x7c // 5b [ ш
,0x7c, 0x10, 0x38, 0x44, 0x38 // 5c \ ю
,0x7c, 0x40, 0x7c, 0x40, 0xfc // 5d ] щ
,0x04, 0x02, 0x01, 0x02, 0x04 // 5e ^
,0x40, 0x40, 0x40, 0x40, 0x40 // 5f _
,0x1c, 0x10, 0x10, 0x7c, 0x00 // 60 ` ч
,0x20, 0x54, 0x54, 0x54, 0x78 // 61 a а
,0x3e, 0x49, 0x44, 0x44, 0x38 // 62 b б
,0x7c, 0x40, 0x40, 0x7c, 0xc0 // 63 c ц
,0xc0, 0x7c, 0x44, 0x7c, 0xc0 // 64 d д
,0x38, 0x54, 0x54, 0x54, 0x18 // 65 e е
,0x30, 0x48, 0xfe, 0x48, 0x30 // 66 f ф
,0x7c, 0x04, 0x04, 0x04, 0x00 // 67 g г
,0x44, 0x28, 0x10, 0x28, 0x44 // 68 h х
,0x7c, 0x20, 0x10, 0x08, 0x7c // 69 i и
,0x7c, 0x20, 0x12, 0x09, 0x7c // 6a j й
,0x7c, 0x10, 0x28, 0x44, 0x00 // 6b k к
,0x40, 0x3c, 0x04, 0x04, 0x7c // 6c l л
,0x7c, 0x08, 0x10, 0x08, 0x7c // 6d m м
,0x7c, 0x10, 0x10, 0x10, 0x7c // 6e n н
,0x38, 0x44, 0x44, 0x44, 0x38 // 6f o о
,0x7c, 0x04, 0x04, 0x04, 0x7c // 70 p п
,0x48, 0x34, 0x14, 0x14, 0x7c // 71 q я
,0x7c, 0x14, 0x14, 0x14, 0x08 // 72 r р
,0x38, 0x44, 0x44, 0x44, 0x20 // 73 s с
,0x04, 0x04, 0x7c, 0x04, 0x04 // 74 t т
,0x0c, 0x50, 0x50, 0x50, 0x3c // 75 u у
,0x6c, 0x10, 0x7c, 0x10, 0x6c // 76 v ж
,0x7c, 0x54, 0x54, 0x54, 0x28 // 77 w в
,0x7c, 0x50, 0x50, 0x50, 0x20 // 78 x ь
,0x04, 0x7c, 0x50, 0x50, 0x20 // 79 y ъ
,0x28, 0x44, 0x44, 0x54, 0x28 // 7a z з
,0x7f, 0x40, 0x7f, 0x40, 0x7f // 7b { Ш
,0x7f, 0x08, 0x3e, 0x41, 0x3e // 7c | Ю
,0x7f, 0x40, 0x7f, 0x40, 0xff // 7d } Щ
,0x07, 0x08, 0x08, 0x08, 0x7f // 7e ← Ч
,0x30, 0x3e, 0x7f, 0x3e, 0x30 // 7f → "a bell-like icon"

void LcdWriteCharacter(char character)
  LcdWrite(LCD_D, 0x00);
  for (int index = 0; index < 5; index++) {
    LcdWrite(LCD_D, pgm_read_byte_near(ASCII + (character - 0x20)*5 + index));
  LcdWrite(LCD_D, 0x00);