www.beck-ipc.com

Multitasking with @Chip-RTOS


Some common explanations about multitasking with the @CHIP-RTOS

MULTITASKING INTRODUCTION

PRIMARY TASK ATTRIBUTES

DOS PROGRAM TASKS

SYSTEM TIMING

CRITICAL SECTIONS IN PROGRAMS

CONTROL AND COMMUNICATION BETWEEN TASKS


IPC@CHIP Documentation Index



Multitasking Introduction

The @Chip-RTOS 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 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

      AcronymStands For
      APIApplication Programmers Interface
      ISRInterrupt Service Routine
      RTIReal-Time Interrupt
      RTOSReal-Time Operating System
      TCP/IPTransport Control Protocol / Internet Protocol

      Table 1 ) Abbreviations


    Reasons for using Multitasking

    Here are some situations where multitasking can be helpful.


      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:

      • 50 Hz Data Acquisition Loop (top priority)
      • 10 Hz Data Pre-processing Loop
      • 1 Hz Data Processing Loop
      • 0.1 Hz Kalman Filter
      • Background Loop for Integrity Self-test

      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.



    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.


      Resources Expended per Task

      Each task requires its own stack space. This stack must be made large enough to support the system's interrupt handlers, some of which operate without a switch to a system stack. A minimum stack space of 1024 bytes is recommended.


      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 on chapter Critical Sections in Programs.


Top of this document
IPC@CHIP Documentation Index



Primary Task Attributes

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


    Task Priority

    Each task executing under the @Chip-RTOS 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_Change_Task_Prio() 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 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:

      a) the task is created
      b) the task's priority is changed
      c) the task's time-slice period times out (see chapter Time-Slicing).

    Application program tasks can range in user priority from 3 to 127 (inclusive), where 3 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.


    Task State

    In Table 2 below, the possible states and sub-states for @Chip-RTOS 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_Restart_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_Restart_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

    1) - Trigger Wait sub-state is entered after RTX_Create_Task_Without_Start() or after a task has terminated.
    2) - A specified time-out period in milliseconds can be applied to these states.
    3) - Only the sub-state has changed here.
    Table 2 ) @Chip-RTOS Task States

    The set of active tasks we speak of as executing concurrently. However, only a single task (at most1) is executing at any given time since the IPC@Chip contains only a single CPU. The task selected for execution by the @Chip-RTOS 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 Routine (ISR) 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.

    1 Hardware interrupt service routines can momentarily suspend the executing task.

Top of this document
IPC@CHIP Documentation Index



DOS Program Tasks

Each DOS program is launched as a task under @Chip-RTOS. These tasks are created with initial priority 25 and time-slicing disabled. Within these DOS programs, users can create additional tasks with the RTX_Task_Create() or RTX_Task_Create_Without_Run() API





System Timing

The @Chip-RTOS uses a 1000 Hz Real-Time Interrupt (RTI) for its time base. Therefore one millisecond is the lower resolution available for task timing.

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


    Time-Slicing

    For the tasks created within 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 time_slice member of the TaskDefBlock structure passed to the RTX_Task_Create() or RTX_Task_Create_Without_Run() functions.

    A time-sliced task will be permitted to execute for time_slice milliseconds (RTI ticks) 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 ticks 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.

    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. During execution of a time-sliced task, there is a very slight additional load placed on the system's 1000 Hz RTI.



    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.


      Roughly Periodic

      The simplest form for a periodic task uses a RTX_Sleep_Time call 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.

      
      #define SLEEP_10HZ  (90)  // Assuming 10 ms CPU load per 100 ms
      
      
      
      void huge 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.


      Precisely Periodic

      A precisely periodic loop can be controlled with an RTOS timer. This will result in a periodic loop which on average tracks the CPU quartz clock. A RTOS timer periodically wakes up the periodic task loop as illustrated below in Figure 2.

      
      static int TaskID_10Hz ;
      
      
      
      void huge Task_10Hz(void)
      
      {
      
          while (1)           // 10 Hz loop
      
          {
      
               RTX_Sleep_Request () ;
      
               Activity_10Hz() ;   // Get here each 100 ms.
      
          }
      
      }
      
      
      
      static void huge Timer_Callback(void) // Clean 10 Hz
      
      {
      
          RTX_Wakeup (TaskID_10Hz) ; 
      
      }
      
      
      
      static TimerID ;
      
      static TimerProc_Structure Timer_Spec = {
      
          &TimerID,
      
          Timer_Callback,
      
          0,
      
          { '1', '0', 'H', 'z'},
      
          100              // .interval = 100 milliseconds
      
      } ;
      
      
      
      void Initialize(void)
      
      {
      
          extern TaskDefBlock Task_10Hz_Def ;
      
          RTX_Create_Task (&TaskID_10Hz, &Task_10Hz_Def) ;
      
          RTX_Install_Timer (&Timer_Spec) ;
      
          RTX_Start_Timer (TimerID) ;
      
      }           
      Figure 2 ) Timer Based Periodic Loop

      An alternative method here would be to use RTX_Suspend_Task in Task_10Hz and RTX_Resume_Task in Timer_Callback. However, the RTX_Wakeup has the advantage of a wakeup pending flag used in the implementation which covers for the case where, due to CPU loading, the Task_10Hz may not yet have reached the RTX_Sleep_Request call before the RTX_Wakeup is executed in the Timer_Callback. In this case when the RTX_Sleep_Request is later called in Task_10Hz, the API will return immediately after clearing the task's internal wakeup pending flag, which had been set when Timer_Callback called RTX_Wakeup before Task_10Hz reached its sleep.


Top of this document
IPC@CHIP Documentation Index



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.


    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.

    
    static unsigned long My_Ticker = 1 ;
    
    
    
    void huge Task_A(void)
    
    {
    
        if (My_Ticker < 0x7FFFFFFFL)
    
        {
    
           My_Ticker++ ;
    
            //  Borland compiler's machine code:
    
            //     ADD    My_Ticker,1H   ; Increment LS 16 bits
    
            // → Sensitive to task switch here!
    
            //     ADC    My_Ticker+2,0H ; Carry into MS 16 bits
    
        }
    
    }
    
    
    
    
    
    void huge 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 possible2.

    2 And "if something bad can happen, it will happen".

    All tasks in the @Chip-RTOS 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 has already executed 0xFFFE times and on its 0xFFFF'th execution it is preempted by Task_B at the indicated "Sensitive" point immediately after executing the ADD opcode which increments the lower half of the 32 bit up counter. At this exact point, the My_Ticker value will read zero due to the carry from the lower 16 bits not yet being applied to the upper half word. And thus Task_B lands in the Surprised_to_be_Here() function when it encounters the half updated My_Ticker reading.



    Protecting Critical Sections

    Three methods for protecting critical sections are presented here.

      1) Semaphore
      2) Interrupt Masking
      3) RTOS Task Switch Lock

    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.


      Semaphore Protection

      A common way to protect critical sections is with the use of semaphores. The @Chip-RTOS 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.

      
      static unsigned long My_Ticker = 1 ;
      
      
      
      void huge Task_A(void)
      
      {
      
          RTX_Reserve_Sem(semID, 0) ;
      
          if (My_Ticker < 0x7FFFFFFFL)
      
          {
      
             My_Ticker++ ;
      
          }
      
          RTX_Release_Sem(semID) ;
      
      }
      
      
      
      
      
      void huge Task_B(void)
      
      {
      
          unsigned long ticker ;
      
          RTX_Reserve_Sem(semID, 0) ;
      
          ticker = My_Ticker ;
      
          RTX_Release_Sem(semID) ;
      
          if (ticker == 0)
      
          {
      
                Surprised_to_be_Here() ;  // How did this 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:

        Task_A - Priority 60 (low priority)
        Task_B - Priority 4 (very high priority)

      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 60, which would mean that Task_B (priority 4) must sit waiting behind time consuming system activities such as FTP transfer (priority 41). 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.


      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.

      
      define MASK_INTERRUPTS   asm{CLI}
      
      #define ENABLE_INTERRUPTS asm{STI}
      
      
      
      static unsigned long My_Ticker = 1 ;
      
      
      
      void huge Task_A(void)
      
      {
      
          MASK_INTERRUPTS ;   // Needed if Task_A is lower priority
      
          if (My_Ticker < 0x7FFFFFFFL)
      
          {
      
             My_Ticker++ ;
      
          }
      
          ENABLE_INTERRUPTS ;
      
      }
      
      
      
      
      
      void huge Task_B(void)
      
      {
      
          unsigned long ticker ;
      
          MASK_INTERRUPTS ;  // Needed if Task_B is lower priority
      
          ticker = My_Ticker ;
      
          ENABLE_INTERRUPTS ;
      
          if (ticker == 0)
      
          {
      
                Surprised_to_be_Here() ;  // How did this 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).


      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.

      
      static unsigned long My_Ticker = 1 ;
      
      
      
      void huge Task_A(void)
      
      {
      
            // Disable needed if Task_A is lower priority
      
          RTX_Disable_Task_Scheduling() ;  // Task switch lock  
      
          if (My_Ticker < 0x7FFFFFFFL)
      
          {
      
             My_Ticker++ ;
      
          }
      
          RTX_Enable_Task_Scheduling() ;   // Resume task switching
      
      }
      
      
      
      
      
      void huge Task_B(void)
      
      {
      
          unsigned long ticker ;
      
            // Disable needed if Task_B is lower priority
      
          RTX_Disable_Task_Scheduling() ;  // Task switch lock  
      
          ticker = My_Ticker ;
      
          RTX_Enable_Task_Scheduling() ;   // Resume task switching
      
          if (ticker == 0)
      
          {
      
                Surprised_to_be_Here() ;  // How did this 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.


      Critical Section Protection Methods Summary

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

      MethodAdvantageLimitations
      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


Top of this document
IPC@CHIP Documentation Index



Control and Communication between Tasks

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

  • Semaphores
  • Event Groups
  • Message Exchanges

The usage of these @Chip-RTOS resources is covered in depth within the on-line HTML help.


Top of this document
IPC@CHIP Documentation Index