Building a Remote Target Compiler
Saved from http://www.nm.net/~dtaliafe/bldgtcom.htm
by Dave Taliaferro
At long last we reach the apex in this three-part expose that rips the lid off the obscure yet powerful embedded development technique known as remote target compilation.
In the first article we observed the CREATE DOES> mechanism for implementing defining words which can generate classes of child words with identical run-time behavior and variable data attributes. In the second article, it was seen that defining words can easily spawn data compiling child words, and when these words are fed into the Forth interpretation stream a custom assembler or compiler emerges in a few lines of code.
remote Forth target compiler runs on an embedded development host machine and allows interactive production of executable Forth and assembler routines to a target microprocessor. As keyboard definitions or source files are interpreted by the host Forth, the resulting code and data is transparently uploaded to the target for immediate testing and further programming.
In this article I will demonstrate how a remote target compiler can be built using defining words, the Forth interpreter, and a few other nifty tricks. Since we have already covered the necessary groundwork, this piece will first explore the basic elements of remote Forth target compilation, then describe a 56002 DSP target compiler I wrote using a handful of Forth concepts that collectively do something amazing - interactively compile and execute code from one architecture to another. These ideas are merely an extension to the the last article, „Easy Target Compilation“.
Because I used a standard macro assembler to write the 56k Forth, my target compiler had some interesting twists to true Forth target compilation, which uses a Forth assembler to generate the target nucleus. In addition to producing a target Forth kernel, the macro assembler provides a means for the host Forth to „clone“ the target Forth. Using a listing of the target Forth code addresses generated during the nucleus assembly process, a new dictionary is created on the host that enables the interactive development of Forth and assembler routines on the target.
This technique makes remote target compiler construction accessable to the pedestrian Forth programmer. Public domain Forths exist in assembly form for most processors; coupled with any host Forth on their favored operating system, the development environment described here can be easily duplicated.
Some Basic Concepts of Remote Forth Target Compilation
A „resident“ Forth contains the Forth interpreter and compiler entirely on the native processor that the Forth is running on. In the original eForth for the 56002, the target contained an interpreter, compiler, and name dictionary, in addition to the target executable code dictionary. This consumed about 9K processor words in the 56002. The goal of the remote target compiler concept is to move the Forth interpretation and execution tasks from the target microprocessor to the host computer. This leaves the minimal executable target Forth system needed to create and execute new applications interactively, and to act as an operating system for applications when the host computer is removed.
In other words, the embedded target includes a run-time interpreter to execute the application Forth source routines but does not include the interactive Forth interpreter/compiler. The interactive development occurs on the host machine; a run-time interpreter and the Forth kernel words on the target communicate with the host during development and allow for immediate testing of routines. When development is complete a binary image is produced for romming or loading on the target.
This opens up interesting possibilities for embedded development, for now the target system's behavior can be controlled by ascii text scripts interpreted on the host Forth. Both host and target code execution and compilation can occur within the same source code scripts.
Forth exhibits meta compilation, the ability to reproduce itself on the machine it is running on or to another computing system. For a Forth to regenerate itself to a different architecture, a target assembler implementing the target's instruction set is written (see last article, „Easy Target Compilation“), which is used to construct a Forth virtual machine for the target, which is itself used to build a high-level Forth model. This compilation process generally takes place into the host Forth's memory, producing an image of the target Forth that can be loaded into the target memory for execution.
In the 56002 target compiler, meta compilation is used to „clone“ the target nucleus, which already exists in the form of the eForth macro assembly source code. The tricky task of cross meta compiling the host Forth to a target architecture is eliminated. The assembler source provides the addresses for all the words in the target Forth's dictionary, which are used to create a target clone dictionary on the host. ( This new target dictionary may be identical to the host's own Forth dictionary; depending on the state of target compilation, either dictionary may have the correct homograph. )
Meta compilation becomes simple, because now we merely use CREATE DOES> to construct a defining word that creates host clones for each target word, and then use [ to interpret those words. With the addition of some serial communication routines and the embedded Forth virtual machine, remote target compilation is easily achieved.
A minimal target Forth system would contain the core primitives (about 32 in eForth), and whatever additional words needed to support the application. In a well-tuned application this could amount to a few hundred bytes of code. Each assembly primitive consists of a few lines of machine code that implement some operation in the Forth virtual machine, such as pushing a data item to the stack or transmitting a byte out the serial interface.
One important primitive provides a threading mechanism, called the „inner interpreter“, that executes the list of addresses that comprises a high level Forth definition. This primitive can be called by through a communication routine to remotely execute target Forth definitions.
Thus the component parts of a generic remote target compiler include:
- A target Forth nucleus consisting of the inner interpreter and core primitives.
- A „host“ Forth for target program development and file management.
- The host forth target compiler - a program running on the host that interactively compiles executable code into the target.
- Host and target communication routines; how one Forth talks to another.
Figure 1 gives a celestial representation to the remote target compiler development system universe.
Simple Example:A Remote Forth Target Compiler for the 56002 DSP
The target Forth nucleus - 56002 eForth
The Motorola 56002 is a 24-bit „digital signal processor “(DSP) with a Harvard architecture that can execute memory operation in parallel. The multiple address registers and three memory spaces allow it to operate on time sequences of input samples, the basis of digital filtering and signal processing.
I ported eForth, by C.H. Ting, to the 56002 by converting the MASM 8086 source to Motorola's freeware 56k macro assembler. Since eForth, and its descendant, hForth, are designed for porting, the task was pretty straightforward. Implementations for a number of microprocessors exist in the public domain.
At reset, a microprocessor fetches its starting code location from a specific hardware address, called the reset vector. It then jumps to that location and begins executing whatever instructions are there. From this raw beginning the system's hardware configuration is set up, after which the application code is started. On even the simplest architecture we can breathe life into silicon by having a Forth virtual machine awaiting the execution path of the microprocessor after hardware initialization. For 56002 eForth, this consists of the inner interpreter, called doLIST, and the core primitives that implement the stack-based virtual computer.
In the eForth assembly source code, macros are used to create a linked list of the Forth dictionary. The macros package each definition with a sequence of interpreter instructions that keep the chain of threaded execution resolved. These same macros also define the address for each word in the Forth dictionary, and links to the next and previous word.
The address of an executable Forth word (routine) is called the word's code address.
A high-level Forth word in memory is basically a list of data and/or the code addresses of other Forth words that make up the definition.
The code address, the first address in this list, contains a jump to the Forth inner interpreter, doLIST, the mechanism for executing the list of data and code addresses that make up the word being executed.
To boot eForth on an embedded micro, the first instructions after hardware initialization set up the Forth vm by initializing the Forth stack pointer, return pointer, and the inner interpreter pointer. To initiate the Forth machine, the code address of a high level Forth word is copied into the interpreter pointer register. The code then jumps to the location pointed to by the register, which for every high level, or „colon“, Forth word, is a jump to doLIST.
For an embedded application the first word may be a control program of some sort. In a resident eForth, the first Forth instruction is COLD, which initializes the stack, the TIB, and the terminal I/O, then enters QUIT<, the familiar interactive Forth interface, also known as the „outer interpreter“.
To use the 56k nucleus in a target compiler system, the name dictionary was removed by modifying the assembly macros. In the target compiler, we don't need it since name interpretation takes place on the host. Only the code addresses are necessary. Most of the words that make up the high-level Forth model were also removed, leaving the primitives and math words. With the addition of simple communication routines the eForth nucleus becomes fully usable for remote target development.
The hForth host
The host Forth system used for this project is hForth 86, by Wonyong Koh of South Korea. This is a public-domain ANSI compliant Forth that runs under MS-DOS. The kernel can be adapted to non-DOS x86 systems and other micros as well. hForth is a derivative of eForth 86, which was the model used for eForth 56 - the target kernel.
Any decent Forth can be used since the target and host do not have to be the same model. For the 68HC11, Pygmy would be a good candidate to try this out on, since the registered version comes with a metacompiler, a 68HC11 Forth assembler, and serial i/o routines.
The host Forth remote target compiler - HFTCOM56
When the 56k target Forth nucleus is assembled through the Motorola 56000 DOS assembler, the assembly macros write the ascii names for the Forth words along with their code addresses to a text file.
[56ke4th.asm line 139]: doLIST code address: A0Fh [56ke4th.asm line 165]: next code address: A16h [56ke4th.asm line 187]: ?branch code address: A28h ...etc.
Thus we have a listing of the 200+ words that make up the operating system/language known as Forth, as well as the target system code addresses for each word.
Send in the clones...
Using the listing from the target56.asm assembly, a Forth compiler can be produced by parsing each line for a name and code address. A host Forth „clone“ definition is created with the same name as the target word, and contains the code address in the host definition. Listing 1 shows the the essential target compiler.
The target compiler reads the listing file and creates an executable host „clone“ of each embedded target nucleus word. Observe the defining word TCLONE in listing 1. When this word is executed it creates a host Forth word with a parameter field value that is either a target word code address, or the address for the target HERE - the next available dictionary location in the target. The run-time behavior of the new child word is defined by TCOMPILE? :
- if the host is in target compile mode : the clone word compiles its code address into the host memory target image (where a new word is being compiled)
- if the host is in remote mode : the clone word transmits its code address to the target for remote execution
- if the host is in host mode : the clone word pushes its code address onto the host stack
The host Forth system has also been modified so that stack items entered into the interpreter are either compiled into the host target image or pushed onto the target stack, depending on the state of the remote flag.
To clone the target Forth dictionary from the assembly listing file, each line of the file is parsed into a buffer, to which the string TCLONE is appended. The buffer is passed to EVALUATE, which performs the normal Forth input stream interpretation.
We now have one leg of the target compiler - a means through TCLONE to create executable clones of the target nucleus. The next step is to devise a means to create new target routines using the existing nucleus clones and data in Forth definitions. We again use the defining word TCLONE, and the Forth interpreter to create a target compiler for the target architecture.
Host Forth interpretive "umbilical compiling" to the target
To create new target Forth definitions interactively, which is a foundation of the Forth paradigm, we can co-opt the host Forth interpreter and compiler to generate executable code for the target, even if it has a different architecture.
When a new target definition is created in the host Forth it is „umbilical compiled“ to the target transparently. This means that the list of target Forth code addresses or assembly object code that comprises the new definition is linked into the target memory as soon as the definition has been correctly entered. The code is now a part of the target Forth operating system.
T: and T; from Listing 1 define the rest of the remote target compiler. To create a new target word, the user enters T: (pronounced „t colon“) and the desired name for the new word, its definition, and T; ( „t semi“ ), which terminates the compilation sequence.
T: MYWORD TARGETWORD1 ....TARGETWORDn ...T;
We could have named these routines „:“ and „;“ , since T: and T; reside in a seperate dictionary than the Forth dictionary.
A target definition buffer is created in host memory with a Motorola .lod file header and the jmp doLIST opcode. As the programmer types existing target word names into the host terminal interface they are executed by the host. On execution, they compile their code addresses into the definition buffer.
When the programmer ends his definition, (by typing T; ) another .lod directive is appended to the host target definition image. The image is then transmitted to the embedded target and becomes part of the target Forth.
A „clone“ of the newly defined word is also created at this time, and if the host Forth is not in the target compiling state, the word can be executed on the embedded target by typing its name into the host Forth terminal interface.
As more definitions are added, the compiled code is appended to the image buffer. This buffer can be transmitted to the target or saved to a DOS .lod file for later use.
On reset the target retains the new definitions - this is useful in case the programmer locks up the target. With some additional programming, new definitions could be written into target flash ram (as on the 56002 EVM) - allowing the target to power-on autostart the new Forth application.
How the target compiler works :
Here again is the essential target compiler :
: TCLONE CREATE , DOES> TCOMPILE? ; : T; 'TEXIT @ EXECUTE !TCOMPILE DEF>T ; : T: LOCATE! THERE? TCOMPILE TCLONE JMPDOLIST, [ ] ;
When T: initiates a new target definition, LOCATE! sets the host's target buffer pointers. THERE? fetches the next available target code address and pushes it on the host stack. TCOMPILE sets the host to target compile mode.
When the code address and name string (MYWORD in the example above) are passed to TCLONE, a new host Forth definition is created whose run-time behavior (TCOMPILE?) is to transmit its code address to the target, compile it into the host target definition buffer, or push it to the host Forth stack.
At this point we begin compiling the new target word's definition - the assembly object code that executes on the target. This high-level Forth object code is composed of addresses of other target words or literals (embedded numeric values), arranged as a list to be executed by doLIST, the Forth inner interpreter. JMPDOLIST, compiles a jump to doLIST into the code address for each new word.
[ in the T: definition puts Forth in interpretation state, that is, it parses the blank delimited words from the input stream and executes them.
When each clone word entered in a new target definition executes, it compiles its code address into the target definition buffer. That's all there is to it.
The compilation is actually finished before the user types in T; - notice that when the user hits CR after entering in a sequence of literals or previously defined Forth words the system is returned to compile mode by the word ] (rbrack). T; thus appends a code address for EXIT, the target Forth primitive that ends a colon definition - the compliment to jmp doLIST. !TCOMPILE turns off target compilation so that DEF>T can transmit the new definition to the target.
Now you may be wondering how „literals“, numeric data entered in definitions, get compiled into target code. I wondered myself, as the project deadline loomed. The host Forth outer interpreter would have to be changed so that it would understand when to target compile numbers. For instance, a definition containing hex numbers :
/ strobe the pins on port B T: STROBE-PORTB 0 PBD C! FF PBD C ! 0 PBD C! T;
The problem is that normally Forth would see the numbers, 0h and FFh, and push them on the host stack, when we need them compiled into the target. This level of Forth programming seemed beyond my reach, but luckily I had chosen hForth, which offered a simple solution (fortuitously described in the distribution readme file).
When Forth encounters a token during interpretation, it first searches the dictionary to find a word match, and on failure tries to convert it to a number. In hForth, the interpretation number conversion routines are vectored in a table. To add new number types or change Forth's behavior with numeric input, one merely has to write the handling routine and store its address in the interpretation vector table, called 'doWord.
Here is a definition that causes hForth to target compile literals :
: targetAlso (doubleAlso) \ the normal numeric interpretation routine TSTATE IF \ if we are in target compile mode... 'doLIT @ EXECUTE \ compile target doLIT \ doubleAlso leaves 1 on stack if number is a single 1 = IF T, \ if single, target compile it ELSE \ if double, target compile it WHERE? ROT ROT (d.) T_, THEN EXIT THEN REMOTE IF \ if we are in remote mode... 1 = IF SPUSH EOT TX \ if single, push on target stack ELSE (d.) DPUSH EOT TX \ if double, push on target stack THEN EXIT THEN DROP ;
In a target compiled definition, a literal must end up on the target stack when the routine is being executed by the target kernel. doLIT is a target word that extracts a literal from the memory location following doLIT and pushes it on the Forth stack.
To store the address of targetAlso in the interpretation table, we simply enter :
' targetAlso 'doWord 3 CELLS + !
How the newly compiled word is remotely executed on the target :
At the beginning of a colon word is a jump to doLIST, which processes the word by threading through the addresses (other Forth words) in the definition. If the host is not in target compiling mode when a target clone is executed, the clone's code address is transmitted to the target instead of getting compiled into a new definition. The target system has a small monitor routine, affectionately called the „mini-interpreter“, which picks up this code address and passes it to doLIST for execution. Since the mini-interpreter is the first address in the threading chain, it is the point that execution returns to when the Forth program (starting from the code address, a high level word) is finished.
Communication between host and target
Target to host communication interface
The embedded target, in addition to containing the executable Forth nucleus words and the inner interpreter mechanism, has a small assembly level routine (the mini-interpreter) for communicating with the host computer. This routine recognizes two commands :
- push a 24 bit value (data or address) to the target Forth stack
- execute the target Forth word code address which is on the target Forth stack
This is all that is required for complete host/target communication and control, since the routines for program loading, character I/O, etc. are all part of the target Forth nucleus, and are executed by placing their code address on the stack and remotely executing them through the mini-interpreter.
Listing 2 is the the mini-interpreter assembly source code.
The mini-interpreter employs a jump table to retrieve the command vector to execute, and recognizes single-byte ASCII commands in the range 10h - 1Fh. This allows for dumb terminal testing of the interface. Currently, three commands are in the table with room for 13 more. Character I/O is vectored to remove dependence on the serial port for host/target control. The commands :
- ^T push data to stack (must be followed by 04h - EOT)
- ^U execute address on stack
To insert a new command, the address of the desired subroutine is written into the table. Pressing the corresponding terminal key will then execute the function. Usually, the kernel code would be re-assembled, although it is possible to modify the interpreter while it is RAM.
Host to target communication interface
On the host side are the Forth routines to communicate with the target.
Given the two commands required by the target, the host Forth can be modified to transmit code addresses and stack elements to the target. The host can emulate target routines by having the host „clones“ transmit their code addresses to the target stack and executing them.
Here is the Forth source code for pushing data to the target and executing target routines. TX and RX are the PC serial port character transmit and fetch routines. EOT< signals an end of transmission to the target mini-interpreter.
\send an ascii numeric string to target \( addr count -- ) : TYPE>T ?DUP IF 0 DO DUP C@ TX CHAR+ LOOP THEN DROP ; \send a double to target (up to 24 bits) : D>T (d.) SPILL TYPE>T ; \send an ascii string to target : $>T SPILL COUNT TYPE>T ; \send a single to target (up to 16 bits) : S>T S>D D>T ; \push a 16 bit single number to the target stack : SPUSH ^T TX S>T EOT TX ; \push a 24 bit double number to the target stack : DPUSH ^T TX D>T EOT TX ; \execute word on target stack : TEXEC ^U TX ;
From these primitives the rest of the host-target interaction is established.
A real-time linker?
I didn't have time to write a Forth assembler for the 56k, but wanted to demonstrate that the system could interactively target compile assembly object code. To do this, I came up with a host word, :ASM56 that shells from hForth to DOS, where the assembly module is written and assembled, and then reads the resulting object code file into hForth and links it as a Forth word into the target.
To create an assembly language routine that integrates directly into the target kernel, the user defines a new Forth word with the desired name (ex. :ASM56 FIRBLK1.ASM ).
On entering the new definition name, the host Forth :
- Fetches the current target code pointer.
- Opens an assembler template file containing macros and hardware equates pertinent to the target system memory map.
- Copies the template file, and the current target code pointer to a file named in the definition (ex. FIRBLK1.ASM)
- Shells to DOS.
At this point any DOS editor can be used to add code into the new file, which has the necessary pointers for the assembler to locate the code correctly. After the code has been edited, assembled, and converted to a COFF format file (.lod file) without errors, the user exits back to the host Forth (type c:>exit).
When the host Forth is returned, it „clones“ the assembly object code into the host target dictionary, and transmits the .lod file to the target. The new executable assembly object code exists on the target, and can be executed by typing its name into the host Forth interpreter while it is in REMOTE mode. It can also be used with other target Forth words in a new T: definition.
This entire process is transparent to the user, with the exception of the editing/assembling loop. :ASM56 thus performs the duty of a real-time linker by allowing new assembly object code to be located into the target Forth operating system. Here is a screen dump, with additional comments, of the shell process…
:ASM56 FILTER1.ASM Now edit your new assembly source file : FILTER1.ASM ...and run it thru asm56000.exe... press a key to shell to dos \ at this point edit the file filter1.asm, assemble, and convert to a .lod file c:> code filter1 <I>\ this batch file runs filter1.asm thru the assembler</I> c:> fix filter1 <I>\ this runs the strip and lod utilities</I> c:> exit <I>\ exit DOS to hForth...</I> Target word name : FILTER1 .ASM file : FILTER1.ASM .LOD file : FILTER1.LOD \ FILTER1 is now part of the target vocabulary and can be executed by entering \ its name into hForth WORDS FILTER1 CTOP COLD :CODE RESET GETLINK SETLINK 'BOOT WARMhi hi ?CSP !CSP t.S tDUMP X Y P PMEM YMEM XMEM ~ t. U. U.R .R PACE EMIT KEY ?KEY D2F STR2FRAC FRACTABL DECPNT tDECIMAL tHEX FILL MOVE CMOVE @EXECUTE HERE