Link here

Chapter 10 - Multitasking


QED-Forth is a multitasking language and operating system. It includes a multitasking executive that can "concurrently" execute a number of tasks. In actuality, the processor only executes one instruction at a time. However the multitasker allows the processor to rapidly switch among tasks to give the impression that several tasks are being performed at once. The tasks are linked in a circular "round robin" list, and each task switch causes the next task in the list to be entered.

 

The Advantages of Multitasking

Multiple functions can be performed without a multitasking executive by explicitly coding them into a large loop that performs all of the needed functions. The drawbacks of this approach are poorly controlled real-time performance and lack of modularity. The multitasking approach overcomes these drawbacks.

Real-time systems must be able to perform tasks in a timely fashion. For example, an instrument may have to update a display every tenth of a second, and it might also have to perform a calculation that requires half a second to complete. If these functions are performed in a single large loop in a non-multitasked system, all of the functions in the loop interact to determine the timing of the functions. In the example just cited, the calculation function might have to be broken into several parts and interspersed with calls to the display update function so that the display is updated frequently enough. But then the timing changes every time the computation is re-coded or another function is added to the loop; this is due to lack of modularity. Because all of the details of the implementation of the loop affect the other functions in the loop, the program is extremely difficult to maintain and modify.

Another solution is to implement the time-critical display function as an interrupt routine. This will work, but if the application has many time-critical tasks the number of interrupts grows, making the code less manageable.

The QED-Forth multitasker can handle task switching for you, allowing you to design modular code with well specified real-time behavior. Each task is defined as a separate environment with access to all of the resources it needs to function. Each task is debugged individually, and then installed into the round robin task loop to allow concurrent execution. A properly designed multitasked system allows the tasks to be modified without degrading system performance, and the application program can be upgraded by adding more modular tasks to the round robin loop.

 

Cooperative and Timesliced Task Switching

There are two types of task switching supported by the QED-Forth multitasker. The first is cooperative task switching, a software-invoked task switch that is initiated by executing the kernel word PAUSE. To use cooperative task switching, PAUSE must be invoked by each task. Non-time-critical tasks can call PAUSE often to allow time-critical tasks to use more CPU time.

In addition to cooperative multitasking, QED-Forth supports timesliced task switching. This scheme forces a task switch with every tick of an interrupt-driven clock. The period of the clock is selected by the programmer. Timesliced task switching is very powerful, allowing the programmer to guarantee that each task is executed with a specified minimum frequency. For example, in a 4-task system with one time-critical task that must refresh a display every 100 milliseconds (msec), a timeslice period of less than 25 msec guarantees that the refresh operation is accomplished.

Unlike most multitasking FORTHs, QED-Forth allows you to choose cooperative or timesliced multitasking, or to mix them in any combination you choose. The multitasker also supports controlled access to shared resources, and the passing of messages among tasks for data sharing and task synchronization.

 

Multitasking Lexicon

To describe QED-Forth's multitasker, it is important to first define some terms. First, a task is not a program or routine. Rather, a task is an environment capable of running a program. The environment contains at the minimum a user area and a return stack, both in common ram. Tasks typically have a full user area (up to 256 bytes) and a data and return stack. A user-defined program that contains an infinite loop can be made the action program for a task. A shared resource is a hardware or software resource that is used by more than one task; for example, a serial I/O port could be a shared resource, as could an A-to-D converter or an area of memory. A resource variable is used to control access to a shared resource. A mailbox is a variable that is used to pass messages among tasks.

QED-Forth's multitasking executive provides a means for defining tasks, associating action programs with tasks, switching tasks, killing tasks, passing messages, and managing access to shared resources so that the state of the resource is not corrupted by access from several tasks at once.

 

Initializing the Multitasker and Elapsed Time Clock

When QED-Forth first comes up after a restart, timeslicing is disabled. Task switching is occurring, however. The task-switching routine PAUSE is being executed by the routines that access the serial I/O port (namely, by KEY and EMIT and ?KEY). The KEY and EMIT routines spend a lot of time waiting to receive or send a character, and while waiting, they execute PAUSE to give other tasks a chance to execute. Because there is only one task in the round robin list after a restart, the main task running QED-Forth maintains control.

When it is enabled, the timeslicer manages task switching and maintains an elapsed-time clock. Executing

INIT.ELAPSED.TIME

initializes to zero the contents of QED-Forth's 32-bit elapsed-time clock, called TIMESLICE.COUNT. This clock is incremented with each tick of the timeslice clock. Executing

START.TIMESLICER

starts a timeslice clock based on the OC2 interrupt with a default 5 msec period. The default timeslice period is 5 msec regardless of whether an 8 MHz or 16 MHz crystal is installed on the QED Board. The timeslice period can be changed by the programmer as explained below.

In addition to the task switches invoked by PAUSE during serial I/O operations, the multitasking executive is also switching tasks every 5 msec when the timeslice clock is active. No other tasks have been defined yet, so the task switches just return control to the main QED-Forth task. Note that the timeslice clock is not itself a task. It is an interrupt service routine that interrupts the processor only briefly every 5 msec.

To turn off the timeslicer, execute

STOP.TIMESLICER

This disables the OC2 interrupt but does not alter the contents of TIMESLICE.COUNT.

 

Reading the Elapsed-Time Clock

The timeslicer increments a 32-bit elapsed-time clock counter whose address is left on the stack by the command TIMESLICE.COUNT. The command

TIMESLICE.COUNT |2@|

leaves on the stack a double number equal to the elapsed time that the timeslicer has been active since the counter was initialized, in units of the timeslice period (typically 5 msec). The elapsed-time clock can also be read using

READ.ELAPSED.TIME ( -- u\ud | u =#elapsed.msec, ud= #elapsed.seconds)

If the elapsed-time clock was initialized to zero with the command INIT.ELAPSED.TIME, the outputs of READ.ELAPSED.TIME represent the number of milliseconds and seconds that the timeslicer has been active.

The command

READ.ELAPSED.SECONDS ( -- u1\u2\u3\u4\u5 )

returns the number of milliseconds, seconds, minutes, hours, and days that the timeslice clock has run since it was initialized to zero by INIT.ELAPSED.TIME. u5 is the number of days, u4 is the number of hours since the last integral day on the clock, u3 is the number of minutes since the last integral hour on the clock, u2 is the number of seconds since the last integral minute on the clock, and u1 is the number of milliseconds since the last integral second on the clock. The resolution equals the period of the timeslice clock (the default is 5 msec). The maximum time that the clock can represent is proportional to the timeslice period; the clock can represent times to over 248 days if the default 5 msec timeslice period is used.

 

Changing the Timeslice Period

To change the timeslice period, express the desired period as a multiple of 100 microseconds (usec) and execute the command *100US=TIMESLICE.PERIOD (pronounced "times-100-microseconds-equals-timeslice-period"). For example, to switch tasks every 25 msec (instead of the default 5 msec), execute

DECIMAL 250 *100US=TIMESLICE.PERIOD

With this period, the elapsed-time clock can run for over 3 years before rolling over to zero.

If the selected timeslice period is so long that it cannot be achieved by the OC2 interrupt clock, an "invalid parameter" error message will be issued. For example, the main timer counter register TCNT is incremented every 2 usec (regardless of whether your QED Board is clocked at 8 MHz or 16 MHz). This means that the 16 bit free-running TCNT register (and hence the OC2 interrupt) can deal with times up to 65,535 * 2 usec, or about 131 msec. To achieve timeslice periods longer than 131 msec you can change the period of the main TCNT timer using the INSTALL.REGISTER.INITS command described in the next chapter.

 

Setting Up a New Task

There are three steps required to set up a new task. First, the task is created. This assigns a name and a status xaddress to the task. Second, the task is built. This sets up the new task's user area, puts the task "asleep" so it cannot be entered, and installs the task into the round robin task loop. Third, the task is activated. This sets up the task to run a specified action program and leaves the task "awake" so that it will be entered on the next pass around the task loop.

The multitasking example that was introduced in the "Getting Started With The QED Board" booklet is explained in detail here. The example sets up a task whose action word continually increments a variable named COUNTER. The multitasker switches between this task and another task which runs the QED-Forth interpreter program.

 

Creating a Task

To create a name for the task and assign the base address of its user area (which is also referred to as the "status address" or "task identifier address"), execute

HEX↓ ok
9600 0 TASK: MY.TASK↓ ok

TASK: checks to make sure that the specified address is in the common RAM and then calls XCONSTANT. Thus invoking the name MY.TASK simply leaves the assigned status xaddress on the data stack. The specified status xaddress must be in the common RAM (i.e., in modifiable memory at an address greater than 8000H), and for "standard tasks" (explained below) there must be 1K of RAM available above the base address. A check of the memory map in Appendix A shows that the address 9600H meets these criteria.

Just as an array or a matrix is identified by its parameter field address, so a task is identified by its status xaddress, which is the base xaddress of its user area. The first user variable in the user area is named STATUS, so executing the word STATUS places the base xaddress of the current task on the stack. For example, executing

STATUS↓ ok [ 2 ] \ 8400 \ 0
..↓ 0 8400 ok \ clear the stack

tells us that the user area for the task that is running the QED-Forth interpreter starts at address 8400H on the default page.

The STATUS user variable contains a flag whose value is either AWAKE (0) or ASLEEP (1) . As the multitasking executive traverses the round robin loop, it enters only those tasks whose STATUS variable indicates that the task is AWAKE.

 

Building a Task

The next step is to build the task. Recall that a task is "an environment capable of running a program". Building the task involves setting up the environment by initializing the user area, including the user variables that specify the memory map. The word BUILD.STANDARD.TASK sets up a user area, return and data stacks, TIB, POCKET, and PAD in the 1 Kbyte of RAM above the status xaddress. The memory map is as follows:

Memory area Start address Size (bytes)
User area Status+0 256
Return stack Status+200H -256
Data stack Status+300H -256
TIB Status+300H 96
POCKET Status+360H 36
PAD Status+3A8H +88/-36

where a negative size indicates that the area grows downward in memory. Because memory is allocated for TIB, POCKET, and PAD, the standard task is capable of interpreting commands from the serial line and performing number/string conversion. The name and definitions areas are initialized to address 0\0 in ROM so that the standard task does not have compilation privileges. The programmer specifies where the heap and variable area reside. In general, multiple tasks should not share a common heap; this can lead to hard-to-diagnose multitasking failures.

If the task does not need to use a private heap or variable area, they can be placed at address 0\0 in ROM. BUILD.STANDARD.TASK expects a heap specification (start xaddress under end xaddress) under an xaddress that specifies the variable area, under the status xaddress. Since MY.TASK will not be using the heap or allocating any variables, default addresses of 0\0 can be specified for the heap start, heap end, and variable area. To build the task execute

0\0 0\0 0\0 MY.TASK BUILD.STANDARD.TASK↓ ok

The first two 0\0's are the heap specification, and the next 0\0 specifies the start of the variable area. MY.TASK leaves its extended status address/task identifier on the stack. BUILD.STANDARD.TASK sets up a user area for MY.TASK starting at its assigned status xaddress. It also stores the ASLEEP constant into the task's STATUS xaddress, activates the word to run the default program HALT explained below, and installs the task into the round robin loop. The new task will not be entered until it is awakened and activated with an action program.

With the exception of the user variables that set the memory map as discussed above, the initial values written into MY.TASK's user area are copied from the "parent task" (i.e., the task that is active when MY.TASK is built), which in this case is the task that is running the QED-Forth interpreter. This means, for example, that because QED-Forth was in the hexadecimal base while MY.TASK was built, the user variable BASE in MY.TASK's user area is also set for hexadecimal number conversion. CONTEXT and CURRENT are also copied from the parent task, so if the new task runs a FORTH-style interpreter, all of the words in the parent task's dictionary can also be found from within the new task. 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" which means that the multitasking executive will not attempt to run the task. Even if we were to wake the task up by storing the AWAKE constant to the task's STATUS user variable, the task's default action program (called HALT) would immediately put the task back to sleep and execute PAUSE to pass control to the next task.

 

Activating a Task

Next we define a word to serve as the action program for the task. The action program is typically either an infinite loop or a finite routine that ends with a HALT instruction (which is itself an infinite loop). The ACTIVATE routine automatically places a call to HALT on the task's return stack frame, so the task will always terminate gracefully even if the action program stops executing. If a finite action program is specified, the task will go ASLEEP running HALT after the action program finishes executing.

This simple action program increments the value of a variable named COUNTER

VARIABLE COUNTER↓ ok
: INC.COUNTER ( -- )
1 COUNTER +!
;
: COUNT.FOREVER ( -- )
BEGIN
INC.COUNTER PAUSE
AGAIN
;

Placing PAUSE inside the infinite loop makes this action program suitable for cooperative task switching. Each time the counter is incremented, control is passed to the next task in the loop.

The word ACTIVATE expects the extended code field address (xcfa) of the action program under the task's status xaddress:

CFA.FOR COUNT.FOREVER MY.TASK ACTIVATE↓ ok

This command initializes MY.TASK's return stack frame to run COUNT.FOREVER, and stores the constant AWAKE into MY.TASK's STATUS xaddress.

Now two concurrent tasks are executing: one task runs the QED-Forth interpreter, and the second runs COUNT.FOREVER. To verify this, type

COUNTER ?

which is interpreted and executed by the QED-Forth task. You will see some random number that changes each time you type the command as the background task continually updates the value.

 

Modifying Other Task's User Variables

The kernel word TASK'S.USER.VAR allows one task to access another task's user variables. This of course must be done with care to avoid corrupting the user area of a task. As an example, to check the value in VP in MY.TASK, execute

VP MY.TASK TASK'S.USER.VAR X@ ..↓ 0 0 ok

which shows that 0\0 is the variable area for MY.TASK, as we specified when building the task.

One common use of TASK'S.USER.VAR is to awaken a task or put it to sleep. For example, to put MY.TASK to sleep, the constant ASLEEP should be stored into the STATUS variable of MY.TASK. This is accomplished by executing

ASLEEP STATUS MY.TASK TASK'S.USER.VAR !↓ ok

ASLEEP leaves the value 1 on the stack. STATUS leaves the extended address of STATUS in the current task on the stack, and MY.TASK leaves the base address of the target task on the stack. TASK'S.USER.VAR uses this information to compute the address of STATUS in the target task. Then ! stores the value ASLEEP into STATUS in the user area of MY.TASK. After this command is executed, the value of COUNTER is no longer incremented because the multitasking executive does not enter MY.TASK. To awaken a sleeping task, store the value AWAKE into its STATUS user variable.

 

Killing a Task

Killing a task removes it from the round robin task list, freeing its memory to be used for other purposes:

MY.TASK KILL

Once this command is executed, the QED-Forth task is again the only task in the round robin loop. The name of MY.TASK still exists. To again set up MY.TASK to run a program, it must be built using BUILD.TASK or BUILD.STANDARD.TASK, and activated using the ACTIVATE command. Note that unpredictable results will occur if a task KILLs itself!

 

Implementation Details

The line input routine of the QED-Forth interpreter repeatedly calls KEY to input the next character from the serial port, and EMIT to echo the characters and print output characters. Each time KEY or EMIT executes, PAUSE is called until the serial port is ready to receive or transmit a character. PAUSE causes control to shift to MY.TASK which increments COUNTER and executes PAUSE to return to the QED-Forth task. This rapid task switching makes it appear as though both tasks are executing simultaneously.

The first four user variables in each task's user area control the multitasking functions. STATUS contains a flag that tells if the task is awake. The user variable NEXT.TASK points to the STATUS address of the next task in the round robin loop. The headerless user variable RP.SAVE contains the saved return stack pointer for the task, and the user variable SERIAL.ACCESS controls access to the serial I/O resource variable.

When the task-switch routine PAUSE is executed, it stacks the contents of the programming registers (PC, IY, IX, A, B, CCR) on the return stack in the same order that an interrupt does, saves the page on the return stack, and fetches the contents of NEXT.TASK to get the STATUS address of the next task. If the contents of the next STATUS address equal zero (i.e., if the task is awake), then the 68HC11's return stack pointer is loaded with the contents of the next task's RP.SAVE, the page is popped off the return stack and stored to the PAGE.LATCH, and an RTI is executed to restore the registers from the saved values on the return stack and resume execution where the task left off. The next execution of PAUSE will repeat the procedure to enter the next awake task in the list. If a task has a nonzero value in STATUS, it is not awake and is skipped.

Timesliced multitasking works in the same way as PAUSE, except that the timeslice (OC2) interrupt automatically stacks the registers (while PAUSE has to execute assembly commands to explicitly stack the registers). The OC2 service routine pushes the page onto the stack with the other saved registers, increments the 32-bit elapsed-time clock, and enters the next awake task in the round robin loop as described above.

If there is only one task in the round robin list (as there is after any restart), NEXT.TASK contains the address of the task's own STATUS. Thus execution of PAUSE causes the task to stack its 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 #1 #2 and #3 are in the round robin task loop, NEXT.TASK of task#1 points to the STATUS address of task#2, NEXT.TASK of task#2 points to the STATUS address of task#3, and NEXT.TASK of task#3 points to the STATUS address of task#1, completing the loop. As many tasks as desired can be added to the loop, as long as there is sufficient common RAM to accommodate their user areas and stacks.

When a task is built, it is added to the round robin task loop by modifying the values of the user variable NEXT.TASK. In addition, the return stack is initialized with a stack frame containing default values of the registers, and RP.SAVE is initialized. The value of the program counter (PC) and page saved on the return stack points to the routine HALT, an infinite loop that repeatedly puts the calling task ASLEEP and executes PAUSE.

When a task is activated, the return stack is again initialized with a stack frame containing default values of the registers, and RP.SAVE is initialized. The values of the program counter and page saved on its return stack are modified to point to 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 HALT 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, the value of the condition code register (CCR) that is placed on the task's return stack frame is set equal to the actual contents of the CCR register. One of the bits in the CCR is the I-bit which controls whether interrupts are globally enabled. If interrupts are required in your multitasked application, and if cooperative (PAUSE) multitasking will be used exclusively, make sure that interrupts are globally enabled when the tasks are ACTIVATEd. Otherwise interrupts may become globally disabled and re-enabled at unpredictable times as task switching occurs. If the timeslicer will be running in your application (that is, if you execute START.TIMESLICER), you need not worry about this matter. The START.TIMESLICER routine automatically examines all of the return stack frames in the task loop and initializes the CCR contents in each stack frame so that interrupts remain globally enabled as task switching occurs.

 

Task Switch Times

If the QED Board is running with an 8 MHz crystal, a task switch caused by PAUSE requires 44 usec, and interrupts are disabled for 30 of the 44 usec. A typical timesliced task switch requires 58 usec; however, 64.5 usec are required once every 65,536 times when the least significant cell of the elapsed-time clock overflows. Interrupts are disabled during the entire timesliced task switch. These times are cut in half if the board is clocked at 16 MHz.

 

Customizing a Task's Memory Map

The command BUILD.STANDARD.TASK allows you to set up a new task with a fairly simple command. Using BUILD.STANDARD.TASK is convenient and easy, but the default memory map that it sets up may be unsuitable for some tasks. A more powerful command called BUILD.TASK (see its glossary entry) allows a complete specification of the memory map of the new task. It facilitates the design of tasks that optimally use QED-Forth's limited common RAM space.

BUILD.STANDARD.TASK sets up a default memory map for a new task in a 1 Kbyte area above the task's status address. The first 1/4K is the user area, followed by a 1/4K return stack, a 1/4K data stack, and a 1/4K area that contains TIB, POCKET, and PAD. The programmer specifies the location of the variable area and the heap, and the name and definitions areas are placed in ROM to deprive the new task of compilation privileges.

This default memory map may not provide enough stack space for some tasks, and may allocate more common RAM than necessary for other tasks. For example, the task defined above runs the program COUNT.FOREVER which does not use TIB, POCKET, or PAD. In fact, to run this task only the first four user variables (the four that relate to multitasking) and small data and return stacks are necessary. This task can be built to require much less memory by allocating an 8-byte user area, a 64-byte return stack, and a 24 byte data stack, as

 
DECIMAL
0\0  0\0    \ specify heap
0\0        \ specify variable area
0\0        \ specify definitions area
0\0        \ specify name area
0\0        \ specify TIB area
0\0        \ specify PAD area
0\0        \ specify POCKET area
MY.TASK 72 XN+    \ specify bottom of return stack
MY.TASK 96 XN+    \ specify bottom of data stack
MY.TASK        \ specify task's base xaddress
8        \ specify size of user area
BUILD.TASK

This builds a task that requires only 96 bytes of common RAM, less than a tenth of what a standard task requires. It is easy to define a word called BUILD.TINY.TASK that expects a return stack size under a data stack size under a user area size and builds a task without any of the other memory areas (dictionary, heap, TIB, POCKET, or PAD). It is also easy to use BUILD.TASK to set up tasks that use more than 1/4K for data or return stacks, or that can compile into private dictionaries. BUILD.TASK gives you complete flexibility in determining a task's memory resources.

 

Shared Resources

Why Access to Shared Resources Must Be Carefully Managed

The multitasking executive must manage each shared resource so that the state of the resource is not corrupted by uncontrolled access from multiple tasks. For example, suppose that two tasks need to share the LCD display. One task might display standard messages, and the other task might need to display special alerts or error messages. Of course we don't want part of an error message interspersed with parts of a standard message; we want one task or the other, but not both, to write to the display at any given time. This can be accomplished by:

  1. Defining a "resource variable" that mediates access to the resource (in this case the LCD display) by holding a code that identifies which task has control of the resource at any given time;
  2. requiring that, before using the resource, a task must request and GET permission to use it; and
  3. prohibiting other tasks from using the resource until the requesting task has RELEASEd it.

The device driver routines that are built into the QED-Forth kernel automatically call resource management routines (named GET and RELEASE) associated with pre-defined resource variables as described below. These help to minimize contention for shared I/O devices in a multitasked system.

Let's consider another example in which a resource variable is required. Suppose that two distinct tasks use port PPA on the peripheral interface adapter. Task#1 might need to read and modify the lower 4 bits of the port, and Task#2 might need to control the upper 4 bits. We do not want either task to inadvertently modify the bits that are controlled by the other task. If we aren't careful, the following scenario could occur:

Assume that all the bits of PPA are initially 0. Task#1 wants to set the lowest order bit to a 1. It executes the following code to accomplish this:

PPA C@
1 OR
PPA C!

This code implements a "read/modify/write" operation and seems to change only the lowest bit in the port. But let's assume that the timeslicer causes a switch to Task#2 just after the C@ command (which returns a 0 on the stack), and that Task#2 sets the highest order bit in PPA to a 1 by writing to the port as

HEX
PPA C@
80 OR
PPA C!

But Task#1 doesn't know this; it has read the port with the C@ command and thinks that it should restore the top 4 bits as all zeros. Now when control returns to Task#1, it will store the value 1 into PPA which clears the top bit in the port. This is incorrect behavior; Task#1 has modified the state of the highest order bit in PPA which is supposedly controlled only by Task#2!

Defining a resource variable to mediate access to PPA would solve this problem. As explained in detail below, Task#1 could GET the resource, and when Task#2 subsequently tried to GET the same resource it would be forced to wait (PAUSE) until Task#1 RELEASEs control. QED-Forth provides a defining word called RESOURCE.VARIABLE: that creates resource variables, and three utility words (GET, ?GET, and RELEASE) to manage the appropriation and release of the resources.

Another method of solving the port contention problem is presented at the end of this chapter; it involves using "uninterruptable operators" to access the port so that no task switch can occur in the middle of a read/modify/write operation.

 

Resource Variables

The programmer should associate each shared resource with a resource variable. A resource variable is a 32-bit variable that contains 0\0 if the resource is available, and contains the task identifier (i.e., the base address of the task's user area) if it is owned by a task. The following resource variables are pre-defined in the QED-Forth kernel :

Resource Variable Name Controls Access To:
A/D8.RESOURCE 8 bit A/D converter
DISK.RESOURCE Mass memory (blocks) interface
KEYPAD/DISPLAY.RESOURCE Keypad and LCD display
SERIAL Synonym for SERIAL1.RESOURCE
SERIAL1.RESOURCE Primary serial port (68HC11 UART)
SERIAL2.RESOURCE Secondary serial port (software UART)
SPI.RESOURCE SPI, controls 12 bit A/D and 8 bit D/A

The pre-defined I/O drivers in the QED-Forth kernel GET and RELEASE the appropriate resource variables to ensure proper operation in multitasked environments. Consult the "Device Drivers" section of the Categorized Word List in the QED-Forth Glossary for a complete list of the built-in I/O driver routines.

To define a new resource variable, say one that controls access to the port PPA on the peripheral interface adapter, use the defining word RESOURCE.VARIABLE: and then initialize the resource to 0\0 as

RESOURCE.VARIABLE: PPA.RESOURCE
0\0 PPA.RESOURCE X!

RESOURCE.VARIABLE: is a synonym for XVARIABLE. It creates a 32-bit variable. It is very important to explicitly initialize the resource variable to contain 0\0 as part of the application program's initialization after each startup. After the first initialization of the resource variable to 0\0, only special operators GET ?GET and RELEASE should be used to modify its contents; the standard X@ and X! commands should not be used.

A resource is available if its associated resource variable contains the value 0\0. A task can only use a resource if the resource variable contains either 0\0 or the task id (i.e., the STATUS xaddress) of the requesting task. If the status xaddress of another task is in the resource variable, then the resource cannot be used. When a task is finished using a resource, it executes RELEASE to clear the resource variable so that other tasks may use the resource.

 

Getting and Releasing a Resource

If a task executes the command

PPA.RESOURCE GET

it claims control of the port PPA resource if it is available. The GET routine stores the task id (equal to the STATUS xaddress) into PPA.RESOURCE if it is available. If the resource is not available (i.e., PPA.RESOURCE is nonzero and does not contain the requesting task's id), GET enters a loop executing PAUSE until the resource is available. This allows other tasks to operate while the requesting task is waiting for the resource to be released. After a task is done using a resource (in this example, after port PPA has been modified), it executes

PPA.RESOURCE RELEASE

which erases the contents of the resource variable so that other tasks can claim it. A task can only RELEASE a resource that it has control over; unless the current task's id is stored in the resource variable, RELEASE does nothing.

Returning to the example in which Task#1 and Task#2 are accessing port PPA, we can now formulate code that would prevent corruption of the port's contents during multitasking. Assuming that PPA.RESOURCE has been defined and initialized as shown above, Task#1 should execute the following code to set the least significant bit of PPA:

PPA.RESOURCE GET
PPA C@
1 OR
PPA C!
PPA.RESOURCE RELEASE

and Task#2 should execute the following code to set the most significant bit in PPA:

HEX
PPA.RESOURCE GET
PPA C@
80 OR
PPA C!
PPA.RESOURCE RELEASE

If Task#1 GETs the resource first, Task#2 is prevented from accessing PPA (that is, it enters a PAUSE loop) until Task#1 RELEASEs the resource. This ensures that the accesses to the port do not improperly interact with one another.

The word ?GET is similar to GET, except that it does not pause if the resource is not available. This is convenient if a task prefers to continue processing rather than entering a PAUSE loop to wait for an un-available resource. For example, if another task has control of port PPA, executing

PPA.RESOURCE ?GET ( -- flag )

leaves a false flag on the stack to indicate that the resource was not claimed. If the resource is available, ?GET claims the resource by storing the calling task's id into the resource variable, and places a true flag on the stack to indicate that the resource was successfully claimed.

Multitasking application programs should use X! or ERASE to ensure that each defined resource variable is initialized to 0\0 each time the system starts up. Failure to perform the one-time initialization could leave "garbage" in the resource variable that precludes any task from using the associated resource. The pre-defined resource variables listed above are automatically cleared to 0\0 by every warm or cold restart.

After a program first initializes a resource variable to zero, the words GET ?GET and RELEASE are the only routines that should be used to operate on them. These words are carefully written to ensure that only one task at a time controls a resource. As specified in their glossary entries, they disable interrupts for short periods while the resource variable contents are being verified so that interrupting tasks do not corrupt the verification process.

 

Access to the Serial Ports

The QED Board has two serial communications ports. The primary serial port is implemented by the 68HC11's built-in serial communications interface, and the secondary port is implemented by QED-Forth software routines that use pin 3 of PORTA as the serial2 input and pin4 of PORTA as the serial2 output. The QED-Forth interpreter can communicate using either serial port by appropriately revectoring the three fundamental serial routines called KEY, ?KEY, and EMIT.

The resource variables SERIAL1.RESOURCE and SERIAL2.RESOURCE are defined in the kernel to manage access to the primary and secondary serial I/O ports, respectively. A user variable called SERIAL.ACCESS determines when the serial routines GET and RELEASE the serial resource variables.

If SERIAL.ACCESS contains the constant RELEASE.ALWAYS, then KEY and EMIT and ?KEY always GET the serial resource in use before accessing the port, and RELEASE the serial resource after accessing it. If SERIAL.ACCESS contains the constant RELEASE.NEVER, then KEY and EMIT and ?KEY always GET the serial resource in use before accessing the port, but they do not RELEASE the serial resource. This allows a task (such as the QED-Forth interpreter) to seize and retain control of a serial port.

After a COLD restart the user variable SERIAL.ACCESS contains the constant RELEASE.AFTER.LINE. In this case KEY, EMIT and ?KEY do not GET or RELEASE the serial resource. Rather, the QED-Forth interpreter GETs the serial resource before each line is interpreted, and RELEASEs the serial resource after each line is interpreted. This eliminates the considerable overhead involved in executing GET and RELEASE as each character is received and echoed by the QED-Forth interpreter during program development. To reliably attain the maximum baud rate during downloads to the QED Board, SERIAL.ACCESS should contain RELEASE.AFTER.LINE.

To see the effect of this scheme, let's define a task that beeps as

HEX 9A00 0 TASK: BEEPER

and define an action program that calls the kernel word BEEP as

: BEEP.LOOP ( %%--%% )
BEGIN BEEP
AGAIN
;

The kernel word BEEP puts the ascii code for the bell character on the stack and calls EMIT. As discussed at the beginning of the chapter, EMIT executes PAUSE as it waits for the serial port to become available while the prior character is being transmitted. This means that we don't have to explicitly code a call to PAUSE into the loop.

Before building the task, execute

RELEASE.ALWAYS SERIAL.ACCESS !

so that the new task will inherit the correct value of SERIAL.ACCESS. We must ensure that the beeper task always releases control of the serial port after sending the bell character. Now build the task as

0\0 0\0 0\0 BEEPER BUILD.STANDARD.TASK

and activate it by executing

CFA.FOR BEEP.LOOP BEEPER ACTIVATE

You notice that after each character is typed and echoed, the terminal beeps. To cut down on the amount of beeping, execute

RELEASE.AFTER.LINE SERIAL.ACCESS !

which modifies the value of SERIAL.ACCESS in the QED-Forth task's user area so that control of the serial line is released only after each line. Now the beep occurs only once per line. To silence the beep completely, execute

RELEASE.NEVER SERIAL.ACCESS !

which causes the QED-Forth task to maintain control of the serial line so that BEEPER never gets to EMIT the bell character. The BEEPER task is still being called, but it is stuck in a PAUSE loop inside the command GET which is waiting for the serial resource variable to be released by the QED-Forth task.

The SERIAL.ACCESS variable is included in the user area to give you maximum flexibility in designing multiple tasks that share the serial I/O line.

 

Mailboxes

Mailboxes are the medium by which messages are passed between tasks for data exchange or task synchronization. For example, suppose that a data gathering task that produces data must pass the data to a calculation task that consumes the data. This data is passed by placing it in a mailbox and using the commands SEND and RECEIVE. If the calculation task consumes data faster than the gatherer produces it, the tasks need to be synchronized. Using the SEND and RECEIVE operators achieves synchronization by making the calculation task wait for new data before it operates. The data producer puts the data in a mailbox and SENDs it to the data consumer. The consumer calls RECEIVE to check whether there is fresh data in the mailbox; if not, RECEIVE calls PAUSE to allow other tasks to execute while the consuming task is waiting for the data.

 

Defining a Mailbox

The creation and use of mailboxes parallels the creation and use of resource variables described above. To create and initialize a mailbox called DATA.BUFFER, execute

MAILBOX: DATA.BUFFER
0\0 DATA.BUFFER X!

MAILBOX: is a synonym for XVARIABLE. The preceding commands create and zero a 32 bit variable called DATA.BUFFER. It is important to explicitly initialize the resource variable to contain 0\0 as part of the application program's initialization after each startup. After initializing the mailbox, only the routines

SEND ?SEND RECEIVE and ?RECEIVE

should be used to modify the contents of a mailbox. These operators are carefully designed; they disable interrupts for short periods of time (as specified in their glossary entries) to ensure that task switches do not cause data in a mailbox to be written over before it is received.

 

Sending and Receiving Mail

To write the quantity 7000\2 to the mailbox, a task executes

7000 2 DATA.BUFFER SEND

If the contents of DATA.BUFFER are non-zero when this command is executed, the task PAUSEs until the mailbox is cleared (i.e., read by a destination task).

To receive the message, the destination task executes

DATA.BUFFER RECEIVE ( -- 7000\2 )

If the contents of DATA.BUFFER are zero when this command is executed, the destination task enters a PAUSE loop until the mail arrives. Thus mail can be used to ensure that a producer of messages and a consumer of messages become synchronized. The contents of the mail may be a 32-bit address which points to a block of data, thus giving a mechanism for sharing large amounts of data.

The words ?SEND and ?RECEIVE may be used to attempt to send or receive mail. If unsuccessful, they do not PAUSE to pass control to other tasks. Rather, they pass a failure flag to the calling task so that it can do something else instead of waiting for the mail.

Multitasking application programs should use a 0\0 <mailbox name> X! command to ensure that each defined mailbox is initialized to 0\0 each time the system starts up. Failure to perform the 1-time initialization could leave "garbage" in the mailbox that prevents any task from writing valid data into the mailbox.

Resource variables and mailboxes allow the programmer to carefully define the manner in which different tasks interact and share resources and information. Proper use of these tools leads to reliable modular multitasked applications.

 

Uninterruptable Memory Operations

Earlier in this chapter we presented an example in which two tasks interfered with one another as they tried to access different bits in a shared I/O port (PPA). The basic problem was that the timeslicer caused a task switch in the middle of a read/modify/write process. Task#1 read the port, but before it could complete the modification and write to the port, it was interrupted by the timeslicer. This gave Task#2 a chance to modify the port, and when control returned to Task#1, the modification made by Task#2 was undone. We solved the problem by defining a resource variable called PPA.RESOURCE and using GET and RELEASE so that only one task at a time could access the port.

Another way to solve this problem is to use uninterruptable operations to modify the contents of a memory location. If you need to modify bits in a single byte, QED-Forth provides convenient operators that disable interrupts during the read/modify/write operation so that no task switch or interrupt service routine can corrupt the operation. The following uninterruptable operators modify the contents of specified bits in a specified byte:

Uninterruptable Byte Operations Page-less Variations
CHANGE.BITS (CHANGE.BITS)
CLEAR.BITS (CLEAR.BITS)
SET.BITS (SET.BITS)
TOGGLE.BITS (TOGGLE.BITS)

These routines disable interrupts just before reading the memory contents and restore the prior state of the interrupt flag (enabled or disabled) after writing to the specified address. Interrupts remain disabled for only 10 to 16 cycles, corresponding to 5 to 8 microseconds if the crystal frequency is 8 MHz. Consult the glossary for detailed descriptions of these operators.

Returning to our example, Task#1 can set the least significant bit in port PPA by executing

01 PPA SET.BITS

Because SET.BITS is uninterruptable, we can be sure that there will not be a task switch during the read/modify/write operation. Likewise, Task#2 can set the most significant bit in PPA by executing

HEX 80 PPA SET.BITS

These instructions work properly in a multitasked system.

Similar problems can arise when one task writes to a floating point or other 4-byte variable, and a second task needs to read the saved value. The data that is read may be invalid if the read or the write is interrupted between the time of the writing/reading of the first 16 bits and the writing/reading of the second 16 bits. The SEND and RECEIVE mailbox operators described earlier are one solution to this problem. Another solution is to use one of QED-Forth's uninterruptable 32-bit operators denoted by the | ("bar") character are in the kernel. The uninterruptable storage operators are

|2!| |F!| |X!|

and the uninterruptable fetch operators are

|2@| |F@| |X@|

The assembly instructions for 8 bit and 16 bit read and write operations are themselves uninterruptable. Thus, with the unlikely exception a 16 bit variable that straddles a page boundary, the C@, C!, @, and ! operators are already uninterruptable and can be used to share data among tasks.

</box>

 
This page is about: Forth Language Multitasking Operating System – QED Forth is multitasking language and operating system. It includes multitasking executive that can concurrently execute number of tasks. In actuality, processor only executes one instruction at time. However multitasker allows processor to rapidly …
 
 
Navigation