Wednesday, January 2, 2008

Your Favorite Geek Desk Toy Sucks

I've seen these binary clocks around the office. They are fairly cool, but they fail to please in one important way: Each decimal digit is represented separately. 14 seconds is represented as a binary 1 and a binary 4, rather than a binary 14. Granted, reading 6 bits depicting 0-59 seconds is a little harder than the 4 bits required for 0-9. But is ease of use really the primary concern of a binary clock?

MISSION: Build this clock the right way. Namely, 6 bits for the seconds, 6 bits for the minutes and 5 bits for the hour (or maybe 4 bits for the hour with 1 bit for AM/PM). (Another idea would be a straight-up 17 bits for the 86400 seconds in a day, but seriously.)

Now then. I know there are clock chips out there. And it is probably possible to do this using hardware only, say with a 555. But I'm a dumb programmer, so everything looks like a Turing-complete problem to me. Plus I already have an Arduino. So that's the medium of choice. Using an Arduino, some LEDs and resistors and pure force of will, I'm going to make this work.

But there's a problem already. My design calls for 17 LEDs. The Arduino only has 14 output ports1, 2 of which I can't use because they are special. The solution to this problem is multiplexing. The basic idea is that you use X/Y coordinates to address each LED, Battleship-style. So for MxN LEDs, you only need M+N ports.

Let's say I want to light up the LED labeled 0,0. I need to apply positive voltage to A (the left column) and negative to 1 (the bottom row). B and C should be low while 1 and 2 should be high to "push the wrong way" against the remaining diodes.

But now there's another problem. Let's say I want to light up both 0,0 and 2,2. I apply positive to A and C and negative to 1 and 3...and I get all four corners lit up. Long story short, it is also necessary to employ a spot of subterfuge. If I want 0,0 and 2,2 lit up, I have to do them one at a time, but switch back and forth so fast nobody's the wiser.

And finally, there's the little matter of the clock function itself. There isn't an API call for that exactly, but the underlying chip supports interrupts. I basically just copyandpasted the timer code from elsewhere and then added a long comment explaining it to myself, probably incorrectly.

Grainy video (the ticking is an amazingly coincidental loud clock in the same room):

Somewhat less grainy still shot:

The code:

#include <avr/interrupt.h>
#include <avr/io.h>

#define NUMPOS 6
#define NUMNEG 3

int pos[NUMPOS] = {9,8,7,6,5,4};
int neg[NUMNEG] = {12,11,10};

int i = 0;
int j = 0;
int k = 0;
int lastpos = 0;
int lastneg = 0;

int isr_counter = 0;
int oldsecond = 0;
volatile int second = 0;
int seconds = 0;
int minutes = 31;
int hours = 13;

/*  
   Here's how I think this works.  The Atmega168 clock runs at 16MHz.  
   The "prescaler" divides that down.  In this case, it clicks at 2MHz.  
   Each time it clicks, it increments at 8 bit register by 1.  The register
   overflows after 256 clicks.  That overflow is the interrupt we receive.

   2000000 clicks/second divided by 256 clicks/overflow = 7812.5 overflows/second.
   So if I could count 7812.5 overflows, I know a second has elapsed.  I can't
   find .5 of an overflow, so I should really use the /4 prescaler.  But 
   a) that uses more power and b) I can't figure out what bits to set to do that.  
*/

ISR(TIMER2_OVF_vect) {
  isr_counter++;
  if (isr_counter > 7811) {
    second++;
    isr_counter = 0;
  }
};  

void SetupTimer2(){
  //Timer2 Settings: Timer Prescaler /8, mode 0
  //Timer clock = 16MHz/8 = 2Mhz or 0.5us
  //The /8 prescale gives us a good range to work with
  //so we just hard code this for now.
  TCCR2A = 0;
  TCCR2B = 0<<CS22 | 1<<CS21 | 0<<CS20;

  //Timer2 Overflow Interrupt Enable
  TIMSK2 = 1<<TOIE2;

  //load the timer
  TCNT2=0;
}

void setup() {
  for(i = 0; i<NUMPOS; i++) {
    pinMode(pos[i], OUTPUT);
  }
  for(i = 0; i<NUMNEG; i++) {
    pinMode(neg[i], OUTPUT);
  }

  for(i = 0; i<NUMPOS; i++) {
    digitalWrite(pos[i],LOW);
  }
  for(i = 0; i<NUMNEG; i++) {
    digitalWrite(neg[i],HIGH);
  }
  
  Serial.begin(9600);
  SetupTimer2();
}

void showXY(int col, int row) {
  digitalWrite(pos[lastpos],LOW);
  digitalWrite(neg[lastneg],HIGH);

  digitalWrite(pos[row],HIGH);
  digitalWrite(neg[col],LOW);
  
  lastpos = row;
  lastneg = col;
}

void loop() {
  // time changed, so readjust all the details
  if (oldsecond != second) {
    if (second > 59) {
      second = 0;
      minutes++;
      if (minutes > 59) {
        minutes = 0;
        hours++;
        if (hours > 23) {
         hours = 0;
        }
      }
    }          
    seconds = second;
    oldsecond = second;
  }

  // seconds is column 2 and has 6 bits
  k = 2;
  for(j=0; j<6; j++) {
    if (seconds >> j & 1) {
      showXY(k,j);
    }
  }

  // minutes is column 1 and has 6 bits
  k = 1;
  for(j=0; j<6; j++) {
    if (minutes >> j & 1) {
      showXY(k,j);
    }
  }

  // hours is column 0 and has 5 bits
  k = 0;
  for(j=0; j<5; j++) {
    if (hours >> j & 1) {
      showXY(k,j);
    }
  }

}
1Possibly not true. I found one post that said the 6 analog in ports could be used as digital out. But even if multiplexing isn't strictly necessary for this project, it would be for a larger one.

6 comments:

  1. It is truly awesome.

    ReplyDelete
  2. The electronic clock industry will think twice before tangling with me again, now that I've exposed their lies.

    ReplyDelete
  3. Whoa! Not too shabby.

    For the record, the favorite geek desk toy does have a "true binary" mode where seconds, minutes, and hours are represented as you represent them . . . except sideways.

    But making your own is more, uh, impressive than figuring out how to switch modes on the toy. I guess.

    ReplyDelete
  4. I wondered about that when I found this description: Startup option allows user to display time in 'True' binary mode (using the binary coding of 32/16/8/4/2/1)

    "'True' binary mode" sounds good, but I couldn't see how you could display it. And then the 32/16 thing is confusing. So I wasn't sure if that was just marketing double (binary) talk.

    ReplyDelete
  5. Here's the dumb thing about the "true binary" mode (other than the fact that they call it "true binary" as opposed to the "powers of two" mode): Wasted LEDs, since displaying each digit individually needs more bits than displaying H/M/S. When certain LEDs are never used, the display is less elegant, which makes it harder to understand (I think) to someone who hasn't seen it before.

    So yeah. Totally needed to be hacked.

    ReplyDelete
  6. We can't figure out how to put J's into "true binary" mode. Since it's a "startup option", I'm guessing you hold some buttons down when you turn it on...?

    OK, we found it. That isn't too bad, really. Just 3 unused LEDs.

    Either way, though, right: Hack it. Also, now I know how to multiplex. I'm thinking LED cube next.

    ReplyDelete