Project Overview
This adjustable timer displays seconds, minutes, and hours and is powered by a PIC16F 877A microcontroller. There are five buttons included: start/stop, up for incrementing digits, down for decrementing digits, and left and right for moving between digits during adjustment. I’ve included a relay circuit that will trigger when the timer is finished.
Introduction
A timer is a typical project but not really a simple one. Here, I designed a six-digit countdown timer that, when finished, will trigger a relay. The timer can be set to a maximum of 99 hours, 59 minutes, and 59 seconds and to a minimum of 1 second. This is useful as a timed switch for appliances or other electrical devices that draw power from mains.
Schematic Diagram
I wanted to use the least amount of components possible but I also don’t want to make the code too long. Thus, I settled to use one decoder even if it’s possible not to use one. The POV refresh rate is based on timer0 overflow with x32 prescale. This is using a 4 MHz crystal oscillator. Adjust the prescale through the OPTION register if you are using other oscillator frequencies.
Code
/*
* File: timer_relay.c
* Author: Roland Pelayo
* For Adjustable Timer with Relay Project
* See full tutorial at: https://www.teachmemicro.com/adjustable-timer-relay
* Created on April 5, 2018, 11:25 AM
*/
#pragma config FOSC = XT // Oscillator Selection bits (XT oscillator)
#pragma config WDTE = OFF // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = OFF // Power-up Timer Enable bit (PWRT disabled)
#pragma config BOREN = OFF // Brown-out Reset Enable bit (BOR disabled)
#pragma config LVP = OFF // Low-Voltage (Single-Supply) In-Circuit Serial Programming Enable bit (RB3 is digital I/O, HV on MCLR must be used for programming)
#pragma config CPD = OFF // Data EEPROM Memory Code Protection bit (Data EEPROM code protection off)
#pragma config WRT = OFF // Flash Program Memory Write Enable bits (Write protection off; all program memory may be written to by EECON control)
#pragma config CP = OFF // Flash Program Memory Code Protection bit (Code protection off)
#define _XTAL_FREQ 4000000
#include <xc.h>
//structs for the digits
struct _digits{
int ones;
int tens;
};
struct timer{
struct _digits digits; //a struct within a struct just because I wanted to :)
};
//declare struct objects for every tick
struct timer seconds = {9,5};
struct timer minutes = {9,5};
struct timer hours = {9,5};
//declare struct objects for updating
struct timer set_seconds = {9,5};
struct timer set_minutes = {9,5};
struct timer set_hours = {9,5};
bit start_timer; //flag for starting or stopping the timer
int counter = 0; //used for counting up/down digits during time adjustment
int shifter = 0; //used for shifting digits left and right during time adjustment
int dummy; //used to clear mismatch for RB Change interrupt
//Function for updating time every tick
void runTime(void){
RB1 = !RB1; //make the dots blink for every tick
seconds.digits.ones--;
if(seconds.digits.ones < 0){
seconds.digits.tens--;
seconds.digits.ones = 9;
if(seconds.digits.tens < 0){
minutes.digits.ones--;
minutes.digits.tens = 6;
if(minutes.digits.ones < 0){
minutes.digits.tens--;
minutes.digits.ones = 9;
if(minutes.digits.tens < 0){
hours.digits.ones--;
minutes.digits.tens = 6;
if(hours.digits.ones < 0){
hours.digits.tens--;
hours.digits.ones = 9;
}
}
}
}
}
if(seconds.digits.ones == 0 && seconds.digits.tens == 0 && minutes.digits.ones == 0 && minutes.digits.tens == 0 && hours.digits.ones == 0 && hours.digits.tens == 0){
RB2 = 1;
start_timer = 0;
}
set_seconds.digits.ones = seconds.digits.ones;
set_seconds.digits.tens = seconds.digits.tens;
set_minutes.digits.ones = minutes.digits.ones;
set_minutes.digits.tens = minutes.digits.tens;
set_hours.digits.ones = hours.digits.ones;
set_hours.digits.tens = hours.digits.tens;
}
//Function for adjusting time
void adjustTime(int direction, int position){
if(direction == 0){
//decrement the digits for every button press
switch(position){
case 0:
set_seconds.digits.ones--;
if(set_seconds.digits.ones < 0){
set_seconds.digits.ones = 9;
}
break;
case 1:
set_seconds.digits.tens--;
if(set_seconds.digits.tens < 0){
set_seconds.digits.tens = 5;
}
break;
case 2:
set_minutes.digits.ones--;
if(set_minutes.digits.ones < 0){
set_minutes.digits.ones = 9;
}
break;
case 3:
set_minutes.digits.tens--;
if(set_minutes.digits.tens < 0){
set_minutes.digits.tens = 5;
}
break;
case 4:
set_hours.digits.ones--;
if(set_hours.digits.ones < 0){
set_hours.digits.ones = 9;
}
break;
case 5:
set_hours.digits.tens--;
if(set_hours.digits.tens < 0){
set_hours.digits.tens = 9;
}
break;
}
}else{
//increment the digits for every button press
switch(position){
case 0:
set_seconds.digits.ones++;
if(set_seconds.digits.ones > 9){
set_seconds.digits.ones = 0;
}
break;
case 1:
set_seconds.digits.tens++;
if(set_seconds.digits.tens > 5){
set_seconds.digits.tens = 0;
}
break;
case 2:
set_minutes.digits.ones++;
if(set_minutes.digits.ones > 9){
set_minutes.digits.ones = 0;
}
break;
case 3:
set_minutes.digits.tens++;
if(set_minutes.digits.tens > 5){
set_minutes.digits.tens = 0;
}
break;
case 4:
set_hours.digits.ones++;
if(set_hours.digits.ones > 9){
set_hours.digits.ones = 0;
}
break;
case 5:
set_hours.digits.tens++;
if(set_hours.digits.tens > 9){
set_hours.digits.tens = 0;
}
break;
}
}
}
//Function for checking buttons for adjusting time
void checkButtons(){
__delay_ms(200);
if(!RB3){
shifter++;
if(shifter > 5) shifter = 0;
}else if(!RB7){
shifter--;
if(shifter < 0) shifter = 5;
}else if(!RB5){
adjustTime(1, shifter);
}else if(!RB6){
adjustTime(0, shifter);
}else{;}
//the updating digits must be the same as the adjusted digits
switch(shifter){
case 0:
seconds.digits.ones = set_seconds.digits.ones;
break;
case 1:
seconds.digits.tens = set_seconds.digits.tens;
break;
case 2:
minutes.digits.ones = set_minutes.digits.ones;
break;
case 3:
minutes.digits.tens = set_minutes.digits.tens;
break;
case 4:
hours.digits.ones = set_hours.digits.ones;
break;
case 5:
hours.digits.tens = set_hours.digits.tens;
break;
}
}
void main(void) {
INTCON = 0b10101000; //Use RB Change interrupt
OPTION_REG = 0b00000100; // Prescale
TRISB = 0b11111000; //RB1, RB2 outputs, RB3-RB7 inputs
TRISC = 0; //All PORTC output
TRISD = 0; //All PORTD output
RB2 = 0; //relay pin is initialized as cleared
PORTC = 0; //initialize PORTC and PORTD as cleared
PORTD = 0;
while(1){
//When Start/Stop is pressed, the start_timer variable is toggled
//Start Timer
while(start_timer){
__delay_ms(1000); //ticks every second
runTime(); //update digits per the specified delay above
}
//Stop/Pause Timer
while(!start_timer){
checkButtons(); //adjust time
}
}
return;
}
//Function to turn on the dot as indicator of the digit currently adjusting
void setDot(void){
if(!start_timer){
if(shifter == counter-1){
RB1 = 1;
}else{
RB1 = 0;
}
}
}
//Interrupt service routine: The interrupts used are RB Change and Timer Interrupt. The shifting of digits is done every time
// timer0 overflows. The RB change interrupt is used for starting and stopping the timer.
void interrupt isr(void){
//RB Change interrupt is triggered
if(RBIF){
dummy = PORTB;
if(!RB4){
if(!start_timer){
start_timer = 1;
}else{
start_timer = 0;
}
}
RBIF = 0; //manual clearing of interrupt flag
}
//Timer Overflow interrupt is triggered
if(TMR0IF){
//switch digits for every interrupt. Read about POV for better understanding
counter++;
switch(counter){
case 1:
PORTC = 0b11011111;
PORTD = seconds.digits.ones;
setDot();
break;
case 2:
PORTC = 0b11101111;
PORTD = seconds.digits.tens;
setDot();
break;
case 3:
PORTC = 0b11110111;
PORTD = minutes.digits.ones;
setDot();
break;
case 4:
PORTC = 0b11111011;
PORTD = minutes.digits.tens;
setDot();
break;
case 5:
PORTC = 0b11111101;
PORTD = hours.digits.ones;
setDot();
break;
case 6:
PORTC = 0b11111110;
PORTD = hours.digits.tens;
setDot();
break;
case 7:
counter = 0;
break;
}
TMR0IF = 0; //manual clearing of interrupt flag
}
}
The project was coded using Microchip XC8. There are some peculiarities with my code, especially the use of structs. You can actually remove the structs and just use simple variables. I just find the structs more readable given the number of variables I had to use.
I used two interrupts in this code: timer overflow and RB change interrupt. The timer overflow interrupt is used for POV while the RB change interrupt is responsible for starting or stopping the timer. The adjustments are not done with interrupts but by simple button testing. The debounce problem is handled by a delay after every button pressed.