@Elliot Williams convinced me that writing interrupt handlers in Forth instead of assembly (or C) is a Cool Thing. I had pondered a lot about how to do that with the least amount of overhead but it took a lengthy discussion with Elliot to get it right!
There was a problem to solve: the STM8 register X is the Data Stack pointer, and it's also needed for implementing certain core words. The assumption that "X is TOS" isn't always justified. There is no way around this, except by blocking interrupts, or by rewriting code so that X always represents a valid stack pointer (with undesired effects like increased code size or longer runtime).
My "the most simple thing that works" solution assigns a small clean data stack to user-defined interrupts. The data stack is just 8 cells deep, but that should be more than sufficient (please read below why).
Interrupt handlers in Forth code are now based on the following words:
- SAVEC to save the context
- IRET to restore the context and return from the interrupt
- IVEC! to set an interrupt vector
Writing an interrupt handler is easy - here is an AWU (Auto Wake Up) handler as an example:
: IVEC! ( a n - - ) 2* 2* $800A + ! ; nvm : HALT ( -- ) [ $8E C, ] ; : awuint SAVEC awu_csr1 c@ IRET ; : initawu 38 awu_apr c! 1 awu_tbr c! 16 awu_csr1 c! ; ' awuint 1 IVEC! ram
The interrupt handler awuint first does a Forth VM context switch with SAVEC. Reading awu_csr1 clears the AWU interrupt flag, and IRET restores the Forth VM context and returns to the interrupted code with IRET. It's not necessary to leave the stack balanced (otherwise DROP would be required). The word IVEC! stores the address of our new hander to interrupt vector 1 (the AWU interrupt in Flash memory). Since IVEC! is only needed at "compile time" it can be compiled to RAM and doesn't need to be part of the Forth image.
The word HALT encodes the STM8 HALT instruction that shuts down the CPU clock until an interrupt occurs (I first added a word HALT to the Forth core but this user code implementation is just as good). When I run HALT with this code, it returns because of the Auto Wake Up interrupt. Note that I
didn't find the time to make sense of the AWU configuration, and I simply
took the AWU timing values from
When writing interrupt handlers in Forth, I would like to recommend the following practice:
- interrupts should only be used for low-level code, e.g. for using µC peripherals that require interrupts to work
- be careful about data- and return stack use - 5 levels deep should be plenty!
- the code should be fast - if in doubt use pin-debugging with a scope or a simple logic analyzer for testing the timing
- only do the data processing that's absolutely required for meeting your application's timing constraints. The rest should be done in a low-priority task (e.g. background)
- simplicity is important: don't use any fancy high-level words (e.g, output string formatting shouldn't be used)
- one should be carefully assess potential side effects of character I/O (if required!)
- in many cases you'll need the µC manual to understand what your code is doing, and fancy abstractions won't make your code more readable
Working code with SAVEC and IRET is in the develop branch on GitHub. I'm still working on some details but it will be part of the next release.