After the general discussion of Forth we may now look at the Forth implementation used for the PIC18F. IRTC creates subroutine threaded code with optimisation if the FAST-CODE directive is used. Many Forth words are compiled as in-line code to further improve execution speed.
The Forth Data stack size is 16 bits. Eight bit byte values are represented as a 16 bit value with the upper 8 bits set to zero. 32 bit, double, values are represented by two stack values, the top value the least significant. The top stack value is always in the TOS register, $000, $001. Register, FSR0, DP, holds the pointer to the rest of the data stack. Macros PUSHT and POPT push and pop the top stack item on and off the external stack.
NOTE: For double, 32 bit, values the top stack item is the least significant word of the 32 bit result. This is the opposite way round to most Forth implementations, but is done to improve the code size and speed in the PIC18F.
The return stack for the Forth word calls is controlled by the PIC stack pointer STKPTR. The PUSH and POP mnemonics control the stack along with CALL/RCALL and RETURN.
The return stack for the Forth words >R, R> and loops etc is controlled by the Forth stack pointer RP, $004, $005 in the PIC18F. This means that Forth return stack operators may be used interactively in the PIC18F Forth as they do NOT affect the cpu stack. However, Forth tricks like R> DROP to exit word will not work.
As all the parameters are passed on the stack Forth has several words to manipulate the stack contents.
DUP (S s1 - s1 s1 ) will duplicate the top stack item, leaving two identical values.
SWAP (S s1 s2 - s2 s1 ) exchanges the top two stack items.
OVER (S s1 s2 - s1 s2 s1 ) copies the second stack item onto the top of the stack, pushing down the first.
ROT (S s1 s2 s3 - s2 s3 s1 ) brings the third stack item to the top of the stack and the top and second are pushed down.
DROP (S s1 - ) removes the top stack item, opposite to duplicate.
NIP (S s1 s2 - s2 ) removes the second stack item.
TUCK (S s1 s2 - s2 s1 s2 ) copies the top stack item beneath the second stack item.
These operators allow us to use the stack for local storage during words and re-ordering the results to pass on to the next word.
The PIC18F has three memory areas to which it has access, code and RAM. C@ and C!, fetch from and store to a single 8 bit RAM File or I/O locations. These two words are optimised during compilation if fixed values, literals, are used as the operands. By using I/O before an register C@ and C! may be used to access the I/O locations as though they are RAM. @ and ! are 16 bit RAM operators. All these use a 16 bit address to access the location.
The EEPROM space is accessed with EEC@ and EEC! which are similar to C@ and C! but work on the EEPROM only.
The code space in the PIC18F is accessible for reading by using MC@ and @. Tables may be made using CREATE to construct lists of values in the code space with C,-T and ,-T the compiler words to store bytes and words into the code space. When the table name is later executed it leaves a 16 bit address on top of the stack that points to the beginning of the list. MC@ and M@ may then use this address to return the stored values.
Note: C,-T and ,-T only store in the Host image. You need to use ?DNLD to update the Flash in Remote.
CREATE 7-SEG.DIGITS (S - a )
( 0 ) $3F C,-T ( 1 ) $06 C,-T ( 2 ) $5B C,-T ( 3 ) $4F C,-T
( 4 ) $66 C,-T ( 5 ) $6D C,-T ( 6 ) $7D C,-T ( 7 ) $07 C,-T
( 8 ) $7F C,-T ( 9 ) $6F C,-T
: GET-DIGIT (S b - b' ) 7-SEG.DIGITS + MC@ ;
Here the table returns an address which is added to the offset of the digit required by +. MC@ uses the new byte address to fetch the value to the stack.
C@ (S a - b ) Fetch a byte b from file address a.
EEC@ (S a - b ) Fetch a byte b from EEPROM address a.
@ (S a - n ) Fetch a word n from file address a.
C! (S b a - ) Store byte b to file address a.
EEC! (S b a - ) Store byte b to EEPROM address a.
! (S n a - ) Store word n to file address a.
MC@ (S a - b ) Fetch a byte b from code address a.
M@ (S a - n ) Fetch a word n from code address a.
MC! (S b a - ) Store byte b to code address a.
M! (S n a - ) Store word n to code address a.
In embedded controllers the need for logical operators is paramount for the bit control necessary.
AND (S s1 s2 - s3 ) creates the logical AND of the top two stack items.
OR (S s1 s2 - s3 ) creates the logical OR of the top two stack items.
XOR (S s1 s2 - s3 ) creates the logical XOR of the top two stack items.
INVERT (S s1 - s1 ) inverts all the bits of the top stack item.
The code produced is in-line and optimised.
These are TRANSITIONal definitions that do not appear in the Library. Their Library counterparts are SET-BITS, RES-BITS and TEST-BITS.
ON (S m fa - ) takes an 8 bit mask and address and sets all the bits set in the mask high at the address leaving the rest unchanged.
OFF (S m fa - ) takes an 8 bit mask and address and clears all the bits set in the mask low at the address leaving the rest unchanged.
CHECK (S m fa - m' ) returns the logical AND of the 8 bit mask and the contents of the address.
These are only 8 bit functions and operate on any RAM address. They may be used to manipulate flags, registers or ports producing tight code.
+ (S s1 s2 - s3 ) adds the top two stack items leaving the result on top of the stack.
- (S s1 s2 - s3 ) subtracts the top stack item from the second leaving the result on top of the stack.
UM* (S s1 s2 - s3 s4 ) multiplies the top two stack items to leave a double result on top of the stack
UM/MOD (S s1 s2 s3 - s4 s5 ) divides the double value under the top stack item by the top stack item leaving two values, remainder second and quotient on top i.e. 32/16=16+16 bits.
1+ (S s1 - s2 ) increments the top stack item by one.
2+ (S s1 - s2 ) increments the top stack item by two.
1- (S s1 - s2 ) decrements the top stack item by one.
2- (S s1 - s2 ) decrements the top stack item by two.
2* (S s1 - s2 ) arithmetic left shift of the top stack item by one.
2/ (S s1 - s2 ) arithmetic right shift of the top stack item by one.
Often we require a value that is fixed and used throughout our application, for example the bit on a port. This may be compiled as a CONSTANT;
2 CONSTANT PUMP
IRTC will cause this to be compiled as an in-line literal whenever the word PUMP is encountered. If the COMPACT-CODE directive is set, a subroutine will be compiled immediately and all references will be via calls. This may save code space but does not allow the optimiser to reduce code.
When the value required needs to be changed during execution a VARIABLE is defined;
VARIABLE COUNTER
Here VARIABLE compiles an in-line literal of the address of a 16 bit, two byte, location in the file. These locations start at the address in RAM-START and an error is generated if too many variables are declared resulting in the pointer going above the value in RAM-END. The variable allocation pointer is incremented automatically by each variable declaration. The pointer may be set absolutely by VORG. VARIABLE allots two bytes and CVARIABLE one byte. @ and C@ may be used to access the contents, ! and C! to change the value.
Note: The contents of a variable location are undefined at compile time. It is necessary to initialise the contents at the start of your application if not implicitly done so by your code.
The flow of control in a program is normally sequential; control structures allow you to alter this by a branch or loop. The change is often brought about by a piece of (runtime) data. This gives the program the ability to choose which direction it will take.
Control structures must be used inside a : definition and may not start in one definition and finish in another. They cannot be used directly from the command line. But they may be nested inside one another as long as they do not overlap.
IF ... THEN
Use: flag IF true words THEN
The 16 bit flag controls the outcome and is consumed by IF. If the flag is non-zero the true words are executed, otherwise they are not.
If the flag is required again it may be duplicated by DUP. If the flag value is only required between IF .. THEN it may be conditionally duplicated by ?DUP.
IF ... ELSE ... THEN
Use: flag IF true words ELSE false words THEN
This is similar to IF ... THEN above but with execution phrases for both true and false (zero) values of the flag.
BEGIN ... AGAIN
Use: BEGIN words AGAIN
This structure forms an endless loop that is only terminated by a PIC reset. This is often the structure used in your application run word.
BEGIN ... UNTIL
Use: BEGIN words flag UNTIL
This structure forms a loop that is always executed at least once. The loop will return to BEGIN as long as the 16 bit flag is false (zero) when tested by UNTIL. As soon as flag is true (non-zero) the loop is terminated. The flag is consumed by UNTIL in both cases.
BEGIN ... WHILE ... REPEAT
Use: BEGIN words flag WHILE words REPEAT
This is perhaps the most powerful of the Forth control structures. The loop starts at BEGIN and executes all the words as far as WHILE. If the 16 bit flag on the data stack is true (non-zero) the words between WHILE and REPEAT are executed, and the cycle returns to BEGIN. If the flag tested by WHILE is false (zero) the loop is terminated. WHILE consumes the flag.
CASE ... OF ... ENDOF ... ENDCASE
Use: CASE (S b - )
value1 OF words ENDOF
value2 OF words ENDOF
...
default words ( otherwise clause )
ENDCASE
The case statement used in IRTC is the result of a competition run by the Forth Interest Group (FIG) in the USA. It was invented by Dr. Charles E. Eaker, and was first published in Forth Dimensions, Vol. II No. 3.
CASE exists to replace large and unwieldy chains of IF ... ELSE ... THEN statements.
The function of CASE is to execute one action dependent on the 16 bit value passed to it on the stack. The OF words compare value1,2 etc. to the stack value and if equal the words between OF and ENDOF are executed, the structure is then exited. If no match is found the words between the last ENDOF and ENDCASE are executed with the input stack value still on the data stack.
FOR ... NEXT
Use: index FOR words NEXT
This is the simplest counted loop in IRTC. The index is transferred to the Return Stack and the words between FOR and NEXT executed. NEXT decrements the index and if true (non-zero) returns to FOR. If the index decrements to zero, false, it is removed from the Return Stack and the loop terminates.
DO ... LOOP
Use: limit index DO words LOOP
DO transfers the 16 bit values of the loop limit and the start index to the Return Stack. The words between DO and LOOP are executed at least once. LOOP increments the index and tests it against the limit. If the index is still less than the limit the loop executes again. If not the Return Stack is cleared and the loop exits.
To leave the loop early use LEAVE. This forces the index to be the same as the limit so that the loop will exit when LOOP is next run.
0= (S s1 - f ) the flag is true if the top stack item is zero.
0< (S s1 - f ) the flag is true if the top stack item is less than zero.
0> (S s1 - f ) the flag is true if the top stack item is positive, greater than zero.
= (S s1 s2 - f ) the flag is true if the top two stack items are equal.
<> (S s1 s2 - f ) the flag is true if the top two stack items are not equal.
< (S s1 s2 - f ) the flag is true if s1 is less than s2.
> (S s1 s2 - f ) the flag is true if s1 is greater than s2.
U< (S s1 s2 - f ) the flag is true if s1 is logically less than s2.
U> (S s1 s2 - f ) the flag is true if s1 is logically greater than s2.
As IRTC creates PIC18F machine code and optimises it as it goes it may be reassuring to see the result. IRTC contains a simple de-compiler which decompiles the code using only the basic set of PIC18F op-codes. It decompiles until it reaches a RETURN instruction which is the normal end to a definition. If one is not found it will continue until the ESC key is pressed.
SEE SERVER
[ SERVER ]
$01F8 RCALL [ >STACK ]
$01FA RCALL [ EXECUTE ]
$01FC BRA [ SERVER ]
As you may see, if an address that is known to be that of a Target word is found, the de-compiler substitutes the Target word name for the hex address.
You may also LIST an area of code by specifying the start address and the number of instructions to list.
Now we have communication we may modify any of the RAM locations, hence the ports. The command FDUMP will list all the RAM locations and C@ will return the value of a single location.
To read from Port1 all we need is:
I/O PORTA C@ H.
This returns the value of PortA.
all the registers and flags are known to IRTC, if you use one from a device other than the one chosen an error will be issued.
To write to PortA, first TRISA must be set to an output on the port pin we wish to use. Say we are using bit 0, then we could set this to an output with:
$FE I/O TRISA C!
The port bit may then be set high with:
1 I/O PORTA C!
or low with:
0 I/O PORTA C!
The same applies to any other ports, timers or internal function.