Tuesday, February 7, 2012

Arduino ISR Clock Demo

The engineering blog engblaze.com recently posted a great tutorial on using Arduino/AVR timer interrupts. This led to a lunch-hour challenge: To convert their once-a-second LED blinker into a minimal 7-segment-style clock display using our $39 GLO-216G serial OLED. Mission accomplished:




The clock program is minimal. All I did was take the excellent Engblaze timer/interrupt framework and drop in functions to keep time and do the serial stuff to drive the display. What may be of interest to Arduino programmers is the way that I organized the time values: Rather than declare separate variables to hold the hours, minutes and seconds, I used a single char array.

Since a char array that ends with null/zero in the last position is also a string for the purposes of Arduino serial routines, this approach allowed me to avoid the typical Arduino bugaboo of  needing multiple serial "print" instructions to print a list of variables.

/* Simple Clock Demo for GLO-216Y/G
 A tutorial at engblaze.com shows how to 
 set up AVR timers to fire an interrupt at 
 regular intervals--in this case 1.0000 second. 
 The engblaze program toggles an LED every 
 second. This version, for the Arduino Uno, 
 runs a 24-hour clock and shows the time on 
 a GLO-216Y/G serial OLED display. 
 
 =Program is written for GLO-216Y/G with intact 
 Spol jumper (inverted serial). Connect GLO
 SER to Arduino pin 3; power to +5 and GND. 
 */

// avr-libc library includes for timer ISR
#include <avr/io.h>
#include <avr/interrupt.h>

// Software Serial Stuff
// 
#include <SoftwareSerial.h>
#define rxPin 255       // Not used, set to invalid #
#define txPin 3         // GLO input to this Arduino pin
#define inverted 1     // If GLO-216 Spol jumper is intact  

// Instructions sent to GLO-216Y/G at startup. 
// 0x0c = clear the screen.
// 0x03,0x02,0x02,0x02 = Set big font. 
// 0x19 = Use the 7-segment number font.
char const gloSetup[] = {
  0x0c,0x03,0x02,0x02,0x02,0x13,0x00 } 
;

// Dual-use char-array string ========================== 
// As an array, it stores the digits of the time
// hh mm ss. Functions below increment the digits as 
// appropriate for their units (0-59 for minutes and 
// seconds, 0-23 for hours). Digits are stored as 
// ASCII characters rather than raw values, allowing for 
// the string to be printed without further processing. 
// In addition to the digits, the string also contains
// a pair of ':' characters, and starts with the GLO
// home-cursor instruction 0x01. Packing the digits, 
// colons and formatting instruction together in one 
// string allows the time to be updated in one "print."
char hmsTime[10] = {
  0x01,
  '1','2',':','4','1',':','5','0',0x00} 
;  

// Defining names for the digit positions in the 
// time string makes it easier to read the functions 
// that increment the time
#define sOnes 8    // Ones digit of seconds. 
#define sTens 7    // Tens digit of seconds. 
#define msColon 6  // ':' between minutes and seconds.
#define mOnes 5    // Ones digit of minutes.
#define mTens 4    // Tens digit of minutes. 
#define hmColon 3  // ':' between hours and minutes.
#define hOnes 2    // Ones digit of hours.
#define hTens 1    // Tens digit of hours.

// Establish a soft serial port using the pin definitions above.
SoftwareSerial mySerial =  SoftwareSerial(rxPin, txPin, inverted);

// Set up the Soft Serial output and send the GLO its setup 
// instructions. Then, using code copied from engblaze.com, 
// set up a timer to fire an ISR at precise 1-second intervals. 
void setup()
{
  digitalWrite(txPin, LOW);   // Stop bit state
  pinMode(txPin, OUTPUT);
  mySerial.begin(9600);
  delay(500);             
  mySerial.print(gloSetup);

  // initialize Timer1
  cli();          // disable global interrupts
  TCCR1A = 0;     // set entire TCCR1A register to 0
  TCCR1B = 0;     // same for TCCR1B

  // set compare match register to desired timer count:
  OCR1A = 15624;
  // turn on CTC mode:
  TCCR1B |= (1 << WGM12);
  // Set CS10 and CS12 bits for 1024 prescaler:
  TCCR1B |= (1 << CS10);
  TCCR1B |= (1 << CS12);
  // enable timer compare interrupt:
  TIMSK1 |= (1 << OCIE1A);
  // enable global interrupts:
  sei();
}

// Nothing in the loop. All the action is in the ISR.  
void loop()
{
  // Put your code here.  
}

// Timer fires this ISR every second. 
ISR(TIMER1_COMPA_vect)
{
  mySerial.print(hmsTime);  // print the string. 
  incSeconds() ;            // update for next time. 
}

// Timekeeping routines =======================
// The digits of the hours,  minutes and 
// second are stored as characters in a char-
// array string. Since the only math needed is 
// increment (and carry-the-one), it's efficient
// to just work with the chars rather than 
// regular integers (or bytes, etc.). 
// ============================================

//Increment seconds. If seconds value exceeds
//59, increment minutes. 
void incSeconds() {
  hmsTime[sOnes] += 1 ;
  if (hmsTime[sOnes] > '9') {
    hmsTime[sOnes] = '0' ;
    hmsTime[sTens] += 1 ;
    if (hmsTime[sTens] > '5') {
      hmsTime[sTens] = '0' ;
      incMinutes(); 
    }
  }
}

//Increment minutes. If minutes value exceeds
//59, increment hours. 
void incMinutes() {
  hmsTime[mOnes] += 1 ;
  if (hmsTime[mOnes] > '9') {
    hmsTime[mOnes] = '0' ;
    hmsTime[mTens] += 1 ;
    if (hmsTime[mTens] > '5') {
      hmsTime[mTens] = '0' ;
      incHours() ;       
    }
  }
}

//Increment hours. If hours value exceeds
//23, rollover to 0. 
void incHours() {
  if (hmsTime[hTens] == '2' && hmsTime[hOnes] == '3')
  {
    hmsTime[hOnes] = '0' ;
    hmsTime[hTens] = '0' ;  
  }
  else
  {
    hmsTime[hOnes] += 1 ;
    if (hmsTime[hOnes] > '9') {
      hmsTime[hOnes] = '0' ;
      hmsTime[hTens] += 1 ;
    }   
  }     
}

Arduino-compatible serial OLED: $39 at seetron.

No comments:

Post a Comment