Jump to content

Recommended Posts

A while back I saw code for erasing memory that was created by GCC. It was 2X faster than 0 FILL  and so I took it as is and put it in a library.

 

I got around to testing it and found that it was erasing 1 cell more than in the input argument.

 

This is version is correct.  It will always erase an even number of cells even if you give it an odd number which is ok if you keep you Forth memory aligned to word boundaries. 

 

CODE ERASE ( addr cnt  -- )  \ 2x faster than 0 FILL
    *SP+ R1 MOV,
    BEGIN,
        TOS DECT,
    OC WHILE,   
        *R1+ CLR,
    REPEAT,
    TOS POP,
    NEXT,
    ENDCODE

 

image.thumb.png.9acc186171b5044f5f8888c4bde6363b.png

  • Like 3

Things you learn when forced to use your own system. :) 

 

I did some work to improve the Breakout game clone and I learned something that others might want to know about how to better handle cooperative tasking under Forth. 

The new version makes a separate task for the paddle so the player has better control of it. 

 

The original paddle task called the joy stick function JOYST, which is small and fast but it ran the output through a case statement, did a timed delay and looped around again. 

It worked but it was not ideal.  I could not get the ball moving fast enough for "Olympic" level play. 

 

I tried re-compiling with the direct threaded compiler and it was almost no different. That's when the light went on!

 

: PADDLE-MOVER 
    BEGIN 
      0 JOYST 
      CASE
        2 OF  -2 PADDLE+!  ENDOF
        4 OF   2 PADDLE+!  ENDOF 
      ENDCASE
      SPEED @ 2* TICKS 
    AGAIN  
; 

(TICKS is  multi-tasking friendly delay that reads the 9901 timer for duration) 

 

The problem was that even when the joystick was not outputting anything, the loop ran the entire case statement, which used CPU power for no purpose. 

 

The solution is to make a joystick version of KEY that passes control to other tasks if the joystick is doing nothing. 

The reason why pause is first is so that when the joystick is used the loop exits right away. 

 

: JOYKEY  ( -- n) \ wait for any joystick output 
  BEGIN  
    PAUSE 
    0 JOYST 
    ?DUP 
  UNTIL ;

 

Then put JOYKEY is the task loop. 

: PADDLE-MOVER 
    BEGIN 
      JOYKEY 
      CASE
        2 OF  -2 PADDLE+!  ENDOF
        4 OF   2 PADDLE+!  ENDOF 
      ENDCASE
      SPEED @ 2* TICKS 
    AGAIN 
; 

' PADDLE-MOVER CONSTANT PADDLE-TASK

 

This worked much better at preventing the paddle movement from slowing down the ball movement. 

 

Updated code is here: 

CAMEL99-ITC/GAMES.TI/BREAKOUT/src/BREAKOUT-MTASK.FTH at master · bfox9900/CAMEL99-ITC · GitHub

 

BTW the way, compiling this same program with the direct threaded Forth compiler added 4K to the image size !!!

 

 

  • Like 3
  • 4 weeks later...

I finally figured out how to get this code working that I found in an old TI book from 1981, called Software Development. "2nd Edition"

 

The way the thing works is:

  • Reset the UART
  • Set the protocol control register to a predefined value (Camel99 is 8,n,1) 
  • Loops until the UART input line goes low, the "mark" condition, meaning a key was pressed. 
  • Enters a counter loop waiting for the input line to go high.
  • The value of the counter is compared to a *table entry, that also has the correct baud rate divisor for each timed value.
  • Set the baud rate register to the value in the table. 
  • Read a key and throw it away. 

 

I had to change the table values to match the timings of a TI-99 with registers in scratchpad but the program running in expansion RAM.

They were about 2X smaller than the values in the TI book because our machine is slower. 

The other thing that tripped me up two years ago was the need to reset the input buffer bit.  This is not in the original code but was needed for Forth running over the serial port.

I suspect because I hit a key to start autobaud but then the autobaud program resets the UART and the key is not read.  

 

I added one more table entry so you can use 38400 bps.  

 

The video shows the operation. Code is in the spoiler. 

 

https://youtu.be/923vD4ElP9Q

 

Spoiler
\ * autobaud for 9902 for CAMEL TTY Forth  July 2023
\ * Source: Software Development, Texas Instruments 1981

\ Polls the Receive Line Input (RIN) BIT and counts the time
\ it takes to return to 0. This count tells you the tms9902
\ speed to READ 8 bits ie: the baud rate of the sender.
\ Use that number to look up the correct baud rate divisor
\ and set the TI-99 UART baud rate. Voila!

\ 9902 bit # references 
\    13 EQU LDIR     \ "load interval register"
\    16 EQU RTSON    \ request to send
\    18 EQU RIENB    \ rcv interrupt enable
\    21 EQU RXRL     \ receive register loaded bit
\    22 EQU TXRE     \ transmit register empty bit
\    27 EQU -DSR     \ NOT data set ready
\    28 EQU -CTS     \ NOT clear to send
\    31 EQU RESET    \ 9902 reset bit

\ Defined in the kernel. Here for reference 
\ HEX 
\ 1300 CONSTANT CARD  \ card CRU base address 
\ 0040 CONTANT  UART  \ RS232/1 uart offset from base
\ VARIABLE PROTO      \ holds protocol


NEEDS DUMP FROM DSK1.TOOLS
NEEDS MOV, FROM DSK1.ASM9900 

MARKER REMOVE

HEX
CREATE BAUDTB
\  counter baudrate
\ -------- --------
    0002  , 000D ,    \ 38400
    0004  , 001A ,    \ 19200
    0008  , 0034 ,    \ 9600
    000F  , 0068 ,    \ 4800
    001E  , 00D0 ,    \ 2400
    003C  , 01A0 ,    \ 1200
    0078  , 0340 ,    \  600
    00F0  , 04D0 ,    \  300
    01E0  , 0638 ,    \  110


CODE  AUTOBAUD ( -- n)
DECIMAL 
        TOS PUSH,          \ TOS=R4, save R4 to free up for use
        R3 CLR,            \ this is our time counter register  
        CARD @@ R12 MOV,   \ load the card address
        7 SBO,             \ LED on
         5 SBZ,            \ CARD CTS line LOW. You are clear to send

        UART @@ R12 ADD,   \ add 9902 port address
        31 SBO,            \ reset UART
        PROTO @@ 8 LDCR,   \ set protocol (8,n,1)
        13 SBZ,            \ reset LDIR , allows setting baud rate

\ ******* critical time measuring loops ********
\ ** HAD TO ADD THIS INSTRUCTION TO ORIGINAL TI CODE **
        18 SBZ,            \ RIENB resets bit 15 
\ * wait for start bit to go to "mark" (logic 0)
        BEGIN,
          15 TB, 
        NE UNTIL,       
\ * wait until rin goes back to "space" (logic 1)
        BEGIN,
          R3 INC,       
          15 TB,          
        EQ UNTIL,         
        18 SBZ,             \ reset 9902 rcv buffer
\ ************************************************

\ * find the baud rate timer divisor in BAUDTB 
        TOS BAUDTB 2- LI,  \ set table base address-2 in R4 
        BEGIN, 
          TOS INCT,         \ select table record  
          R3 *TOS+ CMP,    \ compare counter to table record, bump R4 
        LE UNTIL, 

\ * baud rate found - set receive and transmit data interval
        *TOS 12 LDCR,      \ need to write 12 bits.

\ * read a char and throw it away
        BEGIN, 
           21 TB,         \ test receive register loaded bit  
        EQ UNTIL,         \ loop until true 
        18 SBZ,           \ reset 9902 rcv buffer
        
        CARD @@ R12 MOV,  \ select the card address
        7 SBZ,            \ LED off
        
        TOS POP,          \ refill top of stack cache register  
        NEXT,
ENDCODE

 

 

Edited by TheBF
fixed more typos
  • Like 2

@TheBF

 

I love that book! More about that later. 
 

I think this assumes an odd or even character?

 

It measures duration from the start bit to the first SPACE bit. So either:

 

1. Assuming even: Measures duration of start bit MARK assuming  LSBit is SPACE. (Maybe TI's "Gold" key?)



2. Assuming odd: measures duration of start bit (MARK) plus least significant bit (MARK) until a SPACE. The characters 'A' and CR meet this condition: 

(LSBit first)

CR 1011 0000

'A' 1000 0010
So it measures the time for two bits. Probably better accuracy!

 

 

A character having the unexpected LSBit would mess with it. 
 

I think you could detect that by watching the framing error bit; if next char is a framing error, try autobaud again. (When stop bit is not MARK.)

Edited by FarmerPotato
  • Like 3
2 hours ago, FarmerPotato said:

@TheBF

 

I love that book! More about that later. 
 

I think this assumes an odd or even character?

 

It measures duration from the start bit to the first SPACE bit. So either:

 

1. Assuming even: Measures duration of start bit MARK assuming  LSBit is SPACE. (Maybe TI's "Gold" key?)



2. Assuming odd: measures duration of start bit (MARK) plus least significant bit (MARK) until a SPACE. The characters 'A' and CR meet this condition: 

(LSBit first)

CR 1011 0000

'A' 1000 0010
So it measures the time for two bits. Probably better accuracy!

 

 

A character having the unexpected LSBit would mess with it. 
 

I think you could detect that by watching the framing error bit; if next char is a framing error, try autobaud again. (When stop bit is not MARK.)

Yes my understanding from the code is that is measures how long it takes to go from mark to space.  I only tested with the enter key actually. There might be a key that beats it. ??

 

You are too clever.  I was wondering if I could do more with the framing error but it starts to get bigger. It's 130 bytes now and I think that's probably enough. 

When vi99 boots on the version I am working on, it writes to the VDP screen and says: 

 

VI99 TTY EDITOR

 

RS232/1  8,n,1  RTS/CTS handshake

Press enter on your terminal 

to set the baud rate

  • Like 2
  • 2 weeks later...

I was away for a few days and when I came back I wanted to address an idea that had been bubbling in my head for ASMForth II.

It's not rocket science but it does make the "language" a bit higher level. 

 

The idea is to leverage the fact that 9900 can use memory almost as easily as it uses registers.

Remember that ASMForth is an Assembler that renames 9900 instructions to use Forth nomenclature where ever possible.

The new idea was to add some simple smarts so that operations would detect if an argument was an address or a register. 

 

It turned out that I needed to deal with two situations.  Source arguments and destination arguments.

Once I realized that, it became pretty simple.  

 

DECIMAL
: ADDRESS?  ( n -- ?) 8191 U> ; \ lowest RAM address is 8192 

HEX
: REG?      ( n -- ?)   0 10 WITHIN ;
: MODE?     ( n -- ?)  1F 30 WITHIN ;

: <SRC>  ( n -- n | n 20)  \ used with fetch operators
    DUP ADDRESS? IF  @@  EXIT   THEN 
    DUP REG?     IF  **  EXIT   THEN 
    DUP MODE?    IF  ( nothing) THEN  
;

: <DST>  ( n -- n | n 20)  \ used with store operators
    DUP ADDRESS? IF  @@   THEN 
;

 

With these simple tests we just modified @ and ! like this:

Using Fetch '@' on a register simply gives you indirect addressing mode; on an address you get symbolic addressing. 

\ SMART fetch operator 
: @     ( addr|reg -- u | Rx n) <SRC> ;
: C@    ( addr|reg -- u | Rx n) <SRC> ;

\ Add some smarts to store
: !    ( src dst -- )  <DST> MOV, ; 
: +!   ( src mem -- )  <DST> ADD, ; 
: C!   ( c dst -- )    <DST> MOVB, ; 

 

I also added some compile time error checks to protect operations that must have a register argument and also where an address was expected. 

\ aborting error detection words 
: ?ADDR     ( addr -- addr) DUP ADDRESS? 0= ABORT" Valid address expected" ;
: ?REGISTER ( reg --reg)    DUP REG? 0= ABORT" Register expected" ;

 

So what you might ask?

Well the result is that we can get some pretty nice code out of this thing that reads kind of like Forth but generates native machine code.

This is the one I think I like the best. 

VARIABLE X   VARIABLE Y 

CODE MEM2MEM    
     X @ Y !   
;CODE 

 

This generates the following code:

DE76  C820  mov  @>de5c,@>de66        

:) 

 

The implication of this is that all the operations that can work on memory or registers like those below now do not need any extra syntax. They just work.

And if you try to use an address where a register is required it aborts on that line. (at least on the operations that I have added that too) :) 

This might take some getting use to coming from Assembler, but it makes a pretty high level Assembler. 

 

: 1+     ( arg -- ) <DST> INC, ;
: 1-     ( arg -- ) <DST> DEC, ;
: 2+     ( arg -- ) <DST> INCT, ;
: 2-     ( arg -- ) <DST> DECT, ;
: ><     ( arg -- ) <DST> SWPB, ;

: ABS    ( arg -- ) <DST> ABS, ;
: NEGATE ( arg -- )  <DST> NEG, ;
: ON     ( arg -- ) <DST> SETO, ;
: OFF    ( arg -- ) <DST> CLR, ;
: NEGATE ( arg -- ) <DST> NEG, ;
: OR     ( src dst --) <DST> SOC, ;
: ON     ( arg -- ) <DST> SETO, ;
: OFF    ( arg -- ) <DST> CLR,  ;
: CMP    ( reg gad) <DST> CMP, ;
: CMPB   ( reg gad) <DST> CMPB, ;

 

Another minor change was made to make 9900 LI and AI  instructions work more like Forth where the number comes first, then the destination register and then the instruction. 

99  R1 #! 
 7  R1 #+!

 

 

  • Like 2
  • Thanks 1

Of course now I have to go back and review the instruction manual and demo programs and then test the programs. 

 

To give you a feel of how this  looks compared to Forth and Assembler here is a demo that can copy data from one array to another.

 

\ CODE word that can be called from Forth 
CODE MOVECELLS ( src dst n -- ) \ tos=n NOS=DST  3rd=src 
    NOS^ R1 !             \ POP dst into a register for auto-incrementing 
    NOS^ R0 !             \ POP source into a register  
    TOS FOR               \ FOR takes n from TOS register and pushes it onto Rstack
       R0 @+  R1 @+ !     \ store a cell and auto-increment both registers
    NEXT 
    DROP                  \ refill TOS register from data stack memory  
;CODE    

 

In the video we copy 4096 CELLS (8k bytes) from the ROM at >0000 to low RAM at >2000 very fast. :)

 

  • Like 3
  • Thanks 1
  • 2 weeks later...

Now that I have my head in this ASMForth, I realized that my FOR NEXT loop could be improved very easily.

 

Edit:  Updated binary program files are here: ASMFORTH/bin/DSK2 at main · bfox9900/ASMFORTH · GitHub

 

The first version pushed the loop limit onto the return stack and decremented the value in memory.

 

*RP DEC, 

 

According to my notes this is about 11% slower than using a register as the down counter.  

I was ok with that because I was just happy the darned thing worked at all. 

 

However now I realize that R11 is available as a loop counter as long as I save it on the return stack and then pop it back with NEXT. 

Re-running the Byte Mag. Sieve with this new FOR NEXT loop reduced the runtime from 9.94 seconds to 9.25 seconds, a 7 % improvement. 

 

The high level parts of the compiler are written in ASMForth II :) , so here is what the FOR NEXT code looks like now.

I am still giddy over this kind of thing actually working. 

: FOR   ( arg --) 
    R11 RPUSH 
    ( ARG) R11 !  
    BEGIN ; 

\ Alternative using literal number, N goes thru TOS -> R11 
: #FOR  ( n --)   
    #           \ put literal into TOS register 
    R11 RPUSH   \ save R11  
    TOS R11 !   \ put loop limit into R11 
    DROP        \ restore TOS register from stack memory
    BEGIN ;

: NEXT  ( -- )   
    R11 1-      \ DEC  R11 
    NC UNTIL    \ JOC  BEGIN 
    R11 RPOP ;  \ MOV *RP+,R11  

: NEXT2  \ dect loop counter by 2 
    R11 2-  
    NC UNTIL 
    R11 RPOP ; 

 

Here is the slightly altered Sieve code. (FILLW had one instruction removed) 

Spoiler
\ SIEVE in ASMFORTH for Camel99 Forth               Aug 2023 Brian Fox
\ based on code by @Reciprocating Bill atariage.com 

\ Minor mods for Version 0.81 

\ Original notes by BIll.
\ * SIEVE OF ERATOSTHENES ------------------------------------------
\ * WSM 4/2022
\ * TMS9900 assembly adapted from BYTE magazine 9/81 and 1/83 issues
\ * 10 iterations 6.4 seconds on 16-bit console
\ * ~10 seconds on stock console

\ * ASMForth II V.81 version runs in 9.26 seconds 

HOST 
INCLUDE DSK1.ELAPSE  \ for timing measurements

DECIMAL 8190 CONSTANT SIZE
HEX     2000 CONSTANT FLAGS   \ array in Low RAM 

ASMFORTH 
: FILLW ( addr size U --)  
    R0 POP         \ size in R0 for FOR to pickup 
    R1 POP         \ Memory location in R1
    R0 FOR         \ FOR takes the R0 argument 
       TOS *R1+ !  \ write U to addr, bump addr by 2
    NEXT2          \ *NEW* counts down by 2 
    DROP 
;                 

HEX
CODE DO-PRIME ( -- n)  
  FLAGS # SIZE # 0101 # FILLW

\ inits 
  R0 OFF               \ 0 constant
  R3 OFF               \ clear array index register

  FLAGS R5 #!          \ array base address 
  SIZE R8 #!           \ size of array 
  
  0 #                  \ counter on top of Forth stack (ie: in R4)
  R8 FOR               \ use R8 to load loop counter 
  R5 @+ R0 CMPB        \ FLAGS byte-compared to 0 
  <> IF                \ not equal to zero ? 
      R3 R1 !          \ I -> R1
      R1 2*  
      3 R1 #+!         \ R1 3+
      R3 R2 !          \ I -> R2 ( R2 is K index) 
      R1 R2 +          \ PRIME K +! 
      BEGIN  
        R2 R8 CMP      \ K SIZE compare 
      < WHILE  
        R0 FLAGS (R2) C! \ reset byte FLAGS(R2)
        R1 R2 +        \ PRIME K +! 
      REPEAT 
      TOS 1+           \ increment count of primes
    THEN 
    R3 1+              \ bump LOOP index register
  NEXT 
;CODE  

HOST   ( Switch back to Host Forth )
DECIMAL 
: PRIMES ( -- )
   PAGE ."  10 Iterations"
   10 0 DO   DO-PRIME  CR . ." primes"  LOOP
   CR ." Done!"
;

 

 

  • Like 2

I looked at this after a few hours and realized that since the loop counter is now a register, #FOR can take the loop length as a literal number in the source code.

I can load R11  with #!. 

#! is an alias for the LI instruction. So this gets far simpler and removes a bunch of instructions generated by the # operator. 

 

: #FOR  ( arg --)  
    R11 RPUSH      \ save R11  
    ( arg) R11 #!  \ LI R11,arg
    BEGIN ;

 

  • Like 1

Just for testing I ran my FORNEXT demo program with elapsed timing. 

Wow did this change to the new #FOR code make a difference!

 

\ 1,000,000 iterations 
\ V .73 14.51 seconds  
\ V .81 10.11 seconds 
CODE NESTED  
    100 #FOR
      100 #FOR
          100 #FOR
           NEXT
        NEXT
    NEXT
;CODE

\ 1,000,000 iterations 
\ v .73 35.1 seconds   
\ V.81  23.8 seconds 
CODE DEEPER  
    10 #FOR
      10 #FOR
        10 #FOR
          10 #FOR
            10 #FOR
              10 #FOR
              NEXT
            NEXT
          NEXT
        NEXT
      NEXT
    NEXT
;CODE

 

  • Like 2
  • 3 weeks later...

While snooping around the internet I found a version of XMODEM written in Forth83 dated Sept 1985 but ported to ANS Forth in 2010. 

 

I am looking at how hard it will be to make it run on Camel99 Forth. I think it's doable.

That would be really handy for sending/receiving files to real iron with CAMELTTY Forth running on my TI99.

 

One thing that it needs is a long duration timer.  I remembered playing with an ISR based 32 bit counter.

I went back to that code and reflected on something that MPE Forth had back in the 1990s.

That is, a programmable timer "creator". 

 

The concept is that you make a word that records a duration and a time in future. (DURATION+CURRENT_TIME)

Every time you use that word it checks if the current time is past the future time you set. 

If is not,  just return false. 

 

ELSE if  it is past the current time, the timer has expired so...

        Reset the future time one more duration in the future, and return true. 

 

With 16mS interrupt ticks and a 32 bit unsigned number you can keep continuous time for over 19 hours.  828 days.

 

Edit: Oops bad math.  Edited.

Edited again.
\ This allows timing up to:
\ 1/60 * 2^32
\ = 71,582,788 seconds 
\ = 1,193,046.5 minutes
\ = 19,884 hours 
\ =  828.5 days

 

However you have to use double integer computations and comparisons.

 

Here is what I came up with.  I have not figured how to deal with what happens if the master timer rolls over to zero 

except to say reset the master timer when the program starts?  :) 

 

There is also problem here with the current ELAPSE function in that it RESETS the master timer but since that is mostly for

interactive testing at the console, I will ignore it for now. (wouldn't take much to fix it) 

 

But for XMODEM waiting for packets to start and such this would be great because you just poll the timer you made as you have time.

The ISR is keeping track of real time. 

 

Spoiler
\ ELAPSE32.FTH  elapsed time measurment words
\ This timer installs a 32bit ISR driven timer

\ This allows timing up to:
\ 1/60 * 2^32
\ = 71,582,788 frames 
\ = 1,193,046.5 secs
\ = 19.884 minutes 
\ = 331.4 hours 
\ = 13.8 days 

NEEDS INSTALL FROM DSK1.ISRSUPPORT

INCLUDE DSK1.TOOLS   \ debug

 HEX
 CREATE TIMER32 0 , 0 ,   \ 32 bit variable

: RESET32  ( -- ) 0 0 TIMER32 2! ;

 CREATE COUNT32    \ native sub-routine.  
      05A0 , TIMER32 CELL+ ,  \ TIMER32 CELL+ @@ INC,
      1702 ,                  \ OC IF,
      05A0 , TIMER32 ,        \    TIMER32 @@ INC, 
                              \ ENDIF,
      045B ,                  \ RT,

DECIMAL
\ UTILITY WORDS 
: 2,    ,  ,  ;
: D0=      ( d -- ?)   OR 0= ;
: DU<     ( d d -- ?) ROT U> IF 2DROP -1  ELSE U<  THEN ;
: CELLS+  ( a n -- a') CELLS + ;

: SEXTAL   6 BASE ! ;
: <:>     [CHAR] : HOLD ;
: <.>     [CHAR] . HOLD ;
: :00     DECIMAL # SEXTAL # <:> ;
: .00   ( ticks)   DECIMAL # SEXTAL #  <.> ; 

\ time$ is volatile. TYPE it or SAVE it shortly after use. 
: TIME$   ( d -- addr len)
     BASE @ >R
 \   frames   secs minutes  hours
     <# .00   :00   :00   DECIMAL # #  #>
     R> BASE ! ;

: .TIME   TIMER32 2@  TIME$ TYPE ;
: .ELAPSED ( -- )   CR ." Elapsed time =" .TIME ;

: ELAPSE   ( -- <text> ) 1 PARSE  RESET32 EVALUATE .ELAPSED ;

: TIME>SECS ( hr min secs -- d) ROT 3600 *  ROT 60 * +  + ;
: SECS>TICKS  ( n -- d) 60 UM*  ;
: >TICKS  ( hr min sec -- d )  TIME>SECS  SECS>TICKS ;


\ TIMER:  creates a data structure of 2 doubles
\ - the duration in ticks 
\ - the end time in ticks 
\ compile time usage:  0 5 30 TIMER: T1  \ 5 minute 30 second timer 
\ Run time  T1 returns false until timer expires.
\           T1 returns true when it expires and resets itself  

: TIMER:  ( hr min secs -- )  
  CREATE  >TICKS  2DUP         \ compute duration in ticks, dup
          2,                   \ compile duration ( field 1)
 ( duration) TIMER32 2@ D+  2, \ compute end time & compile ( field2 )

  DOES>  ( -- ?) 
    ( field1) DUP                    \ address of this timer's data 
    2 CELLS+ 2@  TIMER32 2@ DU<      \ compare field2 to running isr timer  
    0= IF   DROP FALSE  EXIT  THEN   \ no time out, return false & get out
 
 \ timer has expired:       
    ( field1) DUP 2@ TIMER32 2@ D+   \ compute new endtime 
     ROT 2 CELLS+ 2!                 \ store the new end time in field2 
     TRUE 
;

\ redefined to stop isr timer before resetting
: COLD    0 INSTALL  COLD ; 

 COUNT32 INSTALL

 CR .( 32bit counter interrupt installed)

 \ TEST 
0 0 5 TIMER: 5SECS?    \ 5 second timer 

:  TEST   
   BEGIN  
     BEGIN  
        5SECS?  
     UNTIL      \ loop until expires 
     CR .TIME 

   ?TERMINAL 
   UNTIL 
 ;

 

 

TIMER-TEST.png

  • Like 2
21 hours ago, TheBF said:

Edit: Oops bad math.  Almost two weeks is probably long enough for my purposes. :)

\ This allows timing up to:
\ 1/60 * 2^32
\ = 71,582,788 frames 
\ = 1,193,046.5 secs
\ = 19,884 minutes 
\ = 331.4 hours 
\ = 13.8 days 

 

I have not yet examined your code, but, if you are ticking a 32-bit timer 60 times a second, you get 828.5 days before it rolls over!

 

...lee

3 hours ago, Lee Stewart said:

 

I have not yet examined your code, but, if you are ticking a 32-bit timer 60 times a second, you get 828.5 days before it rolls over!

 

...lee

LOL.  Thanks!  That's even better.

Essentially don't worry about it. :) 

  • Like 1
19 hours ago, Lee Stewart said:

 

I have not yet examined your code, but, if you are ticking a 32-bit timer 60 times a second, you get 828.5 days before it rolls over!

 

...lee

BTW where did I go wrong? :dunce:

 

16.66666 ms per tick ~= 60 ticks per second. 

 

Edited per Lee's correction

2^32= 4,294,967,296 ticks / 60

= 71582788 seconds / 60 

= 1193046 minutes  / 60 

= 19884 hrs / 24

= 828.5 days

= ~ 2.27 years

 

 

  • Like 2
13 hours ago, TheBF said:

BTW where did I go wrong?

 

16.66666 ms per tick ~= 60 ticks per second. 

 

2^32= 4,294,967,296 ticks / 60

= 71582788 seconds / 60 

= 1193046 minutes  / 60 

= 19884 minutes / 60 

= 331.4 hrs / 24

= 13.8 days ???

 

Dividing the number of ticks by 60 gives you seconds, not frames (as you first posted). In the above post, your second "minutes" line is 19884 hours, which divided by 24 gives you 828.5 days.

 

...lee

  • Like 1
  • Thanks 1
  • 2 weeks later...

I have always found it inconvenient that I can save my Forth programs as E/A5 images but I have to include the compiler, interpreter and the dictionary headers with the program.

 

I have been noodling on how to use the existing compiler to build a new Forth system on the TI-99 where we don't include words we don't need AND we don't include the dictionary headers either. 

Dictionary headers can take up to 30% of the total size of the binary image in a Forth program.

 

This is not very different than the cross-compiler that I have that runs under MS DOS, but the one advantage I have on TI-99 is there is ton of code primitives in the kernel that are just there for the picking. :)  I am calling this a "RECOMPILER" for now.  I now have all the tools now on the TI-99 that I have in the DOS Forth so this could replace my DOS cross-compiler one day. 

 

All this thinking lead to the word IMPORT:  which copies CODE words from the kernel into a memory buffer (low RAM) but keeps the dictionary info in a vocabulary in Camel99 Forth. 

It isn't a 100% solution because some of the kernel words jump into other words to save space, but most them are useable. 

 

For the regular Forth CODE words it's just this easy to "steal" their code for the new system.

 

\ steal some primitives code from the Kernel and copy into target
IMPORT: C!  2!  2@  COUNT  +! C+! 
IMPORT: RP@  RP! DUP>R  >R  R>  R@  SP@  SP!  2>R 2R>  
IMPORT: NIP  ?DUP  SWAP  OVER  ROT -ROT  ><  2DROP 2DUP  2SWAP PICK  
IMPORT: AND  OR  XOR 
IMPORT: 1+  1-  2+  2-  2*  4*  8*  2/   
IMPORT: 1+!  1-! 
IMPORT: -  D+  RSHIFT LSHIFT INVERT ABS  NEGATE  ALIGNED 
IMPORT: UM*  *  UM/MOD  M/MOD  
IMPORT: =  OVER= 0<  U<  >  < 
IMPORT: MIN  MAX SPLIT FUSE 
IMPORT: MOVE FILL SKIP  SCAN 
IMPORT: ON OFF 

 

Here is what it took to make IMPORT:  (there are some words used below [ COMPILER  T, ] that are defined in the cross-assembler and the preamble code)

\ import.fth 

COMPILER 

HEX 
045A CONSTANT 'NEXT'  \ 9900 CODE for B *R10   Camel99 Forth's NEXT code

: TNEXT,    'NEXT'  T, ;

\ Read code word from kernel, compile into target memory
: TCODE,  ( xt --)  
    >BODY 80 CELLS  ( -- addr len)
    BOUNDS ( -- IPend IPstart)
    BEGIN
      DUP @ 'NEXT' <>  \ the instruction is not 'RET'
    WHILE
      DUP @        ( -- IP instruction)
      T,           \ compile instruction
      CELL+        \ advance IP
      2DUP < ABORT" End of code not found"
    REPEAT
    2DROP
;

: ?CODE   ( xt --) DUP @ 2- - ABORT" Not a CODE word" ;

\ like CREATE but takes the name from a stack string 
: CREATE, ( addr len -- ) HEADER,  POSTPONE DOVAR ; 

\ IMPORT, finds a code word in the kernel and copies the code into TARGET
\ It creates a VARIABLE in the Forth dictioinary that returns the target
\ execution token in interpret mode. 

:  IMPORT,  ( addr len -- )
    PAD PLACE    \ save the name in PAD 
    PAD FIND 0= ABORT" IMPORT, can't find"
    ( xt) DUP ?CODE 
\ CREATE/DOES> but with a string argument     
    PAD COUNT CREATE, 
       THERE ,     \ record the target address in Forth
       TCODE,      \ compile code from Kernel into the target  
       TNEXT,      \ append NEXT to the TARGET code 
    DOES>  
    STATE @ ABORT" Can't run TARGET word"    
    @   ( interpret mode, return the address )
;

: IMPORT:  
    BEGIN PARSE-NAME DUP WHILE  
      IMPORT,  
    REPEAT 
    2DROP ;

 

Next I need to make a version of VARIABLE CONSTANT : and ;   and I should be able to recompile high-level Forth words and build a small program , in threaded code, that has no headers or branch/loop compilers built-in.   

I am envisioning this for making utility programs that don't require disk in the beginning, which means I should be able to put both stacks in scratchpad RAM. ;) 

 

 

 

Edited by TheBF
typo
  • Like 3

This is fascinating. I've felt stuck over how to "package" a Forth (game) without all the compiler stuff.
 

Smalltalk approached this problem with a "packager", but Smalltalk's runtime flexibility made it hard to predict what was used and what wasn't. 
 

Could you compile the dictionary to a dummy address? Say, at >6000 for  a ROM cartridge.  Like, a game written in Forth.
 

Assuming 32K RAM required.  Variables would need to point to RAM.  Maybe you could have a defining word PADVARIABLE? ALLOT would consume  32K RAM. 
 

I'm also recalling Lee's fbForth cartridge--where names are not mixed in with the code. 

 

A really crazy idea I just had is: your IMPORT: is kind of like an assembly REF. What if the  re-compilation made "relocatable" code containing unresolved references?


I see this is completely unnecessary if compilation just uses a dummy base address, say 6000, while actually building  the image in RAM elsewhere. 

 

But: Analogous to the TI assembler, a re-compiler would build a REF/DEF table. A REF table entry is a linked list of everywhere the symbol is needed. Each new word gets a DEF with its address. (All addresses to be relative to a base.) 
 

The recompiler could even infer the IMPORT:   for any word not yet encountered. (Not yet present in the DEF table.) 


Again just like the E/A loader:  at load/link time, there is a base address, like 6000 or A000, and DEFs are calculated to be absolute addresses.  REFs are then resolved to the corresponding DEF. 

Probably an overly complicated solution. 

Anyway, your  work here is a big step toward solving the "packaging" problem!

  • Like 3
9 hours ago, FarmerPotato said:

This is fascinating. I've felt stuck over how to "package" a Forth (game) without all the compiler stuff.
 

Smalltalk approached this problem with a "packager", but Smalltalk's runtime flexibility made it hard to predict what was used and what wasn't. 
 

Could you compile the dictionary to a dummy address? Say, at >6000 for  a ROM cartridge.  Like, a game written in Forth.
 

Assuming 32K RAM required.  Variables would need to point to RAM.  Maybe you could have a defining word PADVARIABLE? ALLOT would consume  32K RAM. 
 

I'm also recalling Lee's fbForth cartridge--where names are not mixed in with the code. 

 

A really crazy idea I just had is: your IMPORT: is kind of like an assembly REF. What if the  re-compilation made "relocatable" code containing unresolved references?


I see this is completely unnecessary if compilation just uses a dummy base address, say 6000, while actually building  the image in RAM elsewhere. 

 

But: Analogous to the TI assembler, a re-compiler would build a REF/DEF table. A REF table entry is a linked list of everywhere the symbol is needed. Each new word gets a DEF with its address. (All addresses to be relative to a base.) 
 

The recompiler could even infer the IMPORT:   for any word not yet encountered. (Not yet present in the DEF table.) 


Again just like the E/A loader:  at load/link time, there is a base address, like 6000 or A000, and DEFs are calculated to be absolute addresses.  REFs are then resolved to the corresponding DEF. 

Probably an overly complicated solution. 

Anyway, your  work here is a big step toward solving the "packaging" problem!

I love your excitement.

You have touched on a bunch of possibilities.  I am going to get something working and then start to think about where it goes

For example it is relatively simple to change the threading method from indirect threading to direct threading or even sub-routine threading with inlining.

 

The infer idea could possibly be done with a first pass through the source code and compile the run time code for all the primitives that are required.

That might be overkill however since it's pretty simple to add primitives to an import statement.

I am thinking about adding an import statement to every source file and make IMPORT:  a bit smarter so it only compiles a primitive once.

 

Lots to think about.   And yes I can compiler headers in other memory because I can move the Forth dictionary to other memory.

I am also considering putting the compiled code into VDP RAM so that I can make programs bigger than 8k, but that will mean putting some of the compiler in SuperCart Ram or SAMS. 

 

BTW the way I think I will "borrow" that word 'PACKAGE'  for something.  :) 

 

 

  • Like 3

For more historical context: check "Image Based Persistence" under Wikipedia Smalltalk article. Citation 37 looks like a whole (online) book. 

Quote

Smalltalk images are similar to (restartable) core dumps


BSAVE 🙂

 

 

 

  • Thanks 1
6 minutes ago, FarmerPotato said:

For more historical context: check "Image Based Persistence" under Wikipedia Smalltalk article. Citation 37 looks like a whole (online) book. 

 

Smalltalk images are similar to (restartable) core dumps


BSAVE 🙂

 

Yes Forth systems work the same way. I have the function in a word called SAVESYS  ( OR if you run Forth from SUPER CART I call it SUPERSAVE)  :)

I didn't use the name BSAVE because that has historical usage for saving a Forth system in BLOCKS. 

 

Here is the code and an example of a minimal save start up word call GO.  WARM inits Forth and ABORT restarts the interpreter

 

This newest version saves the program and if LOW RAM has been used ( H variable <> 0)  it saves that too as a separate program file

with the appropriate header for the E/A5 loader to pull it in.

 

One day I will add saving VDP RAM and SAMS. VDP is simplest because I have a VDP memory management variable in the kernel (VP) so it will work like LOW RAM.

 

Spoiler
CR .( SAVESYS.FTH V2 creates EA5 program Dec 2022 B Fox)
\ creates a binary program E/A 5 format.
\ Makes as many files as needed to save the system
\ Jun 2022 version fixed section overlap. Tested with check sum.
\ Dec 2022 saves the HEAP (Low RAM) as a file if variable H <> 0

\ Usage example:
\  INCLUDE DSK2.MYPOGRAM   ( load all your code)
\  : STARTUP     WARM   CR ." Myprogram ready"  ABORT" ;
\  LOCK   ( this locks the dictionary to the current size )
\
\   INCLUDE DSK1.SAVESYS
\  ' STARTUP SAVESYS DSK3.MYFILENAME

\ NEEDS DUMP      FROM DSK1.TOOLS
NEEDS LOCK      FROM DSK1.MARKER
NEEDS LOAD-FILE FROM DSK1.LOADSAVE  \ we use SAVE-FILE from this library
NEEDS U.R       FROM DSK1.UDOTR

HERE
HEX
A000 CONSTANT HIMEM     \ start of Camel99 Forth program in CPU RAM
1000 CONSTANT VDPBUFF  \ Programs write to file from VDP Ram
2000 CONSTANT LOWRAM
2000 CONSTANT 8K
8K 3 CELLS - CONSTANT IMGSIZE \ makes space for header cells
  13 CONSTANT PROGRAM     \ file mode for Program files

\ define the file header fields. *THESE ARE VDP ADDRESSES*
VDPBUFF            CONSTANT MULTIFLAG
VDPBUFF  1 CELLS + CONSTANT PROGSIZE
VDPBUFF  2 CELLS + CONSTANT LOADADDR
VDPBUFF  3 CELLS + CONSTANT CODEBUFF  \ COPY 8K program chunks to here
         3 CELLS   CONSTANT HEADLEN

: HEADER  ( Vaddr size ?) \ store header info in VDP RAM
    MULTIFLAG V!  PROGSIZE V!  LOADADDR V! ;

: END  ( -- addr )
  ORGDP @ DUP C000 < IF HONK CR ." WARNING: missing LOCK directive" THEN ;

 \ words to compute Forth system properties
: SYS-SIZE    ( -- n)  HIMEM  END  SWAP - ;
: #FILES      ( -- n)  SYS-SIZE 8K /MOD SWAP IF 1+ THEN ;
: CODECHUNK   ( n -- addr) IMGSIZE * HIMEM + ;
: CHUNKSIZE   ( n -- n ) CODECHUNK END SWAP -  IMGSIZE MIN ;
: LASTCHAR++  ( Caddr len --)  1- +  1 SWAP C+! ;
: HEAPSIZE    ( -- n)  H @ LOWRAM - ;
: ?PATH  ( addr len -- addr len )
  2DUP  [CHAR] . SCAN NIP 0= ABORT" Path expected" ;

: GET-PATH    ( <text>) BL PARSE-WORD ?PATH  PAD PLACE ;

: FILENAME    ( -- addr len) PAD COUNT ;

VARIABLE FILECOUNT

: SAVE-IMAGE ( addr len Vaddr size -- )
    CR ." Writing file: " FILENAME TYPE
    HEADLEN +  PROGRAM SAVE-FILE
    FILENAME LASTCHAR++
    FILECOUNT 1+! ;

: SAVELO ( -- )
    HEAPSIZE
    IF
        LOWRAM HEAPSIZE DUP>R FALSE HEADER \ heap is last file saved
        LOWRAM CODEBUFF R@ VWRITE          \ copy HEAP to VDP
        FILENAME VDPBUFF R> SAVE-IMAGE
    THEN ;

HEX
: SAVEHI ( XT -- <textpath> )
    #FILES 0
    ?DO
      \ compute file header values
       I CODECHUNK  I CHUNKSIZE       ( -- addr size )
       I 1+ #FILES <>  HEAPSIZE 0> OR \ multiflag=true if heap has data
       ( addr size ?) HEADER          \ store in file header
      \ Copy to VDP RAM
       LOADADDR V@  CODEBUFF  PROGSIZE V@ HEADLEN +  VWRITE
      \ write VDP to disk"
       FILENAME   VDPBUFF   PROGSIZE V@  SAVE-IMAGE
    LOOP
;
: .BYTES&ADDR ( addr size --)
   DECIMAL 5 U.R ."  bytes, at " HEX ." >" 4 U.R ;

: REPORT
    CR
    CR ." Himem : "  HIMEM  ORGDP @ OVER -  .BYTES&ADDR
    CR ." Heap  : "  LOWRAM  HEAPSIZE  .BYTES&ADDR
    CR ." Saved in " FILECOUNT @ .  ." EA5 files"
    CR
;

: SAVESYS ( xt -- <path>)
    BOOT !
    FILECOUNT OFF
    GET-PATH  SAVEHI  SAVELO REPORT ;

HERE SWAP - CR DECIMAL . .( bytes)

\ ----------------
\  TEST CODE
INCLUDE DSK1.MALLOC

HEX 800 MALLOC CONSTANT MYBUFFER \ mybuffer is in Low RAM

MYBUFFER 800  CHAR $ FILL

: GO   WARM  ABORT ; \ minimum startup code to start Forth interpreter

LOCK                 \ lock dictionary to current size on re-boot

' GO SAVESYS DSK7.TESTKERNEL

 

 

  • Like 3

Now I remember why I never tried to completely re-write my cross-compiler.  My head is spinning. 

 

Last Wednesday I got the import utility working. It as taken me until now, working on and off to get this re-compiler to work.

I just had to get it to build a program and see it run.  :) 

 

Here is the first recompiled program with all the magic incantations it currently takes to make it go. 

I will work on improving the noise but it's good in the beginning to understand what is happening. 

I also have see if my relocation method in Machine Forth can be squeezed into service here.

Right now the programs are AORG >2000  only. 

 

The demo starts and runs BYE. :) 

 

It is real indirect threaded code but there is no dictionary or interpreter or compiler included in the program. 

That is all provided by the "HOST" Forth.    It compiles to 122 bytes. 

\ TEST PROGRAM 1

COMPILER 
NEW
HEX 2000 ORG 

WARNINGS OFF 
INCLUDE DSK7.EXECUTORS  \ load EXIT DOCOL DOVAR ETC.
INCLUDE DSK7.BRANCHING  \ compilers: IF THEN BEGIN AGAIN...
INCLUDE DSK7.ITCTYPES   \ CONSTANT VARIABLE : ; ETC.
WARNINGS ON 

TARGET 
ALSO FORTH  \ import needs to see Forth to find kernel primitives 
IMPORT: + 

COMPILER HEX

TARGET 
VARIABLE BOOT  \ hold the cfa of the word that boots from COLD 

\ bye does not end with NEXT so we can' import it 
CODE BYE   0 LIMI,  0 @@ BLWP,  NEXT,  ENDCODE 

CODE COLD   \ COLD is a key primitive that builds the Forth VM 
      8300 LWPI,               \ set 9900 workspace
      SP 3FFE LI,              \ data stack
      RP 3FE0 LI,              \ return stack
      IP  BOOT  LI,            \ load interpreter pointer with boot word

      R10 EXIT CELL+ LI,       \ EXIT + 2 = NEXT -> R10
      *R10 B,                  \ jump to NEXT (inner interpreter)
ENDCODE

TARGET
: MAIN  
    BYE  
;

COMPILER HEX  T' MAIN  BOOT HOST !       \ set the boot variable 
COMPILER      T' COLD >BODY 2002 HOST !  

SAVE DSK7.TESTPROG1

 

For the masochists in the group you can see the source code here and the bin folder has everything in TI-99 format.

 bfox9900/RECOMPILER: Re-compiles Forth code as binary program without interpreter or dictionary (github.com)

 

Here is a little video showing the process. The program is totally "under-whelming" but its a start.  :) 

 

 

 

 

  • Like 3

No matter how many times I do these Forth compiler things I always get something messed up initially.

I guess if it was easy everybody would be doing it. :) 

 

I had an error in what I compiled into the program when ';' is encountered but thanks to the Classi99 debug window it was easy to see. 

I had forgotten that EXIT must be a proper CODE word so the Forth compiler can"compile" it correctly.

I initially gave it a label like DOVAR DOCON etc. 

The new file is here: RECOMPILER/src/EXECUTORS.FTH at main · bfox9900/RECOMPILER · GitHub

 

Here is the little program I used to test "nested" colon definitions and it works as advertised now. It compiles to 176 bytes. 

Something to be aware of is that these programs have no code in scratchpad RAM currently.

So they will run ~20% slower than Camel99 Forth.  But that can be fixed later. 

 

I am slowly learning how to simplify the "noise" in the code.   

I realized that COLD just needs to be a label since I branch directly into it at startup. 

I think I will wrap up the first 26 lines into a "PREAMBLE" file that sets up the program. 

 

Things I think I like:

  1. The Virtual machine is a separate file that you load first
  2. The branch and loop compilers are a separate file
  3. The data description words are a separate file 

In "theory" this means we could change the threading type to direct-threaded with very little trouble.  😇

 

So with this level of compiler working I should be able to make a little VDP I/O library and write HELLO world next.

I played too many games with Camel99 VDP I/O to allow it to imported as is.

(I used a BL sub-routine to set VDP addresses which is not relocatable)

No worries. I can make something smaller if I don't worry about speed. 

 

\ NESTTEST.FTH  test nested calls 

COMPILER 
NEW
HEX 2000 ORG 

WARNINGS OFF 
INCLUDE DSK7.EXECUTORS  \ load EXIT DOCOL DOVAR ETC.
INCLUDE DSK7.BRANCHING  \ compilers: IF THEN BEGIN AGAIN...
INCLUDE DSK7.ITCTYPES   \ CONSTANT VARIABLE : ;  etc.
WARNINGS ON 

COMPILER HEX

TARGET 
VARIABLE BOOT  \ hold the cfa of the word that boots from COLD 

L: COLD               \ COLD runs at boot time to build the Forth VM 
      8300 LWPI,      \ set 9900 workspace
      SP 83FE LI,     \ data stack in scratchpad
      RP 83D0 LI,     \ return stack in scratchpad 
      IP BOOT LI,     \ load interpreter pointer with boot word

      R10 _NEXT LI,   \ inner interpreter stays in R10 
      *R10 B,         \ jump into the interpreter 

TARGET ALSO FORTH 
IMPORT: DUP DROP 1- 

TARGET
CODE BYE    0 LIMI, 0 @@ BLWP,  ENDCODE 

 : SUB3  BEEF ;
 : SUB2  SUB3 ;
 : SUB1  SUB2 ;

: MAIN  
    4000 
    BEGIN  
      1- DUP 
    WHILE    
      SUB1  DROP 
    REPEAT
    DROP 
    BYE 
;

COMPILER HEX  
   COLD  2002 T!    \ jumps into cold on startup
T' MAIN  BOOT T!    \ set the boot variable 


SAVE DSK7.NESTTEST

 

  • Like 1

So now that the compiler works correctly I took some time to improve the user experience.

  1. As mentioned I consolidated all the preamble into a file called ITC-FORTH.   So you just include that file before any other code is compiled.
  2. I changed IMPORT:   so it knows to search in the CAMEL99 Forth dictionary for primitive words. Before you had to manually add Forth to the search order.
  3. I add the command AUTOSTART so it is simple to specify the routine that will run after COLD builds the Forth stacks and sets the workspace. 

And with that here is modestly complicated "recompiled" Forth program that compiles and runs.

\ TESTPROG2.FTH  Demo IMPORT: CODE  loops and AUTOSTART   Sep 2023 Fox 

HEX 2000 ORG   \ this must be set before compiling any code 

INCLUDE DSK7.ITC-FORTH \ preamble for indirect threaded Forth

IMPORT: DUP DROP  1- 0=

COMPILER HEX 

TARGET
CODE BYE ( --) 0 LIMI,   0 @@ BLWP,    NEXT,  ENDCODE 

: LOOP2 ( -- ) 5000 BEGIN  1- DUP   0= UNTIL   DROP ;
: LOOP1 ( -- ) 5000 BEGIN  1-  DUP WHILE REPEAT DROP ;

: MAIN  ( -- ) LOOP1 LOOP2 BYE ;

COMPILER 
AUTOSTART MAIN 
SAVE DSK7.TEST2

 

That's enough for one day. I am behind on my Viola practicing. :) 

 

  • Like 2

So I wondered what would happen if I wrote an entire VDP driver in Forth? 🙂

It's not fast. That for certain.

But to satisfy everyone's curiosity here is a set of VDP words written entirely in Forth except of a code word to turn off the interrupts.

I added -UNTIL (not until) which is something Chuck invented for Machine Forth because it is faster than 0= UNTIL. 

I chose to try it without DO/LOOP first because DO LOOP adds some runtime code to the program.

 

It's pretty tidy, but it would benefit greatly from a code word like  R+!  or R1-! to manage the counter on the return stack 

As I think about it adding a machine Forth style FOR/NEXT loop would be trivial with R1-! in the system. 

 

: -UNTIL  POSTPONE WHILE POSTPONE REPEAT ; IMMEDIATE

HEX

8800 CONSTANT VDPRD               \ vdp ram read data
8802 CONSTANT VDPSTS              \ vdp status
8C00 CONSTANT VDPWD               \ vdp ram write data
8C02 CONSTANT VDPWA               \ vdp ram read/write address

\ VDP set-address sub-routines
CODE 0LIMI   0 LIMI,   NEXT,  ENDCODE

: RMODE ( vdpaddr -- ) DUP 0LIMI VDPWA C! >< VDPWA C! ;
: WMODE ( vdpaddr -- ) 4000 OR RMODE ;

: VC@+  ( Vdpaddr -- c) VDPRD C@ ; \ read & inc. address
: VC!+  ( c --) VDPWD C! ; \ write & inc. address

: VC@   ( VDP-adr -- char ) RMODE VDPRD C@ ;
: VC!   ( c vaddr --) WMODE VC!+ ; \ set address and write

\ VDP integer fetch & store
: V@    ( VDPadr -- n) VC@  VC@+  FUSE  ;
: V!    ( n vaddr --) >R  SPLIT R> VC! VC!+ ;

: VWRITE ( RAM-addr VDP-addr cnt -- )
        SWAP WMODE
        >R
        BEGIN
          COUNT VC!+
          R> 1- DUP >R
        -UNTIL
        R> 2DROP ;


: VREAD  ( Vaddr Ram cnt --)
         >R
         SWAP RMODE
         BEGIN
           VC@+ OVER C!
           R> 1- DUP >R
         -UNTIL
         R> 2DROP ;

: VFILL  ( vaddr cnt char --)
         SWAP >R
         SWAP WMODE
         BEGIN
           DUP VC!+
           R> 1- DUP >R
         -UNTIL
         R> 2DROP ;

 

  • Like 1
  • Thanks 1

Once you have the VDP driver you can make some standard output words. 

I am testing all this on Camel Forth before making it a library for the recompiler project. 

I will probably have to relent and use Assembler for the VDP library to make work better but this was a fun exercise. 

 

Spoiler
\ Standard Forth output words

VARIABLE C/L       C/L@  C/L !
VARIABLE COL
VARIABLE ROW
VARIABLE CURSOR
VARIABLE C/SCR     3C0 C/SCR !

20 CONSTANT BL

: >VPOS  ( col row -- vaddr) C/L @ * + ;

: CLIP   ( n lo hi -- n) ROT MIN MAX ;

: CURSOR ( -- Vaddr) COL @ ROW @ >VPOS  0 C/SCR @ CLIP ;

: COL+!  ( n -- )
  COL @ +  DUP C/SCR @ >
  IF DROP COL OFF EXIT
  THEN COL ! ;

: ROW+!  ( n -- ) ROW @ +  0 23 CLIP ROW ! ;

: EMIT   ( c --) CURSOR VC! 1 COL+! ; 
: CR     ( -- ) 1 ROW+!  COL OFF ;

: SPACE   BL EMIT ;

: TYPE   ( addr len -- )
        >R
        BEGIN
           COUNT EMIT
           R> 1- DUP >R
        -UNTIL
        R> 2DROP ;

: AT-XY  ( col row -- ) ROW ! COL ! CURSOR WMODE  ;

: PAGE   0 C/SCR @ 20 VFILL  0 0 AT-XY ;

: VDPTYPE  ( addr len --) TUCK CURSOR SWAP VWRITE  COL+! ;

 

 

It not FAST doing it this way, putting a slooooow  EMIT in a loop, but it works. 

(PAGE is painful to watch) :) 

 

But the text writing speed is not bad if you use the block write word VDPTYPE as seen in the 2nd video 

 

Test programs

: TEST
    PAGE
    100
    BEGIN
       S" HELLO WORLD!  " TYPE
       1- DUP
    -UNTIL
    DROP ;

: TEST2
    PAGE
    40
    BEGIN
       S" Hello World!  " VDPTYPE
       1- DUP
    -UNTIL
    DROP
;

 

  • Like 2

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...