Wiki

Clone wiki

ArdOS / lib

The ArdOS Library

The ArdOS library is divided into:

  • Initialization Routines: Initializes the core ArdOS kernel routines and starts the first task.
  • Task Creation Routines: Registers tasks with ArdOS. Tasks can be thought of as processes in conventional operating systems.
  • Task Management Routines: Routines that allow a task to surrender control of the microcontroller, or to put task to sleep for a given time.
  • Semaphore Routines: Routines to create, take and give semaphores.
  • Queue Routines: Routines to create, enqueue for other tasks, and dequeue messages from other tasks.
  • Mutexes and Conditional Variables: For protecting critical sections and managing task coordination.
  • Interrupt Service Routines: Special considerations when writing interrupt service routines.

Include Files

#include "ArdOS.h" to bring in all the function prototypes and data structures that you need.


Initialization Routines

void OSInit()

Initializes the ArdOS core.

void OSRun()

Starts ArdOS, which will then start running the highest priority task.


Task Creation Routines

These routines let you create and register tasks. Tasks are written as functions in the conventional sense of the word, but their codes are executed as processes. The easiest way to understand tasks is that they are very similar to threads in pthread programming. You can also define an idle task that is executed whenever none of the other tasks can run.

To ensure correct execution of the ArdOS core, idle tasks should never make blocking calls (e.g. taking a semaphore).

void OSCreateTask(int prio, unsigned long *taskStack, void (*rptr)(void *), void *arg)

Creates a new task. Parameters to be provided are:

  • prio: The task priority. This is a number between 0 (for the highest priority task) to N-1 (lowest priority) for N tasks. If ArdOS is configured to priority mode, this number must be unique for each task. If a task of a higher priority is able to run, it will always have control over the microcontroller. Lower priority tasks must wait for higher priority tasks to suspend before they can run. If ArdOS is configured for round-robin mode (OS_RR), then the tasks are run in order of creation irrespective of priority number. The priority number in this case determines the amount of time quantum (in milliseconds) each task is given. See OSMIN_QUANTUM, OSMAX_QUANTUM and OSMAX_PRIOLEVEL in the Configuration Section.

  • taskStack: You will need to create an array of unsigned long, and pass in a pointer to the last element of the array. ArdOS uses this as the calling stack for the task.

  • rptr: Pointer to the task code.

  • arg: Arguments to be passed to the task code on first start up.

Example:

#include "ArdOS.h"

// Code for tasks 1 and 2. Delay time passed into p
void task1(void *p)
{
    int delay=(int) p;
    while(1)
    {
        digitalWrite(9, HIGH);
    OSSleep(delay);
    digitalWrite(9, LOW);
    OSSleep(delay);
    }
} 

void task2(void *p)
{
    int delay=(int) p;
    while(1)
    {
        digitalWrite(6, HIGH);
        OSSleep(delay);
        digitalWrite(6, LOW);
        OSSleep(delay);
    }
}

// Calling stack for tasks 1 and 2
unsigned int t1stack[30], t2stack[30];

int main()
{
    pinMode(6, OUTPUT);
    pinMode(9, OUTPUT);

    // Initialize operating system
    OSInit();

    // Register tasks. Pass in 125 and 250 to task 1 and 2
    // Notice that we pass a pointer to the LAST item of
    // the stacks. This is because the stack grows downwards
    // in the ATMega.

    OSCreateTask(0, &t1stack[29], task1, (void *) 125);
    OSCreateTask(1, &t2stack[29], task2, (void *) 250);

    // Start the OS
    OSRun();

    // We will never reach here.
    return 0;
}

void OSSetIdleTask(void (*idletask)(void *))

When no other tasks are able to run, ArdOS runs its own idle task that essentially just burns through CPU time using a busy-wait. If you have better use of this idle time you can specify your own idle hook using OSSetIdleTask. You do not have to specify a task stack as ArdOS will provide one for you. Parameters are:

  • idletask: Pointer to the idle task code.

Example:

void myIdler(void *p)
{
    volatile unsigned long count;
    while(1)
    {
        // Let's see how large this gets!
        count++;
    }
}

...

int main()
{
    ...
    OSInit();
    OSCreateTask(0, &t1Stack[29], task1, (void *) 125);
    OSCreateTask(1, &t2Stack[29], task2, (void *) 250);

    // Set up an idler task that will run whenever there is nothing else to run.
    OSSetIdleTask(myIdler);
    OSRun();

    // Never reaches here
    return 0;
}

Task Management

The OSSwap and OSPrioSwap routines allow the currently running task to surrender control of the microcontroller. These are usually used with the co-operative multiasker (OS_PREEMPTIVE set to 0), but ArdOS does not prevent you from using them when in pre-emptive mode.

####void OSSwap()

Task relinquishes control of CPU. Control is handed to the next task that is ready to run. If no other tasks are ready to run, control is handed back to the calling task.

####void OSPrioSwap()

Similar to OSSwap, but control is handed over to the next task only if it has a priority that is higher than that of the calling task. If the next runnable task has a lower priority or if there are no runnable tasks, control is handed back to the calling task.

####void OSSleep(unsigned int millis)

Relinquished control of the microcontroller for the specified time in milliseconds. The sole parameter "millis" specifies the number of milliseconds that this task will sleep. Use this in place of the Arduino delay or AVR-GCC _delay_ms functions as OSSleep allows the microcontroller to handle other tasks. Both delay and _delay_ms are busy-waits that will simply hog microcontroller time. Parameters are:

  • millis: Time to sleep in milliseconds.

The actual time a task sleeps could be more than what is specified in millis if the task has a lower priority.
An example of OSSleep was given earlier.

####unsigned long OSticks()

Returns the number of 1 ms clock ticks since OSRun was called. This therefore gives you the elapsed time in milliseconds.


Semaphores

ArdOS supports both binary and counting semaphores. Binary semaphores are excellent for coordinating tasks, while counting semaphores are excellent for keeping track of limited resources. To use semaphores, set OSUSE_SEMA in ArdOSConfig.h to 1.

The TOSSema data structure

This is the data structure defined in ArdOS for semaphores.

void OSInitSema(TOSSema *sema, unsigned int initval, unsigned char isBinary)

Creates a new semaphore. This function must be called to initialize a semaphore before using it in OSTakeSema or OSGiveSema. Parameters are:

  • sema: Pointer to semaphore variable of type TOSSema.
  • initval: Initial value of the semaphore. Can be 0 or more. Binary semaphores will be initialized to 1 if initval is anything other than 0. Counting semaphores will be initialized to initval.

  • isBinary: Put 0 if you are creating a counting semaphore, 1 if you are creating a bianry semaphore.

OSTakeSema(TOSSema *sema)

Takes (decrements) a semaphore. Suspends the calling task if the semaphore is 0. Parameters are:

  • sema: Pointer to semaphore variable of type TOSSema, initialzied using OSInitSema.

OSGiveSema(TOSSema *sema)

Gives (increments) a semaphore. If there are any tasks that are blocking on sema, the task with the highest priority is woken up and becomes runnable, and sema remains as 0. If there are no tasks blocking on sema, sema is incremented by 1 if it is a counting semaphore, or set to 1 if it is a binary semaphore.

Example:

#include <stdlib.h>
#include "ArdOS.h"

// This code flashes the LED at pin 6 5 times rapidly, then flashes the LED at pin 9 twice a second and repeats. Coordination is via semaphores.

// Declare the semaphore variables.
TOSSema sem1, sem2;

void task1(void *p)
{
    while(1)
    {
        for(int i=0; i<5; i++)
        {
            digitalWrite(6, HIGH);
            OSSleep(5); // Sleep for 5 ms
            digitalWrite(6, LOW);
            OSSleep(5);
        }

        // Switch off pin 6
        digitalWrite(6, LOW);

        // Release task 2
        OSSemGive(&sem2);

        // Wait for task 2 to complete
        OSSemTake(&sem1);
    }
}

void task2(void *p)
{
    while(1)
    {
        // Wait for task 1 to allow us to run
        OSSemTake(&sem2);

        for(int i=0; i<5; i++)
        {
            digitalWrite(9, HIGH);
            OSSleep(250); // Sleep for 5 ms
            digitalWrite(9, LOW);
            OSSleep(250);
        }

        // Switch off pin 9
        digitalWrite(9, LOW);

        // Let task 1 continue
        OSSemGive(&sem1);
    }
}

unsigned long t1Stack[30], t2Stack[30];
int main()
{
    // Set the pin modes
    pinMode(6, OUTPUT);
    pinMode(9, OUTPUT);

    // Initialize the OS
    OSInit();

    // Initialize the semaphores as binary semaphores, starting
    // value of 0
    OSInitSema(&sem1, 0, 1);
    OSInitSema(&sem2, 0, 1);

    // Register the tasks, no parameters
    OSCreateTask(0, &t1Stack[29], task1, NULL);
    OSCreateTask(1, &t2Stack[29], task2, NULL);

    // Start the first task
    OSRun();

    // Never reaches here

    return 0;

}

Queues

While semaphores are good for coordinating tasks and securing critical sections, they are not always well suited for passing messages between tasks. ArdOS queues provide atomic communications between tasks. ArdOS supports both standard FIFO queues where messages are read off in the order that they are written, and prioritized queues where messages with higher priorities appear ahead of messages with lower priorities.

ArdOS queues are designed to be written to by many tasks, but read from by at most one task. This is intuitive and fits the bulk of real-world use cases for queues.

The TMsgQ data structure

This is the standard data structure for queues.

Standard Message Queues

To use standard queues, set OSUSE_QUEUES in ArdOSConfig.h to 1.

void OSMakeQueue(int *buffer, unsigned char length, TMsgQ *queue)

Creates a standard FIFO queue. Parameters are:

  • buffer: A predefined integer array for storing messages to be passed.
  • length: The length of buffer in number of integers.
  • queue: A pointer to the queue variable of type TMsgQ.

void OSEnqueue(int data, TMsgQ *queue)

Enqueues a message to be passed to another task. If there is a task that is blocking on queue, this task will be unblocked and made runnable. If the blocked task has a higher priority than the calling task and OSSCHED_TYPE is set to OS_PRIORITY, the calling task will be pre-empted immediately. Parameters are:

  • data: Message to be passed.
  • queue: Pointer to queue variable of type TMsgQ, initialized using OSMakeQueue.

void OSDequeue(TMsgQ *queue)

Dequeues a message from queue. For each queue this should be called by at most one task. If the queue is empty the calling task will be blocked. Parameters are:

  • queue: Queue variable of type TMsgQ, initialized using OSMakeQueue.

Priority Queues

To use priority queues, set OSUSE_PRIOQUEUES in ArdOSConfig.h to 1.

The TPrioNode data structure

While standard queues are based on integer arrays, prioritized queues are based on arrays of TPrioNode. However messages are still integer based.

void OSMakePrioQueue(TPrioNode *buffer, unsigned char length, TMsgQ *queue)

Create a new priority queue. Parameters are:

  • buffer: An array of TPrioNode structures. This is the actual array used to maintain the queue.
  • length: The length of buffer in TPrioNode structures.
  • queue: The queue variable of type TMsgQ.

void OSPrioEnqueue(int data, unsigned char prio, TMsgQ *queue)

Prioritized version of OSEnqueue. As before if there is a task that is blocking on queue, that task will be unblocked and made runnable, and may pre-empt the calling task. Paramters are:

  • data: Data to be enqueued
  • prio: A non-negative number denoting the priority. Smaller prio numbers denote higher priority, larger prio numbers denote lower priority.
  • queue: A queue variable of type TMsgQ initialized using OSMakePrioQueue.

void OSDequeue(TMsgQ *queue)

This is exactly the same as for the ordinary FIFO queue.

Example:

#include "ArdOS.h"
#include <stdlib.h>

// This program starts with task1 flashing the LED at pin 6 once, 
// increments the count by 1, and passes this value to task 2 via a FIFO
// queue. Task 2 flashes the LED at pin 9 the number of times as read off
// from the queue, increments it by two, then passes the new value back to
// task 1, etc.

TMsgQ q1, q2;

void task1(void *p)
{
    int count=0;

    while(1)
    {
        for(int i=0; i<count; i++)
    {
        digitalWrite(6, HIGH);
        OSSleep(125);
        digitalWrite(6, LOW);
        OSSleep(125);
    }

    count++;

    // Pass count to task 2
    OSEnqueue(count, &q2);

    // Wait for task 2 to pass back a value
    count = OSDequeue(&q1);


    }
}

void task2(void *p)
{
    int count;
    while(1)
    {
        // Wait for task 1 to pass in a value
        count=OSDequeue(&q2);

        for(int i=0; i<count; i++)
        {
            digitalWrite(9, HIGH);
            OSSleep(250);
            digitalWrite(9, LOW);
            OSSleep(250);
        }
        count+=2;

        // Send over to task1
        OSEnqueue(count, &q1);
    }
}

// Task stacks
unsigned long t1Stack[30], t2Stack[30];

// Queues
int q1buf[8], q2buf[8];

int main()
{
    pinMode(6, OUTPUT);
    pinMode(9, OUTPUT);

    // Initialize the OS
    OSInit();

    // Intialize the queues
    OSMakeQueue(q1buff, 8, &q1);
    OSMakeQueue(q2buff, 8, &q2);

    // Register the tasks
    OSCreateTask(0, &t1Stack[29], task1, NULL);
    OSCreateTask(1, &t2Stack[29], task2, NULL);

    // Start the OS
    OSRun();
}


Mutexes and Conditional Variables

ArdOS supports mutex locks for securing critical sections, as well as conditional variables for task coordination.

Mutex Locks

To use mutex locks, set OSUSE_MUTEXES in ArdOSConfig.h to 1.

The OSMutex data structure

This is the standard data structure for defining mutexes.

void OSCreateMutex(OSMutex *mutex)

Initializes a mutex lock. Parameters:

  • mutex: Mutex lock of type OSMutex to be initialized. The lock is initially set to "unlocked".

void OSTakeMutex(OSMutex *mutex)

If the mutex lock is currently unlocked, this function returns immediately. If it is locked, the calling task is blocked.

  • mutex: The mutex lock of type OSMutex, initialized through calling OSCreateMutex.

void OSGiveMutex(OSMutex *mutex)

Unlocks a mutex. If there are any tasks blocked on mutex, the highest priority task is unblocked and made runnable, and the mutex remains locked. If there are no tasks blocked, then the mutex lock is unlocked. If an unblocked task has a higher priority than the calling task, the calling task will be pre-empted immediately by the newly unblocked task if OSSCHED_TYPE is set to OS_PRIORITY.

Conditional Variables

Conditional variables are used together with mutex locks to allow tasks to sleep ("wait") until it is woken ("signalled") by a different task. Conditional variables in ArdOS are designed so that many tasks can signal a conditional variable, but at most one task can wait on it. This is intuitive and in-line with the majority of conditional variable use-cases.

The OSCond data structure

This is used to declare conditional variables in ArdOS.

void OSCreateConditional(OSCond *cond)

Initialize a conditional variable.

  • cond: A pointer to the conditionalk variable to be initialized.

void OSWait(OSCond cond, OSMutex mutex)

Lets a task sleep (wait) on a conditional variable. For proper operation the task must first acquire the mutex lock "mutex" using OSTakeMutex. This call releases the mutex lock and puts the task to sleep. When the task is woken (signalled) by another task, it automatically re-acquires the mutex as soon as the calling task releases it.

  • cond: Pointer to the conditional variable to sleep on. cond must be initialized using OSCreateConditional.

void OSSignal(OSCond *cond)

Wakes (signals) a task that is sleeping on conditional variable cond. If there are no tasks currently sleeping on cond, the latest signal is saved so that the next OSWait will exit immediately instead of blocking the calling task.

  • cond: Conditional variable properly initialized using OSCreateConditional.

Example:

#include <stdlib.h>
#include "ArdOS.h"

// This program is similar to our queue example, but uses a global
// count variable and coordinates through conditional variables.

OSMutex mutex;
OSCond c1, c2;

int count=0;

void task1(void *p)
{
    while(1)
    {
        // Acquire the mutex
        OSTakeMutex(&mutex);
        for(int i=0; i<count; i++)
        {
            digitalWrite(6, HIGH);
            OSSleep(125);
            digitalWrite(6, LOW);
            OSSleep(125);
        }

        // Signal task 2
        OSSignal(&c2);

        // This automatically releases the mutex so
        // task 2 can run, but will regain it when task 2
        // exits
        OSWait(&c1, &mutex);

        // Release the mutex
        OSGiveMutex(&mutex);

        // Sleep for 1 ms
        OSSleep(1);
    }
}

void task2(void *p)
{
    while(1)
    {
        // Acquire the mutex
        OSTakeMutex(&mutex);

        // And wait for task 1 to signal
        OSWait(&c2, &mutex);

        for(int i=0; i<count; i++)
        {
            digitalWrite(9, HIGH);
            OSSleep(250);
            digitalWrite(9, LOW);
            OSSleep(250);
        }

        // Signal task 1
        OSSignal(&c1);

        // Release mutex
        OSGiveMutex(&mutex);
    }
}

unsigned long t1Stack[30], t2Stack[30];

int main()
{
    pinMode(6, OUTPUT);
    pinMode(9, OUTPUT);
    OSInit();

    // Create the mutex
    OSCreateMutex(&mutex);

    // Create the conditional variables
    OSCreateConditional(&c1);
    OSCreateConditional(&c2);

    // Register the tasks
    OSCreateTask(0, &t1Stack[29], task1, NULL);
    OSCreateTask(1, &t2Stack[29], task2, NULL);

    // Start the OS
    OSRun();

    // Never reach here
    return 0;
}


Interrupt Service Routines

Special caution must be made when using blocking and unblocking calls in interrupt service routines (ISR). Blocking calls like OSTakeSema and OSDequeue can cause ISRs to be suspended, possibly causing incorrect hardware operation. If an ISR is not fully executed and gets suspended, it can cause all interrupts of the same and lower priority to become indefinitely suspended. In general blocking calls should not be used within ISRs.

Similarly if an ISR makes use of an unblocking call like OSQueue or OSGiveSema, if the unblocked task has a higher priority than the blocked task, the ISR will get pre-empted and may be suspended indefinitely.

To correctly use unblocking calls within an ISR, you should suspend the scheduler using OSSuspendScheduler, and restart the scheduler using OSResumeScheduler just before exiting the ISR.

There are also special versions of OSSwap and OSPrioSwap that will correctly clear interrupt flags on exiting.

void OSSuspendScheduler()

This causes task switching to be temporarily suspended. Use this within ISRs if you intend to use unblocking calls like OSGiveSema, OSDequeue, OSSignal, etc.

If OSSuspendScheduler is called, it is imperative that OSResumeScheduler is called just before exiting the ISR. Calling OSSuspendScheduler without calling OSResumeScheduler will cause all task swapping to become suspended.

void OSResumeScheduler()

Causes task switching to be resumed. OSResumeScheduler will automatically call the task scheduler, which may cause the currently running task to become preempted. It also clears interrupt flags and should only be called within an ISR.

There is no need to execute a reti within a naked ISR. This is handled by OSResumeScheduler.

void OSSwapFromISR()

ISR version of OSSwap. Use this if you want to manually swap tasks (e.g. in a co-operative environment with OS_PREEMPTIVE set to 0) within an ISR. This version correctly clears interrupt flags on exit.

void OSPrioSwapFromISR()

ISR version of OSPrioSwap. Use this if you want to manually swap tasks within an ISR. This version correctly clears interrupt flags on exit.

Example:

#include <stdlib.h>
#include <avr/interrupt.h>
#include "ArdOS.h"

/* This program assumes that there is an LED connected to digital pin 9 
and a push-button connected to INT0 (digital pin 2). It works like a 
very fancy on-off switch. There is no debouncing so switching can be a 
bit wonky depending on the quality of your pushbutton. The semaphore is c
ompletely unnecessary of course and serves just as a simple demonstration 
of how to use unblocking calls in an ISR. */

unsigned char flag=0;
TOSSema sema;

ISR(INT0_vect)
{
    // Suspend the scheduler so that the ISR does not accidentally get pre-empted.

    OSSuspendScheduler();

    // Toggle the flag
    flag=!flag;

    // Release the semaphore. task1 is unblocked but will not be started because
    // of the OSSuspendScheduler call.

    OSGiveSema(&sema);

    // Resume the scheduler. This will may unblock a task and automatically 
    // restarts it if it has a higher priority than the interrupted task.
    // Interrupt flags are correctly cleared using a reti

    OSResumeScheduler();
}


// Note: Only one task. OSTask_Count in ArdOSConfig.h must be set to 1.
void task1(void *p)
{
    while(1)
    {
        OSTakeSema(&sema);
        if(flag)
            digitalWrite(9, HIGH);
        else
            digitalWrite(9, LOW);
    }
}

unsigned long t1Stack[30];

int main()
{
    // Initialize the OS
    OSInit();

    // Set up INT0 to trigger on rising edge and enable INT0 interrupts.
    EICRA|=0b11;
    EIMSK|=0b1;

    // Set up pin 9 as output and pin 2 as input
    pinMode(9, OUTPUT);
    pinMode(2, INPUT);


    // Initialize the semaphore as a binary semaphore with initial
    // value of 0.

    OSInitSema(&sema, 0, 1);

    // Register the task
    OSCreateTask(0, &t1Stack[29], task1, NULL);

    // Start the OS
    OSRun();

    // Never reaches here
    return 0;
}


Go Home

Building ArdOS Applications

ArdOS I/O Access Calls

Configuring ArdOS

Updated