Contents of the PROGSPEC.DOC file

Technical Questions: Please refer all technical questions to the author at

12 South Walker Street Lowell, MA 01851

Telephone: 508/ 454-1620

--------------------------------------------------------------------

I. General Notes

Be sure to specify the actual values in custom.h for maximum number of tasks and timers. Any custom events must be added to the custom.h events list. System events may be added to rtx.h files.

II. Building A System

rtx.mak shows all of the commands necessary to make all components of the real time system. Basically you must recompile any changes made to custom.h and rtx.h with the rest of the real time system. Then, link your code with the rtx.obj and regs.obj modules.

III. Components Not Supplied

There are no interrupt service routines supplied with the code except for a minimal keyboard interrupt service routine.

IV. Source Modules

rtx.h contains all prototypes for functions and data structures used by the real-time system.

rtx.c is the main source module and contains almost all of the routines for the real-time system.

regs.asm contains some assembly language routines. This is because register saving cannot be done well within C.

Almost all functions use a task pointer. This is a pointer to a structure of data maintained by the real-time executive for controlling a task. Task ids is an offset into this table. Sometimes it is more convenient to use the id, other times the task pointer.

The entire real-time system needs to be initialized by initTaskSystem().

The defineTask() function initializes task parameters and allocates a stack from the heap. The stack will contain both the start address of the task and a return address to terminateTask_id. A return from the task will terminate the task.

terminateTask_id() will free the allocated stack and free zero the task's priority. The task will not run again until redefined.

You can find a task by name using the findTask function. One powerful use of the naming convention is to create services. For example, a printer service could be called "Printer". To find the task pointer of the printer service, use the findTask() function.

Each task contains a queue. Queueing any data to the task is the equivalent of setting an event for the task. Tasks with queued data get a chance to run. This queue mechanism is maintained to support printer spooler and other task to task transaction processing needs.

A queue consists of a two queue pointers, one points to the start of the queue, the other to the end of the queue. The start of the queue is the start of a linked chain of data. The starting queue pointer points to any data. The first element of that data is assumed to be a queue pointer to the next queued data block. A NULL pointer is the end of the queued list.

defineEventFct() defines a function to test the SystemEvents bit for a specific event id. You can set, clear, and toggle system events that do not have a defined event function by using functions setSystemEvent(), clearSystemEvent(), and toggleSystemEvent().

A task can be set to wait for an event by the function waitEvent(). A NULL task pointer is converted to the current task.

Often tasks depend and must communicate with each other. A task may want to start or terminate other tasks. One example of this is my earlier problem with networks. A task needed to start another task copying all data in a temporary file to the network when the network was running.

Because of the event mechanism starting a waiting task is simple. An event flag is defined. One task sets the event flag and calls the scheduler. The scheduler will startup the task like any other event.

Passing data between tasks is more a matter of convention than of system support. Both tasks could agree that specific variables or structures will be used to communicate between tasks. This would constitute a voluntary or overt data sharing and it will always work between the two tasks so long as each task updates the data in this shared spaced according to established rules.

Sharing data invariably constitutes of a writting task and at least one reading task. That is, one task generates and writes the data to the shared space while the another task reads the data.

To stay out of trouble, make sure you write data in ways in which the reading task cannot get confused. The obvious example is when filling a buffer place the dat in the buffer before updating a buffer count or pointer. For more complex cases you may need to disable interrupts during the update process. This is generally the safest mechanism for updating shared data.

This simple rule is violated in many unconscious ways and "C" is particularly facilitating in this regard. For example, the code in Figure 2a shows how utterly simple it is to update a counter in a buffer before placing the contents in the buffer.

-------------------------------------------------------------------- Figure 2b. Correct way to update dat in a shared buffer.

If the updating task is interrupted, that interruption could occur at any time and between any two machine level instructions including any two critical instructions. You will experience occasional garbage characters or occassional unexplained behavior which is almost always the result of miscommunicated data.

Disabling Interrupts And the Scheduler

The problems associated with sharing data can be eliminated by disabling interrupts during the data update process. Two functions are provided to disable and reenable interrupts, disableInts() and enableInts().

By disabling interrupts you disable any event that may cause the scheduler to run. Your task remains in control and no other task has a chance to run.

An alternative would be to disable the scheduler itself and leave hardware interrupts alone. This is preferential intellectually. The scheduler is disabled and enabled by the disableSched() and enableSched() functions.

There is a potentially more serious problem in a multitasking system, that of inadvertant data sharing. This is different from the above mentioned problem of intentional data sharing.

When a product is built, all tasks are linked together with the real time executive. Global data declared for any one task is available to all tasks and inadvertant data sharing can and does unfortunately happen.

One easy way to dramatize the problem of sharing data is to assume a variable 'n' used as a loop counter. Two tasks inadvertantly use the variable. While the first task is in the loop counting down the value in variable 'n' the task is interrupted to run the other task. The second task resets and uses the variable. The results for the first task, when reactivated, will be unamussing and unpredictable.

If you are not careful about sharing variables there may be behavior within your application that at best you don't understand and at worst highly sporadic.

Other than being careful, you can avoid inadvertant data sharing by using and allocating variables in a stack or on the heap. Each task is given a separate stack so variable allocated on the stack are safe from inadvertant data sharing.

Within C, variables are made private by declaring them on the stack or within {...}. Declaring them 'static' will make them sharable.

A 'static' declaration will make it private for the source module in which it appears. So long as you know that this code is not shared by other tasks then you have a private variable. Figures 1a and 1b shows how to make variables private by declaring them on the stack.

All of these examples make these variables sharable across tasks. A 'static' variable is only known inside this module, but the code in this module may belong to more than one task.

---------------------------------------------------------------------

/* declare variables inside functions to make them private */

int any_routine() { int n, k, p, q = 0; char far * ptr;

. . .

}

--------------------------------------------------------------------- Figure 1b. Private use of a variable.

Having cautioned you about inadvertant data sharing let me state that deliberate data sharing is not bad. You may want, so long as you observe certain rules, to share data. One example of this is an interrupt service routine that stores the current status of the hardware in a common location. The "rule" on data sharing is that the value is always just modified by a single piece of code, the interrupt service routine. It can then be read by any task.

In the newswire capture application a common buffer for storing received data was shared between the receive driver and processing task. To minimize problems rules need to be established as to how the data was accessible. A single routine accessed data. Interrupts were disabled when the character was retrieved and the character count changed. This was a short period of time but it meant that the interrupt service routine would not have a chance to access this shared data.

Code Sharing

Interestingly, while you need to be very careful about sharing data between tasks, there is no restriction on sharing code. Our real-time multitasking system imposes no restriction whatsoever on code sharing and in fact may actually happen all the time in ways which you may not have realized.

Code sharing can and does happen. Assume that task A is running executing a very simple function max(). Max accepts two values and returns the higher of the two. When max() is called within task A, the two arguments are pushed on the stack. If at any time task A is interrupted while inside max(), the stacked values and any register values are preserved for task A. If task B is started while task A is interrupted, it may execute the same function max() with values stored in a different stack. Code is sharable because the data for each task is kept separate.