I've cleaned up Fleet Forth's multitasker source. Fleet Forth has a round robin cooperative multitasker, although another type could be written. It only has support for the one main task with background tasks. Each task uses a portion of page one for its return stack and a portion of the area from address $02 to address $7E in page zero for its data stack. A task is a CREATE word, like a variable but with a larger parameter field. A task's user area is in its parameter field. A task's parameter field may also have room for a task to have its own PAD .
In this post, any mention of a task's return stack refers to the portion of the return stack used by the task. The same goes for mention of a task's data stack.
Here is the multitasking lexicon:
Code: Select all
(PAUSE) ( -- )
ENTRY ( -- ADR )
READY ( -- ADR )
TOS ( -- ADR )
MULTI ( -- )
SINGLE ( -- )
WAKE ( TADR -- )
SLEEP ( TADR -- )
LOCAL ( TADR ADR1 -- ADR2 )
( ADR1 TADR -- ADR2 )
STOP ( -- )
LINK-TASK ( TASK -- )
UNLINK-ALL ( -- )
UNLINK-TASK ( TASK -- )
TASK ( U SP0 RP0 -- )
ACTIVATE ( TASK -- )
(PAUSE) is the task switcher. Here is the source:
Code: Select all
CODE (PAUSE) ( -- )
IP 1+ LDA PHA // IP ON RET
IP LDA PHA //
TXA PHA // SP ON RET
TSX TXA // RP STORED
4 # LDY UP )Y STA // IN TOS
BEGIN
0 # LDY
UP )Y LDA TAX INY
UP )Y LDA UP 1+ STA INY
UP STX UP )Y LDA
0= NOT UNTIL
4 # LDY UP )Y LDA TAX TXS
PLA TAX
' EXIT @ JMP END-CODE
The next three words, ENTRY , READY , and TOS are user variables with offsets 0, 2, and 4 respectively. These offsets are reserved in the Fleet Forth kernel for multitasking.
ENTRY points to the beginning, or entry, of the user area for the next task. READY has the logic value true if the task is "awake" and ready to go or false if it is "asleep", although (PAUSE) only checks the low byte. TOS is used to store a task's return stack pointer.
(PAUSE) saves the value of IP to the task's return stack as well as the value of the current task's data stack pointer. The return stack pointer is saved in the user variable TOS . (PAUSE) then follows ENTRY to the next task, testing READY until it finds a task which is awake. Once a task which is awake is found, its return stack pointer is fetched from TOS and stored in the S register and the data stack pointer is restored from the task's return stack. A jump to EXIT fetches the saved value of IP from the task's return stack and returns to NEXT to resume execution of the Forth thread for this task.
Code: Select all
0 USER ENTRY 2 USER READY
4 USER TOS
: MULTI ( -- )
READY ON
['] (PAUSE) IS PAUSE ;
: WAKE ( TADR -- ) 2+ ON ;
: SLEEP ( TADR -- ) 2+ OFF ;
: LOCAL ( BASE ADR1 -- ADR2 )
[ ASSEMBLER ] UP [ FORTH ]
@ - + ;
: STOP ( -- )
BEGIN
READY OFF PAUSE
AGAIN ; -2 ALLOT
MULTI assures the task executing it is "awake" (normally the main task) and sets the deferred word PAUSE to (PAUSE) . The counterpart of MULTI is SINGLE . It is the only word in the multitasker lexicon defined in Fleet Forth's kernel. SINGLE sets PAUSE to NOOP a no operation word. NOOP is a word without a parameter field. Its code field points to NEXT .
WAKE takes a task's address and wakes it up.
SLEEP takes a task's address and puts it to sleep.
LOCAL takes a task's address and the address returned by a user variable in the currently executing task (usually main) and returns the address of that user variable in the given task. For example, suppose there is a task named DUMMY1 . I can tell if DUMMY1 is awake or not with the phrase:
Code: Select all
DUMMY1 READY LOCAL ?
where ? displays the contents of an address. LOCAL has two stack comments because it doesn't matter whether it is used with the user variable address on top or the task address, it returns the same result.
STOP is used for a one-shot task, one which does not have an infinite loop. STOP puts the task to sleep then pauses. STOP has an infinite loop in the event that a stopped task is woken up inadvertently. The task will put itself back to sleep and pause the next time it runs.
Linking and unlinking:
Code: Select all
: LINK-TASK ( TASK -- )
DUP SLEEP
ENTRY 2DUP @ SWAP! ! ;
: UNLINK-ALL ( -- )
[ ASSEMBLER UP @ ] LITERAL
DUP UP ! ENTRY ! ;
: UNLINK-TASK ( TASK -- )
[ ASSEMBLER ] UP [ FORTH ]
BEGIN
@ DUP @
[ ASSEMBLER ] UP [ FORTH ] @ =
IF 2DROP EXIT THEN
DUP @ 2PICK =
UNTIL
SWAP @ SWAP! ;
LINK-TASK takes a task's address and puts it to sleep because a newly created task does not have anything to do and would "crash and burn" if awake. It then links the task into the round robin list of tasks. It is important that a task does not get linked into the round robin list more than once!
UNLINK-ALL , as its name implies, unlinks all the tasks by setting UP , Forth's user pointer, to the main task and setting ENTRY for the main task to point to itself. The ability to unlink all the tasks makes it safe to forget a task. I've read somewhere that once a task is created, it should not be forgotten. It seems to me that this would make prototyping harder with the need to leave the Forth system and reload it to remove background tasks. To provide a little more protection, FORGET switches off multitasking. If the need to forget a Forth word arises, one or more tasks may be forgotten in the process. It would be prudent to unlink all tasks before switching multitasking back on and link them back into the round robin list one at a time. A cold or warm start will unlink all tasks as a side effect of setting up the user area and user pointer. ABORT or any word which executes ABORT switches multitasking off.
UNLINK-TASK takes the address of a task and will unlink just that task from the round robin list of tasks. If the address on the stack is not the address of a task linked in the round robin list, UNLINK-TASK will, after traversing the list, exit without changing anything.
Task creation and activation:
Code: Select all
: TASK ( U SP0 RP0 -- )
CREATE
HERE RP0 LOCAL !
HERE SP0 LOCAL !
// OPTIONAL
10 HERE BASE LOCAL !
HERE #USER + HERE DP LOCAL !
10 UMAX ALLOT ;
: ACTIVATE ( TASK -- )
DUP WAKE
R> OVER RP0 LOCAL @ 1- DUP>R !
DUP SP0 LOCAL @ R@ 1- C!
TOS LOCAL R> 2- SWAP! ;
As I said, TASK is a CREATE word, it returns the address of its parameter field. The definition of TASK has been changed. The original definition took three parameters, the size of the user area for the task being created, the size of its portion of data stack, and the size of its portion of return stack. There were some constants and values used to keep track of how much of the stacks were set aside for each task. With the ability to forget tasks, this would necessitate resetting the values used to keep track of the portion of the stacks set aside for the background tasks.
The new version of TASK also takes three parameters. The size of the user area for the task, the address of the base of the data stack for the task being created, and the address of the base of its return stack.
A task's parameter field will have room for at least five user variables, possibly more. The five required user variables are:
ENTRY , READY , TOS , RP0 , and SP0 . RP0 holds the address of the base of the return stack and SP0 holds the address of the base of the data stack. When a stack is created, its value of BASE is set to ten and its DP , dictionary pointer, is set to the address after the latest user variable.
ACTIVATE takes the address of a task and is used in a word to give a task something to do. Here is an example with two tasks. They both flash the border, but at different intervals.
Code: Select all
180 VALUE DELAY1 175 VALUE DELAY2
0 $1C $12F TASK DUMMY1
0 $38 $15F TASK DUMMY2
: FLASH1
DUMMY1 ACTIVATE
BEGIN
$D020 C@ 1+ BORDER
DELAY1 JIFFIES
AGAIN ;
: FLASH2
DUMMY2 ACTIVATE
BEGIN
$D020 C@ 1+ BORDER
DELAY2 JIFFIES
AGAIN ;
Code: Select all
SEE FLASH1
FLASH1
7DB4 7D84 DUMMY1
7DB6 7CB2 ACTIVATE
7DB8 A35 LIT D020
7DBC 1298 C@
7DBE 11BF 1+
7DC0 455F BORDER
7DC2 7D66 DELAY1
7DC4 42A6 JIFFIES
7DC6 BD0 BRANCH 7DB8
16
OK
FLASH1 is executed from the main task. ACTIVATE wakes the task DUMMY1 . It then places the address $7DB8 on the task's return stack. The data stack pointer for DUMMY1 is then placed on its return stack. The adjusted return pointer for DUMMY1 is stored in its version of TOS . DUMMY1 is now ready for the Forth thread at address $7DB8 to run.