Skip to content

STM8 eForth Interrupts

Thomas edited this page Mar 5, 2022 · 21 revisions

Low-level Interrupts in Forth

Interrupt service routines (ISR) can be coded in Forth with the help of the STM8 eForth lightweight-context-switch feature where almost all aspects are under your control. As long as you follow certain guidelines interrupt routines can be written in plain Forth. For writing time-critical routines that require mixing Forth with STM8 assembler library words are provided.

The primary use cases for ISRs are:

By default the data stack is 8 cells deep - this should be more than sufficient for most applications since ISRs usually apply simple algorithms and don't do number formatting or parsing beyond simple state machines.

Low-level interrupts have to run with the highest "software priority" (which is the default). This also means that ISRs should have a higher priority than the background task. Interrupts at the same "software priority level" can't preempt one another and will be chained (except for the TLI, but see below).

Care should be taken when the "simulated serial interface" (e.g. SWIMCOM) is used: in order not to jam the serial communication, any interrupt code should set ITC_SPRx to a lower priority than the T4 timer (bit sampling) and the RX edge detection interrupt.

Note that STM8 eForth also has a Background Task feature for executing input-processing-output code with a constant rate, and the Idle Task, e.g., for implementing higher level protocols with string evaluation. A layered approach (ISR, Background Task, Idle Task, REPL) should offer a solution for most problems.

Low level interrupt code uses the following words:

  • SAVEC: save the context
  • IRET: restore the context and return from the interrupt

Interrupt entry point doesn't require a dictionary entry - it's good practice to use :NVM SAVEC .. IRET ;NVM INT_.. ! (see example below).

Debugging interrupt code is difficult (real time, context change) but it's often possible to test ISR code interactively if SAVEC and IRET are removed (or temporarily replaced by dummy words). The MODBUS and nRF24L01 libraries in the STM8 eForth Example Code were developed using mixed interactive and "pin-debugging" methods.

Guidelines for STM8 eForth Interrupt Code

For writing interrupt handlers in Forth the following practice is recommended:

  • care should be taken regarding data- and return stack use (8 levels should be more than sufficient, the return stack must be balanced)
  • only the data processing that's absolutely required should be performed in interrupt routines. The rest should be done in a low-priority task (i.e. Background Task or Idle Task)
  • the code should be fast - if in doubt use pin-debugging with a scope or a simple logic analyzer for testing the timing
  • consider avoiding literals - use machine code generating words like [ a ]@ instead
  • high-level words, e.g, output string formatting, should not be used since these require a more comprehensive context switch (example: background task code in [bgtask.inc](https://github.com/TG9541/stm8ef/blob/master/inc/bgtask.inc))
  • using character I/O should be avoided (otherwise potential side effects should be carefully assessed)

Fast and lean control structures with >REL

The library word >REL provides an overlay for IF ... ELSE ... THEN using relative addressing. Code blocks in between IF, ELSE and THEN must not exceed 127 bytes (which should be sufficient for well factored code). The real benefit of this approach, however, isn't speed or code size but the possibility to use in-line assembly for creating special IF words like so:

#require >REL

: ]B@IF ( -- ) 2* $7201 + , , ] >REL ;  \ BTJF  a,#bit,rel

NVM
VARIABLE vt

: testbit  [ vt 1 ]B@IF ."  set" ELSE ."  not set" THEN ;
RAM

A number of IF words are provided in the library, e.g: [ .. ]Y<IF, [ .. ]A<IF, [ .. ]@IF or [ .. ]C@IF. Other examples for constructing "]IF words" like [ .. ]B@IF are provided in the "Examples" section of >REL.

The I2CMA I2C Master driver) is a more elaborate example for mixing inline assembler with Forth control structures.

TLI is a "Danger Zone" for STM8 eForth Interrupts

The "Top Level Interrupt" TLI (not available in all STM8 devices) can preempt other ISRs. Since STM8 eForth assumes that ISRs can't preempt one another all ISRs share a data stack. If a TLI ISR is to be used together with other ISRs a variant of SAVEC for the TLI will have to be used that initializes a different data stack memory area (except if the TLI ISR is designed not to use any Forth words).

When writing an ISR in Forth for the TLI (i.e. the non-maskable external Top Level Interrupt), Forth literals (i.e. numbers) must not be used because these use the TRAP instruction which is also a non-maskable (software-) interrupt. In any ISR it's recommended to use machine code generating words like [ a ]@ or [ c ]C! that avoid using literals. For loading a literal to the stack in-line machine code can be used in combination with A> or Y>.

Please note that the STM8 eForth core words PAD, NUF?, PARSE, .(, <#, find, VARIABLE, TIB, COLD ans RESET use literals. While it's hard to see the purpose of any of these words in ISR code, these words can't be used in TLI-ISR code!

Wake-Up Timer Interrupt Example

The following code implements an AWU (Auto Wake Up) handler is a simple example.

The AWU is used to provide an internal wake-up time base that can be used to return from the MCU "Active-Halt" power saving mode. This time base can be clocked by the low speed internal (LSI) RC oscillator clock or by the HSE crystal oscillator clock (both divided by a prescaler). Usage of the register names commencing with "AWU_" below are explained in the STM8S Reference Manual, Chapter 12.

RAM
#require :NVM

\res MCU: STM8S103
\res export AWU_APR AWU_TBR AWU_CSR1 INT_AWU

DECIMAL

:NVM              \ interrupt handler, "headerless" code
   SAVEC
   AWU_CSR1 C@
   IRET
;NVM ( xt ) INT_AWU !

: initawu  ( -- ) \ AWU period about 1s
  31 AWU_APR C!   12 AWU_TBR C!   16 AWU_CSR1 C!
;

: HALT ( -- )    \ encodes the STM8 `HALT` instruction
  [ $8E C, ]
;
RAM

\\ Example
initawu HALT \ test - will return after the AWU period

The "nameless" interrupt handler is initiated with :NVM (put start address on stack, switch to NVM mode, start compiler mode). It first does a Forth VM context switch with SAVEC. Reading AWU_CSR1 then clears the AWU interrupt flag, and IRET restores the Forth VM context, and returns to with the STM8 IRET instruction. The stack can be left unbalanced (DROP isn't required). ;NVM switches back to interpreter mode, and the start address of the handler (the execution token xt which is still on the stack) is then written to the AWU interrupt vector.

initawu initializes the Auto Wakeup Timer with about 1s delay, and the word HALT encodes the STM8 HALT instruction that shuts down the CPU clock until an interrupt occurs.

When initawu followed by HALT is executed, the CPU stops, and returns through the execution of the Auto Wake-Up Timer interrupt.

Restart on interrupt example

This example arose in response to a need to restart the STM8 in response to an interrupt on Port A2 when a switch to ground was closed. A number of peripherals were hanging off the STM8 and when resuming from a 'HALT' all had to be re-initialised.

In my case, the external interrupt could be weeks after the 'HALT' was executed. Instead of jumping back into the setup procedure for the peripherals, it was more expedient to simply force the STM8 to restart. I had no data that needed to be preserved since "HALT" was called so a restart was expedient and avoided the unlikely possibility that stray electric and magnetic fields had corrupted the RAM since last being turned. Here is how I did it:

: SLEEP \ Put the STM8 to sleep with minimal current draw
   PortPrep \ set all ports to input, with pull-up resistors enabled
   [ 1 PA_CR2 _MUT ]B! \ enable ext int. on Port A2 _MUT is a constant of 2
   [ $8E C, ] \ the HALT instruction
;

\ EXTI0 handler, forces power on reset
:NVM               \ interrupt handler, "headerless" code
   SAVEC           \ unnecessary in this example?
   0  WWDG_CR C! \ Saving 0 into the Watchdog Control Register forces an immediate power on reset cycle
   IRET            \ unnecessary in this example?
;NVM ( xt ) EXTI0 ! \ Save the interrupt handler code into the interrupt table

The interrupt handler in this instance may have worked without SAVEC and IRET, since a full power on reset cycle is triggered before the IRET is reached. This effectively formed an "ON" switch but saved me fitting another switch to a device which was space constrained, being some 80mmx50mmx8mm in volume.

Using the Pin-Change interrupt

STM8S can configure interrupts "per-port" and STM8L either "per-pin" or limited "per-port" on some ports (see architecture differences below).

@Danya0x07 figured out the hard way that both cores have in common that configuring the interrupt mode through EXTI_CRx requires a SIM ... RIM sequence. That had already been documented in the example code "STM8L051F3: a low power Forth console" but not in the Wiki.

So here it is. Consider the following code:

\ Temporary words in RAM

\res MCU: STM8S103
\res export EXTI_CR1

\ extend the compiler - in Forth that's easy!
: SIM $9B C, ; IMMEDIATE  \ disable interrupts
: RIM $9B C, ; IMMEDIATE  \ enable interrupts

\ The following will be compiled into the Flash ROM
NVM
  : check 
    EXTI_CR1 C@ . ;

  : PcIntOn
    48 EXTI_CR1 C! ; \ PortC interrupt on

  : init
    SIM PcIntOn RIM ;
RAM

The words SIM and RIM extend the compiler - they'll compile the corresponding machine code instructions. The word check is just for demonstration purposes and in practice there is no point using PcIntOn since it can be better inlined (e.g. using : init SIM [ 48 EXTI_CR1 ]C! RIM ;).

The following demonstrates that EXTI_CR1 is only writable while interrupts are disabled:

check 0 ok
PcIntOn ok
check 0 ok
init ok
check 48 ok

You may also want to check out @Danya0x07's fine project and especially the code in encoder.fs!

Architecture differences in STM8 Families

Unfortunately there are significant architecture differences between STM8S and STM8L which often require different implementations of ISRs. For STM8 family independent functionality in libraries it's necessary to separate low level interrupt configuration and peripheral handlers from higher level control.

As an example, the STM8 Families STM8S and STM8L have strikingly different architectures, and trade-offs for external interrupt configuration:

STM8S uses "configuration per port" for PA, PB, PC, PD and PE:

  • STM8S assigns one interrupt vector and one edge/level configuration to a port (EXTI_CR1 for ports A through D, EXTI_CR2 for port E)
  • interrupts are enabled or disabled per GPIO (Px_CR2)
  • interrupts remain active as long as the interrupt condition is present
  • the ports PF, PG, PH and PH can't produce external interrupts
  • one TLI (top level interrupt) is assigned to either PC3 or PD7

STM8L provides more versatile, but also more complex features (see STM8L reference manual 12.6))

  • configuration per port (PB/PG, PD/PH, PE/PF) or per GPIO index Px0 ... Px7 (PA, PB, PC, PD, PE) and PF0
  • for both modes interrupts are enabled or disabled per GPIO (Px_CR2)
  • interrupts remain active unless EXTI_SRx is cleared by writing a 1 to the corresponding bit

Other peripherals often have less obvious or minor differences, however, sometimes key features, like STM8L USART DMA, make family dependent code rather attractive.

Clone this wiki locally