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
- Previous setup with LCD display connected to PIC
- Knowledge of timers which I described before
- Download my LCD library (or any other you’re familiar with)
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:
#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:
#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:
#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:
#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:
#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:
// 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:
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:
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 and the prescale value of 4, this will produce the output frequency
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:
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:
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):
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:
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:
hours = 16;
minutes = 45;
seconds = 5;
Then, we’ll initialize our timer by calling the init function we’ve made before:
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:
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).
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.
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).
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.
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.
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.
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:
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:
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:
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.
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`:
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
:
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:
/* (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
- PIC18F452 datasheet: http://ww1.microchip.com/downloads/en/DeviceDoc/39564c.pdf
- My LCD library: https://github.com/marianlonga/LCD-library
Leave a Reply