Jump to content
IGNORED

General Turbo Forth questions


Vorticon

Recommended Posts

I thought I'd start this topic as I am constantly peppering poor Willsy with questions since I started work on Jetpac :P I figure othersas well could also benefit from the questions and answers.

 

So here's one:

I need to use some assembly in-line to create a special effect for Jetpac, and it would be useful to have a quick tutorial on how to do this is TF. I know that the syntax needs to be changed a little:

 

Normal assembly:

 

MOV R1,R2

 

TF:

 

R1 R2 MOV

 

However, I'm sure there is more to it. What are the rules to set up in-line assembly, and are there other specific syntactic changes that one needs to be aware about?

Link to comment
Share on other sites

I thought I'd start this topic as I am constantly peppering poor Willsy with questions since I started work on Jetpac :P I figure othersas well could also benefit from the questions and answers.

 

So here's one:

I need to use some assembly in-line to create a special effect for Jetpac, and it would be useful to have a quick tutorial on how to do this is TF. I know that the syntax needs to be changed a little:

 

Normal assembly:

 

MOV R1,R2

 

TF:

 

R1 R2 MOV

 

However, I'm sure there is more to it. What are the rules to set up in-line assembly, and are there other specific syntactic changes that one needs to be aware about?

 

In TurboForth, you must first load the TMS9900 Assmbler, which starts at block #9 in BLOCKS: 9 LOAD

 

Then you must define a word with the ASM: ;ASM pair of words—you cannot just execute ALC inline:

 

ASM: NEWWORD

R1 R2 MOV,

...

;ASM

 

Generally, you should not rely on the contents of any register entering or exiting a word. Once you have compiled the ALC for your new word and tested it, you can use the Assembler-to-Code utility (starts in block #29 of BLOCKS) to generate code that will not require the Assembler to be loaded. All ALC instruction words end in “,”. The only difference with ALC instructions among the assemblers in TurboForth, fbForth and TI Forth is the “Compare” instruction. In TurboForth, it is CMP, . in the other two it is C, . @Willsy's website has a link to the documentation for the TMS9900 Assembler for TurboForth on the homepage.

 

...lee

Link to comment
Share on other sites

I did find the TF assembler instructions on the site: http://turboforth.net/resources/Assembler.html#s1

 

I had missed it somehow. Thanks for the tip. It looks like quite a read.

 

I am finding out that TF (or likely any other Forth flavor for that matter) is at its best when given a small amount of support from in-line assembly for certain speed sensitive tasks.

Link to comment
Share on other sites

I did find the TF assembler instructions on the site: http://turboforth.net/resources/Assembler.html#s1

 

I had missed it somehow. Thanks for the tip. It looks like quite a read.

 

I am finding out that TF (or likely any other Forth flavor for that matter) is at its best when given a small amount of support from in-line assembly for certain speed sensitive tasks.

 

Almost certainly. Though I'd like to see what it is you are working on specifically in case there is an alternative way to do it that doesn't require assembly language.

 

Regarding assembly language in Forth, it has a different syntax - for example, the instructions come at the end - that's because each instruction is actually a word in the forth dictionary. So, MOV, is word that knows how to compile MOV instructions. The word comes last because the parameters to it go on the stack. R0 pushes 0 to the stack. R1 pushes 1 to the stack. It's the Forth way of doing things: Using the system itself to do the work - i.e. the stack, and the dictionary. Thusly, the assembler is bonkers small (2.7K).

 

Things to be aware of in TF Assembler:

 

  • When your assembly language code runs, you're in TurboForth's workspace in PAD ram! Thusly:
    • R3 is reserved (Forth program counter)
    • R4 is reserved (Data stack pointer)
    • R5 is reserved (Return stack pointer)
    • R12 is reserved (Pointer to NEXT (the code that fetches the next instruction from your code and executes it))

It's no problem to use R12 in your code, just set it back to $8326 before returning.

 

You'll almost certainly never want to touch the return stack pointer. There be demons.

 

Data stack pointer

 

You'll probably use that all the time.

 

Getting data on/off the stack:

 

In normal assembler:

 

 

MOV *R4+,R0

 

That would "pop" a value off the top of the stack and put in R0.

 

In TurboForth assembler:

 

 

R4 *+ R0 MOV,

 

However, TF has a few special words reserved for the stack pointer (R4), so you can actually do this:

 

 

*SP+ R0 MOV,

 

Pushing data to the stack:

 

Let's say the value you want to push is in R0:

 

 

SP DECT,
R0 *SP MOV,

 

SP DECT, - this line decrements the stack pointer to make room for a new stack entry (the stack 'grows' to lower memory addresses)

R0 *SP MOV, - this is the equivalent of MOV R0,*SP or MOV R0,*R4

 

Order Of Operands:

Note: The order of operands is the same as conventional assembler. Always. It's only the instruction that has moved to the end, so:

 

 

LI R0,>994A
would be written
R0 $994A LI,

 

 

Comma at the end of Instructions:

All instructions have a comma at the end. This is frankly, a bit weird, it's a convention in Forth. It is related to the word , (comma) in Forth. Word , takes a value off the stack, and places it in memory at the current compilation address (pointed to by HERE - try HERE $. ). The assembler instructions do the same thing: They take values off the stack (operands and immediate values) and compile them to memory. The comma at the end is a reminder that the data is compiled at the moment the instruction is encountered.

 

Addressing modes:

Saved the worse for last. This is most confusing aspect when getting used to the different syntax.

 

Register Indirect:

  • Normal assembler: *R0
  • Forth assembler R0 ** (note the space between R0 and ** )
  • Special case for stack pointer: *SP

Register Indirect with increment:

  • Normal assembler: *R0+
  • Forth assembler: R0 *+
  • Special case for stack pointer: *SP+

[ At this point, it might have occurred to you that ** and *+ are just words that set a flag somewhere that the instruction will check and modify the op-code accordingly. You'd be right! ]

 

Memory Indirect (or whatever it's called):

  • Normal assembler: @address (e.g. MOV R1,@>A000
  • Forth assembler: address @@ (e.g. R1 $A000 @@ MOV,)

Indexed Memory Addressing:

  • Normal assembler: @>A000(R7)
  • Forth assembler: $A000 R7 ()

See section 5 in the Assembler manual for more info.

 

Expressions/Constants/Variables in Assembler Code:

You can do expressions, and interact with Forth CONSTANTS and VARIABLES in your assembler code. It's all just Forth. This is fairly mind blowing, but:

 

 

VARIABLE FRED
 
ASM: LOAD-FRED
  R0 $99$A LI,
  R0 FRED @@ MOV,
;ASM
 
: TEST LOAD-FRED FRED @ $. ;

 

If you ran the above code, the assembler code would load the Forth defined variable FRED. Why does this work? What do variables do? They push their address to the stack. Ponder on that, let it sink in!

 

You can use constants to, to make code more friendly:

 

 

$8C00 CONSTANT VDPW \ vdp write register address
 
ASM: SOMETHING
  ...
  ...
  R0 VDPW @@ MOVB,
  ...
  ...
;ASM

 

Labels (or, rather, the lack of them)

We're used to using labels in our source code to allow our code to jump to certain addresses. This is not necessary in Forth assembler, and indeed, the concept is not supported. In order to support labels, the assembler would have to be an (at least) two pass assembler. Our assembler makes no passes. It's a 0 pass assembler. It assembles as it goes with no memory of the code it just assembled. It's stateless. The solution is actually VERY nice... Rather than repeat it all here, have a read of section 6 and 7 of the assembler manual.

 

Let me know how you get on. And if there's anything in the manual that can be improved for the newbie I'm all ears. Lee helped a lot with the assembler, but we're both experienced and used to it.

 

By the way, nearly all the above, with the exception of the reserved registers, applies to the fbForth assembler. My assembler for TurboForth is merely a port of the original TI Forth assembler, with a few enhancements (special names for certain registers, and Wycove Forth Assembler addressing modes, which I find/found preferable. IIRC Lee back-ported those extensions to the fbForth assembler).

 

Mark

  • Like 2
Link to comment
Share on other sites

When you say blinking text, do you mean where the text is (say) white, then disappears, then appears again, or do you want to display normal text then inverted text?

 

Either, way, I gurantee you don't need machine code for that.

 

"The Forth cat many skins has; use the Forth. The Forth strong in you is :grin: "

  • Like 1
Link to comment
Share on other sites


: delay ( n -- )
0 do loop ;

: flash ( addr len flag -- )
\ if flag=0: displays the string
\ if flag<>0: erases the string
if spaces drop else type then ;

: test ( -- )
page
50 0 do
10 10 gotoxy
s" This is a test is see if it works" i 2 mod flash
5000 delay
loop ;
Edited by Willsy
Link to comment
Share on other sites

No, that one I had already figured out :) What I couldn't figure out was how to do efficiently is alternating between normal and inverted text without affecting other text items. I could define an alternate inverted character set, but I thought that was a waste of space and also likely slow to use since the text will have to be replaced character by character manually. If I constantly define and redefine the standard character set, then other standard text items will be also be affected.

It would be amazing if you could do it efficiently and speedily with straight TF!

 

EDIT: I think I may be able to do this using V!, V@ and NOT in TF to achieve this task without having to resort to straight assembly. Not enough time to hammer something out right now, but I'll look into it tomorrow. Essentially read the VDP byte needed with V@, inverted with NOT, and write it back with V!. Repeating the process twice will invert the text and then convert back to normal. I'm really loving TF :)

Link to comment
Share on other sites

TurboForth has inverse video of ‘space’ – ‘z’ in the font table at 128 – 218, i.e., offset from the normal 32 – 122 by 96. If we rewrite flash to emit the ASCII code of characters in the string with an offset of 96, they will display in inverse video as happens between if and else below:

 

: flash ( addr len flag -- )

\ if flag=0: displays the string
\ if flag<>0: displays the string in inverse video
if
over + swap do \ addr+len addr do
i c@ 96 + emit \ get char at address=i; add 96; emit
loop
else
type
then ;

 

If you want colors different from the inverse colors of the text, you can change the color table entries corresponding to characters 128 – 218 (character sets 16 – 28) to whatever color combination you want with “charset fg bg COLOR ”. Just remember that the foreground color of the inverse video characters is for the highlight!

 

...lee

Link to comment
Share on other sites

I did not know about that second font table! Yes, that will definitely work. Unfortunately, I am using these characters for my game definitions, although I may have enough space to move them after character 218. Also I'm using the Sinclair font, so I will have to redefine that font in inverse and replace the stock inverse font in TF. It may be just easier to use V! and V@ to manipulate the VDP directly. I'll experiment and see. Thanks for the info :)

Link to comment
Share on other sites

I did not know about that second font table! Yes, that will definitely work. Unfortunately, I am using these characters for my game definitions, although I may have enough space to move them after character 218. Also I'm using the Sinclair font, so I will have to redefine that font in inverse and replace the stock inverse font in TF. It may be just easier to use V! and V@ to manipulate the VDP directly. I'll experiment and see. Thanks for the info :)

 

You probably know this, but haven't thought it through vis-à-vis your suggested scenario above. There are 3 tables in VRAM that affect text in graphics mode:

  1. Screen Image Table (usually starts at VRAM = 0000h, with each byte limited to a number from 0 – 255—the ASCII code in the case of text)
  2. Color Table (32 bytes for 32 character sets of 8 characters each, covering all 256 character codes that can appear in the SIT)
  3. Pattern Descriptor Table (8 bytes for each of the 256 possible characters for the SIT)

Without a predefined character pattern in the PDT (as is the case with TF's inverse video font subset), there is no single byte you can change to do what you want. Whether you change the character code to use emit (my suggestion somewhere above) or change it directly, you are only affecting 1 byte. Of course, all other instances of that code on the screen will change accordingly.

 

For the secondary font, you could cut down on the intrusion into your available patterns by using only ‘space’ – 'Z’ (128 – 186), which would give you back 32 characters. Of course, you could load just ‘space’ and the numeric characters and uppercase alphabetics at 128 – 164, giving you back 22 more character patterns. This latter scenario would, however, require 3 different manipulations, one for ‘space’, a second for numerics and a third for UC alphabetcs.

 

...lee

 

[EDIT in blue above]

Edited by Lee Stewart
Link to comment
Share on other sites

... I may have enough space to move them after character 218. ...

 

That would be just enough space for ‘space’, numerics and UC alphabetics.

... Also I'm using the Sinclair font, so I will have to redefine that font in inverse and replace the stock inverse font in TF. ...

 

That should be easy enough by XORing the pattern bytes with -1 (FFFFh), one cell (2 bytes) at a time.

 

...lee

Link to comment
Share on other sites

I think you're only using upper case for flashing text, yes? I have a cunning plan, Baldrick!

 

Yes. And you just gave me an idea: Replace the lower case letters with the inverse set, and repeatedly write the string in upper and lower case, thus emulating normal and inverse text! I have to see how fast that works, and I don't have to relocate my graphics either.

  • Like 1
Link to comment
Share on other sites

 

Yes. And you just gave me an idea: Replace the lower case letters with the inverse set, and repeatedly write the string in upper and lower case, thus emulating normal and inverse text! I have to see how fast that works, and I don't have to relocate my graphics either.

 

You can do that without the need of inverting the pattern table bytes by swapping the nybbles in each byte of the color table for character sets 12 – 15. The only caveat is that it would also change to inverse video the characters: `{|}~

 

...lee

Link to comment
Share on other sites

 

Yes. And you just gave me an idea: Replace the lower case letters with the inverse set, and repeatedly write the string in upper and lower case, thus emulating normal and inverse text! I have to see how fast that works, and I don't have to relocate my graphics either.

 

And don't forget - you have 256 characters that you can define in TF. Not 143 (or whatever it is) like in Basic/XB. Not only that, but the sprite patterns DO NOT occupy the ascii character set. They have their own set of (256) patterns that do not interfere with the ASCII patterns.

Link to comment
Share on other sites

Okay, here's something for 'yall to get your teeth into. I've been playing with this and really enjoying hacking a solution together.

First, I had a routine that took a normal string, and flag, and if the flag was true it displayed inverse, and if false, it displayed normal. It looked fine, and worked fine. But we want something FAST yes? So, the best way to get something is to do as much work as we can at compile time so that we don't have to do any work (or rather, less work) at run time.

When you're talking in terms of minimising run-time work, it generally means compiling something at compile time, and that's what we're going to do here. We're going to pre-compute normal and inverse strings and display them. The state (inversed/not inversed) will be stored internally, such that calling the same word over and over will display normal/inverse with no work to do on the part of the programmer. Not only that, but the screen position will be stored too.

And the cherry on the cake? You can create as many of these as you want!

Here's a word called flasher: that in turn makes other words encapsulating the functionality described above.

: flasher: ( row column saddr slen -- ) \ runtime ( -- )
    create  >r >r  swap xmax * + ,  r> r> dup c,
    2dup here swap cmove dup allot
    0 do dup i + c@ 96 + c, loop drop align
    does>  ( runtime code)
    >r r@ @ abs  r@ 2+ c@   r@ 3 +   r@ @ 0< if over + then  swap vmbw
    r@ @ negate r> ! ;

Here's how to use it:

10 10 s" GAME OVER SUCKER!" flasher: gameOver

The above will create a new word called gameOver that when executed will display GAME OVER SUCKER! at row 10, column 10. Each time it is executed, the video is inversed.

Two things to note:

  • It assumes the standard TF inversed character set at ascii code+96. If you move your characters somewhere else then change the 96 to whatever your offset is.
  • It calculates the screen address from the row and column. As written, it uses the screen mode that it is currently in (when compiling) to calculate the screen address. It's probable that you'll want to code/compile in 80 column mode, but your code will run in 32 column mode. In that case, just replace xmax with 32.

Here's some test code to try it out:

1 gmode
10 10 s" GAME OVER!" flasher: gameOver
: delay 0 do loop ;
: test begin 
    gameOver 
    6000 delay 
    key? -1 
  <> until ;

Type test to try it. Hold a key to exit.

Very briefly, here's how it works: It creates a new word with the name you give it. Then it calculates the screen address for the string from the row and column and stores it in the word (so that we don't have to calculate it at run-time). It also stores the length of the string (as a byte) immediately after. Then it copies the string that you give it into the word. Then it copies an inversed version of the string immediately after it.

The most significant bit of the screen address is used to specify inversed/not inversed. After each invocation, the bit is toggled.

This is good example of the power of Forth. You have a word that makes other words, and those words manage their own state and look after themselves. Sort of like simple classes and objects (the class would be flasher: and the object would be gameOver which is an instance of flasher:)

All Forth. No assembly. How cool is that? Forth is Mega powerful! :thumbsup:

Edited by Willsy
  • Like 1
Link to comment
Share on other sites

Here's another little test program for the above:

 

 

0 3 s" PLAYER 1 UP" flasher: 1up
0 20 s" PLAYER 2 UP" flasher: 2up
12 15 s" GAME OVER :-(" flasher: gameOver
: test
    page
    100 0 do
        1up 2up gameOver
        6000 0 do loop
    loop ;

 

Notice how, like variables and values, the flashers are set up OUTSIDE of a colon definition.

Link to comment
Share on other sites

 

You can do that without the need of inverting the pattern table bytes by swapping the nybbles in each byte of the color table for character sets 12 – 15. The only caveat is that it would also change to inverse video the characters: `{|}~

 

...lee

 

Oooh cool idea. Let me see... how to switch a nibble over....

 

Erm...

 

 

: flip-nibble ( a -- b )
    dup $F and 4 << swap $F0 and 4 >> or ;

hex

12 flip-nibble .

21 ok:0

 

:D

Link to comment
Share on other sites

flasher: does not work correctly all the time. I am not sure exactly what goes wrong. It seems to be related to odd/even address boundaries before string storage, i.e., storing slen as a single byte. I cannot see why it shouldn't work; but, changing the storage of slen to a cell instead of to a byte forces cell alignment up until we store the strings, which doesn't seem to matter re even address boundaries. The following appears to work in all cases (3 changes marked in red):

 

: flasher: ( row column saddr slen -- ) \ runtime ( -- )
create >r >r swap xmax * + , r> r> dup ,
2dup here swap cmove dup allot
0 do dup i + c@ 96 + c, loop drop
does> ( runtime code)
>r r@ @ abs r@ 2+ @ r@ 4 + r@ @ 0< if over + then swap vmbw
r@ @ negate r> ! ;

 

...lee

  • Like 1
Link to comment
Share on other sites

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