В данной статье я попытался рассмотреть вопрос использования ассемблера в Си-программах для компилятора 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 удобно рассматривать в качестве какой-то базовой системы, этакой точкой отсчета.
Полные исходники, сборочные файлы и скомпилированные прошивки можно будет скачать по ссылке в конце статьи.
Примечание от 01.09.2022г. В SDCC версии 4.2 поменялся формат передачи аргументов функций. Если раньше все аргументы передавались через стек, то теперь они передаются через регистры. Поэтому для совместимости со старым кодом следует добавлять опцию компиляции "--sdcccall 0".
Вначале предстоит сделать несколько рутинных действий. Создаем директорию проекта:
$ 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
В итоге, код сократился на шесть байтов, не ахти что, но все же.
Первым делом мне хотелось сделать на ассемблере аналог 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, как тяжелая вода отличается от той, что идет из крана.
Так же как и 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
Теперь у нас есть функция которая формирует программную задержку на одну секунду.
К сожалению, в документации к 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мс.
Теперь попробуем сделать что-то посложнее. Например написать на ассемблере обработчик прерывания таймера 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-флаг и микроконтроллер возвращается из спящего режима.
Теперь добавим в проект драйвер 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.
Но, если драйвер работает корректно, идем дальше.
Единственный способ уменьшить размер драйвера - это переписать его на ассемблере. Если бы речь пошла об 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 байт, и как следствие скорость выполнения также возраста.
Еще одним способом снизить размер кода и повысить скорость работы драйвера, заключается в использовании аппаратного 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 байт.
Теперь можно приступать к тому, ради чего все и делалось: тестированию на скорость выполнения. Тестировать я буду традиционно на скорость срабатывания внешних прерываний 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.
На ассемблере у нас имеется больше возможностей управления процессом исполнения кода. 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, в данном случае, задержку можно будет убрать совсем. Т.е. импульсы будут идти со скоростью в несколько мегагерц.
Полные исходники, сборочные файлы и скомпилированные прошивки можно скачать здесь.