6502.org Forum  Projects  Code  Documents  Tools  Forum
It is currently Wed Jun 05, 2024 9:11 pm

All times are UTC




Post new topic Reply to topic  [ 343 posts ]  Go to page Previous  1 ... 10, 11, 12, 13, 14, 15, 16 ... 23  Next
Author Message
PostPosted: Thu Jun 03, 2021 2:40 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 865

I've recently removed the virtual memory words from Fleet Forth's metacompiler's source, which it uses to build the target kernel, and placed them in the system loader but may move them to the optional utilities.
Since the kernel is less than 10k, Fleet Forth's virtual memory words are limited to 64k. Since a Ram Expansion Unit with 256 banks, the maximum possible with a C64 REU, can have 16 megabytes, I mention some ways around this limitation later in this post.
Fleet Forth uses blocks in the REU for virtual memory. Blocks in the REU start at block number 32768, or $8000. Fleet Forth includes a convenience word, RAM , which takes a number from the stack and adds $8000.
SPLIT , defined in the kernel, splits a number into its low and high bytes with the high byte on top.
JOIN takes a low byte and high byte and joins them into a sixteen byte number.
U/BBUF ( U divided by B/BUF ) divides an address by B/BUF and returns an offset into a block and a block number.
>VIRTUAL takes a virtual address and returns an address in a block buffer.
VC@ and VC! fetch and store a byte respectively in virtual memory.
V@ and V! are the virtual memory counterparts of @ and ! built on VC@ and VC! .
VDUMP takes an address and count. It does the same thing as DUMP , dump the contents of memory to the screen, or other output device, but it dumps the contents of virtual memory.
VMSAVE ( virtual memory save ) saves a section of virtual memory to a file on a Commodore 64 disk drive. VMSAVE takes the starting address, the address just past the end of the section of memory to be saved, the address of a name, and the length of the name. The name must end with ",P,W" to write the section of memory as a Commodore 64 program file.
There are a few display formatting words. 8BITS and 16BITS return the widths of the string representation of the largest 8 bit number and 16 bit number respectively. SETWIDTH sets these words according to the current number base.
Code:
CODE JOIN  ( LO HI -- U )
   0 ,X  LDA  3 ,X STA
   POP JMP  END-CODE
CODE U/BBUF  ( U -- OFFSET BLK# )
   1 ,X LDA  TAY
   3 # AND  1 ,X STA
   TYA  .A LSR  .A LSR
   APUSH JMP  END-CODE
: >VIRTUAL  ( VADR -- ADR )
   U/BBUF RAM BLOCK + ;
: VC@  ( VADR -- B )
   >VIRTUAL C@ ;
: VC!  ( B VADR -- )
   >VIRTUAL C! UPDATE ;
: V@  ( VADR -- N )
   DUP VC@  SWAP 1+ VC@  JOIN ;
: V!  ( N VADR -- )
   SWAP SPLIT
   ROT TUCK 1+ VC! VC! ;
: CMOVE>V  ( ADR VADR CNT -- )
   BOUNDS
   ?DO  COUNT I VC!  LOOP
   DROP ;
: (VDUMP)  ( ADR -- )  8 BOUNDS
   DO  I VC@ 8BITS .R SPACE  LOOP ;
: VDUMP  ( ADR CNT -- )
   SETWIDTH BOUNDS
   DO
      CR I DUP 16BITS U.R 2 SPACES
      DUP (VDUMP) SPACE
      DUP 8 + (VDUMP)
      2 SPACES 16 BOUNDS
      DO
         I VC@ QEMIT
      LOOP
      DONE? ?LEAVE
   16 +LOOP
   CR ;

The code for VMSAVE is somewhat convoluted just prior to and within the DO LOOP . When writing virtual memory to disk, I was trying to avoid opening and closing a channel for each byte written and it got a little messy. I plan to test a new version of VMSAVE to see how opening and closing the channel for each byte affects the time to save memory.
Code:
: VMSAVE  ( AS AE+1 ANAME CT -- )
   1 DR# 1 OPEN IOERR ?DISK
   1 CHKOUT IOERR
   SWAP DUP SPLIT SWAP
   (EMIT) (EMIT) CLRCHN
   DUP VC@ DROP 1 CHKOUT IOERR
   ?DO  I B/BUF MOD 0=
      IF  CLRCHN  THEN
      I VC@  I B/BUF MOD 0=
      IF  1 CHKOUT IOERR  THEN
      (EMIT)
   LOOP
   CLRCHN  1 CLOSE ;

These are the only general purpose virtual memory words needed by the metacompiler.
For completeness, here is CMOVEV> to move memory from virtual memory to the C64's memory.
Code:
: CMOVEV>  ( VADR ADR CNT -- )
   0
   DO
      OVER I + VC@  OVER I + C!
   LOOP
   2DROP ;

The metacompiler adds a few more virtual memory words specifically related to building a system in virtual memory. Here are just a few.
TDP target dictionary pointer.
THERE target HERE .
VALLOT virtual ALLOT .
VC, and V, the virtual memory counterparts of C, and , .
SALLOT string (at HERE ) allot. Takes size n on the stack and copies a string from HERE to THERE and allots n bytes of virtual memory.

Three possibilities come to mind to store more data than 64k in virtual memory.
64k at a time could be stored and the block moving words could be used to move those 64 blocks to higher numbered blocks in virtual memory. This is feasible because Fleet Forth has the word BMOVE to move blocks. The first 64 blocks in the REU could be copied to the next 64 with the following:
Code:
0 RAM 64 RAM 64 BMOVE

Another approach is modifying >VIRTUAL .
Code:
0 VALUE BANK
: >VIRTUAL  ( VADR -- ADR )
   U/BBUF RAM BANK 6 LSHIFT + BLOCK + ;

The REU can be though of as having a certain number of 64k banks (which is actually the case). The first 64k blocks can be selected for virtual memory with:
Code:
0 TO BANK

the next 64k blocks with:
Code:
1 TO BANK

and so on.

The third approach is to define a set of long virtual memory access words.
Code:
: >LVIRTUAL  ( LADDR -- ADDR )
   B/BUF UM/MOD RAM BLOCK + ;
: VLC@  ( LADDR -- B )
   >LVIRTUAL C@ ;
: VLC!  ( B LADDR -- )
   >LVIRTUAL C!  UPDATE ;
: VL@  ( LADDR -- N )
   2DUP VLC@  -ROT 1 0 D+
   VLC@ JOIN ;
: VL!  ( N LADDR -- )
   ROT SPLIT  2OVER 1 0 D+ VLC!
   -ROT VLC! ;

The LADDR in the stack comments is a double number virtual memory address. Here are the rest:
Code:
: VLCOUNT  ( LADDR -- LADDR+1 B )
   2DUP VLC@ -ROT 1 0 D+ ROT ;
: (VLDUMP)  ( LADDR -- LADDR+8 )
   8 0
   DO
      VLCOUNT 8BITS .R SPACE
   LOOP ;
: VLDUMP  ( LADDR CNT -- )
   SETWIDTH  0
   DO
      CR 2DUP 32BITS UD.R
      2 SPACES  2DUP
      (VLDUMP) SPACE (VLDUMP) 2DROP
      2 SPACES 16 0
      DO
         VLCOUNT QEMIT
      LOOP
      DONE? ?LEAVE
   16 +LOOP
   2DROP CR ;
: CMOVE>LV  ( ADR VLADR CNT -- )
   >R ROT R> 0
   DO
      COUNT 2OVER I 0 D+ VLC!
   LOOP
   2DROP DROP ;
: CMOVELV>  ( VLADR ADR CNT -- )
   0
   DO
      I 2OVER 0 -ROT D+ VLC@
      OVER I + C!
   LOOP
   DROP 2DROP ;

I realize that the REU could be accessed directly, rather than going through the block system. Part of the reason I do this on a simulated Commodore 64 is the nostalgia. Back in the day, not everyone had a Ram Expansion Unit. Some may have had more than one disk drive. Both versions of >VIRTUAL and >LVIRTUAL can be changed to access one of the disk drives for virtual memory by replacing RAM with the phrase
9 DR+ to access drive nine or
10 DR+ to access drive ten.


Top
 Profile  
Reply with quote  
PostPosted: Tue Jun 08, 2021 1:56 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 865
JimBoyd wrote:

The code for VMSAVE is somewhat convoluted just prior to and within the DO LOOP . When writing virtual memory to disk, I was trying to avoid opening and closing a channel for each byte written and it got a little messy. I plan to test a new version of VMSAVE to see how opening and closing the channel for each byte affects the time to save memory.


To deal with the Commodore 64 kernal using the same kernal routine to send a byte to the screen or any I/O device, I/O words in Fleet Forth, such as EMIT , TYPE , and the disk block read write words normally direct output to the desired device, transfer the data, and clear the channel. This version of VMSAVE did things a little differently to save time. I ran a test with a version of VMSAVE that did as other Fleet Forth I/O words. It directed output to the disk, emitted a byte, then cleared the channel. This version took about one and a half times as long to transfer 8k from virtual memory to a program file on one of the disk drives.
Since these listings are for a Commodore 64, just read each double slash // as a backslash \
Code:
// VMSAVE -- REALLY SLOW
: DDEMIT  ( C -- )
   1 CHKOUT ?IO DEMIT CLRCHN ;
: VMSAVE  ( AS AE+1 ANAME CT -- )
   1 DR# 1 OPEN IOERR ?DISK
   SWAP DUP SPLIT SWAP
   DDEMIT DDEMIT
   ?DO
      I VC@  DDEMIT
   LOOP
   1 CLOSE ;

I even used a very fast version of I/O error checking, ?IO .
Code:
// ERROR CODES FROM C64 KERNAL
// ARE ONE BYTE
CODE ?IO  ( EC -- )
   0 ,X LDA
   0= IF
      POP JMP
   THEN
   >FORTH
   IORESET IOERR ;


This next version of VMSAVE uses DTYPE to send up to 1k of data directly from the block buffer to the program file being created, rather than sending one byte at a time.
Code:
// VMSAVE
: VMSAVE  ( AS AE+1 ANAME CT -- )
   1 DR# 1 OPEN IOERR ?DISK
   OVER -
   OVER  SPLIT SWAP
   1 CHKOUT ?IO  DEMIT DEMIT CLRCHN
   BEGIN
      OVER $3FF AND B/BUF SWAP -
      OVER UMIN 2PICK >VIRTUAL OVER
      1 CHKOUT ?IO DTYPE CLRCHN
      /STRING ?DUP 0=
   UNTIL
   DROP 1 CLOSE ;

After opening a data channel to the disk, it converts the starting address and ending address plus one to the starting address and length. Since each kilobyte of virtual memory aligns on a block buffer boundary, the address is ANDed with $3FF and subtracted from B/BUF ( $400 ) . The unsigned minimum of this and the count is the amount to send. /STRING uses this unsigned minimum to adjust the starting address and the count. When the count reaches zero, all of the virtual memory from the starting address to the ending address has been sent.
Although the primary purpose of DTYPE is to send data to one of the disk drives, it does not redirect output. The only difference between DTYPE and TYPE are, DTYPE is a primitive and it does not update CHARS and LINES , the two variables used for output formatting.
In the line with DTYPE , output is directed to the disk drive before DTYPE and the channel cleared after, in keeping with how Fleet Forth I/O works.
This version of VMSAVE takes only forty four percent as much time as the original.

The same technique used by this version of VMSAVE , handling up to 1k at a time and directly accessing this data in the block buffer, could also be used to speed up CMOVE>V and CMOVEV> , as well as their long virtual address counterparts, if these words are used to move large amounts of data. In the metacompiler, they normally move a small amount, such as a name or small string, from HERE to THERE .


Top
 Profile  
Reply with quote  
PostPosted: Tue Jul 06, 2021 2:14 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 865

I've been working with Fleet Forth's multitasker again. One of the difficulties with the multitasker is the word EXPECT . It is a primitive in Fleet Forth, but even if it was high level, it would still present difficulties. EXPECT , actually its vector, (EXPECT) , calls the C64 Kernal routine CHRIN at $FFCF. EXPECT is a deferred word.
Each call to CHRIN returns a single character, but CHRIN handles keyboard input differently than other sources. If the keyboard is the input source, CHRIN echoes the characters received to the screen until the return key is pressed. Once the return key is pressed, each call to CHRIN returns a character by reading it from the current logical line on the screen (the one with the cursor).
The advantage is that this is how the Commodore 64 screen editor works. It is possible to scroll to any line and type return. That line will be read in as if it had just been typed followed by a carriage return.
The disadvantage is that CHRIN will return nothing until the return key is pressed. It will not even return! It will keep running, echoing characters to the screen ( including characters generated by the scroll keys ), waiting for a carriage return.
This means that cooperative round robbin multitasking will not work.
My first work around was to write a multitasker version of (EXPECT) called (MEXPECT) . (MEXPECT) started as a primitive. If the input source was NOT the keyboard then (MEXPECT) jumped to the body of (EXPECT) . If input was from the keyboard, the jump to the body of (EXPECT) was branched over. (MEXPECT) transitioned to high level and performed the function of the screen editor in high level Forth.
This was not an elegant solution. (MEXPECT) was large enough that I didn't want to include it in the kernel as a better version of (EXPECT) . It was switched in by the word MULTI and switched back out by the word SINGLE . (MEXPECT) wasn't even a proper vector for EXPECT since I/O redirection could change which word EXPECT executed.
The multitasker word SINGLE , which is defined in Fleet Forth's kernel so it is always available, had two jobs:
1) set the deferred word PAUSE to NOOP , a no operation word.
2) set the code field of (EXPECT) to point to its own body.
The multitasker word MULTI had three jobs:
1) make sure the task executing MULTI is awake.
2) set the deferred word PAUSE to (PAUSE) , the task switcher.
3) set the code field of (EXPECT) to point to the body of (MEXPECT) . This worked since they both start out as code words.

I have since worked out a better solution: trick CHRIN.
The new version of (MEXPECT) still tests to see if input is from the keyboard and jumps to the body of (EXPECT) if it isn't. If input is from the keyboard, (MEXPECT) prints a carriage return, if necessary, to start a new line. It still echoes the characters to the screen until it receives a carriage return. It then sets location $D3 to zero to indicate the first column. It also sets $D4 to zero to clear quote mode. It then stuffs a carriage return into the keyboard buffer and executes (EXPECT) . CHRIN reads the carriage return and dutifully returns characters from the current logical line on the screen. The logical line could be 40 columns or 80 columns, the C64 Kernal maintains a table for that.
Code:
CODE (MEXPECT)  ( ADR CNT -- )
   $99 LDA
   0= NOT IF
      ' (EXPECT) >BODY JMP
   THEN
   >FORTH
   COLS ?CR
   BEGIN
      PAUSE  KEY DUP 13 <>
   WHILE
      DEMIT
   REPEAT
   $277 C!  1 $C6 C!  $D3 OFF
   (EXPECT) ;

This version of (MEXPECT) is small enough that I am considering making (EXPECT) headerless and including (MEXPECT) in the Fleet Forth kernel as (EXPECT) .
With the other changes to the Fleet Forth kernel to support it, such as simplifying SINGLE , it would only add about 45 bytes to Fleet Forth's kernel.

[Edit: fixed an off by one error in my count]


Top
 Profile  
Reply with quote  
PostPosted: Sun Jul 11, 2021 11:02 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 865

I've made the changes to my latest Fleet Forth kernel. The extra cost of 45 bytes would have been if I had removed the CR in QUIT and replaced the phrase COLS ?CR in the new (EXPECT) with CR .
I left those in place which should have made the new kernel 49 bytes bigger than it was before the new version of (EXPECT) .
Rather than make the old version of (EXPECT) a headerless word in the kernel, I incorporated it into the new version of (EXPECT) . The new Fleet Forth kernel is now only 42 bytes bigger with this new version of (EXPECT) .
Code:
CODE (EXPECT)  ( ADR CNT -- )
   $99 LDA
   0= IF
      >FORTH
      COLS ?CR
      BEGIN
         PAUSE  KEY DUP 13 <>
      WHILE
         DEMIT
      REPEAT
      // STUFF CR IN KBD BUFFER
      // AND SET CURSOR TO COLUMN 0
      $277 C!  1 $C6 C!  $D3 OFF
      >ASSEM
   THEN
   2 # LDA  SETUP JSR
   XSAVE STX  SPAN 1+ STY
   BEGIN
         N CPY
      0= WHILE
         N 1+ DEC
      0< NOT WHILE  CS-ROT
      THEN
         $FFCF JSR  // CHRIN
         13 # CMP
      0= NOT WHILE
         N 2+ )Y STA  INY
      CS-DUP 0= UNTIL
      N 3 + INC  SPAN 1+ INC
   AGAIN
   CS-SWAP
   THEN
      $99 LDA
      0= IF
         BEGIN
            $FFCF JSR  // CHRIN
            13 # CMP
         0= UNTIL
      THEN
   THEN
   SPAN STY
   XSAVE.NEXT JMP  END-CODE
' (EXPECT) IS EXPECT

There is an odd bug in the Commodore 64's CHRIN Kernal routine. The Commodore 64's I/O routines can be used with the keyboard and screen or I/O devices. The default is reading from the keyboard and writing to the screen. When using this routine to read the keyboard (actually, using the C64 screen editor), there is an edge case. Under certain conditions, If less than the number of characters on the logical line are read before writing to the screen, there will be odd cursor behavior. The following section of code in (EXPECT) consumes all characters after the requested amount until a carriage return is received. This eliminates the bug and is only done when using the C64 screen editor (when the input source is the keyboard) so will not affect using (EXPECT) to read from other sources, such as the disk drives.
Code:
      $99 LDA
      0= IF
         BEGIN
            $FFCF JSR  // CHRIN
            13 # CMP
         0= UNTIL
      THEN

With the new version of (EXPECT) , SINGLE is now just:
Code:
: SINGLE  ( -- )
   ['] NOOP IS PAUSE ;

That takes care of the multitasker support built into Fleet Forth's kernel. Now I need to clean up the rest of the multitasker source.


Top
 Profile  
Reply with quote  
PostPosted: Sun Jul 25, 2021 9:11 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 865

I've briefly mentioned Fleet Forth's cold start and warm start routines in previous posts. I'll try to explain them in this post.
When loading and running Fleet Forth, the cold start routine starts with Fleet Forth's one line of BASIC:
Code:
10 SYS XXXXX

where XXXXX is the address of the cold start routine. There is a subroutine used by both the cold start and warm start routines. It is headerless and has no code field since it is a subroutine, but it has the label (WARM) in the Fleet Forth source.
Here is the source for (WARM) :
Code:
HEX
HSUBR (WARM)
   SEI  CLD
   2F # LDA  0 STA
   36 # LDA  1 STA
   USER.DATA    LDA  UP    STA
   USER.DATA 1+ LDA  UP 1+ STA
   6C # LDA  W 1- STA
   'THERE USER.DATA - 1- # LDY
   BEGIN
      USER.DATA ,Y LDA  UP )Y STA
      DEY
   0< UNTIL
   CLI
   >FORTH
   SP! AP! [COMPILE] [
   IORESET SINGLE DECIMAL
   PAGE BOOTCOLORS
   >ASSEM
   RTS
END-CODE

The subroutine (WARM) has a mix of low and high level code.
It first disables maskable interrupts and clears the decimal mode. The memory map for Fleet Forth is set (switching out the BASIC rom) and the opcode for an indirect jump is stored at address W-1.
The label USER.DATA points to an area just after the BASIC fuse but before the first Forth word. This area holds the bootup, or startup, values for the first nine user variables. The first value is the startup value of the first user variable and the value for the Forth virtual machine register UP , the user pointer. It is copied to UP then the startup value for the first eight user variables are copied to the user area.
The first three are unnamed. They are reserved for the multitasker, which is not a part of Fleet Forth's kernel.
The next five are:
RP0 The address of the base of the return stack.
SP0 The address of the base of the data stack.
SPLIM The limit or "full mark" for the data stack. Since the data stack is only checked for overflow (and underflow) by the word ?STACK , which is normally only used by INTERRPET , there are fourteen cells available past this point.
AP0 The address of the base of the auxiliary stack.
APLIM The limit of the auxiliary stack. Since the words to move items to and from the auxiliary stack perform bounds checking on the auxiliary stack, this limit is a "hard" limit.

This subroutine then transitions to high level Forth and executes the following words:
SP! Clears the data stack. This word is a primitive that uses the value from the user variable SP0 to initialize the data stack.
AP! Clears the auxiliary stack using the value in AP0 .
[ Sets the Forth state to interpreting.
IORESET Resets the deferred words used for I/O to their default values.
SINGLE Switches off multitasking by setting PAUSE to a no-op.
DECIMAL Sets the number base to decimal.
PAGE Clears the screen.
BOOTCOLORS Sets the screen colors to their bootup values.
And finally >ASSEM causes a transition back to low level for an RTS back to the caller.

Here is the source for Fleet Forth's word COLD:
Code:
HEX // USER COLD START
: COLD  ( -- )
   SAVE-BUFFERS
   ACLOSE     // CLOSE ALL FILES
   >ASSEM
   // POWERUP COLD START
   // PATCH THE BASIC FUSE
   DECIMAL HERE 0 <# #S #>  HEX
   FUSE  OVER - SWAP CMOVE>V
   SEI  CLD  FF # LDX  TXS
   FF87 JSR  // RAMTAS
   FF8A JSR  // RESTOR
   FFE7 JSR  // CLALL
   FF84 JSR  // IOINIT
   FF81 JSR  // CINT
   WARM  SPLIT SWAP
      # LDA  $300 STA
      # LDA  $301 STA
   (WARM) JSR
   >FORTH
   EMPTY 0 DRIVE CONFIGURE
   #12 SPACES
   [ HERE #18 + >A ]
   ." C64 FLEET FORTH  COPYRIGHT (C) 1995-2021 BY JAMES BOYD "
   [ HERE 1- >A ]
   INITIAL QUIT ; -2 ALLOT
   $D A> C!  $D0D A> !

COLD is used from within Fleet Forth to perform a cold start.
It saves the contents of the block buffers with SAVE-BUFFERS and closes all open files with ACLOSE .
COLD then transitions to a code word and flows into the cold start routine.
This section of the source:
Code:
   DECIMAL HERE 0 <# #S #>  HEX
   FUSE  OVER - SWAP CMOVE>V

patches the one line of BASIC so the SYS instruction points to the cold start routine.
In the cold start routine interrupts are disabled, the processor stack is initialized and some Commodore 64 Kernal routines are called. I performed an experiment by replacing those five JSR instructions with fifteen NOP instructions and saving the modified Forth system. The test system loaded and ran just fine. This is why those five C64 kernal calls are there. Some of the C64 Kernal routines are vectored through page 3 so that the user can replace these routines or add functionality. This added functionality can be in code that is stored in the C64 tape buffer or in a range of RAM from $C000 TO $CFFF (49152 to 53247). Fleet Forth uses both of these memory areas so the C64 Kernal routines are called to reset RAM and reset these vectors to their default values.

$FF87 RAMTAS Clears pages 0 2 and 3 to zeros. It also performs a nondestructive test of memory to set the Kernal bottom and top of memory pointers and sets screen memory to $400.
$FF8A RESTOR As its name implies, restores the default C64 Kernal vectors in page 3.
$FFE7 CLALL Although this routine's name implies that it closes all, it merely sets the count of open files to zero at address $98 and falls through to the C64 kernal routine CLRCHN, which clears the channels and sets the keyboard as the input device and the screen as the output device.
$FF84 IOINIT This routine initializes the Complex Interface Adapter chips, sets CIA #1 to generate an interrupt via timer A every sixtieth of a second, and turns off the volume of the SID chip.
$FF81 CINT Initializes the screen editor and the VIC chip.

The cold start routine then stores the address of the warm start routine at addresses $300-$301 (768-769). The warm start routine will execute when the STOP and RESTORE keys are pressed. The RESTORE key is hardwired to generate a non-maskable interrupt. This is a convenient way to get out of an infinite loop without the need to reset the machine. A BRK instruction executed will also cause a Fleet Forth warm start.

The cold start routine calls (WARM) then transitions to high level Forth and executes the following:
EMPTY Empties the dictionary to its latest empty point. It accomplishes this by fetching the startup value for the ninth user variable with a value in the boot area, DP , the dictionary pointer. EMPTY stores a copy of this value in the variable FENCE before branching into FORGET . FORGET does the "heavy lifting" of removing all vocabularies defined after the "forget point" and pruning the remaining vocabularies.
0 DRIVE Sets the default drive to device 8.
CONFIGURE Initializes the block buffer system with all buffers empty.
The cold start routine prints the startup message before executing the word INITIAL and then QUIT .
INITIAL is a deferred word which is normally set to the no-op NOOP .

Here is the source for Fleet Forth's warm start routine:
Code:
HSUBR WARM
   $FF # LDX  TXS
   (WARM) JSR
   >FORTH
   FORTH DEFINITIONS
   ." RWARM START."
   ABORT ; -2 ALLOT

The processor stack is initialized and the subroutine (WARM) is called. The warm start routine then transitions to high level. It sets the CONTEXT and CURRENT search orders to FORTH , displays the warm start message in reverse video and aborts.

The biggest difference between the cold start and warm start routines for a programmer is this: The warm start routine will not empty the dictionary, alter the default disk drive or empty the block buffers.


Top
 Profile  
Reply with quote  
PostPosted: Sun Sep 19, 2021 8:26 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 865

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.


Top
 Profile  
Reply with quote  
PostPosted: Sun Oct 03, 2021 7:32 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 865

I posted about Fleet Forth's cold and warm start routines here.
There was an error in the cold start routine in the section of C64 Kernal calls which has since been corrected.
Code:
   FF87 JSR  // RAMTAS
   FF8A JSR  // RESTOR
   FFE7 JSR  // CLALL
   FF84 JSR  // IOINIT
   FF81 JSR  // CINT

The Commodore 64 Kernal routine CLALL clears the table of open files, but it also falls through to the CLRCHN routine. This routine restores the default input device (the keyboard) and the default output device (the screen). It will also send an UNTALK or UNLISTEN command if necessary. The problem is that the C64 routine RAMTAS clears the memory locations holding the necessary information.
Here is the corrected list of C64 Kernal calls.
Code:
   FFE7 JSR  // CLALL
   FF87 JSR  // RAMTAS
   FF8A JSR  // RESTOR
   FF84 JSR  // IOINIT
   FF81 JSR  // CINT

I've never had a problem with this because I load and run Fleet Forth immediately after powerup, or in recent years immediately after starting the VICE C64 simulator.


Top
 Profile  
Reply with quote  
PostPosted: Sun Oct 03, 2021 8:47 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 865

Fleet Forth has a word, DIGIT , which is not in the Forth-83 standard, but it is in Fig Forth, F83 and F-PC. DIGIT is a primitive which attempts to convert a character into a valid digit in the specified number base and returns the result and a flag.
In Fig Forth, DIGIT has the following stack effect.
Code:
DIGIT  ( CHAR BASE -- N TRUE )
       ( CHAR BASE -- FALSE )

However, in F83 and F-PC it has this stack effect.
Code:
DIGIT  ( CHAR BASE -- N TRUE )
       ( CHAR BASE -- CHAR FALSE )

I've also seen this stack effect where accessing the value of BASE is built in.
Code:
DIGIT  ( CHAR -- N TRUE )
       ( CHAR -- CHAR FALSE )

Which version would be best for a Forth-83 Standard system such as Fleet Forth?
Of the first two versions, the Fig Forth version has the advantage that CONVERT does not need to discard the unneeded character when a nonconvertible character is encountered, thus saving one cell. On the other hand, the F83 and F-PC version could be more generally useful since it always returns two stack items. Here is an example from Inside F83 by C. H. Ting where the result of the conversion is not needed.
Code:
: DIGIT? ( char -- f )
   BASE @ DIGIT
   NIP ;

The third version is the one I'm leaning toward. The built in access of BASE saves two cells in each word which uses DIGIT , making that word slightly smaller and faster. This change only makes DIGIT two bytes longer comparing the value in the accumulator with the value of BASE
Code:
   ' BASE >BODY C@ # LDY
   UP )Y CMP

versus comparing it with the value on the top of the data stack.
Code:
   0 ,X CMP

DIGIT? would then be
Code:
DIGIT?  ( char -- f )
   DIGIT NIP ;


[Edit: Fixed some typos]


Top
 Profile  
Reply with quote  
PostPosted: Sun Oct 03, 2021 11:15 pm 
Offline
User avatar

Joined: Fri Aug 30, 2002 1:09 am
Posts: 8454
Location: Southern California
I thought I'd look up what Starting Forth has to say about DIGIT since it often has foot notes telling the differences in figForth, Forth-79, Forth-83, etc.; but I was shocked to see it wasn't even there! I have DIGIT> which has the stack effect you show first.

_________________
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?


Top
 Profile  
Reply with quote  
PostPosted: Tue Oct 12, 2021 1:04 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 865

I've decided to change Fleet Forth's word DIGIT to have the third stack effect mentioned in my previous post.
Code:
DIGIT  ( CHAR -- N TRUE )
       ( CHAR -- CHAR FALSE )

Code:
CODE DIGIT  ( CHAR -- N TRUE )
            ( CHAR -- CHAR FALSE )
   SEC  0 ,X LDA  $30 # SBC
   ' FALSE @ CS NOT BRAN
   $A # CMP
   CS IF
      7 # SBC  $A # CMP
      ' FALSE @ CS NOT BRAN
   THEN
   ' BASE >BODY C@ # LDY
   UP )Y CMP
   ' FALSE @ CS BRAN
   0 ,X STA
   ' TRUE @ JMP END-CODE

DIGIT is a primitive and including the access of the value in BASE only makes it two bytes bigger than before. This change saves two cells, or four bytes, in CONVERT , the high level word which uses DIGIT .
Returning two stack items even when conversion is unsuccessful adds a cell to CONVERT for a DROP to discard the unconverted character when it branches out of its loop.
Code:
: CONVERT  ( D1 ADR1 -- D2 ADR2 )
   1+
   BEGIN
      COUNT DIGIT         \ count base @ digit
   WHILE
      2SWAP BASE @ *
      SWAP BASE @ UM* D+
      ROT
      DPL @ 0< 1+ DPL +!
   REPEAT
   DROP 1- ;              \ 1- ;

The comments show the original lines.
As I mentioned in my previous post, having DIGIT always return two stack items makes it more generally useful. Although this change will make CONVERT two bytes bigger, if I only intended DIGIT to be used by CONVERT , I would have made it headerless.


Top
 Profile  
Reply with quote  
PostPosted: Tue Oct 12, 2021 2:46 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 865

I mention Fleet Forth's word NUMBER? here. Back then it was named -NUMBER . I've managed to reduce the size of its body to 132 bytes.
I've tried two other versions. One is 122 bytes but requires the first character (after any leading conversion base specifier or minus sign) to be a valid digit. The following would all be valid numbers with this version:
Code:
$0.1234
%234.5.8.
#-42.3
123.

And these would not be valid:
Code:
$.1234
.1234
0.1..2
123...45

The other version also requires the first digit after any conversion specifier or minus sign to be a valid digit. It is only 116 bytes because it allows more than one valid non-digit punctuation character in a row. Here are examples of valid numbers with this version:
Code:
$0.123.....56.
123...45.2..
#-137....2



Top
 Profile  
Reply with quote  
PostPosted: Wed Oct 20, 2021 11:28 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 865

I have previously mentioned the word VALID? . This word is used to test if a non-digit character is valid punctuation for a number. It is a deferred word and by default is set to (VALID?) , which only recognizes the period ( . ) as valid punctuation in a double number. Since VALID? is a deferred word, what is accepted as valid punctuation for a number can be changed. Starting Forth mentions these as valid punctuation for a double number:
Code:
, . / - :

Some of these, as well as other punctuation, could be added by using a new version of (VALID?) . Taking this into consideration, I have decided to use the version of NUMBER? which requires valid non-digit punctuation in a number be separated by at least one digit. I believe a typo would be less likely slip by as a valid number with the tighter restriction and that it is well worth the extra six bytes.
My fellow Forthwrites, am I being a bit too cautious? Is it worth the extra six bytes?


Top
 Profile  
Reply with quote  
PostPosted: Wed Oct 20, 2021 11:47 pm 
Offline
User avatar

Joined: Sun Jun 30, 2013 10:26 pm
Posts: 1933
Location: Sacramento, CA, USA
I say it's worth it, but you should just follow your gut ... any decision you make can be easily revised later.

_________________
Got a kilobyte lying fallow in your 65xx's memory map? Sprinkle some VTL02C on it and see how it grows on you!

Mike B. (about me) (learning how to github)


Top
 Profile  
Reply with quote  
PostPosted: Thu Oct 21, 2021 12:40 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 865
barrym95838 wrote:
I say it's worth it, but you should just follow your gut


Thanks.
This was one of those decisions where I could have gone either way, but leaned toward the more restrictive, and slightly larger, version of NUMBER? .


Top
 Profile  
Reply with quote  
PostPosted: Thu Oct 21, 2021 1:58 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 865

I have previously made brief mention of Fleet Forth's word WHERE . It is used to display the location of an error when (ABORT") aborts with an error message. WHERE can be useful apart from (ABORT") to display an error message which is not known beforehand.
-SET is one example:
Code:
: -SET  ( -- )
   WHERE
   R@ 2- @ >NAME CR RON ID. ROFF
   ."  NOT SET" ABORT ; -2 ALLOT

-SET is the vector for a new deferred word until a new vector is set with IS :
Code:
' NOOP IS PAUSE
' (PAUSE) IS PAUSE
' (EMIT) IS EMIT

If the new deferred word, prior to being set to a new vector, is executed from the command line, -SET displays the odd message that EXECUTE is not set. When executed from within a high level Forth word, the message shows the name of the deferred word which is not set.
Another example is ?D , the word which checks the disk drive for an error condition after block read/write.
Code:
: ?D  ( -- )
   $0F >SLF# (?D) 0EXIT
   WHERE
   CR ." DISK ERROR:" CR
   DEB S? ABORT ;  -2 ALLOT

WHERE displays where the error occurred. The message 'DISK ERROR:' is displayed, followed my the message in the Disk Error Buffer or DEB , which comes from the disk drive as a text string.
To get a better feel of what Fleet Forth error messages look like, here are some examples.
This one is an example of using PAD as a buffer for sector 10 of track 37 on disk 0 of the current drive to read (r/w flag value is 1) 256 bytes with the sector read/write word. Device 8 is a 1541 disk drive and does not have 37 tracks. I've added comments after the fact.
Code:
8 DRIVE  OK              \ select device 8 as the current drive
DOPEN  OK                \ open current drive for block access
PAD 10 37 0 1 256 SR/W   \ try to read sector 10 of track 37
PAD 10 37 0 1 256 SR/W   \ this line shown by Fleet Forth in reverse video
                  ^^^^   
DISK ERROR:
66,ILLEGAL TRACK OR SECTOR,37,10

When WHERE shows the line in a screen or the text input buffer or an evaluated string, it shows that line in reverse video so it stands out. The carets ( ^ ) identify the text which caused the error.
Here are some errors due to (intentional) typos.
From the console:
Code:
DECIMAL  OK
CR 1 2 + . 123OOPS 4 5 * .
3
CR 1 2 + . 123OOPS 4 5 * .
           ^^^^^^^
WHAT?

and from a screen:
Code:
1 RAM LOAD 7
SCR# 32769 LINE# 7
2 5 + . 321OOPS  7 9 * .
        ^^^^^^^
WHAT?

Missing delimiter for a string:
Code:
: TEST2
CR ." START TEST2 TEST1
CR ." START TEST2 TEST1
   ^^
" MISSING

and using a deferred word which has not been set:
Code:
DEFER PWM  OK
: TEST  50 PWM ;  OK
TEST
TEST
^^^^
PWM NOT SET

Here is a screen shot of trying to forget a word protected by FORGET's internal 'fence'.
Attachment:
forget test fleet forth.png
forget test fleet forth.png [ 18.96 KiB | Viewed 10818 times ]



Top
 Profile  
Reply with quote  
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 343 posts ]  Go to page Previous  1 ... 10, 11, 12, 13, 14, 15, 16 ... 23  Next

All times are UTC


Who is online

Users browsing this forum: No registered users and 13 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to: