Friday, 31 March 2017

Resurrecting more old posts - word clock with Arduino and TinyRTC DS1307 module

I've always loved the idea of a word clock; a few of us have even built a couple over the years (though I don't actually manage to ever keep hold of the ones I've made). Mostly they're a fun way to prove our multiplexing/charlie-plexing is working properly. Sometimes just to demonstrate how to use a MAX7219 chip.

Recently we had to convert some old code over to Arduino, to use a MAX7219 LED driver. We'd had some trouble getting a 4-way 7-segment LED display to work properly with the MAX7219 chips - mostly because we were using common anode displays and the driver chips are best suited to common cathode (or was it the other way around...?). So we thought it'd be a good idea to strip the bigger project back to individual pieces; and for this task we focussed on just making the MAX7219 chips work.

A few months back we'd done some playing around with Arduino and MAX7219. This time we cascaded two chips and connected each MAX7219 to a single LED matrix. A bit of copy-n-paste coding later and we had a working example using the Arduino MAX Library


Having just moved a load of stuff into the new workshop bungalow, we had boxes and boxes of components hanging around - to hand were some RTC modules from a few years back; this gave us a perfect opportunity to put one to use


The RTC Library had us reading the current date/time off the TinyRTC module in just minutes!

#include <Wire.h>
#include "RTClib.h"
RTC_DS1307 RTC;

void setup () {
      Serial.begin(9600);
      Wire.begin();
      RTC.begin();
   if (! RTC.isrunning()) {
      Serial.println("RTC is NOT running!");
      // following line sets the RTC to the date & time this sketch was compiled
      RTC.adjust(DateTime(__DATE__, __TIME__));
   }
}
void loop () {
      DateTime now = RTC.now();
      Serial.print(now.year(), DEC);
      Serial.print('/');
      Serial.print(now.month(), DEC);
      Serial.print('/');
      Serial.print(now.day(), DEC);
      Serial.print(' ');
      Serial.print(now.hour(), DEC);
      Serial.print(':');
      Serial.print(now.minute(), DEC);
      Serial.print(':');
      Serial.print(now.second(), DEC);
      Serial.println();
      delay(1000);
}


After putting the two matrix boards side-by-side all we needed to do was read the current time over I2C and decide which LEDs needed to light up when. Here's the pattern of letters/words we came up with:


As we're effectively using a larger 16x8 matrix, we had plenty of extra letters after writing out the hours and minutes, so went to town with improving the precision of our clock.

Normally a word clock  might say something like "it is ten past two" and display this until the time had changed to the next five-minute segment (in this case, "it is quarter past two"). We decided that we'd improve the precision by adding in "nearly" and "just gone" elements to each five-minute segment.

So, within two minutes of a time, we'd describe it as "nearly". For example at 14:04 we'd say "it is nearly five past two". Similarly, for up to two minutes after a time, we'd describe it is "just gone". So at 14:31 we'd say "it has just gone half past two".

This gives us the ability to tell the time to within two minutes, using language that many of us commonly use every day (although, as Nick pointed out, a margin of error of two minutes still leaves plenty time to miss your bus if you're not careful!)

Here's the code we came up with.
It's written to be readable/understandable rather than particularly "clever" (for example, we'd normally use an array for multi-line variables, like R[1][3] rather than multiple "single" variables, like R1C3 but as we've been getting a few questions of late on things we'd consider to be pretty simple, we tried to keep the code as understandable as possible!)


#include "Wire.h"
#define DS1307_I2C_ADDRESS 0x68

int dataIn = 2;
int load = 3;
int clk = 4;
int maxInUse = 2;      //change this variable to set how many MAX7219's you'll use

// define max7219 registers
byte max7219_reg_noop         = 0x00;
byte max7219_reg_digit0         = 0x01;
byte max7219_reg_digit1         = 0x02;
byte max7219_reg_digit2         = 0x03;
byte max7219_reg_digit3         = 0x04;
byte max7219_reg_digit4         = 0x05;
byte max7219_reg_digit5         = 0x06;
byte max7219_reg_digit6         = 0x07;
byte max7219_reg_digit7         = 0x08;
byte max7219_reg_decodeMode      = 0x09;
byte max7219_reg_intensity       = 0x0a;
byte max7219_reg_scanLimit       = 0x0b;
byte max7219_reg_shutdown      = 0x0c;
byte max7219_reg_displayTest    = 0x0f;

int e = 0;                // just a variable


// these are used to run in test mode
bool test_mode = false;
int curr_min = 0;
int curr_hr = 0;


// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val){
   return ( (val/10*16) + (val%10) );
}

// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val){
   return ( (val/16*10) + (val%16) );
}

void setDateDs1307(byte second, byte minute, byte hour, byte dayOfWeek, byte dayOfMonth, byte month, byte year){
   Wire.beginTransmission(DS1307_I2C_ADDRESS);
   Wire.write(0);
   Wire.write(decToBcd(second)); // 0 to bit 7 starts the clock
   Wire.write(decToBcd(minute));
   Wire.write(decToBcd(hour)); // If you want 12 hour am/pm you need to set
                                             // bit 6 (also need to change readDateDs1307)
   Wire.write(decToBcd(dayOfWeek));
   Wire.write(decToBcd(dayOfMonth));
   Wire.write(decToBcd(month));
   Wire.write(decToBcd(year));
   Wire.endTransmission();
}

// Gets the date and time from the ds1307
void getDateDs1307(byte *second, byte *minute, byte *hour, byte *dayOfWeek, byte *dayOfMonth, byte *month, byte *year){
   // Reset the register pointer
   Wire.beginTransmission(DS1307_I2C_ADDRESS);
   Wire.write(0);
   Wire.endTransmission();

   Wire.requestFrom(DS1307_I2C_ADDRESS, 7);

   // A few of these need masks because certain bits are control bits
   byte b;
   b=Wire.read();
   Serial.print(b,HEX);
   *second = bcdToDec(b & 0x7f);

   b=Wire.read();
   Serial.print(b,HEX);
   *minute = bcdToDec(b);

   b=Wire.read();
   Serial.print(b,HEX);
   *hour = bcdToDec(b & 0x3f); // Need to change this if 12 hour am/pm

   b=Wire.read();
   Serial.print(b,HEX);
   *dayOfWeek = bcdToDec(b);

   b=Wire.read();
   Serial.print(b,HEX);
   *dayOfMonth = bcdToDec(b);

   b=Wire.read();
   Serial.print(b,HEX);   
   *month = bcdToDec(b);

   b=Wire.read();
   Serial.print(b,HEX);
   *year = bcdToDec(b);

   Serial.print(" ");
   
}


void putByte(byte data) {
   byte i = 8;
   byte mask;
   while(i > 0) {
      mask = 0x01 << (i - 1);         
      digitalWrite( clk, LOW);   
      if (data & mask){                  
         digitalWrite(dataIn, HIGH);
      }else{
         digitalWrite(dataIn, LOW);
      }
      digitalWrite(clk, HIGH);
      --i;                                    
   }
   
}

void maxAll (byte reg, byte col) {      // initialize   all   MAX7219's in the system
   int c = 0;
   digitalWrite(load, LOW);   // begin      
   for ( c =1; c<= maxInUse; c++) {
   putByte(reg);   // specify register
   putByte(col);//((data & 0x01) * 256) + data >> 1); // put data
      }
   //digitalWrite(load, LOW);
   digitalWrite(load,HIGH);
}

void maxOne(byte maxNr, byte reg, byte col) {      
//maxOne is for addressing different MAX7219's,
//while having a couple of them cascaded

   int c = 0;
   digitalWrite(load, LOW);   // begin      

   for ( c = maxInUse; c > maxNr; c--) {
      putByte(0);      // means no operation
      putByte(0);      // means no operation
   }

   putByte(reg);   // specify register
   putByte(col);//((data & 0x01) * 256) + data >> 1); // put data

   for ( c =maxNr-1; c >= 1; c--) {
      putByte(0);      // means no operation
      putByte(0);      // means no operation
   }

   //digitalWrite(load, LOW); // and load da stuff
   digitalWrite(load,HIGH);
}

void showClockOutput(byte hour, byte minute, byte second, byte dayOfMonth, byte month, byte year){
   Serial.print(hour, DEC);
   Serial.print(":");
   Serial.print(minute, DEC);
   Serial.print(":");
   Serial.print(second, DEC);
   Serial.print(" ");
   Serial.print(dayOfMonth, DEC);
   Serial.print("/");   
   Serial.print(month, DEC);
   Serial.print("/");
   Serial.print(year, DEC);   
   Serial.println();
}


void setup () {
   byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
   
   pinMode(dataIn, OUTPUT);
   pinMode(clk,   OUTPUT);
   pinMode(load,    OUTPUT);

   //initiation of the max 7219
   maxAll(max7219_reg_scanLimit, 0x07);         
   maxAll(max7219_reg_decodeMode, 0x00);   // using an led matrix (not digits)
   maxAll(max7219_reg_shutdown, 0x01);      // not in shutdown mode
   maxAll(max7219_reg_displayTest, 0x00); // no display test
   for (e=1; e<=8; e++) {      // empty registers, turn all LEDs off
      maxAll(e,0);
   }
   maxAll(max7219_reg_intensity, 0x0f & 0x0f);      

   // initialise the RTC module
   // (inc setting the time if necessary)

   Wire.begin();
   bool set_date=true;
   if(set_date==false){

      // change these values as necessary
      second = 10;
      minute = 46;
      hour = 16;
      dayOfWeek = 4;
      dayOfMonth = 30;
      month = 3;
      year = 17;
      setDateDs1307(second, minute, hour, dayOfWeek, dayOfMonth, month, year);
   }
                                                                           
   Serial.begin(9600);
   Serial.println("Let's go");
                                                                           
}   

void loop () {
   
   byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
   getDateDs1307(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year);
   showClockOutput( hour, minute, second, dayOfMonth, month, year);

   int r;
   int r1c1, r1c2;
   int r2c1, r2c2;
   int r3c1, r3c2;
   int r4c1, r4c2;
   int r5c1, r5c2;
   int r6c1, r6c2;
   int r7c1, r7c2;
   int r8c1, r8c2;
   
   if(test_mode==true){
      curr_min++;
      if(curr_min > 59){ curr_hr++; curr_min=0;}
      if(curr_hr > 23 ){ curr_hr=0; }

      hour = curr_hr;
      minute = curr_min;
   }

      // from the time, work out whether we start with its or it has
      // (its nearly or it has just past)
      r=0;
      
      r1c1 = B11100000; r1c2 = 0x00;
      r2c1 = 0x00; r2c2 = 0x00;
      r3c1 = 0x00; r3c2 = 0x00;
      r4c1 = 0x00; r4c2 = 0x00;
      r5c1 = 0x00; r5c2 = 0x00;
      r6c1 = 0x00; r6c2 = 0x00;
      r7c1 = 0x00; r7c2 = 0x00;
      r8c1 = 0x00; r8c2 = 0x00;
      
      if(minute==1 || minute==2 || minute==6 || minute==7 || minute==11 || minute==12 || minute==16 || minute==17){ r=1; }
      if(minute==3 || minute==4 || minute==8 || minute==9 || minute==13 || minute==14 || minute==18 || minute==19){ r=2; }
      if(minute==21 || minute==22 || minute==26 || minute==27 || minute==31 || minute==32 || minute==36 || minute==37){ r=1; }
      if(minute==23 || minute==24 || minute==28 || minute==29 || minute==33 || minute==34 || minute==38 || minute==39){ r=2; }
      if(minute==41 || minute==42 || minute==46 || minute==47 || minute==51 || minute==52 || minute==56 || minute==57){ r=1; }
      if(minute==43 || minute==44 || minute==48 || minute==49 || minute==53 || minute==54 || minute==58 || minute==59){ r=2; }

      // just before or just after major clock minutes, light up the
      // word "nearly" or "just past"
      if(r==1){ r1c1 = B11011101; r1c2 = B11101111; }
      if(r==2){ r1c1 = B11100000; r1c2 = B00000000; }
      if(r==2){ r2c1 = B11111100; }

      // maybe we could use the word "just gone" for one minute past and drop the "just"
      // for two minutes past?
      r=0;
      if(minute==2 || minute==7 || minute==12 || minute==17){ r=1; }
      if(minute==22 || minute==27 || minute==32 || minute==37){ r=1; }
      if(minute==42 || minute==47 || minute==52 || minute==57){ r=1; }
      if(r==1){
         // mask out the word "just"
         r1c1 = r1c1 & B11111110;
         r1c2 = r1c2 & B00011111;
      }
      
      // at quarter past and quarter to the hour, light up the word quarter
      if(minute >=13 && minute<=17){ r2c1+=1; r2c2 = B11111100; }
      if(minute >=43 && minute<=47){ r2c1+=1; r2c2 = B11111100; }

      // at twenty to and twenty past the hour, light up the word "twenty"
      // (also at twenty-five to and twenty-five past)
      if(minute >=18 && minute<=27){ r3c1 = B11111100; }
      if(minute >=33 && minute<=42){ r3c1 = B11111100; }

      // light up the word "five" at not only five to/past but also
      // twenty-five to and twenty-five past
      if(minute >=3 && minute<=7) {   r3c1 +=1; r3c2 = B11100000;}
      if(minute >=53 && minute<=57){ r3c1 +=1; r3c2 = B11100000;}
      if(minute >=23 && minute<=27){ r3c1 +=1; r3c2 = B11100000;}
      if(minute >=33 && minute<=37){ r3c1 +=1; r3c2 = B11100000;}

      // at ten past/to the hour, light up the word ten
      if(minute >=8 && minute <=12){ r3c2 = B00001110;}
      if(minute >=48 && minute <=52){ r3c2 = B00001110;}

      // at around half past...
      if(minute >=28 && minute <=32){ r4c1 = B11110000;}

      // display the either the word "past" or "two"
      if(minute > 2   && minute <= 32){ r4c1 += B00000011; r4c2 = B11000000; }
      if(minute > 32 && minute < 58) { r4c1 += B00001100; }

      // this is for on the hour, o'clock
      if(minute >=58 || minute <=2) { r8c1 = B11111110; }

      // show the correct hour(s)
      // (why compare to 32 and not 30 for half past the hour?
      //   because at 11:32 we want it still to read "it has just
      //   gone half past 11" - not half past twelve!
            
      // just before/after one o'clock
      if((hour==0   && minute > 32)   || (hour == 1   && minute <=32)){ r7c2 = B00000111; }
      if((hour==12   && minute > 32) || (hour == 13 && minute <=32)){ r7c2 = B00000111; }

      // just before/after two o'clock
      if((hour== 1   && minute > 32) || (hour == 2   && minute <=32)) { r5c1 = B11100000; }
      if((hour==13   && minute > 32) || (hour == 14 && minute <=32)) { r5c1 = B11100000; }

      // just before/after three o'clock
      if((hour== 2   && minute > 32) || (hour == 3   && minute <=32)) { r4c2 += B00011111; }
      if((hour==14   && minute > 32) || (hour == 15 && minute <=32)) { r4c2 += B00011111; }

      // just before/after four o'clock
      if((hour== 3   && minute > 32) || (hour == 4   && minute <=32)) { r5c1 = B00011110; }
      if((hour==15   && minute > 32) || (hour == 16 && minute <=32)) { r5c1 = B00011110; }
      
      // just before/after five o'clock
      if((hour==4   && minute > 32) || (hour == 5   && minute <=32)){ r5c1 += 1; r5c2 = B11100000; }
      if((hour==16 && minute > 32) || (hour == 17 && minute <=32)){ r5c1 += 1; r5c2 = B11100000; }

      // just before/after six o'clock
      if((hour==5   && minute > 32) || (hour == 6   && minute <=32)){ r6c1 = B11100000; }
      if((hour==17 && minute > 32) || (hour == 18 && minute <=32)){ r6c1 = B11100000; }

      // just before/after seven o'clock
      if((hour==6   && minute > 32) || (hour == 7   && minute <=32)){ r5c2 = B00011111; }
      if((hour==18 && minute > 32) || (hour == 19 && minute <=32)){ r5c2 = B00011111; }

      // just before/after eight o'clock
      if((hour==7   && minute > 32) || (hour == 8   && minute <=32)){ r6c1 = B00011111; }
      if((hour==19 && minute > 32) || (hour == 20 && minute <=32)){ r6c1 = B00011111; }

      // just before/after nine o'clock
      if((hour==8   && minute > 32) || (hour == 9   && minute <=32)){ r6c2 = B11110000; }
      if((hour==20 && minute > 32) || (hour == 21 && minute <=32)){ r6c2 = B11110000; }

      // just before/after ten o'clock
      if((hour==9   && minute > 32) || (hour == 10 && minute <=32)){ r6c2 = B00001110; }
      if((hour==21 && minute > 32) || (hour == 22 && minute <=32)){ r6c2 = B00001110; }

      // just before/after eleven o'clock
      if((hour==10 && minute > 32) || (hour == 11 && minute <=32)){ r7c1 = B11111100; }
      if((hour==22 && minute > 32) || (hour == 23 && minute <=32)){ r7c1 = B11111100; }

      // this is just before/after twelve (noon)
      if((hour==11 && minute > 32) || (hour == 12 && minute <=32)){ r7c1 = B00000011; r7c2 = B11110000; }

      // this is just before/after midnight
      if((hour==23 && minute > 32) || (hour == 0   && minute <=32)){ r8c1 = B00000001; r8c2 = B11111110; }


      // now light up the LEDs
      maxOne(1,1,r1c1);
      maxOne(1,2,r2c1);
      maxOne(1,3,r3c1);
      maxOne(1,4,r4c1);
      maxOne(1,5,r5c1);
      maxOne(1,6,r6c1);
      maxOne(1,7,r7c1);
      maxOne(1,8,r8c1);
   
      maxOne(2,1,r1c2);
      maxOne(2,2,r2c2);
      maxOne(2,3,r3c2);
      maxOne(2,4,r4c2);
      maxOne(2,5,r5c2);
      maxOne(2,6,r6c2);
      maxOne(2,7,r7c2);
      maxOne(2,8,r8c2);
   
      delay(2000);
   
}



Normally, when telling the time, anything before 30 minutes is described as "past the hour" and anything after is "to the following hour". But because we're allowing for "a few minutes past a specific time point" we had to allow for up to 32 minutes to be described as "past" (so 15:32 would be written as just gone half past three). Similarly, up to two minutes before the o'clock position would be described as "nearly x o'clock" so any checks against minutes are 0-32 and 33-58 (not against 30 and 59 as might be expected).

With the LEDs wired up and the code working, we fired up the laser cutter to create a fascia. We cut the protective film from one side of some clear perspex and sprayed it with black paint....


....then laser-etched the letters onto the paint. As this was to be the back of the display, the lettering had to be mirrored.



Amazingly, the clock booted up and worked first time.


So we added a test mode to increase the minutes every couple of seconds, so we can see it cycle through all available times in just a few minutes instead of having to stay awake (and gawp at the clock face without a break) for 24 hours or more!


Maybe if we build another one we might try smoked acrylic so the unlit letters are less prominent when they're not in use?