Skip to content

STM8 eForth Programming

Thomas edited this page Sep 1, 2023 · 107 revisions

Practical STM8 eForth Programming

STM8 eForth is a small stack-oriented language that's fun to work with: it brings interactive programming to low-cost and easy to use µCs. It provides OS features like scripting, simple background task execution and even character I/O redirection. The built-in compiler targets RAM or the Flash ROM so that you can switch between experimenting with code and keeping it for the next iteration towards your solution.

STM8 eForth is compact and easily configurable: on a $0.20 STM8S003F3 (8K Flash, 1K RAM) it can operate on less than half of the chip's memory. Of course, beefier STM8 devices can be used, e.g., the $0.50 STM8S005K6 with 32K Flash and 2K RAM, or an STM8L052R8 with even more Flash, 4K RAM, LCD driver, RTC and other features.

Best of all, Forth is extremely modular: the "dictionary" of "words", Forth code units, is also a library. This means a lot can be done with very little memory!

Hello Forth!

Forth, as a programming language, starts with a very simple syntax, but it can be extended as needed.

Here is a simple "Hello World" in Forth:

: hello ."  Hello World!" ;

When the Forth interpreter reads : it enters "compile mode" and starts the definition of the word hello. ." compiles a printable string of characters terminated by ". Finally, ; compiles a RET instruction and returns to interpreter mode. The new word hello is now in the dictionary as machine code. STM8 eForth keeps the dictionary as a linked list that starts in RAM and that ends in the non-volatile memory (NVM, i.e, Flash ROM). By default new words will be added to the RAM portion but you can change that with the NVM command.

STM8 eForth is an Subroutine Threaded Code (STC) Forth with a data width of 16 bit. STC means that source code gets compiled to machine code (mostly subroutine calls - more about that in Code Generation). 16 bit word width means that data stack, return stack, a "memory cell" and "addresses" are all 16 bit wide. Of course, it's also possible to process 8 or 32 bit data (or wider data if you supply the necessary words).

The STM8 eForth project focuses on ease of use: it combines 1980s simplicity of the machine with today's convenience of GitHub and Docker containers. Of course there's always room for improvement. If you face a problem with this tutorial please open a support ticket!

Hands-On

If you have a machine with Docker installed you can give it a try without needing any hardware: STM8 eForth Hands On with uCsim walks you through the steps. Most examples in this tutorial will work.

Forth is more fun with real hardware, of course, but it won't break the bank - $5 to $10 should buy you the following items:

  • a supported board like C0135 or a simple breakout board
  • an ST-Link adapter for programming a binary, e.g. a cheap ST-LINK V2 compatible USB dongle
  • a "TTL" serial interface adapter for your PC (some people have also used a Raspberry-Pi).

The getting started section in the Wiki walks you through the steps for bringing your first board up.

STM8 eForth uses a terminal emulation program and a serial interface. The default communicating parameters are "9600-N-8-1" but that can be changed at runtime. The Forth mini-IDE e4thcom for Linux (including Raspbian) works best but a standard terminal program like Picocom or Hyperterm will also work.

After resetting your board you should see a prompt like this:

STM8eForth 2.2.28 ok

If this works: congrats, your board now runs STM8 eForth! Otherwise, please check your serial interface (e.g. port, rate, RxD and TxD swapped).

Some new words

Forth code units (subroutines, functions, actions) are called "words" which are collected in a "dictionary". By default the STM8 eForth dictionary search is case insensitive (with the exception of the svelte "CORE" binary). To list all Forth words available in a session (including "user-defined" words) type WORDS or words on the Forth console.

WORDS<enter>
  IRET SAVEC RESET RAM NVM LOCK ULOCK WORDS .S DUMP IMMEDIATE ALLOT VARIABLE CON
STANT CREATE DOES> ] : ; OVERT ." $" ABORT" AFT REPEAT WHILE AHEAD ELSE THEN IF 
AGAIN UNTIL BEGIN +LOOP LOOP DO NEXT FOR COMPILE LITERAL CALL, C, , [COMPILE] ' 
hi CR [ NAME> \ ( .( ? . U. TYPE U.R .R SPACE KEY DECIMAL HEX <# SIGN HOLD #S # 
#> ERASE FILL CMOVE HERE COUNT +! DEPTH PICK 0= ABS NEGATE NOT 1+ 1- 2+ 2- 2* 2/
 EXG */ */MOD M* * UM* / MOD /MOD M/MOD UM/MOD WITHIN MIN MAX < U< = 2DUP ROT ?D
UP BG TIM BL OUT last '?KEY 'EMIT BASE - 0< OR AND XOR + UM+ I OVER SWAP DUP 2DR
OP DROP NIP >R R@ R> C! C@ ! @ B! 2C@ 2C! 2@ 2! EXIT EXECUTE LEAVE EMIT ?KEY TX!
 ?RX ADC@ ADC! OUT! COLD 'BOOT ok

If this also works you're all set.

Depending on the board configuration (e.g. globalconf.inc) not all words listed in words.md are in the dictionary. The Alias feature can add "invisible code" to the dictionary when it's needed.

Now it's maybe a good time to enter the "Hello World" program above and type hello. The code will be compiled to RAM. It's best to think of the RAM as a scratch-pad: unless you enter NVM mode and switch back to RAM all words you compile will be temporary (but more about that later).

Working with the Stack

Forth is a stack-oriented language: the data-flow between words (subroutines) uses the data stack. The advantage of a stack oriented approach is that assigning parameters and copying results is unnecessary. Also temporary (local) variables won't be needed in most cases.

A simple Forth like STM8 eForth is "untyped": all data on the stack, including addresses, is represented by 16 bit numbers (i.e. two 16 numbers for 32 bit data).

If you've ever used RPN you already know how to write arithmetic expressions for a stack machine:

For computing the problem (12 + 7) * 5 with an ordinary pocket calculator one has to type 12 + 7 = * 5 =. On an RPN pocket calculator one would probably type 12 ENTER 7 + 5 *.

Interactive code execution in Forth follows the RPN calculator pattern, except that there is a line buffer and evaluation happens at the end of the input: "put data on the stack, write the name of one or more Forth words (e.g. + and . for printing) and press enter".

12 7 + 5 * .<enter> 95 ok

Pressing <enter> starts the evaluation of the input buffer. 12 and 7 are pushed on the stack, + pops (consumes) both numbers, adds them and pushes the result back. 5 * does the second calculation and . pops the result and prints it. The interpreter prints ok and waits for new input. The simple Forth command line interface is known as a "REPL" (Read Evaluate Print Loop).

Note: in the following listings ok or OK means that <enter> was pressed and that the Forth REPL is ready to accept the next input.

Entering numbers and printing depends on the active base (i.e. HEX, DECIMAL or the value of the system variable BASE). It's also possible to use a base modifier for entering numbers:

160 ok
HEX ok
.<enter> A0 ok
&16 .<enter> 10 ok
DECIMAL ok
$10 .<enter> 16 ok
%10000 .<enter> 16 ok

The modifiers $, % and & are a shorthand for BASE. The lines above contain the bases 16, 10 and 2. You can select any base from 2 to 35 by setting BASE directly (there is an example later in this text).

Words for manipulating the data stack, e.g. DUP (duplicate), SWAP (swap the two elements), DROP (remove), ROT (rotate the top 3 elements), are on the lowest level of the Forth Dictionary:

Word stack effect description
DROP ( a -- ) remove top of stack
NIP ( b a -- a ) remove next item after top of stack
DUP ( a -- a a ) duplicate top of stack
SWAP ( b a -- a b ) swap topmost items on stack
OVER ( b a -- b a b ) duplicate next item after top of stack
ROT ( c b a -- b a c ) rotate the three topmost items on stack
PICK ( ... +n -- ... w ) copy the nth stack item to top of stack

There are words for the manipulation of double (32bit data) words (e.g. 2DROP and 2DUP), some of which are in the library (e.g. 2SWAP and 2OVER). These words can be constructed of existing stack manipulation words. The stack effect, by the way, describes the ( input -- ) and the ( -- output ) of a word.

You don't need to memorize these words. As soon as you solve your first problem you can come back to this table. Refer to Starting Forth for an overview of standard words for stack manipulation.

The word .S is a debugging word: it prints (.) the contents of the stack ("S") without changing it:

6 7 -1 2 ok
* DUP .<enter> -2 ok
.S<enter>
6 7 -2 <sp  ok
SWAP ok
.S<enter>
6 -2 7 <sp  ok

Note that in the Forth community the use of words for "deep stack" manipulation (e.g. PICK) are considered a sign of poor "factoring" (data flow structure). Just that much: 0 PICK has the same effect as DUP and 1 PICK corresponds to OVER, and the only place where the STM8 eForth core uses PICK is ... .S.

To sum it up: in an interpreter cycle ("REPL") Forth takes a number (0 .. n) of items from the stack and pushes (0 .. m) items back.

When there is an error (e.g. a word is unknown) the interpreter resets the stack:

6 7 -1 2 ok
.S
 6 7 -1 2 <sp  ok
abc abc?
.S
 <sp  ok

It will also tell you when there is a stack "underflow" after execution, which means that more items were consumed than what was initially on the stack.

.S  
 <sp  ok
DROP  underflow?

Note that executing DROP DROP DROP 0 0 0 on an empty stack will cause an underflow without triggering an error! For performance reasons a simple Forth like STM8 eForth won't test the stack balance during evaluation (but Forth can be extended to do that). Instead it's the programmer's responsibility to test, and document, the stack effect of the words they program (this should be a seen as a unit test).

Accessing Memory

STM8 eForth programming is "close to the iron" and accessing memory is an essential operation in Forth. The following example demonstrates 16bit (word) and 8bit (byte) access to the RAM locations 0 and 1:

$AA55 0 ! ok
1 C@ .<enter> 55 ok
0 10 DUMP<enter>
   0  AA 55 83 25 5B 2F 52 75 71 2E B4 DB 76 2D BF  5  _U_%[/Ruq.__v-__ ok

When working with memory locations setting the base to HEX is often useful (DECIMAL restores the default base).

At this point some general information about the memory layout of STM8 µCs may be helpful. The designers of this 8bit µC made it really easy for the user. Program code can be executed from RAM or Flash ROM, and EEPROM and Flash ROM, if unlocked, can be written like RAM (although program execution gets stalled until an automatic write or erase has finished).

STM8 Memory Map

Address Memory area Family
$0000 RAM start all
$1000 EEPROM start STM8L
$4000 EEPROM start STM8S
$5000 peripherals registers start all
$8000 Flash ROM start all

Note that RAM locations 0 and 1 won't be used by the STM8 eForth core (that's a promise). When writing programs you'll most likely prefer to declare a VARIABLE and ALLOT memory. That's more convenient and safer than using RAM directly. You can also write TO a named VALUE or to a table made with CREATE in RAM or Flash ROM.

The memory write words !, C! and 2! take an address and perform a write access to a 16, 8 or 32 bit memory location. The words @, C@ and 2@ perform a read access.

There are also the read-modify-write word +! and the read-print word ?:

10 0 ! ok
1 0 +! ok
0 ? 11 ok

The address of Forth words can accessed with ' (tick) and the word DUMP is handy for debugging:

' hi 10 DUMP
8D07  CD 89 BB 12  A 53 54 4D 38 65 46 6F 72 74 68 20  _____STM8eForth  ok

We'll later see that code addresses are also data (manipulating the control flow in Forth is easy).

Defining Constants and Variables

In Forth, numeric constants can be defined with CONSTANT:

42 CONSTANT TheAnswer ok
TheAnswer .

Note that a constant word has a different behavior in Interpreter than in Compiler mode. While it puts a value on the stack in Interpreter Mode it inserts a DOLIT <value> into the memory stream in Compiler Mode.

Forth variables are named memory locations. A small set of variables control the Forth console, e.g. Interpreter/Compiler Mode, the last dictionary entry, or the base of numbers:

2 BASE ! ok
$AA55 .<enter> 1010101001010101 ok
DECIMAL BASE ? 10 ok

The active number base is controlled by the built-in variable BASE which has a valid range of 2 to 35 (the word ? is a shortcut for @ .).

For user variables the defining word VARIABLE can be used:

VARIABLE apples ok
10 apples ! ok
-2 apples +! ok
apples ?

When you press <enter> at the end of the last line the result of apples ? should be 8.

If you need more than one cell, you can use the same thing that creates a variable, but define the amount of memory bytes you need with ALLOT:

\ Create an array "table" with 6 cells
VARIABLE table 5 2* ALLOT 

Accessing cells in an array works with simple math. The following phrase writes the value 500 to index 2, the 3rd cell, of the array table:

500 2 2* table + !

32bit arithmetic is on a system with a 16bit stack requires "double words", e.g. M*, 2! or 2@.

Here is how to create a "double variable":

VARIABLE MyDouble 2 ALLOT
1000 1000 M* MyDouble 2!

Note that VARIABLE already allots 2 bytes. ALLOT here reserves 2 more bytes (this makes 4).

Constants for STM8 Peripheral Addresses

Programming microcontrollers isn't just about algorithms - it's also about "programming" peripherals like ports, timers or serial interfaces (e.g., UART, SPI or I2C).

STM8 eForth provides symbols for addresses of peripheral registers and memory areas (e.g., EEPROM). With the help of e4thcom or codeload.py, using \res MCU: and \res export, these can be exported as a CONSTANT. Please refer to the Programming Tools and the Example Code pages).

Most of the STM8S ([RM0016(https://www.st.com/resource/en/reference_manual/cd00190271-stm8s-series-and-stm8af-series-8-bit-microcontrollers-stmicroelectronics.pdf)) peripheral registers addresses are the same across "Low density" and "Medium" or "High density" devices and device independent code can be written by using \res MCU: STM8S.

Using e4thcom as a terminal program is recommended. With e4thcom (or codeload.py) mcu/STM8S.efr can be used to load peripheral register address constants:

\res MCU: STM8S
\res export SPI_CR1 SPI_CR2 SPI_DR SPI_SR

Register addresses of the timers TIM2 and TIM4 are different in STM8S "Low Density" devices like the STM8S103F3. Address constants for these peripherals can be imported with, e.g., \res MCU: STM8S103.

UART constants are again a special case: writing device independent code works best if a sub-set of registers with a UART_ prefix is used. This can be achieved by using \res MCU: STM8S103. The "device independent" \res MCU: STM8S can also be used by reassigning constants (e.g. UART1_DR CONSTANT UART_DR). More on this in "Some STM8S peripherals are equal, some are the same on Hack-a-Day".

The Compiler: Defining Words

Like CONSTANT and VARIABLE the word : (colon) is a "defining word" that consume a text symbol and create a dictionary entry from it.

Since Forth is a language with self-reference, the Forth console is the interpreter (also written in Forth) and the compiler simply extends the interpreter by placing machine code into a dictionary. The Forth console can thus be used for defining new words. Usually a Forth programmer breaks the problem down into easily testable words (units of code) and then defines new words that contain "phrases" of already defined words.

This enables a bottom-up approach to programming: through program-test iterations and refactoring, the programmer creates a "domain specific language" (for most programming languages building domain specific languages is an advanced feature). Please refer to Thinking Forth for an in-depth discussion of the method.

Defining new words is very simple: write : followed by the name of the new word, a sequence of already defined words and ; marks the end:

:                    \ enter compilation state and start word definition
  by-2   ( n -- n )  \ "by-2" is the new dictionary entry ( comment w/ stack effect )
  -2 *               \ compile: multiply "n" by "-2"
;                    \ finalize dictionary entry, leave compilation state

10 by-2 .<enter> -20 ok

The sequence ":, identifier, code, ;" is called a "colon definition". The word : brings Forth into compilation state.

From now on, all the Forth examples will contain a Brodie Style stack comment: ( n -- n ) indicates that the new word by-2 consumes a "single-length signed number" and puts another one on the stack.

In a simple Forth, like STM8 eForth, there is next to no syntax checking (in Forth syntax is a convention). There is only one requirements for the word "identifier": it must consist of one or more printable characters that's not "whitespace" (i.e. numbers, punctuation marks, etc, are valid). Forth words can be redefined but definitions compiled before the redefinition won't change (they will continue to use the old word).

There are words that can only be used during compilation (e.g. ; which terminates a colon definition). Other words will be executed immediately in compilation mode (by using the IMMEDIATE modifier). It's also possible to write words that do one thing in interpreter mode and anther during compilation (execution semantics sensitive words, e.g. CONSTANT).

Compiling to RAM or Flash Memory

STM8 eForth can compile code to RAM or to NVM (Non Volatile Memory, Flash ROM).

When the system is in RAM mode, new words will be compiled into RAM and dictionary entries will be in RAM. After switching to NVM mode, the compile target for code and dictionary entries is the Flash memory:

\ we're in RAM Mode
42 CONSTANT TheAnswer

NVM
  : TellMeTheAnswer ( -- )
    CR ." The answer is" TheAnswer .
  ;
RAM

Note that NVM mode must be terminated by the word RAM or WIPE or newly defined words will be lost! Please refer to STM8 eForth Compile to Flash for more information.

In other words, the definitions between NVM and RAM will still be there after you switch you board off and on again. If you want to start over, and remove all those words, you can use the word RESET. If, for some reason, you want some words to "survive", you can protect them with the library word PERSIST (after which re-flashing you device is the only obvious way to remove them).

In NVM mode STM8 Forth performs a simple form of RAM management for variables, which, in most cases, is completely transparent. When you need to allocate more than 16 cells (with VARIABLE or ALLOT) in one session, or if you're a Forther and want to understand why the example above doesn't use CREATE, please refer to Forth VARIABLE in NVM mode.

Control Structures in Forth

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 ;

0 test<enter> false ok
5 test<enter> true ok

This looks very unusual, indeed. If you think of THEN as ENDIF it's easy to get used to it.

Note that in Forth executable code is data, too. The compiler simply translates one stream of data (text) into another stream of data (code). Structure words like IF, ELSE, or THEN rely on storing addresses for yet unresolved branch targets (HERE) on the data stack, as if it were an arithmetic expression.

Of course, there are also conditional and unconditional loops:

Here is an example for a conditional loop:

: waitforkey ( -- )
  CR ." Press a key!"
  BEGIN
    ?KEY
  UNTIL
  CR ." you pressed " EMIT
;

Unconditional loops are only a good idea if you don't plan to interact with your system:

: foreever ( -- )
  CR ." Now I'll loop forever!"
  0 BEGIN
    1+ DUP 64 = IF DROP 0 CR THEN
    ." ."
  AGAIN
;

Note that in most cases conditions can be seen as "transitions of a state machine") and a Background Task (cycles have a fixed cadence) or an Idle Task can be used for running the state machine.

Interpreter and Compiler Interaction

When Forth compiles a program it normally just interprets the text, but after finding a word in the dictionary it writes the "execution token" to the target memory instead of executing it. During compilation the interpreter can be enabled temporarily and the interpreter works a bit like the C pre-processor:

: param ( -- n )   \ use the interpreter for calculating a parameter
  [ -622 37 100 */ ] LITERAL ; \ only store the result of the calculation
param .<enter> 230 ok

[ switches from the compilation to the interpreter state. The code until ] is executed and the result is then pushed to the data stack. The immediate word LITERAL compiles the a stack value as a constant into the word param. The action of the words [ and LITERAL is possible because their dictionary entry carry the IMMEDIATE flag.

Note that any new word can be flagged for immediate execution by putting the word IMMEDIATE after ; when compiling it. As stated before, in Forth "code addresses" are data. Control structure words can be created by simply defining a new word that puts code addresses on the stack for later consumption. Here are the definitions of the control words BEGIN, UNTIL and AGAIN:

: BEGIN ( -- a )
  here ; IMMEDIATE
: AGAIN ( a -- )
  COMPILE branch , ; IMMEDIATE
: UNTIL ( a -- )
  COMPILE ?branch , ; IMMEDIATE

This simple going back-and-forth between the modes "interpreter" and "compiler" is indeed all that's needed for programming a compiler extension: during compilation the IMMEDIATE words will be executed, not compiled.

During execution of an immediate word during compilation, any word in it that has been prefixed with COMPILE gets compiled into the memory stream at the next address (here) and not executed.

In order to compile an immediate word (like BEGIN), instead of executing it during compilation, it needs to be prefixed with [COMPILE].

Please note that from STM8 eForth 2.2.28 on the more modern POSTPONE will do the work of both COMPILE and [COMPILE].

Some words like ?branch or LEAVE crash the system when executed outside of their structure. The library word COMPONLY can be used to flag a just compiled word so that the interpreter will return an error message instead of executing it.

Using the Return Stack

Forth provides access to a second stack, the return stack. In STM8 eForth this is the same stack that the STM8 controller uses for subroutines (CALL), interrupts context, or C parameter passing. In compilation mode, the return stack can be used to store temporary values, for more complicated data flows, or even for manipulating the program flow.

The word for moving data between the data stack and the return stack are:

  • >R pop a word from the data stack and push it to the return stack
  • R@ read the top of the return stack and push it to the data stack
  • R> pop a word from the return stack and push it to the data stack

Using the return stack should be done with care: any Forth programmer quickly learns that one >R or R> too much will crash the program!

Here is an example:

: RROT ( n1 n2 n3 -- n3 n1 n2 )
  SWAP >R SWAP R> ;

RROT rotates the 3 topmost values on the stack in the opposite direction of ROT: 1 2 3 RROT will result in 3 1 2 on the stack (an alternative implementation would be : RROT ROT ROT ; but the one above is faster).

The simple fact that >R and R> dictionary entries have the "compilation only" (COMPONLY) flag set protects from crashing the Forth system just by entering them on the console.

Counted Loops in STM8 eForth

STM8 eForth, like all eForth implementations, provides FOR .. NEXT as the basic counted loop structure:

: countdown ( n -- ) 
   FOR I . NEXT ;

9 countdown 9 8 7 6 5 4 3 2 1 0 ok

FOR .. NEXT keeps the loop counter on the return stack (I is an alias for R@).

Note that "pure eForth" uses idiomatic structures with AFT and WHILE that have rather unusual (surprising, idiosyncratic, peculiar...) properties - refer to eForth FOR .. NEXT for details.

Most Forth dialects provide the loop structure DO <condition> IF LEAVE THEN +LOOP. Unlike "pure eForth" STM8 eForth provides this structure, too!

DO .. LOOP not only keeps both the loop counter and the limit on the return stack. The loop increment can be any number (also negative) and it's easy to LEAVE a loop without needing to clean up the return stack.

Here is a basic example:

: test-doloop ( n n -- )
   DO
      I .
   LOOP ;

The input 5 -3 test-doloop results in the output -3 -2 -1 0 1 2 3 4 ok.

Sometimes when one uses nested loops it's necessary to access the outer loop counter. In that case a bit of return stack yoga can help:

: asctable ( -- )
  5 FOR CR 15 FOR 
    127 R> R@ SWAP >R 16 * I + - EMIT 
  NEXT NEXT
;
asctable
 !"#$%&'()*+,-./
0123456789:;<=>?
@ABCDEFGHIJKLMNO
PQRSTUVWXYZ[\]^_
`abcdefghijklmno
pqrstuvwxyz{|}~ ok 

The "phrase" R> R@ SWAP >R does something that can't be directly turned into a Forth word since calling that word would add one more level to the return stack. Many Forth systems provide a word J that's like I for the index of an outer loop. STM8 eForth doesn't provide such a word since DO ... LOOP and FOR ... NEXT would require a different implementation for J depending on the type of the inner loop.

Using return stack manipulation in this example is, of course, for demonstration. It's also possible (and easier) to use the data stack for bringing the outer loop counter into the inner loop:

: asctable ( -- )
  5 FOR CR I 15 FOR 
    127 OVER 16 * I + - EMIT
  NEXT DROP NEXT ;

Here, I is used both for the outer and for the inner loop counter. OVER copies the outer loop counter in each inner loop cycle. DROP removes it from the stack at the end of the inner loop.

With DO .. +LOOP any loop increment can be used, including negative numbers or even variables:

: test-do+loop ( n n -- )
   DO 
      I .
   -2 +LOOP ;
11 20 test-do+loop 20 18 16 14 12 ok

Aborting counted loops with LEAVE is straightforward: it automatically removes loop counter and limit from the return stack:

: test-leave ( n n n -- )
   DO
      I .
      DUP I = IF
         ."  leaving!"
         LEAVE
      THEN
   LOOP DROP ;
2 4 0 test-leave 0 1 2 leaving! ok

Note that the DO..LOOP structure can removed from the core if memory is an issue (as the eForth system shows the "pure eForth" counted loop structure FOR .. NEXT usually gets the job done).

Defining Start-up Code

For embedded Forth applications it's necessary to do initialization, start a background tasks and to run the actual control code.

The following example defines a simple greeting word before falling back to the console loop:

NVM
: mystart ( -- )
   CR 3 FOR
      I . 
   NEXT CR ." Hi!" CR ;

' mystart 'BOOT !
RAM

NVM switches to Flash mode. mystart is the word that's to be run as start-up code. ' (tick) returns the aaddress of the following word mystart, 'BOOT returns 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.

Setting Board Outputs

The STM8 eForth board support provides the word OUT! for setting the binary state of up to 16 relays, LEDs or other digital output devices specific to that board (new board support packages should the mapping in board.fs or in boardcore.inc).

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) ):

: task ( -- )
   TIM 128 AND IF 
      1 
   ELSE 
      0 
   THEN OUT! ;

' task BG !

You can set individual port bits with the STM8 eForth word B! (bit store) which accepts "BitState, Address and Bit#" as input from the stack:

    1 $5011 0 B!   \  PD0 is output in PD_DDR
    0 $500F 0 B!   \ led ON (low) in PD_ODR
    1 $500F 0 B!   \ led off

B! can be used, e.g. for defining a word that sets the LED of the LED of STM8S Discovery:

: PD0! ( n -- )  \ write to PD0
   1 $5011 0 B!
   ( n ) 0= NEGATE $500F 0 B! ;

: OUT! ( n -- )  \ overwrite OUT! to use the STM8S Discovery LED
   PD0! ;

Note that B! isn't the fastest or the most compact way to manipulate bits: STM8 eForth library words like ]B!, ]BCCM, ]BC or ]CB compile bit manipulation instructions directly into the code.

You can find all the register addresses in the specific MCUs .efr file.

The easiest way is using the \res feature of e4thcom and codeload.py:

\res MCU: STM8S103
\res export PB_ODR PB_DDR
#require ]B!

: LED.init ( -- ) [ 0 PB_DDR 5 ]B! ;
: LED.on ( -- )  [ 0 PB_ODR 5 ]B! ;
: LED.off ( -- ) [ 1 PB_ODR 5 ]B! ;

Since the library function ]B! produces the STM8 instructions BSET or BRES the code density of LED.on and LED.off is optimal.

The WS2812 demo is an example for time-critical code that mixes Forth and machine code instruction.

7S-LED Display Character Output

STM8 eForth supports a range of low-cost control boards and printing data to LED or LCD displays is supported by vectored I/O. In background tasks the EMIT vector points to E7S and using it is as simple as printing to the console.

The LED display is organized in right-aligned digit groups (e.g. boards W1219 2x 3-digits, W1401 3x 2-digits, XY-LPWM 2x 4-digits) that all work similar to the display of a pocket calculator:

  • CR moves the cursor to the first (leftmost, upper) 3-digit group without blanking it
  • SPACE, after printing at least one printable character, moves the cursor to the next group and blanks it
  • . is rendered as DP without shifting characters to the left
  • , is rendered as a blank

Emitting more than the characters of a group won't spill into the next group (23..,5 is rendered as 3. 5).

Please refer to STM8 eForth Board Character IO for a detailed discussion and example code.

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:

: timer ( -- n )
   TIM 655 / ;

: ain ( -- n )
   5 ADC! ADC@ 100 1023 */ ;

: show ( -- )
   timer . ain . BKEY . CR ;

' show BG !

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.

Reading an Analog Port

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:

: conv ( -- n )
   ADC! ADC@ ;
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!.

Note that in STM8Sx003F3P6 chips, AIN5 and AIN6 are an alternative function of the ports PD5 and PD6. These GPIO pins 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).

Defining Defining Words with CREATE .. DOES>

As an extension of eForth, STM8 eForth supports defining defining words with CREATE..DOES>. Defining words, like CREATE, VARIABLE, or : can be compared to classes with a constructor and a single method in an OOP language (e.g. Java).

As an example, the definition of the defining word VALUE, which is similar to CONSTANT:

: VALUE CREATE , DOES> @ ;

New VALUE instances can now be defined with in the following way:

10000 VALUE ONE
31415 VALUE PI

: circumference ( n -- C )
   2* PI ONE */ ;

500 circumference . 3141 ok

The clause between CREATE and DOES> is executed during "define time", whereas the clause between DOES> and ; is the run time part of the new word (note: DOES> replaces the normal run time code of CREATE at compile time).

For prototyping and for high-level code CREATE .. DOES> is useful and the overhead it causes doesn't matter. For frequently used defining words (e.g. CONSTANT, or VARIABLE) directly coded compile time and run time words are better.

Here is an example from Forth.com:

DECIMAL

: HASH ( -- )
  42 EMIT
;

: .row ( c -- )
  CR 1 7 FOR
    2DUP AND IF HASH ELSE SPACE THEN 2*
  NEXT 2DROP
;

: SHAPE ( idstr 8-times-n -- )
  \ define time behavior
  CREATE 7 FOR 
    C, 
  NEXT
DOES> 
  \ run time behavior - gets reference to define time data
  ( a ) DUP 7 FOR
    ( a ) DUP I + C@ ( a c ) .row 
  NEXT
  ( a ) DROP CR
;

HEX AA AA FE FE 38 38 38 FE SHAPE castle
7F 46 16 1E 16 06 0F 00 SHAPE F

The new word SHAPE uses CREATE to build a new "CREATE-like" defining word. SHAPE uses a FOR .. NEXT loop for storing 8 bytes in the array it will create when SHAPE is executed. This is called the define time behavior. DOES> links a run time behavior to words defined with SHAPE (in the same way as CREATE normally returns the address of the defined array).

When we define a word like F or castle with SHAPE, the definition consumes 8 words from the stack and stores it in the dictionary together with a the reference to the run time code.

We can use F like any other Forth word:

F<enter>
#######
 ##   #
 ## #
 ####
 ## #
 ##
####
    
 ok

When we execute F, the run time code receives the address of the 8 byte array stored at define time and the run time code is applied to it.

Example STM8 Forth Code

The following section contains some simple idioms, patterns and example programs. The page STM8 eForth Example Code contains links to projects, libraries and demos and the Wiki sidebar lists detailed technical information with more examples.

Note that Forth code the library folder often contain usage examples in the comment section.

Recursion in STM8 eForth

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 ! ;<enter> 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 will be delayed until ; gets executed.

The following example shows how to do recursion in eForth:

: RECURSE ( -- ) last @ NAME> CALL, ; IMMEDIATE

The variable last is a key feature of a Forth compiler: after executing : it points to the name of the (unfinished) new word. NAME> turns that address into the code address of the new word even before it's been added to the dictionary, and with CALL, a call to that address is compiled into the same new word. Finally, IMMEDIATE makes sure the RECURSE gets executed during compilation, not compiled.

The fibonacci function demonstrates a recursive reference with f(n-1) and f(n-2).

#require RECURSE

: fibonacci ( n -- n )
   DUP ( n n ) 2 < IF 
      ( n ) DROP 1
   ELSE ( n )
      1- DUP  (  n-1   n-1 ) RECURSE (   n-1  f[n-1] )
      SWAP 1- ( f[n-1] n-2 ) RECURSE ( f[n-1] f[n-2] ) + 
   THEN
;

15 fibonacci . 987 ok

On an STM8S clocked with 16Mhz, the 27199 calls of 23 fibonacci (the maximum for 16bit arithmetics) take about 2.6s from RAM and 2.3s from ROM. While that's no big problem for the stack this is a very long time for most embedded applications (one would use a table lookup or "dynamic programming" if recursion is really required).

Tree Traversal

On a µC with limited RAM, recursion should be used with care. However, for some algorithms, e.g. tree traversal, recursion is the got-to solution. The following code demonstrates 3 types of tree traversal, a tree metric and building tree data structures:

#require RECURSE

\ 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

Note that the example traversal code (e.g. preorder) accepts the address of an action word (here it is .). Of course, most practical applications (e.g. binary search) require some changes, but Forth makes very lightweight and powerful solutions possible. This example should also give you an idea of the code density: the 32 lines of code, including tree definition and 4 traversal routines, compile to 385 bytes of binary code. In practical applications just one traversal routine is needed and tree building words might only be needed at compile time. Building trees at runtime, however, is possible, too: the compiler can be used in an embedded system!

Copying characters into a buffer

Loops are also powerful ways of copying data one character at a time into or out of a buffer. The following example demonstrates one way of doing this. It was taken from the nRF24 library under development and has been heavily commented with stack notations to help showcase Forth.

: nRF>b ( a c r -- s )  
\ copy count c bytes from reg. r to buffer at address a  
\ reg. r is on the nRF24 device and can be up to 32 bytes long.  
\ It holds the message the nRF24 received from another nRF24 transmitter  
\ return nRF24 STATUS s  
   _CSN.LOW ( a n r -- ) \ this definition pulls a pin low as a precursor to SPI communication  
   SPI ( a n s -- )      \ we have send the register r to the nRF24 and the status bytes was returned  
   >R ( a n -- )         \ put the status byte onto the return stack for now  
   0 DO (  a -- )        \ start a DO...LOOP   
      -1 SPI ( a c -- )  \ here we transmit a dummy byte, -1 could be anything,  
                         \ and the SPI command returns the next character in the nRF24 message buffer  
      OVER ( a c a -- )  \ bring a copy of a to the top of the stack   
      C! ( a -- )        \ c was stored to address a as 8 bit character ( C!)   
      1+ ( a -- a+1 )    \ now add 1 to the address ready to store the next character  
   LOOP                  \ go do it all again until this has been done n times  
   _CSN.HIGH ( a+n --- ) \ pull this pin high again, the address pointer is still on the data stack  
   DROP                  \ drop the address pointer from the data stack  
   R> ( -- s)            \ move the status register from the return stack back to the data stack 
                         \ Caution: the return stack must be identical to what it was when this definition was called  
                         \ since the top of the return stack is the address telling Forth where to jump to next  
;  

Of course, one of the features of Forth loops is they don't have to start at 0. The do loop could have been written as follows (with less comments this time) :

: nRF>b ( a c r -- s )  
   _CSN.LOW ( a n r -- ) 
   SPI ( a n s -- ) 
   >R ( a n -- ) 
   over + swap ( a n --- a+n a  )
   DO ( a+n a -- ) \ our do loop index will start at a and end at a+n-1
      -1 SPI ( -1 -- c )
      I ( c -- c a ) \ I is now the address pointer
      C! ( c a -- ) \ c was stored to address a as 8 bit character ( C!) 
   LOOP 
   _CSN.HIGH 
   R> ( s ) 
; 

One of the great things about Forth is that it "extends" to suit the way you, the programmer, might think about coding.

Further Reading

The introduction on this page, among other topics, doesn't cover working with temporary words in RAM, using aliases for accessing unlinked words in the binary, handling Interrupts in Forth and other technicalities. Please refer to the links at the beginning of the page and in the sidebar!

Many skilled programmers have worked with Forth for decades and it has a rich tradition. There are many useful resources on-line and the following free books are recommended:

  • Starting Forth, which provides a friendly introduction to the programming language (most of the examples work with STM8 eForth)
  • Thinking Forth by Leo Brodie as an introduction to the Forth programming method.

Congrats if you made it to this point - if you liked it please leave a star!

If you have comments or questions please drop a note on Hack-a-Day or write a GitHub Issue.

Clone this wiki locally