Link here

Advanced Forth Programming Topics

Program development techniques

Using ANEW

FORTH is a language that encourages interactive and incremental programming and debugging. You fully test low level words, and build up to higher level words. Often you will discover a bug in a word that has been defined, and will want to re-compile a new version of the word to fix the error. The interpreter always finds the most recently compiled version of a word, so you need not worry about entering multiple definitions of a word. But after many re-definitions memory space is wasted as the dictionary becomes cluttered with unneeded buggy definitions. The QED-Forth word ANEW is solves this problem.

The action of ANEW is based on the standard FORTH word FORGET which is explained here. If a definition for a word called GARBAGE is compiled and then you decide to re-compile it, you can execute the command

FORGET GARBAGE ↓

before re-compiling. This changes some user variables so that the interpreter can no longer find any of the words that have been compiled since GARBAGE was defined. Consequently, GARBAGE and all subsequent words are “forgotten”. FORGET resets the definitions pointer DP to point to the first byte in the code field of GARBAGE. Thus the next definition you enter will over-write the original definition. FORGET also sets the name pointer NP to point to the first byte of the header of GARBAGE in the dictionary. This will cause the header for the next word entered to write over the original header for GARBAGE.

But there is a problem with using FORGET: it does not reset the variable pointer VP or the EEPROM variable pointer EEP to the values they held before GARBAGE was defined. So if GARBAGE or any of the words compiled after it advanced VP or EEP, this variable or EEPROM variable space will not be recovered, thus wasting memory.

The word ANEW is a more sophisticated and easy to use word for dictionary cleanup. Here’s an example showing the use of ANEW:

ANEW DEBUG.SESSION
: GARBAGE   ( -- )
.” I dont do much”
;
VARIABLE MYVAR

There is a spelling error in the word “don’t” in GARBAGE, so the error is corrected and the source code is re-loaded as:

ANEW DEBUG.SESSION
: GARBAGE   ( -- )
.” I don’t do much”
;
VARIABLE MYVAR

There is now only one definition each of GARBAGE and MYVAR in the dictionary; ANEW took care of forgetting the previous definitions and properly resetting NP, DP, VP and EEP. To use it, just put ANEW followed by a “marker” name of your choice (usually one that describes the general function of the code you’re compiling) at the top of the source code file that you download. Then each time you download a new version of the file, ANEW will search the dictionary for the marker name (in this case, DEBUG.SESSION), and intelligently forget back to that point. It will leave the marker name in the dictionary so that future ANEW commands can find it.

Using ANEW <marker.name> as the first command in each source code file provides easy and automatic cleanup of the dictionary during debugging. If the memory map is altered at the start of a source code file, the ANEW command should follow rather than precede the code that changes the memory map. Setting the memory map is described later in this chapter.

How ANEW Works

 

Defining an APPLICATION segment

If your program relies on pre-compiled device driver libraries provided by Mosaic, you’ll need to declare these using the REQUIRES syntax described in the Segment Management Chapter. The APPLICATION (or LIBRARY) command works just like ANEW: it expects a unique name (in this case, the name you choose for your application program).

When the source code file containing the APPLICATION command is reloaded, the memory map is automatically set back to the state it was in when the APPLICATION command was initially executed. Unlike the ANEW command, the APPLICATION command typically requires an END.SEGMENT statement to be placed at the end of the program.

For example, let’s say you’re designing a product that includes the AnalogIO Wildcard, and you want to use the pre-coded library called AnalogIO supplied by Mosaic to interface to the Wildcard. To declare an application program named ANALOG_CONTROLLER that requires the loading of the AnalogIO driver, you could execute

APPLICATION ANALOG_CONTROLLER
REQUIRES.FIXED AnalogIO
<your program code goes here>
END.SEGMENT

Your program code can call any of the functions defined in the AnalogIO library. When you reload the program, the APPLICATION statement will perform the ANEW action to simplify your development. For more information about LIBRARY and APPLICATION segments, consult the Segment Management Chapter in this document.

 

Using ON.FORGET for heap recovery

The one area of memory that ANEW, APPLICATION and LIBRARY do not recover is heap memory associated with forgotten words or data structures. You can accomplish this by defining a word having the (non-unique) name ON.FORGET. FORGET (which is called by ANEW, APPLICATION and LIBRARY) both check the names of all words being forgotten; if the name is ON.FORGET, the definition is executed before being forgotten. For example, the following code creates and dimensions an array and provides for recovery of the heap space when the code is forgotten:

ANEW EXAMPLE
ARRAY: MY.ARRAY
7 1 2 ‘ MY.ARRAY DIMENSIONED     \ 1 dimension, 7 2-byte elements
: ON.FORGET    ( -- )
‘ MY.ARRAY DELETED
;

When this code is compiled, QED-Forth will report that ON.FORGET is not unique; this is expected and is not a problem. On subsequent execution of ANEW EXAMPLE or even FORGET EXAMPLE, MY.ARRAY will be forgotten, and the ON.FORGET word will be executed before it is forgotten. This will delete MY.ARRAY to recover any heap space that had been allocated to the array.

 

Using the interactive debugging tools

Summary of the Forth interactive debugger

QED-Forth’s debugger can be used to debug high level and assembly coded routines. The debugger allows you to:

  • Trace program flow
  • Single-step through a program or routine
  • Install break points by simply compiling a call to the BREAK routine
  • Enter a “break” mode while debugging that allows you to inspect or modify memory locations
  • Install a customized diagnostic routine that is executed after each program instruction, and
  • Print the contents of the machine registers after each high level and/or assembly instruction.

The debugger is configured by the logical values of four user variables: TRACE, DEBUG, SINGLE.STEP, and DUMP.REGISTERS. Each of these can be turned ON or OFF by the programmer. If TRACE is ON, QED-Forth compiles a trace instruction before every compiled command. When the traced words execute, the trace instruction checks the DEBUG flag and, if it is ON, prints the stack picture at that point in the program and the name of each traced command. If SINGLE.STEP is ON, the trace instruction stops after each command and enters the BREAK mode which is a FORTH-style interpreter that lets the programmer examine and alter variables, stack items, etc. If DUMP.REGISTERS is ON, the trace instruction prints the contents of the registers so you can see how each instruction affects the machine registers. This is especially useful when debugging assembly-coded routines.

Those who need even more sophisticated debugging tools can define a customized diagnostic action and install it (using the word IS.TRACE.ACTION) as the first action that is executed by each compiled trace instruction.

A Sample Debugging Session

The BREAK Mode and Single Step Execution

Printing the Register Contents

Specifying a Customized Trace Action

Other Handy Debugging Words

 

Floating point mathematics

Many applications require floating point mathematics. Unlike many small microcontrollers, the PDQ Board provides a built-in floating point package including trigonometric, logarithmic, and exponential functions and formatted real number input and output.

Programming in QED-Forth with floating point arithmetic confers many advantages. Fractional numbers are easy to represent, and the wide dynamic range of floating point numbers means that you don’t have to worry about the range limitations of integer math or the hassle of scaling operations to preserve precision. Floating point math is simple; instead of a host of integer, double number, and mixed-number operations, the four basic floating point operations F+ F- F* and F/ perform the math while optimally preserving precision. Error checking is supported, and floating point numbers can be recognized and printed in a variety of convenient formats. This section describes the floating point operations in detail.

 

Floating point number input format

Numbers that include either a decimal point or an embedded “E” or “e” are recognized by the QED-Forth interpreter as floating point numbers. Numbers without a decimal point or “E” are recognized as integers. It is strongly recommended that all floating point numbers be entered with a decimal point.

The following general floating point format is accepted: Sxxx.yyyEszz

where

S

is the sign of the number, either “+”,” -”, or missing.

xxx.yyy

is the mantissa part of the number, any length, with an optional (but recommended) decimal point. Isolated embedded commas are permitted.

E ( or “e”)

designates that an optional base 10 exponent may follow.

s

is the sign of the base 10 exponent, either “+”,”-”, or missing.

zz

is the exponent, up to 2 digits.

Positive and negative numbers are represented over a magnitude range of 1.0x2^-126 to 1.99999x2^+127 corresponding to approximately 1.2x10^-38 to 3.4x10^38. The resolution of the mantissa is 6.9 decimal digits.

Use a Decimal Point When Entering a Floating Point Number

It is recommended that a decimal point always be included when entering a floating point number. If the current number base is hexadecimal, “E” is a valid digit, and a number containing an E but no decimal point could be interpreted as an integer.

As described in the previous chapter, when the QED-Forth interpreter gets a string from the input stream, it first tries to find it in the dictionary. If it is not found, it tries to convert it to an integer in the current number base and, if it cannot be converted, tries to convert it to a floating point number. An embedded decimal point in the numeric string prevents the string from being interpreted as a valid integer, but allows it to be interpreted as a floating point number (if the rest of the string is in the proper floating point format). Depending on the current number base, an embedded “E” may or may not be sufficient to flag the number as a floating point quantity.

Floating point numbers are always converted and printed as decimal numbers, irrespective of the contents of the user variable BASE. Some examples of valid input numbers are:

.414         +1.414e
1234.56E11    1.5678E-23
-0.123e5         -.0E
-.00001E+1    5.
12,344.        1e
0.            .1e-4

Printing Formats

Fixed Format

Scientific Format

Floating Format

 

Stack notation and floating point stack operators

A floating point number uses two normal stack locations. A family of stack manipulation words is available to manipulate these 32-bit items, including:

FDROP    F2DROP        FSWAP
FROT    FDUP        F2DUP
FOVER     FDUP>R        F>R
FR>    FR@        FR>DROP

In writing stack notation floating point numbers are identified with an “r” (for real numbers), while signed and unsigned integers are identified with an “n” and “u”, respectively (consult the glossary for a list of stack symbols).

 

Floating point operators

The floating point operators F+ F- F* and F/ perform floating point addition, subtraction, multiplication, and addition, respectively. Each requires two operands on the stack and places its result on the stack. A number of transcendental functions are also available for powering (F^N F** and 10^N), arithmetic inversion (FNEGATE), multiplicative inversion (1/F), absolute value (FABS), square root (FSQRT), scaling (F2* F2/ FSCALE), trigonometric, logarithmic, and exponential conversions.

Trigonometric Functions

Logarithmic and Exponential Functions

Random Number Generation

Floating Point Comparison

Pre-defined Floating Point Quantities

 

Floating point memory access, variables, and constants

The kernel words FCONSTANT and FVARIABLE define floating point constants and variables. For example, the square root of three may be defined as a floating point constant by executing

3.0 FSQRT FCONSTANT SQRT.OF.3  ↓   ok  

Now execution of SQRT.OF.3 will place the square root of 3.0 on the stack:

SQRT.OF.3   F.  ↓    1.7321   ok 

F@ and F! can be used with floating point variables just as @ and ! are used with integer variables. These are “page smart” memory operators that can access memory on any page, and correctly handle memory accesses that cross page boundaries.

Floating point self-fetching variables may be defined with the word REAL: as

REAL: BUDGET.DEFICIT↓

To set the value, use the TO operator as

4.0E12 TO BUDGET.DEFICIT↓

Stating the name of the self-fetching variable leaves the value on the stack:

BUDGET.DEFICIT F.  ↓    4.000E+12  ok
 

Number conversion

Integer to Floating Point Conversion

Floating Point to Integer Conversion

Floating Point to Integer to Floating Point Conversion

Floating Point to String Conversion

String to Floating Point Conversion

Numeric Input

 

Floating point error handling

The user variable FP.ERROR holds an error flag that can be inspected after any floating point operation. If the contents are 0, there was no error, while contents of 1 indicate underflow and -1 indicates overflow. The FORTH words UNDERFLOW and OVERFLOW set FP.ERROR to 1 and -1, respectively. In an application in which errors must be detected, the FP.ERROR flag must be polled before the next floating point operation changes them.

Errors detected in the floating point math operations do not cause an ABORT. If the programmer desires, this can be accomplished by polling the FP.ERROR variable and executing an ABORT with an appropriate message if it is non-zero. For example, F/ could be redefined to halt processing if an error is detected during division:

: F/     ( r1\r2 -- r3  | r3 = r1/r2 )
F/ FP.ERROR @ ?DUP
IF            \ if error occurred
-1 =
IF  .” Overflow or divide by zero error!”
ELSE .” Underflow error!”
ENDIF      \ print message...
ABORT      \ ... and abort
ENDIF
;
 

The heap memory manager

QED-Forth provides a heap memory management system. QED-Forth’s array and matrix math routines make use of the heap memory manager, but they do it transparently. To use these features you need only a cursory familiarity with the heap. On the other hand, if you want to create your own sophisticated dynamically allocated data structures, you should read this section carefully.

A “heap” is a pool of memory from which smaller blocks of memory of variable size are allocated. The heap manager allocates blocks of memory requested by the user’s program from this pool. When the program is finished with the block of memory, it can return it to the heap. This results in efficient use of memory, especially for data structures which can vary in size and for temporary data structures which are used and then de-allocated. The de-allocated memory becomes available for use by other segments of the program. QED-Forth can maintain any number of heaps simultaneously, each containing numerous data structures.

Heap Compacting

Handles to Heap Items

Heap Size

Initializing the Heap

Allocating Heap Items

De-allocating Heap Items

Maximizing the Efficiency of Heap Compaction

Resizing and Copying Heap Items

Multiple Heaps

Transferring Heap Items

 

Heap-based data structures

Data structures that are maintained in the heap are usually created by “defining words” such as ARRAY: and MATRIX:. These defining words create and initialize a parameter field in the variable area, as described in the next section. The contents of the parameter field are filled in when the data structure is dimensioned or allocated. When the data structure is dimensioned it is assigned to the heap specified by CURRENT.HEAP. At this same time the dimensioning information (e.g., number of rows and columns), the handle (which contains the base xaddress of the data) and the value of CURRENT.HEAP are saved in the parameter field associated with the data structure. Based on the information in the parameter field, the memory manager can resize, copy, or de-allocate the heap item irrespective of the current contents of the user variable CURRENT.HEAP.

For example, the word DELETED which de-allocates arrays and matrices automatically saves CURRENT.HEAP, looks at the parameter field of the item to be deleted, puts its heap specification into the user variable CURRENT.HEAP, executes TO.HEAP to de-allocate the item, and restores the original value of CURRENT.HEAP. Thus arrays and matrices in any heap can be deleted. Other array and matrix operations perform similar user-transparent manipulations of CURRENT.HEAP to make the words powerful and remove book-keeping chores from the programmer.

Because both the heap and the variable area where the parameter field is stored are in modifiable memory (RAM), the data structure can be allocated, re-dimensioned, or de-allocated “on the fly” as the program executes. This is a powerful programming capability. It is the basis for the comprehensive array and matrix package which is discussed in the next section.

 

Arrays and matrices

QED-Forth provides a set of data structures and associated operations that simplify and streamline sophisticated processing tasks. Arrays and matrices can be easily created, dynamically dimensioned, deleted, initialized, and copied. Matrices are defined as 2-dimensional arrays of floating point numbers. This chapter describes how to define and use these data structures.

 

Arrays

An array is a data structure stored in memory whose elements can be individually addressed and accessed. QED-Forth supports dynamically dimensioned arrays whose elements are stored in heap. The array can be redimensioned as needed under program control and the array’s storage space can be dynamically re-allocated. Consult the “Array” section of the Categorized Word List in the QED-Forth Glossary for a list of available routines in the array library.

Array Size and Dimensions

Creating Arrays

Addressing Array Elements

References to Entire Arrays

Operations for Entire Arrays

Determining a Pre-existing Array’s Dimensions

Internal Representation of Arrays

 

Matrices

A matrix is a two-dimensional array of floating point numbers. Matrix defining words work analogously to array defining words except that the number of dimensions (2) and element size (4 bytes) are fixed and need not be explicitly stated.

Creating, Dimensioning, and Addressing Matrices

Matrix Initialization

Matrix Display

Copying and Swapping Matrices

An Example: Transposing a Matrix

 

Structures

A “structure” is an object that groups different types of data according to a template designed by the programmer, and allows the programmer to designate names that can be used to store and retrieve the data items in the structure. While arrays hold data that is all one size and refer to the data elements using index numbers, structures let you group data of different sizes and refer to them using names of your choice. The structure defining words declare the type of the data (integer, real, address, extended address, etc.) and this enhances the clarity of the program.

Using structures involves defining them, instantiating them, and addressing their elements which are called “members”. Defining a structure can be thought of as setting up a template for the data. The template comprises a set of named offsets that specify the position of each member relative to the base of the structure. Instantiating a structure creates an instance of the structure and assigns it to a particular named memory location, either in the variable area, the definitions area of the dictionary, or in the heap. Addressing a member in a structure is accomplished by stating the name of the structure instance followed by the name of the structure member. Executing the name of the structure instance leaves its base xaddress on the stack, and then executing the name of the structure member automatically adds an offset to the base xaddress to produce the extended address of the member. Standard fetch and store operations can then be used to access the member.

Defining Structures

Creating Instances of Structures

Heap Instances

Nested Structure Definitions

Multiform Structures

Structure of an Array or Matrix Parameter Field

 

Designing re-entrant code

For a multitasking system to operate at its full potential, the kernel routines in the system must be “re-entrant”. A re-entrant routine functions properly even if it is “re-entered” while it is executing; this applies to both C and Forth language programs. Routines that are not re-entrant will fail under these circumstances. There are two contexts where this is important: recursion and multitasking.

A recursive routine is one that calls itself; to ensure “re-entrancy with respect to recursion”, a routine may modify only stack-based quantities such as data stack items and local variables (which are kept on the return stack).

Routines that modify only stack or task-private memory locations are “re-entrant with respect to multitasking”. “Task-private” locations are those that can be accessed by one task only. A user variable is a task-private memory location. Routines that modify variables that are shared by more than one task are not re-entrant with respect to multitasking. They may work properly if used by only one task, but are prone to failure if they are called by multiple tasks. A routine that is re-entrant with respect to recursion is automatically re-entrant with respect to multitasking, but the converse is not true. The rest of this discussion will focus on re-entrancy with respect to multitasking. If you are designing recursive routines, keep in mind that the rules for re-entrancy are stricter than those presented below.

 

Re-entrant and non-re-entrant definitions

The following definition (originally presented in the prior Chapter) is re-entrant. The word calculates the volume and cross-sectional area of a cylinder, and modifies only the data stack and local variables (which are maintained on the return stack):

:  CYLINDER.STATISTICS  ( r1\r2 -- r3\r4 )
\  r1 = radius, r2 = height, r3 = volume, r4 = cross-sectional area
LOCALS{ f&height f&radius | f&area }
f&radius  FDUP F*   PI  F*       \ cross-sectional area = πR**2
TO f&area
f&area f&height F*             ( -- volume = height * area )
f&area                       ( -- volume\area )
;

The following version of this word is not re-entrant:

REAL: CYLINDER.AREA  \ define a self-fetching variable to hold area
:  CYLINDER.STATISTICS   ( r1\r2 -- r3\r4 )
\  r1 = radius, r2 = height, r3 = volume, r4 = cross-sectional area
LOCALS{ f&height f&radius  }
f&radius  FDUP F*   PI  F*  \ cross-sectional area = πR**2
TO CYLINDER.AREA
CYLINDER.AREA f&height F*   ( -- volume = height * area )
CYLINDER.AREA               ( -- volume\area )
;

This second version uses a global self-fetching variable named CYLINDER.AREA. Assume that CYLINDER.STATISTICS is called by task #1, and that the multitasker’s timeslicer transfers control to task#2 just before the final statement of the definition (that is, before the final invocation of CYLINDER.AREA). If task #2 now puts a radius and height on the stack and calls CYLINDER.STATISTICS, the value in the variable CYLINDER.AREA will be changed. When control returns to task#1, the area placed on the stack will be incorrect for that task. This shows how non-re-entrant code causes errors in multitasking systems.

 

Techniques for ensuring re-entrancy

Each task has its own user area, data and return stacks, POCKET, PAD, and TIB. To ensure re-entrancy, each task that uses the heap must have its own heap area. These memory areas are called “task-private” because only one task has access to them. To ensure re-entrancy, words should modify only stack-based quantities (including local variables) and task-private memory. Non-task-private memory such as the contents of variables and self-fetching variables should not be modified if the word is to be called by more than one task.

To ensure re-entrancy, data structures such as temporary matrices that hold intermediate results within a word must reside wholly on a stack or within a task-private heap. Recall that arrays and matrices have parameter fields that hold dimensioning information and a handle to the heap memory block. The parameter field of a globally defined array or matrix is allotted in the variable area when the data structure is defined. Because a function that uses such a globally defined temporary matrix could be called from multiple tasks, a parameter field in the variable area would make the routine non-re-entrant.

 

Stack frames

Stack frames solve the problem of how to create a parameter field for a temporary array or matrix while preserving re-entrancy. The temporary array/matrix parameter field can be created and kept on the data stack as a “stack frame”, and using stack-based items preserves re-entrancy.

The kernel word STACK.FRAME allows a structure that has been defined with the structure defining words (see the previous chapter) to be instantiated on the data stack. STACK.FRAME expects a size (in bytes) of the structure to be placed on the data stack, allocates room on the stack, and returns on the top of the stack the extended address of the base of the stack frame. The word FRAME.DROP is used to remove the stack frame from the stack before the calling word finishes executing. The word PF.STACK.FRAME (“parameter-field-stack-frame”) performs the operation of STACK.FRAME and then stores 0\0 into the first 4 bytes of the stack frame in the position where the heap xhandle will reside. Initializing the xhandle to zero is good programming practice that avoids hard-to-diagnose bugs, and PF.STACK.FRAME makes this easy to accomplish when allocating stack-based parameter fields.

 

Stack frame example

For example, let’s define a re-entrant version of a word that places the transpose of a source matrix into a destination matrix, even if the specified source and destination are the same. If the source and destination matrices are the same we must dimension a temporary matrix, place the transpose into it, and then copy the transpose back to the source. In the discussion of arrays and matrices earlier in this Chapter, we defined the word MAT.TRANSPOSE which can transpose a matrix, but only if the source and the destination are different matrices. We can use MAT.TRANSPOSE to perform the actual transposition, but we’ll add the capability of dimensioning a temporary data matrix to handle the case when the source and the destination are the same.

: SMART.TRANSPOSE ( src.xpfa\dest.xpfa -- )
\ places the transpose of the source in the destination;
\ the destination can be the same as the source
LOCALS{ x&dest x&src | x&temporary }
x&dest x&src X=               ( source = destination?--)
IF                    \ if source = destination...
MATRIX.PF PF.STACK.FRAME    \ make room on stack for pf
TO x&temporary            \ save temporary xpfa
x&src x&temporary MAT.TRANSPOSE \ transpose is in temp matrix
x&dest x&temporary SWAP.MATRIX  \ now x&dest has the answer
x&temporary DELETED        \ delete the temporary matrix
MATRIX.PF FRAME.DROP        \ clear frame off data stack
ELSE                \ else if source not= dest...
x&src x&dest MAT.TRANSPOSE    \ ...just do it
ENDIF
;

The first line of the definition loads the source and destination xpfa’s into 32-bit local variables, and creates an un-initialized local variable. The next line checks if the source and destination are equal. If they are not, the ELSE part of the conditional executes, and MAT.TRANSPOSE can perform the operation directly. If the source equals the destination, however, MAT.TRANSPOSE will not work, and we have to create a temporary destination matrix. MATRIX.PF puts the size of the parameter field of the temporary matrix on the stack, and PF.STACK.FRAME allocates space for the parameter field on the data stack and initializes its heap handle to 0\0. The base xaddress of the stack frame is loaded into the local variable x&temporary, and the source and temporary matrix xpfa’s are passed to MAT.TRANSPOSE, which dimensions the temporary matrix and transposes the source into it. Next, SWAP.MATRIX interchanges the contents of the temporary and destination parameter fields so that x&dest is the transposed matrix and x&temporary points to the original matrix. Next the temporary matrix is deleted from the heap, and FRAME.DROP clears the temporary parameter field off the data stack. The use of local variables and stack frames renders the SMART.TRANSPOSE routine re-entrant so it can be used in multitasking applications.

 
This page is about: Forth Program Development Techniques, Using Interactive Debugging Tools in Forth, Floating Point Mathematics in Forth, Using Forth Heap Manager, Forth Arrays and Matrices, Using Structures in Forth, Designing Re-entrant Code in Forth – Explains features that make it easy to program in QED-Forth, including the interactive debugger, floating point mathematics, heap memory manager, arrays and matrices, structures, and the multitasking executive.
 
 
Navigation