Task Management

This chapter aims to give a good understanding of:

  • How Mbed RTOS allocates processing time to each task within an application
  • How Mbed RTOS chooses which thread should execute at any given time
  • How the relative priority of each thread affects system behavior
  • The states a thread can exist in

This chapter will give a good understanding of:

  • How to implement threads
  • How to create one or more instances of a thread
  • How to use the thread parameter
  • How to change the priority of a thread that has already been created
  • How to delete a thread
  • How to implement periodic processing
  • When the idle task will execute an how it can be used

Thread functions

Tasks are implemented as C functions The only thing special about them is their prototype, which must return void and take a void const pointer argument

void thread(void const * args)

Each task is a small program in its own right

  1. It has an entry point
  2. Will normally run forever within an infinite loop
  3. Will not exit
void thread(void const * args){
    int variableExample = 0;
    while(true){
        /*code will go here */
    }    
}

Mbed RTOS functions must NOT be allowed to return from their implementing function in any way They must not contain a ‘return’ statement They must not be allowed to execute past the end of the function

If a task is no longer required, it should be deleted explicitly.

A single thread function definition can be used to create any number of tasks Each thread created

  • Being a separate execution instance
  • Having its own stack
  • Having its own copy of any (stack) variables defined within the task itself

Top Level Thread States

An application can consist of many threads A microcontroller typically has a single core Only one single task can be executed at any given time

This implies that a thread can exist in one of two states

  • Running
  • Not running

!!! figure

When a thread is in the “running” state, the processor is executing its code When a thread is in the “not running” state

  • The task is dormant
  • Its status is saved ready for it to resume execution
  • When resuming execution, it does so from the instruction it was about to execute before it last left the “running” state

Only the RTOS scheduler can switch the state of a thread

Creating Threads

Thread    ( 
    void(*)(void const *argument)     task,
    void *                 argument = NULL,
    osPriority             priority = osPriorityNormal,
    uint32_t               stack_size = DEFAULT_STACK_SIZE,
    unsigned char *        stack_pointer = NULL 
)

task Type: void()(void const argument) Function pointer to function to be executed by this thread The type dictates that the function must: Have a return type of void Has only one argument that takes a const pointer to a void

argument Type: void * Pointer that is passed to the thread function as start argument Default is NULL

priority Type: osPriority Set the initial priority of the thread function Default value is osPriorityNormal

stack_size Type: uint32_t (integer) Stack size in bytes required for the thread function Default value: DEFAULT_STACK_SIZE

In mbed-rtos, the default stack size is 2048 bytes. Each thread has a unique stack that is allocated by the kernel There is no easy way to determine the stack space. It is possible to calculate it (but not always easy) Mostly users will assign what they think is reasonably

stack_pointer Type: pointer to unsigned char Pointer to the stack area to be used by this thread Default value is NULL

Normally the thread will allocate its own stack in memory, but you have the flexibility to do it yourself Can be handy if you want more control over the memory

Example

#include "mbed.h"
#include "rtos.h"

void task2_thread(void const * arg){
    while(true){
        printf("Task 2 is running\r\n");
        wait(0.5);
    }    
}

int main() {
    Thread thread2(task2_thread);

    while(true){
        printf("Task 1 is running\r\n");
        wait(0.5);
    }
}

The main() function is automatically the first thread scheduled by the rtos ! Thread2_thread is the function we want to use as a thread. Return type: void One argument of type void const pointer Thread thread2(task2_thread) Creates a new Thread object using the function task2_thread

Output:

!!! image

You can do the exact same thing by creating 2 separate threads and putting the main in a WaitForever state This might be more logical and easier to read the code The RTOS will not give the main function any cpu time It is in a waiting (“not running”) state Nothing will trigger it to the “running” state

Thread::wait(osWaitForever);
#include "mbed.h"
#include "rtos.h"

void task1_thread(void const * arg){
    while(true){
        printf("Task 1 is running\r\n");
        wait(0.5);
    }    
}

void task2_thread(void const * arg){
    while(true){
        printf("Task 2 is running\r\n");
        wait(0.5);
    }    
}

int main() {
    Thread thread1(task1_thread);
    Thread thread2(task2_thread);

    Thread::wait(osWaitForever);
}

!!! image

Removing duplication

Since both tasks/threads almost did the same thing, we can refactor and remove code duplication We can use a single thread function and pass the difference with the Thread arguments in the constructor

#include "mbed.h"
#include "rtos.h"

void task_thread(void const * args){
    while(true){
        printf((const char*) args);
        wait(0.5);
    }    
}

int main() {
    Thread thread1(task_thread, 
        (void *) "Task 1 is running\r\n");
    Thread thread2(task_thread, 
        (void *) "Task 2 is running\r\n");

    Thread::wait(osWaitForever);
}

Note: we need to cast the argument to a void pointer Satisfying the generic prototype of the Thread constructor Only one constructor needed to pass every imaginable type to the thread In the thread we need to cast is back to its original type In this case the string of text was referenced by a const char pointer

Thread thread1(task_thread, (void *) "Task 1 is running\r\n“);

…

printf((const char*) args);

Thread Priorities

Mbed-rtos enables thread to have different priorities The kernel will always ensure to schedule the highest priorities to enter the running state When threads have the same priority the scheduler will transition each task in turn Each task executed for a ‘time slice’ To select the next task, the scheduler must execute at the end of each time slice. A periodic interrupt, called the tick interrupt, is used for this purpose The period of the tick interrupt is set in the configuration of the RTOS 1 tick is set to 1ms Round robin timeout is set to 5 ticks (schedule next task)

Execution sequence showing the tick interrupt execution

!!! image

Thread 2 is given a higer priority than thread 1 The priority is set by the constructor when creating the Thread object The priority is set to osPriorityAboveNormal

#include "mbed.h"
#include "rtos.h"

void task_thread(void const * args){
    while(true){
        printf((const char*) args);
        wait(0.5);
    }    
}

int main() {
    Thread thread1(
        task_thread,
        (void *) "Task 1 is running\r\n");
    Thread thread2(
        task_thread, 
        (void *) "Task 2 is running\r\n",         osPriorityAboveNormal);

    Thread::wait(osWaitForever);
}

Thread2 always has a higher priority than thread1. Therefor thread2 only gets scheduled by the RTOS

!!! image

Execution sequence with one thread with a higher priority than the other

!!! image

Mbed RTOS by default has 7 levels of priority They are declared in an enum called osPriority

Typedef enum{
    osPriorityIdle            = -3,
    osPriorityLow            = -2,
    osPriorityBelowNormal    = -1,
    osPriorityNormal        = 0,
    osPriorityAboveNormal    = +1,
    osPriorityHigh            = +2,
    osPriorityRealtime        = +3,
    osPriorityError        = 0x84
} osPriority;
osPriority get_priority()

Get priority of an active thread. Returns: current priority value of the thread function.

osStatus set_priority    (osPriority priority)

Set priority of an active thread. Parameters: priority new priority value for the thread function. Returns: status code that indicates the execution status of the function

Expanding the "Not Running" State

So far, threads always had processing to perform and never had to wait Because they never had to wait, they are always able to enter the running state This type of “continuous processing” threads have limited usefulness because they can only be created at low priorities Running at higher priority will prevent any other task of lower priority ever running at all

Solution is to allow threads to be event driven

Event Driven Threads

Event driven threads has work to perform only after the occurrence of the event that triggers it Threads are not able to enter the running state before that event occurred High priority threads not able to run cannot be selected by the scheduler The scheduler can instead select tasks with lower priorities that is able to run

Event driven threads enable threads with different priorities to run without starving all the lower priority tasks of processing time

Thread that are not running now can be able to run or not We need to expand the states a thread can exist in

  • RUNNING: The thread that is currently running is in the RUNNING state. Only one thread at a time can be in this state.
  • READY: Threads which are ready to run are in the READY state. Once the RUNNING thread has terminated or is WAITING the next READY thread with the highest priority becomes the RUNNING thread.
  • WAITING: Threads that are waiting for an event to occur are in the WAITING state.
  • INACTIVE: Threads that are not created or terminated are in the INACTIVE state. These threads typically consume no system resources.

Thread state statemachine

!!! image

Thread::wait()

The normal wait() function of the mbed library (used in previous examples) is nothing more than a very long for loop using the cpu for doing nothing Instead we can use the Thread::wait(500); function Set the state of the thread to “waiting” Trigger an event in 500ms to change the state back to ready

#include "mbed.h"
#include "rtos.h"

void task_thread(void const * args){
    while(true){
        printf((const char*) args);
        Thread::wait(500);
    }    
}

int main() {
    Thread thread1(task_thread, 
        (void *) "Task 1 is running\r\n");
    Thread thread2(task_thread, 
        (void *) "Task 2 is running\r\n");

    Thread::wait(osWaitForever);
}
osStatus wait (uint32_t millisec) [static]

Wait for a specified time period in millisec: Parameters: millisec time delay value Returns: status code that indicates the execution status of the function.

The result is exactly the same, but has big advantages

Execution pattern when threads use Thread::wait() instead of a null loop (standard wait() from mbed.h)

!!! image

Thread state statemachine

!!! image

RtosTimer

RtosTimer is a class that allows creating and controlling timer functions in the system A timer function is called when a time period expires One-shot and periodic timers are possible A timer can be started, restarted or stopped

RtosTimer    ( 
    void(*)(void const *argument)     task,
    os_timer_type         type = osTimerPeriodic,
    void *                argument= NULL
)

task Type: void()(void const argument) Function pointer to function to be executed by the RtosTimer The type dictates that the function must: Have a return type of void Has only one argument that takes a pointer to a void const

type osTimerOnce for a one-shot timer osTimerPeriodic for periodic behavior (= default)

argument Type: void * Pointer that is passed to the thread function as start argument Default is NULL

osStatus start( uint32_t millisec) Start or restart a timer Parameter: millisec, the time delay value of the timer Returns the status code that indicates the execution status of the function

osStatus stop (void) Stop the timer Returns the status code that indicates the execution status of the function

Example with a periodic thread using RtosTimer

…

void periodictask_thread(void const * args){
        printf((const char*) args);
}

int main() {
    Thread thread1(task_thread, (void *) "Continous thread 1 is running\r\n");
    Thread thread2(task_thread, (void *) "Continous thread 2 is running\r\n");
    RtosTimer periodicThread(
        periodictask_thread, 
        osTimerPeriodic, 
        (void *) "Periodic thread is running.............\r\n“
    );
    periodicThread.start(2000);

    Thread::wait(osWaitForever);
}

The result with the periodic RtosTimer

!!! image

Execution pattern of 2 continuous threads and 1 periodic thread

!!! image

The idle Task

The created tasks in previous example spend most of their time in the Waiting state While in this state they are not able to run an cannot be selected by the scheduler The cpu always needs something to execute There must be at least one task that can enter the Running state To ensure this case, the Idle task is automatically created by the scheduler. The idle task does not more than sit in a loop, and is always able to run

for (;;);

The idle task has the lowest possible priority to prevent never blocking a higher priority thread It is possible to add specific functionality to the Idle task Low priority, background or continuous processing Measuring the amount of spare processing capacity Placing the cpu in low power mode The idle task is a function called os_idle_demon and is placed in RTX_config.c (RTX_Conf_CM.c for mbed-rtos)

The idle Thread

Example changing the idle thread To be continued

Changing the Priority of a Thread

The scheduler wil always select the highest ready state thread as thread to enter the Running state Next example will demonstrate this by using the set_priority() method to change the priority Two threads with different priorities, neither making Thread::wait() calls that cause it to enter the Waiting state The thread with the highest relative priority will always be selected by the schedular

  1. Thread 1 is created with the highest priority
    • It is guaranteed to run first
    • It prints out some text and changes the priority of Thread 2 to a higher priority above its own priority
  2. Thread 2 starts to run as soon as it has the highest priority
    • Only one thread can be in the running state, so thread 1 is placed in the Ready state
  3. Thread 2 prints out some text and sets it priority back to below that of Task 1
  4. Thread 1 once again has the highest priority
    • Thread 1 re-enters the Running state, forcing Thread 2 back into the Ready state
#include "mbed.h"
#include "rtos.h"

Thread* thread1;
Thread* thread2;

void task1_thread(void const * arg){
    while(true){
        printf("Task 1 is running\r\n");
        printf("About to raise the Thread 2 priority\r\n");
        thread2->set_priority(osPriorityHigh);
    }    
}

void task2_thread(void const * arg){
    while(true){
        printf("Task 2 is running\r\n");
        printf("About to lower the Thread 2 priority\r\n");
        thread2->set_priority(osPriorityLow);
    }    
}

int main() {
    thread1 = new Thread(task1_thread, NULL, osPriorityNormal);
    thread2 = new Thread(task2_thread, NULL, osPriorityLow);

    Thread::wait(osWaitForever);
}

!!! image

Changing priorities at runtime This allows to have a thread to trigger another thread for a period of time If the work of the other thread is ready, it gives back the control to the original thread

!!! image

Deleting a thread

  1. Thread 1 created by main() with Normal priority
    • When it runs, it will crate Thread 2 at a higher priority
    • Thread 2 has the highest priority so it starts to execute immediately
  2. Thread 2 prints out some text and exits then
    • By exiting the thread function for thread 2, the Thread no longer has any work to do and will terminate itself
  3. When thread 2 terminates, task 1 is again the highest priority task.
    • It continues executing and then has to wait
    • Thread 2 goes into the waiting state
  4. The idle thread executes while task 1 is in the waiting state
    • (the idle thread will free the memory that was allocated to the thread 2)
#include "mbed.h"
#include "rtos.h"

void task2_thread(void const * arg){
    printf("Task 2 is running and about to delete itself\r\n");
}

void task1_thread(void const * arg){
    while(true){
        printf("Task 1 is running\r\n");
        Thread thread2(task2_thread, NULL, osPriorityHigh);
        Thread::wait(500);
    }    
}

int main() {
    Thread thread1(task1_thread, NULL, osPriorityNormal);

    Thread::wait(osWaitForever);
}

note: Thread2 goes out of scope and delete’s itself

!!! image

Thread 2 deleting itself, Thread 1 creates a new thread2 every time the loop has completed

Note: ! Some RTOS’s use the idle task to free up memory that was used by a deleted task

!!! image

The Sceduling Algorithm

RTX allows you to build an application with three different kernel-scheduling options. These are:

  1. Pre-emptive scheduling Each task has a different priority and will run until it is pre-empted or has reached a blocking OS call.
  2. Round-Robin scheduling Each task has the same priority and will run for a fixed period, or time slice, or until has reached a blocking OS call.
  3. Co-operative multi-tasking Each task has the same priority and the Round-Robin is disabled. Each task will run until it reached a blocking OS call or uses the os_tsk_pass() call.

The default scheduling option for mbed-rtos and RTX is Round-Robin Pre-emptive. For most applications, this is the most useful option.

Pre-emptive Scheduling

Mbed-rtos is a pre-emptive multitasking operating system. If a task with a higher priority than the currently running task becomes ready to run, mbed-rtos suspends the currently running task.

A preemptive task switch occurs when:

  • the task scheduler is executed from the system tick timer interrupt. Task scheduler processes the delays of tasks. If the delay for a task with a higher priority has expired, then the higher priority task starts to execute instead of currently running task.
  • an event is set for a higher priority task by the currently running task or by an interrupt service routine. The currently running task is suspended, and the higher priority task starts to run.
  • a token is returned to a semaphore, and a higher priority task is waiting for the semaphore token. The currently running task is suspended, and the higher priority task starts to run. The token can be returned by the currently running task or by an interrupt service routine.
  • a mutex is released and a higher priority task is waiting for the mutex. The currently running task is suspended, and the higher priority task starts to run.
  • a message is posted to a mailbox, and a higher priority task is waiting for the mailbox message. The currently running task is suspended, and the higher priority task starts to run. The message can be posted by the currently running task or by an interrupt service routine.
  • a mailbox is full, and a higher priority task is waiting to post a message to a mailbox. As soon as the currently running task or an interrupt service routine takes a message out from the mailbox, the higher priority task starts to run.
  • the priority of the currently running task is reduced. If another task is ready to run and has a higher priority than the new priority of the currently running task, then the current task is suspended immediately, and the higher priority task resumes its execution.

Round-Robin Scheduling

Round-Robin allows quasi-parallel execution of several tasks. Tasks are not really executed concurrently but are time-sliced (the available CPU time is divided into time slices and mbed-rtos assigns a time slice to each task). Since the time slice is short (only a few milliseconds) it appears as though tasks execute simultaneously. Tasks execute for the duration of their time-slice (unless the task's time slice is given up). Then, mbed-rtos switches to the next task that is ready to run and has the same priority. If no other task with the same priority is ready to run, the currently running task resumes it execution. The duration of a time slice can be defined in the RTX_config.c configuration file.

Cooperative Multitasking

If you disable Round-Robin Multitasking you must design and implement your tasks so that they work cooperatively. Specifically, you must call the system wait function like the os_dly_wait() function or the os_tsk_pass() function somewhere in each task. These functions signal the RTX kernel to switch to another task. This type of scheduling is not used by the Mbed-rtos, but is available as option in RTX

Execution pattern with pre-emptive points highlighted

!!! image

results matching ""

    No results matching ""