Thursday, January 17, 2008

A Simple Binary Clock Project

In about April of 2004 I started this binary clock project which was inspired by the Think Geek binary clock. I put the project on hold a number of times and finally finished it around October of 20032004. When I started the project the TG clock was only available with red LEDs, and while it was definitely pretty cool, there were a number of things about it that bothered me. Obviously it needed blue LEDs, which are all the rage for the modern appliances. It also displays the time in 6 binary registers, one each for the 6 decimal digits of a digital clock. While this arrangement generates some pretty gnarly patterns, and is probably why it was chosen, it seemed very un-geekish to me. Lastly, and most importantly, since one of the guys I work with already had a red TG binary clock on his desk, if I was to have one, it couldn't be the same design, and it would have to somehow out-geek that other clock.

Clearly I would have to create my own binary clock from scratch to meet my requirements. As the only 'crossover geek' in the office (programming and hobby-level digital electronics), I could meet the primary goal of out-geeking the existing clock with my own AVR microcontroller based design.

Since presentation is important as well I choose to use a dead harddrive as the case for the clock. This presented some
interesting challenges, as there isn't really much room inside a hard drive. I considered using surface mount parts as I have
with other projects, but eventually decided against it. I don't like making PCBs for SMT parts as I don't have a well refined technique for etching boards, the fine features associated with SMT parts don't come out well and sometimes cause frustration and SMT parts don't fit well on the perf board I thought would look suitably homemade. While I could have a PCB made for the surface mount parts, it could end up looking more professional than I wanted. I wanted this thing be reasonably good looking, but I also wanted it to look home-built.

Rather than use 6 binary registers for a 6 digit display, I choose to go with two 6 bit registers, one each for seconds and minutes, and one 5 bit register for hours (I prefer the 24 hour time format, so support for that was required). This loses some cool points when it comes to the fascinating patterns that the Think Geek clock generates, but I think it is easier to read, more elegant, and, importantly, different.

I briefly considered keeping the HD platters on the motor and mounting some SMT LEDs so I could create a radial matrix display, but the difficulty of working with the moving parts required for that design added a level of commitment that I wasn't prepared to deal with (it took about 6 months to finally get it done as it was!). I still think it's a cool idea, and I've seen a number of such 'propeller' or 'persistence of vision' as they are called now, clocks. Perhaps I'll do something like it in the future. Since I made this clock 'persistence of vision' displays have become pretty common (see MAKE magazine), so doing it now shouldn't be too difficult.

I decided to use the Atmel AT90S2313 for this project. It is a 20 pin, 8 bit, 2kb flash-based RISC controller with built-in
UART. It's small, and has plenty of I/O for this task, and I've got a pile of them that aren't doing anything but taking up space and taunting me for not using them. The code is written in C, and compiled with the AVRGCC compiler. Since I do not use C I'm sure the quality of the code is abysmal. I referred to the K&R book quite often while writing the code you see below. It's been so long since I wrote it that I really don't recall what the heck I was thinking when I invented it, and most of the coding sessions where late-night Git'er Done sessions, so it is highly likely that there is some pretty whacked out code in there. Normally I'd take a lot more care in designing it, but by the time I got to the software I just wanted to get the darn thing finished.

The 3 registers are arranged around the platter in two horizontal and one vertical row. The seconds register is laid out horizontally at the bottom of the platter with the LSB on the right, minutes is vertically oriented on the right, LSB at the bottom, and hours is parallel to seconds at the top. Since the hours register is one bit shorter, the right-most position is not drilled out. I had actually intended to leave the left-most position undrilled, but I'm a goober and soldered the LEDs in wrong. After the difficulty I had wiring them up to the right angle headers with magnet wire (a horrible choice of material, I should have waited and picked up some more suitable wire at the local electronics supply store, but I was overeager to get it running. That decision cost me many hours of tedious wiring and troubleshooting), I wasn't inclined to change them.



I built three small boards to go on the back of the HDD case, onto which the LEDs are mounted, one board for each register. The boards contain 5 or 6 LEDs, an 6 pin single row 90 degree header, and a darlington PNP transistor and base resistor. The header pins are for power, enable and 5 or 6 LED ground wires. The PNP is controlled by the enable line, which is connected to the base via the resistor. When on it provides current to the LEDs. The ground lines are switched on the main board by a row of NPN transistors to turn on or off each LED.

I didn't have the Taig CNC milling machine at the time, so I used a hand drill and some files to make the openings in the back of the disk drive. The aluminum used for the case is very soft and cuts beautifully, if the mill had been available I would have considered doing some more interesting work on the exposed metal areas. I also would have etched something onto the platters I used for the face (I also would have had neatly aligned holes for the LEDs instead of the slightly wonky holes it has now).



The cables from the three boards run to a concentration point, also on the back of the drive. This wires together all the LED
ground wires and power, and brings the three column enable lines into the cable which runs through the existing access hole in the back of the clock to connect to the main board. I liberally applied hot glue to keep everything in place.




On the main board is a row of NPN transistors and their resistors, 3 buttons for setting the time, and the microcontroller itself, along with a few supporting parts. The NPN's are the other side of the control of the current to the LEDs, they work with the PNP's on the LED boards to control the current to each individual LED. There are other ways to control banked arrays of LEDs that can save IO lines, but you don't save much with this small number of LEDs, and I didn't have any shortage of IO lines.

To provide a nice look, I was pretty set against showing the tops of the LEDs. While the lenses are water clear, they will
always look like LEDs, which, while certainty geeky, isn't very pretty. I located some 1/4 inch clear lexan rod that I found
would carry the light very nicely, like an optical fiber. I cut it into pieces about 3/8ths of an inch long and frosted both
ends (chuck the rod into the drill and apply fine sandpaper) to diffuse the light from the LEDs. By mounting the bits of rod with about an 1/8th inch sticking out above the polished drive platter, I got a very nice modern look. The frosted end of the rod glows bright blue, but the sides are clear, with very little light leakage. The effect is of an intense blue disk hovering over the mirrored surface.

I dislike the very abrupt on-off nature of the Think Geek binary clock LEDs, the sudden changes catch my attention when the clock is at the edge of my field of view. It was apparent that this would not be acceptable for something that would be sitting on my desk all day. I used an ugly hack in the software to adjust the pulse width of the signal sent to each LED that was changing states. Each transition has a 1/4 second linear ramp to full on or off, which makes it look much more serene, and eliminates the snap of the TG clock.

Unfortunately, I've set the scan rate such that under just the right conditions I can see the flicker of the LEDs when they
are ramping on or off, but its very minor. Someone with very 'fast' vision might be more irritated by it. I can see and am annoyed by CRTs set for a 60Hz refresh rate, and the flicker of fluorescent lights and movie screens often bothers
me but not most others, so perhaps I'm just picky.

Overall the clock turned out pretty nice, although it has lots of room for improvement in all aspects. The software could be
much more elegant and extensible, the hardware has a number of dumb design elements, and the fit and finish could use plenty of work. In particular the hardware needs a microprocessor supervisor chip and battery backup, and it should be using a crystal that lends itself to more accurate timekeeping. The 4MHz crystal I used does the job acceptably, but choosing constant values for calculating the time in the software is dodgy. It also drifts with the temperature more than it ought to, a more stable crystal would be nice.

If I do another version I will include a Real Time Clock chip to handle the timekeeping, including battery backup. I can then
use the microcontroller to run a speaker for alarms and timers (1 hour 'lunch timer' and 5 minute tea timers would be handy, as well as an 'its 5pm, go home' notifier). I'd also like to include USB support to allow the clock to draw power and accurate time sync data from the computer.

The code is under the GPL, and the hardware is too trivial to be anything but public domain. Feel free to use the software under the terms of the GPL, and do whatever you wish with the hardware designs.

You can see video of the clock. The frame rate and shutter speed make the LEDs look like they pulsate a bit in the video. In person the fade is smooth and seamless.

Here is the code for the project:
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>
#include <avr/delay.h>

#define F_CPU 4000000 /* 4Mhz */
#define UART_BAUD_RATE 9600 /* 9600 baud */

#define COLUMN_DWELL 5000

#define BTN_SET_TIME _BV(4)
#define BTN_SET_HOURS _BV(3)
#define BTN_SET_MINUTES _BV(2)

#define UART_BAUD_SELECT (F_CPU/(UART_BAUD_RATE*16l)-1)

typedef
unsigned char u08;
typedef
char s08;

typedef
unsigned short u16;
typedef
short s16;

/* uart globals */

static volatile
u08 *uart_data_ptr;
static volatile
u08 uart_counter;
static volatile
u08 received_byte;

static volatile
u08 seconds;
static volatile
u08 minutes;
static volatile
u08 hours;
static volatile
u08 old_seconds;

static volatile
u08 old_minutes;
static volatile
u08 old_hours;
static volatile
u16 fade_step;

static volatile
u08 btn_inc_hours; // hour set button pressed, inc hour value

static volatile u08 btn_inc_minutes; // min set button pressed, inc min value

enum
{ SECONDS, MINUTES, HOURS };

void
uart_send_byte( u08 val ){
uart_counter = 1;

UDR = val;

for
(;;){
if
(uart_counter == 0)
break
;
}
}


SIGNAL(SIG_UART_TRANS)
/* signal handler for uart txd ready interrupt */

{

uart_data_ptr++;

if
(--uart_counter)
UDR = *uart_data_ptr;
}


SIGNAL(SIG_UART_RECV){
/* signal handler for receive complete interrupt */

received_byte = UDR;
}


void
inc_time(u08 time_type){

switch
(time_type){
case
SECONDS:
if
(++seconds > 59) seconds = 0;
break
;

case MINUTES:
if
(++minutes > 59) minutes = 0;
break
;

case HOURS:
if
(++hours > 23) hours = 0;
break
;
}
}



SIGNAL(SIG_OUTPUT_COMPARE1A){
// Don't update the time if the set time button is down
if ((~PIND & BTN_SET_TIME) == BTN_SET_TIME) return;

// Store the old values for cross-fading
old_seconds = seconds;
old_minutes = minutes;
old_hours = hours;

// 4992 is close to COLUMN_DWELL and allows for 13 integral steps
fade_step = 4992;

// overflow each into the next
inc_time(SECONDS);
if
(!seconds) inc_time(MINUTES);
if
(!minutes && !seconds) inc_time(HOURS);
}



void
uart_send(u08 *buf, u08 size){
/* send buffer &lt;buf&gt; to uart */


if
(!uart_counter) {
/* write first byte to data buffer */

uart_data_ptr = buf;
uart_counter = size;
UDR = *buf;
}
}



// init system
void do_init( void ){
// variable initialization
seconds = 1;
minutes = 2;
hours = 4;

// Turn all the LEDs off
DDRB = 0xff; // PortB output
PORTB = 0xff; // outputs off
DDRD = _BV(5); // bit 5 on
PORTD = 0xdf;

// COM port can be useful for debugging
// Enable serial transmit and receive at 9600/8/1/n
UCR = _BV(RXCIE) | _BV(TXCIE) | _BV(RXEN) | _BV(TXEN); // enable RxD/TxD and ints
UBRR = (u08)UART_BAUD_SELECT; // Set baud rate

// Enable Timer/Counter 1 with a 256 prescale
TIMSK = _BV(OCIE1A); // enable TCNT0 overflow
OCR1 = 15623; // 15603 ticks with a 256 prescale on 4Mhz clock makes 1 second
TCCR1A = 0; // disable the output pin (PB3)
TCCR1B = _BV( CTC1 ) | _BV( CS12 ); // clear on compare match, 256 prescale

}

void display( u08 out_type, u08 value ){

// all the values are masked by 0x3F
u08 port_value = value | 0xC0;

// turn off all cols
PORTB = 0xC0;
PORTD = _BV(5);

// turn on the correct column
switch (out_type){
case
SECONDS:
port_value &= ~0x80;
break
;

case MINUTES:
port_value &= ~0x40;
break
;

case
HOURS: // Note, this would be cleaner if used portd for all cols
PORTD &= ~_BV(5); // hours is displayed via a different port
break;
}


// Display the value
PORTB = port_value;
}


void
display_fade( u08 out_type, u08 old_value, u08 value ){

if
(fade_step>312) {
display( out_type, old_value );
_delay_loop_2( fade_step );
}

display( out_type, value );
_delay_loop_2( COLUMN_DWELL-fade_step );
}


int
main(void)
{


do_init(); // initialze stuff
sei(); // enable interrupts
for (;;) { // loop forever
// combined state of three buttons used to set time
switch (~PIND & (BTN_SET_TIME | BTN_SET_HOURS | BTN_SET_MINUTES)){

// just timeset down
case (BTN_SET_TIME):
btn_inc_minutes = 0;
btn_inc_hours = 0;
break
;

// timeset and minutes down
case (BTN_SET_TIME | BTN_SET_MINUTES):
// immediate response to button press and auto inc on hold
if ((btn_inc_minutes == 0)|(++btn_inc_minutes == 20)){
inc_time(MINUTES);
btn_inc_minutes = 1; // don't reset to zero for auto inc
seconds = 0;
}

break
;

// timeset and hours down
case (BTN_SET_TIME | BTN_SET_HOURS):
// immediate response to button press and auto inc on hold
if ((btn_inc_hours == 0)|(++btn_inc_hours == 20)){
inc_time(HOURS);
btn_inc_hours = 1; // don't reset to zero for auto inc
seconds = 0;
}

break
;
}
// switch

// Display the time
display_fade( SECONDS, old_seconds, seconds );
display_fade( MINUTES, old_minutes, minutes );
display_fade( HOURS, old_hours, hours );
if
(fade_step > 312) fade_step-= 312;

}
// for
} // main
Post a Comment