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