IPC@CHIP® RTOS-PPC – API Documentation

Header image

Main page


RTOS Multitasking introduction

1. Multitasking introduction
    1.1 Reasons for using Multitasking
        1.1.1 Different Priority Work
        1.1.2 Event Triggered Actions
    1.2 Reasons not to use Multitasking
        1.2.1 Resources Expended per Task
        1.2.2 Critical Sections
2. Primary Task Attributes
    2.1 Task Priority
    2.2 Task State
3. Application (DOS) Program Tasks
4. System Timing
    4.1 Time-Slicing
    4.2 Time Limited Tasks
    4.3 Periodic Tasks
        4.3.1 Roughly Periodic
        4.3.2 Precisely Periodic
5. Critical Sections in Programs
    5.1 Example Critical Section
    5.2 Protecting Critical Sections
        5.2.1 Semaphore Protection
        5.2.2 Interrupt Masking
        5.2.3 RTOS Task Switch Lock
        5.2.4 Critical Section Protection Methods Summary
6. Control and Communication between Tasks

1. Multitasking introduction

The @CHIP-RTOS-PPC provides for a multitasking operation. A task provides a thread of execution. Each task has its own context, including an instruction pointer and program stack. Each task in the @CHIP-RTOS-PPC has a unique priority of execution, providing a preemptive form of multitasking. By executing multiple tasks at the same time, various activities can be performed concurrently.

Abbreviations Used

Acronym

Stands For

API

Application Programming Interface

ISR

Interrupt Service Routine

RTI

Real-Time Interrupt

RTOS

Real-Time Operating System

TCP/IP

Transport Control Protocol / Internet Protocol

Table 1 ) Abbreviations

1.1 Reasons for using Multitasking

Here are some situations where multitasking can be helpful.

1.1.1 Different Priority Work

Probably the most compelling reason for using multitasking is when required activities have different priorities. For example, an application with a user interface may need to be responsive to keyboard entry from a user console while at the same time the program is conducting a time consuming calculation or data base search. A low priority task could perform the calculation/data base search while a high priority task periodically polls (or sleeps waiting) for keyboard input. Typically there would be some communication between these two tasks, such as having the keyboard task cancel the background data base search in the event that the user presses the escape key.

In more involved applications, there might be a complete spectrum of concurrent activity at different priorities. Just for an example, the set of tasks within a real-time application might be as follows. Listed in priority order:

1.1.2 Event Triggered Actions

Sometimes an activity is required only upon an event such as some data becoming available. For these situations a task could be made to block, awaiting the event or data arrival. For example a task could make a blocking call on a TCP/IP port with the recv() library function. The task sleeps until some data arrives at the respective socket, or until a prescribed time-out period expires.

Note that polling can usually be an alternative software design approach, as opposed to applying a dedicated task to each event or data source.

1.2 Reasons not to use Multitasking

While multitasking can be an ideal solution for some applications, it also has its disadvantages. Some of these disadvantages are noted here. The point is that you should not complicate an application with multiple tasks unless to do so truly makes your application simpler.

1.2.1 Resources Expended per Task

Each task requires its own stack space. A minimum stack space of 1024 bytes is enforced by the RTX_NewTask() API, which fails to create a task if the user provided stack space is less than this amount. Note that inside API calls (system calls) and interrupt service routines, the system will switch to a different stack space. So the user stacks do not need to provide space for these purposes.

1.2.2 Critical Sections

Objects (memory or devices) shared between tasks can lead to critical sections. A critical section is a section of code in a task which is sensitive to order of execution relative to code in some other task, such that there is a possible (prior to adding the necessary protection) firing order of the two instruction streams which leads to incorrect results.

When you design your application with only a single task, it is safe to say you have no critical sections. When more than one task is used, you must beware. Due to the importance of understanding and recognizing critical sections in multitasking systems, this topic is covered in more detail below.


2. Primary Task Attributes

Two important attributes of each task are task priority and state.

2.1 Task Priority

Each task executing under the @CHIP-RTOS-PPC has a unique internal task priority. This internal task priority is represented with a 16 bit value, the most significant byte of which is the user task priority which is set at task creation time or using the RTX_Task_Priority() API (and is visible through the RTX_Get_Task_State() API). The hidden least significant byte of the task priority is used internally by the @CHIP-RTOS-PPC to assign each task at a given user priority a unique priority by appending a sequence number to the upper byte.

A task is assigned the lowest internal priority of all tasks with the same user priority whenever that task is appended to the list of tasks at that given user priority. This occurs when:

Application program tasks can range in user priority from 2 to 127 (inclusive), where 2 is higher priority. Generally, user task priorities between 20 and 30 are recommended. This recommendation is based on the priority assignments of the built-in system tasks. Too high a priority for an application task may block urgent system tasks: e.g. the Ethernet receiver task.

2.2 Task State

In Table 2 below, the possible states and sub-states for @CHIP-RTOS-PPC tasks are summarized. There are three primary states: Active, Blocked and Suspended.

State

Sub-State

Notes and State Transitions

Active

Executing

Highest priority non-waiting task

Pending

In queue ordered by task priority

Blocked

Trigger Wait1

RTX_Trigger_Task() --> Active

Semaphore Wait2

RTX_Signal_Sem(), RTX_Release_Sem() --> Active

Event Group Wait2

RTX_Signal_Events() --> Active

Message Exchange Wait2

RTX_Send_Msg() --> Active

Asleep2

RTX_Wakeup() --> Active

Suspended

Free to run

RTX_Resume_Task() --> Active

Trigger Wait1

RTX_Resume_Task() --> Blocked
RTX_Trigger_Task() --> Free to run3

Semaphore Wait2

RTX_Resume_Task() --> Blocked
Granted semaphore --> Free to run3

Event Group Wait2

RTX_Resume_Task() --> Blocked
RTX_Signal_Events() --> Free to run3

Message Exchange Wait2

RTX_Resume_Task() --> Blocked
RTX_Send_Msg() --> Free to run3

Asleep2

RTX_Resume_Task() --> Blocked
RTX_Wakeup() --> Free to run3

Table 2 ) @CHIP-RTOS-PPC Task States

The set of active tasks we speak of as executing concurrently. However, only a single task is executing at any given time since the IPC@CHIP® contains only a single CPU. The task selected for execution by the @CHIP-RTOS-PPC will always be the highest priority of the tasks that are in the active state.

The C-library routines which force a task to exit the Blocked and Suspended states when called by some other task or Interrupt Service Procedure (ISP) are stated in the table. The two inactive states, Blocked and Suspended, differ in their exit state transitions. The RTX_Suspend_Task() API transitions a task into the Suspended state.


3. Application (DOS) Program Tasks

Each application (DOS) program is launched as a task under @CHIP-RTOS-PPC. These tasks are created with initial priority 50 and time-slicing disabled. Within these programs, users can create additional tasks with the RTX_NewTask() API.


4. System Timing

The @CHIP-RTOS-PPC uses at default a 1000 Hz Real-Time Interrupt (RTI) for its time base. Therefore one millisecond is the lower resolution available for task timing. (The Real-Time Interrupt frequency and the time base can be changed with a CHIP.INI entry.)

Users can install Timer Callback procedures with the RTX_NewTimerCB() API. Your callback procedure is invoked within the top priority kernel task at a specified interval.

4.1 Time-Slicing

For the tasks created within application (DOS) programs by the user, a time-slicing feature is available. This feature is enabled for a specific task by specifying a non-zero number of milliseconds in the tkdSlice member of the RtxTaskDefS structure passed to the RTX_NewTask() API with the RTX_TA_TIME_SLICED flag set in the tkdAttr bit field.

A time-sliced task will be permitted to execute for tkdSlice milliseconds after which time it will be cycled to the end of the list of tasks at this task's user priority. (The task is not charged for time during which it was blocked, suspended or active-pending preempted by some higher priority task.) In the special case where it is the only active task at that user priority, it would then on time-out immediately be given another time_slice milliseconds execution time budget and allowed to continue execution. Otherwise one of the other active tasks pending execution at this same user priority will begin execution and the previously executing task whose time-slice expired will be cycled to the end of the priority queue in a round-robin fashion. Note that the next task to execute may or may not be configured for time-slice operation.

Hardware interrupt masking or the RTX_Disable_Task_Scheduling() API will interfere with the time-slice operation. No reaction to a time-out condition will occur until these locking mechanisms are released.

The time-slice operation would apply primarily to fully independent tasks which do not pass any data between each other. Time-slicing can introduce chaos into a program which could execute more orderly using explicit yields (e.g. RTX_Sleep_Time() API). The extra task switching due to time-slice operation can cause critical sections to appear where they otherwise would not if the task was permitted to execute up to where the program yields voluntarily.

There may be times where time-slicing is the graceful design solution, but reliance on this technique raises the suspicion that the software design was not thought out thoroughly. Also keep in mind that any task with lower user priority than the time-sliced tasks will never be executed so long as any of the time-sliced tasks are active.

This system implements time-slicing with the CPU's decrementer hardware counter and interrupt.

4.2 Time Limited Tasks

Tasks can be executed with hardware enforced time limits. This feature is enabled in either of two ways:

  1. At task creation time by specifying a non-zero number of milliseconds in the tkdSlice member of the RtxTaskDefS structure passed to the RTX_NewTask() API with the RTX_TA_TIME_LIMIT flag set in the tkdAttr bit field.
  2. With the RTX_Task_Watchdog() API.

When a time limited task executes beyond its specified time limit the system treats this as a fault (same manner as for a MMU access violation) and the task will either be automatically killed or it will resume execution in the failure path of a TryDejavu() user installed (on program stack) exception handler.

The task time limiter is implemented using the CPU's decrementer hardware counter and interrupt. A time limited task can not be time-sliced (hardware resource conflict). The task is not charged for time during which it has been preempted by higher priority tasks. However, the task will be charged for any time spent within interrupt service routines (ISR) due to that the CPU's decrementer continues counting on behave of the interrupted task during the execution of ISR. Consequently this time limit mechanism is intended for rough time limits and not highly accurate machine cycle counts.

A task's time limiter is defeated by either hardware interrupt masking or the RTX_Disable_Task_Scheduling() API. No reaction to a time-out condition will occur until these locking mechanisms are released.

At any time during execution, the task can recharge its time limiter one-shot by calling the RTX_Task_Watchdog() API. On each new execution of a time limited task started from its start gate, the task's time out limit is renewed for a another full count.

4.3 Periodic Tasks

Periodic tasks can be created in either of two ways, depending on how accurate the execution period is required to be for the respective application.

4.3.1 Roughly Periodic

The simplest form for a periodic task uses the RTX_Sleep_Time() API within a loop as shown below in Figure 1. The period of this loop will not be exact, but would be close enough for many applications.

Example:
#define SLEEP_10HZ  (90)  // Assuming 10 ms CPU load per 100 ms
     
void Task_Roughly_10Hz(void)
{
    while (1)
    {
        Activity_10Hz() ;   // Get here about each 100 ms
        RTX_Sleep_Time(SLEEP_10HZ) ;
    }
}
Figure 1 ) Sleep Based Periodic Loop

The SLEEP_10HZ constant used in this example is adjusted based on the expected system loading, including CPU dwell within the Activity_10Hz() procedure. This would require some timing measurements to be made during the program's development, if this extra accuracy is of any concern.

4.3.2 Precisely Periodic

A precisely periodic loop can be achieved using a periodic task. Periodic tasks require an extra kernel object, which is used as a task trigger timer. This means that each periodic task requires two kernel objects (both timers) instead of only the one kernel object wait timer which every task has. The fact that a periodic task is implemented with two kernel timers allows the RTX_Sleep_Time() and the kernel object timed wait API to be used without affecting the periodic trigger timing.

Figure 2 provides an example of a periodic task. This task's loop period on the average tracks the CPU quartz clock accuracy. A kernel timer periodically triggers the task to release it from the RTX_Wait_Period() sleep API.

Example:
#include <clib.h>

extern void Activity_10Hz(void) ;

static void Task_10Hz(void)
{
    while (1)               // 10 Hz loop
    {
        Activity_10Hz() ;   // Get here each 100 ms.
        RTX_Wait_Period() ; // Await next auto-trigger.
    }
}

static DWORD    MyStack[500] ;

static const RtxTaskDefS Task_10Hz_Def = 
{
    { '1', '0', 'H', 'z' },             // tkdTag[4]
    (RTX_TASK_PROC) Task_10Hz,          // tkdProc
    &MyStack[0],                        // tkdStackBase
    sizeof(MyStack),                    // tkdStackSize
    0,                                  // tkdlParam (not used here)
    30,                                 // tkdPriority
    RTX_TA_PERIODIC | RTX_TA_CATCH_UP,  // tkdAttr
    0,                                  // tkdSlice
    100,                                // tkdPeriod [ms] -> 10 Hz
    // Remaining members tkdPhase, and tkdPhaseRef
    // play no role in this example since their corresponding tkdAttr 
    // flags are zero.
} ;

static RTX_ID   TaskID_10Hz ;

void Initialize(void)
{
    TaskID_10Hz = RTX_NewTask (&Task_10Hz_Def) ;
}           
Figure 2 ) Periodic Task Loop

The use of the RTX_TA_CATCH_UP mode specified in the task definition was done only for illustration. This would be a design decision based on the nature of your Activity_10Hz() procedure. If this function, for example, contained a recursive filter, the "catch up" mode would probably be your choice so that what ever value is currently present at the filters input can be run through the filter the appropriate number of times, based on amount of elapsed real-time. On the other hand, if returning to the service again within the stated task period is sufficient then no catch up (spinning) would be required.

The RTX_GearChg() API can be used to change a tasks period during operation.


5. Critical Sections in Programs

When multitasking is used, the programmer must beware of critical sections which may occur between threads.

Critical sections can be protected with proper design. The important step at program design time is to identify these code sections, which are not always obvious. Most programmers are well aware of what critical sections are. However, due to their importance when multitasking, a simple example is provided here for emphasis.

5.1 Example Critical Section

Data sharing between tasks leads to critical sections when the shared data object can not be read or written atomically, within a single non-interruptible machine instruction.

Example:
static QINT My_Ticker = (0x1 << 32) ;

void Task_A(void)
{
    if (My_Ticker > 1)
    {
       My_Ticker-- ;
        //  Compiler's machine code:
        //    LWZ    r11,My_Ticker(r13)
        //    LWZ    r12,My_Ticker+0x4(r13)
        //    ADDIC  r12,r12,-1
        //    ADDME  r11,r11
        //    STW    r11,My_Ticker(r13)
        //  --> Sensitive to task switch here!
        //    STW    r12,My_Ticker+0x4(r13)
    }
}


void Task_B(void)
{
    if (My_Ticker == 0)
    {
        Surprised_to_be_Here() ;  // How did this happen?
    }
}
Figure 3 ) Critical Section Example

After a brief review of the C code in the above example, the C programmer might suspect a hardware problem if the Surprised_to_be_Here() function was to ever execute. However, with a closer examination of the resulting machine assembly code and multitasking consideration, we will see that execution of the Surprised_to_be_Here() function is possible. And as the rule goes, "if something bad can happen, it will happen".

All tasks in the @CHIP-RTOS-PPC system have a unique task priority. So in the above example either Task_A can potentially interrupt execution of Task_B, or visa-versa, depending on the assigned priorities. Consider the case where priority of Task_A is lower (higher numerically) than priority of Task_B, such that Task_B can preempt Task_A. This case can lead to execution of the Surprised_to_be_Here() function under the following circumstances.

Let us say that Task_A on its first execution is preempted by Task_B at the indicated "Sensitive" point immediately after executing the "STW r11" opcode which will be writing a zero into the MSH of the My_Ticker 64 bit integer. At this exact point, the My_Ticker value will read zero due to that the pending write of 0xFFFFFFFF from r12 into the LSH of My_Ticker is still pending. And thus Task_B lands in the Surprised_to_be_Here() function when it tests the partially updated My_Ticker.

5.2 Protecting Critical Sections

Three methods for protecting critical sections are presented here.

Each method has its advantages and limitations, which are summarized at the end of this discussion. The choice of which method to use will depend on the design situation.

Note: A fourth method of protection available for 32 bit objects are the atomic editing series of inline assembly functions. The atomic editing methods are more efficient than any of the three methods given here.

5.2.1 Semaphore Protection

A common way to protect critical sections is with the use of semaphores. The @CHIP-RTOS-PPC provides resource semaphores which provide a mutually exclusive access to a resource or data object.

The example defective code from Figure 3 above can be corrected with the use of a resource semaphore as shown below.

Example:
static QINT My_Ticker = (0x1 << 32) ;

void Task_A(void)
{
    if (My_Ticker > 1)
    {
        RTX_Sem_Wait(semID) ;
        My_Ticker-- ;
        RTX_Release_Sem(semID) ;
    }
}


void Task_B(void)
{
    QINT ticker ;
    RTX_Sem_Wait(semID) ;
    ticker = My_Ticker;
    RTX_Release_Sem(semID) ;
    if (ticker == 0)
    {
        Surprised_to_be_Here() ;  // Not going to happen.
    }
}
Figure 4 ) Protected Critical Section with Semaphore

Now the Surprised_to_be_Here() function will never be executed.

A potential disadvantage to using semaphores is a possible task priority inversion, where a high priority task is blocked by a lower priority task as it awaits the semaphore. To illustrate this point, consider an example where task priorities are designed as follows:

If Task_A is suspended while it has possession of the semaphore, Task_B will have to wait if it then tries to access the same semaphore at that moment. This wait is effectively at the very low priority 100, which would mean that Task_B (priority 4) must sit waiting behind time consuming system activities such as FTP transfer (priority 90). In applications where this potential priority inversion is not acceptable, either the interrupt masking or task lock methods of protecting critical sections discussed below can be considered as an alternative to using semaphores.

The priority inversion problem can also be avoided in some cases by using the priority inheritance type of resource semaphore. The simple critical section in the above example would be such a case, since the work done within the critical section is very limited (no blocking on other objects or sleep).

5.2.2 Interrupt Masking

Interrupt masking can in some cases be a safe alternative to using semaphores to protect critical sections. This fast method places a minimum load on the system, so is most suitable where performance is a concern. The interrupt masking method is used in the example below.

Example:
static QINT My_Ticker = (0x1 << 32) ;

void Task_A(void)
{
    if (My_Ticker > 1)
    {
        MaskInterrupts() ;   // Needed if Task_A is lower priority
        My_Ticker-- ;
        EnableInterrupts() ;
    }
}


void Task_B(void)
{
    QINT ticker ;
    MaskInterrupts() ;  // Needed if Task_B is lower priority
    ticker = My_Ticker ;
    EnableInterrupts() ;
    if (ticker == 0)
    {
        Surprised_to_be_Here() ;  // Not going to happen.
    }
}
Figure 5 ) Protected Critical Section with Interrupt Masking

This method of protection is safe to use when the section being protected executes in very few machine cycles, as is the case in this example. The concern is the hardware interrupt latency created by this interrupt mask period. Masking interrupts for as long as 50 microseconds should be tolerable on most systems. Caution must be used to assure that interrupts are always quickly re-enabled when ever they are disabled!

Note that when the nature of the two tasks competing for access to the resource (Task_A and Task_B in this example) dictates that one is higher priority than the other, only the lower priority task requires the interrupt masking. It is not possible that the lower priority task could preempt the higher priority task (unless the program design was to change task priorities dynamically somewhere).

5.2.3 RTOS Task Switch Lock

A further alternative to using semaphores to protect critical sections is to prevent task switching within the critical section. This method is shown in the example below.

Example:
static QINT My_Ticker = (0x1 << 32) ;

void Task_A(void)
{
    if (My_Ticker > 1)
    {
        // Needed if Task_A is lower priority
        RTX_Disable_Task_Scheduling() ;   
        My_Ticker-- ;
        RTX_Enable_Task_Scheduling() ;
    }
}


void Task_B(void)
{
    QINT ticker ;
    // Needed if Task_B is lower priority
    RTX_Disable_Task_Scheduling() ;  
    ticker = My_Ticker ;
    RTX_Enable_Task_Scheduling() ;
    if (ticker == 0)
    {
        Surprised_to_be_Here() ;  // Not going to happen.
    }
}
Figure 6 ) Protected Critical Section with Task Lock

Hardware interrupts continue to be serviced during the task lock, so you can include more work now within the critical section than was possible with the interrupt masking method. However, the task lock period should still be keep to some reasonable small amount of time. Note that task locks also inhibit all system timer activity.

5.2.4 Critical Section Protection Methods Summary

The design trade-offs for the three methods presented above for protecting critical sections are summarized in Table 3.

Method

Advantage

Limitations

Semaphore

A long duration of critical section does not adversely affect portions of system not accessing the semaphore.

Can result in a priority inversion.

Interrupt Mask

Most efficient of all three methods. (Executes quickly!)
No priority inversion

Impact on system operation becomes a concern if interrupt mask time can exceed around 50 us.

Task Lock

No priority inversion.

Impact on system operation becomes questionable if task lock duration exceeds around 400 us.

Table 3 ) Critical Section Protection Methods

6. Control and Communication between Tasks

The @CHIP-RTOS-PPC provides the following mechanisms for tasks to control one another and to communicate. These interactions can either be between tasks within the same application, or across applications.






Top of page | Main page

Copyright © 2017 Beck IPC GmbH
Generated on Thu Jan 26 16:21:35 2017 by Doxygen 1.6.1