Skip to content

Instantly share code, notes, and snippets.

@TG9541
Last active March 2, 2023 19:00
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TG9541/61db6e1bdef35d10a7aa02e321d99aa6 to your computer and use it in GitHub Desktop.
Save TG9541/61db6e1bdef35d10a7aa02e321d99aa6 to your computer and use it in GitHub Desktop.
STM8L051F3: low-power Forth console experiments

STM8L051F3: a low power Forth console

In the stopped state the STM8L needs just a few µA - but in this state the console can't wake up the device: the USART gets no clock. Only GPIO interrupts and peripherals that are clocked from the LSI or the LSE will work! A possible solution is a pin-change interrupt on RxD for waking the core - and the USART - up. In "Active-Halt" mode RTC and WUT peripherals run on 38kHz LSI or 32.768kHz LSE clock (see here). This means that it's possible to have a background task without keeping the core busy.

One of the challenges ist that STM8L docs doesn't describe the USART behavior if the clock is turned on after the start-bit edge, i.e., if the character that wakes the core from "Active-Halt" can still be received.

A pin change interrupt

The first step is a "falling-edge pin change interrupt" - it can trigger on the falling edge of PA3/USART_RX (which is used in the STM8 eForth 2.2.26 binary fpr STM8L051F3).

RM0031 12.6 "External interrupts" shows the structure of pin change interrupts. I'll use a "bit-x" not the "port-x" option. Using this option requires the interrupt to be acknowledget by setting the corresponding bit in EXTI_SR1 (RM0031 12.9.7). The interrupt priority configuration in RM0031 12.9.1 and 12.9.2 is of no importance since we only want to exit HALT mode.

For each port bit interrupt "edge" or "level" configuration has to be done with EXTI_CR1 for port bits #0..#3 or EXTI_CR2for bits #4..#7 with a pair of bits. RM0031 12.9.3 and 12.9.4 shows the configuration of the interrupt condition in EXTI_CRx:

Bits Configuration
00 Falling edge and low level
01 Rising edge only
10 Falling edge only
11 Rising and falling edge

The text states that "these bits can only be written when I1 and I0 in the CCR register are both set to 1 (level 3)" but the meaning of this isn't self-evident: both flags are set after RESET, TRAP or when interrupts are disabled using the SIM instruction (POP CC will also work, see RM0031 12.7).

This means that setting EXTI_CRx will work in BOARDINIT (in boardcore.inc) but it will require a SIM ... RIM sequence after COLD.

Of course, I didn't know that right away. I learned it while trying to figure out why the ISR ran at a cadence of aboyt 8.5µs during the low-time of the serial interface:

level interrupt

Here is the code that works as intended:

\res MCU: STM8L051
\res export PA_DDR PA_CR1 PA_CR2
\res export EXTI_SR1 EXTI_CR1 INT_EXTI3

#require ]B!
#require :NVM

3 CONSTANT RXBIT

$9A CONSTANT RIM    \ STM8 assembler enable interrupts 
$9B CONSTANT SIM    \ STM8 assembler disable interrupts

NVM
  VARIABLE telly 

  :NVM 
    SAVEC
    [ 1 EXTI_SR1 RXBIT       ]B!  \ 12.9.7 aknowledge interrupt
    1 telly +!
    IRET
  ;NVM  INT_EXTI3 !

  : init
    0 telly !
    [ SIM C, ]
    [ 1 EXTI_CR1 RXBIT 2* 1+ ]B!  \ 12.9.3 falling edge
    [ RIM C, ]
    [ 0 PA_DDR   RXBIT       ]B!  \ 10.9.3 set to input         
    [ 1 PA_CR1   RXBIT       ]B!  \ 10.9.4 enable pull-up         
    [ 1 PA_CR2   RXBIT       ]B!  \ 10.9.5 enable interrupt         
  ;
RAM

Tests show that the PA3 pin interrupt and the PA3/USART_RX function work concurrently: entering telly ? on the console shows that the value of the variable increases with every falling edge of PA3/USART_RX.

This means that we can move on to the next step, using HALT when there is no input.

Stopping the CPU in the IDLE Task

When the STM8 eForth console waits for the next keystroke from the serial interface it executes the 'IDLE task (which, in the absence of buffering, must have a shorter execution time than the character time at the current baud rate). In a low power application it would be better to halt the MCU while there is nothing to do and wake it up just in time.

Using HALT means that the CPU stops the HSI (the 16MHz "High Speed Internal" clock) but that means that the serial interface is halted as well. As demonstrated, a falling edge interrupt can be used on PA3/USART_RX and that can be used to get the MCU out of the HALT mode when a new character arrives. The question remains if starting the USART clock after the falling edge of the start-bit will work.

Also it's important not to put the MCU to sleep while thare are characters to be transmitted. A counter in the 'IDLE task is used to keep the MCU awake while reception or transmission is expected to continue.

The GPIOs PB0 and PB1 are used for testing (I use PulseView and cheap "8-channel 24MHz Logic Analyzer").

Here is the code:

\res MCU: STM8L051
\res export PA_DDR PA_CR1 PA_CR2
\res export PB_DDR PB_ODR PB_CR1 PB_CR2
\res export EXTI_SR1 EXTI_CR1 INT_EXTI3

0   CONSTANT DEB0   \ pin debug PB0
1   CONSTANT DEB1   \ pin debug PB1

500 CONSTANT CLOOPS \ idle loops before halt (@9600 baud)
3   CONSTANT RXBIT  \ GPIO port bit number PA3/USART_RX

$8E CONSTANT HALT   \ STM8 assembler halt
$9A CONSTANT RIM    \ STM8 assembler enable interrupts
$9B CONSTANT SIM    \ STM8 assembler disable interrupts

#require ]B!
#require ]B?
#require :NVM
#require 'IDLE

NVM
  VARIABLE iloops

  :NVM
    SAVEC
    [ 1 PB_ODR DEB0 ]B!     \ pin debug: wakeup from HALT
    [ 1 EXTI_SR1 RXBIT ]B!  \ 12.9.7 acknowledge interrupt
    CLOOPS iloops !             \ do idle loops - keeps the CPU from HALTing
    IRET
  ;NVM  INT_EXTI3 !

  \ idle loop and  HALT
  : dohalt ( -- )
    [ 0 PB_ODR DEB0 ]B!     \ pin debug: idle start
    [ 1 PB_ODR DEB1 ]B!     \ pin debug: idle loop
    iloops @ ?DUP IF
      1- iloops !
    ELSE
      [ HALT C, ]
    THEN
    [ 0 PB_ODR 1 ]B!
  ;

  : init
    [ SIM C, ]
    [ 1 EXTI_CR1 RXBIT 2*    ]B!  \ 12.9.3 rising edge
    [ 1 EXTI_CR1 RXBIT 2* 1+ ]B!  \ 12.9.3 and falling edge
    [ 0 PA_DDR   RXBIT ]B!        \ 10.9.3 set to input
    [ 1 PA_CR1   RXBIT ]B!        \ 10.9.4 enable pull-up
    [ 1 PA_CR2   RXBIT ]B!        \ 10.9.5 enable interrupt
    [ ' dohalt ] LITERAL 'IDLE !

    [ 1 PB_DDR   DEB0 ]B!   \ pin debug PB0
    [ 0 PB_ODR   DEB0 ]B!
    [ 0 PB_CR1   DEB0 ]B!
    [ 1 PB_CR2   DEB0 ]B!
    [ 1 PB_DDR   DEB1 ]B!   \ pin debug PB1
    [ 0 PB_ODR   DEB1 ]B!
    [ 0 PB_CR1   DEB1 ]B!
    [ 1 PB_CR2   DEB1 ]B!
    CLOOPS iloops !
    [ RIM C, ]
  ;
RAM

DEB0 signals the "handshake" between the PA3 edge interrupt and the 'IDLE loop. A DEB1 puls indicates one run of the IDLE task. In between arriving characters DEB1 stays high since the MCU is in HALT state. For a first test I use picocom, a simple serial terminal, since it doesn't buffer the input as e4thcom does it. The following PulseView diagram shows words<enter>:

image

The DEB1 shows the 'IDLE task loops and the HALT state. At the moment executing WORDS, for listing the vocabulary, keeps the CPU active. It would also be possible to HALT the MCU while waiting for the TX buffer to be emptied but for a battery powered application that would be just a minor improvement.

The following recording shows how RX, edge change interrupt, 'IDLE task and the "keep awake" time through iloops interact:

image

The number of 'IDLE task loops was tested at 9600 baud (CLOOPS). Possibly the USART transmission status flags can be used to detect when it's safe to enter the HALT state but I've found no way to detect that the USART is receiving a character.

Adding a RTC/WUT Background Task

Through STM8 eForth issue #375 and STM8 eForth issue #379 the groundwork was laid for creating Background Task implementations that doesn't depend on the STM8 timers (TIM2, TIM3 or TIM1): a custom bgtask.inc that uses any periodic ISR (or an external interrupt if you like) can be placed into a board configuration folder. The C ISR dummy functions for the SDAS linker (called by SDCC) were moved to forth.h so that they can target specific.

low-power operation

The RTC peripharal uses the LSE (32.768 kHz clock crystal) or the LSI (internal 38kHz oscillator) and it can wake up the device in active-halt mode when the CPU is stopped and the main power regulator and the HSI clock are shut down.

Conclusion

This write-up shows how all STM8 eForth features, including the background task, can be used in an STM8L low-power application.

Consumption is down to 10 to 20µA whith a fully responsive console and an (empty) 10ms background task. The background task scheduler uses the LSE crystal oscillator that also feeds the RTC. When there is "load" on the background task, consumption increases up to what's to be expected at the full clock speed of 16MHz clock (it draws about 700µA). The background task rate is thus an important parameter for the application. It's also possible to use the RTC alarm, not the WUT for waking up the application. That's an interesting option, e.g., for data logging or for home automation tasks.

A point left to clarify is that AN3147 chapter 2.2 states that applications of RM0031 devices, unlike RM0013 devices, need to select whether the MVP (main core voltage regulator) or the LPVR (low-power voltage regulator) is to be used in Active-Halt mode.

Code for the low-power console is below (files bgtask.inc, boardcore.inc and stm8l-halt.fs).

;--------------------------------------------------------
; Public variables in this module
;--------------------------------------------------------
.globl _RTC_IRQHandler
;****** Board variables ******
.ifne HAS_BACKGROUND
RamWord BGADDR ; address of background routine (0: off)
RamWord TICKCNT ; "TICKCNT" 16 bit ticker (counts up)
BSPPSIZE = BG_STACKSIZE ; Size of data stack for background tasks
.endif
;****** timer macro ******
; init BG timer interrupt
.macro BGTASK_Init
.ifne HAS_BACKGROUND
BG_INT = ITC_IX_RTC
BRES ITC_SPR1+(BG_INT/4),#((BG_INT%4)*2+1) ; Interrupt prio. low
; enable LSE clock
BSET CLK_PCKENR2,#2 ; peripheral clock gate RTC
MOV CLK_CRTCR,#0x10 ; p.101 clock RTCDIV=1, RTCSEL:LSE
8$: BTJT CLK_CRTCR,#0,8$ ; p.101 RTCSWBSY set/reset by HW at clock change
; 24.3.4 WUT initialization and configuration
MOV RTC_WPR,#0xCA ; unlock calendar write protection
MOV RTC_WPR,#0x53
; 24.3.5 the clear WUTE-spinlock combination is certainly necessary
BRES RTC_CR2,#2 ; 24.6.10 clear WUTE
9$: BTJF RTC_ISR1,#2,9$ ; 24.6.12 spin until WUT write is allowed
MOV RTC_CR1,#0x03 ; 24.6.9 WUCKSEL/2
WUTR = (32768/2)/100 ; WUCKSEL/2 -> 10ms
MOV RTC_WUTRL,#WUTR ; 24.6.18 wakeup timer register low default: $FF
MOV RTC_WUTRH,#0x00 ; 24.6.17 wakeup timer register high default: $FF
BSET RTC_CR2,#2 ; 24.6.10 set WUTE, enable WUT
BSET RTC_CR2,#6 ; 24.6.10 set WUTIE, enable WUT interrupt
.endif
.endm
;****** ISR handler ******
; RTC/WTU handler for background task
_RTC_IRQHandler:
.ifne HAS_BACKGROUND
BRES RTC_ISR2,#2 ; clear WUTF
.ifne HAS_LED7SEG
CALL LED_MPX ; "PC_LEDMPX" board dependent code for 7Seg-LED-Displays
.endif
; Background operation saves & restores the context of the interactive task
; Cyclic context reset of Forth background task (stack, BASE, HLD, I/O vector)
.ifne HAS_BACKGROUND
LDW X,TICKCNT
INCW X
LDW TICKCNT,X
; fall through
.ifne BG_RUNMASK
LD A,XL ; Background task runs if "(BG_RUNMASK AND TICKCNT) equals 0"
AND A,#BG_RUNMASK
JRNE TIM2IRET
.endif
LDW Y,BGADDR ; address of background task
TNZW Y ; 0: background operation off
JREQ TIM2IRET
LDW X,YTEMP ; Save context
PUSHW X
PUSH USRBASE+1 ; 8bit since BASE should be < 36
MOV USRBASE+1,#10
LDW X,USREMIT ; save EMIT exection vector
PUSHW X
LDW X,#EMIT_BG ; "'BGEMIT" xt of EMIT for BG task
LDW USREMIT,X
LDW X,USRQKEY ; save QKEY exection vector
PUSHW X
LDW X,#QKEY_BG ; "'?BGKEY" xt of ?KEY for BG task
LDW USRQKEY,X
LDW X,USRHLD
PUSHW X
LDW X,#PADBG ; "BGPAD" empty PAD for BG task
LDW USRHLD,X
LDW X,#BSPP ; "BGSPP" data stack for BG task
CALL (Y)
POPW X
LDW USRHLD,X
POPW X
LDW USRQKEY,X
POPW X
LDW USREMIT,X
POP USRBASE+1
POPW X
LDW YTEMP,X
TIM2IRET:
.endif
IRET
.endif
;****** BG User Words ******
.ifne HAS_BACKGROUND
; TIM ( -- T) ( TOS STM8: -- Y,Z,N )
; Return TICKCNT as timer
HEADER TIMM "TIM"
TIMM:
LDW Y,TICKCNT
JP AYSTOR
; BG ( -- a) ( TOS STM8: -- Y,Z,N )
; Return address of BGADDR vector
HEADER BGG "BG"
BGG:
LD A,#(BGADDR)
JP ASTOR
.endif
; STM8L051F3P6 "Core" STM8L device dependent routine default code
; Note: for supporting a new board create a new board configuration
; folder with a "globconfig.inc" and a copy of this file.
; ==============================================
.ifne HAS_LED7SEG
; LED_MPX driver ( -- )
; Code called from ISR for LED MPX
LED_MPX:
RET
.endif
; ==============================================
.ifne HAS_OUTPUTS
; OUT! ( c -- )
; Put c to board outputs, storing a copy in OUTPUTS
.dw LINK
LINK = .
.db (4)
.ascii "OUT!"
OUTSTOR:
RET
.endif
;===============================================================
.ifne HAS_KEYS
; BKEY ( -- f ) ( TOS STM8: -- A,Z,N )
; Read board key state as a bitfield
.dw LINK
LINK = .
.db (4)
.ascii "BKEY"
BKEY:
CLR A
JP ASTOR
; BKEYC ( -- c ) ( TOS STM8: -- A,Z,N )
; Read and translate board dependent key bitmap into char
BKEYCHAR:
JRA BKEY ; Dummy: get "no key" and leave it as it is
.endif
;===============================================================
; BOARDINIT ( -- )
; Init board GPIO
.macro PSET_TX PUART, TXPIN
.ifeq HALF_DUPLEX
BSET PUART+DDR,#TXPIN ; HDSEL: USART controls the data direction
.endif
BSET PUART+CR1,#TXPIN
.endm
BOARDINIT:
; Board I/O initialization
.ifne HAS_BACKGROUND
.ifne BG_USE_TIM3
; BSET CLK_PCKENR1,#1 ; TIM3[1]
.else
; BSET CLK_PCKENR1,#0 ; TIM2[0]
.endif
.endif
.ifne HAS_TXUART
BSET CLK_PCKENR1,#5 ; Enable USART1 clock
.ifeq ALT_USART_STM8L
; Map USART1 to PC5[TX] and PC6[RX]
PSET_TX PORTC, 5
.endif
.ifeq (ALT_USART_STM8L-1)
; Map USART1 to PA2[TX] and PA3[RX]
BSET SYSCFG_RMPCR1,#4
PSET_TX PORTA, 2
.ifeq (ALT_USART_STM8L-2)
; Map USART1 to PC3[TX] and PC2[RX]
PSET_TX PORTC, 3
.endif
.endif
.endif
RET
// When creating mixed C/Forth applications, especially with Medium density or
// High density devices, assign memory by matching forthData[] start and size
// with the range defined in target.inc
// *** The Forth system ***
#define FORTHRAM 0x30
#define UPPEND 0x7f
#define CTOPLOC 0x80
#define RAMEND 0x03FF
// Forth will take possession of the return stack and it won't return!
// C-code should be exposed as Forth words or operate through interrupts
void forth(void);
// declare trap handler for Forth literals
void TRAP_Handler() __trap;
// *** Any interrupt that can be used for the simulated serial interface ***
#ifdef INTVEC_EXTI0
// declare interrupt handler for external interrupts STM8L:Px.0 or STM8S:PA
void EXTI0_IRQHandler() __interrupt (INTVEC_EXTI0);
#endif
#ifdef INTVEC_EXTI1
// declare interrupt handler for external interrupts STM8L:Px.1 or STM8S:PB
void EXTI1_IRQHandler() __interrupt (INTVEC_EXTI1);
#endif
#ifdef INTVEC_EXTI2
// declare interrupt handler for external interrupts STM8L:Px.2 or STM8S:PC
void EXTI2_IRQHandler() __interrupt (INTVEC_EXTI2);
#endif
#ifdef INTVEC_EXTI3
// declare interrupt handler for external interrupts STM8L:Px.3 or STM8S:PD
void EXTI3_IRQHandler() __interrupt (INTVEC_EXTI3);
#endif
#ifdef INTVEC_EXTI4
// declare interrupt handler for external interrupts STM8L:Px.4 or STM8S:PE
void EXTI4_IRQHandler() __interrupt (INTVEC_EXTI4);
#endif
#ifdef INTVEC_EXTI5
// declare interrupt handler for external interrupts STM8L:Px.5
void EXTI5_IRQHandler() __interrupt (INTVEC_EXTI5);
#endif
#ifdef INTVEC_EXTI6
// declare interrupt handler for external interrupts STM8L:Px.6
void EXTI6_IRQHandler() __interrupt (INTVEC_EXTI6);
#endif
#ifdef INTVEC_EXTI7
// declare interrupt handler for external interrupts STM8L:Px.7
void EXTI7_IRQHandler() __interrupt (INTVEC_EXTI7);
#endif
// *** Timer Interrupt handler for the simulated serial interface ***
#ifdef INTVEC_TIM4
// declare interrupt handler for TIM4 ticker
void TIM4_IRQHandler() __interrupt (INTVEC_TIM4);
#endif
// *** Any interrupt vector that can be used for the background task ***
#ifdef INTVEC_TIM1_UPDATE
// declare interrupt handler for RTC/WUT
void RTC_IRQHandler() __interrupt (INTVEC_RTC);
#endif
\res MCU: STM8L051
\res export PA_DDR PA_CR1 PA_CR2
\res export PB_DDR PB_ODR PB_CR1 PB_CR2
\res export EXTI_SR1 EXTI_CR1 INT_EXTI3
\res export PB_CR1 PC_CR1 PD_CR1 PE_CR1 PF_CR1
\res export CLK_PCKENR2
0 CONSTANT DEB0 \ pin debug PB0
1 CONSTANT DEB1 \ pin debug PB1
500 CONSTANT CLOOPS \ idle loops before halt (@9600 baud)
3 CONSTANT RXBIT \ GPIO port bit number PA3/USART_RX
$8E CONSTANT HALT \ STM8 assembler halt
$9A CONSTANT RIM \ STM8 assembler enable interrupts
$9B CONSTANT SIM \ STM8 assembler disable interrupts
#require ]C!
#require ]B!
#require ]B?
#require :NVM
#require 'IDLE
#require hi
NVM
VARIABLE iloops
:NVM
SAVEC
[ 1 PB_ODR DEB0 ]B! \ pin debug: wakeup from HALT
[ 1 EXTI_SR1 RXBIT ]B! \ 12.9.7 acknowledge interrupt
CLOOPS iloops ! \ do idle loops - keeps the CPU from HALTing
IRET
;NVM INT_EXTI3 !
\ idle loop and HALT
: dohalt ( -- )
[ 0 PB_ODR DEB0 ]B! \ pin debug: idle start
[ 1 PB_ODR DEB1 ]B! \ pin debug: idle loop
iloops @ ?DUP IF
1- iloops !
ELSE
[ HALT C, ]
THEN
[ 0 PB_ODR 1 ]B!
;
: init
\ set GPIO pull ups
[ $FF PA_CR1 ]C!
[ $FF PB_CR1 ]C!
[ $FF PC_CR1 ]C!
[ $FF PD_CR1 ]C!
[ $FF PE_CR1 ]C!
[ $FF PF_CR1 ]C!
[ 0 CLK_PCKENR2 7 ]B! \ disable Boot ROM clock
[ SIM C, ]
[ 1 EXTI_CR1 RXBIT 2* ]B! \ 12.9.3 rising edge
[ 1 EXTI_CR1 RXBIT 2* 1+ ]B! \ 12.9.3 and falling edge
[ 0 PA_DDR RXBIT ]B! \ 10.9.3 set to input
[ 1 PA_CR1 RXBIT ]B! \ 10.9.4 enable pull-up
[ 1 PA_CR2 RXBIT ]B! \ 10.9.5 enable interrupt
[ ' dohalt ] LITERAL 'IDLE !
[ 1 PB_DDR DEB0 ]B! \ pin debug PB0
[ 0 PB_ODR DEB0 ]B!
[ 0 PB_CR1 DEB0 ]B!
[ 1 PB_CR2 DEB0 ]B!
[ 1 PB_DDR DEB1 ]B! \ pin debug PB1
[ 0 PB_ODR DEB1 ]B!
[ 0 PB_CR1 DEB1 ]B!
[ 1 PB_CR2 DEB1 ]B!
CLOOPS iloops !
[ RIM C, ]
hi
;
RAM
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment