Зеркало сайта: vivacious-stockings-frog.cyclic.app

STM8S + SDCC: Программирование на связке языков Си и ассемблер

разделы: STM8 , АССЕМБЛЕР , дата: 13 марта 2018г.

В данной статье я попытался рассмотреть вопрос использования ассемблера в Си-программах для компилятора SDCC. Данный компилятор часто подвергается критике по качеству кода, однако он является единственным Open Source компилятором для архитектуры STM8, и единственным компилятором доступным в Linux. Поэтому данная статья может рассматриваться как HowTo по выжимании максимума возможного из SDCC.

Когда я пытался в прошлый раз (почти два года назад) подружиться с SDCC у меня оказалось большое количество вопросов к нему, что вылилось в метания между SDCC, IAR и Cosmic. Была даже идея отказаться от STM8 в пользу Cortex-M0. Однако теперь для Linux появился отладочный интерфейс, что побудило меня еще раз взглянуть на SDCC и попытаться найти к нему подход.

Статья построена аналогично предыдущей: ATtiny13a: использование ассемблера GNU-AS в программах на Си с той разницей, что вместо ATtiny13a будет использоваться 20-пиновый STM8S103F3P6, а вместо GCC - SDCC.

В качестве операционки при написании использовался Slackware GNU/Linux (русские физики рекомендуют), но теоретически, все должно работать и в Windows при условии использования CYGWIN.

По ходу изложения, я буду сравнивать систему команд STM8 и AVR. Я делаю это не для того что "уронить" AVR, а потому что считаю эту архитектуру хорошо сбалансированной, удобной в использовании и легкой в освоении. STM8 это более современная архитектура, и по определению обязана быть более лучшей. Однако AVR удобно рассматривать в качестве какой-то базовой системы, этакой точкой отсчета.

    Содержание:
  1. Создание Си - проекта Blink для SDCC.
  2. Создание проекта Blink на ассемблере SDCC.
  3. Добавление ассемблерной функции в Си-программу.
  4. Передача параметров из Си в ассемблерную функцию.
  5. Задержка по таймеру TIM4, с обработчиком прерывания на ассемблере.
  6. Счетчик на 4-x разрядном семисегментном индикаторе.
  7. Переписывание драйвера индикатора на ассемблере STM8.
  8. Использование аппаратного SPI-интерфейса в режиме мастера.
  9. Счетчик импульсов, вариант на Си.
  10. Счетчик импульсов, вариант на ассемблере.

Полные исходники, сборочные файлы и скомпилированные прошивки можно будет скачать по ссылке в конце статьи.

Примечание от 01.09.2022г. В SDCC версии 4.2 поменялся формат передачи аргументов функций. Если раньше все аргументы передавались через стек, то теперь они передаются через регистры. Поэтому для совместимости со старым кодом следует добавлять опцию компиляции "--sdcccall 0".

1. Создание Си - проекта Blink для SDCC

Вначале предстоит сделать несколько рутинных действий. Создаем директорию проекта:

$ mkdir ./01_blink
$ cd ./01_blink

В директорию проекта добавляем сборочный файл Makefile:

MCU=stm8
DEVICE=stm8s103f3
FLASHER=stlinkv2
CFLAGS=-V --opt-code-size
CC=sdcc
SIZE=stm8-size
OBJ=main.rel
REL=main.o
TARGET=blink

.PHONY: all clean

%.rel:	%.c
	$(CC) -m$(MCU)  $(CFLAGS)  $<

all:	$(OBJ)
	$(CC) -m$(MCU) -o $(TARGET).ihx $(OBJ) $(LIB) 
	$(SIZE) $(TARGET).ihx

install:
	stm8flash -c $(FLASHER) -p $(DEVICE) -w $(TARGET).ihx
clean:
	@rm -v *.??? *.??

Текст программы в main.c:

#include "main.h"

#define LED (1<<P5)

static void delay(unsigned int t)
{
    while(t--);
}

int main( void )
{
    // GPIO setup
    PB_DDR|=(LED);
    PB_CR1|=(LED);
    // main loop
    for(;;){
        PB_ODR ^= (LED);
        delay(0xffff);
    }
}

И заголовочный файл main.h:

#define PB_ODR *(unsigned char*)0x5005
#define PB_DDR *(unsigned char*)0x5007
#define PB_CR1 *(unsigned char*)0x5008

#define P7 7
#define P6 6
#define P5 5
#define P4 4
#define P3 3
#define P2 2
#define P1 1

собираем проект:

$ make all
sdcc -mstm8  -V --opt-code-size  main.c
+ /usr/local/bin/sdcpp -nostdinc -Wall -std=c11 -obj-ext=.rel -D__SDCC_STACK_AUTO -D__SDCC_CHAR_UNSIGNED -D__SDCC_INT_LONG_REENT -D__SDCC_FLOAT_REENT -D__SDCC=3_6_0 -D__SDCC_VERSION_MAJOR=3 -D__SDCC_VERSION_MINOR=6 -D__SDCC_VERSION_PATCH=0 -DSDCC=360 -D__SDCC_REVISION=9615 -D__SDCC_stm8 -D__STDC_NO_COMPLEX__=1 -D__STDC_NO_THREADS__=1 -D__STDC_NO_ATOMICS__=1 -D__STDC_NO_VLA__=1 -D__STDC_ISO_10646__=201409L -D__STDC_UTF_16__=1 -D__STDC_UTF_32__=1 -isystem /usr/local/bin/../share/sdcc/include/stm8 -isystem /usr/local/share/sdcc/include/stm8 -isystem /usr/local/bin/../share/sdcc/include -isystem /usr/local/share/sdcc/include  main.c
+ /usr/local/bin/sdasstm8 -plosgffw "main.asm"
+ /usr/local/bin/sdldstm8 -nf "main.lk"
sdcc -mstm8 -o blink.ihx main.rel
stm8-size blink.ihx
   text    data     bss     dec     hex filename
      0     205       0     205      cd blink.ihx

прошиваем микроконтроллер:

$ make install
stm8flash -c stlinkv2 -p stm8s103f3 -w blink.ihx
Determine FLASH area
Due to its file extension (or lack thereof), "blink.ihx" is considered as INTEL HEX format!
205 bytes at 0x8000... OK
Bytes written: 205

Итого имеем 205 байт для минимальной программы. Напомню, что Blink для ATmega8 занимал на флеше 90-байт. Где деньги Зин? Куда уходят байты? Попробуем разобраться.

Преимуществом SDCC является прозрачная система компиляции: препроцессор->компилятор->ассемблер->линкер.

Ассемблерный файл: main.asm должен появится в директории проекта после компиляции. Его полное содержимое можно посмотреть под спойлером.

;--------------------------------------------------------
; File Created by SDCC : free open source ANSI-C Compiler
; Version 3.6.0 #9615 (Linux)
;--------------------------------------------------------
    .module main
    .optsdcc -mstm8

;--------------------------------------------------------
; Public variables in this module
;--------------------------------------------------------
    .globl _main
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
    .area DATA
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
    .area INITIALIZED
;--------------------------------------------------------
; Stack segment in internal ram 
;--------------------------------------------------------
    .area   SSEG
_fp_:: .ds 2

__start__stack:
    .ds 1

;--------------------------------------------------------
; absolute external ram data
;--------------------------------------------------------
    .area DABS (ABS)
;--------------------------------------------------------
; interrupt vector 
;--------------------------------------------------------
    .area HOME
__interrupt_vect:
    int s_GSINIT ; reset
    int 0x0000 ;trap
    int 0x0000 ;int0
    int 0x0000 ;int1
    int 0x0000 ;int2
    int 0x0000 ;int3
    int 0x0000 ;int4
    int 0x0000 ;int5
    int 0x0000 ;int6
    int 0x0000 ;int7
    int 0x0000 ;int8
    int 0x0000 ;int9
    int 0x0000 ;int10
    int 0x0000 ;int11
    int 0x0000 ;int12
    int 0x0000 ;int13
    int 0x0000 ;int14
    int 0x0000 ;int15
    int 0x0000 ;int16
    int 0x0000 ;int17
    int 0x0000 ;int18
    int 0x0000 ;int19
    int 0x0000 ;int20
    int 0x0000 ;int21
    int 0x0000 ;int22
    int 0x0000 ;int23
    int 0x0000 ;int24
    int 0x0000 ;int25
    int 0x0000 ;int26
    int 0x0000 ;int27
    int 0x0000 ;int28
    int 0x0000 ;int29
;--------------------------------------------------------
; global & static initialisations
;--------------------------------------------------------
    .area HOME
    .area GSINIT
    .area GSFINAL
    .area GSINIT
.globl start
start:
__sdcc_gs_init_startup:
    ldw x, #0x7ff
    ldw x, sp
__sdcc_init_data:
; stm8_genXINIT() start
    ldw x, #l_DATA
    jreq    00002$
00001$:
    clr (s_DATA - 1, x)
    decw x
    jrne    00001$
00002$:
    ldw x, #l_INITIALIZER
    jreq    00004$
00003$:
    ld  a, (s_INITIALIZER - 1, x)
    ld  (s_INITIALIZED - 1, x), a
    decw    x
    jrne    00003$
00004$:
; stm8_genXINIT() end
    .area GSFINAL
    jp  __sdcc_program_startup
;--------------------------------------------------------
; Home
;--------------------------------------------------------
    .area HOME
    .area HOME
__sdcc_program_startup:
    jp  _main
;   return from main will return to caller
;--------------------------------------------------------
; code
;--------------------------------------------------------
    .area CODE
;   main.c: 5: static void delay(unsigned int t)
;   -----------------------------------------
;    function delay
;   -----------------------------------------
_delay:
    pushw   x
;   main.c: 7: while(t--);
    ldw x, (0x05, sp)
00101$:
    ldw (0x01, sp), x
    decw    x
    ldw y, (0x01, sp)
    jrne    00101$
;   main.c: 8: }
    popw    x
    ret
;   main.c: 10: int main( void )
;   -----------------------------------------
;    function main
;   -----------------------------------------
_main:
main:
    .globl main
;   main.c: 13: PB_DDR|=(LED);
    ldw x, #0x5007
    ld  a, (x)
    or  a, #0x20
    ld  (x), a
;   main.c: 14: PB_CR1|=(LED);
    ldw x, #0x5008
    ld  a, (x)
    or  a, #0x20
    ld  (x), a
00102$:
;   main.c: 17: PB_ODR ^= (LED);
    bcpl    0x5005, #5
;   main.c: 18: delay(0xffff);
    push    #0xff
    push    #0xff
    call    _delay
    popw    x
    jra 00102$
;   main.c: 20: }
    ret
    .area CODE
    .area INITIALIZER
    .area CABS (ABS)

Здесь нас будут интересовать следующие разделы:

__interrupt_vect:
    int s_GSINIT ; reset
    int 0x0000 ;trap
    int 0x0000 ;int0
    int 0x0000 ;int1
    int 0x0000 ;int2
    int 0x0000 ;int3
    int 0x0000 ;int4
    int 0x0000 ;int5
    int 0x0000 ;int6
    int 0x0000 ;int7
    int 0x0000 ;int8
    int 0x0000 ;int9
    int 0x0000 ;int10
    int 0x0000 ;int11
    int 0x0000 ;int12
    int 0x0000 ;int13
    int 0x0000 ;int14
    int 0x0000 ;int15
    int 0x0000 ;int16
    int 0x0000 ;int17
    int 0x0000 ;int18
    int 0x0000 ;int19
    int 0x0000 ;int20
    int 0x0000 ;int21
    int 0x0000 ;int22
    int 0x0000 ;int23
    int 0x0000 ;int24
    int 0x0000 ;int25
    int 0x0000 ;int26
    int 0x0000 ;int27
    int 0x0000 ;int28
    int 0x0000 ;int29

Таблица векторов прерываний. 32 вектора, каждый размером в 4 байта. Итого 32 * 4 = 128 байт. Заметьте, что все вектора кроме RESET указывают на начало оперативной памяти 0x0000. В тексте адреса записаны как двухбайтные числа, но если дизассемблировать прошивку с помощью команды:

$ stm8-objdump -D -m stm8 ./main.ihx

то ситуация проясняется:

./main.ihx:     формат файла ihex


Дизассемблирование раздела .sec1:

00008000 <.sec1>:
    8000:       82 00 80 83     int 0x008083 ;0x8083
    8004:       82 00 00 00     int 0x000000
    8008:       82 00 00 00     int 0x000000
    800c:       82 00 00 00     int 0x000000
    8010:       82 00 00 00     int 0x000000
    8014:       82 00 00 00     int 0x000000
    8018:       82 00 00 00     int 0x000000
    801c:       82 00 00 00     int 0x000000
    8020:       82 00 00 00     int 0x000000
    8024:       82 00 00 00     int 0x000000
    8028:       82 00 00 00     int 0x000000
    802c:       82 00 00 00     int 0x000000
    8030:       82 00 00 00     int 0x000000
    8034:       82 00 00 00     int 0x000000
    8038:       82 00 00 00     int 0x000000
    803c:       82 00 00 00     int 0x000000
    8040:       82 00 00 00     int 0x000000
    8044:       82 00 00 00     int 0x000000
    8048:       82 00 00 00     int 0x000000
    804c:       82 00 00 00     int 0x000000
    8050:       82 00 00 00     int 0x000000
    8054:       82 00 00 00     int 0x000000
    8058:       82 00 00 00     int 0x000000
    805c:       82 00 00 00     int 0x000000
    8060:       82 00 00 00     int 0x000000
    8064:       82 00 00 00     int 0x000000
    8068:       82 00 00 00     int 0x000000
    806c:       82 00 00 00     int 0x000000
    8070:       82 00 00 00     int 0x000000
    8074:       82 00 00 00     int 0x000000
    8078:       82 00 00 00     int 0x000000
    807c:       82 00 00 00     int 0x000000

Если уж мы взялись за дизассемблер, то посмотрим на этот код:

Дизассемблирование раздела .sec4:

000080a4 <.sec4>:
    80a4:       89              pushw X
    80a5:       1e 05           ldw X,(0x05,SP) ;0x5
    80a7:       1f 01           ldw (0x01,SP),X ;0x1
    80a9:       5a              decw X
    80aa:       16 01           ldw Y,(0x01,SP) ;0x1
    80ac:       26 f9           jrne 0x80a7 ;0x80a7
    80ae:       85              popw X
    80af:       81              ret
    80b0:       ae 50 07        ldw X,#0x5007 ;0x5007
    80b3:       f6              ld A,(X)
    80b4:       aa 20           or A,#0x20 ;0x20
    80b6:       f7              ld (X),A
    80b7:       ae 50 08        ldw X,#0x5008 ;0x5008
    80ba:       f6              ld A,(X)
    80bb:       aa 20           or A,#0x20 ;0x20
    80bd:       f7              ld (X),A
    80be:       90 1a 50 05     bcpl 0x5005,#5 ;0x5005
    80c2:       4b ff           push #0xff ;0xff
    80c4:       4b ff           push #0xff ;0xff
    80c6:       cd 80 a4        call 0x80a4 ;0x80a4
    80c9:       85              popw X
    80ca:       20 f2           jra 0x80be ;0x80be
    80cc:       81              ret

Если в AVR все инструкции были строго двухбайтными, или иногда 4-байтными, то в STM8 одна инструкция может содержать от одного до пяти байт. Чаще всего инструкции содержат два байта.

Так как компилятор выдает нам ассемблерный код, мы можем на ходу его подправить. Например: участок кода

;   main.c: 13: PB_DDR|=(LED);
    ldw x, #0x5007
    ld  a, (x)
    or  a, #0x20
    ld  (x), a
;   main.c: 14: PB_CR1|=(LED);
    ldw x, #0x5008
    ld  a, (x)
    or  a, #0x20
    ld  (x), a

Заменим на:

;   main.c: 13: PB_DDR|=(LED);
    bset 0x5007,#5
;   main.c: 14: PB_CR1|=(LED);
    bset 0x5008,#5

перекомпилируем ассемблерный листинг:

$ sdasstm8 -plosgffw "main.asm"

линкуем:

$ sdldstm8 -nf "main.lk"

прошиваем:

$ stm8flash -c stlinkv2 -p stm8s103f3 -w main.ihx
Determine FLASH area
Due to its file extension (or lack thereof), "main.ihx" is considered as INTEL HEX format!
199 bytes at 0x8000... OK
Bytes written: 199

В итоге, код сократился на шесть байтов, не ахти что, но все же.

2. Создание проекта Blink на ассемблере SDCC

Первым делом мне хотелось сделать на ассемблере аналог blink'а, что был для AVR с задержкой на цикле из пяти тактов. На этом примере хотелось сравнить архитектуры, и сделать предварительную оценку для STM8: лучше он или хуже AVR.

Из-за наличия 3-х уровневого конвейера в STM8 делать задержки в стиле _delay_ms() сильно не рекомендуют. Рекомендуют использовать таймер, но это излишне усложняет код. Я потратил время на изучение документации по конвейеру STM8, и вооружившись полученными знаниями я все-таки сделал задуманное.

Итак, согласно документации, после сброса микроконтроллер работает от внутреннего генератора с предделителем =8, т.е. на 2 МГц.

Следовательно, чтобы получить задержку равной одной секунде на цикле из пяти машинных циклов(тактов), нужно будет прокрутить цикл 400000 раз (0x061A80). Остается лишь сделать счетчик на пяти циклах

Создаем директорию проекта:

$ mkdir ./02_blink_assmbler
$ cd ./02_blink_assmbler

В директорию проекта добавляем сборочный файл Makefile:

MCU=stm8
DEVICE=stm8s103f3
FLASHER=stlinkv2
ASFLAGS=-plosgffw
AS=sdasstm8
LD=sdldstm8
SIZE=stm8-size
OBJ=main.rel
TARGET=blink

.PHONY: all clean debug

%.rel:	%.s
	$(AS) $(ASFLAGS)  $<

all:	$(OBJ)
	$(LD) -f ld.lk -i $(TARGET).ihx $(OBJ)
	$(SIZE) $(TARGET).ihx
install:
	stm8flash -c $(FLASHER) -p $(DEVICE) -w $(TARGET).ihx
clean:
	@rm -v *.???

К сожалению, ассемблер SDCC не позволяет использовать Си-шные заголовочные файлы. Поэтому пришлось поработать sed'ом над STM8F103F.h из комплекта STVD, чтобы привести имена периферийных регистров к ассемблерному виду. Его нужно скачать с сайта и добавить в директорию проекта.

Таблицу векторов оформим отдельным файлом init.s:

;   Interrupt vector mapping for 
;   STM8S103F3
vectors:
    int reset   ; Reset
    int reset   ; Trap - Software Interrupt
    int reset   ; int0 TLI - External Top Level Interrupt
    int reset   ; int1 AWU - Auto Wake Up From Halt
    int reset   ; int2 CLK - Clock Contoller
    int reset   ; int3 EXTI0 - Port A External Interrupts
    int reset   ; int4 EXTI1 - Port B External Interrupts
    int reset   ; int5 EXTI2 - Port C External Interrupts
    int reset   ; int6 EXTI3 - Port D External Interrupts
    int reset   ; int7 EXTI4 - Port E External Interrupts
    int reset   ; int8 - Reserved
    int reset   ; int9 - Reserved
    int reset   ; int10 SPI  - End Of Tranfer
    int reset   ; int11 TIM1 - Update/Overflow/Underflow/trigger/break
    int reset   ; int12 TIM1 - Capure/Compare
    int reset   ; int13 TIM2 - Update/Overflow
    int reset   ; int14 TIM2 - Capure/Compare
    int reset   ; int15 - Reserved
    int reset   ; int16 - Reserved
    int reset   ; int17 UART1 - Tx Complete
    int reset   ; int18 UART1 - Reveive Register DATA FULL
    int reset   ; int19 I2C - Interrupt
    int reset   ; int20 - Reserved
    int reset   ; int21 - Reserved
    int reset   ; int22 ADC1 - End Of Conversion/Analog Watchdog Interrupt
    int reset   ; int23 TIM4 - Update/Overflow
    int reset   ; int24 Flash - EOP/WR_PG_DIS
    int reset   ; int25 - Reserved
    int reset   ; int26 - Reserved
    int reset   ; int27 - Reserved
    int reset   ; int28 - Reserved
    int reset   ; int29 - Reserved

Текст самой программы в main.s:

.module main
.area HOME
.include "init.s"
.include "stm8s103f.s"

.equ LED, 0x05              ; LED on PB5

.globl reset
reset:
    ldw x, #0x3ff           ; Top RAM 1k
    ldw sp, x               ; initialization StackPointer
    callr main              ; goto main programm
    jra reset

delay:
    ld a, #0x06
    ldw y, #0x1a80          ; 0x61a80 = 400000 i.e. (2*10^6 MHz)/5cycles
loop:
    subw y, #0x01           ; decrement with set carry
    sbc a,#0x0              ; decrement carry flag i.e. a = a - carry_flag
    jrne loop
    ret
                            ; main programm
main:
    .globl main
    bset PB_DDR, #LED       ; PB_DDR|=(1<<LED)
    bset PB_CR1, #LED       ; PB_CR1|=(1<<LED)
main_loop:
    bcpl PB_ODR, #LED       ; PB_ODR^=(1<<LED)
    callr   delay           ; _delay_ms(1000)
    jra main_loop
    ret

Последнее, что осталось, это скрипт линкера ld.lk:

-muwx
-Y
-b HOME = 0x8000

-e

Цикл задержки состоит из трех инструкций: subw, sbc и jrne. Первая и третья используют по два машинных цикла на исполнение, вторая - один цикл. Итого получается пять циклов. После jrne происходит сброс очереди конвейера. Т.к. конвейер в STM8 всего лишь 3-уровневый, это может увеличить время работы только на два цикла: на этап выборки (если речь не идет об индексной или косвенной адресации) и этап декодирования. Но как мы знаем из документации, в случае перехода выборка начинается уже на этапе выполнения перехода, то теряется всего один цикл. Т.е. jrne будет выполняться два цикла в случае перехода и один цикл в случае его отсутствия.

Собираем проект:

$ make all
sdasstm8 -plosgffw  main.s
sdldstm8 -f ld.lk -i blink.ihx main.rel
ASlink >> -f ld.lk
ASlink >> -muwx
ASlink >> -Y
ASlink >> -b HOME = 0x8000
ASlink >>
ASlink >> -i
ASlink >> blink.ihx
ASlink >> main.rel
stm8-size blink.ihx
   text    data     bss     dec     hex filename
      0     168       0     168      a8 blink.ihx

Прошиваем:

$ make install
stm8flash -c stlinkv2 -p stm8s103f3 -w blink.ihx
Determine FLASH area
Due to its file extension (or lack thereof), "blink.ihx" is considered as INTEL HEX format!
168 bytes at 0x8000... OK
Bytes written: 168

Размер прошивки составил 168 байт. Если учесть, что 128 байт было занято на таблицу векторов, то остается 40 байт самой программы.

Можно сравнить код STM8 с аналогичным (тот же blink) кодом для AVR из февральской статьи:

8080:       ae 03 ff        ldw X,#0x03ff  ;0x3ff
8083:       94              ldw SP,X
8084:       ad 11           callr 0x8097   ;0x8097
8086:       20 f8           jra 0x8080     ;0x8080
8088:       a6 06           ld A,#0x06     ;0x6
808a:       90 ae 1a 80     ldw Y,#0x1a80  ;0x1a80
808e:       72 a2 00 01     subw Y,#0x0001 ;0x1
8092:       a2 00           sbc A,#0x00
8094:       26 f8           jrne 0x808e    ;0x808e
8096:       81              ret
8097:       72 1a 50 07     bset 0x5007,#5 ;0x5007
809b:       72 1a 50 08     bset 0x5008,#5 ;0x5008
809f:       90 1a 50 05     bcpl 0x5005,#5 ;0x5005
80a3:       ad e3           callr 0x8088   ;0x8088
80a5:       20 f8           jra 0x809f     ;0x809f
80a7:       81              ret
14:   00 27        eor     r16, r16
16:   0f bf        out     0x3f, r16       ; 63
18:   0f e9        ldi     r16, 0x9F       ; 159
1a:   0d bf        out     0x3d, r16       ; 61
1c:   b8 9a        sbi     0x17, 0         ; 23
1e:   91 e0        ldi     r25, 0x01       ; 1
20:   88 b3        in      r24, 0x18       ; 24
22:   89 27        eor     r24, r25
24:   88 bb        out     0x18, r24       ; 24
26:   2f e3        ldi     r18, 0x3F       ; 63
28:   3d e0        ldi     r19, 0x0D       ; 13
2a:   83 e0        ldi     r24, 0x03       ; 3
2c:   21 50        subi    r18, 0x01       ; 1
2e:   30 40        sbci    r19, 0x00       ; 0
30:   80 40        sbci    r24, 0x00       ; 0
32:   e1 f7        brne    .-8             ;  0x2c
34:   f5 cf        rjmp    .-22            ;  0x20

Лично мне код STM8 нравится больше. Он отличается от AVR, как тяжелая вода отличается от той, что идет из крана.

3. Добавление ассемблерной функции в Си-программу

Так же как и GCC, SDCC позволяет компоновать прошивку из объектных файлов написанных как на Си, так на ассемблере.

В качестве примера, для программы из первой главы, напишем на ассемблерную функцию задержки из второй главы, вместо функции delay().

Создаем и заходим в директорию проекта:

$ mkdir -p ./03_mix_blink/inc
$ cd ./03_mix_blink

Добавляем туда Makefile:

MCU=stm8
DEVICE=stm8s103f3
FLASHER=stlinkv2
CC=sdcc
AS=sdasstm8
LD=sdldstm8
SIZE=stm8-size
CFLAGS=-V --opt-code-size -I ./inc -c
ASFLAGS=-plosgffw
OBJ=main.rel
ASM=delay.asm
LINK=delay.rel
TARGET=blink

.PHONY: all clean

%.rel:  %.c
    $(CC) -m$(MCU)  $(CFLAGS)  $<

%.asm:  %.s
    $(AS) $(ASFLAGS)  $<

all:    $(OBJ) $(ASM)
    $(LD) -f ld.lk -i $(TARGET).ihx $(OBJ) $(LINK)
    $(SIZE) $(TARGET).ihx

install:
    stm8flash -c $(FLASHER) -p $(DEVICE) -w $(TARGET).ihx
clean:
    @rm -v *.???

Наша Си программа в main.c:

#include "stm8s103f.h"
#define bset(X,Y) __asm bset X,Y __endasm
#define LED 5

extern void delay();

int main( void )
{
    // GPIO setup
    bset(0x5007,#5);    // PB_DDR|=(1<<LED);
    bset(0x5008,#5);    // PB_CR1|=(1<<LED);
    // main loop
    for(;;){
        PB_ODR ^= (1<<LED);
        delay();
    }
}

В директорию inc нужно добавить Си-версию заголовочного файла с объявлениями регистров ввода-вывода. Скачать его можно с сайта: http://www.count-zero.ru/stm8/stm8s103f.h Напомню, что заголовочный файл был взят комплекта из STVD, и с помощью потокового редактора sed преобразован в формат понятный SDCC.

Функция задержки в файле delay.s будет выглядеть так:

.module DELAY
.area CODE
.area HOME
.globl _delay
_delay:
    pushw y
    push a
    ld a, #0x06
    ldw y, #0x1a80          ; 0x61a80 = 400000 i.e. (2*10^6 MHz)/5cycles
loop:
    subw y, #0x01           ; decrement with set carry
    sbc a,#0x0              ; decrement carry flag i.e. a = a - carry_flag
    jrne loop
    pop a
    popw y
    ret

Остается добавить файл компоновщика ld.lk:

-muwx
-Y
-b HOME = 0x8000

-e

Ok. Теперь собираем проект:

$ make
sdcc -mstm8  -V --opt-code-size -I ./inc -c  main.c
+ /usr/local/bin/sdcpp -nostdinc -Wall -std=c11 -I./inc -obj-ext=.rel -D__SDCC_STACK_AUTO -D__SDCC_CHAR_UNSIGNED -D__SDCC_INT_LONG_REENT -D__SDCC_FLOAT_REENT -D__SDCC=3_6_0 -D__SDCC_VERSION_MAJOR=3 -D__SDCC_VERSION_MINOR=6 -D__SDCC_VERSION_PATCH=0 -DSDCC=360 -D__SDCC_REVISION=9615 -D__SDCC_stm8 -D__STDC_NO_COMPLEX__=1 -D__STDC_NO_THREADS__=1 -D__STDC_NO_ATOMICS__=1 -D__STDC_NO_VLA__=1 -D__STDC_ISO_10646__=201409L -D__STDC_UTF_16__=1 -D__STDC_UTF_32__=1 -isystem /usr/local/bin/../share/sdcc/include/stm8 -isystem /usr/local/share/sdcc/include/stm8 -isystem /usr/local/bin/../share/sdcc/include -isystem /usr/local/share/sdcc/include  main.c
+ /usr/local/bin/sdasstm8 -plosgffw "main.asm"
sdasstm8 -plosgffw  delay.s
sdldstm8 -f ld.lk -i blink.ihx main.rel delay.rel
ASlink >> -f ld.lk
ASlink >> -muwx
ASlink >> -Y
ASlink >> -b HOME = 0x8000
ASlink >>
ASlink >> -i
ASlink >> blink.ihx
ASlink >> main.rel
ASlink >> delay.rel
stm8-size blink.ihx
   text    data     bss     dec     hex filename
      0     203       0     203      cb blink.ihx

Прошиваем:

$ make install
stm8flash -c stlinkv2 -p stm8s103f3 -w blink.ihx
Determine FLASH area
Due to its file extension (or lack thereof), "blink.ihx" is considered as INTEL HEX format!
203 bytes at 0x8000... OK
Bytes written: 203

Теперь у нас есть функция которая формирует программную задержку на одну секунду.

4. Передача параметров из Си в ассемблерную функцию

К сожалению, в документации к SDCC ничего не сказано про передачу параметров в ассемблерную функцию относительно архитектуры STM8. Получить представления о механизме передачи параметров можно дизассемблировав прошивку из первой главы:

Дизассемблирование раздела .sec4:

000080a4 <.sec4>:
    80a4:       89              pushw X
    80a5:       1e 05           ldw X,(0x05,SP) ;0x5
    80a7:       1f 01           ldw (0x01,SP),X ;0x1
    80a9:       5a              decw X
    80aa:       16 01           ldw Y,(0x01,SP) ;0x1
    80ac:       26 f9           jrne 0x80a7 ;0x80a7
    80ae:       85              popw X
    80af:       81              ret
    80b0:       ae 50 07        ldw X,#0x5007 ;0x5007
    80b3:       f6              ld A,(X)
    80b4:       aa 20           or A,#0x20 ;0x20
    80b6:       f7              ld (X),A
    80b7:       ae 50 08        ldw X,#0x5008 ;0x5008
    80ba:       f6              ld A,(X)
    80bb:       aa 20           or A,#0x20 ;0x20
    80bd:       f7              ld (X),A
    80be:       90 1a 50 05     bcpl 0x5005,#5 ;0x5005
    80c2:       4b ff           push #0xff ;0xff
    80c4:       4b ff           push #0xff ;0xff
    80c6:       cd 80 a4        call 0x80a4 ;0x80a4
    80c9:       85              popw X
    80ca:       20 f2           jra 0x80be ;0x80be
    80cc:       81              ret

Здесь, вначале идет функция декремента, затем после ret начинается main(). По адресу 0x80c6 идет вызов функции. Перед ее вызовом, программа сохраняет в стеке параметр функции - число 0хffff. Сначала старший байт, затем младший. После завершения работы функции, указатель стека восстанавливается в исходное положение. В данном случае это происходит с помощью инструкции popw X. Если в качестве параметра функции указать число типа long, то к указателю стека просто будет прибавлено число четыре.

Т.к. после вызова функции, указатель стека восстанавливается, извлекать переданные параметры возможно только с помощью индексной адресации на указателе стека. При этом важно помнить: 1) при вызове подпрограммы через инструкцию CALL, адрес возврата также сохраняется в стеке; 2) указатель стека всегда указывает на свободную ячейку памяти.

Т.к. кроме параметра и адреса возврата, вначале подпрограммы сохраняется в стеке содержимое двухбайтного регистра X(на самом деле не сохранение, а простое прибавление двойки к SP), то обращение к переданному параметру из подпрограммы будет с инкрементом указателя стека на пять ячеек памяти:

Ок. Теперь попробуем сделать аналог функции _delay_ms() с возможностью передавать время задержки через параметр (на самом деле более актуальна была бы функция delay_us()).

Опираясь на предыдущий пример, приведем Си программу к такому виду:

#include <stdint.h>
#include "stm8s103f.h"

#define LED 5

extern void delay(uint16_t value);

int main( void ) {
    PB_DDR=(1<<LED);
    PB_CR1=(1<<LED);
    // main loop
    for(;;)
    {
        PB_ODR ^= (1<<LED);
        delay(1000);
    }
}

Тогда delay.s будет выглядеть так:

.module DELAY
.area CODE
.area HOME
.globl _delay
_delay:
    ldw x, (03,sp)
l0:
    ldw y, #0x01ef      ; ->(500-5)=495
l1:
    subw y,#1
    jrne l1
    decw x
    jrne l0
    ret

Здесь вначале извлекается параметр - количество миллисекунд из стека, в цикле это значение уменьшается на единицу за каждую итерацию. В этот цикл вложен другой цикл из четырех тактов, который формирует задержку на 1мс.

5. Задержка по таймеру TIM4, с обработчиком прерывания на ассемблере

Теперь попробуем сделать что-то посложнее. Например написать на ассемблере обработчик прерывания таймера TIM4. Пример реализации с помощью SPL рассматривался пару лет назад: STM8+SDCC+SPL: функции delay_ms() и delay_us() на таймере TIM4 .

Прошивка весила тогда 1896 байт, почти два килобайта. К сожалению я не нашел способа, как указать компоновщику SDCC удалять из прошивки неиспользуемые функции. В данном случае, в прошивку полностью вошли модули: stm8s_clk, stm8s_tim4, stm8s_gpio, вместе со всеми содержащимися в них функциями. Т.е. можно сказать, что использование SPL c SDCC в принципе бессмысленно.

В данном случае наша прошивка будет иметь размер 252 байта. Это несколько больше чем аналогичный код для ATtiny13 (110 байт), но думаю, что мы еще сократим разрыв в следующих главах.

Для работы с таймером понадобится заголовочный файл: stm8s_tim4.h, с именованными константами из SPL:

#ifndef __STM8S_TIM4_H
#define __STM8S_TIM4_H

#define TIM4_CR1_ARPE ((uint8_t)0x80) /*!< Auto-Reload Preload Enable mask. */
#define TIM4_CR1_OPM  ((uint8_t)0x08) /*!< One Pulse Mode mask. */
#define TIM4_CR1_URS  ((uint8_t)0x04) /*!< Update Request Source mask. */
#define TIM4_CR1_UDIS ((uint8_t)0x02) /*!< Update DIsable mask. */
#define TIM4_CR1_CEN  ((uint8_t)0x01) /*!< Counter Enable mask. */
/*IER*/
#define TIM4_IER_UIE  ((uint8_t)0x01) /*!< Update Interrupt Enable mask. */
/*SR1*/
#define TIM4_SR1_UIF  ((uint8_t)0x01) /*!< Update Interrupt Flag mask. */
/*EGR*/
#define TIM4_EGR_UG   ((uint8_t)0x01) /*!< Update Generation mask. */
/*CNTR*/
#define TIM4_CNTR_CNT ((uint8_t)0xFF) /*!< Counter Value (LSB) mask. */
/*PSCR*/
#define TIM4_PSCR_PSC ((uint8_t)0x07) /*!< Prescaler Value  mask. */
/*ARR*/
#define TIM4_ARR_ARR  ((uint8_t)0xFF) /*!< Autoreload Value mask. */

typedef enum
{
    TIM4_PRESCALER_1    = ((uint8_t)0x00),
    TIM4_PRESCALER_2    = ((uint8_t)0x01),
    TIM4_PRESCALER_4    = ((uint8_t)0x02),
    TIM4_PRESCALER_8    = ((uint8_t)0x03),
    TIM4_PRESCALER_16   = ((uint8_t)0x04),
    TIM4_PRESCALER_32   = ((uint8_t)0x05),
    TIM4_PRESCALER_64   = ((uint8_t)0x06),
    TIM4_PRESCALER_128  = ((uint8_t)0x07)
} TIM4_Prescaler_TypeDef;

/** TIM4 Flags */
typedef enum
{
  TIM4_FLAG_UPDATE                   = ((uint8_t)0x01)
}TIM4_FLAG_TypeDef;


/** TIM4 interrupt sources */
typedef enum
{
  TIM4_IT_UPDATE                     = ((uint8_t)0x01)
}TIM4_IT_TypeDef;

#endif

Этот файл нужно будет положить в директорию inc.

Исходник с вариантом функции delay_ms() на 1кГц счетчике таймера будет выглядеть так:

#include <stdint.h>
#include "stm8s103f.h"
#include "stm8s_tim4.h"

#define CFG_GCR_AL  ((uint8_t)0x02) /*!< Activation Level bit mask */
#define LED 5

extern void delay(void) __interrupt(23);
volatile uint16_t count;

void delay_ms(uint16_t ms)
{
    count = ms;
    CFG_GCR   = CFG_GCR_AL;                 // =2,  set AL bit
    TIM4_SR   = 0x0;                        // Clear Pending Bit
    TIM4_PSCR = TIM4_PRESCALER_128;         // =7, prescaler =128
    TIM4_ARR  = 124;                        // freq Timer IRQ =1kHz
    TIM4_IER  = (uint8_t)TIM4_IT_UPDATE;    // =1, enable interrupt
    TIM4_CR1  = TIM4_CR1_CEN;               // =1, enable counter
    wfi();                                  // goto sleep

    TIM4_CR1  = 0x0;                        //  disable counter
}

int main( void ) {
    // Set fCPU = 16MHz
    CLK_CKDIVR=0;
    // Setup GPIO
    PB_DDR=(1<<LED);
    PB_CR1=(1<<LED);
    // let's go..
    enableInterrupts();
    // main loop
    for(;;)
    {
        PB_ODR ^= (1<<LED);
        delay_ms(1000);
    }
}

В самом начале функции main(), записью нуля в регистр CLK_CKDIVR, частота работы микроконтроллера устанавливается в 16МГц.

В функции delay_ms() происходит инициализация таймера. При этом, в глобальном конфигурационном регистре CFG_GCR, устанавливается флаг AL, при котором после выполнения кода обработчика прерывания, CPU возвращается обратно в спящий режим. Т.о. вместо цикла ожидания используется переход в спящий режим WFI.

Обработчик прерывания на ассемблере у меня получился таким:

.module DELAY
.area CODE
.area HOME
.include "stm8s103f.s"

.globl _delay
_delay:
    ldw x, _count       ; X=count
    jreq l0             ; if (count == 0) exit
    decw x              ; else count--;
    ldw _count,x        ; count=X;
    jra l1
l0:
    bres CFG_GCR, #1    ; clear AL-bit for WakeUp
l1:
    bres TIM4_SR, #0    ; Clear Pending Bit
    iret

Здесь последовательно, из переменной count вычитается по единице, а при достижении нуля сбрасывается AL-флаг и микроконтроллер возвращается из спящего режима.

6. Счетчик на 4-x разрядном семисегментном индикаторе

Теперь добавим в проект драйвер 4-x разрядного семисегментного индикатора, аналогичный написанному ранее для ATtiny13. В данном случае это позволить задействовать инструкции аппаратного целочисленного деления, которые в случае AVR реализовывались программно.

В директорию inc добавляем заголовочный файл led.h:

#ifndef __LED_H__
#define __LED_H__

#define SCLK 5
#define RCLK 4
#define DIO  6

extern void show_led(uint16_t num);

#endif

В каталог проекта нужно будет добавить директорию src, и в него поместить исходник драйвера led.c:

#include <stdint.h>
#include "stm8s103f.h"
#include "led.h"

uint8_t reg=0;

const char digit[10] = {
      0b11000000, // 0
      0b11111001, // 1
      0b10100100, // 2
      0b10110000, // 3
      0b10011001, // 4
      0b10010010, // 5
      0b10000010, // 6
      0b11111000, // 7
      0b10000000, // 8
      0b10010000, // 9
};

void spi_transmit(uint8_t data) {
    uint8_t i;
    for (i=0; i<8; i++)
    {
        PC_ODR=(data & 0x80) ? PC_ODR | (1<<DIO) : PC_ODR & ~(1<<DIO);

        data=(data<<1);
        PC_ODR |= (1<<SCLK);
        PC_ODR &= ~(1<<SCLK);
    }
}

void to_led(uint8_t value, uint8_t reg) {
    if (value <10 && reg < 4) {
        PC_ODR &= ~(1<<RCLK);
        spi_transmit(digit[value]);
        spi_transmit(1<<reg);
        PC_ODR |= (1<<RCLK);
    }
}

void show_led(uint16_t num) {
    switch (reg) {
    case 0:
        to_led((uint8_t)(num%10),0);
        break;
    case 1:
        if (num>=10)
            to_led((uint8_t)((num%100)/10),1);
        break;
    case 2:
        if (num>=100)
            to_led((uint8_t)(num/100),2);
        break;
    }
    reg = (reg ==  2) ? 0 : reg+1;
}

Главный файл: main.c тогда будет таким:

#include <stdint.h>
#include "stm8s103f.h"
#include "stm8s_tim4.h"
#include "led.h"

#define CFG_GCR_AL  ((uint8_t)0x02) /*!< Activation Level bit mask */

extern void delay(void) __interrupt(23);
volatile uint16_t count;

void delay_ms(uint16_t ms)
{
    count = ms;
    CFG_GCR   = CFG_GCR_AL;                 // =2,  set AL bit
    TIM4_SR   = 0x0;                        // Clear Pending Bit
    TIM4_PSCR = TIM4_PRESCALER_128;         // =7, prescaler =128
    TIM4_ARR  = 124;                        // freq Timer IRQ =1kHz
    TIM4_IER  = (uint8_t)TIM4_IT_UPDATE;    // =1, enable interrupt
    TIM4_CR1  = TIM4_CR1_CEN;               // =1, enable counter
    wfi();                                  // goto sleep

    TIM4_CR1  = 0x0;                        //  disable counter
}


int main() {
    // Set fCPU = 16MHz
    CLK_CKDIVR=0;
    // Setup GPIO
    PC_DDR = ((1<<SCLK) | (1<<RCLK) | (1<<DIO)); // Push-Pull Mode
    PC_CR1 = ((1<<SCLK) | (1<<RCLK) | (1<<DIO)); //
    PC_CR2 = ((1<<SCLK) | (1<<RCLK) | (1<<DIO)); // Speed up 10 MHz
    // set vaiables
    // let's go..
    enableInterrupts();
    // main loop
    for(;;)
    {
        static uint8_t i=0;
        static uint16_t  num=0;
        show_led(num);
        delay_ms(5);
        if( ++i == 0)
            num++;
    }
}

Ну и Макеfile претерпевает некоторые изменения:

MCU=stm8
DEVICE=stm8s103f3
FLASHER=stlinkv2
CC=sdcc
AS=sdasstm8
LD=sdldstm8
SIZE=stm8-size
CFLAGS=-V --opt-code-size -I ./inc -c
ASFLAGS=-plosgffw
OBJ=main.rel led.rel
ASM=delay.asm
LINK=delay.rel
TARGET=driver

.PHONY: all clean

%.rel:  %.c
    $(CC) -m$(MCU)  $(CFLAGS)  $<

%.rel:  ./src/%.c
    $(CC) -m$(MCU)  $(CFLAGS)  $<

%.asm:  %.s
    $(AS) $(ASFLAGS)  $<

all:    $(OBJ) $(ASM)
    $(LD) -f ld.lk -i $(TARGET).ihx $(OBJ) $(LINK)
    $(SIZE) $(TARGET).ihx

install:
    stm8flash -c $(FLASHER) -p $(DEVICE) -w $(TARGET).ihx
clean:
    @rm -v *.???

Сборка, прошивка:

$ make all
sdcc -mstm8  -V --opt-code-size -I ./inc -c  main.c
+ /usr/local/bin/sdcpp -nostdinc -Wall -std=c11 -I./inc -obj-ext=.rel -D__SDCC_STACK_AUTO -D__SDCC_CHAR_UNSIGNED -D__SDCC_INT_LONG_REENT -D__SDCC_FLOAT_REENT -D__SDCC=3_6_0 -D__SDCC_VERSION_MAJOR=3 -D__SDCC_VERSION_MINOR=6 -D__SDCC_VERSION_PATCH=0 -DSDCC=360 -D__SDCC_REVISION=9615 -D__SDCC_stm8 -D__STDC_NO_COMPLEX__=1 -D__STDC_NO_THREADS__=1 -D__STDC_NO_ATOMICS__=1 -D__STDC_NO_VLA__=1 -D__STDC_ISO_10646__=201409L -D__STDC_UTF_16__=1 -D__STDC_UTF_32__=1 -isystem /usr/local/bin/../share/sdcc/include/stm8 -isystem /usr/local/share/sdcc/include/stm8 -isystem /usr/local/bin/../share/sdcc/include -isystem /usr/local/share/sdcc/include  main.c
+ /usr/local/bin/sdasstm8 -plosgffw "main.asm"
sdcc -mstm8  -V --opt-code-size -I ./inc -c  src/led.c
+ /usr/local/bin/sdcpp -nostdinc -Wall -std=c11 -I./inc -obj-ext=.rel -D__SDCC_STACK_AUTO -D__SDCC_CHAR_UNSIGNED -D__SDCC_INT_LONG_REENT -D__SDCC_FLOAT_REENT -D__SDCC=3_6_0 -D__SDCC_VERSION_MAJOR=3 -D__SDCC_VERSION_MINOR=6 -D__SDCC_VERSION_PATCH=0 -DSDCC=360 -D__SDCC_REVISION=9615 -D__SDCC_stm8 -D__STDC_NO_COMPLEX__=1 -D__STDC_NO_THREADS__=1 -D__STDC_NO_ATOMICS__=1 -D__STDC_NO_VLA__=1 -D__STDC_ISO_10646__=201409L -D__STDC_UTF_16__=1 -D__STDC_UTF_32__=1 -isystem /usr/local/bin/../share/sdcc/include/stm8 -isystem /usr/local/share/sdcc/include/stm8 -isystem /usr/local/bin/../share/sdcc/include -isystem /usr/local/share/sdcc/include  src/led.c
+ /usr/local/bin/sdasstm8 -plosgffw "led.asm"
sdasstm8 -plosgffw  delay.s
sdldstm8 -f ld.lk -i driver.ihx main.rel led.rel delay.rel
ASlink >> -f ld.lk
ASlink >> -muwx
ASlink >> -Y
ASlink >> -b HOME = 0x8000
ASlink >>
ASlink >> -i
ASlink >> driver.ihx
ASlink >> main.rel
ASlink >> led.rel
ASlink >> delay.rel
stm8-size driver.ihx
   text    data     bss     dec     hex filename
      0     523       0     523     20b driver.ihx

$ make install
stm8flash -c stlinkv2 -p stm8s103f3 -w driver.ihx
Determine FLASH area
Due to its file extension (or lack thereof), "driver.ihx" is considered as INTEL HEX format!
523 bytes at 0x8000... OK
Bytes written: 523

Итого имеем 523 байта на прошивку. Несмотря на аппаратное деление, это все равно больше 384 байта для ATtiny13.

Но, если драйвер работает корректно, идем дальше.

7. Переписывание драйвера индикатора на ассемблере STM8

Единственный способ уменьшить размер драйвера - это переписать его на ассемблере. Если бы речь пошла об AVR, то я бы не решился на этот мартышкин труд никогда в жизни. Отсутствие аппаратного деления требует познания в алгоритмах арифметики и далеко не факт, что код написанный то руки будет лучше кода сгенерированного GCC. Если и будет, то скорее всего, совсем немого.

С ассемблером STM8 совсем другая картина. Большой набор 16-битных операций и поддержка аппаратного целочисленного умножения и деления делают процесс написания ассемблерной программы почти таким же легким как и на Си. Хотя, внимательности требуется, наверное, все-таки побольше.

С другой стороны, это позволяет погрузится в ассемблер, и решая задачки на разложение той или иной Си конструкции - на практике оценить преимущества или недостатки архитектуры STM8.

Не стоит забывать, что SDCC предоставляет ассемблерный код скомпилированных Си исходников. Для начинающих туда будет полезно посматривать, хотя бы для того, чтобы не наломать дров.

Приступаем. Это может быть не очевидно когда пишешь программу на Си, но алгоритм ассемблерной программы существенно упроститься, если:
1) в параметрах функции: void to_led(uint8_t value, uint8_t reg) поменять тип value на uint16_t:

void to_led(uint16_t value, uint8_t reg)

2) В самой функции to_led() поменять:

spi_transmit(1<<reg);

на:

spi_transmit(reg);

3) Соответственно, в вызовах этой функции, поменять числа: 0,1,2 на степени двойки: 1,2,4.

Таким образом, после перекомпиляции, размер прошивки уменьшится с 523 до 518 байт.

Начнем с функции реализации программного SPI - void spi_transmit(uint8_t data):

void spi_transmit(uint8_t data) {
    uint8_t i;
    for (i=0; i<8; i++)
    {
        PC_ODR=(data & 0x80) ? PC_ODR | (1<<DIO) : PC_ODR & ~(1<<DIO);

        data=(data<<1);
        PC_ODR |= (1<<SCLK);
        PC_ODR &= ~(1<<SCLK);
    }
}

Слева показан код который у меня получилось написать, а справа код этой функции сгенерированный SDCC компилятором:

_spi_transmit:
    ld a,#08            ; for (i=0; i<8; i++)
l4:
    tnz (03,sp)         ; PC_ODR=(data & 0x80) ? PC_ODR | (1<<DIO) : PC_ODR & ~(1<<DIO);
    jrpl l2
    bset PC_ODR, #06
    jra l3
l2:
    bres PC_ODR, #06
l3:
    sll (03,sp)         ; data=(data<<1);
    bset PC_ODR, #05    ; PC_ODR |= (1<<SCLK);
    bres PC_ODR, #05    ; PC_ODR &= ~(1<<SCLK);
    dec a
    jrne l4
    ret
_spi_transmit:
    push    a
;   src/led.c: 22: for (i=0; i<8; i++)
    clr (0x01, sp)
00102$:
;   src/led.c: 24: PC_ODR=(data & 0x80) ? PC_ODR | (1<<DIO) : PC_ODR & ~(1<<DIO);
    ldw x, #0x500a
    ld  a, (x)
    tnz (0x04, sp)
    jrpl    00106$
    or  a, #0x40
    jra 00107$
00106$:
    and a, #0xbf
00107$:
    ldw x, #0x500a
    ld  (x), a
;   src/led.c: 26: data=(data<<1);
    sll (0x04, sp)
;   src/led.c: 27: PC_ODR |= (1<<SCLK);
    ldw x, #0x500a
    ld  a, (x)
    or  a, #0x20
    ld  (x), a
;   src/led.c: 28: PC_ODR &= ~(1<<SCLK);
    ldw x, #0x500a
    ld  a, (x)
    and a, #0xdf
    ld  (x), a
;   src/led.c: 22: for (i=0; i<8; i++)
    inc (0x01, sp)
    ld  a, (0x01, sp)
    cp  a, #0x08
    jrc 00102$
;   src/led.c: 30: }
    pop a
    ret

Можно сравнить так же код STM8 с аналогичным кодом AVR для ATtiny13a:

a6 08          ld A, #$08                 cycles=1
0d 03          tnz ($03,SP)               cycles=1
2a 06          jrpl $80a3  (offset=6)     cycles=1-2
72 1c 50 0a    bset $500a, #6             cycles=1
20 04          jra $80a7  (offset=4)      cycles=2
72 1d 50 0a    bres $500a, #6             cycles=1
08 03          sll ($03,SP)               cycles=1
72 1a 50 0a    bset $500a, #5             cycles=1
72 1b 50 0a    bres $500a, #5             cycles=1
4a             dec A                      cycles=1
26 e5          jrne $8099  (offset=-27)   cycles=1-2
81             ret                        cycles=4
00000068 <spi_transmit>:
e028 ldi r18, 0x8         1
b398 in r9, 0x18          1
ff87 sbrs r24, 7          1-3
c002 rjmp 0x3a (2)        2
6098 ori r25, 0x8         1
c001 rjmp 0x3b (1)        2
7f97 andi r25, 0xf7       1
bb98 out 0x18, r25        1
0f88 add r24, r24         1
9ac2 sbi 0x18, 2          2
98c2 cbi 0x18, 2          2
5021 subi r18, 0x1        1
f7a1 brne 0x35 (-12)      1-2
9508 ret                  4

Далее идет функция void to_led(uint16_t value, uint8_t reg):

void to_led(uint16_t value, uint8_t reg) {
    if (value <10 && reg < 4) {
        PC_ODR &= ~(1<<RCLK);
        spi_transmit(digit[value]);
        spi_transmit(reg);
        PC_ODR |= (1<<RCLK);
    }
}

В ассемблерном варианте я убрал проверку допустимых значений входных параметров:

_to_led:
    bres PC_ODR, #04    ; PC_ODR &= ~(1<<RCLK);
    ldw x,(03,sp)       ; x=value
    ld a, (_digit+0,x)  ; digit[value]
    push a
    call _spi_transmit  ; spi_transmit(digit[value]);
    ld a,(06,sp)        ; a=reg
    ld (01,sp),a
    call _spi_transmit  ; spi_transmit(reg);
    pop a               ; restore stack
    bset PC_ODR, #04
    ret
_to_led:
;   src/led.c: 33: if (value <10 && reg < 4) {
    ldw x, (0x03, sp)
    cpw x, #0x000a
    jrc 00114$
    ret
00114$:
    ld  a, (0x05, sp)
    cp  a, #0x04
    jrc 00115$
    ret
00115$:
;   src/led.c: 34: PC_ODR &= ~(1<<RCLK);
    ldw x, #0x500a
    ld  a, (x)
    and a, #0xef
    ld  (x), a
;   src/led.c: 35: spi_transmit(digit[value]);
    ldw x, #_digit+0
    addw    x, (0x03, sp)
    ld  a, (x)
    push    a
    call    _spi_transmit
    pop a
;   src/led.c: 36: spi_transmit(reg);
    ld  a, (0x05, sp)
    push    a
    call    _spi_transmit
    pop a
;   src/led.c: 37: PC_ODR |= (1<<RCLK);
    ldw x, #0x500a
    ld  a, (x)
    or  a, #0x10
    ld  (x), a
;   src/led.c: 39: }
    ret

Сравнение с AVR:

72 19 50 0a    bres $500a, #4    cycles=1
1e 03          ldw X, ($03,SP)   cycles=2
d6 81 1a       ld A, ($811a,X)   cycles=1
88             push A            cycles=1
cd 80 97       call $8097        cycles=4
7b 06          ld A, ($06,SP)    cycles=1
6b 01          ld ($01,SP),A     cycles=1
cd 80 97       call $8097        cycles=4
84             pop A             cycles=1
72 18 50 0a    bset $500a, #4    cycles=1
81             ret               cycles=4
93cf push r28             2
308a cpi r24, 0xa         1
f488 brcc 0x56 (17)       1-2
3064 cpi r22, 0x4         1
f478 brcc 0x56 (15)       1-2
2fc6 mov r28, r22         1
98c4 cbi 0x18, 4          2
2fe8 mov r30, r24         1
e0f0 ldi r31, 0x0         1
5dee subi r30, 0xde       1
4fff sbci r31, 0xff       1
9184 lpm r24, Z           3
dfe5 rcall 0x34 (-27)     3
e081 ldi r24, 0x1         1
c001 rjmp 0x52 (1)        2
0f88 add r24, r24         1
95ca dec r28              1
f7ea brpl 0x51 (-3)       1-2
dfdf rcall 0x34 (-33)     3
9ac4 sbi 0x18, 4          2
91cf pop r28              2
9508 ret                  4

На самом деле вариант STM8 можно еще сократить на пару байт, если заменит call на callr.

И последняя, самая громоздкая из-за наличия деления функция - void show_led(uint16_t num):

void show_led(uint16_t num) {
    switch (reg) {
    case 0:
        to_led((uint8_t)(num%10),1);
        break;
    case 1:
        if (num>=10)
            to_led((uint8_t)((num%100)/10),2);
        break;
    case 2:
        if (num>=100)
            to_led((uint8_t)(num/100),4);
        break;
    }
    reg = (reg ==  2) ? 0 : reg+1;
}

Вариант написанный от руки (слева) и вариант сгенерированный компилятором SDCC (справа):

_show_led:
    ldw x, (03,sp)      ; x=num
    ld a,_reg+0
    jrne l6             ; case 0:
    ld a,#02
    ld _reg+0,a         ; reg=01
    ldw y, #0x000a      ; y=10
    divw x,y            ; y=(num%10)
    push #01
    pushw y
    call _to_led        ; to_led((uint16_t)(num%10),1);
    jra l5              ; break
l6:
    dec a
    ld _reg+0,a
    jrne l7
    cpw x,#0x000a       ; if (num >= 10)
    jrmi l8
    ldw y,#0x0064       ; y=100
    divw x,y            ; y=(num%100)
    ldw x,y             ; x=y
    ldw y, #0x000a      ; y=10
    divw x,y            ; x=(num%100)/10
    push #0x02
    pushw x
    call _to_led        ; to_led((uint16_t)((num%100)/10),2);
    jra l5              ; break
l7:
    cpw x,#0x0064       ; if (num>=100)
    jrmi l8
    ldw y, #0x0064      ; y=100
    divw x,y            ; x=(num/100)
    push #0x04
    pushw x
    call _to_led        ; to_led((uint16_t)(num/100),4)
l5:
    addw sp,#03
l8:
    ret
_show_led:
    pushw   x
;   src/led.c: 42: switch (reg) {
    ld  a, _reg+0
    cp  a, #0x00
    jreq    00101$
    ld  a, _reg+0
    cp  a, #0x01
    jreq    00102$
    ld  a, _reg+0
    cp  a, #0x02
    jreq    00105$
    jra 00108$
;   src/led.c: 43: case 0:
00101$:
;   src/led.c: 44: to_led((uint8_t)(num%10),1);
    ldw x, (0x05, sp)
    ldw y, #0x000a
    divw    x, y
    ld  a, yl
    clrw    x
    ld  xl, a
    push    #0x01
    pushw   x
    call    _to_led
    addw    sp, #3
;   src/led.c: 45: break;
    jra 00108$
;   src/led.c: 46: case 1:
00102$:
;   src/led.c: 47: if (num>=10)
    ldw x, (0x05, sp)
    cpw x, #0x000a
    jrc 00108$
;   src/led.c: 48: to_led((uint8_t)((num%100)/10),2);
    ldw x, (0x05, sp)
    ldw y, #0x0064
    divw    x, y
    ldw (0x01, sp), y
    ldw x, (0x01, sp)
    ldw y, #0x000a
    divw    x, y
    clr a
    ld  xh, a
    push    #0x02
    pushw   x
    call    _to_led
    addw    sp, #3
;   src/led.c: 49: break;
    jra 00108$
;   src/led.c: 50: case 2:
00105$:
;   src/led.c: 51: if (num>=100)
    ldw x, (0x05, sp)
    cpw x, #0x0064
    jrc 00108$
;   src/led.c: 52: to_led((uint8_t)(num/100),4);
    ldw x, (0x05, sp)
    ldw y, #0x0064
    divw    x, y
    clr a
    ld  xh, a
    push    #0x04
    pushw   x
    call    _to_led
    addw    sp, #3
;   src/led.c: 54: }
00108$:
;   src/led.c: 55: reg = (reg ==  2) ? 0 : reg+1;
    ld  a, _reg+0
    cp  a, #0x02
    jrne    00111$
    clr a
    jra 00112$
00111$:
    ld  a, _reg+0
    inc a
00112$:
    ld  _reg+0, a
;   src/led.c: 56: }
    popw    x
    ret

Сравнение с AVR в данном случае будет таким:

1e 03         ldw X, ($03,SP)            cycles=2
c6 00 05      ld A, $5                   cycles=1
26 13         jrne $80e9  (offset=19)    cycles=1-2
a6 02         ld A, #$02                 cycles=1
c7 00 05      ld $5,A                    cycles=1
90 ae 00 0a   ldw Y, #$a                 cycles=2
65            divw X, Y                  cycles=2-17
4b 01         push #$01                  cycles=1
90 89         pushw Y                    cycles=2
cd 80 b5      call $80b5                 cycles=4
20 2e         jra $8117  (offset=46)     cycles=2
4a            dec A                      cycles=1
c7 00 05      ld $5,A                    cycles=1
26 18         jrne $8107  (offset=24)    cycles=1-2
a3 00 0a      cpw X, #$a                 cycles=2
2b 25         jrmi $8119  (offset=37)    cycles=1-2
90 ae 00 64   ldw Y, #$64                cycles=2
65            divw X, Y                  cycles=2-17
93            ldw X, Y                   cycles=1
90 ae 00 0a   ldw Y, #$a                 cycles=2
65            divw X, Y                  cycles=2-17
4b 02         push #$02                  cycles=1
89            pushw X                    cycles=2
cd 80 b5      call $80b5                 cycles=4
20 10         jra $8117  (offset=16)     cycles=2
a3 00 64      cpw X, #$64                cycles=2
2b 0d         jrmi $8119  (offset=13)    cycles=1-2
90 ae 00 64   ldw Y, #$64                cycles=2
65            divw X, Y                  cycles=2-17
4b 04         push #$04                  cycles=1
89            pushw X                    cycles=2
cd 80 b5      call $80b5                 cycles=4
5b 03         addw SP, #$03              cycles=2
81            ret                        cycles=4
000000b0 <show_led>:
  b0:   20 91 63 00     lds r18, 0x0063
  b4:   21 30           cpi r18, 0x01
  b6:   49 f0           breq    .+18
  b8:   18 f0           brcs    .+6
  ba:   22 30           cpi r18, 0x02
  bc:   91 f0           breq    .+36
  be:   1a c0           rjmp    .+52
  c0:   6a e0           ldi r22, 0x0A
  c2:   70 e0           ldi r23, 0x00
  c4:   46 d0           rcall   .+140
  c6:   60 e0           ldi r22, 0x00
  c8:   14 c0           rjmp    .+40
  ca:   8a 30           cpi r24, 0x0A
  cc:   91 05           cpc r25, r1
  ce:   90 f0           brcs    .+36
  d0:   64 e6           ldi r22, 0x64
  d2:   70 e0           ldi r23, 0x00
  d4:   3e d0           rcall   .+124
  d6:   6a e0           ldi r22, 0x0A
  d8:   70 e0           ldi r23, 0x00
  da:   3b d0           rcall   .+118
  dc:   86 2f           mov r24, r22
  de:   61 e0           ldi r22, 0x01
  e0:   08 c0           rjmp    .+16
  e2:   84 36           cpi r24, 0x64
  e4:   91 05           cpc r25, r1
  e6:   30 f0           brcs    .+12
  e8:   64 e6           ldi r22, 0x64
  ea:   70 e0           ldi r23, 0x00
  ec:   32 d0           rcall   .+100
  ee:   86 2f           mov r24, r22
  f0:   62 e0           ldi r22, 0x02
  f2:   c8 df           rcall   .-112
  f4:   80 91 63 00     lds r24, 0x0063
  f8:   82 30           cpi r24, 0x02
  fa:   11 f0           breq    .+4
  fc:   8f 5f           subi    r24, 0xFF
  fe:   01 c0           rjmp    .+2
 100:   80 e0           ldi r24, 0x00
 102:   80 93 63 00     sts 0x0063, r24
 106:   08 95           ret

00000152 <__udivmodhi4>:
 152:   aa 1b           sub r26, r26
 154:   bb 1b           sub r27, r27
 156:   51 e1           ldi r21, 0x11
 158:   07 c0           rjmp    .+14

0000015a <__udivmodhi4_loop>:
 15a:   aa 1f           adc r26, r26
 15c:   bb 1f           adc r27, r27
 15e:   a6 17           cp  r26, r22
 160:   b7 07           cpc r27, r23
 162:   10 f0           brcs    .+4
 164:   a6 1b           sub r26, r22
 166:   b7 0b           sbc r27, r23

00000168 <__udivmodhi4_ep>:
 168:   88 1f           adc r24, r24
 16a:   99 1f           adc r25, r25
 16c:   5a 95           dec r21
 16e:   a9 f7           brne    .-22
 170:   80 95           com r24
 172:   90 95           com r25
 174:   68 2f           mov r22, r24
 176:   79 2f           mov r23, r25
 178:   8a 2f           mov r24, r26
 17a:   9b 2f           mov r25, r27
 17c:   08 95           ret

Согласно документации (PM0044), инструкция деления divw, которая может выполняться до 17 машинных циклов, прерываема. Т.е. во время ее выполнения прерывание не будет ждать, пока инструкция завершит работу, управление будет сразу передано обработчику прерывания.

Что касается программной реализации деления в AVR, тов данном случае страшен не столько размер кода, сколько то, что эта операция выполняется в цикле.

Я уже приводил ранее эту табличку:

Шестнадцатибитное беззнаковое деление, с оптимизацией по размеру кода (как в данном случае), согласно этой табличке будет выполняться за 243 машинных цикла:

Ок. В результате всех этих трудов, размер прошивки удалось сократить до 430 байт, и как следствие скорость выполнения также возраста.

8. Использование аппаратного SPI-интерфейса в режиме мастера

Еще одним способом снизить размер кода и повысить скорость работы драйвера, заключается в использовании аппаратного SPI интерфейса, вместо его программной реализации в функции void spi_transmit(uint8_t data).

Переподключать индикатор никуда не надо, т.к. использовались пины SPI: С6(SPI_MOSI)->DIO, C5(SPI_CLK)->SCLK и С4->RCLK.

В main.c, вместо строк:

    // Setup GPIO
    PC_DDR = ((1<<SCLK) | (1<<RCLK) | (1<<DIO)); // Push-Pull Mode
    PC_CR1 = ((1<<SCLK) | (1<<RCLK) | (1<<DIO)); //
    PC_CR2 = ((1<<SCLK) | (1<<RCLK) | (1<<DIO)); // Speed up 10 MHz

,нужно будет разместить код инициализации SPI модуля:

    // Setup GPIO
    PC_DDR = (1<<RCLK);
    PC_CR1 = (1<<RCLK);
    // SPI setup
    // master mode,  maximum speed = fCPU/2, enable SPI
    SPI_CR1=(uint8_t)(SPI_ENABLE|SPI_FIRSTBIT_MSB|SPI_MODE_MASTER|SPI_BAUDRATEPRESCALER_2);

Функция void spi_transmit(uint8_t data) тогда будет выглядеть так:

void spi_transmit(uint8_t data) {
    SPI_DR=data;
    while (SPI_SR & SPI_FLAG_BSY);
}

Вариант с Си-драйвером в таком случае сокращается до 481 байта.

В ассемблерном варианте драйвера подпрограммы: _spi_transmit и _to_led теперь будут выглядеть так:

_spi_transmit:
    ld SPI_DR,a
l2:
    btjt SPI_SR,#7,l2
    ret

_to_led:
    bres PC_ODR, #04    ; PC_ODR &= ~(1<<RCLK);
    ldw x,(03,sp)       ; x=value
    ld a, (_digit+0,x)  ; digit[value]
    callr _spi_transmit ; spi_transmit(digit[value]);
    ld a,(05,sp)        ; a=reg
    callr _spi_transmit ; spi_transmit(reg);
    bset PC_ODR, #04
    ret

В этом случае прошивка сокращается до 403 байт.

9. Счетчик импульсов, вариант на Си

Теперь можно приступать к тому, ради чего все и делалось: тестированию на скорость выполнения. Тестировать я буду традиционно на скорость срабатывания внешних прерываний STM8S на примере счетчика импульсов. Ранее, аналогичные тесты для AVR ATtiny13 я описывал здесь: Счетчик импульсов на ассемблерном прерывании и здесь: Простой протокол на счетчике импульсов.

Забегая вперед сразу скажу, что скорость обработки внешних прерываний в STM8 на порядок выше чем у AVR. И дело не в том, что ATtiny13 работает на 9.6 МHz, а STM8S103f3 на 16MHz.

Напомню картинку из предыдущей статьи:

В STM8 имеется несколько уровней привилегий исполнения того или иного кода. Во-первых имеется основная программа в которой работает драйвер индикатора, во-вторых прерывание таймера которое отсчитывает задержки динамической индикации. Поверх всего этого нужно на внешних прерываниях построить счетчик импульсов, который бы работал так, что при начале поступления импульсов весь остальной код останавливался, включался режим WFI(без этой штуки скорость будет на уровне AVR), т.е. чтобы процессор занимался только их подсчетом. После завершения подсчета, нужно разблокировать ход основной программы, и загрузить в драйвер полученное число.

В итоге у меня получился такой вариант:

#include <stdint.h>
#include "stm8s103f.h"
#include "stm8s_tim4.h"
#include "stm8s_spi.h"
#include "led.h"

#define CFG_GCR_AL  ((uint8_t)0x02) /*!< Activation Level bit mask */
#define LATCH_C (1<<3)
#define DATA_B  (1<<4)

extern void delay(void) __interrupt(23);
volatile uint16_t count;
volatile uint16_t num;
volatile uint16_t i;

void ext_port_b(void) __interrupt(4) {
    i++;
}

void ext_port_c(void) __interrupt(5) {
    if (PC_IDR & LATCH_C) {     // if end of receive
        num=i;
        CFG_GCR = 0;            // wake-up
        PB_CR2 = 0;             // disable counter interrupt
        if(count)               // if timer was work
            TIM4_CR1=TIM4_CR1_CEN;  // =1, enable timer
    } else {                    // if begin of recive
        if (count)              // if timer wokring...
            TIM4_CR1 = 0;       // then disable timer.
        i=0;                    // clear counter
        CFG_GCR = CFG_GCR_AL;   // =2,  set AL bit
        PB_CR2 = (DATA_B);      // enable counter interrupt
        // let's go...  sleeeep
        wfi();                  // disable main program, only irq handlers is ON
    }
}

void delay_ms(uint16_t ms)
{
    count = ms;
    CFG_GCR   = CFG_GCR_AL;                 // =2,  set AL bit
    TIM4_SR   = 0x0;                        // Clear Pending Bit
    TIM4_PSCR = TIM4_PRESCALER_128;         // =7, prescaler =128
    TIM4_ARR  = 124;                        // freq Timer IRQ =1kHz
    TIM4_IER  = (uint8_t)TIM4_IT_UPDATE;    // =1, enable interrupt
    TIM4_CR1  = TIM4_CR1_CEN;               // =1, enable counter
    wfi();                                  // goto sleep

    TIM4_CR1  = 0x0;                        //  disable counter
}


int main() {
    // Set fCPU = 16MHz
    CLK_CKDIVR=0;
    // Setup GPIO
    PC_DDR = (1<<RCLK);
    PC_CR1 = (1<<RCLK);
    // IRQ setup
    PC_CR2 =(LATCH_C);
    EXTI_CR1 = (1<<2)|(1<<4)|(1<<5);   // PORTB<-->rising edge; PORTC<-->any edge
    // SPI setup
    // master mode,  maximum speed = fCPU/2, enable SPI
    SPI_CR1=(uint8_t)(SPI_ENABLE|SPI_FIRSTBIT_MSB|SPI_MODE_MASTER|SPI_BAUDRATEPRESCALER_2);
    // variables init
    num=0;
    // let's go..
    enableInterrupts();
    // main loop
    for(;;)
    {
        delay_ms(5);
        show_led(num);
    }
}

На выводе С3 находится "защелка", у которой смена уровня с высокого на низкий означает начало передачи, а смена уровня с низкого на высокий означает конец передачи. На PB4 и прерывании порта B расположен пин подсчета импульсов.

Проверочная программа на Arduino будет такой:

#define PULSE 3
#define LATCH 2

void send_number(uint16_t value);

void setup() {
    Serial.begin(9600);
    // put your setup code here, to run once:
    pinMode(PULSE,OUTPUT);
    pinMode(LATCH,OUTPUT);
    digitalWrite(PULSE, LOW);
    digitalWrite(LATCH, HIGH);
}

void loop() {
  static long previous;
  static uint16_t num;
  num=(num == 573) ? 219 : 573;
  previous=millis();
  send_number(num);
  Serial.print("latency: ");
  Serial.print(millis()-previous);
  Serial.println(" ms");
  delay(1000);
}

void send_number(uint16_t num)
{
    digitalWrite(LATCH, LOW); // START
    delayMicroseconds(10);
    for(int i=0; i<num;i++)
    {
      PORTD |= (1<<PD3);
      PORTD &= ~(1<<PD3);

      //for(uint8_t j=0;j<1;j++) // DEALY
      asm volatile("nop");
      asm volatile("nop");
    }
    digitalWrite(LATCH, HIGH); // STOP
}

Здесь вместо цикла из 21-такта (как для ATtiny13), для интервала между импульсами используется всего две инструкции NOP. Т.е. пакет импульсов возможно будет передать со скоростью почти в 10 раз(!) большей нежели в случае использования ATtiny13.

10. Счетчик импульсов, вариант на ассемблере

На ассемблере у нас имеется больше возможностей управления процессом исполнения кода. STM8 поддерживает вложенные прерывания, т.е. когда во время выполнения обработчика прерывания возникает прерывание с бо́льшим уровнем привилегий, то выполнение первоначального прерывания приостанавливается, и управление переходит к обработке нового прерывания.

Внешние прерывания имеют более высокий уровень привилегии нежели прерывания таймера. И это означает, что драйвер индикатора можно будет целиком разместить в прерывании таймера. В случае STM8 работа одного индикатора не будет как-либо значимо загружать CPU.

На внешних прерываниях можно построить счетчик которых будет "незримо" работать в фоне. Главный hack здесь заключается в том, что прерывание счетчика можно будет сделать всего на одной инструкции инкремента. В сочетании с WFI это даст максимальную скорость работы.

Код Си-программы main.c:

#include <stdint.h>
#include "stm8s103f.h"
#include "stm8s_tim4.h"
#include "stm8s_spi.h"

#define CFG_GCR_AL  ((uint8_t)0x02) // Activation Level bit mask
#define RCLK 4
#define PERIOD 5 // ms
#define LATCH_C (1<<3)
#define DATA_B  (1<<4)

extern void delay(void) __interrupt(23);
extern void ext_port_b(void) __interrupt(4);
extern void ext_port_c(void) __interrupt(5);
volatile uint16_t count;
volatile uint8_t reg=0;
volatile uint16_t num;
volatile uint16_t i;


int main() {
    // Set fCPU = 16MHz
    CLK_CKDIVR=0;
    // Setup GPIO
    PC_DDR = (1<<RCLK);
    PC_CR1 = (1<<RCLK);
    // IRQ setup
    PC_CR2 =(LATCH_C);
    EXTI_CR1 = (1<<2)|(1<<4)|(1<<5);   // PORTB<-->rising edge; PORTC<-->any edge
    // SPI setup
    // master mode,  maximum speed = fCPU/2, enable SPI
    SPI_CR1=(uint8_t)(SPI_ENABLE|SPI_FIRSTBIT_MSB|SPI_MODE_MASTER|SPI_BAUDRATEPRESCALER_2);
    // Timer setup
    count = PERIOD;
    CFG_GCR   = CFG_GCR_AL;                 // =2,  set AL bit
    TIM4_SR   = 0x0;                        // Clear Pending Bit
    TIM4_PSCR = TIM4_PRESCALER_128;         // =7, prescaler =128
    TIM4_ARR  = 124;                        // freq Timer IRQ =1kHz
    TIM4_IER  = (uint8_t)TIM4_IT_UPDATE;    // =1, enable interrupt
    TIM4_CR1  = TIM4_CR1_CEN;               // =1, enable counter
    // set variables
    num=0; reg=0;
    // let's go..
    enableInterrupts();
    wfi();                                  // goto sleep
    // main loop
    for(;;){ };
}

Ассемблерная часть программы:

.module DELAY
.area CODE
.area HOME
.include "stm8s103f.s"
.globl _ext_port_b
_ext_port_b:
    incw x              ; increment of counter
    iret

.globl _ext_port_c
_ext_port_c:            ; START
    btjt PC_IDR,#3,l3
    mov TIM4_CR1,#0     ; stop timer
    ldw _i,x            ; save x register
    ldw x,#0            ; clear x
    bset PB_CR2,#4      ; enable ext PORT_B IRQ
    iret
l3:                     ; STOP
    ldw _num,x          ; result
    ldw x,_i            ; restore x register
    mov PB_CR2,#0       ; stop ext PORT_B IRQ
    mov TIM4_CR1,#01    ; continue timer
    iret

.globl _delay
_delay:
    ldw x, _count       ; X=count
    jreq l0             ; if (count == 0) exit
    decw x              ; else count--;
    ldw _count,x        ; count=X;
    jra l1
l0:
    ldw x,#5
    ldw _count,x
    call  _show_led
l1:
    bres TIM4_SR, #0    ; Clear Pending Bit
    iret

_spi_transmit:
    ld SPI_DR,a
l2:
    btjt SPI_SR,#7,l2
    ret

_to_led:
    bres PC_ODR, #04    ; PC_ODR &= ~(1<<RCLK);
    ldw x,(03,sp)       ; x=value
    ld a, (_digit+0,x)  ; digit[value]
    callr _spi_transmit ; spi_transmit(digit[value]);
    ld a,(05,sp)        ; a=reg
    callr _spi_transmit ; spi_transmit(reg);
    bset PC_ODR, #04
    ret

_show_led:
    ldw x, _num
    ld a,_reg+0
    jrne l6             ; case 0:
    ld a,#02
    ld _reg+0,a         ; reg=01
    ldw y, #0x000a      ; y=10
    divw x,y            ; y=(num%10)
    push #01
    pushw y
    call _to_led        ; to_led((uint16_t)(num%10),1);
    jra l5              ; break
l6:
    dec a
    ld _reg+0,a
    jrne l7
    cpw x,#0x000a       ; if (num >= 10)
    jrmi l8
    ldw y,#0x0064       ; y=100
    divw x,y            ; y=(num%100)
    ldw x,y             ; x=y
    ldw y, #0x000a      ; y=10
    divw x,y            ; x=(num%100)/10
    push #0x02
    pushw x
    call _to_led        ; to_led((uint16_t)((num%100)/10),2);
    jra l5              ; break
l7:
    cpw x,#0x0064       ; if (num>=100)
    jrmi l8
    ldw y, #0x0064      ; y=100
    divw x,y            ; x=(num/100)
    push #0x04
    pushw x
    call _to_led        ; to_led((uint16_t)(num/100),4)
l5:
    addw sp,#03
l8:
    ret

_digit:
    .db #0xc0   ; 192
    .db #0xf9   ; 249
    .db #0xa4   ; 164
    .db #0xb0   ; 176
    .db #0x99   ; 153
    .db #0x92   ; 146
    .db #0x82   ; 130
    .db #0xf8   ; 248
    .db #0x80   ; 128
    .db #0x90   ; 144

В проверочном примере для Arduino, в данном случае, задержку можно будет убрать совсем. Т.е. импульсы будут идти со скоростью в несколько мегагерц.

Полные исходники, сборочные файлы и скомпилированные прошивки можно скачать здесь.