PIC TMR0 Interrupts, (or any PIC interrupts!) can be very confusing to setup, but the rewards are well worthwhile! They allow you to have something happen after a certain period of time, or for some interrupt types, something happens when a button is pushed or a sensor reads a certain value. This means that the microcontroller can automatically respond to user input. This article shows how to set up a PIC16F628 TMR0 Interrupt, using MikroC.
What on earth are interrupts?
An interrupt is basically what it sounds like. It is a piece of code that interrupts the main program and is executed when asked. Once the interrupt code is executed, the normal program flow resumes. PIC Interrupts can be based on PORT values (e.g. if PORTB is high) or on a "timer overflow".
On the PIC16F628A (and most PICs), a register is set aside to act as a counter. This basically counts upwards as the microcontroller clock "ticks". Counters are commonly "8 bits", meaning they can count from 0 to 255. When the counter tries to go past 255, it "overflows" and goes back to 0. If the correct settings are made in the registers, this triggers the PIC TMR0 Interrupt code to run.
Why would I use interrupts?
One way to get the same functionality as an interrupt is to put in some sort of check in your main loop. Say you want to execute some code when a button is pressed, in your program loop you could have something similar to the following:
do {
if (BUTTON == 1)
{
execute your "interrupt code"
}
} while(1);
This code looks fairly simple, but what if you have five buttons, or want something to be checked on a regular basis (say every second). Or what if your main loop has some extremely intensive calculations going on which means that your test of whether the button is pushed or not may only happen every 20 seconds or so? Not a very good user experience!
An interrupt basically allows you to offload this functionality from the program loop and let the micro hardware work out when the button is pushed or a second has elapsed. This article focuses on PIC TMR0 interrupts, so if you are interested in the PORT interrupts, then have a look at the datasheet!
The Code
I have broken out the interrupt configuration separately in the code below and the comments should be fairly self explanatory. Basically we tell the micro what source to use for its timer interrupts, "scale" the timer and then turn on interrupts.
The OPTION_REG.T0CS
bit sets our source for the TIMER0
clock. In this case I
am using the PIC16F628A internal clock and hence I clear (set to 0) this bit.
Recall that most TMR0 registers are 8-bit and can only count up to 255. We can
count the number of overflows in our interrupt code to get a certain time
period, or if we are really clever us the inbuilt prescaler to only call the
interrupt after a certain number of loops. If we set the prescaler to 1:64, like
we have below this means that we are only calling the interrupt every 64 times
that the TMR0 register overflows. The last 3 bits of OPTION_REG
on the 628A
control the prescalar, and OPTION_REG.PSA
sets what the prescaler is applied
to. We clear OPTION_REG.PSA
to turn the prescaler on for TMR0 and set the last
three bits to 1 to set a 1:64 ratio. This can be done in one line with the
following:
OPTION_REG |= 0x07
Finally we set INTCON.GIE
and INTCON.T0IE
to enable interrupts globally, and
TMR0
in particular.
void setup_interrupts()
{
// clear the TMR0 register
TMR0 = 0;
// set up interrupt registers
OPTION_REG.T0CS = 0; // clock source is internal instruction clock
OPTION_REG.PSA = 0; // prescaler assigned to TMR0
OPTION_REG |= 0x07; // set the prescaler to 1:64 scale using an or
INTCON.GIE = 1; // enable global interrupts
INTCON.T0IE = 1; // enable TMR0 interrupt
}
The main loop is pretty basic - it sets some standard PIC16F628A configurations,
calls the setup_interrupts()
function declared above and then loops forever.
void main() {
// some standard PIC16F628A configuration
TRISA = 0x00; // output
TRISB = 0x07; // input
PORTA = 0x01; // set PORTA to off
CMCON = 0x07; // turn off comparators
// setup interrupts
setup_interrupts();
// loop forever and ever and ever and ...
while(1);
}
If we turn the interrupts on, we also need to define an interrupt function. This
is saved at the "interrupt vector" which is basically the location in memory
where the micro goes when an interrupt is called. This is done with the line
void ISR() iv 0x0004
below. This declares an interrupt service routine (ISR)
that is located at interrupt vector 0x0004
in memory. From there its a
standard function!
Note that its considered poor practice to call functions from within your
interrupt, and if you do want a specific function called its best to set a flag
in your interrupt function and process this somewhere in your main loop. The
interrupt function below basically flips PORTA. This can be used to blink an
LED, sound a buzzer or whatever... Once your interrupt routine has run you need
to restart the timer interrupts. This can be done by clearing INTCON.TMR0IF
.
// the interrupt vector
void ISR() iv 0x0004
{
if(INTCON.TMR0IF && INTCON.T0IE)
{
PORTA = ~PORTA;
INTCON.TMR0IF = 0; // unset the interrupt flag for TMR0
}
}
A note on "clocks"
One of the things I found most confusing when learning about PIC timers was the concept of clocks, oscillators, resonators, etc. Why did some micros need an external crystal oscillator, whilst others didn't? What was the benefit of having an external crystal, and what was the difference between a clock and a crystal?
To cut to the chase, most micros will need some sort of external timing source to function at all, and those that have this timing source built in are frequently not very accurate. The best way to find out whether a timing source is needed is to read the data sheet, but as an example the PIC16F628A has an internal crystal but the PIC16F877A does not.