Link here

Multitasking in Forth

How to use the multitasking real time operating system from a Forth application program.

QED-Forth is a multitasking language and operating system that is ideal for instrument control and automation applications. 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 include 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, with 1 millisecond as the default on PDQ controllers.

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.

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 via “mailboxes” 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 RTI (real-time interrupt) with a default 1.024 millisecond period. In this discussion we’ll round this to 1 millisecond. 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 millisecond 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 millisecond.

To turn off the timeslicer, execute

STOP.TIMESLICER↓

This disables the RTI 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 (1.024 msec default). 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. Even though the timeslice clock has a period that is a multiple of 1.024 msec, this routine mathematically compensates for the non-integer period, reducing the reported error to 1 part in 5000, equivalent to reporting 17 too few seconds per day. The maximum time that the clock can represent is proportional to the timeslice period; the clock can represent times to over 49 days if the default 1 msec timeslice period is used.

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. This routine also compensates for the non-integer timeslice period to report accurate times. 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 maximum time that the clock can represent is proportional to the timeslice period; the clock can represent times to over 49 days if the default 1 msec timeslice period is used.

 

Changing the timeslice period

To change the timeslice period, execute the command MSEC.TIMESLICE.PERIOD. For example, to switch tasks every 5 msec, execute

5 MSEC.TIMESLICE.PERIOD↓

The actual period of the timeslice clock is set in units of 1.024 milliseconds; in this case the actual timeslice period is 5 * 1.024 = 5.12 milliseconds. With this period, the elapsed-time clock can run for over 245 days before rolling over to zero. The maximum valid parameter that can be passed to is decimal 15.

 

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.

A multitasking example 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↓
ALLOCATE.TASK: MY.TASK      ok↓

The ALLOCATE.TASK: function VALLOTs a 1 KByte task area in the common RAM, and creates the daughter function MY.TASK that, when executed, leaves the base xaddress of the task area on the data stack.

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 ] \ 1200 \ 0
SP!↓   ok

tells us that the user area for the task that is running the QED-Forth interpreter starts at address 0x1200 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 starting at the STATUS xaddress (which is simply another name for the base of the task area). The memory map is as follows:

Table 16-2 Standard Task Area Memory Allocation.
Memory area Start address Size (decimal bytes)
User Area STATUS + 0x000 256
PAD STATUS + 0x124 +82/-36
TIB STATUS + 0x180 94
POCKET STATUS + 0x1E0 64
Data Stack STATUS + 0x300 -256
Return Stack STATUS + 0x400 -256

FIXME Include the reint area and any other task space.

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 so that the standard task does not have compilation privileges. The programmer specifies where the heap and variable areas 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 given the default xaddress 0\0. 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 in this example 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 we execute

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

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 the MY.TASK 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. For this example, the following simple action program increments the value of a variable named COUNTER.

VARIABLE COUNTER
: 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↓

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

If all of the Forth commands in this “Setting Up a New Task” section have been typed in, there are 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 ! ↓

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 (and typically bad) results will occur if a task KILLs itself!

 

Multitasker 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. It then 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 HCS12’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 instruction 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 real-time interrupt (RTI) automatically stacks the registers (while PAUSE has to execute assembly commands to explicitly stack the registers). The RTI 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 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.

 

Customizing a task’s memory map

The command BUILD.STANDARD.TASK allows you to set up a new task with a fairly simple command; the resulting task area is detailed in Table 16 2. 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. If you want to take this approach, use the task creation word TASK: instead of the more powerful ALLOCATE.TASK: routine. TASK: does not allocate space in the variable area, thus allowing you to allocate the amount of space required by your custom BUILD.TASK routine.

 

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 a Liquid Crystal Display (LCD). 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) 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 PORTM on the Freescale HCS12 (9S12) microcontroller. 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 PORTM are initially 0. Task#1 wants to set the lowest order bit to a 1. It executes the following code to accomplish this:

PORTM C@
1 OR
PORTM 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 PORTM to a 1 by writing to the port as

HEX
PORTM C@
80 OR
PORTM 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 PORTM which clears the top bit in the port. This is incorrect behavior; Task#1 has modified the state of the highest order bit in PORTM which is supposedly controlled only by Task#2!

Defining a resource variable to mediate access to PORTM 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 :

SERIAL            \ Synonym for SERIAL1.RESOURCE
SERIAL1.RESOURCE  \ For primary serial port (HCS12 UART0)
SERIAL2.RESOURCE  \ For secondary serial port (HCS12 UART1)
SPI.RESOURCE      \ For SPI0 channel (goes to Wildcard Bus, real-time clock)

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” and “Serial I/O” sections 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 PORTM on the HCS12 processor, use the defining word RESOURCE.VARIABLE: and then initialize the resource to 0\0 as

RESOURCE.VARIABLE:  PORTM.RESOURCE↓
0\0 PORTM.RESOURCE  X!↓

RESOURCE.VARIABLE: creates a 32-bit variable in common RAM. 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

PORTM.RESOURCE  GET↓

it claims control of the port PORTM resource if it is available. The GET routine stores the task id (equal to the STATUS xaddress) into PORTM.RESOURCE if it is available. If the resource is not available (i.e., PORTM.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 PORTM has been modified), it executes

PORTM.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 PORTM, we can now formulate code that would prevent corruption of the port’s contents during multitasking. Assuming that PORTM.RESOURCE has been defined and initialized as shown above, Task#1 should execute the following code to set the least significant bit of PORTM:

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

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

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

If Task#1 GETs the resource first, Task#2 is prevented from accessing PORTM (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 unavailable resource. For example, if another task has control of port PORTM, executing

PORTM.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 PDQ Single Board Computer (SBC) has two asynchronous serial communications ports called Serial1 and Serial2. Both are implemented by hardware UARTs (Universal Asynchronous Receiver/Transmitters) built into the HCS12 processor. Both are initialized to run at a default baud rate of 115,200 baud, and Serial1 is the default serial port used by QED-Forth after a factory cleanup. 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 default constant RELEASE.AFTER.LINE. In this case KEY and 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 PDQ Board, SERIAL.ACCESS should contain RELEASE.AFTER.LINE.

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

ALLOCATE.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↓

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: creates a 32 bit variable in common RAM. The commands shown above create and zero a mailbox named 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 following routines

SEND   ?SEND   RECEIVE  ?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 9000\2 to the mailbox, a task executes

9000 2 DATA.BUFFER SEND↓

If the contents of the DATA.BUFFER mailbox 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  ( -- 9000\2 )

which leaves 9000\2 on the data stack. 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 0\0 X! or ERASE to ensure that each defined mailbox is initialized to 0\0 each time the system starts up. Failure to perform the startup 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 (PORTM). 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 PORTM.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:

CHANGE.BITS    CLEAR.BITS    SET.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 under 13 cycles, corresponding to less than 2/3 of a microsecond. Consult the Forth glossary for detailed descriptions of these operators.

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

01 PORTM 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 PORTM by executing

HEX 80 PORTM SET.BITS↓

These 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, but they do not allow 0\0 (a 32-bit zero) as a valid data value. 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.



See also → A Turnkeyed Forth Application Program

 
This page is about: Multitasking in Forth Language, Cooperative and Timesliced Task Switching in Forth – How to use the multitasking real time operating system from a Forth application program: using the elapsed time clock, building and activating a task, understanding the task memory map, and using resource variables and mailboxes to control access to shared resources.
 
 
Navigation