Co-operative Multitasking Library For AVR Microcontrollers
1. Introduction
Well, it’s not really multitasking, it’s just a collection of C functions (let’s call them ‘tasks’) that can be scheduled to be called automatically at fixed intervals. There is no pre-emption or even a cooperative yield()
function; once a task is called it runs until the function returns; a long-running task that wants to allow other tasks to have a chance to run must be implemented as a state-machine, the library provides a couple of way making this relatively easy.
While a ‘proper’ register-saving multitasker and even pre-emption is no doubt possible on AVR micros, the relatively large number of registers that need to potentially be saved compared to the limited amount of RAM and/or stack space make the prospect more or less unviable. There system presented here, whereby a thread can be either ready (ie runnable) and being called by the scheduler, or unready and waiting on an event to occur or timer expiration, is more than adequated for many applications. I find myself using it in most of my projects these days.
An added benefit is that resource-contention is never a problem as each task’s function always runs to completion. Flags, semaphores, critical-regions and all those other multitasking goodies are not required.
The only synchronisation that is required is between ISRs and tasks, and this is done in the normal way by simply disabliing interrupts during the critical regions.
2. Overview
API functions are documented here, a quick summary follows. They can be divided into two groups as follows:
1. Scheduler Functions:
There are two timers on which a task can wait:
- tick timer – used for finer resolution timing. Counts time in ‘ticks’. I usually set this for a tick of 1ms.
- seconds timer – counts time in seconds, useful for a temperature sensor that might only be read every ten minutes, for example
The scheduling is handled by the program’s main loop which needs to call three functions:
task_tick() which keeps track of tasks that are waiting on the timer and makes them ready when the timer expires.
task_seconds_tick() which keeps track of tasks that are waiting on the seconds timer and makes them ready when the timer expires.
task_run() is then called which in turn calls the callback functions of all tasks that are in the ready state.
2. Task functions
Each task is assigned a task number. Task numbers need to be assigned by the programmer, and TASK_NUM_TASKS
must be defined, for example:
// tasks #define TASK_WRITER 0 #define TASK_READER 1 // Don't forget to update TASK_NUM_TASK whenever tasks are added or subtracted #define TASK_NUM_TASKS 2
The task functions are in two groups: task_num_XXX()
type, in which the task number must be explicity passed, and task_XXX()
type, which implicitly work on the currently running task.
The task_ready() and task_num_ready() functions make a task ready to run, or unready.
A task is initialised by a call to task_init() function which is passed a pointer to task’s callback function. It is also passed a pointer to some arbitary data structure which is provided as a paramter to the callback function, this can be useful for tasks that need to maintain state between callback function called.
task_set_tick_time() is used to make a task unready until the specified number of ticks has elapsed at which time it is made ready and its callback function called on the next call to task_run(), task_cancel_tick_time() cancels the timer and leaves task in unready state.
Similarly, task_set_seconds_timer() is used to do as above but on a number-of-seconds basis.
3. Example
- Clone the code from the source code on GitHub:
~>git clone --recurse-submodules git@github.com:telecnatron/avr-task.git
Cloning into 'avr-task'...
...
Submodule path 'src/lib': checked out 'f502fb6e0cf8ca4f1730f9903d3c03c75357a962'
Which puts the code in the newly created avr-task
subdirectory. Note that the AVR Library is included as a git submodule, we used the --recurse-submodules
directive to have it automatically cloned into the avr-task/lib
subdirectory.
- Define a unique number and a meaningful name for each task:
In config.h:
// tasks #define TASK_WRITER 0 #define TASK_BLINKY_LED 1 #define TASK_DOTTY 2 #define TASK_BLINKING_CONTROL 3 #define TASK_WINKY 4 // Don't forget to update TASK_NUM_TASK whenever tasks are added or subtracted #define TASK_NUM_TASKS 5
Note that task numbers must start at 0 and be sequential.
In main.c:
- Define a callback for a task:
void task_blinky_led(void *data) { LED_TOGGLE(); task_set_tick_timer(500); }
which toggles a LED every 500ms (ticks). Note that when a timer expires it is not automatically restarted, hence the call to task_set_tick_timer()
Then, in the main()
function:
- Initialise the hardware timer, and initialise the tasks:
int main() { ... // ticker, uses timer2 sysclk_init(); ... // tasks // initialise writer task and make it ready to run task_init(TASK_BLINKY_LED, task_blinky_led, NULL, 1); ... }
Note that the sysclk library functions are beign used as a wrapper around the AVR hardware timer.
- Define the main loop:
for(;;){ if(sysclk_has_ticked()){ // this block called about every 1ms task_tick(); if( sysclk_have_seconds_ticked()){ // this block called every second task_seconds_tick(); } task_run(); } // sleep here }
wherein we check that 1ms (ie a tick) has elapsed and, if so, call task_tick() to ready any tasks whose tick timer has expired, and similary, if a second has elapsed, we call task_seconds_tick, then finally, call task_run() which calls the callback function of all tasks that are in the ready state.
And that’s basically all that needs to be done to be running multiple tasks.
Furthermore:
- A task can change it’s callback function:
Which is useful for maintaining task-state by using a different function for each state, for example:
void task_winky_on(void *data) { LED_ON(); task_set_tick_timer(10); // change this task's callback to task_winky_off() task_set_callback(task_winky_off); } void task_winky_off(void *data) { LED_OFF(); task_set_tick_timer(250); // change this task's callback to task_winky_on() task_set_callback(task_winky_on); }
- A task can access its unique data:
// initialise writer task and make it ready to run: uint8_t writer_count =0; task_init(TASK_WRITER, task_writer, (void *)&writer_count, 1);
task_writer’s callback now gets passed a pointer to the writer_count variable:
void task_writer(void *data) { LOG_INFO_FP("message %u", (*(uint8_t *)data)++ ); task_set_seconds_timer(2); }
so, every two seconds, it can log a message telling us how many messages it has logged.
4. Summary
It seems that my AVR applications comprise mostly of polling for events, reacting to interrupts and periodically reading sensors and sending messages, I find this library useful for managing large number of such things in an organised and controllable way. Hopefully others will find it useful too.