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:
-
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.
- 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)
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:
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.


Recent Comments