Part 2 - Dictionary / Stack / Variables / Constants
published: 4 February 2023 / updated 9 February 2023
Extending the dictionary
Forth belongs to the class of Threaded Interpretive Languages. This means that it can interpret commands typed at the console, as well as compile new subroutines and programs.
The Forth compiler is part of the language and special words are used to make
new dictionary entries (i.e. words). The most important are :
(start a new definition)
and ;
(terminate the definition). Let’s try this out by typing:
: *+ * + ;
What happened? The action of :
is to create a new dictionary entry named *+
and
switch from interpret to compile mode. In compile mode, the interpreter looks up words
and, rather than executing them, installs pointers to their code. If the text is a number,
instead of pushing it onto the stack, FlashForth builds the number into the dictionary
space allotted for the new word, following special code that puts the stored number onto
the stack whenever the word is executed. The run-time action of *+
is thus to execute
sequentially the previously-defined words *
and +
.
The word ;
is special. It is an immediate word and is always executed, even if the
system is in compile mode. What ;
does is twofold. First, it installs the code that
returns control to the next outer level of the interpreter and, second, it switched back
from compile mode to interpret mode.
Now, try out your new word:
decimal 5 6 7 *+ . \ display 47 ok<#,ram>
This example illustrated two principal activities of working in Forth: adding a new word to the dictionary, and trying it out as soon as it was defined.
Note that, in FlashForth, names of dictionary entries are limited to 15 characters. Also, FlashForth will not redefine a word that already exists in the dictionary. This can be convenient as you build up your library of Forth code because it allows you to have repeated definitions, say for special function registers, in several files and not have to worry about the repetition.
Dictionary management
The word empty
will remove all dictionary entries that you have made and reset all memory allocations to the original
values of the core FlashForth interpreter. As you develop an application, it will often be convenient to return to an earlier, intermediate
dictionary and memory allocation state. This can be done with the word marker. For example, we could issue the command:
marker -my-mark
Later, after we have done some work with the FlashForth system and defined a few of
our own words and variables, we can return the dictionary and memory allocation to
the earlier state by executing the word -my-mark
. Here, we have arbitrarily chosen the
word -my-mark
so it would be good to choose a word that has some specific and easily remembered meaning for us.
Learn more about the role of the word marker
:
Stacks and reverse Polish notation
Forth has an explicitly visible stack that is used to pass numbers between words (commands). Using Forth effectively requires you to think in terms of the stack. That can be hard at first, but as with anything, it becomes much easier with practice.
The stack is the Forth analog of a pile of cards with numbers written on them. The numbers are always added to the top of the pile, and removed from the top of the pile. FlashForth incorporates two stacks: the parameter stack and the return stack, each consisting of a number of cells that can hold 16-bit numbers.
The Forth input line:
decimal 2 5 73 -16
leaves the parameter stack in the state
cell number | content | comment |
---|---|---|
0 | -16 | TOS (Top Of Stack) |
1 | 73 | NOS (Next Of Stack) |
2 | 5 | |
3 | 2 |
We will usually employ zero-based relative numbering in Forth data structures such as stacks, arrays and tables. Note that, when a sequence of numbers is entered like this, the right-most number becomes TOS and the left-most number sits at the bottom of the stack.
Suppose that we followed the original input line with the line
+ - * .
to produce a value xxx. What would the xxx be? The operations would produce the successive stacks:
After both lines, the console shows:
decimal 2 5 73 -16 \ display: ok<#,ram>2 5 73 65520 + - * . \ display: -104 ok<#,ram>
Note that FlashForth conveniently displays the stack elements on interpreting each line
and that the value of -16 is displayed as the 16-bit unsigned integer. Also, the
word .
consumes the -104 data value, leaving the stack empty. If we execute .
on the
now-empty stack, the outer interpreter aborts with a stack pointer error (SP ?).
The programming notation where the operands appear first, followed by the operator(s) is called Reverse Polish Notation (RPN).
Manipulating the parameter stack
Being a stack-based system, FlashForth must provide ways to put numbers onto the stack, to remove them and to rearrange their order. We’ve already seen that we can put numbers onto the stack by simply typing the number. We can also incorporate the number into the definition of a Forth word.
The word drop
removes a number from the TOS thus making NOS the new TOS. The
word swap
exchanges the top 2 numbers. dup
copies the TOS into NOS, pushing all of
the other numbers down. rot
rotates the top 3 numbers. These actions are shown below.
FlashForth also includes the words over
, tuck
and pick
that act as shown below. Note
that pick
must be preceeded by an integer that (gets put on the stack briefly and) says
where on the stack an element gets picked. Also, for the PIC18 version of FlashForth, the
definition of pick
is provided as Forth source code in the file pick.fs. The content of this file
must be sent to the microcontroller to define the word before we try to use it.
From these actions, we can see that 0 pick
is the same as dup
, 1 pick
is a synonym for
over
. The word pick
is mainly useful for dealing with deep stacks, however, you should
avoid making the stack deeper than 3 or 4 elements. If you are finding that you often
have to reason about deeper stacks, consider how you might refactor your program.
Double length (32-bit) numbers can also be handled in FlashForth. A double number
will sit on the stack as a pair of 16-bit cells, with the cell containing the least-significant
16-bits sitting below the cell containing the most-significant 16-bits. The words for manipulating pairs
of cells on the parameter stack are 2dup
, 2swap
, 2over
and 2drop
. For
example, we can put a double value onto the stack by including a period in the number literal.
hex 23.
\ display FORTH prompt: ok<$,ram>23 0
Note that memory on the PIC18 microcontrollers is limited and, for FlashForth, the parameter stack is limited to 48 cells.
The return stack and its uses
During compilation of a new word, FlashForth establishes links from the calling word to the previously-defined words that are to be invoked by execution of the new word. This linkage mechanism, during execution, uses the return stack (rstack). The address of the next word to be invoked is placed on the rstack so that, when the current word is done executing, the system knows where to jump to the next word. Since words can be nested, there needs to be a stack of these return addresses.
In addition to serving as the reservoir of return addresses, the user can also store to and retrieve from the rstack but this must be done carefully because the rstack is critical to program execution. If you use the rstack for temporary storage, you must return it to its original state, or else you will probably crash the FlashForth system. Despite the danger, there are times when use of the rstack as temporary storage can make your code less complex.
To store to the rstack, use >r
to move TOS from the parameter stack to the top of
the rstack. To retrieve a value, r>
moves the top value from the rstack to the parameter
stack TOS. To simply remove a value from the top of the rstack there is the word rdrop
.
The word r@
copies the top of the rstack to the parameter stack.
Using memory
As well as static RAM, the PIC18 microcontroller has program memory, or Flash memory, and also EEPROM. Static RAM is usually quite limited on PIC18 controllers and the data stored there is lost if the MCU loses power. The key attribute of RAM is that it has an unlimited endurance for being rewritten. The Flash program memory is usually quite a bit larger and is retained, even with the power off. It does, however, have a very limited number of erase-write cycles that it can endure. EEPROM is also available, in even smaller amounts than static RAM and is non-volatile. It has a much better endurance than Flash, but any particular cell is still limited to about 100000 rewrites. It is a good place to put variables that you change occasionally but must retain when the power is off. Calibration or configuration data may be an example of the type of data that could be stored in EEPROM. The registers that configure, control and monitor the microcontroller’s peripheral devices appear as particular locations in the static RAM memory.
In FlashForth, 16-bit numbers are fetched from memory to the stack by the word @
(fetch) and stored from TOS to memory by the word !
(store). @
expects an address on
the stack and replaces the address by its contents. !
expects a number (NOS) and an
address (TOS) to store it in. It places the number in the memory location referred to by
the address, consuming both parameters in the process.
Unsigned numbers that represent 8-bit (byte) values can be placed in character-sized
cells of memory using c@
and c!
. This is convenient for operations with strings of text, but
is especially useful for handling the microcontroller’s peripheral devices via their specialfunction file registers.
For example, data-latch register for port B digital input-output
is located at address $ff8a and the corresponding tristate-control register at address
$ff93. We can set pin RB0 as an output pin by setting the corresponding bit in the
tristate control register to zero.
%11111110 $ff93 c!
and then set the pin to a digital-high value by writing a 1 to the port’s latch register
1 $ff8a c!
If we had a LED attached to this pin, via a current-limiting resistor, we should now see it light up as in the companion hardware tutorial. Here is what the terminal window contains after turning the LED on and off a couple of times.
%11111110 $ff93 c! 1 $ff8a c! 0 $ff8a c! 1 $ff8a c! 0 $ff8a c!
FlashForth allows you to very easily play with the hardware.
Variables
A variable is a named location in memory that can store a number, such as the intermediate result of a calculation, off the stack. For example:
variable x
creates a named storage location, x
, which executes by leaving the address of its storage location as TOS:
x . \ display f170
We can then fetch from or store to this address:
marker -play
variable x
3 x !
x @ . \ display: 3
For FlashForth, the dictionary entry, x
, is in the Flash memory of the microcontroller but
the storage location for the number is in static RAM (in this instance).
FlashForth provides the words ram
, flash
and eeprom
to change the memory context
of the storage location. Being able to conveniently handle data spaces in different memory
types is a major feature of FlashForth. To make another variable in EEPROM, try:
eeprom variable y
Constants
A constant is a number that you would not want to change during a program’s execution. The addresses of the microcontroller’s special-function registers are a good example of use and, because the constant numbers are stored in nonvolatile Flash memory, they are available even after a power cycle. The result of executing the word associated with a constant is the data value being left on the stack.
\ PORT B 37 constant PORTB 36 constant DDRB \ 35 constant PINB : initDDRB ( c DDR ---) $ff DDRB c! \ set all PORT B bits in output mode ;
Values
A value is a hybrid type of variable and constant. We define and initialize a value and invoke it as as we would for a constant. We can also change a value as we can a variable.
decimal 13 value thirteen thirteen . \ display: 13 47 to thirteen thirteen . \ display: 47
The word to
also works within word definitions, replacing the value that follows it with
whatever is currently in TOS. You must be careful that to is followed by a value and not something else.
Basic tools for allocating memory
The words create and allot are the basic tools for setting aside memory and attaching
a convenient label to it. For example, the following transcript shows a new dictionary
entry x
being created and an extra 16 bytes of memory being allotted to it.
ram
create graphic-array ( --- addr )
%00000000 c,
%00000010 c,
%00000100 c,
%00001000 c,
%00010000 c,
%00100000 c,
%01000000 c,
%10000000 c,
When executed, the word graphic-array
will push the address of the first entry in its allotted memory
space onto the stack. The word u.
prints an unsigned representation of a number and
the word here returns the address of the next available space in memory. In the example
above, it starts with the same value as graphic-array
but is incremented by (decimal) sixteen when
we allotted the memory.
We can now access the memory allotted to graphic-array
using the fetch and store words discussed
earlier. To compute the address of the third byte allotted to graphic-array
we could say
graphic-array 1 +
, remembering that indices start at 0.
30 graphic-array 2 + c!
graphic-array 2 + c@ . \ display 30
Finally, note that the memory context for this example has been the static RAM, however, the context for allotting the memory can be changed.