Digital Clock using PIC Timers

12 minute read

In this post we’re going to use our knowledge of timers and our LCD setup to make a digital clock. The clock would operate by displaying current time as HH:MM:SS on an LCD display, and the timer would refresh it every second. This setup can then be easily adapted to work as a stopwatch or a timer.

What you’ll need

Configure LCD library

This time, besides including the standard XC library (xc.h), you’ll also need to include the LCD library so that you can use functions like displaying numbers etc. To include my LCD library, just write:

1
#include "LCD-library.h"

This will include the source of LCD-library.h and LCD-library.c into your program, so make sure these 2 files are present in your project directory.

Also, you’ll need to go to the file LCD-library.h and set all appropriate #defines for your LCD setup — the pins you’ve connected the LCD to, your crystal frequency, the number of rows of your LCD, the data length and font. All of these settings are commented in the file.

For example, if your crystal frequency is 20MHz, like in my case, you set the value for _XTAL_FREQ to the number of Hz of your crystal. In my case, it would be 20000000Hz, so I’d write:

1
#define _XTAL_FREQ 20000000

Then, you need to know how many columns and rows your LCD display has. Mine has 16 columns and 2 rows, so I’d find the lines defining LCD_columns and LCD_rows and change their values to 16 and 2 respectively:

1
2
#define LCD_columns 16
#define LCD_rows 2

Then, you set the data length to either 4 or 8 bits. If you’re unsure, just go with 4 bits — you’ll save 4 pins of your PIC for other purposes. This is set in this line:

1
#define LCD_data_length 4

You’ll also need to set the font, which is either 5×10 dots or 5×8 dots. In my case it’s 5×8 dots, so I’d write 8 as the value for LCD_font (if you have 5×10, you’d write 8). This is set in the line:

1
#define LCD_font 8

Finally, you set to which pins of the PIC you’ve connected which pins of your LCD (to see the position of pins on your LCD, you’ve got to check its datasheet). You’ll need set the pins for both the pins themselves (PORT) and also their direction (TRIS). An example which I use is the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
// pins (PORT)
#define LCD_data PORTD
#define LCD_busy PORTDbits.RD7
#define LCD_rs PORTBbits.RB0
#define LCD_rw PORTBbits.RB1
#define LCD_en PORTBbits.RB2

// direction of pins (TRIS)
#define LCD_data_dir TRISD
#define LCD_busy_dir TRISDbits.RD7
#define LCD_rs_dir TRISBbits.RB0
#define LCD_rw_dir TRISBbits.RB1
#define LCD_en_dir TRISBbits.RB2

And that’s it for setting up the LCD library. Now you just need to call an init function for the LCD, and the library will take care of the initialization process using the settings you provided in LCD-library.h. So, in our main function, we just call:

1
LCD_init();

Now we’re ready to initialize our timer.

Initialize timer

After we’ve set up our LCD, we’ll initialize the timer which will increase the number of seconds on the digital clock. We’ll use timer1 with the exact same settings as I described in the previous post about PIC timers, using the prescale value of 4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void timer_init() {

  // clear timer1 high and low holding registers
  TMR1H = TMR1L = 0;

  // reading in 2 8-bit operations
  //T1CONbits.RD16 = 0;

  // Prescaler value of 1:4
  T1CONbits.T1CKPS1 = 1;
  T1CONbits.T1CKPS0 = 0;

  // disable timer 1 oscillator
  //T1CONbits.T1OSCEN = 0;

  // timer 1 uses internal clock
  //T1CONbits.TMR1CS = 0;

  // enable interrupt flag for timer 1
  PIE1bits.TMR1IE = 1;

  // turn timer 1 on
  T1CONbits.TMR1ON = 1;

  // enable peripheral interrupts
  INTCONbits.PEIE = 1;

  // enable global interrupts
  INTCONbits.GIE = 1;
}

The inside of the function is exactly the same as before. We start counting from 0 (set TMR1 to 0), set the prescale value to 4, enable interrupts for timer1, turn timer1 on and finally enable peripheral and global interrupts.

We know that with our crystal (oscillator) frequency of 20MHz, the timer1’s resolution of 2^{16} and the prescale value of 4, this will produce the output frequency

F_{OUT}=\frac{F_{OSC}}{4\cdot PRESC\cdot RES}=\frac{20MHz}{4\cdot 4\cdot 2^{16}}\approx 19.07Hz

This means that timer1 will fire an interrupt approximately 19 times per second. (If you have a crystal with different frequency, resolution or prescale value, just change the appropriate values and use that output frequency instead.)

Now, that the timer is running, we’re ready to program the brain of the clock.

Interrupt Service Routine

Our ISR (Interrupt Service Routine) function will be called whenever an interrupt is fired. The only thing we’re going to do inside it is that we’re going to increase a variable which will count the number of overflows. Therefore, we first create a new global, volatile (able to be changed externally, e.g. by an interrupt) variable:

1
volatile int count = 0

Then, inside our ISR, we’ll first check if it’s really the timer1 which fired the interrupt (by checking timer1’s interrupt flag), and if that’s true, we’ll increase our number of overflows and reset the overflow flag back to 0:

1
2
3
4
5
6
7
8
9
void interrupt ISR() {
  // has the interrupt been fired by timer 1? (check for interrupt flag from timer 1)
  if(PIR1bits.TMR1IF == 1) {
    // increase number of overflows
    count++;
    // reset timer1's overflow flag back to 0
    PIR1bits.TMR1IF = 0;
  }
}

So, now every time an interrupt is fired by timer1, the variable count is increased. We’ll use this variable in a moment when setting the current time.

Determine current time

Before we can display the time, we need to do the following in our main function. The first thing is to initialize the LCD display (we’ve done this already):

1
LCD_init();

Then, we need to manually set the current time, or better, the time you think will be when you start the device. I know this isn’t very practical in that you’d have to program the current time every time you start the device, but this is used just as an example. If you wanted a more practical clock, you’d have to add buttons to set current time (which isn’t difficult, but I’m not going to do it here). To set the current time, first create 3 global volatile variables for storing hours, minutes and seconds:

1
2
3
volatile int seconds = 0;
volatile int minutes = 0;
volatile int hours = 0;

Then, in the main function set the current time, for example like so:

1
2
3
hours = 16;
minutes = 45;
seconds = 5;

Then, we’ll initialize our timer by calling the init function we’ve made before:

1
timer_init();

Now, using our hours, minutes and seconds variables, we’ll display the initial time by calling a displayClock function which we’ll define later:

1
displayClock();

Now we’ll need a volatile variable which will indicate that the time has changed, which I’ll call change. If it’s 1, the time has has changed and the clock needs updating, and if it’s 0, the time has not changed (it’s less than a second which has passed from previous update).

1
volatile int change = 0;

And now we’re finally ready to determine the current time. We’ll do this in an infinite loop, updating the clock time over and over again.

1
2
3
while(1) {
  // determine and update current time
}

Since we’ve calculated that timer1 will fire interrupts 19 times per second, we need to check if the count variable has reached 19, in which case 1 second has passed. In that case we need to increase the number of seconds, reset the count variable (so that it starts counting from 0 again) and set the change variable to 1 (since the time needs updating).

1
if(count == 19) {seconds++; count = 0; change = 1;}

Similarly, if the number of seconds has reached 60, we need to increase the number of minutes, reset the number of seconds to 0, and set the change to 1.

1
if(seconds == 60) {minutes++; seconds = 0; change = 1;}

Finally, if the number of minutes reaches 60, we increase the number of hours, reset the number of minutes to 0, and set the change variable to 1.

1
if(minutes == 60) {hours++; minutes = 0; change = 1;}

Now, that the current time has been determined, the last thing we’re going to do inside the infinite loop is to determine if there’s been any change to the current time  (if change is equal to 1). If yes, that means we need to update the time on the clock and reset the change variable to 0. To update the time, we’re going to use the displayClock() function again, which we’ll define in a moment.

1
if(change) {displayClock(); change = 0;}

That’s our entire main function, now we just need to write our displayClock() function which will use the number of seconds, minutes and hours to display the current time on the LCD display.

Display current time on LCD display

Inside this displayClock() function, we’ll be using various function from my LCD library (their purpose and use is documented in LCD-library.h) to format how the numbers are displayed.

The first thing we do is to position the cursor so that the clock will be centered. Since the whole clock (“HH:MM:SS”) takes up 8 characters, when displayed on my 16 character LCD display, it will be centered when the first “H” will be in the fifth column. Therefore, we set the initial position of the cursor to 5th column, 1st row:

1
LCD_setCursor(5, 1);

Now we’re going to display the current number of hours. Here we must think of two cases: the first one being that the number is less than 10, in which case it takes up just 1 character and we must add a leading 0; in the second case, it’s greater or equal to 10, in which case it takes up 2 characters and thus no leading 0 is required.

In case the number of hours takes up just 1 character (is less than 10), we’ll display a leading 0, move the cursor one place to the right, display the actual number of hours and then move the cursor one place to the right again:

1
2
3
4
5
6
if(hours < 10) {
  LCD_displayNumber(0);
  LCD_moveRight(1);
  LCD_displayNumber(hours);
  LCD_moveRight(1);
}

In case that the number of hours takes up 2 characters (it’s greater or equal to 10), we’ll just display the whole number and then move the cursor by 2 places to the right:

1
2
3
4
else {
  LCD_displayNumber(hours);
  LCD_moveRight(2);
}

We’ll now display a colon to separate the number of hours and minutes. Then, we move the cursor one place to the right.

1
2
LCD_displayChar(':');
LCD_moveRight(1);

Now we’ve got displayed the number of hours with a colon like so: “HH:”. We’ll do the exact same thing with minutes as we did with hours, just make sure you replace every `minutes` variable with `hours`:

1
2
3
4
5
6
7
8
9
10
11
12
13
if(minutes < 10) { // `minutes` take up 1 char
  LCD_displayNumber(0);
  LCD_moveRight(1);
  LCD_displayNumber(minutes);
  LCD_moveRight(1);
}
else { // `minutes` take up 2 chars
  LCD_displayNumber(minutes);
  LCD_moveRight(2);
}

LCD_displayChar(':');
LCD_moveRight(1);

We’ve got displayed “HH:MM:” so far, now we just need to add seconds. This will be the same as with hours or minutes, just without the ending colon and with seconds instead of hours or minutes:

1
2
3
4
5
6
7
8
9
10
if(seconds < 10) {
  LCD_displayNumber(0);
  LCD_moveRight(1);
  LCD_displayNumber(seconds);
  LCD_moveRight(1);
}
else {
  LCD_displayNumber(seconds);
  LCD_moveRight(2);
}

Finally, our function should now display “HH:MM:SS”, which is what we want.

So that’s it! To recap, we’ve configured our LCD library, initialized the timer, made an ISR which increases a variable with every overflow interrupt, then, using this variable, we’ve calculated the current time, and finally, we’ve displayed this time on the LCD display.

In the next post, I’ll show you how to use PWM (Pulse Width Modulation), which heavily depends on timers, with an example of fading an LED!

Documentation

Source code

For your enjoyment, here’s the full source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/* (c) 2014 Marian Longa. All rights reserved. */
#include <xc.h>
#include "LCD-library.h"

#pragma config OSC = HS // High-Speed oscillator
#pragma config WDT = OFF // Watchdog timer OFF
#pragma config LVP = OFF // Low-voltage programming OFF

#define _XTAL_FREQ 20000000 // 20MHz crystal

volatile int
  count = 0,
  seconds = 0,
  minutes = 0,
  hours = 0;

void delay_ms(int ms) {
  for(int i = ms; i > 0; i--) __delay_ms(1);
}

void timer_init() {

  // clear timer1 high and low holding registers
  TMR1H = TMR1L = 0;

  // reading in 2 8-bit operations
  //T1CONbits.RD16 = 0;

  // Prescaler value of 1:4
  T1CONbits.T1CKPS1 = 1;
  T1CONbits.T1CKPS0 = 0;

  // disable timer 1 oscillator
  //T1CONbits.T1OSCEN = 0;

  // timer 1 uses internal clock (Fosc/4)
  //T1CONbits.TMR1CS = 0;

  // enable interrupt flag for timer 1
  PIE1bits.TMR1IE = 1;

  // turn timer 1 on
  T1CONbits.TMR1ON = 1;

  // enable peripheral interrupts
  INTCONbits.PEIE = 1;

  // enable global interrupts
  INTCONbits.GIE = 1;
}

// interrupt service routine
void interrupt ISR() {
  if(PIR1bits.TMR1IF == 1) { // has the interrupt been fired by timer 1? (check for interrupt flag from timer 1)
    count++; // increase number of overflows
    PIR1bits.TMR1IF = 0; // set the interrupt/overflow flag back to 0
  }
}

// displays the clock on LCD
void displayClock() {

  LCD_setCursor(5, 1);

  // HOURS
  if(hours < 10) {
    LCD_displayNumber(0);
    LCD_moveRight(1);
    LCD_displayNumber(hours);
    LCD_moveRight(1);
  }
  else {
    LCD_displayNumber(hours);
    LCD_moveRight(2);
  }

  LCD_displayChar(':');
  LCD_moveRight(1);

  // MINUTES
  if(minutes < 10) {
    LCD_displayNumber(0);
    LCD_moveRight(1);
    LCD_displayNumber(minutes);
    LCD_moveRight(1);
  }
  else {
    LCD_displayNumber(minutes);
    LCD_moveRight(2);
  }

  LCD_displayChar(':');
  LCD_moveRight(1);

  // SECONDS
  if(seconds < 10) {
    LCD_displayNumber(0);
    LCD_moveRight(1);
    LCD_displayNumber(seconds);
    LCD_moveRight(1);
  }
  else {
    LCD_displayNumber(seconds);
    LCD_moveRight(2);
  }
}

int main() {

  LCD_init(); // initialize LCD with values from `LCD-library.h`

  // set initial time
  hours   = 16;
  minutes = 45;
  seconds = 50;

  timer_init(); // initialize timer1

  displayClock(); // display initial time on LCD

  volatile int change = 0;
  while(1) {
    // calculate current time
    if(count == 19) {seconds++; count = 0; change = 1;}
    if(seconds == 60) {minutes++; seconds = 0; change = 1;}
    if(minutes == 60) {hours++; minutes = 0; change = 1;}

    // if current time has changed, update the clock
    if(change) {displayClock(); change = 0;}
 }

}

References

Leave a comment