Simplicity is the ultimate sophistication - Leonardo da Vinci

Intro

There was a time sometime long ago when I was looking for an RTOS or something similar which would help me easily schedule multiple tasks to execute deterministically, but not require a lot of overhead.  Of course, I came across all of the big guys out there with some very cool capabilities.  Unfortunately, most of them were simply too much... too much RAM, too much time, and too much to learn about for each RTOS.

It was at the point that I reading into the source code for some RTOS or another that I came across the function pointer code.  After some inspection, I realized that this was the answer to what I was looking for... and I'm going to share it with you.

The Task Manager Overview

I have written a simple task manager which allows the coder to easily add and remove tasks from the execution environment.  This seems like a place for some bullet points:

  • Non-preemptive
  • Non-priority driven
  • Has no semaphores, mutexes, etc.
  • Interruptable and works well with interrupt!  I have had this task manager working with several tasks on a motor drive with 50kHz interrupts with no hiccups!
  • Simple
  • Tasks may be added or removed at will
  • Portable to ALL devices that can compile C code (I have used it on PIC18s and on the Raspberry Pi to schedule tasks within a program with zero change to the source code)
  • Easy to understand - the code consists of 6 functions - TASK_init(), TASK_add(), TASK_remove(), TASK_manage(), TASK_getTime(), and TASK_resetTime()
  • Generally does not hinder sleep modes
  • Requires a single timer interrupt

Setting Up

The task manager requires you to do one thing: set up a timer interrupt to occur at the 'system tick' frequency that you desire.  I generally choose 1ms since most of the systems that I have worked with require higher frequency timing than a keypad poll.  Having said that, there is no reason that you couldn't use 10ms or 100ms (or 1 min if your timer will go that long before interrupting).

The timer that you choose should be rock solid.  Measure it on the oscilloscope with an I/O pin.  Make sure that it recovers if you change the clock frequency.  Once you have gotten this ready, then and only then, you may proceed.

A quick personal note about portability, you should try to make the timer initialization function the same between implementation in order to improve code portability.  For instance, in the example below, I refer to my timer initialization as TMR_init(), and I supply a function pointer to the function.  This ensures that my task.h and task.c files remain unchanged between architectures (ARM, MSP430, PIC24), but I only change my underlying TMR_init() based on the architecture.

The System Tick

There is one interrupt in the task manager and it has a single purpose: keeping the system time.  The system time is in the form of a global variable (file scope) called 'milliSeconds'.  When an interrupt occurs, 'milliSeconds' is incremented by one.  That is it.  The frequency that you choose will also determine the smallest amount of time that you can resolve to (on system tasks - you can still configure another timer to operate at a higher frequency for a particular interrupt function).

Configure Task Header

In the 'task.h' file, there is a MAX_NUM_OF_TASKS define.  Configure it for the maximum number of tasks that you expect to have in the queue at one time.  On the MSP430, it takes about 11 bytes of RAM for each task.  This might change a bit on different architectures, depending on the width of function pointers.

Using the Task Manager

Initialization

On device power up, the task manager should be initialized directly after the oscillator initialization.  Initialization goes through the task queue and ensures that all tasks are cleared out and ready to accept tasks into the queue.

Adding Tasks

Adding a task is simply adding a normal function that will execute at intervals when it is 'due'.  The first thing that you need to do is create the function.... any function.  There is only one qualification that the function needs - it needs to be able to fully execute in a fraction of the system tick.  Why?  Because the task manager is non-preemptive, so it won't interrupt a task that is resource hungry, and you may have multiple tasks ready to execute at the same time.  Actually adding a task is as simple as:

1
TASK_add(&myFunction, 3);

The above code will add the function to the task manager to execute every 3 system ticks.  A couple of notes about 'myFunction':

  • return type must be 'void'
  • parameters must be 'void'

Simple enough.

If you attempt to add a task twice, the task manager - as currently written - will simply update the period of the existing task.  For instance, if you executed the above task adder, then executed:

1
TASK_add(&myFunction, 5);

You would change the period from 3 system ticks to 5 system ticks.

Removing Tasks

Removing tasks is a simple as adding them:

1
TASK_remove(&myFunction);

Reading/Writing System Time

For various reasons, you may want to have your software read the system time.  I have used this to coordinate a network of wireless nodes.  Each node had its own scheduled time slot within which to transmit and - thus - there existed a global 'network time' that the nodes shared.  If a node's local time drifted a bit, it could use the TASK_resetTime() function to reset its time to the network time.

Additionally, there are processes that often expire and require some amount of time to lapse before they do.  For instance, in that network example if a node dropped off the network, it remained active on the network until it timed out.  This timeout was kept track of by simply polling TASK_getTime() every now and then and comparing the results to the TASK_getTime() of the last reception from that node.  Once the time had grown large enough, then the node was dropped from the network.

Managing Tasks

Alright, you've initialized your task manager and you have added your tasks.  What now?  The final think that you will do in your main.c is call TASK_manage(), which will NEVER return.  Don't place any statements that you want to execute after that function call.  That's it!

Summary

A quick run down of the things you need to do to get the task manager running:

  1. Setup your oscillator
  2. Setup your TMR_init() function for a system tick
  3. Configure 'task.h' file for the max number of tasks that can be running
  4. Call TASK_init() to initialize all of the tasks AFTER the oscillator has been setup.
  5. Add your initial tasks to the queue using TASK_add()
  6. Call the TASK_manage() function

Example

Lets put all of this to use!  I am going to use an MSP430 launchpad because it is what I have laying around.  The launchpad has two LEDs and a button right on the board.  Our task is to blink one LED until the button is pushed, then blink the other LED until the button is pushed.  Sounds simple enough, so lets start planning out the project architecture:

  • Button needs debouncing and to control the overall state between LED1 and LED2
  • LED1 needs to flash
  • LED2 needs to flash

Setting up, Again

Before we go too deeply into the problem domain, we need to set up a couple of basic routines.  In the sample project, these are located in 'lib430.h' and 'lib430.c'.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <msp430.h>

void (*TMR_timedFunctPtr)();
__interrupt void TMR_intFunct();

void TMR_init(void (*functPtr)()){
    TMR_timedFunctPtr = functPtr;

    TA0CTL |= TASSEL1;  /* SMCLK */
    TACCR0 = 1000;      /* timer period - sets the 'system tick' to ~1ms*/

    TA0CTL |= MC0;
    TA0CTL &= ~MC1;

    TA0CCTL0 |= CCIE;
    __bis_SR_register(GIE);
}

void TMR_disableInterrupt(){
    TA0CCTL0 &= ~CCIE;
}

#pragma vector=TIMER0_A0_VECTOR
__interrupt void TMR_intFunct(){
    if(TMR_timedFunctPtr != 0)
        (*TMR_timedFunctPtr)();

    TA0IV = 0;
}

The functions that we need to set up are TMR_init(), TMR_disableInterrupt(), and TMR_intFunct().

Write the TMR_init() routine based on your architecture, oscillator settings, and desired system tick.  The default oscillator settings for the MSP430 launchpad will set up the oscillator for 1MHz operation, which is passed into the SMCLK, which my timer is assigned to.  As a result, I am setting the period register to '1000' in order to get a system tick at 1kHz, or every 1ms.  Additionally, you will need to setup your timed function pointer to the functPtr value (first line in TMR_init()).

The TMR_disableInterrupt() simply disables interrupts.  It is used by the task manager.

The interrupt function itself has different syntax in different architectures, but the same purpose.  It will call the function that the task manager requires for its primary system tick.

Back to the Problem

Each of the above bullet points will each get their own task.  The task that reads the button will execute continually while turning on and off the LED1 and LED2 tasks as required.

The code for this can be found on github.

First, we create our 'blink' functions.  Notice that they are 'void' return and take no parameters.  These are just normal functions.  They don't have a 'yield' statement or anything like that.  The functionality should be pretty self-explanatory.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void blinkLed1(void){
    if(LED1_IS_ON){
        LED1_OFF;
    }else{
        LED1_ON;
    }
}

void blinkLed2(void){
    if(LED2_IS_ON){
        LED2_OFF;
    }else{
        LED2_ON;
    }
}

The 'debounceButton' task is just a bit more complex.  The purpose of this task is to read the button and ensure that one button press will result in one event.  Once this event is detected (see blue highlighted line below), the appropriate task is added, the opposing task is removed, and the state machine is changed (ledState).  All of the rest of the code is only there to ensure that the button bounce is eliminated.  For more information, see Elliot Williams' articles about button debounce.  I'm not advertising this as the 'best' button debouncer, but it works well and was fast to code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void debounceButton(void){
    static bool btnState = false;
    static int8_t btnCounter = 0;
    const int8_t btnCounterMax = 6;
    static bool ledState = false;

    /* when switch is on, increment btnCounter, else decrement */
    if(SWITCH_IS_ON){
        btnCounter++;
    }else{
        btnCounter--;
    }

    if((btnCounter == btnCounterMax) && (btnState == false)){
        /* the btnCounter just counted up to this point and this
         * is the positive going edge - do something here */
        btnState = true;
        if(ledState){
            TASK_remove(&blinkLed2);
            TASK_add(&blinkLed1, LED_1_BLINK_PERIOD);
            LED2_OFF;
            ledState = false;
        }else{
            TASK_remove(&blinkLed1);
            TASK_add(&blinkLed2, LED_2_BLINK_PERIOD);
            LED1_OFF;
            ledState = true;
        }
    }else if(btnCounter > btnCounterMax){
        btnState = true;
        btnCounter = btnCounterMax;
    }else if(btnCounter < -btnCounterMax){
        btnState = false;
        btnCounter = -btnCounterMax;
    }
}

At this point, we have written the pieces that we need, but still haven't gotten into main.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
int main(void) {
    WDTCTL = WDTPW | WDTHOLD;   // Stop watchdog timer

    TASK_init();

    /* setup the LED pins and switch */
    P1DIR &= ~BIT3;
    P1DIR |= (BIT0 | BIT6);
    LED1_OFF;
    LED2_OFF;

    /* add the debounce task */
    TASK_add(&debounceButton, BTN_POLL_PERIOD);

    /* enter the task execute - this will never return! */
    TASK_manage();

    return 0;
}

The first statement is specific to the MSP430 architecture and simply turns off the watchdog timer.

The next statement initializes the task manager, just as we mentioned before.

The third block of statements sets up the hardware registers so that the LED pins and switch are in the correct input/output configuration and that the LEDs are off.

The next block adds the 'debounce' task to the task queue to execute every 'BTN_POLL_PERIOD' system ticks.  I have set this particular project up so that a system tick is 1ms, so the value of BTN_POLL_PERIOD will also be in milliseconds. Notice the use of the '&' character.  This indicates that I am passing a function pointer and is very important to the functionality of the task manager.  If you don't use a function pointer, then this will not work and you will likely get compiler warnings and errors.

Finally, we call TASK_manage(), which we never expect to return.

That's it!

Conclusions

The task manager is perfect for functions that need to be executed at specific intervals, such as PID loops or button polls, but don't necessarily need to be interrupts.  This allows you to pull all of that code that you once placed into an interrupt and place it outside of the interrupt sequence so that code that truly needs to interrupt can do its thing and not be blocked by an unnecessary interrupt code.

Again, a sample project using the MSP430 can be found on github.



© by Jason R. Jones 2016
My thanks to the Pelican and Python Communities.