Thursday, December 31, 2015

Arduino X Anelace "Powers of 2" Clock

Another fun Arduino hack:

The Anelace "Powers of 2" clock is a favorite of mine. It displays code in a BCD (binary coded decimal) format.

  0   0   0 
  0 0 0 0 0 
0 0 0 0 0 0 
0 0 0 0 0 0 
H H M M S S

  0   *   0 
  0 * 0 0 0 
0 * 0 0 * 0 
* 0 * * * 0 
1 2:5 9:3 0 = 12:59 and 30 seconds


Using BCD format makes the clock easy to read, which is quite important. However, I personally am not a fan of the inefficiency of BCD. Plus, any bit-banger worth their salt should be able to read binary numbers at least up to 2^8, right?

Note that the total information conveyable by the clock is 
2^(number of leds) = 2^20 = 1,048,576 possible values.

In the specific BCD format used, the total values (going from the left-most column to the right-most column) are
4*10*8*10*8*10 = 256,000 possible values.

Now, there are (24*60*60) = 86,400 seconds in a day. So (obviously) the 24 hour time can be perfectly represented within this scheme.

However, if we go to true binary, then we know that we can represent seconds and minutes by 6 LED's (2^6=64) and the 24 hour time by 5 LED's (2^5 = 32). Conveniently enough we can use columns instead of rows and get

  0   0   0 
  0 0 0 0 0 H
0 0 0 0 0 0 M
0 0 0 0 0 0 S

so our 12:59:30 time becomes

  0   0   0 
  0 * * 0 0 12
* * * 0 * * 59
0 * * * * 0 30 = 12:59 and 30 seconds

Note that the top row isn't being used at all! Extra-conveniently: there are 7 days in a week, and there are 2^3=8 total possible values for the top row. So we can do days of the week in the top row! There are 604,800 seconds in a week, and there are 1,048,576 total possible variations of LED lighting, so we're very efficiently using the LED's now.

The Arduino sketch below uses that mode as a default. There are a few other modes though, including a random mode and a grey code mode.


// Arduino code for "Powers of 2" clock by Anelace.
// Remove microcontroller, wire lines from microcontroller pads to Arduino


/*
2 - col 1
3 - col 2
4 - col 3
5 - col 4
6 - col 5
7 - 1st row
8 - 2nd row
9 - 3rd row
10 - 4th row
11 - ?

TODO: HANDLE WRAP
*/
#define CLOCK_STATE_RUNNING (0)
#define CLOCK_STATE_SET_MINUTES (1)
#define CLOCK_STATE_SET_HOURS (2)
#define CLOCK_STATE_SET_DAYS (3)
#define CLOCK_STATE_SET_MODE (4)
#define CLOCK_NUM_STATES (5)

#define CLOCK_MODE_BINARY (0)
#define CLOCK_MODE_BCD (1)
#define CLOCK_MODE_GRAY (2)
#define CLOCK_MODE_SYMMETRIC_GRAY (3)
#define CLOCK_MODE_RANDOM (4)
#define CLOCK_NUM_MODES (5)

#define TIME_SECONDS (3)
#define TIME_MINUTES (2)
#define TIME_HOURS (1)
#define TIME_DAYS (0)
#define TIME_NUM_PARTS (4)

#define NUM_SECONDS (60)
#define NUM_MINUTES (60)
#define NUM_HOURS (24)
#define NUM_DAYS (7)

int clock_mode = CLOCK_MODE_SYMMETRIC_GRAY;
int col0 = 2;
int numcols = 5;
int row0 = 7;
int numrows = 4;
int switch0 = 12;
int last_switch0 = 0;
int last_switch1 = 0;
unsigned short time[TIME_NUM_PARTS] = {1,18,30,00}; // 3 = seconds... 0 = days
int clock_state = CLOCK_STATE_RUNNING;
unsigned long last_seconds_from_millis = 0;

#define BRIGHTNESS_DELAY_NUM (3)
unsigned int brightness_delay_index = 2;
unsigned int brightness_delay[BRIGHTNESS_DELAY_NUM] = {10,1,500};

void update_time() {
  unsigned long seconds_from_millis = millis()/1000;
  time[TIME_SECONDS] += (seconds_from_millis - last_seconds_from_millis);
  last_seconds_from_millis = seconds_from_millis;
  if (time[TIME_SECONDS] >= NUM_SECONDS) {
    time[TIME_MINUTES]++; // assume we didn't miss a whole minute w/o update
    time[TIME_SECONDS] = 0;
    if (time[TIME_MINUTES] >= NUM_MINUTES) {
      time[TIME_HOURS]++;
      time[TIME_MINUTES] = 0;
      if (time[TIME_HOURS] >= NUM_HOURS) {
        time[TIME_DAYS]++;
        time[TIME_HOURS] = 0; 
        if (time[TIME_DAYS] >= NUM_DAYS) {
          time[TIME_DAYS] = 0;
        }
      }
    }
  }  
}
int led_display_random(int row, int col) {
  return rand()&0x1;
}

int led_display_bcd(int row, int col) {
  if (row == 0) {
    switch (col) {
      case 1: // most significant hours 10's place
        return (time[TIME_HOURS]/10)&(0x1<<1);
      case 3: // least significant hours 10's place    
        return (time[TIME_HOURS]/10)&(0x1);
      case 0: // most significant hours 1's place
        return (time[TIME_HOURS]%10)&(0x1<<3);
      case 2: // most significant minutes 1's place
        return (time[TIME_MINUTES]%10)&(0x1<<3);
      case 4: // most significant seconds 1's place
        return (time[TIME_SECONDS]%10)&(0x1<<3);
    }
  }
  switch (col) {
    case 4:
      return (time[TIME_SECONDS]%10)&(0x1<<numrows-1-row);
    case 3:
      return (time[TIME_SECONDS]/10)&(0x1<<numrows-1-row);
    case 2:
      return (time[TIME_MINUTES]%10)&(0x1<<numrows-1-row);
    case 1:
      return (time[TIME_MINUTES]/10)&(0x1<<numrows-1-row);
    case 0:
      return (time[TIME_HOURS]%10)&(0x1<<numrows-1-row);
  }
}

int led_display_binary(int row, int col) {
  if (row == 0) {
      // this row also contains seconds and minutes
      switch (col) {
        case 1:
        // most significant minutes bit
          return time[2]&(0x1<<5);
        case 3:
        // most significant seconds bit
          return time[3]&(0x1<<5);
        case 2:
          return time[0]&(0x1<<1);
        case 4:
          return time[0]&(0x1);
        case 0: // most significant days bit
          return time[0]&(0x1<<2);          
      }
  }
  return time[row]&(0x1<<(numcols-1-col));    
}

int led_display_gray(int row, int col) {
  if (row == 0) {
      // this row also contains seconds and minutes
      switch (col) {
        case 1:
        // most significant minutes bit
          return binaryToGray(time[2])&(0x1<<5);
        case 3:
        // most significant seconds bit
          return binaryToGray(time[3])&(0x1<<5);
        case 2:
          return binaryToGray(time[0])&(0x1<<1);
        case 4:
          return binaryToGray(time[0])&(0x1);
        case 0: // most significant days bit
          return binaryToGray(time[0])&(0x1<<2);          
      }
  }
  return binaryToGray(time[row])&(0x1<<(numcols-1-col));    
}

int led_display_symmetric_gray(int row, int col) {
  if (row == 0) {
      // this row also contains seconds and minutes
      switch (col) {
        case 1:
        // most significant minutes bit
          return binaryToSymmetricGray(time[2],30,6)&(0x1<<5);
        case 3:
        // most significant seconds bit
          return binaryToSymmetricGray(time[3],30,6)&(0x1<<5);
        case 2:
          return binaryToGray(time[0])&(0x1<<1);
        case 4:
          return binaryToGray(time[0])&(0x1);
        case 0: // most significant days bit
          return binaryToGray(time[0])&(0x1<<2);          
      }
  }
  switch (row) {
    case 0:
      return binaryToGray(time[row])&(0x1<<(numcols-1-col));    
    case 1:
      return binaryToSymmetricGray(time[row],12,5)&(0x1<<(numcols-1-col));    
    case 2:
      return binaryToSymmetricGray(time[row],30,6)&(0x1<<(numcols-1-col));    
    case 3:
      return binaryToSymmetricGray(time[row],30,6)&(0x1<<(numcols-1-col));    
  }
}

int led_display(int row, int col) {
  switch (clock_mode) {
    case CLOCK_MODE_BINARY:
      return led_display_binary(row,col);
    case CLOCK_MODE_BCD:
      return led_display_bcd(row,col);
    case CLOCK_MODE_GRAY:
      return led_display_gray(row,col);
    case CLOCK_MODE_RANDOM:
      return led_display_random(row,col);
    case CLOCK_MODE_SYMMETRIC_GRAY:
      return led_display_symmetric_gray(row,col);
  }
}

unsigned char clock_state_led_high(int row, int col) 
// check to see if the settings wants to blank a row
{
    if (clock_mode == CLOCK_MODE_BCD) return 1;
    
    switch (clock_state) {
      case CLOCK_STATE_RUNNING:
        return 1;
      case CLOCK_STATE_SET_MINUTES:
        if (row==2) return 1; else 
        if (row==0 && col==1) return 1; else
          return 0;
      case CLOCK_STATE_SET_HOURS:
        if (row==1) return 1; else return 0;
      case CLOCK_STATE_SET_DAYS:
        if (row==0){
         if (col == 2 || col ==0 || col == 4)
           return 1; 
        }
        return 0;
      default:
        return 1;
    }
}  

void setup() {
  int ii;
  for (ii=2;ii<=11;ii++) {
    pinMode(ii,OUTPUT);
  }
  Serial.begin(9600);  
}

void loop() {
  int ii,jj;
  int input;
  update_time();
  for (ii=0;ii<numcols;ii++) {
  digitalWrite(ii+col0,HIGH);
    for (jj=0;jj<numrows;jj++) {
      if (clock_state_led_high(jj,ii)) {
        if (led_display(jj,ii)) 
        {
          digitalWrite(jj+row0,HIGH);
          delayMicroseconds(brightness_delay[brightness_delay_index]);
          digitalWrite(jj+row0,LOW);
        } 
      }
    }
  digitalWrite(ii+col0,LOW);
  }
  
  input = !digitalRead(switch0);  // inverse polarity
  if (input && last_switch0 != 1) {
    clock_state = (clock_state+1)%CLOCK_NUM_STATES;
  }
  last_switch0 = input;
  
  input = !digitalRead(switch0+1);  // inverse polarity
  if (input && last_switch1 != 1) {
    switch (clock_state) {
      case CLOCK_STATE_RUNNING:
        brightness_delay_index = (brightness_delay_index+1)%BRIGHTNESS_DELAY_NUM;
        break;
      case CLOCK_STATE_SET_MINUTES:
        time[TIME_MINUTES] = (time[TIME_MINUTES] + 1)%NUM_MINUTES;
        break;
      case CLOCK_STATE_SET_HOURS:
        time[TIME_HOURS] = (time[TIME_HOURS]+1)%NUM_HOURS;
        break;
      case CLOCK_STATE_SET_DAYS:
        time[TIME_DAYS] = (time[TIME_DAYS]+1)%NUM_DAYS;
        break;
      case CLOCK_STATE_SET_MODE:
        clock_mode = (clock_mode+1)%CLOCK_NUM_MODES;
        break;       
    }
  }
  last_switch1 = input;
} // loop();

unsigned int binaryToSymmetricGray(unsigned int num, unsigned int num_max_div2,unsigned int num_bits){

  if (num < (num_max_div2)) {
    return (num >> 1) ^ num;
  } else {
    unsigned int output = ((((num_max_div2*2) - 1 - num) >> 1) ^ ((num_max_div2*2) - 1 - num));
    output |= (0x1<<(num_bits-1));
    return output;
  }
}

unsigned int binaryToGray(unsigned int num){
        return (num >> 1) ^ num;
}

unsigned int grayToBinary(unsigned int num){
    unsigned int mask;
    for (mask = num >> 1; mask != 0; mask = mask >> 1){
        num = num ^ mask;
    }
    return num;
}