Accurate timing for smaller embedded projects

Introduction

Previously, I have talked about high-precision, multi-task execution on an arduino / AVR based microcontroller.

But what if you don’t need a scheduler? What if you’re only doing one thing, and all of your functionality can easily be fit into a tick period of 5 or 10 ms, but you still want the timing precision to say “These operations will be executed every x ms, and take no longer than x microseconds”. Of course, on a reasonable-sized micro, you could use a TTC scheduler such as the one described in my previous posts, and only have one task, but what if you can’t afford the additional overhead of running the scheduler. If your project calls for a small 8-bit, 8 MHz micro such as the Microchip PIC12F family, you’re limited in both the code space, and processing power. You need to completely strip down the time-triggered philosophy to its bare bones:

  • Use a timer overflow interrupt to generate ‘ticks’ at a known rate, reload the timer and execute your code before waiting for the next interrupt.
  • USE ONLY ONE INTERRUPT (the timer, above)- you want to keep timing predictable, so all inputs and peripherals should be polled in a known order.
  • Use minimal loop constructs and use incrementing counters where possible.

I have successfully implemented such a system using a PIC12F615 running at 8 MHz, with very satisfying results. This code could easily be modified for any microcontroller that is programmed in C (contact me  if you need help porting this to another language)

The project is split into two files: “main.c” contains the initialisation, Interrupt Service Routine (ISR), and main() and tick_operations() functions, and “hardware.c”, containing the peripheral timer (timer1) handling functions, and any other functions for controlling project-specific peripherals (ADC, PWM, GPIO, etc).

(When I have time, I will put this code up on my github)

In main.c:

#include 
#include
#include "hardware.h"
#include "main.h"

/* Configuration as necessary */
__CONFIG(FOSC_INTOSCIO & IOSCFS_8MHZ & WDTE_OFF & PWRTE_OFF & MCLRE_ON & BOREN_ON & CP_OFF);

/* The ticked flag is used to initiate tick_operations() execution */
volatile uint8_t ticked = 0;

/* Program entry point */
void main(void){
	/* Set up some registers */
	PIE1 	= 0x00;
	INTCON 	= 0xC0;
	PIR1 	= 0x00;
	TMR1H 	= 0x00;
	TMR1L 	= 0x00;

	/* ****************************************
	 * Any peripheral initialisation goes here
	 * ****************************************/

	/* Initialise and start peripheral timer1 */
	Timer_init();

	while(1){
		/* Simple tick operation dispatcher - 'ticks' set to 1 in ISR */
		if(ticked == 1){
			ticked = 0;
			tick_operations();
		}
	}
}

/* Business logic executed every time ISR is serviced */
void tick_operations(void){

	/* ************************************************************************************
	 * Do what you want here, but MAKE SURE IT DOESN'T TAKE LONGER THAN YOUR TICK LENGTH
	 * ************************************************************************************/

}

/* ISR executed on overflow of timer1 */
void interrupt timer1_overflow_ISR(void){

	/* Only the timer1 interrupt should be enabled, but just in case we explicitly check for the timer1 interrupt flag */
	if(TMR1IF == 1){
		Timer_reload();		/* Pre-load timer for precise timing */
		ticked = 1;		/* Flag to signal tick_operations() execution */
	}

	/* make sure all interrupts flags are cleared (in case of erroneous interrupts */
	PIR1 = 0x00;
	INTCON &= 0xF8;
}

In hardware.c:


#include "hardware.h"

void Timer_init(void){

	TMR1IE = 0;		/*explicitly disable timer interrupt */
	T1CON = 0x00;

	T1CON |= 0x10;          /* 1:2 prescalar */

	TMR1CS = 0;		/* use internal clock */
	T1ACS = 1;		/* use Fosc */

	Timer_reload();         /* see below */

	TMR1IF = 0;
	TMR1ON = 1;
	PEIE = 1;
	TMR1IE = 1;		/* Enable interrupt, clear flag, start timer */
}

/* Reload value 'tick_delay' dictates the tick time:
 *
 * Timer1 takes 65536 counts to overflow
 * Osc = 8 MHz = 125ns per clock
 * 1 count = 125 ns x prescalar
 * Tick time = (65536 - tick_delay) x prescaler x 125 ns
 */
void Timer_reload(void){
	/* Reload value of 0xB1E0 (45536) gives a tick length of 5 ms */
	TMR1H = 0xB1;
	TMR1L = 0xE0;
}

Overview of operation

The program sits in a while(1){} super-loop waiting for the ‘ticked’ flag. When timer1 overflows, this flag is set, allowing tick_operations() to run. This was the least expensive way I could find to do this; since the PIC only has a small space for the ISR code, I couldn’t just put a call to tick_operations() in the ISR (which would have been preferable in terms of overhead).

My particular application used the PIC’s internal oscillator. A further development of this would be to use an external oscillator, and use sleep mode within the super-loop, rather than the flag system, in a similar way to my Arduino scheduler implementation. This would give more accurate timing, because the processor will always be in a known state when it wakes up and runs tick_operations().

How do I check my timing?

I did this with an oscilloscope. I initially set my tick length to 10 ms. By setting an output pin just before tick_operations(), and clearing it just after, I could accurately (down to the time it takes the micro to change the state of the pin) measure how long my operations would take. The tick length can then be reduced for faster response if necessary. How close you go to the actual execution time (ET) would depend on how confident you are in the amount of ET variation (or ‘jitter’) you are likely to experience, and also how fast you need to respond to changes in conditions.

If you don’t have access to an oscilloscope, set your tick length to the maximum acceptable response time of your system. If your operations take too long, you need to consider either refining your code, or splitting it up into different tasks and using a scheduler.

Advertisements

Tell me what you think...

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s