Joined: Fri May 05, 2017 9:27 pm Posts: 895
|
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: (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: 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: 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: 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: : 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: : 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: 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: 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.
|
|