-
Notifications
You must be signed in to change notification settings - Fork 66
STM8S eForth Programming
Welcome to Forth!
This page covers practical Forth for $0.20 STM8S µCs with features like interactivity, Flash programming, multitasking, interrupt handlers, and character I/O redirection (just to name a few). Think "smart peripherals" with interactive development and debugging.
Initially transfer of a Forth system to the µC an ST-LINK adapter and STM8FLASH is needed. Please refer to STM8S Programming for more information.
STM8EF uses a serial interface for communication. Please refer to the hardware section for details.
The following pages provide in-depth and technical information on specific topics:
- STM8S eForth Compile to Flash: non-volatile Forth programs
- STM8S eForth Background Task: Forth tasks that run in the background
- STM8S eForth Interrupts: interrupt handlers in Forth
Forth is a very simple but extensible language, and a typical TG9541/STM8EF board support package focuses on simplicity of use.
Starting Forth provides a friendly introduction (most of the examples work with STM8EF). If you want to improve your skills Thinking Forth by Leo Brodie is a very good introduction into the Forth programming method. However, it's possible to define what a Forth implementation can do "in Forth", and its self-refering nature is the most powerful, and often puzzling, feature of advanced Forth programming.
Forth has a console which can be used for interpreting code. If you've ever used a RPN calculator you already know how to write mathematical expressions in Forth: 3 7 + 5 * <ENTER>
is the same as ( 3 + 7 ) * 5 =
(no equals!).
Forth is a stack based language: for most of the data flow between routines ("words") there is no need to use variables. Code execution in Forth follows a simple pattern: put data on the stack, write the name of one or more Forth words (e.g. +
and .
for printing), and press enter:
10 -2 * .<enter> -20 ok
After typing 10 -2 * .
and starting the interpreter with <enter>
, 10
and -2
are pushed, +
pops both numbers, adds them and pushes the result, and .
pops and prints it. The interpreter then prints ok
, and goes to the next line (CR
). When you see ok
in a Forth example it means that <enter>
was pressed, and that the processing of the "phrase" was "OK".
There are several "core" words for manipulating data in the stack, e.g. DUP
(duplicate), SWAP
(swap elements), DROP
(remove), ROT
(rotate top 3). A simple embedded Forth like STM8EF doesn't care about the type of data. All that's important is how many "cells" a word takes from the stack, and how many it puts there after execution. In STM8EF a cell is a 16 bit value (e.g. number, address, etc.) 32 bit values are represented by two consecutive cells.
6 7 -1 2 ok
* DUP . -2 ok
.S
6 7 -2 <sp ok
SWAP
.S
6 -2 7 <sp ok
The word .S
is a debugging word: it prints (.
) the contents of the Stack ("S"). Note that the interpreter will reset the stack when there is an error (e.g. a word is invalid), and it will tell you when there was a stack "underflow" (more items consumed than produced).
If you want to see the available words, simply type (WORDS
). If you want know to know what they do look here.
Forth has a console which can be used for interpreting code, and for defining new words. A Forth programmer breaks the problem down into small bits of code that can be tested easily.
Defining new words is simple: write :
followed by the name of the new word (which may include "structure words" like IF
, THEN
, or DO
) and ;
at the end:
: by-2 ( w -- w ) \ multiply w by -2<enter>
-2 * ; ok
10 by-2 . -20 ok
The sequence :
, identifier
, code, and ;
is called a "colon definition". The word :
brings Forth into compilation state.
The only requirement for identifiers for new words is that it must consist out of one or more printable character, and that the words used in the definition are already known. There is next to no syntax checking because there is no syntax to speak of.
STM8EF starts in mode RAM
, and new words are compiled to volatile memory. After switching to NVM
mode, words are stored in (non volatile) Flash memory. Refer to STM8S eForth Compile to Flash for more information.
Of course there are structure words in Forth, e.g. IF
and THEN
. As Forth is a stack oriented language it does things a bit differently:
: test ( n -- ) \ demonstrate if else then
IF
." true"
ELSE
." false"
THEN ; ok
0 test false ok
5 test true ok
In Forth there is no fundamental difference between compiler and interpreter. In fact, the role of the interpreter is similar to a C preprocessor:
: param ( -- n ) \ use the interpreter for calculating a parameter
[ -622 37 100 */ ] LITERAL ; \ keep the result
param . 230 ok
[
switches from the compilation to the interpreter state. The code until ]
is executed, and the result is passed to the immediate word LITERAL
, which puts it into the word param
as a constant. Note that immediate words like [
and LITERAL
can be defined by labeling a new word as IMMEDIATE
after compilation (likewise a word can be labeled as "compile only" which means that they won't be executed in interpreter state, e.g. IF
or THEN
).
The identifier of a new word can be equal to existing words or even numbers (note that the ability to redefine words is akin to overloading methods in object oriented languages).
STM8EF is a 16bit STC Forth, which means that data stack, return stack, a memory cell, and addresses are all 16 bit wide (some words use 8 or 32 bit data). In STC, compiled code is executable machine code.
An up-to date list of Forth words from forth.asm
, the glossary, is located in docs/words.md.
The listed words have a comment that describes their effect in an extended data-flow notation:
; @ ( a -- n ) ( TOS STM8: -- Y,Z,N )
; Push memory location to stack
@
(read) gets address a
from the stack, reads the 16bit value at a
and puts its value n
back on the stack. The second part is directed at the assembly programmer: after execution of @
the register Y
contains the TOS
(top of stack) value . The Z
(zero), and N
(negative) flags correspond to n
.
Depending on the board configuration in globalconf.inc
only a subset of the words listed in words.md
is linked (visible) or included in a binary release for a board. The words declared in boardcore.inc
are also missing from the list even if they're part of the dictionary.
To list all Forth words available in a session (core, and user-defined) type WORDS
on the Forth console. Also note that in the globalconf.inc
of most boards the option CASEINSENSITIVE
is set (i.e. words
and WORDS
means the same).
While STM8EF stores compiled code on the target the µC doesn't contain enough memory for storage of source code. Hence it makes little sense to have a editor for Forth screens in it (the only source code storage in STM8EF is a 80 bytes line buffer, and backspace ^h
is the only editing feature).
Instead, source code should be stored on a host computer, and transferred to the µC line-by-line (e.g. with the help of ascii-xfr).
For transferring programs (or interpreted sequences), STM8EF can be switched to FILE
mode which basically replaces the OK
prompt by a "pace character" (ASCII VT
- a serial ASCII transfer program can use this to synchronize with the compilation progress). After the transfer, the word HAND
can be used to switch back to interactive mode.
The general pattern is this:
FILE
( lines
with instructions
here )
HAND
@RigTig wrote a nifty little STM8EF Forth transfer tool in Python, which uses the pace character for synchronization.
The following picocom wrapper works for full-duplex and wired-or half-duplex with implicit echo (e.g. W1209). It also sets a suitable line delay for programming Forth to NVM without handshake:
#!/bin/sh
# "$@" --imap lfcrlf --emap crcrlf --omap delbs \
exec picocom \
--send-cmd="ascii-xfr -s -c10 -l500" \
"$@" --imap igncr,lfcrlf --omap delbs \
/dev/ttyUSB0
The following section contains some simple idioms, patterns, and example programs. Board-W1209 contains some more examples for startup code with an interactive background task that uses W1209 I/O.
The following example defines a simple greeting word. It's also possible to initialize background tasks, or to run complex embedded control applications.
FILE
NVM
: mystart CR 3 FOR I . NEXT CR ." Hi!" CR ;
' mystart 'BOOT !
RAM
HAND
FILE
goes to non-interactive mode for ASCII-transfer in the terminal emulator. NVM
switches to Flash mode. mystart
is the word that's to be run as start-up code. '
(tick) retrieves its address of mystart
, 'BOOT
retrieves the address of the startup word pointer, and !
stores the address of our word to it. RAM
changes to RAM
mode and stores pointers permanently.
On reset or through cold start STM8EF now shows the following behavior:
COLD
3 2 1 0
Hi!
The original start-up behavior can be restored by running ' HI 'BOOT !
, or using RESET
, which not only makes STM8EF forget any vocabulary in Flash, but also resets the start-up code to HI
.
The STM8S003F3 and STM8S103F3 both have 5 usable multiplexed ADC channels (AIN2 to AIN6). The words ADC!
and ADC@
provide access to the STM8 ADC.
The following example shows how to read AIN3, which is an alternative function of PD2:
FILE
: conv ADC! ADC@ ;
HAND ok
3 conv . 771 ok
ADC!
selects a channel for conversion, ADC@
starts the conversion and gets the result. The example declares the word conv
to combine both actions. Please note that the conversion time of ADC@
is longer after selecting a different channel with ADC!
.
Also note that AIN5 and AIN6 are an alternative function of the ports PD5 and PD6 which are also used for RS232 TxD and RxD. The phrase 6 ADC!
switches PD6 to analog mode (AIN6) while detaching the UART (RxD). The eForth system will appear to be hanging (the phrase 6 ADC! ADC@ 0 ADC! .
will show a 10bit analog read-out of the RxD level).
The STM8SEF board support provides the word OUT!
for setting the binary state of up to 8 relays, LEDs or other digital output devices (new board support packages should extend provide a new mapping).
The following example blinks Relay 1
of a C0135 STM8S103 Relay Control Board, the relay of a W1209 thermostat or the status LED of a STM8S103F3P6 Breakout Board with a frequency of about 0.75 Hz ( 1/(128 * 5 msec) ):
FILE
: task TIM 128 AND IF 1 ELSE 0 THEN OUT! ;
' task BG !
HAND
Data output to 7S-LED displays is supported by vectored I/O. In background tasks the EMIT
vector points to E7S
by default, and using it is simple (see example code on the W1209 page).
Numeric output to multiple groups of 7S-LED displays (e.g. boards W1219 2 x 3 digits, or W1401 3 x 2 digits) is also simple:
-
CR
moves the output to the first "tab" group without changing the display contents - a
space
character moves the output to the next 7S-LED group if it was not preceded by a move, and clears the current 7S-LED group digits
The following code displays different data (all scaled to a range of 0..99) on the 3x2 digit 7S-LED groups of the board W1401
:
FILE
: timer TIM 655 / ;
: ain 5 ADC! ADC@ 100 1023 */ ;
: show timer . ain . BKEY . CR ;
' show bg !
HAND
The word show
displays the values scaled to 0..99 from the BG
timer, the sensor analog input, and the board key bitmap BKEY
followed by a CR
(new line). When the word show
runs in the background, it displays the ticker on the left yellow 7S-LED group, ain
on the middle red LEDs, and the board key bitmap on the right yellow group.
If an existing word in is re-defined in eForth the old definition can be used in the new definition, e.g.:
16 32 64 .S
16 32 64 <sp ok
: .S BASE @ >R HEX .S R> BASE ! ; reDef .S ok
10 20 40 <sp ok
The downside is that it's difficult to define recursive functions: in eForth, linking the new word to the dictionary is delayed until ;
is executed.
The following example shows how to do recursion in eForth:
: RECURSE last @ NAME> CALL, ; IMMEDIATE
From the execution of :
on, the word last
provides the address of the code field of the new word.
The fibunacci
function demonstrates a recursive reference with f(n-1)
and f(n-2)
.
FILE
: fibunacci DUP 2 < IF DROP 1 ELSE DUP 2 - RECURSE SWAP 1 - RECURSE + THEN ;
: fibnums FOR I fibunacci U. NEXT ;
HAND ok
15 fibnums 987 610 377 233 144 89 55 34 21 13 8 5 3 2 1 1 ok
On a STM8S, the 27199 calls of 23 fibunacci
(the maximum for 16bit arithmetics) execute in about 2.6s from RAM, or 2.3s from Flash. While that's no big problem for the stack this is a very long time for most embedded applications.
On a µC recursion should be used with care, but for some algorithms it can be very useful:
FILE
: RECURSE last @ NAME> CALL, ; IMMEDIATE
\ binary tree (dictionary)
\ https://rosettacode.org/wiki/Tree_traversal#Forth
\ minor modifications for eForth
: node ( l r data -- node ) here >r , , , r> ;
: leaf ( data -- node ) 0 0 rot node ;
: >data ( node -- ) @ ;
: >right ( node -- ) 2+ @ ;
: >left ( node -- ) 2+ 2+ @ ;
: preorder ( xt tree -- )
dup 0= if 2drop exit then
2dup >data swap execute
2dup >left recurse
>right recurse ;
: inorder ( xt tree -- )
dup 0= if 2drop exit then
2dup >left recurse
2dup >data swap execute
>right recurse ;
: postorder ( xt tree -- )
dup 0= if 2drop exit then
2dup >left recurse
2dup >right recurse
>data swap execute ;
: max-depth ( tree -- n )
dup 0= if exit then
dup >left recurse
swap >right recurse max 1+ ;
\ Define this binary tree
\ 1
\ / \
\ / \
\ / \
\ 2 3
\ / \ /
\ 4 5 6
\ / / \
\ 7 8 9
variable tree
7 leaf 0 4 node
5 leaf 2 node
8 leaf 9 leaf 6 node
0 3 node 1 node tree !
\ run some examples with "." (print) as the node action
cr ' . tree @ preorder \ 1 2 4 7 5 3 6 8 9
cr ' . tree @ inorder \ 7 4 2 5 1 8 6 9 3
cr ' . tree @ postorder \ 7 4 5 2 8 9 6 3 1
cr tree @ max-depth . \ 4
HAND
Note that the example traversal code (e.g. preorder
) accepts the address of an action word. Of course, most practical applications (e.g. binary search) require some changes but Forth makes very lightweight and powerful solutions possible. It also gives you an idea of the code density: the 32 lines of code above compile to 385 bytes, that's about 200 bytes per screen.