«

»

Mar 20

Getting Started with PIC Microcontrollers–The Timer Peripheral and Interrupts

This time we shall take a look at the timer peripheral; one of the most common peripherals, available on just about every microcontroller. We will also have a brief look at interrupts and how useful they are.

An excellent way of demonstrating both is by revising the original LED blink code in Getting Started with Microcontrollers to make it a lot more efficient.

Here is the original code for reference:

/* 
 * File:   led_blink.c
 * Author: Oli Glaser
 *
 * Created on 02 January 2013, 06:59
 */

// main include file - this contains all the "friendly names"
// for the pins and peripherals
#include <xc.h>
// Turn the Watchdog Timer off
#pragma config WDTE = OFF 

void delay(int d); // This is the prototype declaration for the Delay function

int main(int argc, char** argv) {

    TRISBbits.TRISB6 = 0; // This sets pin RB6 to output

    while(1) // Infinite loop
    {
        PORTBbits.RB6 = 1;  // Set RB6 pin to logic high (5V)
        delay(500);         // Delay for roughly 500ms
        PORTBbits.RB6 = 0;  // Set RB6 pin to logic low (0V)
        delay(500);         // Delay for roughly 500ms
    }

}

// Delay routine
void delay(int d)
{
    int i;  // Declare variable to be used in the loop

    while(d)    // While d > 0
    {
        i = 100;    // set i to 100 for inner loop
        while(i)    // while i > 0
        {
            i--;    // decrement i (e.g. i will equal 99, 98, 97,...)
        }

        d--;    // decrement d
    }
}

So what’s so bad about this code? Two main things:

  1. The timing is not very accurate. Even if we calculate and count the cycles properly (which we don’t do in the above code, it’s just a rough estimate based on observation of the LED) we have to factor in the time taken to execute the instructions for the delay() loop and main while(1) loop, which complicates matters, and depending on the compiler or optimisation settings may vary quite a bit. 

  2. It spends 99% of it’s time doing nothing but counting cycles, which is a “criminal” waste of processor power. While it is doing this, the main loop is tied up and cannot do anything else. In this code that’s not an issue as we are not doing anything else, but imagine if we were – for example using serial communications such as SPI, I2C, UART, etc, and a message arrived during the delay routine; it would almost certainly be missed. Since we spend less than 1% of the time in the main loop the chances of this are very high.

How can we fix these issues?

For the first issue, we could always use the same compiler, make sure it is always set to the same optimisation level, use the tools to count exactly how many instructions are executed in the loops, and adjust the delay routine for this.

For the second issue, if were expecting it, during the delay loop we could poll for incoming data on every cycle of the loop and read/act on it if some arrives. This type of technique is used sometimes but it’s still quite an inefficient way of doing things since we are checking continuously even though no data may arrive for long periods. Every check requires processor cycles, most of which would be redundant and also have to factored into the delay timing.

So we come to interrupts. To clarify the concept with an everyday analogy, imagine you (the processor) have a task which involves switching on and off a signal light (our LED) every couple of hours. You have nothing but a basic clock on the wall. To avoid being late for the switching you would have to continually check the clock to see if it’s time yet (i.e. “polling”)

Now imagine instead of this you have a watch which has an alarm function (the peripheral) which will alert you a couple of minutes beforehand (the interrupt). In this case you have freed up your time to do other things instead of continually checking the wall clock. Now, humans are intelligent enough to have a sense of time so in reality we probably wouldn’t need to check the clock e.g. every minute, and could manage to do other things in the meantime (even so, the alarm system is more efficient even for the human). However a processor is “dumb” and has to check repeatedly until it has a match for its delay setting, so we waste a lot of cycles.   

So if we do things correctly, we should end up with >99% of the processors time free, and <1% taken up servicing the interrupt when it arrives. So let’s take a look at the revised code for our LED blink firmware. When debugging, an easy way to check how much time you are spending in a routine is to set a spare pin high on entry and low on exit – then you can watch it on the scope to see the percentage of time it is high. For multiple routines you can use more than one pin (you can add this into the code below and the previous code to compare the differences in timing – I’ll leave this as an exercise for the reader)

NOTEIf you choose to use a pin other than RB6 as your LED pin, then as Chris discovered and kindly took the time to comment, you are likely to run into a problem with the default analogue pin settings. The quick solution is to add:

ANSEL = 0;

ANSELH = 0;

To your init function – The ADCON0 register should default to 0 on reset anyway so you should not need to set this (but it won’t do any harm if you wish to)

Basically, any pins with an analogue function (e.g. marked AN0, AN1, AN2, etc) default to this state on power up, so you need to turn the analogue inputs off in order for them to function as digital pins. The name of the register can vary from part to part (for the PIC16F690 it is as above), but the details are always there in the datasheet under the I/O PORTS section and the ADC section. There are quite a few other things that can cause pins not to function as expected, but the analogue settings are the main one that new users run into first. 

Interrupt based LED blink

/* 
 * File:   led_blink_timer.c
 * Author: Oli Glaser
 *
 * Created on 20 March 2013, 21:31
 */

// main include file - this contains all the "friendly names"
// for the pins and peripherals
#include <xc.h>
// Turn the Watchdog Timer off
#pragma config WDTE = OFF

// Function definitions
void init(void);

int main(int argc, char** argv) {

    // To keep things tidy we put the setup in an initialisation routine this time
    init(); 

    while(1) // Infinite loop
    {
        /* Sit and wait for interrupt!
         * We could be doing other things here
         * in the meantime */
    }
    
}

// Interrupt Service Routine (keyword "interrupt" tells the compiler it's an ISR)
void interrupt int_routine(void)
{
    // Check it's the timer that has interrupted
    if (PIE1bits.TMR1IE && PIR1bits.TMR1IF)
    {
        static char i = 0;
        PIR1bits.TMR1IF = 0;
        T1CONbits.TMR1ON = 0;   // Turn timer off to reset count register
        TMR1H = 0x0B;   // Reset timer count - 0x0BDB = 1Hz
        TMR1L = 0xDB;   //
        T1CONbits.TMR1ON = 1;   // Turn timer back on
        PORTBbits.RB6 = ~PORTBbits.RB6;  // Toggle RB6

    }

    // process other interrupt sources here, if required
}

// Initialisation routine
void init(void)
{
    TRISBbits.TRISB6 = 0;   // This sets pin RB6 to output

    // Setup Timer 1
    T1CON = 0;
    T1CONbits.T1CKPS0 = 1;  // T1CKPS = 11 = 1:8 prescaler
    T1CONbits.T1CKPS1 = 1;  // so timer clock = 1MHz / 8 = 125kHz
    /* Calculating for 1 Hz LED flash, so period needs to be 2Hz as
     * interrupt toggles between on/off. Timer 1 clock is 125kHz, so
     * for 2Hz, we divide 125kHz by 2 = 62,500. Then we subtract this
     * from the rollover value of 65,535, so 65,535 - 62,500 = 3035
     * which is 0x0BDB in hex. Timer 1 has two 8-bit high and low
     * registers, so we put 0x0B in the high and 0xDB in the low */
    TMR1H = 0x0B;           // 65,535 - 62500 = 3035 = 0x0BDB
    TMR1L = 0xDB;           // to count for 0.5 seconds till timer rollover
    T1CONbits.TMR1ON = 1;   // Turn timer on
    INTCONbits.GIE = 1;     // Enable global interrupts
    INTCONbits.PEIE = 1;    // Enable peripheral interrupts
    PIR1bits.TMR1IF = 0;    // Clear Timer 1 interrupt flag
    PIE1bits.TMR1IE = 1;    // Enable Timer 1 interrupt
}

The code is well commented, so hopefully not too much needs to be said about it. I would recommend reading the very detailed Timer 1 section (section 6) thoroughly in the datasheet to get a full idea of what the peripheral is capable of.  If anyone has any questions, just leave a comment.

So how accurate is the timing? Even with the internal RC oscillator it’s just about spot on (for better timing accuracy we would use a crystal oscillator) Here is a scope clip of the results:

Blog - Timer LED Blinkb We can see according to the cursor measurement (in the white box, taken between the two vertical purple dotted lines) it’s 1Hz exactly. The oscilloscope is pretty accurate frequency wise so we can assume it’s very close (it does have a 6 digit frequency counter but this only works for frequencies over 2Hz)

Hopefully this simple introduction has helped to demonstrate the importance of peripherals and interrupts. In future tutorials we will look at combining tasks to get an even better sense of how much more efficient they can make your firmware.

11 comments

Skip to comment form

  1. Mark

    Thanks buddy, helps a lot. I am waiting for coming up chapters, especially on adc, sensors, motor control, pmw, and inputs.

    hope you take time to share more chapters.

    thanks again.

    Mark

  2. kiranvarma - npeducations

    How many samples that we can take for a temperature sensor using ADC? like our voice sampling rate could you please help me about this

    1. Oli G

      Any ADC will have the maximum sampling rate in it’s specs (PIC datasheets have an electrical specs section at the end).
      Typical maximum values for PIC16’s are around 100ksps IIRC, though some dsPICs have sample rates exceeding 1Msps. Anything over 40kHz or so is fine for quality audio (CDs use 44.1kHz) and certainly temperature sensors (mostly it is not necessary to sample temperature more than a few times a second. If you need much higher than 1Msps a dedicated ADC is necessary.
      Cheers,
      Oli

  3. dieta

    Hey, I think your website might be having browser compatibility issues.
    When I look at your blog in Chrome, it looks fine but when opening in Internet
    Explorer, it has some overlapping. I just wanted
    to give you a quick heads up! Other then that, amazing blog!

    1. Oli G

      Many thanks for letting me know, I will check it out and see what I can do (I use Chrome so I haven’t tested in IE, but I need to do so for it and other browsers as the site grows)
      Cheers,
      Oli

  4. Chris

    Hi, Thanks for taking the time to make these tutorials (I used to work as a programmer in assembly – making the transition to C and these are really helpful).

    I did have a lot of head scratching with this code not working for me (I’m using Port C 0 as i’m using the microchip LPC board). For anyone else doing the same, it turns out you need to disable the ADC module for this to work correctly with C.0 by adding the following lines to the init function:

    ANSEL = 0;
    ANSELH = 0;
    ADCON0 = 0;

    1. Oli G

      Hi Chris,
      Thanks very much for this comment, I will update the examples to mention the analog inputs. In fact it may be worth a whole page on the many possible reasons why pins will not function normally (although forgetting to turn analog inputs off is the main one)
      Best Regards,
      Oli

  5. vedic maths

    I could not resist commenting. Exceptionally well written!

    1. Sidhu Burre

      Good post on PIC controllers. Thank you.

  6. Abhijit

    Hi,
    Can you please help to write a PWM code in C. I am new in micro controller.
    my is id abhijit2683@gmail.com

  7. Carl @ LED Display

    Hi, Thanks for taking the time to make these tutorials (I used to work as a programmer in assembly – making the transition to C and these are really helpful).

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>