Language Switcher Fallback

Binary clock (ATtiny85, shift registers)

Here is my schematic and code for a very basic binary coded decimal clock.

I took up this project after seeing few binary clocks on Instructables.com as fun way to introduce myself to shift registers and Arduino interrupts. This totally paid off. Also it was a nice challenge in coding at the time (two years back, I think). I did not do much research about coding clocks, so there might be funny things in it.

It ended up as ATtiny project with the method described at highlowtech.org that lets you turn an Arduino into a programmer device for some ATtiny controllers.

What is a binary clock?

This particular kind of binary clock uses a mix of decimal and binary notation. It puts out the time so that first column of LEDs from the left represents tens of hours, next one ones of hours, then tens of minutes, ones of minutes, tens of seconds, ones of seconds. Each column is a binary notation with the bottom LED being the least significant bit or smallest number ... I'll explain in a moment. Lit LED means 1, unlit means 0. Now the least significant bit thing means that when only the bottom LED is lit, the value is 0001 or simply 1 which clearly equals 1 in decimal system also. When two bottom LEDs are lit, the value is 0011 or 11 which equals 3 in decimal. And so on. But I guess wikipedia is better at explaining it.

Electronics

The clock uses two 74HC164N shift registers for driving the LEDs through transistors. It's a POV (persistence of vision) device. Meaning that only one column of LEDs is lit at a time and they are switched fast. One shift register outputs the value that needs to be output to a column and the other selects the currently active column. ATtiny85 has 5 I/O pins (6 if you have access to high voltage programmer device and are willing to sacrifice reset pin). Each shift register needs two pins so I had to make funny voltage divider and analogRead to attach two buttons to the last pin left. I also made a small library to take care of that ... but then found out that it does not work (because of millis() ) on this specific project. Well, I made a modification and crammed it all in the code of this sketch.

There is no external crystal, the ATtiny's internal clock is used. It does not come super calibrated. Therefore the idea is to observe how much the clock goes off per x amount of time and tweak the number of interrupts that is counted as a second. The default is 2500 because that's the Hz value of the interrupts, but I can make it count up to 2292 instead and call that a second.

I used an old 7.5V phone charger and 7805 voltage regulator to power the thing because that amount of LEDs can drain a battery too fast. The downside is that if there is a power outage the timekeeping is lost. On the schematic I drew a pullup resistor and button on the reset pin. It's ought to be there, but on my pictures and in reality it's not - for some funny reason the floating reset does not get randomly triggered unless I touch it. I just cut off the power to reset the thing when I want to set time.

Code

When the ATtiny resets it starts to wait for button input to set the clock. Pressing button1 increases the value of minutes by one. When button2 is pressed first time then following presses on button1 will start to increase hours. When button2 is pressed second time it ends the time setting and starts the clock. To adjust the time one has to reset the controller and start setting from beginning. It's very basic as I said.

Counting and outputting value on the LED POV screen is handeled with timer interrupts. ATtiny has them slightly different than Arduino, but very similar. A good tutorial on timer interrupts is this instructable. I actually first coded the thing on Arduino because you can troubleshoot it with serial monitor and once I got working code, converted it for ATtiny85. (But I have made some important fixes in the AtTiny code only, so the Arduino version is not up to date enough to post that one.)

Interacting with shift registers is done with direct port manipulation because digitalRead is too slow compound function, it caused some trouble.

Specifically what happened with my AnalogDebounced library is that found out I can't use millis() while ATtiny85's timer0 is used for interrupts. The solution was a modification where instead of comparing millis() increase I count some number of loops that read same button value. You can see the result in the code below, near the beginning of sketch there is a class that does it.

And here's the code for those who are looking for some tiny piece of example on something.


/*******************************************************************************
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*******************************************************************************/

/*
ATiny85 pinout        

1 reset
2 pin 3 analog in 3           columnout
3 pin 4 analog in 2           button1 (5V), button2 (2,5V)
4 GND                
5 pin 0 PWM                   valueclock
6 pin 1 PWM                   valueout
7 pin 2 analog in 1           columnclock
8 Vcc                
*/


#define valueout 1                        //physical pin 6
#define valueclock 0                      //physical pin 5
#define columnout 3                       //physical pin 2
#define columnclock 2                     //physical pin 7


/*Because the timer0 is used for interrupts, millis() does not work.
Here I made a class that uses counting of loops for debouncing insted of time difference*/

class AnalogDebouncedC {
  private:
    int pin;
    int buttons;
    int dbDelay;
    int mappings[4];
    int lastDbState;
    int lastDbCount;
    int value;
    int i;
    
  public:
    AnalogDebouncedC(int pinNumber, int debounceCounter, int numberOfButtons, int *valueMappings) {
      pin = pinNumber;
      buttons = numberOfButtons;
      dbDelay = debounceCounter;
      for (i=0; i<buttons; i++) {
        mappings[i*2] = valueMappings[i]-25;
        mappings[i*2+1] = valueMappings[i]+25;
      }
      lastDbState = readMapped(pin);
      value = lastDbState;
      lastDbCount = 0;    
    }

    int dbRead() {
      int dbState = readMapped(pin);
      if (dbState != lastDbState) {// if reading changes
        lastDbCount = 0;     //      we reset timer/counter
      }
      lastDbCount++;        //count same value reading cycles
      lastDbState = dbState;       //save current reading for next loop
      if (lastDbCount >= dbDelay) {  //if reading has been constant for long enough
        value = dbState;                        //    we assign reading as new value
      }
      return value; //return value
    }
     
  private:

    int readMapped(int pin) {
      int reading = analogRead(pin);
      for (i=0; i<buttons; i++) {
        if (reading > mappings[i*2] && reading < mappings[i*2+1]) {
          return i+1;
        }
      }
      return 0;
    }
    
};


//Create analog button object (voltage divider)  at pin 2      ADC 2 = digital pin 4, physical pin 3
int analogButtonReadingsMap[2] = {1023, 511};//approximate analogRead values that match buttons pressed

AnalogDebouncedC buttons = AnalogDebouncedC(2, 10, 2, analogButtonReadingsMap);


const int fullsec = 2500 - 208; // how many interrupt cycles to count for full second, CHANGE IF NESCCESSARY FOR CALIBRATION
                                //2500 is the default value, -208 is my current attempt to calibrate

//on each cycle the LED output is shifted to next column, so output frequency is 2500 divided by 6 columns = 417 Hz


int buttonid = 0; //pressed button id derived from analog input
int lastbuttonid = 0;
int buttonseq = 0; //variable for keeping track of button 2 presses, that chooses between setting minutes, setting hours or starting clock


//Time is stored in array "timenumbers" each column separately
//e.g. 8:32:51 as {0, 8, 3, 2, 5, 1}.
int timenumbers[] = {0, 0, 0, 0, 15, 15}; //h'10 h'1 m'10 m'1 s'10 s'1
//seconds have strange values for start to indicate the setting sequence going on


int secdevs = 0; //for counting cycles up to full second
int timevalue = 0; //value that has to be output for current LED column
int col = 0; //LED column to which to output
int colswitch = 0; //column output to shift register
int i = 0; //assistive looping variable

boolean settingRoutine = true; //helper to enable counting only after time setting by user is done
volatile boolean interrupt = false; //helper to do actual computing in loop() instead of interrupt routine


void setup() {
  /*pin modes*/
  pinMode(valueout, OUTPUT); //four first bits in use, 8421XXXX
  pinMode(valueclock, OUTPUT);
  pinMode(columnout, OUTPUT); //six last bits in use, XXssmmhh
  pinMode(columnclock, OUTPUT);
  //pinMode(buttonin, INPUT);  for Attiny must NOT be declared, used by AnalogDebounced object
 
  /*TIMER INTERRUPT
  from http://www.instructables.com/id/Arduino-Timer-Interrupts/ */
  cli();//stop interrupts
 
  //set clock prescale register to run on 8MHz
  CLKPR = (1<<CLKPCE);
  CLKPR = ((0<<CLKPS3) | (0<<CLKPS2) | (0<<CLKPS1) | (0<<CLKPS0));//clock division factor - none
 
  //set timer0 interrupt at 2.5kHz
  TCCR0A = 0;
  TCCR0B = 0;
  TCNT0  = 0;//initialize counter value to 0
  // set compare match register for 2.5khz increments
  //compare match register = [ clock_speed_in_hz/ (prescaler * desired interrupt frequency) ] - 1
  OCR0A = 49;// = (8*10^6) / (64 * 2.5*10^3) - 1 (must be <256)
  TCCR0A |= (1 << WGM01);
  TCCR0B |= (0 << CS02) | (1 << CS01) | (1 << CS00);// prescaler 64
  TIMSK |= (1 << OCIE0A);                           
  sei();//allow interrupts
 
} //end of setup


/*
Time counting an lighting up leds takes place
in loop when "interrupt" variable is set
*/
ISR(TIMER0_COMPA_vect){ //timer0 interrupt 2.5kHz
 
  interrupt = true;//good practice to keep code in interrupt routine as small as possible
                   //each time this variable is set, loop() will handle things
 
} //end of ISR



void loop() {

  if (settingRoutine) {//while setting is not completed
    setmonitor();
  }
 
  if (interrupt) {
 
    /*TIME COUNTING*/   
    if (!settingRoutine) { //enables time counting only after the setting is completed     
      secdevs += 1; //increment clock cycle counter
      /*counting time in timenumbers[]*/
      if (secdevs == fullsec) { //number of cycles equal to a second have passed
        secdevs = 0; //reset cycle counter
        timenumbers[5] += 1; //increment seconds count
        if (timenumbers[5] >= 10) { //seconds'1
          timenumbers[5] = 0; //reset that number
          timenumbers[4] += 1; //increment next number
          if (timenumbers[4] >= 6) { //seconds'10
            timenumbers[4] = 0;
            timenumbers[3] += 1;
            if (timenumbers[3] >= 10) { //minutes'1
              timenumbers[3] = 0;
              timenumbers[2] += 1;
              if (timenumbers[2] >= 6) { //minutes'10
                timenumbers[2] = 0;
                timenumbers[1] += 1;
                if (timenumbers[1] >= 10) { //hours'1  - hour change at 10 and 20
                  timenumbers[1] = 0;
                  timenumbers[0] += 1;
                }
                if (timenumbers[0] == 2 && timenumbers[1] == 4) { //hour change at 24
                  timenumbers[1] = 0;
                  timenumbers[0] = 0; //set to 00:00:00
                }
              }
            }
          }
        }
      }
    }// /time counting
    
    
    
    /*OUTPUT (it also happens during time setting routine to show numbers)*/
    /*keep track of column to output*/
    col += 1; //increment column count
    if (col == 6) { //reached last one already
      col = 0; //reset to zero
    }
    
    /*Shift register has to output column as binary number that
    is such power of two as the column count currently is*/
    timevalue = timenumbers[col]*16;
    if (col == 0) {
      colswitch = 1;
    }
    else {
      colswitch = 2;
      for (i=2; i<col+1; i++) {
        colswitch = colswitch * 2;
      }
    }
    
    /*output that one column of data*/
    shiftOutCol(0); //turn off lights while shifting value in
    shiftOutValue(timevalue);
    shiftOutCol(colswitch);
    
    interrupt = false;
    
  }// /if (interrupt)

}// /loop()


void setmonitor() {
 
  buttonid = buttons.dbRead();
 
  if (buttonid != lastbuttonid) {//button state has to change to be counted as button press
 
    if (buttonid == 1) { //pressing button 1 increases a number
      if (buttonseq == 0) { //second button not pressed yet, increase minutes at button 1 press
        timenumbers[3] += 1;
        if (timenumbers[3] >= 10) {
          timenumbers[3] = 0;
          timenumbers[2] += 1;
          if (timenumbers[2] >= 6) {
            timenumbers[2] = 0;
          }
        }
      }
      if (buttonseq == 1) { //second button pressed once, increase hours at button 1 press
        timenumbers[1] += 1;
        if (timenumbers[1]== 10) { //hours'1  - hour change at d 20
          timenumbers[1] = 0;
          timenumbers[0] += 1;
        }
        if (timenumbers[0] == 2 && timenumbers[1] == 4) { //hour change at 24
          timenumbers[1] = 0;
          timenumbers[0] = 0;
        }
      }
    }
    if (buttonid == 2) { //button 2 pressed, change sequence from minute setting to hour setting or to ticking
      buttonseq++;
      timenumbers[5] = 0; //indicate change of mode on seconds column
      if (buttonseq == 2) { //setting mode ended
        timenumbers[4] = 0; //reset seconds to 0
      }
    }
  }
 
  lastbuttonid = buttonid;
 
  if (buttonseq == 2) {
    settingRoutine = false; //leave setting and start ticking
  }
    
}// /setmonitor()



//direct port manipulation in following functions is needed for sufficient output speed

void shiftOutValue(byte val) {
  byte i;
  byte mask = 0x01;
  for (i = 0; i < 8; i++) {
    if (val & mask) {
      PORTB |= (1<<PINB1);
    }
    else {
      PORTB &= ~(1<<PINB1);
    }
    delayMicroseconds(2);
    PORTB |= (1<<PINB0);
    delayMicroseconds(2);
    PORTB &= ~(1<<PINB0);
    delayMicroseconds(2);
    mask <<= 1;    
  }
}
void shiftOutCol(byte col) {
  byte i;
  byte mask = 0x01;
  for (i = 0; i < 8; i++) {
    if (col & mask) {
      PORTB |= (1<<PINB3);
    }
    else {
      PORTB &= ~(1<<PINB3);
    }
    delayMicroseconds(2);
    PORTB |= (1<<PINB2);
    delayMicroseconds(2);
    PORTB &= ~(1<<PINB2);
    delayMicroseconds(2);
    mask <<= 1;    
  }
}