GPS Clock Version 3 Part 2

It's 5am on a typical weekday morning. The wife and kids are asleep and I'm 'watching' the sun rise on my weather station. Headphones on. Armin Van Buuren on. Coffee machine on. Yawn. OK, concentrate.

At my last post I had the RTC for my GPS clock giving me a reliable 1PPS signal that I was detecting with an interrupt but, and here's the kicker, I couldn't get the interrupt handler to give me the current time. Something (I guessed) about the Wire library's use of interrupts not being compatible with the interrupt I implemented. Oh well, life goes on.
So, what do to. Actually I had an epiphany of sorts this morning. I realised that during the interrupt handler I could set a binary flag and, after the handler has completed executing (in the loop() function) I can check for the flag. If I find the flag has been set, then loop() reads the time from the RTC into a local variable and resets the flag.

About Interrupts


I'll outline what I know about interrupts, because it's important to this project. If someone has a better understanding and can correct me, please comment.

I should preface this by saying I'm only considering 8 bit single-threaded microcontrollers here. I also use 32-bit multi-threaded microcontrollers and this does not apply to them at all. So, in a normal Arduino program, setup() runs first, then loop() is called. When loop() ends, it's called again and again forever. This is the normal cycle of things for an Arduino program.

An interrupt does just that, interrupts, and whenever it's triggered will pause the execution of setup() or loop() (whichever is currently running) and take over the entire microcontroller until the interrupt code has finished. The cool thing is, the 'normal' code in setup() and loop() have no idea they've been interrupted, they just get frozen between two lines of code and then, some microseconds later, they are allowed to continue on. On the flip side, the interrupt also does not know what it's interrupted. It thinks it's the only program in existence when it's running. It knows nothing of where setup() and loop() are up to.

Synchronisation


To get the time on the display and make it reasonably accurate, this process seems possible:
  1. The real-time clock updates itself internally to the next second
  2. The 1PPS output is changed by the RTC
  3. The Arduino detects the input changed and the interrupt handler is called
  4. The interrupt handler puts the correct time (from its own memory, not directly from the RTC) onto the display
  5. The interrupt handler sets a flag (say, interruptComplete) to notify it has occurred and ends.
  6. Each time the loop() function starts, it looks for the interruptComplete flag. When it finds it true it reads the current RTC time into memory and adds one second to it. It cancels the flag so that each time loop() runs the time is NOT read until after the next interrupt handler executes.
  7. loop() runs repeatedly doing nothing of any value.
  8. Repeat from 1.
I don't know if the 1PPS output changes (2) before or after the time is updated (1). I don't know if the 1PPS changes from low to high or vice-versa when the time updates internally in the RTC. So why not do a little experiment?

Starting with the code we had last time we'll:
  • Return to loop() the code we moved into blink() previously
  • In loop(), change the delay(1000) call at the bottom to delay(100)
  • In setup(), change Serial.begin(4800) to Serial.begin(115200)
  • In blink(), add Serial.println("blink()");
This should now give us fairly different operation. Every tenth of a second the RTC's date/time should be displayed in the serial monitor. Every ten times the date/time is displayed, we should see "blink()" displayed. Here' the code. And here's the output.

blink()
0:47:10  31/3/12  Day of week:Tuesday
0:47:10  31/3/12  Day of week:Tuesday
0:47:10  31/3/12  Day of week:Tuesday
0:47:10  31/3/12  Day of week:Tuesday
0:47:10  31/3/12  Day of week:Tuesday
0:47:11  31/3/12  Day of week:Tuesday
0:47:11  31/3/12  Day of week:Tuesday
0:47:11  31/3/12  Day of week:Tuesday
0:47:11  31/3/12  Day of week:Tuesday
0:47:11  31/3/12  Day of week:Tuesday
blink()
0:47:11  31/3/12  Day of week:Tuesday
0:47:11  31/3/12  Day of week:Tuesday
0:47:11  31/3/12  Day of week:Tuesday
0:47:11  31/3/12  Day of week:Tuesday
0:47:11  31/3/12  Day of week:Tuesday
0:47:12  31/3/12  Day of week:Tuesday
0:47:12  31/3/12  Day of week:Tuesday
0:47:12  31/3/12  Day of week:Tuesday
0:47:12  31/3/12  Day of week:Tuesday
0:47:12  31/3/12  Day of week:Tuesday
blink()

So we're getting basically what I expected. Except that the seconds column is changing in-between the blink() prints. So let's flip the interrupt inside out and see what happens. Change the attachInterrupt line in setup() to:

  attachInterrupt(0, blink, FALLING);

Now that's more like it. We can see now the seconds are being updated at the same time as the interrupt is fired.

blink()
0:47:04  31/3/12  Day of week:Tuesday
0:47:04  31/3/12  Day of week:Tuesday
0:47:04  31/3/12  Day of week:Tuesday
0:47:04  31/3/12  Day of week:Tuesday
0:47:04  31/3/12  Day of week:Tuesday
0:47:04  31/3/12  Day of week:Tuesday
0:47:04  31/3/12  Day of week:Tuesday
0:47:04  31/3/12  Day of week:Tuesday
0:47:04  31/3/12  Day of week:Tuesday
0:47:04  31/3/12  Day of week:Tuesday
blink()
0:47:05  31/3/12  Day of week:Tuesday
0:47:05  31/3/12  Day of week:Tuesday
0:47:05  31/3/12  Day of week:Tuesday
0:47:05  31/3/12  Day of week:Tuesday
0:47:05  31/3/12  Day of week:Tuesday
0:47:05  31/3/12  Day of week:Tuesday
0:47:05  31/3/12  Day of week:Tuesday
0:47:05  31/3/12  Day of week:Tuesday
0:47:05  31/3/12  Day of week:Tuesday
blink()

Now, just for kicks, I'm going to slow the serial down to 4800. I expect the slower serial speed will increase the probability of a collision. The loop() function that's printing out the date and time will, at some point, get interrupted by the blink() function.  Oops, there it is (bolded for emphasis):

blink()
0:48:12  31/3/12  Day of week:Tuesday
0:48:12  31/3/12  Day of week:Tuesday
0:48:12  31/3/12  Day of week:Tuesday
0:48:12  31/3/12  Day of week:Tuesday
0:48:12  31/3/12  Day of week:Tuesday
0:48:12  31/3/12  Day of week:Tuesday
0:48:12  31/3/12  Day of week:Tuesday
0:48:12  31/3/12  Day of week:Tuesday
0:48:12  31/3/12  Day of week:Tuesday
0:48:12  31/3/blink()
12  Day of week:Tuesday
0:48:13  31/3/12  Day of week:Tuesday
0:48:13  31/3/12  Day of week:Tuesday
0:48:13  31/3/12  Day of week:Tuesday
0:48:13  31/3/12  Day of week:Tuesday
0:48:13  31/3/12  Day of week:Tuesday
0:48:13  31/3/12  Day of week:Tuesday
0:48:13  31/3/12  Day of week:Tuesday
0:48:13  31/3/12  Day of week:Tuesday
0:48:13  31/3/12  Day of week:Tuesday
blink()

This is it. A perfect example of how an interrupt works. When loop() is right in the middle of sending characters to the serial port the blink() function executes. Then as if nothing happened, loop() goes on from where it was. Neither function has a clue the other exists.

So if loop() can get the current time from the RTC ... WAIT! I'm a fool. My previous epiphany has been overruled. Better yet, let's have the interrupt handler only set the flag. loop() can do everything else. New plan:
  1. The real-time clock updates itself internally to the next second
  2. The 1PPS output is changed by the RTC
  3. The Arduino detects the input changed and the interrupt handler is called
  4. The interrupt handler sets a flag (say, interruptComplete) to notify it has occurred and the interrupt handler ends.
  5. Each time the loop() function starts, it looks for the interruptComplete flag. When it finds it true it reads the current RTC time and writes it to the display. It cancels the flag so that each time loop() runs the time is NOT updated until immediately after the next interrupt handler executes.
  6. loop() runs repeatedly doing nothing of any value.
  7. Repeat from 1.
So this new plan will mean only a few milliseconds will elapse between the RTC updating its internal time and the 7-segment display being updated. The workaround with reading into memory and adding a second (because we'll always be updating the display one second too late) is gone. Let's try it!

/*
 Example 7.3
 reading and writing to the Maxim DS1307 real time clock IC
 tronixstuff.com/tutorials
 based on code by Maurice Ribble
 17-4-2008 - http://www.glacialwanderer.com/hobbyrobotics
*/

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

const int led = 13;
boolean interruptComplete = false;

void blink()
{
  static boolean state;
  state = !state;
  digitalWrite(led, state);
  //Serial.println("blink()");
  interruptComplete = true;
}

// 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) );
}

// 1) Sets the date and time on the ds1307
// 2) Starts the clock
// 3) Sets hour mode to 24 hour clock

// Assumes you're passing in valid numbers

void setDateDs1307(byte second,        // 0-59
byte minute,        // 0-59
byte hour,          // 1-23
byte dayOfWeek,     // 1-7
byte dayOfMonth,    // 1-28/29/30/31
byte month,         // 1-12
byte year)          // 0-99
{
  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));     
  Wire.write(decToBcd(dayOfWeek));
  Wire.write(decToBcd(dayOfMonth));
  Wire.write(decToBcd(month));
  Wire.write(decToBcd(year));
  Wire.write(B00010000); // sends 0x10 (hex) 00010000 (binary) to control register - turns on square wave
  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
  *second     = bcdToDec(Wire.read() & 0x7f);
  *minute     = bcdToDec(Wire.read());
  *hour       = bcdToDec(Wire.read() & 0x3f);  // Need to change this if 12 hour am/pm
  *dayOfWeek  = bcdToDec(Wire.read());
  *dayOfMonth = bcdToDec(Wire.read());
  *month      = bcdToDec(Wire.read());
  *year       = bcdToDec(Wire.read());
}

void setup()
{
  byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;
  Wire.begin();
  Serial.begin(115200);

  attachInterrupt(0, blink, FALLING);
  pinMode(led, OUTPUT);

  // Change these values to what you want to set your clock to.
  // You probably only want to set your clock once and then remove
  // the setDateDs1307 call.

  second = 0;
  minute = 47;
  hour = 0;
  dayOfWeek = 3;
  dayOfMonth = 31;
  month = 3;
  year = 12;
  setDateDs1307(second, minute, hour, dayOfWeek, dayOfMonth, month, year);
  getDateTime();
}

void loop()
{
  if (interruptComplete)
  {
    getDateTime();
    interruptComplete = false;
  }
}

void getDateTime()
{
  byte second, minute, hour, dayOfWeek, dayOfMonth, month, year;

  getDateDs1307(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year);
  Serial.print(hour, DEC);// convert the byte variable to a decimal number when being displayed
  Serial.print(":");
  if (minute<10)
  {
      Serial.print("0");
  }
  Serial.print(minute, DEC);
  Serial.print(":");
  if (second<10)
  {
      Serial.print("0");
  }
  Serial.print(second, DEC);
  Serial.print("  ");
  Serial.print(dayOfMonth, DEC);
  Serial.print("/");
  Serial.print(month, DEC);
  Serial.print("/");
  Serial.print(year, DEC);
  Serial.print("  Day of week:");
  switch(dayOfWeek){
  case 1: 
    Serial.println("Sunday");
    break;
  case 2: 
    Serial.println("Monday");
    break;
  case 3: 
    Serial.println("Tuesday");
    break;
  case 4: 
    Serial.println("Wednesday");
    break;
  case 5: 
    Serial.println("Thursday");
    break;
  case 6: 
    Serial.println("Friday");
    break;
  case 7: 
    Serial.println("Saturday");
    break;
  }
  //Serial.println(dayOfWeek, DEC);
  //delay(100);
}

Booyah! It works! The code changes I just made are:
  • Added the declaration for the interruptComplete variable at the top of the program.
  • Added interruptComplete = true; to blink()
  • Renamed loop() to getDateTime()
  • Commented out the delay(1000) line at the bottom of getDateTime()
  • Created new loop() function with its enclosed code
The output I get looks like this:

blink()
blink()
0:47:00  31/3/12  Day of week:Tuesday
0:47:00  31/3/12  Day of week:Tuesday
blink()
0:47:01  31/3/12  Day of week:Tuesday
blink()
0:47:02  31/3/12  Day of week:Tuesday
blink()
0:47:03  31/3/12  Day of week:Tuesday
blink()
0:47:04  31/3/12  Day of week:Tuesday

To make it even nicer, I'll drop the Serial.println("blink()") line from blink(). I'll pump up the baud rate to 115200 on the serial again, just 'cause I like it.

OK, almost there. I just need to wind this together with my GPS-based code and, at some regular frequency, check the GPS time against the RTC time. But that's for next time.

That is all.

Comments