STM8: знакомство с архитектурой через дизассемблирование Blink

разделы: STM8 , АССЕМБЛЕР , дата: 18 апреля 2016г.

Думаю, что настало время познакомиться с STM8 поближе. Для тестирования я буду использовать чип STM8S103F3P6, но все сказаное в равной степени будет относится и к STM8L051F3P6. В плане общей архитектуры различия между ними минимальны, совпадают даже адреса регистров GPIO.

Так как ассемблер STM8 является наследником STM7, то при каких-то неясных моментах может помочь методичка ХАИ по STM7 "Проектирование встроенных систем на микроконтроллерах STMicroelectronics". Методичка написана на русском языке.

Код STM7 совместим с STM8, обратное неверно.

    Главные отличия STM8 от STM7:
  • Адресная шина стала 24-битной вместо 16-битной в STM7, как следствие, регистр PC изменил разрядность с 16-и до 24-и бит ;
  • Индексные регистры X, Y стали 16-битными вместо 8-битных в STM7;
  • Регистр указателя стека SP стало возможно использовать как индексный регистр.

Итак. В прошлый раз, когда я скомпилировал Blink для чипа stm8s103f3p3 с помощью компилятора SDCC, то на выходе получил несколько файлов:

leds.asm  leds.c  leds.cdb  leds.ihx  leds.lk  leds.lst  leds.map  leds.rel  leds.rst  leds.sym

Полагаю, что настало время, заглянуть в содержимое файла leds.asm чтобы узнать из чего же состоят программы stm8s и так ли хорош черт как заявляют его пресс-релизы. Но сперва, я приведу программу blink к более классическому виду:

#include "stm8s.h"
#include "stm8s_gpio.h"

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

int main( void )
{
 	GPIOB->DDR != GPIO_PIN_5;
   	for(;;){
    		delay(60000);
      		GPIOB->ODR ^= GPIO_PIN_5;
   	}
}

Ассемблерный код, который мы получаем в результате компиляции:

;--------------------------------------------------------
; File Created by SDCC : free open source ANSI-C Compiler
; Version 3.5.0 #9253 (Aug 26 2015) (Linux)
; This file was generated Sun Apr 17 16:58:06 2016
;--------------------------------------------------------
	.module led
	.optsdcc -mstm8

;--------------------------------------------------------
; Public variables in this module
;--------------------------------------------------------
	.globl _main
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
	.area DATA
;--------------------------------------------------------
; ram data
;--------------------------------------------------------
	.area INITIALIZED
;--------------------------------------------------------
; Stack segment in internal ram 
;--------------------------------------------------------
	.area	SSEG
__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
__sdcc_gs_init_startup:
__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
;	./led.c: 4: static void delay(unsigned int t)
;	-----------------------------------------
;	 function delay
;	-----------------------------------------
_delay:
	sub	sp, #2
;	./led.c: 6: while(t--);
	ldw	x, (0x05, sp)
00101$:
	ldw	(0x01, sp), x
	decw	x
	ldw	y, (0x01, sp)
	jrne	00101$
	addw	sp, #2
	ret
;	./led.c: 9: int main( void )
;	-----------------------------------------
;	 function main
;	-----------------------------------------
_main:
;	./led.c: 11: GPIOB->DDR |= GPIO_PIN_5;
	ldw	x, #0x5007
	ld	a, (x)
	or	a, #0x20
	ld	(x), a
00102$:
;	./led.c: 13: delay(60000);
	push	#0x60
	push	#0xea
	call	_delay
	addw	sp, #2
;	./led.c: 14: GPIOB->ODR ^= GPIO_PIN_5;
	ldw	x, #0x5005
	ld	a, (x)
	xor	a, #0x20
	ld	(x), a
	jra	00102$
	ret
	.area CODE
	.area INITIALIZER
	.area CABS (ABS)

Данный исходник зрительно делится на две секции: таблица векторов(.area HOME) и секция кода (.area CODE). Выглядит так, как будто имеется 30 прерываний, плюс reset и trap(программное прерывание). Однако, если взглянуть на таблицу прерываний stm8s103f3, картинка будет несколько другая:

В stm8l051f3 периферии побольше, поэтому там и таблица прерываний существенно объемнее:

Заголовочный файл под SDCC для L-серии можно найти здесь.

Тогда вариант Blink для stm8l051f3 будет таким:

#include <stm8l.h>

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

int main( void )
{
 	PB_DDR |=  (1<<5);
 	PB_CR1 |=  (1<<5);
   	for(;;){
    		delay(60000);
      		PB_ODR ^= (1<<5);
   	}
}

Ситуация с контрольным регистром CR1, для меня пока не совсем понятна. Казалось бы он должен быть установлен и в STM8S103F3, онднако у меня работает и без него. Потом надо разобраться. Однако вернемся от перефирии обратно к ассемблеру. Секция кода также делится на две части: функцию delay и главную функцию main(). Прежде чем идти дальше, следует разобраться с адресным пространством и регистрами STM8. Регистры в STM8 служат объектом нещадной критики. Их всего(!) шесть. Картинка из PM0044:

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

STM8 имеет общую 24-битную адресную шину котрая позволяет адресовать 16Мбайт памяти.
C адреса 0х000000 начинается оперативная память(RAM).
C адреса 0х008000 начинается таблица векторов.
C адреса 0х008080 начинается код программы.
В stm8l051f3 код программы начинается c адреса 0x008100

Различают короткую адресацию, когда адрес занимает один байт, длинную в пределах двух байт и расширенную когда указывается трех-байтный адрес. Соответственно вся вся память делится на 256 секций, а каждая секция делится на 256 страниц(pages):

Счетчик стека SP по умолчанию указывает на нижнюю границу(максимальный адрес) оперативки. При использовании команды PUSH, счетчик SP автоматически уменьшается. При использовании команды POP, счетчик SP автоматически увеличивается. Аргументом команд PUSH и POP могут быть как 8-битный аккумулятор, так и 16-битные X и Y:

Возвращаясь к ассемблеру, посмотрим как SDCC раскладывает строку кода на Си: GPIOB->ODR ^= GPIO_PIN_5;

; ./l.c: 14: GPIOB->ODR ^= GPIO_PIN_5;
 	ldw x, #0x5005
 	ld a, (x)
 	xor a, #0x20
 	ld (x), a

и сравним с аналогичным кодом для AVR:

	24: 91 e0        ldi r25, 0x01 ; 1
  	26: 88 b3        in r24, 0x18 ; 24
  	28: 89 27        eor r24, r25
  	2a: 88 bb        out 0x18, r24 ; 24

Упс, появились какие-то непонятные скобочки... Попробуем разобраться. Во-первых, что такое GPIO_PIN_5? Ответ кроется в заголовочном файле stm8s_gpio.h:

typedef enum
{
	  GPIO_PIN_0    = ((uint8_t)0x01),  /*!< Pin 0 selected */
	  GPIO_PIN_1    = ((uint8_t)0x02),  /*!< Pin 1 selected */
	  GPIO_PIN_2    = ((uint8_t)0x04),  /*!< Pin 2 selected */
	  GPIO_PIN_3    = ((uint8_t)0x08),   /*!< Pin 3 selected */
	  GPIO_PIN_4    = ((uint8_t)0x10),  /*!< Pin 4 selected */
	  GPIO_PIN_5    = ((uint8_t)0x20),  /*!< Pin 5 selected */
	  GPIO_PIN_6    = ((uint8_t)0x40),  /*!< Pin 6 selected */
	  GPIO_PIN_7    = ((uint8_t)0x80),  /*!< Pin 7 selected */
	  GPIO_PIN_LNIB = ((uint8_t)0x0F),  /*!< Low nibble pins selected */
	  GPIO_PIN_HNIB = ((uint8_t)0xF0),  /*!< High nibble pins selected */
	  GPIO_PIN_ALL  = ((uint8_t)0xFF)   /*!< All pins selected */
}GPIO_Pin_TypeDef;

Как видно, GPIO_PIN_5 это аналог записи (1<<5). Указатель GPIOB->ODR записан менее очевидным способом. Его лучше будет поискать на 32-ой странице официального руководства на чип stm8s103f3, где выложена табличка всех регистров вода-вывода(РВВ):

Т.о. выясняем, что загадочное число 0x5005; это адрес регистра ODR для порта B. Кстати, ODR это аббревиатура от Output Data Register.

Итак с первой строчкой разобрались:

	; ./l.c: 14: GPIOB->ODR ^= GPIO_PIN_5;
 	ldw x, #0x5005 ;Запись адреса PB_ODR регистра в регистр X
 	ld a, (x)
 	xor a, #0x20
 	ld (x), a

Осталось разобраться со скобочками.

STM8 имеет 18 (в документации к stm8s103f3 и stm8l051f3 написано, что 20) различных режимов адресации сгрупированных в 8 основных групп:

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

Использование такого способа может показаться сложнее чем аналогичный код на AVR, однако, это позволяет иметь больше чем 256 регистра I/O и использовать общую адресную шину, что в свою очередь сокращает набор команд микроконтроллера(80 инструкций в STM8 против от 90 до 133 в AVR).

Т.о. алгоритм со скобочками описывается следующим образом:

; ./l.c: 14: GPIOB->ODR ^= GPIO_PIN_5;
	 ldw x, #0x5005 ;Запись адреса PB_ODR регистра в индексный регистр X
	 ld a, (x) ;Копирование содержимого PB_ODR регистра в аккумулятор
	 xor a, #0x20 ; Инвертирование пятого бита
	 ld (x), a ;Копирование полученного значения обратно в регистр PB_ODR

Реализация функции delay выглядит более мудрено:

_delay:
	 sub sp, #2
; ./l.c: 6: while(t--);
	 ldw x, (0x05, sp)
00101$:
	 ldw (0x01, sp), x
	 decw x
	 ldw y, (0x01, sp)
	 jrne 00101$
	 addw sp, #2
	 ret

Здесь используется SP-индексная адресация. Параметр переданный функции через стек, не извлекается, а копируется "под дно" стека. Все опрерации производятся с этой его копией, которую затем помещают в X-регистр. На мой взгляд, инструкции ldw y, (0x01, sp) и ldw (0x01, sp), x здесь лишнии.

Функция delay становится еще более громоздкой, если тип переменной t в параметрах функции сменить с unsigned int на uint32_t:

_delay:
	 sub sp, #8
; ./l1.c: 6: while(t--);
	 ldw y, (0x0b, sp)
	 ldw (0x05, sp), y
	 ldw x, (0x0d, sp)
00101$:
	 ldw (0x03, sp), x
	 ldw y, (0x05, sp)
	 ldw (0x01, sp), y
	 subw x, #0x0001
	 ld a, (0x06, sp)
	 sbc a, #0x00
	 ld (0x06, sp), a
	 ld a, (0x05, sp)
	 sbc a, #0x00
	 ld (0x05, sp), a
	 ldw y, (0x03, sp)
	 jrne 00101$
	 ldw y, (0x01, sp)
	 jrne 00101$
	 addw sp, #8
	 ret
 

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

Хорошая иллюстрация работы SP-индексной адресации есть в PM0044:

Здесь показано, что в результате выполнения кода:
0086 4B11 PUSH #$11
0087 4B22 PUSH #$22
0088 4B33 PUSH #$33
0089 7B03 LD A,($03,SP)

в аккумуляторе окажется число 0x11.

Теперь предагаю посмотреть, как тот же код скомпилирует IAR:

Можно видеть, что для установки PB_DDR регистра используется всего одна операция BSET. Эта широко рекламируемя фирмой STM битовая операция, специально предназначена для для работы с GPIO. Также видно, что XOR для PB_ODR занимает 3 опереции вместо 4-х в SDCC.

А delay занимает всего пять инструкций и релизуется совершенно иным образом:

С помощью программы naken_util, я дизассемблировал hex файл выдаваемый IAR, получив такой листинг:

naken_util - by Michael Kohn
                Joe Davisson
    Web: http://www.mikekohn.net/
  Email: mike@mikekohn.net

Version: March 14, 2016

Loaded hexfile ./blink_s103.hex from 0x8000 to 0x9fff
Type help for a list of commands.

Addr    Opcode Instruction                              Cycles
------- ------ ----------------------------------       ------
0x8000:  82 00 80 d7    int $80d7                                cycles=2
0x8004:  82 00 80 fa    int $80fa                                cycles=2
0x8008:  82 00 80 fa    int $80fa                                cycles=2
0x800c:  82 00 80 fa    int $80fa                                cycles=2
0x8010:  82 00 80 fa    int $80fa                                cycles=2
0x8014:  82 00 80 fa    int $80fa                                cycles=2
0x8018:  82 00 80 fa    int $80fa                                cycles=2
0x801c:  82 00 80 fa    int $80fa                                cycles=2
0x8020:  82 00 80 fa    int $80fa                                cycles=2
0x8024:  82 00 80 fa    int $80fa                                cycles=2
0x8028:  82 00 80 fa    int $80fa                                cycles=2
0x802c:  82 00 80 fa    int $80fa                                cycles=2
0x8030:  82 00 80 fa    int $80fa                                cycles=2
0x8034:  82 00 80 fa    int $80fa                                cycles=2
0x8038:  82 00 80 fa    int $80fa                                cycles=2
0x803c:  82 00 80 fa    int $80fa                                cycles=2
0x8040:  82 00 80 fa    int $80fa                                cycles=2
0x8044:  82 00 80 fa    int $80fa                                cycles=2
0x8048:  82 00 80 fa    int $80fa                                cycles=2
0x804c:  82 00 80 fa    int $80fa                                cycles=2
0x8050:  82 00 80 fa    int $80fa                                cycles=2
0x8054:  82 00 80 fa    int $80fa                                cycles=2
0x8058:  82 00 80 fa    int $80fa                                cycles=2
0x805c:  82 00 80 fa    int $80fa                                cycles=2
0x8060:  82 00 80 fa    int $80fa                                cycles=2
0x8064:  82 00 80 fa    int $80fa                                cycles=2
0x8068:  82 00 80 fa    int $80fa                                cycles=2
0x806c:  82 00 80 fa    int $80fa                                cycles=2
0x8070:  82 00 80 fa    int $80fa                                cycles=2
0x8074:  82 00 80 fa    int $80fa                                cycles=2
0x8078:  82 00 80 fa    int $80fa                                cycles=2
0x807c:  82 00 80 fa    int $80fa                                cycles=2
0x8080:  88             push A                                   cycles=1
0x8081:  a6 08          ld A, #$08                               cycles=1
0x8083:  20 00          jra $8085  (offset=0)                    cycles=2
0x8085:  88             push A                                   cycles=1
0x8086:  7b 02          ld A, ($02,SP)                           cycles=1
0x8088:  88             push A                                   cycles=1
0x8089:  7b 02          ld A, ($02,SP)                           cycles=1
0x808b:  89             pushw X                                  cycles=2
0x808c:  1e 06          ldw X, ($06,SP)                          cycles=2
0x808e:  1f 04          ldw ($04,SP),X                           cycles=2
0x8090:  5f             clrw X                                   cycles=1
0x8091:  97             ld XL, A                                 cycles=1
0x8092:  fe             ldw X, (X)                               cycles=2
0x8093:  1f 06          ldw ($06,SP),X                           cycles=2
0x8095:  85             popw X                                   cycles=2
0x8096:  84             pop A                                    cycles=1
0x8097:  81             ret                                      cycles=4
0x8098:  90 ae 00 00    ldw Y, #$0                               cycles=2
0x809c:  20 0a          jra $80a8  (offset=10)                   cycles=2
0x809e:  93             ldw X, Y                                 cycles=1
0x809f:  1c 00 02       addw X, #$2                              cycles=2
0x80a2:  90 fe          ldw Y, (Y)                               cycles=2
0x80a4:  90 fd          call (Y)                                 cycles=4
0x80a6:  90 93          ldw Y, X                                 cycles=1
0x80a8:  90 a3 00 00    cpw Y, #$0                               cycles=2
0x80ac:  26 f0          jrne $809e  (offset=-16)                 cycles=1-2
0x80ae:  81             ret                                      cycles=4
0x80af:  72 1a 50 07    bset $5007, #5                           cycles=1
0x80b3:  ae ea 60       ldw X, #$ea60                            cycles=2
0x80b6:  cd 80 ea       call $80ea                               cycles=4
0x80b9:  a6 20          ld A, #$20                               cycles=1
0x80bb:  c8 50 05       xor A, $5005                             cycles=1
0x80be:  c7 50 05       ld $5005,A                               cycles=1
0x80c1:  20 f0          jra $80b3  (offset=-16)                  cycles=2
0x80c3:  cd 80 80       call $8080                               cycles=4
0x80c6:  52 02          sub SP, #$02                             cycles=1
0x80c8:  bf 08          ldw $08,X                                cycles=2
0x80ca:  be 08          ldw X, $08                               cycles=2
0x80cc:  1f 01          ldw ($01,SP),X                           cycles=2
0x80ce:  96             ldw X, SP                                cycles=1
0x80cf:  5c             incw X                                   cycles=1
0x80d0:  a6 01          ld A, #$01                               cycles=1
0x80d2:  cd 81 06       call $8106                               cycles=4
0x80d5:  20 f3          jra $80ca  (offset=-13)                  cycles=2
0x80d7:  ae 03 ff       ldw X, #$3ff                             cycles=2
0x80da:  94             ldw SP, X                                cycles=1
0x80db:  cd 80 fd       call $80fd                               cycles=4
0x80de:  5d             tnzw X                                   cycles=2
0x80df:  27 03          jreq $80e4  (offset=3)                   cycles=1-2
0x80e1:  cd 80 98       call $8098                               cycles=4
0x80e4:  cd 80 af       call $80af                               cycles=4
0x80e7:  cc 81 00       jp $8100                                 cycles=1
0x80ea:  90 93          ldw Y, X                                 cycles=1
0x80ec:  93             ldw X, Y                                 cycles=1
0x80ed:  1c ff ff       addw X, #$ffff                           cycles=2
0x80f0:  90 5d          tnzw Y                                   cycles=2
0x80f2:  26 f6          jrne $80ea  (offset=-10)                 cycles=1-2
0x80f4:  81             ret                                      cycles=4
0x80f5:  89             pushw X                                  cycles=2
0x80f6:  85             popw X                                   cycles=2
0x80f7:  cd 80 c3       call $80c3                               cycles=4
0x80fa:  cc 81 03       jp $8103                                 cycles=1
0x80fd:  5f             clrw X                                   cycles=1
0x80fe:  5c             incw X                                   cycles=1
0x80ff:  81             ret                                      cycles=4
0x8100:  cc 80 f5       jp $80f5                                 cycles=1
0x8103:  9d             nop                                      cycles=1
0x8104:  20 fd          jra $8103  (offset=-3)                   cycles=2
0x8106:  81             ret                                      cycles=4

Здесь я красным выделил функцию main(), а зеленым - delay();

поделиться: