Link here

Using the Multitasker in C

How to use the real time multitasking operating system

The PDQ Board includes a multitasking executive that can concurrently execute a number of tasks, making it ideal for automation and instrument control applications. While the processor only executes one instruction at a time, the multitasker enables the processor to switch rapidly among tasks to give the impression that several tasks are being performed simultaneously. The tasks are linked in a round-robin circular list, and each task switch enters the next task in the list.

 

Setting up a task

There are three steps required to set up a new task.

  1. The task is created (declared). This assigns a name and a corresponding base address (also called a status address) to the task, and allocates a block of memory for use by the task in common RAM.
  2. The task is built. The new task’s user area and stacks are initialized, the task is asleep so it cannot be entered, and the task loop pointers are set to insert the task into the round robin task loop.
  3. The task is activated. This configures the task to execute a specified action program and leaves the task awake so that it will be entered by the mulitasker on the next pass around the round robin task loop.

The standard approach to coding a multitasking application is to make the main function itself an infinite loop that performs the actions of one of the required tasks, and then build and activate additional tasks to implement different functions required by the application. Using the standard approach, the Forth monitor task is not active at runtime. The advantage of this standard approach is that the default task area, which has generous allocations of RAM for stacks, is used to run a task, rather than running the Forth monitor task which is not typically useful in a standard C program.

A multitasking example is explained in detail below. This example defines a task whose action program continually increments a variable named counter. The multitasker switches between this task and another task (the main task running in the default task area) that periodically prints the value of counter to the Mosaic terminal, and accepts user keystrokes to wake up or put asleep the task that increments the counter.

 

Multitasking demonstration program in C

Here is the code listing that is discussed in detail.

Multitasking C Demo Program

  1: // Mosaic Industries multitasking demo program
  2:
  3: #include <mosaic\allqed.h>
  4:
  5: #define MAX_COUNTER 30000   // used by Inc_Counter()
  6: #define TWO_SECONDS  2000   // measured in milliseconds, default print interval
  7:
  8: static uint counter = 0;
  9:
 10:
 11:
 12: void Inc_Counter( void )
 13: {
 14:     if( ++counter > MAX_COUNTER )
 15:         counter = 0;      // roll over at max value
 16: }
 17:
 18: void Count_Forever( void )
 19: // infinite loop task action program
 20: {
 21:     for(;;)
 22:     {
 23:         Inc_Counter();
 24:         Pause();
 25:     }
 26: }
 27:
 28: // ******************* Multitasking **********************************
 29:
 30: TASK Counter_Task;  // declare the task and allocate task area in common RAM
 31:
 32: void Setup_Task()
 33: {
 34:     NEXT_TASK = TASKBASE;    // important! empties task loop before building
 35:     SERIAL_ACCESS = RELEASE_ALWAYS; // release serial resource after each char
 36:     RELEASE( &SERIAL1_RESOURCE ); // ensure released if main entered manually
 37:     BUILD_C_TASK( 0, 0, &Counter_Task ); // does not use heap
 38:     ACTIVATE( Count_Forever, &Counter_Task ); // define task's activity
 39: }
 40:
 41: void Nap( void )     // put calculation task asleep
 42: {
 43:     Counter_Task.USER_AREA.user_status = ASLEEP;
 44: }
 45:
 46: void Wakeup( void )  // wake up calculation task
 47: {
 48:     Counter_Task.USER_AREA.user_status = AWAKE;
 49: }
 50:
 51: void Toggle_Counter_Task_Status( void )
 52: // reverses the status of counter task
 53: {
 54:     if( Counter_Task.USER_AREA.user_status == ASLEEP )
 55:         Wakeup();
 56:     else
 57:         Nap();
 58: }
 59:
 60: // ******************* Main task **********************************
 61:
 62: static long last_announce_time;
 63:
 64: void Announce( void )
 65: // print current value of counter variable
 66: {
 67:     printf( "\r\nThe counter value is %6u.\r\n", counter );
 68: }
 69:
 70:
 71: void Announce_Periodically( ulong msec_interval )
 72: // print if more than msec_interval has elapsed since prior announcement
 73: {
 74:     ulong elapsed_msec;
 75:     elapsed_msec = CountToMsec( FetchTSCount() - last_announce_time );
 76:     if( elapsed_msec >= msec_interval )
 77:     {
 78:         last_announce_time = FetchTSCount();
 79:         Announce();
 80:     }
 81: }
 82:
 83: void Init_Vars( void )
 84: // do a runtime initialization of all variables
 85: {
 86:     counter = 0;
 87:     last_announce_time = 0;
 88: }
 89:
 90: void Main_Loop( void )
 91: // infinite loop called by main, prints counter value every 2 seconds
 92: // if user has pressed any key, this routine reverses the
 93: // wakefulness status of the counter task.
 94: {
 95:     int char_present = 0;
 96:     for(;;)
 97:     {
 98:         char_present = AskKey();  // if user has typed any char at keyboard...
 99:         if( char_present )
100:         {
101:             Key();   // ...read in the key to clear it out of serial port
102:             Toggle_Counter_Task_Status(); // reverse wakefulness of other task
103:         }
104:         Announce_Periodically( TWO_SECONDS ); // print counter value if it's time
105:     }
106: }
107:
108:
109: int main( )
110: // Do runtime initialization of global variables,
111: // build and activate the Counter_Task, start timeslicer,
112: // and enter infinite Main_Loop which prints counter value every
113: // 2 seconds, and accepts key inputs from terminal to toggle the
114: // wakefulness status of the counter task.
115: // If counter task is ASLEEP, the counter value will not increment;
116: // if AWAKE, it will change value with each successive printout
117: // on the terminal screen.
118: {
119:     // Disable libc output buffering, which causes unexpected behavior on embedded systems.
120:     // If I/O buffering would benefit your application, see the Queued Serial demo.
121:     setbuf( stdout, NULL );
122:
123:     Init_Vars();           // runtime init of variables and pointers
124:     printf( "\nPress any key to start/stop update of counter value." );
125:     printf( "\nTo halt the program, press the reset button." );
126:     Setup_Task();   // build and activate the CalculationTask
127:     StartTimeslicer();  // start the timeslicer; also calls ENABLE_INTERRUPTS
128:     Main_Loop();   // infinite loop, checks for keystroke
129:     return 0;      // just a formality: main should be declared to return an int
130: }
131:
132: // How to use this program:
133: // Compile the program and send the resulting *.dlf file to the board
134: // using the Mosaic terminal.  Type at the terminal
135: //        main
136: // This starts the program running.
137: // In addition to the Counter_Task, the default task running
138: // main (and thus Main_Loop) is running.
139: // To put the Counter_Task asleep so that the counter is no longer incremented,
140: // type any key.  The next key you type wakes up
141: // the Counter_Task and resumes incrementing the counter.
 

Define the task activation routine

The task activation routine is an infinite loop that is executed by a task. In the demonstration program, the Count_Forever() function is the task activation routine. It increments the counter variable up to the maximum specified value of 30,000, then rolls over to zero and continues incrementing. Placing the Pause() function in the inner loop passes control to the next task after the variable is incremented, granting other tasks a higher percentage of the processor's time. This is an example of using Pause() to make the processor time allocation more efficient.

In a more complex application with a variety of data structures, it is often convenient for each task activation routine to initialize its own task's variables and data structures before entering the infinite loop.

In the unlikely event that a task activation routine is not an infinite loop, then when it terminates the task will automatically execute the Halt() function which stops execution of the task, puts the task asleep, and calls Pause() to transfer control to an active task.

 

Create the task

To create (declare) a task named Counter_Task, execute

TASK Counter_Task;

TASK is a structure typedef that names and allocates a TASK structure (defined in the user.h include file) in common RAM.

For experts: The task area struct created by the TASK typedef has the USER_AREA struct at its base, and the first element in this struct is called user_status. As the multitasking executive routine (invoked either by the timeslicer or by Pause()) moves around the round robin loop, it examines user_status. The task is not entered if the user_status contents equal ASLEEP; the task is entered if the contents equal AWAKE. To transfer control to the next task in the round robin loop, the multitasking executive routine extracts the base address of the next task from the user_nextTask user variable, which is located just above user_status in the USER_AREA.

 

Build the task

Once a task has been declared, the next step is to build it using a statement such as

BUILD_C_TASK( 0, 0, &Counter_Task );

A task is an environment capable of running a program. Building the task involves setting up the environment by initializing the user area and a set of pointers. BUILD_C_TASK() builds a task with a specified Forth heap, locating its task area in a memory block decimal 2048 bytes long (if declared as TASK) starting at the specified taskbase address in common RAM, preferentially on-chip. BUILD_C_TASK sets up a user area, return and data stacks, runtime environment _reent_struct used to ensure reentrancy of C system function calls, TIB (serial terminal input buffer), POCKET (used for interactive debugging), and PAD (scratchpad area) in RAM starting at the base of the task area. The memory map (defined in the user.h include file) is as follows:

TASK Memory Allocation
Memory
area
Start
address
Size
(decimal bytes)
User Area base + 0 186
PAD base + 256 +128/-70
POCKET base + 384 32
Data Stack base + 512 -96 1)
TIB base + 512 96
Return Stack base + 1536 -928 2)
_reent struct base + 1536 501
padding base + 2037 11

The task is appended to the round-robin task list and left ASLEEP running the default action routine Halt().

The programmer specifies where the heap resides. BUILD_C_TASK expects a heap specification (start and end 32-bit xaddresses). Multiple tasks should not share a common heap; this can lead to difficult-to-diagnose multitasking failures. If the task does not need a private heap, the first two parameters passed to BUILD_C_TASK can be 0, as in this example.

With the exception of the user variables that set the memory map as discussed above, the initial values written into the Counter_Task user area are copied from the parent task (i.e., the task that is active when Counter_Task is built), which in this case is the default task that is running main. Note that while the initial values of user variables are derived from the parent task, the new task may modify its user variables at any time.

The task is left ASLEEP meaning that the multitasking executive will not run the task yet; that must wait until the task is activated.

Note that the statement

NEXT_TASK = TASKBASE; 

is executed before building and activating the task in the Setup_Task function in the example listing. This important statement empties the round robin task loop before building the task. This ensures that only one task is active while a new task is being built, thereby avoiding task loop initialization errors.

The statements

SERIAL_ACCESS = RELEASE_ALWAYS;
RELEASE( &SERIAL1_RESOURCE );

specifies the behavior of the tasks with respect to control of the resource variable that controls the serial port, and ensures that Serial1 has been released if main was executed interactively. It causes each of the low-level serial primitive functions AskKey() Key() and Emit() (which are called by functions such as printf() and scanf()) to release the serial resource variable after each character. This is discussed more in Shared Resources: Access to the serial ports.

The default task is pre-built by the operating system; it is the task area that executes main. The default task has a generous memory map with stack areas that are in general larger than those created by BUILD_C_TASK. For this reason, programmers may want to use the infinite loop inside main to execute the most stack-intensive part of the application program. Note that the TIB (terminal input buffer) area contains user input while the interactive Forth interpreter is running, and is overwritten by the _reent struct in the default task area when a C application is running. PAD is typically used in Forth application programs, for Forth formatted numeric output, and for the LOAD_STRING() macro.

Default Task Memory Allocation (Base address = 0x1200)
Memory area Start address Size
(decimal bytes)
User Area base + 0 186
PAD base + 256 +128/-70
POCKET base + 384 68
Data Stack base + 624 -172 3)
Return Stack base + 1952 -1328 4)
_reent struct base + 1952 501
TIB (overwritten) base + 2056 128
 

Activate the task

ACTIVATE associates a task activation routine with a task. In this example we activate the task using the statement

ACTIVATE( Count_Forever, &Counter_Task ); // define task's activity

This command initializes Counter_Task to run the action routine Count_Forever, and stores the constant AWAKE into Count_Forever STATUS variable, enabling the multitasking executive to enter and execute Counter_Task on subsequent passes through the round robin loop.

 

The main routine runs in the default task

The main function runs the entire application program. It runs on the default task that is initialized at startup by the operating system. main performs a runtime initialization of global variables by invoking Init_Vars(), calls Setup_Task() to build and activate the Counter_Task, starts the timeslicer, and enters the Main_Loop() function.

Main_Loop() is an infinite loop that prints the counter value to the terminal every 2 seconds, and accepts key inputs from terminal to toggle the wakefulness status of the Counter_Task. If Counter_Task is ASLEEP, the counter value will not increment; if it is AWAKE, the counter value will change with each successive printout on the terminal screen.

The Announce_Periodically() function accepts an interval specified as a number of elapsed milliseconds. The default is the constant TWO_SECONDS, defined as 2000. This function provides an example of how to use the CountToMsec() function described in an earlier section to manage timing based on the TIMESLICE_COUNT. The code declares a long global variable named last_announce_time that is loaded with the elapsed timeslice counts read with FetchTSCount() each time that an announcement is made. The code statements

elapsed_msec = CountToMsec( FetchTSCount() - last_announce_time );
if( elapsed_msec >= msec_interval )
{
    last_announce_time = FetchTSCount();
    Announce();
}

in Announce_Periodically() test if the number of milliseconds that have elapsed since the last announcement exceeds the specified interval (2000, corresponding to 2 seconds). If so, a the last_announce_time variable is updated and a new announcement is made.

The infinite loop in Main_Loop() continually checks the keyboard using the AskKey() function. If an incoming character is detected, it is read in using Key() and then the wakefulness status of the Counter_Task is toggled. The infinite loop also calls Announce_Periodically() to manage the timed announcement of the counter value.

 

Modifying a task’s status: AWAKE and ASLEEP

To put a task asleep, store the value ASLEEP into the task's user_status. For example, the Nap() function executes

Counter_Task.USER_AREA.user_status = ASLEEP;

To wake up a sleeping task, store the value AWAKE into user_status. For example, the Wakeup() function executes

Counter_Task.USER_AREA.user_status = AWAKE;

For experts: The task area struct created by the TASK typedef has the USER_AREA struct at its base, and the first element in this struct is called user_status. These are declared in the user.h include file. As the multitasking executive routine (invoked either by Pause() or by the timeslicer) traverses the round robin loop, it examines the contents of user_status. If the contents equal ASLEEP then the task is not entered; if the contents equal AWAKE then the task is entered.

 

Run the multitasking demonstration program

Using the standard procedure (Build→ Build in the IDE+), compile the example multitasking demonstration program and send the resulting *.dlf file to the board using the Mosaic terminal. Type at the terminal

main

to start the program running. At this point both the newly defined Counter_Task and default task running main will be executing. You should see in the terminal window a printout such as:

main
Press any key to start/stop update of counter value.
To halt the program, press the reset button.
The counter value is      0.

The counter value is   2064.

The counter value is   4130.

The counter value is   6195.

To control the program, press any key one time. This will toggle (reverse) the status of the Counter_Task from AWAKE to ASLEEP. When ASLEEP the timeslicer skips execution of the Counter_Task so that the value of the counter variable is not updated. You will see this in the terminal window: the announced counter value will be unchanging. Simply type another key to wakeup the Counter_Task, thereby resuming the updating of the counter value by the Counter_Task.

 

Customizing a task’s memory usage

Some large programs with many tasks become constrained by the available amount of common RAM. These programs can benefit from using customized task areas that use less than the standard 2000 bytes of common RAM per task.

The TASK macro allocates 2048 bytes preferentially in on-chip common RAM to support the requirements of a task. TASK is defined in the user.h include file on the Mosaic IDE Plus CD as follows:

#define REENT_PADBYTES       ( 512 - sizeof( struct _reent ) )
 
#define SMALL_BELOWPAD_SIZE      0
#define SMALL_PAD_SIZE           0
#define SMALL_DSTACK_SIZE    ( 256 - sizeof( struct userArea ) )
#define SMALL_RSTACK_SIZE      384
 
#define MEDIUM_BELOWPAD_SIZE ( 256 - sizeof( struct userArea ) )
#define MEDIUM_PAD_SIZE        128
#define MEDIUM_DSTACK_SIZE     128
#define MEDIUM_RSTACK_SIZE    1024
 
#define LARGE_BELOWPAD_SIZE  MEDIUM_BELOWPAD_SIZE
#define LARGE_PAD_SIZE       MEDIUM_PAD_SIZE
#define LARGE_DSTACK_SIZE    MEDIUM_DSTACK_SIZE
#define LARGE_RSTACK_SIZE    1792
 
struct medium_taskArea                     // 2 k total
{
    struct userArea user_area;             // 186 byte user area struct
    char   belowpad[MEDIUM_BELOWPAD_SIZE]; // Up to 70 bytes below pad can be used
    char        pad[MEDIUM_PAD_SIZE];      // 128 bytes
    char     dstack[MEDIUM_DSTACK_SIZE];   // 128 bytes
    char     rstack[MEDIUM_RSTACK_SIZE];   // 1 k
    struct _reent _reent_placeholder;      // 501 bytes
    char reent_padding[REENT_PADBYTES];    // 11 bytes
};
 
#define TASK __attribute__((section (".taskareas"))) struct medium_taskArea

TASK definition

The default task size can support tasks with reentrant (that is, multitasking-robust) calls to the resource-intensive C routines printf() and scanf and their related functions.

If too much common RAM is being used and some tasks are simple (that is, they do not call resource-intensive C functions like printf() or scanf()) then smaller task areas can be declared to conserve memory. The user.h include file has a macro called SMALL_TASK to declare a task area that occupies 1152 bytes of common RAM. Note that such a task cannot support printf functions, even during debugging, so caution must be used to stay within the task's memory constraints.

In the unlikely event that the standard TASK does not allocate enough stack area for a particular application, the user.h file also declares a macro called LARGE_TASK that adds 768 bytes to the return stack size, using a total of 2816 bytes of common RAM.

 

For Experts: Multitasker implementation details

This section describes the "nuts and bolts" of what the multitasking software is doing as it switches between the default task and the other task(s).

The first four user variables in each task’s USER_AREA (defined in the user.h include file) control the multitasking functions. user_status contains a flag that tells if the task is awake. The user variable user_nextTask (settable using the NEXT_TASK macro) points to the base (user_status) address of the next task in the round robin loop. The user variable user_rpSave contains the saved return stack pointer for the task, and the user variable user_serialAccess (settable using the SERIAL_ACCESS macro) controls access to the serial I/O resource variable.

When the task-switch routine Pause() is executed, it pushes the contents of the programming registers (PC, IY, IX, A, B, CCR) onto the return stack in the same order that an interrupt does, and also pushes the page onto the return stack. If the timeslicer is running (that is, if StartTimeslicer() was called) then the timeslicer interrupt pushes the contents of the programming registers (PC, IY, IX, A, B, CCR) onto the return stack, and also pushes the page onto the return stack. In other words, both Pause() and the timeslicer have the same stack effect, and both Pause() and the timeslicer call the same task switching code. This task switching code fetches the contents of user_nextTask to get the base (user_status) address of the next task. If the contents of the next user_status address equal zero (i.e., if the next task is AWAKE), then the processor's stack pointer is loaded with the contents of the next task’s user_rpSave, the page is popped off the return stack and stored to the PAGE.LATCH, and an RTI (return from interrupt) instruction is executed to restore the registers from the saved values on the return stack and resume execution where the task left off the last time it was active. The next execution of Pause() or the next timeslice interrupt will repeat the procedure to enter the next AWAKE task in the list. If a task has a nonzero value in user_status, it is not awake and is skipped.

If there is only one task in the round robin list (as after any restart), user_nextTask contains the address of the task’s own base address. Execution of Pause() causes the task to stack its own register state on the return stack and then re-enter itself by unstacking the state and resuming execution where it left off. If three tasks called A B and C are in the round robin task loop, user_nextTask of task A points to the user_status address of task B, user_nextTask of task B points to the user_status address of task C, and user_nextTask of task C points to the user_status address of task A, completing the loop. As many tasks as desired may be added to the loop, as long as there is enough common RAM to accommodate their task areas.

When a task is built by BUILD_C_TASK it is added to the round robin task loop by modifying the values of the user variable user_nextTask. In addition, BUILD_C_TASK initializes the return stack with a stack frame containing default values of the registers, and the user variable user_rpSave is initialized. The value of the program counter (PC) and page saved on the return stack point to the routine Halt(), an infinite loop that repeatedly puts the calling task ASLEEP and executes Pause().

When a task is activated, the task's return stack is configured with a frame containing default values of the registers, and user_rpSave is initialized to point to the frame. The values of the program counter and page saved on its return stack are modified to point to the execution address of the specified action program. Thus the action program will be executed when the multitasker enters the task. An additional copy of the return address and page of the Halt() function is buried under the return stack frame to ensure graceful termination of the task if a finite action program is specified by the programmer.

The register values saved on the return stack frame are loaded into the machine registers (IX, IY, D, CCR, and PC) each time that the task is entered. At the time ACTIVATE() is executed for a specified task, the value of the condition code register (CCR) that is placed on the task’s return stack frame has its I bit cleared so that interrupts will be globally enabled when the task is entered for the first time.



See also:

 
Notes:
1), 2), 3), 4)
grows down
This page is about: Multitasking in C Language - Creating and Scheduling Tasks Using Timesliced, Preemptive, and Cooperative Task Switching – How to use the real time multitasking operating system from a C application program: understanding the task memory map, building and activating a task, scheduling tasks using preemptive and/or cooperative task switching, and using mailboxes and resource variables to manage access to shared resources.
 
 
Navigation