This program is a Forth compiler for Microchip PIC 16F873/876.
I needed to write some code on a PIC to control a digital model railroad system using the DCC (Digital Control Command) protocol. However, writing it in assembly is error-prone and writing it in C is no fun as C compiled code typically needs a lot of space.
So I wrote this compiler, not for the purpose of writing a compiler, but as a tool to write my DCC engine.
The compiler does not aim to be ANS Forth compliant. It has quite a few words already implemented, and I will implement more of them as needed. Of course, you are welcome to contribute some (see below for license information).
At this time, many words are missing from standard Forth. For example, I have no multiply operation as I have no use for it at this time and won't spend time to implement things I don't need (remember, Forth is a tool before anything else).
The compiler is released at the moment under the GNU General Public License version 2 (I intend to use the less restrictive BSD license in the future, but as it is based on gforth, I have to sort out those issues with gforth copyright holders).
However, the code produced by using this compiler is not tainted by the GPL license at all. You can do whatever you want with it, and I claim absolutely no right on the input or output of this compiler. I encourage to use it for whatever you want.
Note that I would really like people to send me their modifications (be they bug fixes or new features) so that I can incorporate them in the next release.
Mary was a great inspiration source, I even kept some of the names from it. However, no code has been reused, as both Forth do not have the same goal.
The stack is indexed by the only indirect register, fsr. The indf register automatically points to the top of stack.
The w register is used as a scratch. Attempts to use it to cache the top of stack prove to be inefficient, as we often need a scratch register.
The compiler is hosted on gforth, a free software compiler for Unix systems. The command line to use to compile file foo.fs into foo.hex, and getting a usable map into foo.map is:
gforth picforth.fs -e 'include foo.fs final-dump foo.hex map bye' | \ sort -o foo.map
Of course, you should automate this in a Makefile, such as the one provided with the compiler.
If you install the GNU PIC utils (from http://gputils.sourceforge.net/),
then you can read the assembled code by using gpdasm
.
By executing
gforth picforth.fs -e 'host picquit'
(or make interactive
from a Unix shell), you are dropped into an
interactive mode, where you can use the following words to check your code:
see ( "name" -- ) Disassemble a word map ( -- ) Print code memory map dis ( -- ) Disassemble the whole code section
Hexadecimal literals should be prefixed by a dollar sign $
to avoid
confusion with existing constants (such as c
for carry bit). This is a
strong advice.
There is a /if
test which is equivalent to 0= if
much executes much
faster.
Also, c-if
and /c-if
test the carry bit.
rlf-tos
and rrf-tos
respectively shift the top-of-stack left or right,
with the carry entering the byte and the outgoing bit entering the carry.
lshift
and rshift
used with a constant shift, and 2*
and 2/
do have
the last exited bit in the carry.
There exists a v-for
/v-next
structure:
v-for ( n addr -- ) Initialize addr content with n.
v-next ( addr -- ) Decrement addr content. If content is not zero, jump to v-for location.
Also, the words begin
, again
, while
, until
and repeat
are
implemented. /while
does the same thing as while
with a test
reversed, as does /until
compared to until
.
A table starts with table
word which takes the top of stack and executes
the first element for 0, the second for 1, ... Control returns from the
current word after executing the requested action. Action is tc: word
which executes word.
table ( n -- ) tc: ( "name" -- )
Example:
: print-number ( n -- ) \ n <= 3, print number table tc: zero tc: one tc: two tc: three ;
The table structure does not consume return stack slots.
A main
word indicates that the next address is the main program. Use for
example:
main : main-program ( -- ) (do initialisations) (call mainloop) ;
You can switch to macro mode by using the macro
word. You get back to
target mode by using the target
word.
If you want to use interrupts, use
include picisr.fs
Two words do respectively save and restore the context around interrupt handling code:
isr-save ( -- ) isr-restore-return ( -- )
Also, the word isr
is provided to notify that the next address is the
isr handler.
For example, you can write an interrupt handler with:
isr : interrupt-handler ( -- ) isr-save (interrupt handling code here) isr-restore-return ;
Do not forget that the return stack depth is only height. An interrupt can occur at any time unless you mask them or unset the GIE bit.
Two facility words that manipulate GIE are also provided:
enable-interrupts ( -- ) disable-interrupts ( -- )
You have to dispatch the interrupts and clear the interrupt bits manually before you return from the handler.
Versions that do nothing are provided in the default compiler. Useful versions are redefined when using picisr.fs.
Because of this, include picisr.fs as soon as possible, before other files and before using enable-interrupts and disable-interrupts. Other included files may fail to act properly if you don't.
In Forth, argument passing is done on the stack. However, if you want to
transmit the top-of-stack argument in the w register (for example if a word
typically takes a constant which is put on the stack just before calling it),
you can use the defining word ::
instead of :
. All calls will
automatically use this convention.
Note that you cannot use words defined with ::
in a table (see the
Tables
section).
If you want to return a value in the w register, you can use the word >w
which loads the top-of-stack into the w register before every exit point.
After calling a word which returns its result in the w register, you can
call w>
to put the w register value onto the stack.
To ease bit manipulation, the following words are defined for port p:
and! ( n p -- ) logical and with n /and! ( n p -- ) logical and with ~n or! ( n p -- ) logical or with n xor! ( n p -- ) logical xor with n invert! ( p -- ) invert content bit-set ( b p -- ) set bit b of p (both have to be constants) bit-clr ( b p -- ) clear bit b of p (both have to be constants) bit-toggle ( b p -- ) toggle bit b of p (both have to be constants) bit-set? ( b p -- f ) return non-zero if bit b of p is set
Note that there is no bit-clr?
operation. Use a /if
instead of if
and
/while
instead of while
if you want to test for the absence of a bit.
Five words help create a port pin:
pin-a ( n "name" -- ) ( Runtime: -- n porta ) pin-b ( n "name" -- ) ( Runtime: -- n portb ) pin-c ( n "name" -- ) ( Runtime: -- n portc ) pin-d ( n "name" -- ) ( Runtime: -- n portd ) pin-e ( n "name" -- ) ( Runtime: -- n porte )
For example, you can create a pin designating an error LED and manipulate it using:
3 pin-b error-led \ Error LED is on port B3 : error error-led bit-set ; \ Signal error : no-error error-led bit-clr ; \ Clear error
To ease reading, the words high
, low
, high?
and toggle
are
aliases for, respectively, bit-set
, bit-low
, bit-set?
and
bit-toggle
.
You can change the direction of a pin by using >input
or >output
after
a pin defined with pin-x
. For example, to set the error led port as an
output, use:
error-led >output
The word clrwdt
is available from Forth to clear the watchdog timer.
By using
include piceeprom.fs
you have access to new words allowing you to access the PIC EEPROM:
ee@ ( a -- b ) read the content of a and return it ee! ( b a -- ) write b into a
Also, in any case, you can store data in EEPROM using those words:
eecreate ( "name" -- ) similar as create but in EEPROM space ee, ( b -- ) store byte in EEPROM
Two words allow reading from and writing to the flash memory when the file
picflash.fs
is included with
include picflash.fs
Those words expect manipulate a 14 bits program memory cell whose 13 bits address is in EEADRH:EEADR. The data is read from or stored to EEDATH:EEDATA.
flash-read ( -- ) flash-write ( -- )
If picisr.fs
has been included before this file, interrupts will be properly
disabled around flash writes.
A map can be generated in interactive mode using the map
word.
A basic priority-based cooperative multitasker allows you to concurrently run several indenpendant tasks.
Each task should execute in a short time. A true multitasker would be inefficient as the return stack cannot be manipulated.
The following words can be used to define tasks (the entry point for the task is the next defined word):
task ( prio "name" -- ) Define a new task with priority prio. By default, this task will be active. You can use the "start" and "stop" words to control it. Those words can be used from an interrupt handler.
task-cond ( prio "name" -- ) Define a new task with priority prio. By default, this task is inactive. You can enable it by using the "signal" word on it. If you use "signal" N times, then the task will be run exactly N times. "signal" can be used from an interrupt handler.
task-idle ( -- ) Define a new task which will be executed inconditionnaly when there is nothing else to do. Such a task can not be stopped.
task-set ( bit port prio -- ) Define a new task with priority prio that will be run when bit bit of port port is set.
task-clr ( bit port prio -- ) Define a new task with priority prio that will be run when bit bit of port port is clear.
Priority 0 is the greatest one, while priority 255 corresponds to the lowest (idle) priority. You should use priority in the range 0-254 for your own tasks.
The multitasker is run by using the word multitasker
. This word takes care
of scheduling the highest priority tasks first. It also clears the watchdog
once per round.
The multitasker looks for all tasks of priority 0 ready to execute. If it find some, it executes them and starts over. If it doesn't, it looks for priority 1 tasks ready to execute. If it find some, it executes them and starts over. If it doesn't, etc. It does this up to priority 255.
The following optimizations are implemented:
Tail recursion is implemented at exit
and ;
points.
: x y z ;
generates the following code for word x:
call y goto z
The sequence recurse exit
also benefits from tail recursion.
For example, the (particularily stupid and useless)
dup dup drop
sequence generates
movf 0x00,w decf 0x04,f movwf 0x00
which in fact corresponds to a single dup
.
Also, the following sequence
drop 3
generates
movlw 0x03 movwf 0x00
The following sequence
9 and
generates
movlw 0x09 andwf 0x00,f
Also, combined with the redundant push/pop eliminations, the following code
dup 9 and if ...
generates
movf 0x00,f andlw 0x09 btfsc ...
The following sequence (with current
and next
being variables)
current @ 1+ 7 and current !
generates
movf 0x3B,w addlw 0x01 andlw 0x07 movwf 0x3C
Short (one instruction) if
actions are transformed into reversed
conditions. For example, the following word:
\ This word sets port a0 if port c2 is high, and port b1 in any case. : z 2 portc high? if 0 porta high then 1 portb high ;
generates the following code: (hand-commented assembly)
btfsc 0x07,2 ; skip next instruction if port c2 is low bsf 0x05,0 ; set port a0 high bsf 0x06,1 ; set port b1 high return ; return from word
The compiler tries to remove useless bank manipulations. The following word
: eeprom-read eepgd eecon1 bit-set rd eecon1 bit-set ;
generates (hand-commented assembly):
bsf 0x03,5 ; select ... bsf 0x03,6 ; ... bank 3 bsf 0x0c,7 ; set bit eepgd of eecon1 (in bank 3) bsf 0x0c,0 ; set bit rd of eecon1 (in bank 3) bcf 0x03,6 ; restore ... bcf 0x03,5 ; ... bank 0
If an operation result is stored on the stack then popped into w, the operation is modified to target w directly.
For example, the following word:
: timer ( n -- ) invert tmr0 ! ;
generates
comf 0x00,w incf 0x04,f movwf 0x01 return
If a and
operation before a test can be rewritten using a bit test
operation, it will.
For example, the code:
checksum @ 1 and if parity-error exit then ...
will be compiled as:
btfsc 0x33,0 goto 0x037 ; parity-error ...
Using an explicit bit-test holds the same result
3 porta
Before a test, the z status bit is already known and no load is needed. For example, the sequence
9 and dup if foo then
will be compiled as:
movlw 0x09 andwf 0x00,f btfss 0x03,2 call foo ...
The configuration can be configured with the following words:
set-fosc ( n -- ) Choose oscillator mode from: (default: fosc-rc) fosc-lp Low power fosc-xt External oscillator fosc-hs High-speed oscillator fosc-rc RC circuit set-wdte ( flag -- ) Watchdog timer enable (default: true) set-/pwrte ( flag -- ) Power-on timer disable (default: true) set-boden ( flag -- ) Brown-out detect enable (default: true) set-lvp ( flag -- ) Low voltage programming (default: true) set-cpd ( flag -- ) EEPROM protection disable (default: true) set-wrt ( flag -- ) FLASH protection disable (default: true) set-debug ( flag -- ) In-circuit debugger disable (default: true) set-cp ( n -- ) Code protection, choose from: (default: no-cp) no-cp No protection full-cp Full protection
Some files are included as examples with a Makefile. For example, to build
booster.hex
, run make booster.fs
:
This compiler release suffers from the following known limitations. Note that most of them (if not all) will disappear in subsequent releases.
At this time, the PCLATH register is not used thus the code area is limited to 2048 (800h) instructions. The compiler will abort if an attempt is made to set the code pointer outside of this area.
The memory space is limited to only the first bank, from 20h to 7fh (95 bytes). Attempts to write outside of this area will result in a compiler abortion.
There is no link between the compiler and the target.
At this time, the primitive library mechanism is incompatible with the disassembler and the return-stack depth computation.
I would like to thank the following people: