[ASM][AVR] Inne spojrzenie

W tym miejscu zadajemy pytania na temat języka Assembler, dzielimy się swoją wiedzą, udzielamy wsparcia, rozwiązujemy problemy programistyczne.
Awatar użytkownika
gaweł
Geek
Geek
Posty: 1041
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » sobota 24 lip 2021, 21:24

Wstawki asemblerowe w programie w języku C

Nie zawsze jest uzasadnione, by powoływać do życia jakąś funkcję implementowaną w języku asembler by wykonać jakąś mało skompilowaną operację. Wręcz klasycznym przykładem jest pusta instrukcja (w asm jest to nop). Jest uzasadnione by taką wygenerować wprost w ściśle określonym miejscu w programie (w języku C). Tak prostą operację można zapisać jako:

__asm__ __volatile__ ("nop") ;
Wstawka zaczyna się od __asm__ (dwa znaki podkreślenia, wyraz asm i dwa znaki podkreślenia pisane bez odstępów), więcej o syntaktyce w dalszej części. Jednak okazuje się, że jeżeli instrukcja nie korzysta z informacji wejściowych, nie generuje wyników (a takim przypadkiem właśnie jest instrukcja pusta – nie wnosi w algorytmie programu żadnych skutków), to kompilator w ramach akcji optymalizacji kodu może takie fragmenty (tak zwane martwe kody) usunąć z programu. Nie po to programista (człowiek) trudził się by maszyna (kompilator) niszczył jego pracę i uważała, że „wie lepiej” (w końcu kilka instrukcji nop wykonanych w jednym ciągu oznacza ściśle określony czas ich realizacji i można tego chwytu używać do odmierzania bardzo krótkich odcinków czasu). By zapobiec takim sytuacjom, dodawane jest zaklęcie __volatile__ (dwa znaki podkreślenia, słowo volatile i dwa znaki podkreślenia).
Reasumując, powyższy zapis oznacza nieusuwalną wstawkę zawierającą instrukcję pustą. Bardzo podobnie sprawa wygląda przykładowo z instrukcją blokady lub zezwolenia na przyjmowanie przerwań.

__asm__ __volatile__ ("cli") ;
Patrząc na program z punktu widzenia algorytmu, to ta instrukcja (pozornie) nic nie wnosi w programie: nie zmieniły się żadne wartości zmiennych, nie wpłynęło to na stan portów. Pomimo tego istnienie tej instrukcji ma głęboki sens.
Ogólna postać instrukcji jest następująca (podaję z dodatkowym zaklęciem __volatile__ choć nie jest ono obowiązkowe):

__asm__ __volatile__ ( "kod w asm" : operandy wejściowe : operandy wyjściowe : używane rejestry ) ;
gdzie „kod w asm” jest ciągiem instrukcji rozdzielonym znakiem LF (znak nowej linii to \n) często łączy go się ze znakiem tabulacji (daje to „ładniejszy” wydruk z kompilacji, znak tabulacji to \t). Przykładowo ciąg kilku powyższych nop'ów można zapisać następująco:

__asm__ __volatile__ ( "nop \n\t nop \n\t nop \n\t nop” ) ;
Jednak taka postać zapisu jest mało czytelna, znacząco lepiej wygląda:

Kod: Zaznacz cały

__asm__ __volatile__ ( "nop \n\t"
                                       "nop \n\t"
                                       "nop \n\t "
                                       "nop" ) ;

Z ogólnej postaci zapisu instrukcji podanej powyżej, jeżeli któraś część nie występuje może zostać pominięta (co skutkuje przykładowo dwoma znakami ::). W sytuacji, gdy wszystkie części (te po dwukropku) nie występują, to mogą być pominięte w całości razem ze znakami „:”).
Jako ilustrację powyższych rozważań rozpatrzmy następujący przykład:

Kod: Zaznacz cały

#include <inttypes.h>
#include <avr/interrupt.h>
#include <avr/io.h>

#define nop() __asm__ __volatile__ ("nop")

uint8_t    GVar ;

int main ( void )
{
  uint8_t LVar ;
  /* ------------------- */
  LVar = 0xC3 ;   //135
  GVar = 0x3C ;   //120
  __asm__ __volatile__ ( "mov r0, %0"   "\n\t"
                                        "bst r0 , 7"   "\n\t"
                                        "lsl r0"       "\n\t"
                                        "bld r0 , 0"   "\n\t"
                                        "mov %0 , r0"  "\n\t"
                                        : "=r" (LVar)
                                        : "0" (LVar ) ) ;
  __asm__ __volatile__ ( "mov r0, %0"   "\n\t"
                                        "bst r0 , 7"   "\n\t"
                                        "lsl r0"       "\n\t"
                                        "bld r0 , 0"   "\n\t"
                                        "mov %0 , r0"  "\n\t"
                                        : "=r" (GVar)
                                        : "0" (GVar ) ) ;
  for ( ; ; )
  {
    nop();
  } /* for */ ;
} /* main */

Wiadomo, że rejestr R0, jest jest ogólnofilozoficznym rejestrem roboczym i nie należy specjalnie dbać o jego zawartość. W powyższym przykładzie do rejestru roboczego jest wpisana zmienna. Rejestr jest „obrócony cyklicznie” o jeden bit w lewo (w jaki sposób to działa to każdy czytelnik może sam nad tym się zastanowić: ciąg instrukcji bst, lsl i bld). Na końcu instrukcji __asm__ są zapisane następujące zaklęcia: "=r" (LVar): i "0" (LVar). Pierwsze z nich określa operandy wyjściowe (tu konkretnie zmienną lokalną LVar). Drugie z nich określa operandy wejściowe (również zmienna LVar). Wszędzie tam, gdzie występuje %0 zostanie wstawione odwołanie do zmiennej. Jednak warto się zapytać, w jaki sposób to działa, że działa. W zależności od stopnia optymalizacji działa poprawnie zawsze. W zależności od „klasy” zmiennej (zmienna lokalna czy zmienna globalna) działa poprawnie. Warto zadać sobie pytanie: dlaczego?
Otóż, cała wstawka posiada własny prolog i epilog. Prolog to pobranie do jakiegoś rejestru czegoś, co jest opisane jako operandy wejściowe (może być ich wiele). Epilog, to zapis wyniku do zmiennej (w tym wypadku LVar) wyniku operacji. Oczywiście to my musimy wiedzieć, co jest wynikiem.
rot01.png

LVar = C3 hex po rotacji to 87 hex = 135 dec.
rot02.png

Podobnie GVar = 3C hex po rotacji to 78 hex = 120 dec. Wyniki są poprawne.
W przypadku braku optymalizacji (opcje O0), to daje następujący kod:

Kod: Zaznacz cały

  __asm__ __volatile__ ( "mov r0, %0"   "\n\t"
  58:   89 81          ldd   r24, Y+1   ; 0x01
  5a:   08 2e          mov   r0, r24
  5c:   07 fa          bst   r0, 7
  5e:   00 0c          add   r0, r0
  60:   00 f8          bld   r0, 0
  62:   80 2d          mov   r24, r0
  64:   89 83          std   Y+1, r24   ; 0x01
                   "lsl r0"       "\n\t"
                   "bld r0 , 0"   "\n\t"
                   "mov %0 , r0"  "\n\t"
                   : "=r" (LVar)
                   : "0" (LVar ) ) ;

Zmienna lokalna jest lokowana na stosie i ma adres (Y+1). Prolog to: ldd r24,Y+1 oraz epilog to std Y+1,r24.

Kod: Zaznacz cały

  __asm__ __volatile__ ( "mov r0, %0"   "\n\t"
  66:   80 91 60 00    lds   r24, 0x0060
  6a:   08 2e          mov   r0, r24
  6c:   07 fa          bst   r0, 7
  6e:   00 0c          add   r0, r0
  70:   00 f8          bld   r0, 0
  72:   80 2d          mov   r24, r0
  74:   80 93 60 00    sts   0x0060, r24
                   "mov %0 , r0"  "\n\t"
                   : "=r" (GVar)
                   : "0" (GVar ) ) ;

Zmienna globalna na stały adres w pamięci RAM (60 hex). W tym przypadku prolog to lds r24,0x60 oraz epilog to sts 0x60,r24.
W przypadku włączenia optymalizacji, zmienia się sam prolog oraz epilog.

Kod: Zaznacz cały

  __asm__ __volatile__ ( "mov r0, %0"   "\n\t"
  46:   83 ec          ldi   r24, 0xC3   ; 195
  48:   08 2e          mov   r0, r24
  4a:   07 fa          bst   r0, 7
  4c:   00 0c          add   r0, r0
  4e:   00 f8          bld   r0, 0
  50:   80 2d          mov   r24, r0
                   "lsl r0"       "\n\t"
                   "bld r0 , 0"   "\n\t"
                   "mov %0 , r0"  "\n\t"
                   : "=r" (LVar)
                   : "0" (LVar ) ) ;

W tym konkretnym przypadku, to kompiler do tego stopnia wszystko wyoptymalizował, że gdyby nie zaklęcie __volatile__, to cały fragment zostałby usunięty.
Trochę inaczej rzecz ma się ze zmienną globalną. Tak sobie to wszystko wykombinował, że doszedł do wniosku, że zmienną globalną zaalokuje w rejestrze r25.

Kod: Zaznacz cały

  __asm__ __volatile__ ( "mov r0, %0"   "\n\t"
  52:   09 2e          mov   r0, r25
  54:   07 fa          bst   r0, 7
  56:   00 0c          add   r0, r0
  58:   00 f8          bld   r0, 0
  5a:   90 2d          mov   r25, r0
                   "mov %0 , r0"  "\n\t"
                   : "=r" (GVar)
                   : "0" (GVar ) ) ;

Jak się nad tym chwilę zastanowić, to się okazuje, że rozwiązanie nie jest bardzo optymalne. Wróćmy do stanu początkowego (brak optymalizacji):

Kod: Zaznacz cały

#include <inttypes.h>
#include <avr/interrupt.h>
#include <avr/io.h>

#define nop() __asm__ __volatile__ ("nop")

uint8_t    GVar ;

int main ( void )
{
  uint8_t LVar ;
  /* ------------------- */
  LVar = 0xC3 ;   //135
  GVar = 0x3C ;   //120
  __asm__ __volatile__ ( "mov r0, %0"   "\n\t"
                                        "bst r0 , 7"   "\n\t"
                                        "lsl r0"       "\n\t"
                                        "bld r0 , 0"   "\n\t"
                                        "mov %0 , r0"  "\n\t"
                                        : "=r" (LVar)
                                        : "0" (LVar ) ) ;
  __asm__ __volatile__ ( "mov r0, %0"   "\n\t"
                                        "bst r0 , 7"   "\n\t"
                                        "lsl r0"       "\n\t"
                                        "bld r0 , 0"   "\n\t"
                                        "mov %0 , r0"  "\n\t"
                                        : "=r" (GVar)
                                        : "0" (GVar ) ) ;
  for ( ; ; )
  {
    nop();
  } /* for */ ;
} /* main */

Skoro prolog to załadowanie operandu do rejestru %0, to dlaczego nie można wykorzystać go do końca (zamiast operować na r0)? Czasem warto popatrzeć na własne działania tak trochę z boku. Kolejna iteracja rozwiązania to:

Kod: Zaznacz cały

#include <inttypes.h>
#include <avr/interrupt.h>
#include <avr/io.h>

#define nop() __asm__ __volatile__ ("nop")

uint8_t    GVar ;

int main ( void )
{
  uint8_t LVar ;
  /* ------------------- */
  LVar = 0xC3 ;   //135
  GVar = 0x3C ;   //120
  __asm__ __volatile__ ( "bst %0 , 7"   "\n\t"
                                        "lsl %0"       "\n\t"
                                        "bld %0 , 0"   "\n\t"
                                        : "=r" (LVar)
                                        : "0" (LVar ) ) ;
  __asm__ __volatile__ ( "bst %0 , 7"   "\n\t"
                                        "lsl %0"       "\n\t"
                                        "bld %0 , 0"   "\n\t"
                                        : "=r" (GVar)
                                        : "0" (GVar ) ) ;
  for ( ; ; )
  {
    nop();
  } /* for */ ;
} /* main */

Wyszło o kilka instrukcji maszynowych mniej.

Kod: Zaznacz cały

  __asm__ __volatile__ ( "bst %0 , 7"   "\n\t"
  58:   89 81          ldd   r24, Y+1   ; 0x01
  5a:   87 fb          bst   r24, 7
  5c:   88 0f          add   r24, r24
  5e:   80 f9          bld   r24, 0
  60:   89 83          std   Y+1, r24   ; 0x01
                   "lsl %0"       "\n\t"
                   "bld %0 , 0"   "\n\t"
                   : "=r" (LVar)
                   : "0" (LVar ) ) ;
  __asm__ __volatile__ ( "bst %0 , 7"   "\n\t"
  62:   80 91 60 00    lds   r24, 0x0060
  66:   87 fb          bst   r24, 7
  68:   88 0f          add   r24, r24
  6a:   80 f9          bld   r24, 0
  6c:   80 93 60 00    sts   0x0060, r24
                   "bld %0 , 0"   "\n\t"
                   : "=r" (GVar)
                   : "0" (GVar ) ) ;

I to ma sens.
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse

Awatar użytkownika
gaweł
Geek
Geek
Posty: 1041
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » niedziela 08 sie 2021, 12:50

Wstawki asemblerowe w programie w języku C

Poprzednio przedstawione zostało posługiwanie się we wstawkach asmowych zmiennymi 8-bitowymi. Niestety potrzeby często wymagają bardziej zaawansowanego działania. Takim przykładem jest operowanie zmiennymi zajmującymi więcej niż 8-bitów. W przypadku danych 8-bitowych dla procków standardowo przetwarzających dane 8-bitowe nie ma żadnych problemów. Schody zaczynają się, gdy w procku 8-bitowym zachodzi potrzeba przetwarzania danych wykraczających swoją objętością poza 8 bitów. Rodzi się naturalne pytanie: w jaki sposób identyfikować poszczególne części większej całości?
Wstawka asmowa wygeneruje swoisty prolog dla realizowanych instrukcji (jak również i epilog). Do pełni szczęścia konieczna jest znajomość rejestrów, do których zostały załadowane dane. W końcu by zrealizować zamierzone działania konieczna jest znajomość, gdzie co się gdzie znajduje. Tu z pomocą przychodzi „syntaktyka” zapisów. Operand, przykładowo %0, może być poprzedzony jednoliterowym „prefiksem” identyfikującym określoną część. I tak litera „A” określa najmłodszą część operandu. Kolejna starsza (w przypadku danych 16-bitowych to będzie najstarsza część) jest identyfikowana przez „B”. Łatwo jest domyślić się, co będzie dalej...
Jako ilustrację powyższych rozważań rozpatrzmy następujący przykład (podobnie jak w poprzednim przykładzie realizowana jest operacja obrotu cyklicznego danych, tylko w tym przypadku 16-bitowych):

Kod: Zaznacz cały

#include <inttypes.h>
#include <avr/interrupt.h>
#include <avr/io.h>

#define nop() __asm__ __volatile__ ("nop")

uint16_t    GVar ;

int main ( void )
{
  uint16_t LVar ;
  /* ------------------- */
  LVar = 0x300C ;
  GVar = 0xC003 ;  // 32775
  __asm__ __volatile__ ( "bst %B0 , 7"   "\n\t"
                         "add %A0 , %A0"  "\n\t"
                         "adc %B0 , %B0"  "\n\t"
                         "bld %A0 , 0"   "\n\t"
                         : "=r" (GVar)
                         : "0" (GVar)) ;
  __asm__ __volatile__ ( "bst %B0 , 7"   "\n\t"
                         "add %A0 , %A0"  "\n\t"
                         "adc %B0 , %B0"  "\n\t"
                         "bld %A0 , 0"   "\n\t"
                         : "=r" (LVar)
                         : "0" (LVar)) ;
  for ( ; ; )
  {
    nop();
  } /* for */ ;
} /* main */

Kompilacja i uruchomienie:
scr01.png

Tuż po realizacji podstawienia do zmiennej Gvar, zawiera ona C003 hex=49155 dec. Po wykonaniu instrukcji zawartych we wstawce asmowej:
scr02.png

Zmienna ta powinna zawierać C003 hex po cyklicznej rotacji = 8007 hex = 32775 dec, co potwierdza symulator działania procka.
Znaczy się, działa.
Co z tego zrobił kompiler? Z analizy odpowiedniego dokumentu można wysnuć właściwe wnioski: %A0 to rejestr r25 oraz %B0 to rejestr r24. Biorąc pod uwagę „wielkość indianina”, to w AVR-ch starsza część jest na starszym adresie, czyli %A0 to część młodsza (pobrana z pamięci RAM z młodszego adresu) oraz %B0 to część starsza (analogicznie w RAM znajduje się na starszym adresie).
scr03.png
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse


Wróć do „Pisanie programów w Assembler”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 1 gość