PIC18 Basics - Timers

When writing firmware, one of the most common tasks that needs to be done is creating a system clock. This clock, often measuring milliseconds, is used for a great variety of things - switch debouncing, countdowns, blinking lights or any repeated tasks. In order to create a timer, you only need to know a few things about your project's oscillator speed, how to calculate the timer value and how to use interrupts.

In this tutorial, I will be using a PIC18F66K80 (datasheet) and the XC8 1.30 compiler. Most of the code samples in this lesson will only need slight modification to be adapted to any device in the PIC18 series. I will mention the datasheet frequently in this article, along with page numbers for reference. This is so you can compare the PIC18F66K80's datasheet to your devices to find the appropriate configuration settings for your project.

Before I delve into how timers work, I want to define a few things that are important concepts when it comes to timers.

  • The oscillator is a piece of hardware (sometimes internal, but I will use an external crystal for my examples) that generates a wave at a set frequency. I use an 8MHz crystal connected to pins RA6 and RA7.  This 8mhz frequency is what controls the timing on my system, and is known as the system cycle.
  • The instruction cycle, on the PIC18F series, runs at 1/4 the speed of the oscillator. Each period of the instruction cycle will process one instruction within the code. On my setup, I have an instruction cycle running at 2mhz.
  • Phase-lock loop is an optional configuration that I will not be using in this example, but it can be used to speed up the instruction cycle by 4. Were I to use PLL, my 8MHz system cycle would produce an 8MHz instruction cycle.

Part One, Understanding your Hardware

When creating a timer, it is very important to know the speed of your oscillator, and whether it is an internal or external oscillator. The PIC18F66K80 family of microcontrollers contains 3 internal oscillators, 31KHz, 500KHz and 16MHz (datasheet page 53), but I typically use an 8MHz external crystal. Be sure to read your datasheet to understand what oscillator you are using, otherwise you may not get the configuration bits set properly and your microcontroller will not work at all. Since I am using an external crystal, I need to set my configuration bits accordingly.

The relevant configuration bits are stored in CONFIG1H (datasheet page 464), specifically bits 3-0, the FOSC section. As shown on page 53, I am using a high speed external crystal, which is an HS type of oscillator, and at 8MHz, it is considered a medium power oscillator. That means the correct config setting for my 8MHz external crystal is to set FOSC to HS1, which I can do through this line:

#pragma config FOSC=HS1

This line is a compile time flag that will set the config bits when the microcontroller is flashed. Since it is a compile time flag, the oscillator cannot be changed at runtime through this method.

Part Two, Creating a Timer

Now that the device is configured to have the correct oscillator, you can begin to set up a timer. The PIC18F66K80 has 5 timers (datasheet page 211), and you can use all 5 of them and have them set to different speeds. However, I will only focus on Timer 0 in this article. But I will note a few things about the other timers before moving on.

Timers 1-4 are used by the CCP/ECCP modules. If you plan to make use of these modules, be wary of using those timers elsewhere, as this can impact your CCP/ECCP timing. Timer0 has an 8-bit prescalar, while the remaining timers have a much smaller prescalar (more on the prescalar later and why it is useful). Timers 1 and 3 have a 16 bit counter, while 2 and 4 have an 8 bit counter. Timer 0 can be toggled between 8 and 16 bit. All of these things combine to make Timer 0 ideal as the main system timer, but the others can be used if you understand what you are doing.

So how does a timer work? Timer 0 (TMR0, datasheet page 213) is a 16 bit register that increments on every instruction cycle, and interrupts on overflow. What does that mean? As a 16 bit register, it has a range of 0-65535. On every instruction cycle (Fosc / 4 = 8MHz  / 4, or 2 million times per second), the value in that register goes up by one. When the register is at 65535 and increments, it will roll over and go back to 0. When this happens, the timer 0 interrupt flag (TMR0IF) is raised. When an interrupt flag is raised, your interrupt code is triggered and you can handle the interrupt.

In order to do all of this, we need to do a few things. We need to enable Timer 0, enable interrupts, create an interrupt, and create a long int value to store the elapsed milliseconds. I will also disable prescaling for now, and explain more about that later. The code to do all of that is this:

// include the XC and PIC18 libraries
#include <xc.h>
#include <pic18.h>

#pragma config FOSC=HS1

// number of milliseconds that the device has been on
unsigned long int milliseconds;

//  main loop of the program
void main() {
    // enable Timer 0, part of the T0CON register (datasheet page 211)
    TMR0ON = 1;
    
    // enable Timer 0's interrupt
    TMR0IE = 1;
    
    // enable general interrupts
    GIE = 1;
    
    // main loop
    while (1) { }
}

// interrupt service routine to handle all interrupts
void interrupt main_isr(void) {
    // if Timer 0's interrupt flag has been raised
    if (TMR0IF) {
        // increment the millisecond counter
        milliseconds++;
        // lower the interrupt flag
        TMR0IF = 0;
    }
}

Let's do the math on this to see how often the interrupt will be raised. If the instruction cycle is 2MHz, that makes the instruction period 1/2000000 = 0.0000005 seconds. If the interrupt occurs every 65536 pulses, that means an interrupt will happen every 0.0000005 * 65536 = 0.032768 seconds, or 32.768 milliseconds. But we wanted to do our interrupt every 1 millisecond to create a 1 millisecond system clock, so this won't do. How do we speed up the interrupt without changing our oscillator?

That's easy - when the TMR0 register overflows, we don't have to leave it at 0. We can set it to whatever value we like, so long as it is between 0 and 65535, inclusive. This will shorten the duration of the timer by forcing the overflow to happen sooner. One catch with this is that manually adjusting the value of TMR0 will cause a delay of 2 instruction cycles before the system clock is re-synced to Timer 0. Adjust your TMR0 value accordingly.

Now let's calculate out what to set the value to. We know that the increment period is 0.0000005 seconds and we know we want an interrupt every millisecond. 1 millisecond is 0.001 seconds. So we do 0.001 / 0.0000005 = 2,000 increments. If we want to force the interrupt to occur every 2,000 increments, then we manually set the value of TMR0 during the interrupt to 65536 - 2000 = 63536. Now look at our modified interrupt to see how this would look:

// interrupt service routine to handle all interrupts
void interrupt main_isr(void) {
    // if Timer 0's interrupt flag has been raised
    if (TMR0IF) {
        // increment the millisecond counter
        milliseconds++;
        // set the Timer 0 value to 65536 - 2000 = 63536
        TMR0 = 63536;
        // lower the interrupt flag
        TMR0IF = 0;
    }
}

Our timer will now interrupt every millisecond and increment the milliseconds value. We can then look at the milliseconds value to aid in debouncing, blink LEDs, or any other task that cares about timing.

Part Three, Prescaling

The last part of this article is optional, but it does show one additional feature of the timer that can be very important - the prescalar. Imagine if we wanted to create a timer that increments every second. Based on our previous math, that would be once every 2,000,000 increments. But our timer is only 16-bit, so we can't set it to 65,536 - 2,000,000. So how do we do this? We use the prescalar value.

In the previous example, the TMR0 value incremented on every instruction cycle. That is a prescaling value of 1:1, or no prescaling whatsoever. What if we wanted TMR0 to increment on every other instruction cycle? Or every 16th or 128th? Those would be 1:2, 1:16, or 1:128 (datasheet page 211). Timer 0 contains an 8 bit prescalar register that can be configured with 1:1, 1:2, 1:4, 1:8, 1:16, 1:32, 1:64, 1:128 or 1:256 prescaling.

If we were to use 1:256 prescaling, that means that the TMR0 value increments every 256 instruction cycles, and at 0.0000005 seconds per instruction cycle, that means TMR0 increments every 0.0000005 * 256 = 0.000128 seconds. Now can we interrupt every second? Doing the math on that, it would require 1 / 0.000128= 7812.5 increments. That is well under the 65535 cap! But, it is not a whole number. You can't have half of an increment. So instead we will set the prescalar to 1:128, which doubles the number of increments to 15625.

So using the prescalar to adjust how many pulses are needed to increment TMR0, we can set a system timer that interrupts every second. The full code for that would be:

// include the XC and PIC18 libraries
#include <xc.h>
#include <pic18.h>

#pragma config FOSC=HS1

// timer that will increment 4 times per second
unsigned long int timer;
 
//  main loop of the program
void main() {
    // enable Timer 0, part of the T0CON register (datasheet page 211)
    TMR0ON = 1;
    
    // enable prescaler for Timer 0
    T0CONbits.PSA = 0;
    
    // prescale TMR0 to 1:128
    T0CONbits.T0PS = 0b110;
    
    // alternatively, you can set the 3 previous commands in one line, like this:
    // T0CON = 0b10000110;
    
    // enable Timer 0's interrupt
    TMR0IE = 1;
    
    // enable general interrupts
    GIE = 1;
    
    // and you can set those last 2 in one line, like this:
    // INTCON = 0b10100000;
    
    // main loop
    while (1) { }
}
 
// interrupt service routine to handle all interrupts
void interrupt main_isr(void) {
    // if Timer 0's interrupt flag has been raised
    if (TMR0IF) {
        // increment the seconds counter
        timer++;
        // adjust the TMR0 value to 65536 - 15625 = 50271
        TMR0 = 50271;
        // lower the interrupt flag
        TMR0IF = 0;
    }
}