Sunday, July 8, 2012

Whiteboard Clock


Introduction

I have been working on a calculator for my girlfriend, but unforeseen complexity has caused the project to stretch out over months rather than weeks. In place of finishing the calculator, I came up with a more simple, more useful project (birthday present) which has the potential to be profitable if the design is tweaked to be easier to manufacture. This post contains (1) a video of the finished product (2) followed by background information (3) followed by a gallery detailing each step of the build process and (4) finally the Arduino code behind the functionality.


Lizz's Whiteboard Clock


Background

Each led requires a high signal to turn on. This means that I need a total of 72 separate signals to turn on each led individually. However an Arduino only has 14 digital output. If we consider using pins in combinations there are a few methods of controlling large number of signals using a small number of I/O ports.

  1. Multiplexing: This allows a microprocessor to control 2^n signals with n outputs. These are used heavily when building custom hardware (verilog, VHDL, etc.) but there are not large IC chips available. I would have to string multiple smaller DEMUX chips together to achieve a 7 bit DEMUX.
  2. Charlieplexing: This allows a microprocessor to control n(n-1) signals with n outputs. Charlieplexing relies on the fact that LED are directional i.e. they will only turn on when a voltage is applied to then in the correct direction. This means that we can have an LED hooked up to pin 1 and 2 and another LED hooked to pin 2 and 1 while turning them on or off independently.
Both of these methods allows for a single LED to be turning on at any given time. However, for a clock we need at most 2 or 3 LEDs turned on at once (hours, minutes, seconds). We can accomplish this using persistence of vision. Most video on the internet is only 30 frames per second (updates 30 times a second) because the human eye cannot perceive changes faster than that. This means that if we update the clock 30 times per second while cycling through hours, minutes and seconds, then we can make each LED appear to be on simultaneous to the other two. There are specialized chips which do this (MAX7221) for 7-segment display and LED dot-matrices.


The Build


The Code

LizzClock.h

#ifndef LIZZCLOCK_h
#define LIZZCLOCK_h


typedef struct LED {
  char high;
  char low;
} LED;

typedef struct BUTTON {
  long down;
  char set;
  char PIN;
} BUTTON;


typedef struct TIME {
  char hours;
  char minutes;
  char seconds;
} TIME;

#endif

LizzClock.ino

#include "LizzClock.h"
#include <TimerOne.h>

#define TRUE    1
#define FALSE   0

// Display Constants
#define OFFSET  2

LED lights[72];
LED* curr_led = 0;
char PAUSE = 50;
char BRIGHTNESS = 0;
int UPDATE_COUNT (100/PAUSE);
int ON_DELAY = PAUSE;
int OFF_DELAY = 0;

TIME *curr_time;
long start_time;

BUTTON *minutes_button;
BUTTON *hours_button;

void setup() {
  /*
   * Initalize current time and update interrupt
   */
  start_time = millis();
  curr_time = (TIME*) malloc(sizeof(TIME));
  curr_time->seconds = 0;
  curr_time->minutes = 0;
  curr_time->hours = 0;
  Timer1.initialize(100000);
  Timer1.attachInterrupt(update_time);

  /*
   * Initalize buttons
   */
  minutes_button = (BUTTON*)malloc(sizeof(BUTTON));
  minutes_button->down = 0;
  minutes_button->PIN = 12;
  pinMode(minutes_button->PIN, INPUT);
  hours_button = (BUTTON*)malloc(sizeof(BUTTON));
  hours_button->down = 0;
  hours_button->PIN = 13;
  pinMode(hours_button->PIN, INPUT);

  /*
   * Initalize display
   */
  set_brightness(30);  

  /*
   * MINUTES/SECONDS
   */
  init_LED( 0, 0, 1); // 0  and 1
  init_LED( 2, 0, 2); // 2  and 3 
  init_LED( 4, 1, 2); // 4  and 5
  init_LED( 6, 0, 3); // 6  and 7
  init_LED( 8, 1, 3); // 8  and 9
  init_LED(10, 2, 3); // 10 and 11
  init_LED(12, 0, 4); // 12 and 13
  init_LED(14, 1, 4); // 14 and 15
  init_LED(16, 2, 4); // 16 and 17
  init_LED(18, 3, 4); // 18 and 19
  init_LED(20, 0, 5); // 20 and 21
  init_LED(22, 1, 5); // 22 and 23
  init_LED(24, 2, 5); // 24 and 25
  init_LED(26, 3, 5); // 26 and 27
  init_LED(28, 4, 5); // 28 and 29
  init_LED(30, 0, 6); // 30 and 31
  init_LED(32, 1, 6); // 32 and 33
  init_LED(34, 2, 6); // 34 and 35
  init_LED(36, 3, 6); // 36 and 37
  init_LED(38, 4, 6); // 38 and 39
  init_LED(40, 5, 6); // 40 and 41
  init_LED(42, 0, 7); // 42 and 43
  init_LED(44, 1, 7); // 44 and 45
  init_LED(46, 2, 7); // 46 and 47
  init_LED(48, 3, 7); // 48 and 49
  init_LED(50, 4, 7); // 50 and 51
  init_LED(52, 5, 7); // 52 and 53
  init_LED(54, 6, 7); // 54 and 55
  init_LED(56, 0, 8); // 56 and 57
  init_LED(58, 1, 8); // 58 and 59

  /*
   * HOURS
   */
  init_LED(60, 2, 8); // 12 and 1
  init_LED(62, 3, 8); // 2  and 3
  init_LED(64, 4, 8); // 4  and 5
  init_LED(66, 5, 8); // 6  and 7
  init_LED(68, 6, 8); // 8  and 9
  init_LED(70, 7, 8); // 10 and 11
}

/*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
 *      [[  DISPLAY INIT START  ]]
 */

char set_brightness(int new_brightness) {
  if (new_brightness >= 0 && new_brightness <= PAUSE) {
    BRIGHTNESS = new_brightness;
    ON_DELAY = (PAUSE > BRIGHTNESS ? PAUSE - BRIGHTNESS : 0);
    OFF_DELAY = (PAUSE < BRIGHTNESS ? PAUSE : BRIGHTNESS);
    return TRUE;
  }
  return FALSE;
}

void init_LED(char index, char high, char low) {
  lights[index+1].high = high;
  lights[index+1].low = low;
  lights[index].high = low;
  lights[index].low = high;
}
//      [[  DISPLAY INIT END  ]]
//////////////////////////////////////////////////////////


/*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
 *      [[  EXECUTION START  ]]
 */
void loop() {
  // update_time called on timed interrupt 10 times a second
  display_time(curr_time);
  check_user_set_time();
}
//      [[  EXECUTION END  ]]
//////////////////////////////////////////////////////////


/*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
 *      [[  MAINTAIN DISPLAY START  ]]
 */
LED* get_hour(char hour) {
  if (hour <= 0 || hour >= 12)
    return &lights[60];
  return &lights[hour + 60];
}

void all_off()
{
  for(int i = 2; i < 12; i++) {
    pinMode(i, INPUT);
    digitalWrite(i, LOW);
  }
}

char get_pin(char index) {
  return index + OFFSET;
}

void display_time(TIME *time) {
  lightup(&lights[time->seconds]);
  lightup(&lights[time->minutes]);
  lightup(get_hour(time->hours));
}

void lightup(LED* led) {
  // blank display and pause to control brightness
  all_off();
  delayMicroseconds(OFF_DELAY);
  //delayMicroseconds();

  // turn on display for short time to allow for persistance of vision.
  pinMode(get_pin(led->high), OUTPUT);
  digitalWrite(get_pin(led->high), HIGH);
  pinMode(get_pin(led->low), OUTPUT);
  digitalWrite(get_pin(led->low), LOW);
  curr_led = led;
  delayMicroseconds(ON_DELAY);
}
//      [[  MAINTAIN DISPLAY END  ]]
//////////////////////////////////////////////////////////


/*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
 *    [[  MAINTAIN CLOCK START  ]]
 */
void update_time() {
  long temp = (millis() - start_time) / 1000.0;
  //long temp = (millis() - start_time) / 3;
  curr_time->seconds = temp % 60;
  temp /= 60.0;
  curr_time->minutes = temp % 60;
  curr_time->hours = temp / 60.0;
  if (curr_time->hours == 12) {
    curr_time->hours = 0;
    // assume time is updated more than once a second.
    start_time = millis() - (curr_time->seconds * 1000);
    //start_time = millis() - (curr_time->seconds * 3);
  }
}

void check_user_set_time() {
  if (digitalRead(minutes_button->PIN)) {
    // if button down
    if (minutes_button->down) {
      // if currently debouncing
      if (!minutes_button->set && millis() - minutes_button->down > 20) {
        // if debounced long enough
        start_time -= 60000; // seconds * milliseconds
        minutes_button->set = 1;
      }
    } 
    else {
      // start debouncing
      minutes_button->down = millis() ^ 1;
    }
  } 
  else if (minutes_button->down) {
    minutes_button->down = 0;
    minutes_button->set = 0;
  }
  if (digitalRead(hours_button->PIN)) {
    // if button down
    if (hours_button->down) {
      // if currently debouncing
      if (!hours_button->set && millis() - hours_button->down > 20) {
        // if debounced long enough
        start_time -= 3600000; // minutes * seconds * milliseconds
        hours_button->set = 1;
      }
    } 
    else {
      // start debouncing
      hours_button->down = millis() ^ 1;
    }
  } 
  else if (hours_button->down) {
    hours_button->down = 0;
    hours_button->set = 0;
  }
}
//      [[  MAINTAIN CLOCK END  ]]
//////////////////////////////////////////////////////////

No comments:

Post a Comment