1 Introduction
2 The Monitor
3 Incremental development
4 Commands and Target Control
Version: 4.0.2.5

Staapl’s PIC18 Forth

1 Introduction

This tutorial document is an introduction to interactive use of the PIC18 Forth dialect included in the Staapl distribution. It is assumed you have a general idea of what Forth is about. The PIC18 Forth is a non-standard dialect. Its macro language and the connection with Scheme are very different. However, the syntax and the non-reflective semantics are quite similar to the standard.

This document is structured as a transcript of a live session to give you an honest view of the work flow. All code fragments are evaluated during the generation of this document, and uploaded to the PIC18F1220 chip. No cheats!

The first part illustrates how to compile and upload a complete application. On top of this traditional workflow, it is possible to construct an interactive and incremental development style, where part of the application is kept constant while another part is incrementally updated or replaced with the target system running. The base application which serves as an example here is actually the interactive boot monitor interpreter which will later support the incremental development.

To follow the tutorial, you need a PIC18F1220/1320 microcontroller connected to a serial port. See here for a schematic. The essential parts are the ICD2 connector, the serial port connector, the reset switch, and the serial port pulldown resistor. To burn the initial interactive monitor code you also need a PIC programmer that can read Intel HEX files. In this tutorial Microchip’s ICD2 programmer is used, together with the command-line program piklab-prog from Piklab. Staapl is part of PLaneT, so you only need to install PLT Scheme or the command-line tool MzScheme from http://www.plt-scheme.org.

To get going, start MzScheme using the command mzscheme. Then instantiate the interactive compiler namespace at the Scheme prompt.

  > (require (planet zwizwa/staapl/prj/pic18))

  > (init-prj)

  ;; scat

  ;; target

  ;; coma

  ;; (macro) jump

  ;; control

  ;; (macro) exit

  ;; (macro) sym

  ;; (macro) label:

  ;; comp

  ;; asm

  ;; forth

  ;; live

  ;; (macro) +

  ;; (macro) ,

  ;; (macro) dup

  ;; (macro) drop

  ;; (macro) swap

  ;; (macro) or-jump

  ;; (macro) not

  ;; (macro) then

  ;; purrr

  ;; pic18

This loads the different components from Staapl and forces initialization of a project. The first time you enter the require command, the distribution will be downloaded, compiled and locally cached. The output gives some idea of Staapl’s structure. The PIC18 compiler is built in layers that extend the functionality of lower layers and specializes some macros. scat is the functional stack language serving as a representation layer. coma is a layer of target code transformers on top of that. control implements Forth style conditional branching and structured programming. comp is a compiler that performs simple control-flow analysis. asm is the assembler. forth provides concrete Forth syntax. live is the interaction code. purrr is a link layer between Coma semantics and Forth syntax and pic18 implements the Microchip PIC18 code generator. A line like ;; (macro) dup means the word dup from the (macro) namespace has been redefined. The internal Scheme code is structured in a bottom-up manner, but macros are allowed to be backpatched, usually to introduce machine-specific optimizations.

The simplest way to evaluate code in the compiler namespace is to pass a string to forth-compile or a filename to forth-load/compile. Unless otherwise indicated, code that occurs in an isolated paragraph like this:

    : foo 123 ;    

    : bar foo 1 + ;

is passed verbatim to the compiler as a string using forth-compile. The accumulated output assembly code can be viewed using

  > (print-code)

  foo:

   0000 6EEC [dup]

   0002 0C7B [retlw 123]

  

  bar:

   0004 DFFD [jsr 0 foo]

   0006 0F01 [addlw 1]

   0008 0012 [return 0]

  

To save collected binary code to an Intel HEX file use

  > (save-ihex "/tmp/example.hex")

To get rid of code without saving or uploading use

  > (kill-code!)

2 The Monitor

Forth traditionally has two modes: compile mode and interpret mode. We’ve just met compile mode. In Staapl, interpret mode is called command mode, because it’s a bit more general than interpretating of on-target Forth words.

We’ll load the code, save the binary as a HEX, the dictionary as a Scheme data file and upload the binary code to the target.

  > (forth-load/compile "monitor-p18f1220.f")

  ;; (macro) TOSH

  ;; (macro) TOSL

  ;; (macro) TBLPTRH

  ;; (macro) TBLPTRL

  ;; (macro) TABLAT

  ;; (macro) PRODH

  ;; (macro) PRODL

  ;; (macro) INDF0

  ;; (macro) POSTINC0

  ;; (macro) POSTDEC0

  ;; (macro) PREINC0

  ;; (macro) PLUSW0

  ;; (macro) FSR0H

  ;; (macro) FSR0L

  ;; (macro) WREG

  ;; (macro) INDF1

  ;; (macro) POSTINC1

  ;; (macro) POSTDEC1

  ;; (macro) PREINC1

  ;; (macro) PLUSW1

  ;; (macro) FSR1H

  ;; (macro) FSR1L

  ;; (macro) INDF2

  ;; (macro) POSTINC2

  ;; (macro) POSTDEC2

  ;; (macro) PREINC2

  ;; (macro) PLUSW2

  ;; (macro) FSR2H

  ;; (macro) FSR2L

  ;; (macro) STATUS

  ;; (macro) Z

  ;; (macro) C

  ;; (macro) f->

  ;; (macro) ~@

  ;; (macro) ~!

  > (save-ihex "/tmp/monitor.hex")

Before saving the metadata, we set the console port and baudrate parameters so they can be included.

  > (current-console '("/dev/ttyUSB0" 9600))

  > (save-dict "/tmp/monitor.dict")

If you don’t have the ICD2 programmer hardware or the piklab-prog program installed, upload the HEX file to the PIC manually and start the chip. Otherwise, the prog command can be used:

  > (prog "/tmp/monitor.hex")

  piklab-prog -p icd2 --quiet -t usb --target-self-powered false --firmware-dir /home/tom/firmware/icd2 -d 18F1220 -c program /tmp/monitor.hex

  piklab-prog: version 0.15.2 (rev. distribution)

  programmer: icd2

  device: 18F1220

  port: usb

  firmware-dir: /home/tom/firmware/icd2/

  target-self-powered: false

  Connecting ICD2 Programmer on USB Port with device 18F1220...

  Firmware version is 2.6.8

    Set target self powered: false

  Connected.

  Read id: 18F1220 (rev. 7)

  Programming device memory...

    Erasing device

    Write memory: Code memory

    Verify memory: Code memory

    Write memory: Data EEPROM

    Verify memory: Data EEPROM

    Write memory: User IDs

    Verify memory: User IDs

    Write memory: Configuration Bits

    Verify memory: Configuration Bits

  Programming successful.

  piklab-prog -p icd2 --quiet -t usb --target-self-powered false --firmware-dir /home/tom/firmware/icd2 -d 18F1220 -c run

  piklab-prog: version 0.15.2 (rev. distribution)

  programmer: icd2

  device: 18F1220

  port: usb

  firmware-dir: /home/tom/firmware/icd2/

  target-self-powered: false

  Connecting ICD2 Programmer on USB Port with device 18F1220...

  Firmware version is 2.6.8

    Set target self powered: false

  Connected.

  Read id: 18F1220 (rev. 7)

  Run...

Now it’s possible to send commands to the PIC using the forth-command function. The ping command will query the chip for its identification string.

  > (forth-command "ping")

  pic18

3 Incremental development

A convenient way to write a Forth application on a microcontroller with flash memory is to split it in two parts: one part is kept constant: generated as a HEX file and uploaded once, while the other part can be modified dynamically with the chip running. In our case the constant part is just the interpreter that enables run-time interaction.

The data written out by save-dict is the metadata necessary to interact with the programmed target. For maximum flexibility, it’s implemented as a Scheme script that can contain arbitrary code, so be careful with trusting dictionary files from others! This is what the contents looks like.

  > (prj-eval '(write-dict))

  (require (planet zwizwa/staapl/prj/pic18))

  (init-prj)

  (words!

   '((hello code 232)

     (init-all-but-rs code 222)

     (_route code 216)

     (program code 207)

     (erase code 204)

     (engage code 194)

     (~! code 190)

     (~@ code 186)

     (chkblk code 159)

     (ldf code 152)

     (lda code 145)

     (preply code 140)

     (n@a+ code 109)

     (n@f+ code 98)

     (interpreter code 95)

     (receive/ack code 91)

     (fprog code 69)

     (ferase code 65)

     (ack code 62)

     (jsr code 54)

     (async.>tx code 48)

     (async.rx> code 33)

     (boot-40 code 32)

     (boot-isr-high code 4)

     (boot code 0)

     (isr-low code 268)

     (isr-high code 260)

     (application code 256)

     (access>a code 179)

     (foo code 0)

     (warm code 236)

     (_route/e code 213)

     (bar code 2)

     (f-> code 171)

     (n!a+ code 130)

     (n!f+ code 119)

     (fsend code 99)

     (jsr/ack code 93)

     (interpret code 73)

     (boot-isr-low code 12)))

  (console! '("/dev/ttyUSB0" 9600))

  (pointers! '((code 288) (data 0)))

The script starts with a require statement that loads the compiler code and a command that initializes a clean compiler namespace. The words! command initialize the target dictionary which maps symbolic words to word addresses. Further the script initializes the console the project was connected to and the memory allocation pointers indicating the free memory region usable for incremental development.

To use the dictionary file generated in the previous section, start MzScheme as mzscheme -if /tmp/monitor.dict, or load it at the prompt:

  > (load "/tmp/monitor.dict")

  ;; scat

  ;; target

  ;; coma

  ;; (macro) jump

  ;; control

  ;; (macro) exit

  ;; (macro) sym

  ;; (macro) label:

  ;; comp

  ;; asm

  ;; forth

  ;; live

  ;; (macro) +

  ;; (macro) ,

  ;; (macro) dup

  ;; (macro) drop

  ;; (macro) swap

  ;; (macro) or-jump

  ;; (macro) not

  ;; (macro) then

  ;; purrr

  ;; pic18

Note that loading a dictionary file will create a new project namespace, and will discard the one that is active if it is not saved anywhere. At this point, the chip is in an undefined run-time state and it can still contain leftovers from previous sessions. To reset the chip do

  > (forth-command "cold")

to clean the flash leaving only the base application do

  > (forth-command "empty")

  

The numbers printed (if any) are the blocks that are erased. This will bring the state back to where it was after the dictionary file got saved to disk. Instead of using forth-command all the time, we define a shortcut.

  > (define ~ forth-command)

  > (~ "ping")

  pic18

Note that in practice it might be more convenient to start a REPL that reads lines and evaluates them using forth-command. This can be done using (repl forth-command). We’ll see later that for normal development, the Scheme interface isn’t really necessary since most operations are also available from the command mode. This can be easily wrapped in a shell script

#!/bin/sh

[ -z "$1" ] && echo "usage: $0 <dict-file>" && exit 1

exec mzscheme -f $1 -e '(repl forth-command)'

Alternatively, it is possible to use the Snot package in Emacs.

Let’s try to upload some code. Remember that code that appears in paragraphs as below will be passed to forth-compile. Alternatively, it can be placed in a text file and loaded using forth-load/compile.

    : increment 1 + ;

This defines a Forth word increment which takes a number from the stack, adds 1 and places the result back on the stack. The machine code for this word is

  > (~ "print-code")

  increment:

   0240 0F01 [addlw 1]

   0242 0012 [return 0]

  

The remaining task is to transfer the machine code to the target. A dot will be displayed for each uploaded flash unit. For PIC18 this is 8 bytes (4 instructions).

  > (~ "commit")

  .

To test the word, we place a number on the stack, print the stack, execute the word, and print the stack again.

  > (~ "123 ts increment ts")

  <1> 123

  <1> 124

For common operations, if a certain word is not present as instantiated code on the target chip, the behaviour will be simulated. This is indicated in the output.

  > (~ "10 3 + ts")

  (sim)

  <2> 124 13

Note that in the interactive example, the + was simulated because there is no callable code that implements the word in isolation. However, there is code that impelements the operation inlined in the definition of the increment word. This is generally so: the target starts out with no code at all, which means all words are necessarily macros. They can exist only at compile time. This is also the reason of existence for the simulation mode: because so many words are macros, initially, no interaction would be possible without simulation. This approach allows for the creation of very small applications on tiny chips.

It is possible to define new macros in Forth code.

    macro    

    : add-ten 10 + ;    

    forth    

    : add-twenty add-ten add-ten ;

There are two important things to note here. Different from standard Forth, in the Staapl Forth dialect macro words can be defined with (almost) the same syntax as forth words. The latter will be instantiated as real code on the target, while the former are used only as code generators, which means they will be inlined and optimized. For example, one of the addition inside the add-twenty word can be performed at compile time and so the result will be only a single addition.

  > (~ "print-code")

  add-twenty:

   0244 0F14 [addlw (10 10 +)]

   0246 0012 [return 0]

  

Now flush out the code and test it.

  > (~ "commit 100 add-twenty ts")

  .<3> 124 13 120

To inspect the target machine, several commands are available. Words and locations can be disassembled using

  > (~ "see boot-40")

  <anonymous>:

   0040 D0CB [bra warm]

   0042 BA9E [btfsp 1 158 5 0]

   0044 D001 [bra 36]

   0046 D7FD [bra async.rx>]

   0048 A2AB [btfsp 0 171 1 0]

   004A D003 [bra 41]

   004C 98AB [bpf 1 171 4 0]

   004E 88AB [bpf 0 171 4 0]

   0050 D7F8 [bra async.rx>]

   0052 6EEC [movwf 236 0]

   0054 50AE [movf 174 0 0]

   0056 A4AB [btfsp 0 171 2 0]

   0058 D002 [bra 47]

   005A 50ED [movf 237 0 0]

   005C D7F2 [bra async.rx>]

   005E 0012 [return 0]

   0060 B2AC [btfsp 1 172 1 0]

   0062 D001 [bra 51]

   0064 D7FD [bra async.>tx]

   0066 6EAD [movwf 173 0]

   0068 50ED [movf 237 0 0]

   006A 0012 [return 0]

   006C 0005 [push]

   006E DFE9 [rcall async.rx>]

   0070 6EFD [movwf 253 0]

   0072 50ED [movf 237 0 0]

   0074 DFE6 [rcall async.rx>]

   0076 6EFE [movwf 254 0]

   0078 50ED [movf 237 0 0]

   007A 0012 [return 0]

   007C 6EEC [movwf 236 0]

   007E 0E00 [movlw 0]

The symbolic name can be replaced by a numeric address. The assembly language consists of mostly the Microchip PIC18 opcodes, with some minor variations to simplify the compiler. To inspect flash memory code blocks use the Flash Block Dump commands. It prints flash memory in erase units, which is 64 bytes for the PIC18.

  > (~ "0 fbd")

  D01F FFFF FFFF FFFF

  D0FF FFFF FFFF FFFF

  FFFF FFFF FFFF FFFF

  D0FF FFFF FFFF FFFF

  FFFF FFFF FFFF FFFF

  FFFF FFFF FFFF FFFF

  FFFF FFFF FFFF FFFF

  FFFF FFFF FFFF FFFF

For RAM memory, use the Array Block Dump command

  > (~ "0 abd")

  00 02 01 00 01 C8 00 00

  0E 38 C2 00 04 44 20 82

  40 00 29 00 90 00 A0 00

  00 00 90 04 00 44 50 00

  10 82 42 00 40 80 22 88

  80 40 0B 40 18 10 2C 44

  06 00 84 90 50 26 86 40

  00 06 40 80 60 00 00 10

To print a birds-eye view of the first couple of kilobytes of flash memory in flash erase block units use

  > (~ "4 kb")

  x x x x x x x x

  . x . . . . . .

  . . . . . . . .

  . . . . . . . .

  . . . . . . . .

  . . . . . . . .

  . . . . . . . .

  . . . . . . . .

The first line here is the boot monitor code. On the second line resides the code we just uploaded. The first block in the second line is the (currently empty) application boot and interrupt vector block, redirected from the first block.

These last commands give an indication of the general idea behind the Forth command mode. The code present on the target is kept minimal, and all the functionality you would normally expect from a Forth is simulated by the host. This is then provided in a simple target view console interface.

While empty will usually erase the scratch memory properly, due to target crashes or bad communication links it might sometimes fail. Erasing individual blocks can be done using

  > (~ "9 erase-block")

A sequence of dirty blocks can be erased similarly using the erase-from-block command, which is what empty uses too. Note that when erasing target flash blocks, the dictionary still contains references to the now absent code. This is usually not a problem when re-loading the same file with slight changes, which overwrites bindings in the namespace, but it can be problematic when loading different files. In case of confusion, just load the dictionary from afresh and empty the code.

Finally, we arrive at the the edit-compile-run loop. To work on an application relative to fixed code, put all the code in a single file (possibly with nested load statements), and use the command ul <filename>. This will empty the previously uploaded incremental code and upload the freshly compiled contents of the file.

4 Commands and Target Control

Before we continue, we leave the project management namespace and go into the compiler namespace directly.

  > (enter-prj)

To leave again use (leave-prj). Note that it is also possible to load the compiler straight into the toplevel namespace and get rid of the project management altogether, i.e. using (require (planet zwizwa/staapl/pic18)).

To create new new interaction commands in the (target) namespace, use the Scheme subsititutions macro. For example, this is the definition of ul. The symbol file in the left hand side of the substitution rule is a formal parameter that is replaced in the right hand side.

  > (substitutions (target)

      ((ul file) (empty mark load file commit)))

Essentially, target commands are words that override the default target interaction semantics, which is to execute the code indicated by a symbolic word. They do this by influencing the semantics of subsequent words, and are thus essentially prefix words. The normal workflow is to create the necessary live interaction tools in Scheme or Scat, and create a small wrapper around them so they can be used in command mode. Note that target commands are intended for manual interactive use only. The composition mechanism doesn’t mix well with function composition.

Alternative to creating target commands, which are really just a user interface, it is possible control the target more directly from Scheme or Scat code. From within the compiler namespace the the target> macro can be used, which is like forth-command but without a string to s-expression parsing step. It supports the Scat unquote operation. Alternatively scat> can be used to control the target directly. The target> command language is in fact implemented in terms of scat> as a collection of primitive substitution rules. For details see live/commands.ss. Ultimately, most low-level utilities defined in live/tethered.ss are exposed.

For example, to transfer a chunk of memory from the target use the function abytes->list. Note that we’re still in the compiler namespace.

  > (with-console

      (lambda () (abytes->list 0 10)))

  (75 15 83 139 199 199 87 87 243 243)