Autres articles / Other articles

Part 2 - Dictionary / Stack / Variables / Constants

published: 4 February 2023 / updated 9 February 2023

Lire cette page en français

 

 

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:

No word FORGET in FlashForth

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-16TOS (Top Of Stack)
173NOS (Next Of Stack)
25
32

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.