Further task scheduling with Arduino / AVR

** EDIT 27/04/2016: I have now created a ‘user-friendly’ library compatible with the Arduino IDE **

** EDIT: I have now added the code for this to a git repo here. **

Introduction

My previous post on this topic showed a simple time triggered task scheduler for the Arduino / AVR microcontroller platforms. Although this worked very well, there were some areas which could be improved in the way the scheduler worked. To recap, the previous scheduler worked as follows:

How it worked:

  • The program sits in an empty loop when idle.
  • “Ticks” are generated by the timer driven interrupt.
  • Tasks have a period and an offset:
    • Period – How often the task is executed
    • Offset – The first tick in which the task is executed
  • The period and offset of the tasks are adjusted manually to spread the tasks out, ensuring that tasks don’t collide.
  • The ISR iterates through each task in the schedule and executes any task that is ready to run.
  • Doing this ensures that tasks are executed with precise timing (as precise as the ATMEGA timer allows).

Challenges

The main issue with the above solution is that if the program is sitting in an empty loop, the timer interrupt occurs while the processor is in an unknown state.

/* The loop function does nothing as all tasks are time triggered */
void loop()
{
	/*
	 * Do nothing (actually, do EVERYTHING)
	 */
}

Although the loop(); function, appears empty to the coder in the Arduino layer, the processor is still performing several operations which take varying times to complete. When the timer interrupt occurs, the processor has to complete what it is doing before executing the Interrupt Service Routing (ISR). This introduces a (small but not negligible) variation in the time between the interrupt, and the necessary tasks being executed. This time variation is commonly known as ‘jitter’.

Solution

So how can we overcome this jitter? By ensuring (as far as possible) that the processor is in a known state when the timer interrupt occurs. I have done this using the microcontroller’s Sleep mode. The scheduler now operates as follows (changes from before highlighted in bold):

How it works now:

  • The microcontroller is put into sleep mode at the beginning of the loop() function.
  • “Ticks” are generated by the timer driven interrupt.
  • Tasks have a period and an offset:
    • Period – How often the task is executed
    • Offset – The first tick in which the task is executed
  • The period and offset of the tasks are adjusted manually to spread the tasks out, ensuring that tasks don’t collide.
  • The ISR wakes up the processor and resets some flags.
  • The loop() function contains the scheduler, which iterates through each task in the schedule and executes any task that is ready to run.
  • Any tasks that run return to the loop() function, which eventually repeats and puts the processor back to sleep.

Improved Code

In main.h (along with the rest of the standard Arduino stuff):

I had to copy some of the #defines from sleep.h and power.h. I never had time to investigate this, but I couldn’t get Eclipse to reference these any other way. I suspect it has something to do with the build of the AVR plugin that I’m using, as I believe there is a later one now. You may or may not have to do this:

** EDIT: since installing the latest Arduino Eclipse Plugin from , and using Arduino 1.5, I no longer need to add this code to get it to compile. These defines are found in the avr/power.h file as intended **

#define SLEEP_MODE_IDLE (0)
#define SLEEP_MODE_ADC _BV(SM0)
#define SLEEP_MODE_PWR_DOWN _BV(SM1)
#define SLEEP_MODE_PWR_SAVE (_BV(SM0) | _BV(SM1))
#define SLEEP_MODE_STANDBY (_BV(SM1) | _BV(SM2))
#define SLEEP_MODE_EXT_STANDBY (_BV(SM0) | _BV(SM1) | _BV(SM2))

#define set_sleep_mode(mode) \
do { \
 _SLEEP_CONTROL_REG = ((_SLEEP_CONTROL_REG & ~(_BV(SM0) | _BV(SM1) | _BV(SM2))) | (mode)); \
} while(0)

#define power_adc_enable() (PRR &= (uint8_t)~(1 << PRADC))
#define power_adc_disable() (PRR |= (uint8_t)(1 << PRADC))

#define power_spi_enable() (PRR &= (uint8_t)~(1 << PRSPI))
#define power_spi_disable() (PRR |= (uint8_t)(1 << PRSPI))

#define power_usart0_enable() (PRR &= (uint8_t)~(1 << PRUSART0))
#define power_usart0_disable() (PRR |= (uint8_t)(1 << PRUSART0))

#define power_timer0_enable() (PRR &= (uint8_t)~(1 << PRTIM0))
#define power_timer0_disable() (PRR |= (uint8_t)(1 << PRTIM0))

#define power_timer1_enable() (PRR &= (uint8_t)~(1 << PRTIM1))
#define power_timer1_disable() (PRR |= (uint8_t)(1 << PRTIM1))

#define power_timer2_enable() (PRR &= (uint8_t)~(1 << PRTIM2))
#define power_timer2_disable() (PRR |= (uint8_t)(1 << PRTIM2))

#define power_twi_enable() (PRR &= (uint8_t)~(1 << PRTWI))
#define power_twi_disable() (PRR |= (uint8_t)(1 << PRTWI))

#define power_all_enable() (PRR &= (uint8_t)~((1<
#define power_all_disable() (PRR |= (uint8_t)((1<</pre>
<p style="text-align: justify;"><strong>Tasks are written as volatile void x_update(void) functions, with prototypes referenced in main.h (as before):</strong></p>

<pre>

/*
 * Task includes
 * These header files contain the function prototypes for your tasks
 * */
#include "Tasks/ExampleTask1.h"
#include "Tasks/ExampleTask2.h"
#include "Tasks/ExampleTask3.h"

The task structure remains the same:

/*
* Function pointer for task array
* This links the Task list with the functions from the includes
* */

typedef volatile void (*task_function_t)(void);

/* Task properties */
typedef struct
{
	task_function_t task_function;	/* function pointer */
	uint32_t        task_period;	/* period in ticks */
	uint32_t        task_delay;	/* initial offset in ticks */
} task_t;

Other prototypes:

void tick_Start(void);

/* Now is the time to set the sleep mode. In the Atmega8 datasheet
 * http://www.atmel.com/dyn/resources/prod_documents/doc2486.pdf on page 35
 * there is a list of sleep modes which explains which clocks and
 * wake up sources are available in which sleep modus.
 *
 * In the avr/sleep.h file, the call names of these sleep modus are to be found:
 *
 * The 5 different modes are:
 *     SLEEP_MODE_IDLE         -the least power savings
 *     SLEEP_MODE_ADC
 *     SLEEP_MODE_PWR_SAVE
 *     SLEEP_MODE_STANDBY
 *     SLEEP_MODE_PWR_DOWN     -the most power savings
 *
 *  the power reduction management <avr/power.h>  is described in
 *  http://www.nongnu.org/avr-libc/user-manual/group__avr__power.html
 */
void sleepNow();

And now the revised main.cpp code:

/*
 * Simple Time Triggered Co-operative Scheduler for Arduino / AVR
 * by Chris Barlow
 * chrisbarlow.wordpress.com
 * chris.barlow2@gmail.com
 *
 * This is a WORK IN PROGRESS, stripped down implementation of a scheduler.
 * More functionality will be added in due course.
 */
#include <avr/power.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include "telemTestBed.h"

/*
 * Define how often the ticks occur, and the number of tasks in the schedule
 */
#define TICK_PERIOD (2000) 	/* Tick period in microseconds */
#define NUM_TASKS 	(3)		/* Total number of tasks */

bool schedLock = false;

/*
 * The task array.
 * This dictates the tasks to be run from the scheduler
 * The order of these tasks sets their priority, should more than one task run in one tick
 * */
task_t Tasks[NUM_TASKS] =
{
	{
		exampleTask1_update,
		10,
		0
	},

	{
		exampleTask2_update,
		2,
		1
	},

	{
		exampleTask3_update,
		10,
		2
	}

};

/*
 * The familiar Arduino setup() function: runs once when you press reset.
 * x_Init() functions contain initialisation code for the related tasks.
 */
void setup()
{
	wdt_disable();			/* Disable the watchdog timer */

	exampleTask1_Init();
	exampleTask2_Init();
	exampleTask3_Init();

	tick_Start();
}

/*
 * Start the timer interrupts
 */
void tick_Start()
{
	/* initialize Timer1 */
	cli(); 			/* disable global interrupts */
	TCCR1A = 0; 	/* set entire TCCR1A register to 0 */
	TCCR1B = 0; 	/* same for TCCR1B */

	/* set compare match register to desired timer count: */
	OCR1A = (16 * TICK_PERIOD); /* TICK_PERIOD is in microseconds */
	/* turn on CTC mode: */
	TCCR1B |= (1 << WGM12);
	/* enable timer compare interrupt: */
	TIMSK1 |= (1 << OCIE1A);
	TCCR1B |= (1 << CS10);
	sei(); /* enable global interrupts (start the timer)*/
}

/*
 * The ISR runs periodically every TICK_PERIOD
 */
ISR(TIMER1_COMPA_vect)
{

	sleep_disable();        /* disable sleep */
	power_all_enable();		/* restore all power */
	schedLock = false;		/* allow scheduler to run */

}

/* The loop function does nothing as all tasks are time triggered */
void loop()
{
	uint8_t i;
	sleepNow();															/* Go to sleep. Woken by ISR loop continues, then sleep repeats */

/******************** Nothing happens until interrupt tick *****************************************/
	if (schedLock == false)												/*schedLock prevents scheduler from running on non-timer interrupt */
	{
		for(i = 0; i < NUM_TASKS; i++)									/* For every task in schedule */
		{
			if(Tasks[i].task_delay == 0)								/* Task is ready when task_delay = 0 */
			{
				Tasks[i].task_delay = (Tasks[i].task_period - 1);		/* Reload task_delay */
				(*Tasks[i].task_function)();							/* Call task function */
			}
			else
			{
				Tasks[i].task_delay--;									/* task delay decremented until it reaches zero (time to run) */
			}
		}
	}
}

void sleepNow()
{

	set_sleep_mode(SLEEP_MODE_IDLE);  									/* sleep mode is set here */

	sleep_enable();														/* enables the sleep bit in the mcucr register */
																		/* so sleep is possible. just a safety pin */

	power_adc_disable();												/* Power down some things to save power */
	power_spi_disable();
	power_timer0_disable();
	power_twi_disable();

	schedLock = true;													/* Prevent schedule from running on accidental wake-up */
	sleep_mode();            											/* here the device is actually put to sleep!! */
																		/* THE PROGRAM IS WOKEN BY TIMER1 ISR */
}

What’s changed?

The most notable changes in this code are that the ISR no longer handles the scheduling of the tasks directly. At the start of loop(), line 102, sleepNow() is used to put the microcontroller to sleep. As long as power_timer1_disable() is NOT called in sleepNow() the timer will still run, even when the device is asleep. Nothing further happens until the timer reaches the specified ‘tick’ value, and generates an interrupt.

As soon as the interrupt is generated, the code in loop() continues, and runs the scheduler code as before. We then loop back on ourselves and the device is put back to sleep until the next ‘tick’.

Prof. Michael Pont recommends that no other interrupts are used on the device except for the timer interrupt, and that all pins, peripherals, etc are polled from scheduled tasks. Unfortunately with Arduino, many of the libraries such as SPI, softwareSerial, and similar use other interrupts. This could have catastrophic effects on the timing of the system, as these interrupts could wake up the processor before our timer ticks. To get around this (until someone writes interrupt-free libraries for these devices!!) I have taken two precautions. Firstly, I am experimenting with switching off as much as possible in sleepNow(), to ensure there are no erroneous ‘false ticks’. Should a false tick occur, from peripherals that we still need running, I have also implemented the boolean flag, ‘schedLock’. This is made ‘true’ in sleepNow() and only made ‘false’ in the timer1 ISR, and nowhere else in the code. The “if” statement in line 105 ensures that the scheduler operations are only carried out when the timer1 ISR has woken the processor. False ticks cause briefly wake up the processor, carry out whatever library operations they were there to do, and then my code loops and puts it back to sleep again. This is by no means ideal when we’re worrying about microsecond-resolution jitter, but it is better than nothing!

The same rules apply

That’s all I have to report for now, to recap from before, the same basic rules still apply:

  • At the moment, the micro will freeze if the tasks overrun a tick. Some trial and error is required to find a suitable tick period to avoid this.
  • while loops should be avoided where possible. If they are used, a timeout mechanism is required to prevent the program getting stuck.
  • Avoid long for loops: use counters and if(x == y) statements in tasks if you have lots of repeating code.
  • Some things will take longer than 1 tick to execute, for example, writing to an LCD screen. This can be overcome by buffering characters and writing fewer characters at once. I plan on covering this in a future post.

Next to do:

  • Allow tasks to last longer than 1 tick (the new code is more resilient to overruns, but still not perfect).
  • Introduce more functionality, such as the ability to add and remove tasks during runtime.
  • Explore using different interrupts to drive the tick – for future expansion to multiprocessor systems.

Also to soon to come is some example code for a time-triggered LCD controller, which handles different pages, with different refresh rates, and can be run alongside other periodic tasks.

Advertisements

16 thoughts on “Further task scheduling with Arduino / AVR

  1. Pingback: Accurate timing for smaller embedded projects (PIC12F) | [ chris barlow ]

  2. Pingback: (optimistic) Plans for 2014 | [ chris barlow ]

  3. hmm in my opinion, somehow placing the task handling in the loop doesn’t look “nice”
    letting the isr do it looks “nicer” but isn’t really what you want to do anyway in a intterupt routine. what in my opinion would look really nice is if somehow the isr triggered a functions (the task) that is not in the intterupt it’s self.
    but this could all be solved if the avr could run tasks from ram :)

    • Hello there,
      The reason why the task handling is in the loop is because, from a software engineering perspective, this is the most efficient place to put it. The more layers of abstraction you use in your code (functions calling functions) the higher the stack usage, and the the more the compiler will try to optimise the code. Once you give optimisation control to the compiler, you introduce timing uncertainty, which is sub-optimal for high-integrity systems. I’m more interested in the performance of the code, rather than how ‘nice’ it looks. I wanted the response time between wake-up and task execution to be as fast and predictable as possible, hence placing it directly in the loop after wake-up.

      I had thought about calling the tasks from the ISR, but as you say, this is not best practice for ISRs.

      I agree though, that if I were to make this more portable for use in the Arduino IDE, for example, I would need that extra layer of abstraction to ‘hide’ the business logic away from the developer; this will have an impact on timing performance.

  4. Hi,
    could you please explain to an Arduino newby how to use your code in the Arduino own environment (1.6.0) without using the Eclipse plugin ? Thank you.

    • I can’t really answer that as I’ve no experience with AVR studio. I had an Arduino board, and wanted to develop lower-level code than I could easily do with the Arduino environment. The Eclipse plugin will communicate directly with the Arduino bootloader, so it was the quickest and easiest route for me to get code on the board. If you’re familiar with using AVR studio, I’d be interested to know what (if any) modifications you need to make to get this TT code to work

      • No, I’m not familiar with avr studio.
        I was accustomed to use PICs with MikroPascal compiler and they work great.

        I’m trying to use arduino due to the much more economical clones you can find already build in the market and I’m trying to understand what kind of “toolchain” I have to use.
        If I want to use a TT scheduler I see I have to choose something else and not the “idiot proof” arduino IDE

  5. Pingback: Tasks: A Time-Triggered Scheduler for Arduino | chris barlow

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