A friend needs a C0135 based MODBUS Node with two pulse counter inputs.
The requirements are modest:
- provide reliabe pulse counting on two inputs
- at a maximum frequency of about 200Hz
- where the duty cycle isn't well known (but likely not always 50%)
- without jamming the MODBUS server
Polling an input in the "background task" won't do the job as the rate is either too low or, at a higher rate, it jams the modbus server.
According to the C0135 Hardware Description in the STM8 eForth Wiki the C0135 input terminals are connected to the following STM8S103F3 GPIO pins:
Input | Pin | Port | Function |
---|---|---|---|
IN1 | 20 | PD3 | AIN4, TIM2_CH2 |
IN2 | 19 | PD2 | AIN3, [TIM2_CH3] |
IN3 | 17 | PC7 | MISO, [TIM1_CH2] |
IN4 | 16 | PC6 | MOSI, [TIM1_CH1] |
The STM8S103F3P6 datasheet uses brackets around names of GPIO function (e.g., [TIM2_CH3]
) to indicate an "alternative function" of the port. In practive that means "more complicated to use" since the STM8S architecture controlls function remapping through "option bytes" (see datasheet chapters 5.3 and 8.1). In STM8 eForth writing STM8S option bytes can be done with the word OPT!
. This, however, should only be done once, e.g, while programming the device. Knowing the state of the flags is thus a concern.
The counter input TIM2_CH2
is the only one that's an ordinary port function. Using it, however, requires using a different timer for the background task (e.g., using TIM1
if it is available).
According to chapter 8 table 14 the "option byte" OPT2
controls the following alternative functions:
AFR0
enablesTIM1_CH1
andTIM1_CH2
(andTIM2_CH1
)AFR1
enablesTIM2_CH3
This is all good and well - but we have to keep in mind that two pulse inputs are needed (i.e., TIM1
and TIM2
). This means keeping the background task is more complicated than just configuring some options in globconf.inc
.
Freeing up TIM1
can be done by using TIM4
as the background ticker. Unfortunately, as the UART
is needed as the MODBUS interface, TIM4
is used for timing the simulated serial interface.
A MODBUS server doesn't always need the "background task" feature - all that's really needed is a working background counter variable TIM
. Incrementing TIM
using the serial interface bit clock (usually a 104µs cadence) still requires some hackery as the timer is synchronized with Rx bits. This isn't the most attractive option.
The AWU
(Auto Wake-Up) timer is easily overlooked as a periodic timer. In fact it must be clocked from a 128 kHz source, normally from the LSI
(Low Speed Internal) clock. It can, however, also be clocked from the HSE
(High Speed External) source but not from the HSI
(High Speed Internal) clock.
The STM8S Reference Manual RM0016 Chapter 9.3 states:
The division factor for HSE has to be programmed in the HSEPRSC[1:0] option bits Refer to in the option bytes section of the datasheet. The goal is to get 128 kHz at the output of the HSE prescaler.
In fact any C0135 board has a 8MHz crystalm which, when used as the main clock source really cripples the performance of the STM8 µC - a strange design decision!. Now this crystal can be brought to good use.
And RM0016 chapter 12.2 shows how it's to be configured:
The AWU
has one problem, though: it has to be started with the HALT
instruction, i.e., by stopping the CPU. Obviously this can't be done by the background task itself, but only by the 'IDLE
task. This of course, will add a random, but MODBUS dependent, delay to background task cadence, removing any benefit that using the crystal may provide.
For this use-case even an non-calibrated LSI
timer may still be good enough.
The STM8S architecture uses the same interrupt vector (and the same edge/level configuration) for all GPIOs of a port (e.g. PD
). If two or more inputs of the same port are configured to generate an interrupt then it's up to the software to figure out which GPIO triggered the interrupt.
In the MODBUS application, however, PD1
is used as the simulated serial interface, and thus the EXTI
of port D is needed for detecting the start-bit in Forth console host communication.
Unless one decides to use PA1
and/or PA2
for connecting the console, e.g., after desoldering the currently not needed 8MHz crystal, the only option for counting pulses with external interrupts is using port C for both inputs. This, however, requires avoiding race conditions that may occur when edges of both inputs change at nearly the same time.
After considering the option, I decided to use an optimized edge-triggered ISR for detecting rising and falling edges of PC6
and PC7
(i.e. IN3 and IN4). The solution mirrors the state of a pulse signal (i.e., high or low) with a two-state state machine for each input (i.e., using a flag bit).
Here is the code of the resulting new word CCNT
:
\res MCU: STM8S103
\res export PC_IDR PC_CR2
\res export EXTI_CR1 INT_EXTI2
#require ]B!
#require ]B@IF
#require :NVM
#require WIPE
NVM
VARIABLE CCNT 1 ALLOT \ 2 byte counter, 1 byte flags
RAM
CCNT 2+ CONSTANT cflg
NVM
:NVM ( -- ) \ a "headless" interrupt service routine
SAVEC \ save the Forth state
[ PC_IDR 7 ]B@IF \ IN3: count rising-falling edge sequence
[ 1 cflg 0 ]B!
ELSE
[ cflg 0 ]B@IF
[ 0 cflg 0 ]B!
[ $3C C, CCNT C, ] \ inc cnt ; short, MSB
THEN
THEN
[ PC_IDR 6 ]B@IF \ IN4: count rising-falling edge sequence
[ 1 cflg 1 ]B!
ELSE
[ cflg 1 ]B@IF
[ 0 cflg 1 ]B!
[ $3C C, CCNT 1+ C, ] \ inc cnt+1 ; short, LSB
THEN
THEN
IRET \ restore the Forth state and make an IRET
[ OVERT ( xt ) INT_EXTI2 ! \ go to interpreter, finish dict entry, xt to ISR vector
: cinit ( -- ) \ init counters cIN3 and cIN4
\ IN4 (PC6) as interrupt input
[ $9B C, ] \ SIM, make EXTI_CR1 writable
[ 1 EXTI_CR1 4 ]B! \ PC rising and falling edge
[ 1 EXTI_CR1 5 ]B!
[ 1 PC_CR2 7 ]B! \ IN3 (PC7) interrupt enable
[ 1 PC_CR2 6 ]B! \ IN4 (PC6) interrupt enable
[ $9A C, ] \ RIM
;
WIPE RAM
The code uses the fact that STM8 eForth is an STC Forth, which makes mixing assembler and Forth very easy. The word [ ... ]B@IF
is a nice example since it extends the compiler with a native "bit-test relative-addressing" IF ... ELSE ... THEN
structure. The extension resides in the RAM which is cleared by WIPE
- which restores the normal behavior.
The code is, of course, very low-level, and it would have to be rewritten for any other architecture (it would work on STM8L, though). This isn't a big deal since it provides an abstraction for the "reliable counting device" as can be seen in the following example:
#require CCNT
#require ]B!
#require ]BCPL
#require A>
\res MCU: STM8S103
\res export PD_ODR PD_DDR PD_CR1
VARIABLE cIN3
VARIABLE cIN4
: in2tog ( -- ) \ IN2 (PD2) as toggle output for test
[ 1 PD_DDR 2 ]B! \ PD2 test output
[ 1 PD_CR1 2 ]B! \ PD2 push/pull
[ PD_ODR 2 ]BCPL
;
: cread ( -- ) \ read and clear CCNT counters
[ $4F C, \ CLR A
$31 C, CCNT , ] \ EXG A, CCNT MSB
A> cIN3 +!
[ $4F C, \ CLR A
$31 C, CCNT 1+ , ] \ EXG A, CCNT LSB
A> cIN4 +!
;
: ctest ( -- ) \ connect IN2 to IN3 and/or IN4 and run this a few times
in2tog cread cIN3 ? in2tog cIN4 ?
;
cinit ctest
OK, admitedly, cread
again uses in-line assembler code for reading and clearing the ISR counter in an ISR-safe way.
The problem of reliably counting pulse on two C0135 relay board inputs has been solved by using a single STM8 port EXTI
and optimized STM8 eForth code.
Using the AWU
is still an interesting option, and I may consider it for future applications.