BFASM.DOC

(28 KB) Pobierz
@                         Using the Forth Assembler

What this file is:
   A lot of the mail I have been getting about Blazin' Forth concerns the
use of CODE and ;CODE definitions, and combining assembly language with
Forth.  It recently occurred to me that a lot of people don't know how to
use the Forth assembler - and are therefore missing out on a considerable
amount of the power of the Forth language. So this is an attempt at a
tutorial in using Assembler in Forth, and in using it in the BFC in
particular.  Much of what I say here should be applicable to other 6502
Forth implementations - but not necessarily all. Consider yourself warned.

What this file is not:
   This is *not* a course in machine language programming.  If you need
information on the basics of using assembly language, then consult a good
text - my personal favorites are the ones by Lance Leventhal, but there are
many good ones around.

General Considerations:
   Using CODE and ;CODE words are frowned upon in programs which attempt to
be portable.  In fact, the quickest way to insure that your programs are not
83 Standard is to use the word CODE in them.
   Having said this however, there is much to be said for incorporating a
few code definitions in Forth programs.  Typically, a program will spend
most of its time in one or two words - by recoding these words in assembler,
the overall speed of the program can be increased manyfold - 50% increases
or more are not unusual, and for very little work.
   Also, the incompatibility is not as great as it would appear at first.
It is probably safe to say that the most popular 8-bit personal computers
use the 6502 CPU (Apple II, C64 C128, Atari line etc.). As long as your CODE
definitions are not accessing special hardware features (such as the SID
chip in the C64) most code definitions will work on all computers using the
same CPU. I have shared CODE definitions with friends who have Apples and
Ataris running Forth, and I have never yet had to modify one.
   Finally, Forth is the easiest language there is to combine with machine
language.  Many of the hassles and problems ordinarily associated with
combining high level languages and assembler simply do not occur in Forth.
In other languages, the typical process is this:
   1: Write the high level program, and debug.
   2: Figure out where you need to speed things up.
   3: Fire up an editor, and write some assembly code.
   4: Assemble the file and - usually - run a loader program on that output.
   5: Load the Editor, and modify your higher level source.
   6: Compile your higher level source.
   7: Link the compiled program and the loaded assembly language program.
   8: When it doesn't work (which it won't, at first). Go back to step 3.

   There are also inevitable problems in passing parameters to and from the
higher level code, and the inevitable problem of where to put the machine
language. If you have ever done much of this sort of thing, you know what a
headache these problems can be.
   These last two problems are usually the most difficult, and, you will be
surprised, and possibly relieved to hear that they don't occur in Forth. At
all. Ever.
   Combine that with a resident assembler and a resident editor and a
resident compiler, writing Assembly language in Forth becomes almost
ridiculously easy.

A Quick Example:
   Since I have taken up so much of your time with the above advertisement,
you would probably like me to put my money where my mouth is. Here is a
quick example of what I was talking about above.  I hope it's not too
trivial, but the idea I want to get across here is the ease of combining
Assembler with Forth. First, a high level Forth Program:

: SHIFTLEFT ( -- N2 N1 ) // shift N2 left N1 times
      0 ?DO  2* LOOP ;

: SHIFT-4 ( Just show what a left shift is )
      100 0 DO  I 4 SHIFTLEFT .  LOOP ;

Multiple left shifts are fairly common, and so SHIFTLEFT is likely to be a
handy word in certain applications.  As it is, it will run pretty quickly.
But perhaps, for certain speed demons, not quickly enough, so you decide to
recode the SHIFTLEFT primitive in Assembler:

CODE SHIFTLEFT ( -- N2 N1 )  // shift N2 left N1 times
   BOT LDA, TAY, BEGIN, 0 # CPY, 0= NOT  WHILE,
      SEC ASL, SEC 1+ ROL, DEY, REPEAT,   POP JMP, END-CODE

If you are used to conventional assemblers, this probably looks pretty
weird.  The important thing to notice here is that *only* SHIFTLEFT has
changed - SHIFT-4 (or any other word which uses SHIFTLEFT) will work just as
it did before, with the only change being the overall increase of speed
which machine language naturally brings to any situation.  Notice also that
we didn't have to worry about where to put the code - it goes in the same
spot our higher level SHIFTLEFT went. You will also discover, if you type
this example in, that you don't have to call the assembler. This is all
taken care of for you.  As far as you, another user, or other procedures
which use SHIFTLEFT are concerned, there is no difference between using the
hi-level SHIFTLEFT and the CODE SHIFTLEFT.

The Structure of a CODE definition:
   It's pretty straightforward. They all look the same:

CODE [name] [assembler mnemonics] END-CODE

Note the similarity to a colon definition:

: [name]    [forth words]         ;

How to Exit a Code Definition:
   One thing you must remember is that you have to explicitly leave a CODE
definition by doing a JMP, to another code level routine.  This is probably
the single most common error made by newcomers.  Possibly it is caused by
making a false analogy between the higher level ; and the code level
END-CODE. While the Forth word ; does in fact get you back to where you came
from, END-CODE does not. In fact, END-CODE does nothing at all at run-time.
You can exit a CODE definition by doing a JMP, to any of the following:
NEXT POP POPTWO PUSH or PUT .  These are described below:

NEXT
   NEXT is commonly called the address interpreter. It is the word that is
responsible for the execution of all Forth words.  ALL words in Forth
ultimately end up here.  Doing a NEXT JMP, will cause the current code
definition to stop and return to the word that called it.  All of the
following exit points end with a jump to NEXT .  In what follows, remember
that a "stack element" refers to a 16 bit quantity -- i.e. two bytes.

POP
   POP first drops the first element of the stack, and then jumps to NEXT.
Same as DROP in hi-level forth.

POPTWO
   POPTWO drops the top two elements from the stack, and then jumps to NEXT.
Same as 2DROP in hi-level.

PUSH
   PUSH lets you leave a result on the top of the stack.  PUSH expects the
low byte of the new top of stack to be on the return stack, and the high
in the accumulator. PUSH will leave these as the new top of the parameter
stack (the former top will then be the second element). PUSH then calls
NEXT. Since this routine is somewhat more complicated than the others here
is a typical sequence:

      PHA, ( push low byte to return stack )
      TYA, ( assume high byte is saved in Y register, move it to the A reg)
      PUSH JMP, END-CODE ( parameters set, so jump to PUSH)

PUT
   PUT replaces the top of the stack with a new value.  You setup PUT in
the same way as you setup for PUSH - the new lobyte is pushed to the return
stack, and the new hibyte is in the accumulator before the call to PUT. The
only difference between the two is that PUT replaces the present top of the
stack, while PUSH creates a new top of stack. 

Register Usage:
   Since Forth uses two stacks, and the 6502 CPU only implements one
hardware stack, the parameter stack must be "artificially" maintained. In
this implementation (as in most) it is located in the zero page, and the X
register is used as the parameter stack pointer. The machine stack is
Forth's return stack. Therefore instructions which affect the X register or
the machine stack should be used with care.  At entry to a CODE defintion,
the X register points to the top byte of the parameter stack.  This stack
grows downward in memory, so decrementing the X register will make room
for another element on the stack, and incrementing the stack pointer will
remove an element from the stack. Here is a diagram which shows the
situation when two elements are on the stack:

         Hi Memory
      **************
      *   hibyte2  *
      *   lobyte2  *
      **************
      *   hibyte1  *
X --> *   lobyte1  * Top of Stack
      **************

Notice that the two bytes which make up one stack entry are stored in the
usual 6502 order, with the lobyte lower in memory. To remove the top element
of the stack, we can define the code word DROP:

CODE DROP ( N -- )  INX, INX, NEXT JMP, END-CODE

Which in fact, is exactly the the way DROP is defined in the BFC. After DROP
has been executed, the stack looks like this:

        Hi Memory
      *************
      *  hibyte2  *
X --> *  lobyte2  * New top of Stack
      *************

We will return to this topic later, when we talk about accessing the
parameter stack in more detail.  For now, the main point is to remember that
when Forth starts executing your code definition, the X register will
contain a pointer to the top of the stack. You can use the X register to
access the stack, or to remove elements from the stack, but when your code
definition is finished, other Forth words, and the Forth system itself is
going to expect the X register to contain a valid stack pointer, so don't
change it wantonly. You should also remember that since each stack entry is
two bytes, only even multiples of the INX, or DEX, instruction make sense.
(I.E. INX, INX, INX, INX, not INX, INX, INX, - the first will drop two stack
elements, while the second will drop 1 and 1/2 stack elements - and cause
the Forth system to behave oddly.)

Both the Accumulator (A reg) and...
Zgłoś jeśli naruszono regulamin