[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: 1259
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

[ASM][AVR] Inne spojrzenie

Postautor: gaweł » wtorek 09 maja 2017, 16:34

Programowanie w assemblerze jest postrzegane jako proste lub wręcz prymitywne. Chciałbym przekonać, że wcale tak nie jest. Poniższe rozważania są efektem przemyśleń sprzed wielu lat i pozwolą "odczarować" języki wysokiego poziomu.

Koncepcja wywołania funkcji z parametrami przenoszonymi przez stos

Zapewne wiele osób zastanawiało się, w jaki sposób radzą sobie kompilatory języków wysokiego poziomu (przykładowo języka C) realizując wywołanie funkcji. Przenoszenie wartości parametrów wywołania funkcji przez rejestry mikrokontrolera jest najprostszą metodą. Ma ona jednak poważne wady, których eliminacja czasami nastręcza trochę problemów. Dodatkowym problemem jest realizacja algorytmów rekurencyjnych (takich, gdzie dochodzi do wywołania funkcji, w której już znajduje się wykonanie programu). Można oczywiście przed rekurencyjnym wywołaniem funkcji przechować zawartość istotnych rejestrów w obszarze zmiennych. Zaproponowana i opisana poniżej metoda stanowi prostą i uniwersalną metodę przekazywania parametrów. Jej podstawowe cechy użytkowe odpowiadają możliwościom jakie oferują języki wysokiego poziomu (nie znaczy to, że popularny kompilator GCC w ten sposób realizuje wywołanie funkcji). Do najważniejszych należy zaliczyć:
  • brak konieczności jakiejkolwiek ochrony rejestrów zawierających parametry wywołania funkcji (gdyż te znajdują się w pamięci RAM),
  • możliwość realizacji algorytmów rekurencyjnych,
  • praktycznie nie istnieje ograniczenie liczby parametrów (limitem jest wielkość pamięci RAM mikrokontrolera).
Ulepszona metoda przekazywania parametrów wywołania procedur i funkcji opiera się o mechanizmy oferowane przez stos. Przed wywołaniem procedury lub funkcji na stos odkładane są wartości parametrów wywołania. Pozwala to zgromadzić w pamięci w formie jednolitego bloku obszar zawierający wszystkie dane wnoszone do funkcji w momencie jej wywołania. Rozpatrzmy następujący ciąg instrukcji (wywołanie funkcji z jednym parametrem):

Kod: Zaznacz cały

                 ; x := Fun ( 3 ) ;
    ldi   acc,3  ; umieszczenie na szczycie stosu liczby 3 jako
    push  acc    ; parametru wywołania funkcji
    rcall Fun    ; wywołanie funkcji
    pop   acc2   ; usunięcie ze stosu parametru wywołania
    sts   x,acc  ; zakładając, że wynik funkcji jest
                 ; pozostawiony w rejestrze acc ....


W wyniku ich wykonania, na stosie zostaje zachowana zawartość rejestru, w którym zapisany jest parametr wywołania funkcji (w tym przypadku jest to stała o wartości 3). Stan stosu przed jej wywołaniem przedstawia rysunek 1.
avrpa1_01.png
Wywołanie funkcji (instrukcja rcall Fun) spowoduje zapisanie na stosie dwubajtowej informacji będącej adresem powrotu z jej wywołania. W tym momencie stan stosu przedstawia rysunek 2. Analizując ten rysunek można zauważyć, że bezpośrednio za adresem powrotu z wywołanej funkcji Fun, w pamięci RAM mikrokontrolera znajduje się komórka, która przechowuje parametr jej wywołania. Położenie (adres) w pamięci tej komórki może być określone poprzez zawartość rejestru wskaźnika stosu SP. Tuż po wykonaniu instrukcji rcall Fun, rejestr ten wskazuje na pierwsze wolne miejsce w obszarze stosu. Traktując zawartość rejestru SP jako adres
avrpa1_02.png
w pamięci RAM, można określić położenie poszczególnych elementów zachowanych na stosie. Pamiętając, że każda operacja zapisu danych na stos oznacza dekrementację zawartości rejestru SP (stos rośnie w dół), to wskazanie SP+1 oraz SP+2 określa położenie w pamięci informacji będącej adresem powrotu z procedury Fun. Analogicznie, zawartość rejestru SP powiększona o 3, jest adresem w pamięci danych komórki przechowującej parametr wywołania. Ponieważ mikrokontrolery AVR nie mają możliwości użycia wskaźnika stosu jako rejestru adresowego w operacjach wymiany danych pomiędzy rejestrami roboczymi i pamięcią, konieczne jest przepisanie zawartości rejestru SP do odpowiedniej pary rejestrów pozwalających na pośrednie adresowanie operandów w instrukcjach przesyłania danych. Do tej roli doskonale nadaje się rejestr Y (yh:yl), gdyż oprócz możliwości adresowania pośredniego operandów (przykładowo instrukcja ld oraz st) możliwe jest użycie go do adresowania z przesunięciem (instrukcje ldd i std). Po wykonaniu następujących instrukcji:

Kod: Zaznacz cały

    in   yh,sph  ;
    in   yl,spl  ;
para rejestrów yh:yl może być użyta do adresowania komórek pamięci RAM położonych w obszarze stosu. Realizacja instrukcji:

Kod: Zaznacz cały

    ldd  acc,y+3 ;
spowoduje przepisanie do rejestru acc wartości parametru wywołania funkcji (rejestr acc będzie zawierał liczbę 3). Warto tu zauważyć, że nie należy specjalnie chronić rejestru zawierającego wartość parametru (zawsze można sięgnąć po jego wartość ponownie).
Po wykonaniu wszystkich operacji przewidzianych w wywołanej funkcji, wykonanie programu po napotkaniu instrukcji ret, powraca do miejsca wywołania. W tym momencie stan stosu powraca do postaci, jaka była przed wywołaniem funkcji (rysunek 1) i należy usunąć ze stosu zbędne już informacje będące parametrami wywołania. W tym przypadku wystarczy wykonać instrukcję pop a zawartość odtworzonego rejestru potraktować jako nieistotną.
Powyższa koncepcja przekazywania parametrów wywołania do procedur oraz funkcji może być rozszerzona na większą ich liczbę. Ilustruje to następujący przykład:

Kod: Zaznacz cały

    ldi   acc,param1  ;
    push  acc         ;
    ldi   acc,param2  ;
    push  acc         ;
    lds   acc,param3  ;
    push  acc         ;
    lds   acc,param4  ;
    push  acc         ;
    rcall Fun         ;
gdzie param1 i param2 są stałymi oraz param3 i param4 są zmiennymi w pamięci RAM tworząc łącznie parametry wywołania funkcji.
Po wykonaniu powyższych instrukcji, stan stosu przedstawia rysunek 3. Wykonując analogiczne operacje dotyczące przepisania zawartości rejestru SP do rejestru adresowego Y, możliwy jest dostęp do poszczególnych parametrów wywołania za pośrednictwem rejestru adresowego Y z odpowiednim, stałym dla każdego parametru przesunięciem.
avrpa1_03.png

Kod: Zaznacz cały

   ldi  acc3,<wielkość obszaru> ;
   in   acc2,sreg               ;
                 ; Zapamiętanie w rejestrze acc2 aktualnego stanu rejestru wskaźnikowego,
   cli                          ;
   in   acc,spl                 ;
                 ; przepisanie zawartości młodszej części rejestru wskaźnika stosu do wybranego rejestru roboczego,
   add  acc,acc3                ;
   out  spl,acc                 ;
                 ; arytmetyczne dodanie liczby będącej wielkością wyrażoną w bajtach zajętego przez
                 ; parametry obszaru. Wynik zostaje zapamiętany w młodszej części rejestru wskaźnika stosu.
   in   acc,sph                 ;
   ldi  acc3,0                  ;
   adc  acc,acc3                ;
   out  sph,acc                 ;
               ; Mogące wystąpić z dodawania dla młodszej części przeniesienie musi być
               ; uwzględnione w starszej części rejestru wskaźnika stosu (tak jak w operacjach
               ; dodawania 16-bitowego).
   out  sreg,acc2               ;
Istotnym wymogiem dla realizowanych operacji jest ich niepodzielność. Od wykonania instrukcji:

Kod: Zaznacz cały

    in    acc,spl

do wykonania instrukcji:

Kod: Zaznacz cały

   out   sph,acc
nie może wystąpić użycie stosu, ponieważ zawartość rejestru SP jako rejestru adresowego 16-bitowego nie jest poprawna. Brak jawnego użycia instrukcji operujących na stosie (jak przykładowo push, pop, rcall, ret) nie oznacza, że nie może dojść do operacji dotyczących stosu. Przykładem tego może być przejście do obsługi zgłoszonego przerwania, które w swej naturze jest operacją asynchroniczną i może nastąpić w dowolnym momencie. Zmusza to do wcześniejszej blokady obsługi jakichkolwiek przerwań (instrukcja cli). W ogólnym przypadku nie jest możliwe określenie, czy w danej chwili mikrokontroler pracuje z blokadą obsługi przerwań czy z możliwością ich obsługi (w trakcie wykonania programu możliwe jest testowanie wskaźnika I, ale rozważania dotyczą fazy tworzenia programu źródłowego i w ogólnym przypadku nie zawsze jest to możliwe). Z tego powodu, po wykonaniu operacji zmiany zawartości rejestru wskaźnika stosu, następuje powrót stanu wskaźnika I do właściwego ustawienia. Mechanizm działania jest następujący:

Kod: Zaznacz cały

    in   acc2,sreg   ;
    cli              ;
(...)
    out  sreg,acc2   ;
Wykonanie instrukcji:

Kod: Zaznacz cały

    in   acc2,sreg
powoduje zapamiętanie w wybranym rejestrze stanu wszystkich wskaźników z rejestru statusowego (gdzie między innymi znajduje się wskaźnik zezwolenia na obsługę przerwań). Po wykonaniu instrukcji cli, mikrokontroler nie będzie reagował na sygnały przerwań (chodzi o reakcję jako wywołanie procedury obsługi przerwania, z elektronicznego punktu widzenia sygnały przerwań modyfikują stany pewnych bitów w odpowiednich rejestrach, więc mikrokontroler zawsze w jakiś sposób reaguje na sygnały przerwań). Wykonanie instrukcji:

Kod: Zaznacz cały

   out  sreg,acc2
przywraca w rejestrze wskaźników ich stan jaki występował przed wykonaniem instrukcji blokującej obsługę przerwań. Jeżeli (przed wykonaniem instrukcji cli) była zablokowana możliwość ich obsługi, to wykonanie instrukcji out nie da żadnego efektu (chodzi o wskaźnik I, natomiast inne wskaźniki mogą ulec zmianie). W przeciwnym wypadku należy ją uważać za ekwiwalent instrukcji sei.
W sytuacji użycia mikrokontrolerów AVR, które posiadają 8-bitowy rejestr wskaźnika stosu, powyższa operacja dodawania z użyciem rejestru SP ulega znaczącemu uproszczeniu. Ponieważ sam rejestr wskaźnika stosu redukuje się tylko do części młodszej oraz zapis do rejestru jest operacją niepodzielną, nie jest konieczna realizacja tych działań w kontekście zablokowanej możliwości obsługi przerwań. Pociąga to za sobą w algorytmie kolejną redukcję instrukcji związanej z obsługą wskaźnika I. Odpowiedni ciąg instrukcji może być następujący:

Kod: Zaznacz cały

    ldi  acc3,<wielkość obszaru> ;
    in   acc,spl                 ;
    add  acc,acc3                ;
    out  spl,acc                 ;
W praktycznej realizacji przedstawionej wyżej koncepcji przekazywania parametrów warto wnieść kilka modyfikacji, z których najistotniejszą jest zachowanie dotychczasowej zawartości rejestru Y (przyjmując, że do celów adresowania w pamięci komórek zawierających wartości parametrów wywołania jest używany rejestr yh:yl). Często występującym w oprogramowaniu rozwiązaniem jest wywołanie z funkcji kolejnej innej funkcji. Jeżeli w każdej implementacji użyty zostaje przedstawiony mechanizm przekazywania parametrów, to w każdym przypadku zawartość rejestru adresowego yh:yl będzie inna. Implikuje to konieczność zachowania stanu rejestru adresowego Y na początku wywołanej funkcji (przed wpisaniem do niego nowego wskazania obowiązującego w bieżącej funkcji) oraz odtworzeniu przed instrukcją powrotu z funkcji. Druga proponowana zmiana wynika z optymalizacji zajętości kodu programu. Zamiast powielać w każdej funkcji identyczny ciąg instrukcji mający za zadanie zachowanie dotychczasowej zawartości rejestru Y oraz wpisanie do niego nowej wartości wynikającej z aktualnego stanu wskaźnika stosu, można wywołać odpowiednią procedurę, której zadaniem będzie wykonanie niezbędnych czynności związanych z rejestrem adresowym Y. Dodatkowo można zrealizować usunięcie parametrów ze stosu wewnątrz wywołanej funkcji (rozwiązanie to narzuca wprawdzie pewne ograniczenia, o których będzie później). Prowadzi to do powstania schematycznego rozwiązania budowy funkcji i procedury, w której parametry są przekazywane za pośrednictwem stosu. Ilustruje to następujący przykład:

Kod: Zaznacz cały

Fun   :                  ;FUNCTION Fun
;********               ;
.equ   Fun_Param   = 0    ; ( Param : BYTE ) : BYTE[xl] ;
        ; Stała pozwalająca na adresowanie komórki zawierającej
        ; parametr wywołania. Wykonanie przykładowo instrukcji
        ; ldd acc,y+Fun_Param
        ; pozwala pobrać do rejestru acc parametr wywołania
        ; (szczegółowe wyjaśnienia są w dalszej części).
.equ   FunParamSize = 1   ;
        ; Stała określająca wielkość obszaru na stosie zajętego
        ; przez parametry wywołania.
  rcall  _OpenCallParam ;BEGIN (* Fun *)
        ; Na początku implementacji funkcji/procedury należy
        ; wywołać bezparametrową procedurę, której zadaniem jest
        ; zachowanie dotychczasowej zawartości rejestru Y raz
        ; określenie nowego jego wskazania.
   .
   .
   .
   ldi  xl,1              ; Fun := 1 ;
   ldi  param,FunParamSize;
   rcall _CloseCallParam  ;
         ; Przed zakończeniem działania funkcji/procedury należy
         ; wywołać inną procedurę, której zadaniem jest wykonanie
         ; operacji odwrotnych w stosunku do wcześniej wywołanej
         ; procedury (_OpenCallParam). Do procedury jest
         ; dostarczona informacja określająca wielkość obszaru na
         ; stosie przeznaczonego na parametry (poprzez rejestr
         ; param), co jednocześnie pozwala wykonać czynności
        ; związane z usunięciem ich ze stosu.
   Ret                    ;END (* Fun *) ;
;-------------------------------------------------------------
(...)
                       ; ... := Fun ( 5 ) ;
    ldi   acc,5        ; przygotowanie parametru wywołania
    push  acc          ; utworzenie na stosie obszaru
                       ; zawierającego wartość parametru
                       ; wywołania
    rcall Fun          ; wywołanie funkcji
    …                  ; parametr wywołania funkcji jest już
                       ; usunięty ze stosu (patrz: implementacja
                       ; procedury _CloseCallParam).
;-------------------------------------------------------------
Powyższy przykład dotyczy wywołania funkcji z jednym jednobajtowym parametrem. W ogólnym przypadku liczna parametrów może być większa (oraz każdy z parametrów może zajmować więcej niż jeden bajt). Uogólniając powyższy przykład na większą liczę parametrów, po wywołaniu (tuż po wykonaniu instrukcji rcall _OpenCallParam wewnątrz funkcji Fun) stan stosu przedstawia rysunek 4. Zawiera on, poczynając od szczytu stosu, następujące informacje:
avrpa1_04.png

  • adres powrotu z procedury _OpenCallParam (jako efekt wykonania instrukcji rcall _OpenCallParam z funkcji Fun),
  • adres powrotu z funkcji Fun (jako efekt wykonania instrukcji rcall Fun),
  • ciąg parametrów wywołania funkcji Fun (jako efekt działania instrukcji push wykonanych przed wywołaniem funkcji; w zaprezentowanym wyżej przykładzie występuje jeden parametr).
avrpa1_05.png

Kod: Zaznacz cały

_OpenCallParam :       ;

;***************       ;
                  ;   Stan stosu przedstawia rysunek 4.
    pop   acc2         ;
    pop   acc3         ;
                  ; Instrukcje pop nakazują pobranie ze szczytu stosu ostatnio zapisanych tam informacji. W rezultacie
                  ; adres powrotu z bieżącej procedury znajduje się w dwóch rejestrach acc2 i acc3. Stan stosu
                  ; przedstawia rysunek 5 w fazie A.
    push   yh           ;
    push   yl           ;
                  ; W kolejnym kroku, na szczycie stosu umieszczona zostaje zawartość rejestru yh:yl.
    in   yh,sph        ;
    in   yl,spl          ;
                  ; Aktualna zawartość 16-bitowego wskaźnika stosu SP zostaje przepisana do rejestru Y (yh:yl).
    Adiw yl,5           ;
                  ; Po zwiększeniu zawartości rejestru yh:yl o 5 wskazuje on w pamięci danych na miejsce,
                  ; gdzie znajduje się blok zawierający parametry wywołania. Stan stosu przedstawia
                  ; rysunek 5 w fazie B. Adres Y+0 pozwala zaadresować komórkę będącą parametrem wywołania
                  ; funkcji (patrz: wartość stałej Fun_Param w powyższym przykładzie).
    push    acc3        ;
    push    acc2        ;
                  ; Odpowiednie adresy powrotu z wywołania, chwilowo przechowywane w rejestrach, zostają
                  ; umieszczone na stosie. Efektem działania procedury jest zmodyfikowany stos oraz nowa
                  ; zawartość rejestru przeznaczonego do adresowania parametrów wywołania. Stan stosu
                  ; przedstawia rysunek 5 w fazie C.
    ret                 ;
                  ; Polecenie ret pobiera ze szczytu stosu adres powrotu z bieżącej procedury i wykonanie
                  ; instrukcji programu przenosi się do odpowiedniego miejsca w implementacji funkcji Fun.
                  ; Stan stosu (po powrocie z procedury _OpenCallParam) przedstawia rysunek 6.
avrpa1_06.png
Zakończenie działania wywołanej funkcji lub procedury (w przykładzie funkcji Fun) również wymaga wykonania pewnych czynności dotyczących stosu, które sprowadzają się do odtworzenia właściwego wskazania rejestru adresowego Y oraz usunięcia ze stosu obszaru zajmowanego przez parametry wywołania. Sprowadza się to do wywołanie odpowiedniej procedury (_CloseCallParam). Implementacja tej procedury może być następująca:

Kod: Zaznacz cały

_CloseCallParam :             ;
                  ; Procedura wymaga przed jej wywołaniem określenia w rejestrze param
                  ; wielkości obszaru zajmowanego przez blok parametrów (wyrażonej w
                  ; bajtach). W przykładzie jest użyte:
                  ;    ldi  param,FunParamSize
                  ;    rcall _CloseCallParam
                  ; Stan stosu przedstawiony jest na rysunku 7 w fazie A.
    pop    acc3               ;
    pop    acc2               ;
                  ; Aktualnie znajdujący się na szczycie stosu adres powrotu z wywołania
                  ; procedury zostaje przepisany do rejestrów. Po tych operacjach stan
                  ; stosu pokazany jest na rysunku 7 w fazie B.
    pop    yl                 ;
    pop    yh                    ;
    pop    zl                    ;
    pop    zh                    ;
                 ; Kolejne instrukcje pozwalają odtworzyć wcześniej zapamiętaną
                 ; zawartość rejestru adresowego Y (operacja ta była wykonana w
                 ; procedurze _OpenCallParam). Znajdujący się na szczycie stosu adres
                 ; powrotu z wywołanej funkcji (Fun) również jest przepisany do
                 ; rejestrów roboczych mikrokontrolera. Po tych operacjach stos ma
                 ; postać pokazaną na rysunku 7 w fazie C.
    in    acc4,sreg           ;
    cli                       ;
    in    acc,spl             ;
    add   acc,param           ;
    out   spl,acc             ;
    in    acc,sph             ;
    ldi   param,0             ;
    adc   acc,param           ;
    out   sph,acc             ;
    out   sreg,acc4            ;
                 ; W analogiczny sposób jak było to już opisane, usuwany jest ze stosu
                 ; obszar zawierający parametry wywołania. Aktualny stan stosu pokazuje
                 ; rysunek 7 w fazie D.
    push   zh                    ;
    push   zl                    ;
    push   acc2               ;
    push   acc3               ;
                 ; Po umieszczeniu na stosie adresów powrotów z wywołanych funkcji i
                 ; procedur (przechowywanych chwilowo w rejestrach roboczych) stan
                 ; stosu pokazany jest na rysunku 7 w fazie E.
    Ret                       ;
                 ; Wykonanie instrukcji ret w naturalny sposób przenosi wykonanie
                 ; programu do miejsca wywołania.
avrpa1_07.png
Łatwo zauważyć, że każde wywołanie procedury lub funkcji z parametrami przenoszonymi poprzez stos tworzy własny, lokalny obszar w pamięci RAM przeznaczony na ich przechowywanie. W takim przypadku, bez żadnych dodatkowych zabiegów, powyższy algorytm powinien być skuteczny nawet w rozwiązaniach stosujących wywołania rekurencyjne. Przykładem ilustrującym użycie opisanego wyżej sposobu przekazywania parametrów wywołania w przypadku jawnej rekurencji może być poniższy program [EPROG1A.ASM]. Zadaniem tego programu jest obliczenie silni dla liczby N (będącej parametrem wywołania funkcji). Realizacja funkcji jest rekurencyjna i opiera się o następujący algorytm:
jeżeli N=1, to wynikiem N! jest 1,
jeżeli N>1 należy wykonać działanie N * (N-1)!.
Tekst programu jest następujący (sam program prezentuje jedynie istotne z punktu widzenia przekazywania parametrów elementy i nie zawiera żadnych dodatkowych operacji, jak przykładowo prezentacja wyniku na wyświetlaczu):

Kod: Zaznacz cały

(...)
        .nolist
;*******************************************************
.include "m8515def.inc"
;*******************************************************
(...)
.def acc                = r16
.def acc2               = r17
.def acc3               = r18
.def acc4               = r19
.def param              = r20
(...)
;-------------------------------------------------------
                .dseg
;                                      ;VAR
Res :   .byte   2                      ; Res : WORD ;
;-------------------------------------------------------
        .cseg
(...)
;-------------------------------------------------------
        .org    0                       ;
        rjmp    ResetProcessor          ;
;-------------------------------------------------------
(...)
;-------------------------------------------------------
_OpenCallParam :                        ;
(...)
;-------------------------------------------------------
_CloseCallParam :                       ;
(...)
;-------------------------------------------------------
Silnia  :                               ;FUNCTION Silnia
;********                               ;
.equ    Silnia_N        = 0             ;  ( N : BYTE ) : WORD[x] ;
.equ    SilniaParamSize = 1             ;
                     ; Nagłówek funkcji rekurencyjnego obliczenia silni, która posiada
                     ; jeden parametr wywołania będący jednobajtową liczbą. Parametr ten
                     ; jest dostępny w pamięci RAM pod adresem Y+Silnia_N (Y+0), zgodnie
                     ; z rysunkiem 6. Nie znaleziono źródła odwołania. Łączna wielkość
                     ; bloku zajmowanego przez parametry wywołania wynosi 1 bajt (stała
                     ; SilniaParamSize).
        rcall   _OpenCallParam          ;BEGIN (* Silnia *)
                     ; Wywołanie niezbędnej procedury, której zadaniem jest odpowiednia
                     ; modyfikacja stosu i określenie w parze rejestrów yh:yl wskazania w
                     ; pamięci RAM na obszar parametrów wywołania.
        ldd     acc,y+Silnia_N          ; IF N <> 1 THEN
        cpi     acc,1                   ;
                     ; Zbadanie, czy aktualny parametr wywołania nie jest liczbą o
                     ; wartości 1.
        breq    Sil_1                   ; BEGIN
                     ; W przypadku prawdziwości badanego warunku,
        dec     acc                     ;  Silnia := Silnia ( N - 1 ) * N ;
        push    acc                     ;
        rcall   Silnia                  ;
                     ; przygotowany jest parametr do kolejnego rekurencyjnego wywołania
                     ; funkcji.
        mov     Argument1LL,xl          ;
        mov     Argument1LH,xh          ;
                     ; Zawarta w rejestrach xh:xl liczba, jako wynik wywołanej funkcji
                     ; Silnia(N-1), jest przepisana do odpowiednich rejestrów jako
                     ; pierwszy argument w operacji mnożenia.
        ldd     Argument2LL,y+Silnia_N  ;
        ldi     acc,0                   ;
        mov     Argument2LH,acc         ;
                     ; Drugim argumentem w mnożeniu jest liczba będąca aktualnym
                     ; parametrem wywołania (ponieważ parametr N wywołania funkcji jest
                     ; jednobajtowy, starsza część odpowiedniego zespołu rejestrów jest
                     ; wyzerowana).
        rcall   MultArg1AndArg2         ;
        mov     xl,ResultLL             ;
        mov     xh,ResultLH             ;
                     ; Po wykonaniu mnożenia, jego wynik zostaje przepisany do rejestru
                     ; wynikowego funkcji (xh:xl).
        rjmp    Sil_0                   ;  RETURN ;
                     ; Zakończenie działania funkcji wymaga wykonania odpowiednich
                     ; czynności związanych z modyfikacją stosu, więc następuje skok do
                     ; odpowiedniego miejsca.
Sil_1   :                               ; END (* IF *) ;
        ldi     xl,1                    ; Silnia := 1 ;
        ldi     xh,0                    ;
                     ; W przeciwnym wypadku (IF N <> 1 THEN), jako wynik funkcji zwrócona
                     ; jest liczba 1.
Sil_0   :                               ;END (* Silnia *) ;
        ldi     param,SilniaParamSize   ;
        rcall   _CloseCallParam         ;
        ret                             ;
                     ; Właściwe zakończenie działania funkcji sprowadza się do wywołania
                     ; odpowiedniej procedury.
;-------------------------------------------------------
ResetProcessor :                        ;
        ldi     acc,HIGH(RAMEND)        ;
        out     SPH,acc                 ;
        ldi     acc,LOW(RAMEND)         ;
        out     SPL,acc                 ;
Main :                                  ;BEGIN (* Main *)
        rcall   ClearRAM                ; ClearRAM ( ) ;
        rcall   ClearRegs               ; ClearRegs ( ) ;
        rcall   HardwareInit            ; HardwareInit ( ) ;
        rcall   EnvirInit               ; EnvirInit ( ) ;
        rcall   SoftwareInit            ; SoftwareInit ( ) ;
        sei                             ; EI ( ) ;
Main_0  :                               ; LOOP
        ldi     acc,3                   ;  Res := Silnia ( 3 ) ;
        push    acc                     ;
        rcall   Silnia                  ;
                         ; Wywołanie funkcji, której zadaniem jest obliczenie silni dla
                         ; liczby 3.
        ldz     Res                     ;
        st      z+,xl                   ;
        st      z,xh                    ;
                         ; Wynik jest zachowany w pamięci.
        rjmp    Main_0                  ; END (* LOOP *) ;
                                        ;END (* Main *) ;
;-------------------------------------------------------
.exit

Zaprezentowane rozwiązanie posiada jedno ograniczenie, mianowicie lista parametrów wywołania funkcji musi być stała, nie można zrealizować wywołania funkcji z różną liczbą parametrów (raz zdefiniowanej funkcji Fun mającej przykładowo jeden parametr wywołania nie można raz wywołać z jednym parametrem a w innym miejscu wywołać z dwoma parametrami). Wynika to z tego, że wewnątrz funkcji w trakcie usuwania ze stosu parametrów wywołania funkcja usuwa obszar wynikający z liczby spodziewanych parametrów wywołania. Powyższy sposób można nazwać typem PASCAL-CALL. Definicja języka nie dopuszcza tworzenia funkcji i procedur ze zmienną liczbą parametrów wywołania. Niektóre języki (jak przykładowo C) oferują mechanizmy pozwalające na wywoływanie funkcji z różną liczbą parametrów. Rozwiązanie w sumie jest proste, opiera się o podobny do powyższego mechanizm i sprowadza się do następujących różnic w realizacji:
parametry są umieszczone na stosie w odwrotnej kolejności (ostatnim położonym na stosie parametrem jest pierwszy z listy wywołania),
parametry są zdejmowane ze stosu w miejscu wywołania funkcji (skoro wywołując funkcję z parametrami wiadomo ile ich jest umieszczonych na stosie, to również wiadomo ile ich zdjąć ze stosu).
Odwrotna kolejność parametrów na stosie wynika z braku jednoznacznego określenia położenia pierwszego parametru (skoro nie jest znana liczba parametrów). Oddzielną kwestią już jest dowiedzenie się wewnątrz funkcji z iloma parametrami została wywołana funkcja. Taki sposób można nazwać typem C-CALL. Klasycznym wręcz przykładem jest wywołanie funkcji printf w języku C. Przykładowo poprawne są:
  • printf ( "Jakiś napis" ) ; – wywołanie z jednym parametrem,
  • printf ( "Liczba A=%d" , A ) ; – wywołanie z dwoma parametrem,
  • printf ( "Liczba A=%d liczba B=%d liczba C=%d” , A , B , C ) ; – wywołanie z czterema parametrami.
W jaki sposób wykonanie funkcji radzi sobie z realizacją, to temat na całkiem oddzielne rozważania.


Załącznik:
avrasm_p1.zip
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: 1259
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » czwartek 11 maja 2017, 11:20

Zmienne lokalne funkcji

Na podobnej zasadzie, jak w przypadku parametrów wywołania (patrz: „Koncepcja wywołania funkcji z parametrami przenoszonymi przez stos”), można w obrębie stosu utworzyć obszar przeznaczony na zmienne lokalne procedury lub funkcji. Często w implementacji algorytmu procedury konieczne jest użycie zmiennych, które mają znaczenie tylko w trakcie wykonywania instrukcji zawartych w danej procedurze. W przeciwnym wypadku, gdy nie są wykonywane operacje procedury, zawartość zmiennych nie ma żadnego znaczenia. Przykładem takiej sytuacji może być zmienna spełniająca rolę licznika wykonanych cykli pętli w procedurze. Taka zmienna ma znaczenie w trakcie wykonywania kolejnych iteracji pętli. W sytuacji, gdy procedura zakończy swoje działanie, stan zmiennej nie ma już znaczenia lub wręcz zmienna może nie istnieć (czyli nie mieć swego miejsca w pamięci danych). Takie zmienne nazywa się lokalnymi i mają one znaczenie tylko w trakcie wykonywania instrukcji w danej procedurze lub funkcji, są związane lokalnie w określoną procedurą lub funkcją. Istotną cechą zmiennych lokalnych jest ich odrębność w każdym wywołaniu rekurencyjnym. Aby uzyskać taką cechę przy każdym wywołaniu procedury musi zostać utworzony obszar pamięci RAM przeznaczony na zmienne, co z kolei oznacza, że na etapie tworzenia programu (czasami używa się określenia: na etapie kompilacji) nie mają określonego adresu w pamięci i nie jest możliwe przydzielenie im jednoznacznej liczby będącej jej adresem. Implikacją tego jest, że przed ich użyciem należy wykonać pewne czynności, które spowodują przydzielenie tym zmiennym obszaru w pamięci. Zmienne statyczne (takie, które są umiejscowione w segmencie danych poprzez dyrektywę .byte) mają w trakcie kompilacji przydzielony jednoznaczny adres. Analogicznie zmienne lokalne w momencie ich użycia również muszą mieć cechą pozwalającą je jednoznacznie określić, taki odpowiednik adresu zmiennej statycznej. Tym adresem w programie jest ich względne (relatywne) położenie w pewnym obszarze. Mikrokontrolery AVR mają instrukcje pozwalające na adresowanie danych w pamięci RAM w tym trybie. Są to instrukcje ldd i std, które wymagają określenia rejestru adresowego i stałego przesunięcia. Jak łatwo zauważyć, taka implementacja zmiennych lokalnych jest niewrażliwa na rekurencyjne wywołanie funkcji. Każdy poziom rekurencji będzie operował w niezależnym obszarze pamięci.
Po wywołaniu bezparametrowej procedury oraz funkcji lub z parametrami przenoszonymi za pośrednictwem rejestrów (przypadek z parametrami wywołania umieszczonymi na stosie i ze zmiennymi lokalnymi będzie opisany oddzielnie) można na stosie utworzyć obszar o określonej wielkości (tak by pomieścić w tym obszarze zmienne lokalne procedury). Podobnie jak w przypadku parametrów wywołania, do adresowania zmiennych będzie użyty rejestr adresowy yh:yl.
Rozpatrzmy następujący fragment programu:

Kod: Zaznacz cały

Fun     :                               ;FUNCTION Fun ( Data[param]:BYTE ) : BYTE[xl];
;********                               ;VAR
.equ    Fun_Cnt         = 0             ; +2    ; Cnt : WORD[stack] ;
.equ    Fun_Index       = 2             ; +1    ; Index : BYTE[stack] ;
.equ    FunLocVSize     = 3             ; =3    ;
        ldi     acc,FunLocVSize         ;
        rcall   _CreateLocalVarNCP      ;BEGIN (* Fun *)
        ldi     acc,0                   ; Cnt := 0 ;
        std     y+Fun_Cnt,acc           ;
        std     y+Fun_Cnt+1,acc         ;
(...)
        ldi     xl,0xFF                 ; Fun := 0xFF ;
        ldi     param,FunLocVSize       ;END (* Fun *) ;
        rcall   _DestrLocalVarNCP       ;
        ret                             ;

Jest to implementacja funkcji o nazwie Fun posiadającej jeden parametr wywołania przenoszony za pośrednictwem rejestru param. Wynik funkcji jest pozostawiony w rejestrze xl. Występujące w funkcji zapisy mają następujące znaczenie:
.equ Fun_Cnt = 0 ; +2 ; Cnt : WORD[stack] ;
.equ Fun_Index = 2 ; +1 ; Index : BYTE[stack] ;
.equ FunLocVSize = 3 ; =3 ;VAR

Definicja kilku stałych będących relatywnymi adresami zmiennych lokalnych występujących w funkcji. Rzeczywisty ich adres w pamięci będzie sumą zawartości rejestru Y wskazującego ma przydzielony obszar i indywidualnego dla każdej zmiennej stałego przesunięcia.
Wyrażenie Y+Fun_Cnt będzie identyfikować zmienną Cnt w operandach instrukcji. Zaleca się by nazwa stałej zawierała w sobie część nazwy funkcji tworząc unikalny w całym tekście programu identyfikator (w innej funkcji również może występować zmienna o nazwie Index, która może mieć inne relatywne położenia w pamięci).
Powyższe zapisy oznaczają:
  • .equ Fun_Cnt = 0 ; Cnt : WORD ;
    dwubajtowa zmienna identyfikowana przez wyrażenia Y+0 i Y+1 (Y+Fun_Cnt i Y+Fun_Cnt+1) do określenia odpowiednio młodszej i starszej części zmiennej,
  • .equ Fun_Index = 2 ; Index : BYTE;
    jednobajtowa zmienna identyfikowana przez wyrażenie Y+2 (Y+Fun_Index),
  • .equ FunLocVSize= 3
    stała określająca wymaganą wielkość obszaru zmiennych lokalnych.

ldi acc,FunLocVSize ;
rcall _CreateLocalVarNCP ;BEGIN (* Fun *)

Pierwszą czynnością w funkcji jaką należy wykonać, to określenie obszaru przeznaczonego na zmienne lokalne. Operację tą wykonuje procedura _CreateLocalVarNCP, która wymaga podania jako parametr wywołania liczby określającej wielkość wymaganego obszaru (parametrem wywołania jest zawartość rejestru acc). Po wywołaniu rejestr Y wskazuje na początek przydzielonego obszaru pamięci danych. Szczegółowe informacje dotyczące wywoływanej procedury będą przedstawione w dalszej części.
Stan stosu programu przedstawia rysunek 3.


ldi acc,0 ; Cnt := 0 ;
std y+Fun_Cnt,acc ;
std y+Fun_Cnt+1,acc ;

Powyższe instrukcje powodują wyzerowanie dwubajtowej zmiennej, która jest położona w przydzielonym obszarze w pamięci RAM.

(...)
ldi xl,0xFF ; Fun := 0xFF ;
ldi param,FunLocVSize ;END (* Fun *) ;
rcall _DestrLocalVarNCP ;
ret ;

Zakończenie działania funkcji (Fun) wymaga zwolnienia przydzielonego wcześniej obszaru pamięci. Tą czynność wykonuje procedura _DestrLocalVarNCP, która wymaga w parametrze wywołania (zawartość rejestru param) określenia wielkości wcześniej przydzielonego obszaru na zmienne lokalne.
Przedstawiona wyżej koncepcja budowy procedury lub funkcji może stanowić schematyczny wzorzec. Implementując procedurę lub funkcję należy określić zmienne lokalne poprzez zdefiniowanie grupy stałych, których wartości będą relatywnymi adresami w przydzielonym obszarze pamięci oraz jedną stałą określającą jego wielkość. Na początku procedury konieczne jest przydzielenie obszaru na zmienne lokalne poprzez wywoływanie procedury _CreateLocalVarNCP, które wymaga określenia jego wielkości. Informacja ta jest przekazywana za pośrednictwem rejestru acc. Jego wybór jest podyktowany tym, że w przypadku procedury lub funkcji (w przykładzie Fun) z parametrami przenoszonymi za pośrednictwem rejestrów (najczęściej param, zh:zl, xh:xl), użycie rejestru acc nie spowoduje utraty ewentualnego parametru wywołania. Analizując zawartą w dalszej części implementację procedury (_CreateLocalVarNCP) do przenoszenia parametrów nie mogą być użyte następujące rejestry: acc, acc2, acc3, acc4, yh, yl.
avrpa2_01.png
Występująca w przykładzie procedura przydzielająca zmiennym lokalnym obszar w pamięci RAM ma następującą implementację:

Kod: Zaznacz cały

_CreateLocalVarNCP :                    ;
                        ; Procedura wymaga przed jej wywołaniem określenia w rejestrze acc wielkości obszaru zajmowanego
                        ; przez zmienne lokalne procedury (wyrażonej w bajtach). W przykładzie jest użyte:
                        ;     ldi     acc,FunLocVSize
                        ;     rcall   _CreateLocalVarNCP
                        ; Stan stosu przedstawiony jest na rysunku 1.
        pop     acc3                            ;
        pop     acc2                            ;
                        ; Aktualnie na szczycie stosu zachowany jest adres powrotu z wywołanej procedury
                        ; (z  _CreateLocalVarNCP). Wykonanie instrukcji pop pozwala na tymczasowe
                        ; przechowanie adresu w rejestrach roboczych. Stan stosu przedstawia
                        ; rysunek 2 w fazie A.
        push    yh                              ;
        push    yl                              ;
                        ; Zachowanie na stosie zawartości rejestru adresowego yh:yl. Rejestr ten spełnia istotną rolę w
                        ; adresowaniu zmiennych lokalnych oraz parametrów wywołania i z tego powodu podlega szczególnej
                        ; ochronie. Stan stosu przedstawia rysunek 2 w fazie B.
        in      yl,spl                  ;
        in      yh,sph                  ;
        sub     yl,acc                  ;
        ldi     acc,0                           ;
        sbc     yh,acc                  ;
                        ; Przepisując zawartość rejestru wskaźnika stosu do rejestru adresowego yh:yl możliwe
                        ; staje się wykonanie prostych operacji arytmetycznych. Odejmując od zawartości
                        ; rejestru Y liczbę określającą wymaganą wielkość obszaru na zmienne lokalne (jako
                        ; parametr wywołania zawarty w rejestrze acc), zostaje obliczone nowe wskazanie
                        ; dla rejestru wskaźnika stosu.
        in      acc4,sreg                       ;
        cli                                     ;
        out     spl,yl                  ;
        out     sph,yh                  ;
        out     sreg,acc4                       ;
                        ; Nowe wskazanie zostaje wpisane do rejestru SP. Znaczenie dodatkowych operacji poprzedzających
                        ; samą instrukcję wpisania do rejestru SP (sph:spl) nowej zawartości są opisane w
                        ; „Koncepcja wywołania funkcji z parametrami przenoszonymi przez stos”. Stan stosu przedstawia
                        ; rysunek 2 w fazie C.
        push    acc2                            ;
        push    acc3                            ;
                        ; Umieszczenie na stosie (już za blokiem przeznaczonym na zmienne lokalne) przechowywanego
                        ; tymczasowo w rejestrach adresu powrotu. Wykonanie najbliższej instrukcji ret przeniesie
                        ; wykonanie programu do właściwego miejsca. Stan stosu przedstawia rysunek 2 w fazie D.
        adiw    yl,1                            ;
                        ; Końcowa korekta zawartości rejestru adresowego yh:yl. Po wykonaniu instrukcji ret, stan stosu
                        ; będzie zgodny z rysunkiem 2 w fazie C oraz rejestr Y będzie wskazywał w te same miejsce co
                        ; rejestr SP. Chcąc, by adres Y+0 w operandach instrukcji wskazywał na zmienną lokalną,
                        ; konieczne jest zwiększenie zawartości rejestru Y.
        ret                                     ;
avrpa2_02.png

Efektem działania powyższej procedury jest odpowiednia modyfikacja stosu, w obrębie którego znajduje się blok przeznaczony na przechowywanie zmiennych lokalnych procedur. Stan stosu przedstawia rysunek 3. Wskazanie rejestru Y na początek tego bloku pozwala w łatwy sposób zaadresować dowolną zmienną lokalną korzystając z odpowiedniego trybu adresowania oferowanego przez mikrokontrolery AVR.
avrpa2_03.png
Na zakończenia działania procedury lub funkcji (w przykładzie Fun) należy zwolnić przydzielony wcześniej obszar poprzez wykonanie instrukcji rcall _DestrLocalVarNCP. Wywołanie to wymaga określenia wielkości zwalnianego obszaru (informacja ta jest wcześniej umieszczana w rejestrze param). Jego wybór jest podyktowany tym, że w przypadku wywołania funkcji (jak w przykładzie) konieczne jest użycie rejestru lub pary rejestrów na przeniesienie jej wartości. Analizując implementację procedury (_DestrLocalVarNCP) można zauważyć, że do przenoszenia wyniku działania funkcji nie należy używać następujących rejestrów: acc2, acc3, acc4, param, yh, yl.
Procedura zwalniająca obszar zmiennych lokalnych ma następującą implementację:

Kod: Zaznacz cały

_DestrLocalVarNCP :                     ;
                        ; Procedura wymaga przed jej wywołaniem określenia w rejestrze param wielkości obszaru
                        ; zajmowanego przez zmienne lokalne procedury (wyrażonej w bajtach). W przykładzie jest:
                        ;   ldi     param,FunLocVSize
                        ;   rcall   _DestrLocalVarNCP
        pop     acc3                            ;
        pop     acc2                            ;
                        ; Zachowany na szczycie stosu adres powrotu z wywołanej procedury (z  _DestrLocalVarNCP) jest
                        ; tymczasowo przechowany w rejestrach roboczych.
        in      yl,spl                  ;
        in      yh,sph                  ;
        add     yl,param                        ;
        ldi     param,0                 ;
        adc     yh,param                        ;
                        ; W operacjach arytmetycznych mających na celu obliczenie nowej zawartości wskaźnika stosu,
                        ; rejestr Y występuje w roli rejestru roboczego (dotychczasowa zawartość tego rejestru
                        ; staje się nieistotna).
        in      acc4,sreg                       ;
        cli                                     ;
        out     spl,yl                  ;
        out     sph,yh                  ;
        out     sreg,acc4                       ;
                        ; Wpisanie obliczonego położenia szczytu stosu do właściwego rejestru.
        pop     yl                              ;
        pop     yh                              ;
                        ; Odtworzenie stanu rejestru adresowego Y (zachowanego wcześniej na stosie w
                        ; procedurze _CreateLocalVarNCP.
        push    acc2                            ;
        push    acc3                            ;
                        ; Umieszczenie na stosie adresu powrotu z bieżącej procedury pozwala kolejnej instrukcji ret
                        ; przenieść wykonywanie programu we właściwe miejsce.
        ret                                     ;

Jako przykład zastosowania niech posłuży realizacja funkcji poszukiwania danych w tablicy z zastosowaniem metody połowienia kroku. Funkcja ta (o nazwie Search) wymaga podania w rejestrze param danych poszukiwanych w tablicy DataTable. Wynik poszukiwania (jako indeks w tablicy DataTable jest zawarty w rejestrze acc. Jeżeli dane nie są znalezione w tablicy, funkcja zwraca wartość 0xFF. Istotną cechą algorytmu jest to, by dane w tablicy DataTable były już uporządkowane. Jest to bardzo szybka i efektywna funkcja. Jej algorytm działania jest następujący (podobny do poszukiwania hasła w encyklopedii lub słowniku):
  • w obrębie dostępnego zbioru elementów tablicy wybieramy element środkowy, czyli dzielimy zbiór, w którym poszukujemy wzorca na dwie równe części (lub prawie równe jeżeli liczba elementów jest nieparzysta),
  • sprawdzamy, czy mieliśmy farta i wstępnie wyselekcjonowany element jest tym poszukiwanym, jeżeli tak – to trafiony – koniec poszukiwania,
  • w przeciwnym razie patrzymy, czy należy poszukiwać w pierwszej połowie, czy w drugiej, czyli porównując wartość poszukiwanego elementu z wartością elementu wstępnie wyselekcjonowanego i w zależności od wyniku relacji mniejszy, większy do kolejnej iteracji będzie brana pierwsza czy druga połowa,
  • powracamy na początek do kolejnej iteracji.
A jak poszukujemy hasła w encyklopedii. Podobnie, otwieramy książkę mniej więcej w połowie, sprawdzamy czy mamy farta i poszukiwane hasło znajduje się w miejscu otwarcia książki. Jeżeli jest, to mamy znalezione i jest to koniec poszukiwań. Jeżeli nie, to sprawdzamy czy dalej należy poszukiwać w lewej czy prawej części książki (czy poszukiwane hasło jest alfabetycznie wcześnie czy później). Wybraną połowę dzielimy ponownie na pół i ... powtarzamy cykl aż do znalezienia lub stwierdzenia, że poszukiwanego hasło nie ma w encyklopedii.
Ilustracja algorytmu jako program zawiera jedynie samą funkcję poszukiwania bez zbędnej całej otoczki związanej z prezentacją wyników poszukiwania.

Kod: Zaznacz cały

;
(...)
        .nolist
;*******************************************************
.include "m8515def.inc"
;*******************************************************
        .list
        .listmac
;-----------------------------------------------------------------------------
.macro  ldz
        ldi     zl,low(@0)
        ldi     zh,high(@0)
.endm
;-----------------------------------------------------------------------------
.macro  ldx
        ldi     xl,low(@0)
        ldi     xh,high(@0)
.endm
;-----------------------------------------------------------------------------
.macro  pushf
        push    acc
        in      acc,sreg
        push    acc
.endm
;-----------------------------------------------------------------------------
.macro  popf
        pop     acc
        out     sreg,acc
        pop     acc
.endm
;-----------------------------------------------------------------------------
;                       = r0 [reserved]
.def acc                = r16
.def acc2               = r17
.def acc3               = r18
.def acc4               = r19
.def param              = r20
;-----------------------------------------------------------------------------
        .cseg
;*******************************************************
;*******************************************************
;**                                                   **
;**                  CODE  SEGMENT                    **
;**                                                   **
;*******************************************************
;*******************************************************
;-----------------------------------------------------------------------------
        .org    0                               ;
        rjmp    ResetProcessor          ;
;-----------------------------------------------------------------------------
(...)
SoftwareInit :                          ;PROCEDURE SoftwareInit ;
;*************                          ;BEGIN (* SoftwareInit *)
        ret                                     ;END (* SoftwareInit *) ;
;-----------------------------------------------------------------------------
_CreateLocalVarNCP :                    ; acc = size of param
(...)
;-----------------------------------------------------------------------------
_DestrLocalVarNCP :                     ; param = size of param
(...)
;-----------------------------------------------------------------------------
Search  :                       ;FUNCTION Search(Data[param]:BYTE): BYTE[acc] ;
;********                                       ;
                                                ;VAR
.equ    Srch_Left       = 0                     ; +1    ; Left : BYTE ;
.equ    Srch_Right      = Srch_Left+1   ; +1    ; Right : BYTE ;
.equ    Srch_Curr       = Srch_Right+1  ; +1    ; Curr : BYTE ;
.equ    SrchLocVSize= Srch_Curr+1       ; =3    ;
                                                ; TabElement : BYTE[acc] ;
                        ; Algorytm wymaga kilku zmiennych, które będą umiejscowione jako zmienne lokalne w
                        ; obszarze pamięci na sosie. Poszczególne stałe, jako operandy przesunięcia w stosunku
                        ; do zawartości rejestru Y pozwalając adresować poszczególne zmienne. W definicji stałych
                        ; zastosowano „chwyt” pozwalający wygenerować poszczególne stałe oraz automatycznie
                        ; określić wielkość obszaru zmiennych lokalnych.
        ldi     acc,SrchLocVSize                ;
        rcall   _CreateLocalVarNCP      ;BEGIN (* Search *)
                        ; Zostaje utworzony obszar na zmienne lokalne.
        ldi     acc,0                           ; Left := 0 ;
        std     y+Srch_Left,acc         ;
        ldi     acc,TableSize-1         ; Right := TableSize - 1 ;
        std     y+Srch_Right,acc                ;
                        ; Określenie granic w tablicy DataTable, w obrębie którego realizowane będzie poszukiwanie.
                        ; Na początku jest to cała tablica, lewostronnie od zera prawostronnie do końca.
Srch_1  :                               ; LOOP
                        ; Zakładamy pętlę.
        ldd     acc,y+Srch_Left         ;  Curr := ( Left + Right ) / 2 ;
        ldd     acc2,y+Srch_Right               ;
        add     acc,acc2                        ;
        lsr     acc                             ;
        std     y+Srch_Curr,acc         ;
                        ; Określamy element środkowy (lub prawie środkowy).
        ldz     DataTable<<1            ;  TabElement := DataTable [ Curr ] ;
        add     zl,acc                  ;
        ldi     acc,0                           ;
        adc     zh,acc                  ;
        lpm     acc,z                           ;
                        ; Pobieramy z tablicy wartość elementu środkowego.
        cp      param,acc                       ;  IF Data = TabElement THEN
        brne    Srch_2                  ;  BEGIN
                        ; Jeżeli jest to element poszukiwany, to mamy farta – koniec poszukiwań.
        ldd     acc,y+Srch_Curr         ;   Search := Curr ;
                        ; Jako wynik funkcji zwracana jest wartość indeksu (położenie w tablicy) znalezionego elementu.
        rjmp    Srch_0                  ;   RETURN ;
Srch_2  :                               ;  END (* IF *) ;
        cp      param,acc                       ;  IF Data < TabElement THEN
        brcc    Srch_4                  ;  BEGIN
                        ; Jeżeli poszukiwany element jest mniejszy, to do następnej iteracji będzie wzięta lewa połowa.
        ldd     acc,y+Srch_Curr         ;   IF Curr = 0 THEN
        cpi     acc,0                           ;
        brne    Srch_7                  ;   BEGIN
        ldi     acc,0xFF                        ;    Search := 0xFF ;
                        ; Oczywiście pod warunkiem, że jeszcze jest w czym szukać, bo jeżeli element środkowy pokrywa się
                        ; z początkiem tablicy, to znaczy, że poszukiwanego elementu nie ma w tablicy.
        rjmp    Srch_0                  ;    RETURN ;
Srch_7  :                               ;   END (* IF *) ;
        dec     acc                             ;   Right := Curr - 1 ;
        std     y+Srch_Right,acc                ;
                        ; Do następnej iteracji będzie brana ta część, która zawiera elementy o wartościach mniejszych.
                        ; Początek nowego obszaru nie ulega zmianie, koniec nowego obszaru jest aktualnym elementem
                        ; środkowym (dokładnie poprzedzającym element środkowy).
        rjmp    Srch_5                  ;  END (* IF ... *)
Srch_4  :                               ;  ELSE
                                                ;  BEGIN
                        ; Jeżeli poszukiwany element nie jest równy i nie jest mniejszy, to pozostaje jedyna
                        ; ewentualność, że jest większy. Do następnej iteracji będzie wzięta prawa połowa.
        ldd     acc,y+Srch_Curr         ;   IF Curr = ( TableSize - 1 ) THEN
        cpi     acc,TableSize-1         ;
        brne    Srch_8                  ;   BEGIN
        ldi     acc,0xFF                        ;    Search := 0xFF ;
                        ; Oczywiście pod warunkiem, że jeszcze jest w czym szukać, bo jeżeli element środkowy
                        ; pokrywa się z końcem tablicy, to znaczy, że poszukiwanego elementu nie ma w tablicy.
        rjmp    Srch_0                  ;    RETURN ;
Srch_8  :                               ;   END (* IF *) ;
        inc     acc                             ;   Left := Curr + 1 ;
        std     y+Srch_Left,acc         ;
                        ; Do następnej iteracji będzie brana ta część, która zawiera elementy o wartościach większych.
                        ; Początek nowego obszaru jest aktualnym elementem środkowym (dokładnie następnikiem
                        ; elementu środkowego) oraz koniec nowego obszaru nie ulega zmianie.
Srch_5  :                               ;  END (* IF ... ELSE *) ;
        ldd     acc,y+Srch_Left         ;  IF Left > Right THEN
        ldd     acc2,y+Srch_Right               ;
        cp      acc,acc2                        ;
        breq    Srch_6                  ;
                        ; Jeżeli nowe granice obszaru do przeszukania „się przegięły”, czyli lewa granica jest większa
                        ; od prawej, to znaczy, że poszukiwanego elementu w tablicy nie ma.
        brcs    Srch_6                  ;  BEGIN
        ldi     acc,0xFF                        ;   Search := 0xFF ;
        rjmp    Srch_0                  ;   RETURN ;
Srch_6  :                               ;  END (* IF *) ;
        rjmp    Srch_1                  ; END (* LOOP *) ;
Srch_0  :                               ;
        ldi     param,SrchLocVSize      ;END (* Search *) ;
        rcall   _DestrLocalVarNCP               ;
                        ; Zakończenie funkcji wymaga zwolnienia obszaru stosu, który był zajęty przez zmienne lokalne.
        ret                                     ;
;-----------------------------------------------------------------------------
ResetProcessor :                                ;
        ldi     acc,HIGH(RAMEND)                ;
        out     SPH,acc                 ;
        ldi     acc,LOW(RAMEND)         ;
        out     SPL,acc                 ;
Main :                                  ;BEGIN (* Main *)
        rcall   ClearRAM                        ; ClearRAM ( ) ;
        rcall   ClearRegs                       ; ClearRegs ( ) ;
        rcall   SoftwareInit            ; SoftwareInit ( ) ;
        sei                                     ; EI ( ) ;
        ldi     param,0x9E                      ; Search ( 0x9E ) ;
        rcall   Search                  ;
                                                ; (*acc=1E*)
                        ; Kilka przykładowych wywołań. Poszukiwanie w DataTable elementu o wartości 9E hex.
                        ; Powinien on być znaleziony pod indeksem 1E hex.
        ldi     param,0x19                      ; Search ( 0x19 ) ;
        rcall   Search                  ;
                                                ; (*acc=03*)
                        ; Poszukiwanie w DataTable elementu o wartości 19 hex. Powinien on być znaleziony
                        ; pod indeksem 03 hex.
        ldi     param,0x02                      ; Search ( 0x02 ) ;
        rcall   Search                  ;
                                                ; (*acc=FF*)
                        ; Poszukiwanie w DataTable elementu o wartości 02 hex. Tym razem funkcja powinna
                        ; zwrócić FF hex, co oznacza, że takiego elementu nie znaleziono.
        ldi     param,0xA1                      ; Search ( 0xA1 ) ;
        rcall   Search                  ;
                                                ; (*acc=FF*)
        ldi     param,0x67                      ; Search ( 0x67 ) ;
        rcall   Search                  ;
                                                ; (*acc=FF*)
        ldi     param,0x70                      ; Search ( 0x70 ) ;
        rcall   Search                  ;
                                                ; (*acc=17*)
        ldi     param,0x06                      ; Search ( 0x06 ) ;
        rcall   Search                  ;
                                                ; (*acc=00*)
        ldi     param,0xA0                      ; Search ( 0xA0 ) ;
        rcall   Search                  ;
                                                ; (*acc=1F*)
        ldi     param,0x44                      ; Search ( 0x44 ) ;
        rcall   Search                  ;
                                                ; (*acc=FF*)
Main_0  :                               ; LOOP
        rjmp    Main_0                  ; END (* LOOP *) ;
                                                ;END (* Main *) ;
;-----------------------------------------------------------------------------
                                                ;CONST
.equ    TableSize       =       32              ; TableSize = 32 ;
DataTable:      .db     0x06,0x07,0x16,0x19,0x1D,0x26,0x2B,0x2E
;                         00   01   02   03   04   05   06   07
                .db     0x32,0x35,0x3A,0x46,0x49,0x4C,0x51,0x52
;                         08   09   0A   0B   0C   0D   0E   0F
                .db     0x5A,0x5C,0x60,0x64,0x66,0x6E,0x6F,0x70
;                         10   11   12   13   14   15   16   17
                .db     0x75,0x83,0x8A,0x8E,0x93,0x99,0x9E,0xA0
;                         18   19   1A   1B   1C   1D   1E   1F
                                ; DataTable:ARRAY[0..TableSize-1] OF BYTE = { ..... } ;
.exit

Teraz, po napisaniu programu pozostaje go skompilować i odpalić symulator by sprawdzić działanie opisanego wyżej algorytmu. Ja odpaliłem .... zadziałało.

Załącznik:
avrasm_p2.zip
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
j23
Expert
Expert
Posty: 506
Rejestracja: czwartek 08 paź 2015, 18:40

Re: [ASM][AVR] Inne spojrzenie

Postautor: j23 » sobota 13 maja 2017, 15:34

Przede wszystkim dziękuję Kolego Gaweł za wspaniały artykuł na temat pewnych szczegółów (szczególnych przypadków) jeśli chodzi o pisanie kodu w Assemblerze.
Po drugie...
gaweł pisze:Programowanie w assemblerze jest postrzegane jako proste lub wręcz prymitywne. Chciałbym przekonać, że wcale tak nie jest.
, to mnie specjalnie do tego przekonywać nie trzeba ;) Uważam się za osobę, która i tak wie za mało (pomimo mojej jakieś tam podstawowej wiedzy na temat Assemblera). Abstrahując od tego jaki to Assembler jest "prosty" język programowania to IMHO trzeba by brać pod uwagę również pewien rozstrzał jego składni wynikający z różnorodności CPU na jakich jest stosowany.
Jak już pisałem Twój artykuł to prawdziwe arcydzieło jeśli chodzi o wytłumaczenie niuansów w składni Assemblera. Nie przeglądałem jeszcze Twoich komiksów, ale -jeśli tam tego nie ma- to uważam, że może dobrze mógłbyś -PROSZĘ- napisać jakiś wstępny tutorial do pisania tzw.wstawek Assemblerowych w ujęciu optymalizacji kodu (co by w ogóle oswoić ;) ), tzn.na przykład wstawka asm w tzw.funkcji inline? -proszę.
Osobiście właśnie w ten sposób oswajałem się z Assemblerem. Dopiero potem przyszła pora na "czysty" Assembler (segmenty: stosu, kodu, danych, modele pamięci etc. -ale to było pod CPU x86, więc trochę inaczej).

Jeszcze raz dzięki za ciekawy i BARDZO pomocny, wspaniały artykuł o Assemblerze. :)

Pozdrawiam! J23
Internet łączy ludzi, którzy dzielą się swoimi zainteresowaniami, pomysłami i potrzebami, bez względu na geograficzne (przeciwności).
BOB TAYLOR, PARC

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

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » sobota 13 maja 2017, 16:34

j23 pisze:to mnie specjalnie do tego przekonywać nie trzeba ;) Uważam się za osobę, która i tak wie za mało (pomimo mojej jakieś tam podstawowej wiedzy na temat Assemblera). Abstrahując od tego jaki to Assembler jest "prosty" język programowania to IMHO trzeba by brać pod uwagę również pewien rozstrzał jego składni wynikający z różnorodności CPU na jakich jest stosowany.

Masz rację, różne asemblery wynikające z różnorodności CPU i.t.d :arrow: różne spojrzenia. Asembler to nie jest jakiś prymitywny kij na procki. Wszystko jest kwestią pewnej wyobraźni. Napisałem powyższe rozważania, bo pokazać jak języki wysokiego poziomu mogą radzić sobie z pewnymi problemami. Bo przecież języki typu C dają pewne możliwości budowy bardziej złożonych algorytmów. Naturalne pytanie: jak to robią? Może robią to w podobny sposób? Jak się nad tym głębiej zastanowić, to przykładowo kompilator C nie "uprawia żadnej magii". W tym kontekście można nawet zaryzykować twierdzenie, że język C to w gruncie rzeczy makroasembler.
Programowanie jest kwestią pewnej filozofii, język w sumie jest rzeczą wtórną.

j23 pisze:Nie przeglądałem jeszcze Twoich komiksów, ale -jeśli tam tego nie ma- to uważam, że może dobrze mógłbyś -PROSZĘ- napisać jakiś wstępny tutorial do pisania tzw.wstawek Assemblerowych w ujęciu optymalizacji kodu (co by w ogóle oswoić ;) ), tzn.na przykład wstawka asm w tzw.funkcji inline? -proszę.

W tamtych komiksach nie ma tego. Ale... kto wie? Obecnie nie mogę określić żadnego terminu.

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

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

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » sobota 13 maja 2017, 23:36

Parametry wywołania i zmienne lokalne

W podobny sposób jak zostało opisane w części zatytułowanej „Koncepcja wywołania funkcji z parametrami przenoszonymi przez stos”, możliwe jest utworzenie mechanizmu pozwalającego na przenoszenie parametrów wywołania procedur oraz funkcji za pośrednictwem stosu oraz utworzenie, również w obrębie stosu, bloku pamięci przeznaczonego na zmienne lokalne wywołanej funkcji lub procedury. Przygotowanie parametrów wywołania odbywa się w identyczny sposób jak we wspomnianej części. Wewnątrz wywołanej funkcji należy wykonać podobne czynności dotyczące reorganizacji stosu mikrokontrolera (patrz: „Zmienne lokalne funkcji”). Sprowadza się do wykonania na początku wywołanej procedury:
rcall _OpenCallParam
oraz na końcu wywołanej procedury:
ldi param,<wielkość obszaru parametrów>
rcall _CloseCallParam

Analogicznie można wydzielić z obszaru stosu blok pamięci przeznaczonego na zmienne lokalne procedury lub funkcji. W obu przypadkach, przy dostępie do parametrów oraz zmiennych lokalnych, używany jest rejestr yh:yl. Ponieważ oba obszary pamięci są rozłączne i do dyspozycji jest jeden rejestr do adresowania indeksowego z przesunięciem, ten przypadek wymaga realizacji dodatkowych operacji. Identyczne możliwości adresowe posiada rejestr zh:zl, ale jest on często używany do szeroko pojętego adresowania i trudno jest go zarezerwować wyłącznie do adresowania w jednym z wymienionych obszarów pamięci. Sposób rozwiązania będzie posiadał niewielkie zmiany w stosunku do opisanych w poprzednich częściach realizacji. Schematyczny początek procedury lub funkcji będzie wyglądał następująco:
call _OpenCallParam
ldi acc,<wielkość obszaru zmiennych lokalnych>
call _CreateLocalVar

Po wykonaniu powyższych instrukcji stan stosu przedstawia rysunek 1. Rejestr adresowy yh:yl będzie wskazywał na początek obszaru zmiennych lokalnych. Istotną różnicą w stosunku do koncepcji opisanej w części “Zmienne lokalne” jest zarezerwowanie w obszarze zmiennych lokalnych wskazania na początek obszaru zawierającego parametry wywołania. Dowiązanie w obszarze zmiennych do obszaru parametrów jest zachowane w miejscu wskazywanym przez wyrażenie Y+0 (część młodsza) oraz Y+1 (część starsza) i jest zrealizowane przez wywołaną procedurę _CreateLocalVar. Oznacza to, że przydzielając zmiennym lokalnym ich relatywne adresy należy mieć na uwadze, że komórki identyfikowane przez Y+0 i Y+1 są już zajęte. Pierwsza zmienna lokalna ma położenie względne 2 (Y+2) w tym obszarze. Wielkość obszaru zajmowanego przez zmienne należy określić jako sumaryczną wielkość bez uwzględnienia dwubajtowego wskazania na obszar parametrów (te wskazanie jest uwzględnione w odpowiednich procedurach przy tworzeniu i usuwaniu obszaru zmiennych lokalnych).
avrpa3_01.png

Przykładową postać procedury mającej parametry wywołania umieszczone na stosie oraz zmienne lokalne przedstawia poniższy przykład. Zadaniem procedury jest przeszukanie tablicy znakowej zawierającej „rozpoznawalne” znaki (parametr wywołania CharTable). Poszukiwanym kodem w tablicy jest zawartość parametru wywołania Code. W przypadku znalezienia poszukiwanego wzorca w obrębie tablicy CharTable, wywoływana jest bezparametrowa procedura skojarzona ze wzorcem. Wskazania na odpowiednie procedury zawarte są w tablicy ServTable, która jest parametrem wywołania procedury. W przypadku nieznalezienia wzorca w tablicy wywoływana jest procedura identyfikowana jako parametr ElseServ z jednym jednobajtowym parametrem, który jest przenoszony przez stos. Odpowiedniość wzorców i wywoływanych procedur jest następująca:
  • CharTable [ 0 ] ↔ ServTable [ 0 ]
  • CharTable [ 1 ] ↔ ServTable [ 1 ]
  • CharTable [ 2 ] ↔ ServTable [ 2 ]
  • CharTable [ 3 ] ↔ ServTable [ 3 ]
  • ....
Wystąpienie w CharTable wartości 0xFF oznacza logiczny jej koniec.

Kod: Zaznacz cały

LocateService:                          ;PROCEDURE LocateService
;*************                          ;
.equ LocServParamSize   = 7     ; =7    ; (
.equ LocServ_Code               = 6     ; +1    ;   Code      : CHAR[stack] ;
.equ LocServ_CharTable  = 4     ; +2    ;   CharTable : ARRAY OF CHAR[stack] ;
.equ LocServ_ServTable  = 2     ; +2    ;   ServTable : ARRAY OF PROCEDURE()stack];
.equ LocServ_ElseServ   = 0     ; +2    ;   ElseServ  : PROCEDURE(CHAR)[stack] ) ;
                        ; Procedura ma cztery parametry wywołania (jeden jednobajtowy i trzy dwubajtowych).
                        ; Ich położenia w obszarze pamięci przeznaczonej na parametry wywołania są następujące:
                        ; LocServ_Code = 6 – względne przesunięcie w obszarze parametrtów wywołania
                        ;                                 parametru Code,
                        ; LocServ_CharTable = 4 – względne przesunięcie w obszarze parametrtów wywołania
                        ;                                 parametru CharTable,
                        ; LocServ_ServTable = 2 – względne przesunięcie w obszarze parametrtów wywołania
                        ;                                parametru ServTable,
                        ; LocServ_ElseServ = 0 – względne przesunięcie w obszarze parametrtów wywołania
                        ;                               parametru ElseServ,
                        ; LocServParamSize = 7 – łączna wielkość obszaru przeznaczonego na parametry wywołania.
.equ LocServLocVarSize  = 2             ;VAR
.equ LocServ_Index      = 2     ; +1    ; Index : BYTE[stack] ;
.equ LocServ_TabElem    = 3     ; +1    ; TabElem : BYTE[stack] ;
                        ; Kilka zmiennych lokalnych procedury, których względne położenia w odpowiednim obszarze
                        ; pamięci są określone poprzez wartości stałych. Łączna wielkość obszaru wynosi 2 bajty
                        ; (LocServLocVasSize) bez uwzględnienia komórek Y+0 i Y+1 zawierających wskazanie na
                        ; obszar parametrów.
        rcall   _OpenCallParam          ;
        ldi     acc,LocServLocVarSize   ;
        rcall   _CreateLocalVar         ;BEGIN (* LocateService *)
                        ; Reorganizacja stosu w zakresie umożliwienia dostępu do obszaru parametrów wywołania oraz
                        ; utworzenie obszaru zmiennych lokalnych. Realizacja algorytmu procedury zawiera się w
                        ; implementacji odpowiedniej pętli. W jej obrębie zostanie przejrzana zawartość tablicy
                        ; wniesionej do procedury jako parametr wywołania (stała LocServ_CharTable określająca
                        ; położenie parametru w obszarze stosu). W tabeli tej będzie poszukiwana pozycja zawierająca
                        ; identyczne dane, jakie są wniesione do procedury poprzez parametr wywołania (identyfikowany
                        ; przez stałą LocServ_Code). W przypadku wystąpienia w tabeli wzorca, realizowane jest
                        ; wywołanie procedury skojarzonej ze wzorcem. W przypadku niewystąpienia wzorca w tabeli,
                        ; wywoływana jest procedury „awaryjna”, która jest wniesiona do procedury jako parametr
                        ; wywołania identyfikowany przez stałą LocServ_ElseServ.
        ldi     acc,0                           ; Index := 0 ;
        std     y+LocServ_Index,acc     ;
                        ; Do przeglądania zawartości tablicy CharTable będzie używana zmienna lokalna Index pełniąca
                        ; funkcję indeksu, która zostaje wyzerowana przed rozpoczęciem pętli.
LoSrv_1 :                               ; LOOP
                        ; Wewnątrz pętli z tablicy bajtowej (tablicy o elementach jednobajtowych) CharTable wniesionej
                        ; jako parametr wywołania przeniesiony zostaje do zmiennej lokalnej TabElem kolejny jej element
                        ; (wskazany przez zawartość zmiennej lokalnej Index). Z założenia, tablica ta znajduje się w
                        ; obrębie pamięci FLASH, co ma swoje odbicie w realizacji przesłania danych. Znaczenie
                        ; poszczególnych operacji jest następujące:
        GetCallParamLink                        ;  TabElem := CharTable [ Index ] ;
                        ; Ponieważ rejestr Y (yh:yl) dotyczy obszaru zmiennych lokalnych a zachodzi potrzeba sięgnięcia
                        ; do obszaru parametrów, należy uzyskać adres bloku w obszarze stosu zawierający
                        ; parametru wywołania. Adres ten zawarty jest w pamięci o adresie Y+0 i Y+1 (taka
                        ; niejawna zmienna lokalna). Do tego używana jest makroinstrukcja GetCallParamLink,
                        ; której efektem jest umieszczenie w rejestrze Z (zh:zl) adresu obszaru parametrów wywołania
                        ; zawartych na stosie.
        ldd     xl,z+LocServ_CharTable  ;
        ldd     xh,z+LocServ_CharTable+1;
                        ; Mając w rejestrze Z wskazanie na obszar parametrów wywołania, „wysupłanie” adresu
                        ; położenia tabeli nie jest operacją skomplikowaną. W wyniku realizacji powyższych instrukcji
                        ; w rejestrze X (xh:xl) znajduje się adres tabeli w pamięci FLASH (z założenia, tabele CharTable
                        ; i ServTable zawarte są w pamięci FLASH).
        ldd     acc,y+LocServ_Index     ;
        add     xl,acc                  ;
        ldi     acc,0                           ;
        adc     xh,acc                  ;
                        ; Dodając do podstawy adresowej tabeli w pamięci FLASH (jej adres zawarty w
                        ; rejestrze X) aktualną wartość indeksu (zmienna lokalna Index, czyli komórka w
                        ; pamięci o adresie Y+LocServ_Index) uzyskany jest adres odpowiedniego elementu
                        ; tablicy. Dodanie należy wykonać jako operację 16-bitową (z uwzględnieniem ewentualnego
                        ; przeniesienia w operacjach 8-bitowych). Finalny adres komórki zawarty jest w rejestrze X.
        movw    zl,xl                           ;
        lpm     acc,z                           ;
                        ; Ponieważ dla instrukcji lpm adres komórki wymagany jest w rejestrze Z, wcześniej
                        ; następuje przepisanie zawartości rejestru X (xh:xl) do rejestru Z (zh:zl). Rezultat operacji
                        ; znajduje się w rejestrze acc,
        std     y+LocServ_TabElem,acc   ;
                        ; który zostaje przesłany do zmiennej lokalnej.
        cpi     acc,0xFF                        ;  IF TabElem = 0xFF THEN
        brne    LoSrv_2                 ;  BEGIN
                        ; Porównanie, czy nie został osiągnięty koniec tablicy CharTable. W przypadku TAK, zostanie
                        ; wywołana wskazana w parametrach wywołania „procedura awaryjna”. Wywołanie nastąpi
                        ; pod warunkiem, że wskazanie jest różne od NIL (NIL, NULL, to taka stała /o wartości 0/,
                        ; która mówi, że dane wskazanie jest puste).
        GetCallParamLink                        ;   IF ElseServ <> NIL THEN
                        ; W tym celu, podobnie jak wyżej, zastosowana jest makroinstrukcja GetCallParamLink,
                        ; której efektem jest umieszczenie w rejestrze Z wskazania na obszar parametrów wywołania.
        ldd     xl,z+LocServ_ElseServ   ;
        ldd     xh,z+LocServ_ElseServ+1 ;
                        ; Zawartość parametru zostaje umieszczona w rejestrze X.
        mov     acc,xl                  ;
        or      acc,xh                  ;
                        ; Jeżeli wskazanie nie jest „puste” (inne niż NIL), to następuje wywołanie procedury z
                        ; jednym parametrem przenoszonym przez stos.
        breq    LoSrv_4                 ;   BEGIN
        ldd     acc,z+LocServ_Code      ;    ElseServ ( Code ) ;
        push    acc                             ;
        movw    zl,xl                           ;
        icall                                   ;
                        ; W tym przypadku parametrem kolejnego wywołania jest aktualny parametr wywołania
                        ; Code, co można interpretować, że dla danych zawartych w parametrze Code nie został
                        ; znaleziony wzorzec w tablicy CharTable. Komórka Code znajduje się w obszarze parametrów
                        ; wywołania. Dostęp do niej powinien być realizowany poprzez instrukcję GetCallParamLink,
                        ; ale aktualna zawartość rejestru Z jest ważna. Organizacja wywołania procedury sprowadza
                        ; się do umieszczenia na stosie parametru wywołania (instrukcja push).
LoSrv_4 :                               ;   END (* IF *) ;
        rjmp    LoSrv_0                 ;   RETURN ;
                        ; Po powrocie z wywołanej „procedury awaryjnej” następuje wyjście z bieżącej
                        ; procedury (jako skok do miejsca, w którym realizowane jest usunięcie obszaru
                        ; zmiennych lokalnych i parametrów wywołania.
LoSrv_2 :                               ;  END (* IF *) ;
                        ; Gdy nie został osiągnięty koniec obszaru CharTable, to wykonywane jest sprawdzenie,
                        ; czy nie zaistniał przypadek znalezienia wzorca.
        GetCallParamLink                        ;  IF TabElem = Code THEN
        ldd     acc2,z+LocServ_Code     ;
        ldd     acc,y+LocServ_TabElem   ;
        cp      acc,acc2                        ;
                        ; Dostęp do parametrów wywołania (komórka Code) wymaga użycia instrukcji
                        ; GetCallParamLink, w wyniku której w rejestrze Z znajduje się adres obszaru
                        ; parametrów wywołania. Odpowiednie porównanie i skok warunkowy pozwala
                        ; wykryć taki przypadek.
        brne    LoSrv_3                 ;  BEGIN
                        ; Sięgnięcie do parametrów wywołania (ServTable) wymaga użycia instrukcji
                        ; GetCallParamLink, ale dotychczasowa zawartość rejestru Z jest ważna.
        ldd     xl,z+LocServ_ServTable  ;   ServTable [ Index ] ( ) ;
        ldd     xh,z+LocServ_ServTable+1;
                        ; Podobnie jak w określaniu rzeczywistego adresu w tabeli CharTable, tu są
                        ; realizowane podobne działania. Po powyższych instrukcjach w rejestrze X będzie
                        ; zawarty adres właściwej tabeli w pamięci FLASH.
        ldd     acc,y+LocServ_Index     ;
        ldi     acc2,0                  ;
        lsl     acc                             ;
        rol     acc2                            ;
        add     xl,acc                  ;
        adc     xh,acc2                 ;
        movw    zl,xl                           ;
                        ; Tabela Servtable zawiera elementy dwubajtowe, co oznacza, że obliczając
                        ; adres i-tego elementy tej tablicy należy uwzględnić wielkość pojedynczego
                        ; elementu tablicy. W tym przypadku użyta jest instrukcje lsl i rol, które jako
                        ; instrukcje przesunięcia realizują operację mnożenia przez 2 dla rejestru 16-bitowego.
        lpm     acc,z+                  ;
        lpm     acc2,z                  ;
        mov     zl,acc                  ;
        mov     zh,acc2                 ;
                        ; Pobranie dwóch kolejnych bajtów z pamięci FLASH umożliwia uzyskanie
                        ; adresu docelowej procedury.
        Icall                                   ;
                        ; Procedura jest wywoływana z założenia jako bezparametrowa.
        rjmp    LoSrv_0                 ;   RETURN ;
                        ; Po powrocie z wywołanej procedury następuje zakończenie działania bieżącej
                        ; procedury, jako skok do miejsca związanego z usunięciem ze stosu zmiennych
                        ; lokalnych i parametrów wywołania.
LoSrv_3 :                               ;  END (* IF *) ;
        ldd     acc,y+LocServ_Index     ;  Index := Index + 1 ;
        inc     acc                             ;
        std     y+LocServ_Index,acc     ;
                        ; Jeżeli nie ostał odnaleziony wzorzec w tablicy, to następuje zwiększenie zmiennej
                        ; Index o jeden (pobranie z pamięci do rejestru roboczego, jego inkrementacja i
                        ; odesłanie nowej zawartości do pamięci).
        rjmp    LoSrv_1                 ; END (* LOOP *) ;
                        ; Po czym następuje powrót do kolejnego cyklu pętli.
LoSrv_0 :                               ;
        rcall   _DestrLocalVar          ;END (* LocateService *) ;
        ldi     param,LocServParamSize  ;
        rcall   _CloseCallParam         ;
                        ; Usunięcie obszaru zmiennych lokalnych (w wariancie parametry wywołania na
                        ; stosie oraz zmienne lokalne na stosie) sprowadzają się do wywołania
                        ; bezparametrowej funkcji _DestrLocalVar oraz funkcji _ClocseCallParam z parametrem
                        ; zawartym w rejestrze param.
        ret                                     ;
;-----------------------------------------------------------------------------

Implementacja użytych w powyższym przykładzie procedur do przydzielenia i zwolnienia obszarów pamięci przeznaczonych na zmienne lokalne jest opisana niżej.
Przydzielenie obszaru zmiennym lokalnym jest zrealizowane następująco:

Kod: Zaznacz cały

_CreateLocalVar :                       ; acc = size of local variable
                        ; Przed wywołaniem procedury należy w rejestrze acc umieścić wielkość obszaru
                        ; zmiennych lokalnych.
        pop     acc3                    ;
        pop     acc2                    ;
                        ; Przechowanie w rejestrach roboczych adresu powrotu z bieżącej procedury
                        ; (_CreateLocalVar).
        movw    zl,yl                   ;
                        ; Aktualny stan rejestru adresowego yh:yl (jako wskazanie na obszar parametrów
                        ; wywołania będące rezultatem działania wcześniej wywołanej procedury _OpenCallParam)
                        ; jest zapamiętany w rejestrze zh:zl.
        sub     yl,acc          ;
        sbci    yh,0                    ;
        sbiw    yl,6                    ;
                        ; Obliczenie nowego wskazania dla rejestru yh:yl. Odejmując od aktualnej zawartości
                        ; tego rejestru liczbę określającą wielkość obszaru zmiennych lokalnych (w sensie
                        ; operacji 16-bitowej) oraz stałą 6 (jako sumaryczną zajętość pamięci przeznaczonej
                        ; na adres powrotu, dotychczasową zawartość rejestru yh:yl oraz wskazanie na obszar
                        ; parametrów wywołania, patrz rysunek 1) uzyskany jest adres początku obszaru
                        ; przeznaczonego na zmienne lokalne.
        in      acc4,sreg               ;
        cli                             ;
                        ; W sposób opisany już wcześniej, realizowana jest blokada obsługi jakichkolwiek przerwań.
        std     y+0,zl          ;
        std     y+1,zh          ;
                        ; W odpowiednim miejscu obszaru zmiennych lokalnych zapamiętane jest wskazanie
                        ; na obszar parametrów wywołania procedury lub funkcji (chwilowo przechowywane
                        ; w rejestrze zh:zl).
        movw    zl,yl                   ;
        sbiw    zl,1                    ;
                        ; Obliczenie nowego wskazania dla wskaźnika stosu SP, który powinien mieć wartość
                        ; zapisaną w rejestrze yh:yl pomniejszoną o jeden.
        out     sph,zh          ;
        out     spl,zl          ;
                        ; Nowe wskazanie należy zapisać w rejestrach wskaźnika stosu.
        out     sreg,acc4               ;
        push    acc2                    ;
        push    acc3                    ;
        ret                             ;
                        ; Po odtworzeniu stanu wskaźnika zezwolenia na przyjmowanie przerwań, umieszczeniu
                        ; na stosie adresu powrotu z bieżącej procedury następuje powrót do odpowiedniej
                        ; procedury lub funkcji. Efektem działania procedury jest wydzielenie w obrębie stosu
                        ; mikrokontrolera obszaru zawierającego parametry wywołania, którego adres
                        ; pośrednio jest dostępny w obszarze zmiennych lokalnych na pozycji Y+0 i Y+1
                        ; (odpowiednio część młodsza i starsza) oraz wydzielenie obszaru na zmienne
                        ; lokalne, którego początek jest określony zawartością rejestru yh:yl.

Zwolnienie obszaru zmiennych lokalnych zrealizowane jest według poniższego algorytmu. Korzystając ze wskazania na obszar parametrów wywołania zachowanego w zmiennych lokalnych w miejscu Y+0 i Y+1, do jego zwolnienia nie jest konieczna informacja o jego wielkości. Analizując rysunek 1 można zauważyć, że dysponując aktualnym wskazaniem rejestru yh:yl można odtworzyć jego poprzedni stan oraz jednocześnie odtworzyć poprzedni stan rejestru wskaźnika stosu SP.

Kod: Zaznacz cały

_DestrLocalVar :                        ;
                        ; Procedura jest bezparametrowa.
        pop     acc3                    ;
        pop     acc2                    ;
                        ; Przechowanie adresu powrotu z bieżącej procedury w tymczasowych rejestrach roboczych.
        pop     yl                      ;
        pop     yh                      ;
                        ; Po zdjęciu ze stosu adresu powrotu, na jego szczycie dostępne jest wskazanie
                        ; na obszar parametrów wywołania. Po wykonaniu powyższych instrukcji rejestr yh:yl
                        ; wskazuje na obszar parametrów wywołania.
        sbiw    yl,5                    ;
                        ; Pięć bajtów niżej znajduje się pierwsze wolne miejsce do zapisu w operacjach
                        ; z udziałem stosu, czyli wartość wyrażenia Y-5 jest nową zawartością rejestru SP.
        in      acc4,sreg               ;
        cli                             ;
        out     sph,yh          ;
        out     spl,yl          ;
        out     sreg,acc4               ;
                        ; Zgodnie z wymogami niepodzielności wykonania powyższego ciągu instrukcji, zostaje
                        ; wpisany nowy stan do rejestru SP.
        adiw    yl,5                    ;
                        ; Korekta stanu rejestru yh:yl by wskazywał na początek obszaru parametrów wywołania.
        push    acc2                    ;
        push    acc3                    ;
        ret                             ;
                        ; Umieszczenie na stosie adresu powrotu przechowywanego chwilowo w rejestrach
                        ; roboczych i powrót z wywołanej procedury.

Na zakończenia rozważań dotyczących zmiennych lokalnych należy wspomnieć, że w przypadku implementacji funkcji używającej opisane wyżej mechanizmy, istnieje pewna grupa rejestrów, które nie mogą być użyte do przenoszenia wyniku funkcji. Analizując implementację procedury (_DestrLocalVar) można zauważyć, że do przenoszenia wyniku działania funkcji nie należy używać następujących rejestrów: acc2, acc3, acc4, yh, yl. Występująca w parze z procedurą _DestrLocalVar procedura _CloseCallParam do tej listy dokłada kolejne rejestry: acc, param, zh, zl. Najlepszym rejestrem z pozostałego repertuary będzie rejestr należący do pary xh:xl.
Kompletny program wykorzystujący przedstawiony wyżej mechanizm użycia parametrów wywołania i zmiennych lokalnych umiejscowionych w obszarze stosu, w szerszym ujęciu prezentuje poniższy program (zawiera on jedynie elementy istotne). W programie zachodzi potrzeba obsługi zdarzenia związanego z określonym kodem. Implementowane są dwa wywołania: jedno znajduje wzorzec w odpowiedniej tabeli, drugie nie znajduje, w wyniku czego realizowane jest wywołanie „procedury awaryjnej”. Jej implementacja prowadzi do rekurencyjnego wywołania tej samej funkcji z podaniem innej tabeli wzorców.

Kod: Zaznacz cały

;
(...)
        .nolist
;*******************************************************
.include "m8515def.inc"
;*******************************************************
        .list
        .listmac
;-----------------------------------------------------------------------------
.macro  ldz
        ldi     zl,low(@0)
        ldi     zh,high(@0)
.endm
;-----------------------------------------------------------------------------
.macro  ldx
        ldi     xl,low(@0)
        ldi     xh,high(@0)
.endm
;-----------------------------------------------------------------------------
.macro  pushf
        push    acc
        in      acc,sreg
        push    acc
.endm
;-----------------------------------------------------------------------------
.macro  popf
        pop     acc
        out     sreg,acc
        pop     acc
.endm
;-----------------------------------------------------------------------------
.macro  GetCallParamLink
        ldd     zl,y+0
        ldd     zh,y+1
.endm
;-----------------------------------------------------------------------------
;                       = r0 [reserved]
.def acc                = r16
.def acc2               = r17
.def acc3               = r18
.def acc4               = r19
.def param              = r20
;-----------------------------------------------------------------------------
        .cseg
;*******************************************************
;*******************************************************
;**                                                   **
;**                  CODE  SEGMENT                    **
;**                                                   **
;*******************************************************
;*******************************************************
;-----------------------------------------------------------------------------
        .org    0                               ;
        rjmp    ResetProcessor                  ;
;-----------------------------------------------------------------------------
        .org    INT0addr                        ;External Interrupt0 Vector Address
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    INT1addr                        ;External Interrupt1 Vector Address
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    ICP1addr                        ;Input Capture1 Interrupt Vector Address
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    OC1Aaddr                        ;Output Compare1A Interrupt Vector Address
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    OC1Baddr                        ;Output Compare1B Interrupt Vector Address
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    OVF1addr                        ;Overflow1 Interrupt Vector Address
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    OVF0addr                        ;Overflow0 Interrupt Vector Address
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    SPIaddr                 ;SPI Interrupt Vector Address
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    URXCaddr                        ;UART Receive Complete Interrupt Vector
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    UDREaddr                        ;UART Data Register Empty Interrupt Vector
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    UTXCaddr                        ;UART Transmit Complete Interrupt Vector
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    ACIaddr                 ;Analog Comparator Interrupt Vector Address
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    INT2addr                        ;External Interrupt2 Vector Address
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    OC0addr                 ;Output Compare0 Interrupt Vector Address
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    ERDYaddr                        ;EEPROM Interrupt Vector Address
        reti                                    ;
;-----------------------------------------------------------------------------
        .org    SPMaddr                 ;SPM complete Interrupt Vector Address
        reti                                    ;
;-----------------------------------------------------------------------------
.include "util.inc"
;-----------------------------------------------------------------------------
HardwareInit :                          ;PROCEDURE HardwareInit ;
;*************                          ;BEGIN (* HardwareInit *)
        ret                                     ;END (* HardwareInit *) ;
;-----------------------------------------------------------------------------
EnvirInit :                                     ;PROCEDURE EnvirInit ;
;**********                                     ;BEGIN (* EnvirInit *)
        ret                                     ;END (* EnvirInit *) ;
;-----------------------------------------------------------------------------
SoftwareInit :                          ;PROCEDURE SoftwareInit ;
;*************                          ;BEGIN (* SoftwareInit *)
        ret                                     ;END (* SoftwareInit *) ;
;-----------------------------------------------------------------------------
_OpenCallParam :                                ;
;***************                                ;
(...)
;-----------------------------------------------------------------------------
_CloseCallParam :                               ; param = size of call param
;****************                               ;
(...)
;-----------------------------------------------------------------------------
_CreateLocalVar :                               ; acc = size of local variable
;****************                               ;
(...)
_DestrLocalVar :                                ;
;***************                                ;
                        ; Funkcje modyfikujące stan stosu na potrzeby związane z operowaniem w
                        ; obszarze parametrów wywołania oraz zmiennych lokalnych utworzonych w obszarze stosu.
(...)
;-----------------------------------------------------------------------------
NoService:                                      ;PROCEDURE NoService
;***********                            ;
                        ; Procedura, która zostanie wywołana jako „procedury awaryjna” z „awaryjnego wywołania”
                        ; w obrębie procedury ServiceElse. Ta procedura z założenia ma jeden parametr
                        ; przenoszony przez stos, który jest kodem poszukiwanego zdarzenia.
.equ NoServParamSize    = 1     ; =1    ; (
.equ NoServ_Code                = 0     ; +1    ;   Code   : CHAR[stack] ) ;
        rcall   _OpenCallParam          ;BEGIN (* NoService *)
;
;
; ......
        ldi     param,NoServParamSize   ;END (* NoService *) ;
        rcall   _CloseCallParam         ;
                        ; Należy tu zauważyć, że nawet pusta implementacja procedury musi zawierać
                        ; wywołania funkcji modyfikujących stos (ktoś musi zdjąć ze stosu istniejący tam
                        ; parametr wywołania).
        ret                                     ;
;-----------------------------------------------------------------------------
ServiceElse:                            ;PROCEDURE ServiceElse
;***********                            ;
                        ; Procedura, która zostanie wywołana jako „procedura awaryjna”. Zawiera ona
                        ; z założenia jeden parametr przenoszony przez stos, który jest kodem
                        ; poszukiwanego zdarzenia, a który nie został znaleziony dotychczas.
.equ ServElParamSize    = 1     ; =1    ; (
.equ ServEl_Code                = 0     ; +1    ;   Code   : CHAR[stack] ) ;
        rcall   _OpenCallParam          ;BEGIN (* ServiceElse *)
                        ; Jej implementacja zawiera rekurencyjne wywołanie procedury LocateService
                        ; (to z tej funkcji nastąpiło wywołanie do miejsca bieżącego). Zachodzi tu
                        ; przypadek rekurencji niejawnej.
        ldd     acc,y+ServEl_Code               ;  LocateService ( Code ,
        push    acc                             ;
                        ; Po przeniesieniu parametru z bieżącego wywołania do wywołania kolejnej funkcji,
                                                ;                  ExtraCodeTab ,
        ldi     acc,HIGH(ExtraCodeTab<<1)
        push    acc                             ;
        ldi     acc,LOW(ExtraCodeTab<<1);
        push    acc                             ;
                        ; wskazaniu innej tablicy zawierającej wzorce,
                                                ;                  ExtraServiceTab ,
        ldi     acc,HIGH(ExtraServiceTab<<1)
        push    acc                             ;
        ldi     acc,LOW(ExtraServiceTab<<1)
        push    acc                             ;
                        ; innej tablicy zawierającej procedury usługowe,
        ldi     acc,HIGH(NoService)     ;                  NoService ) ;
        push    acc                             ;
        ldi     acc,LOW(NoService)      ;
        push    acc                             ;
                        ; innej „funkcji awaryjnej”
        rcall   LocateService           ;
                        ; następuje wywołanie rekurencyjne procedury LocateService.
        ldi     param,ServElParamSize   ;END (* ServiceElse *) ;
        rcall   _CloseCallParam         ;
                        ; po którym kończone jest działanie bieżącej procedury (były tylko
                        ; parametry na stosie bez zmiennych lokalnych).
        ret                                     ;
;-----------------------------------------------------------------------------
LocateService:                          ;PROCEDURE LocateService
                        ; Opisana szczegółowo wyżej procedura będąca „głównym sprawcą zamieszania”.
;*************                          ;
.equ LocServParamSize   = 7     ; =7    ; (
.equ LocServ_Code               = 6     ; +1    ;   Code      : CHAR[stack] ;
.equ LocServ_CharTable  = 4     ; +2    ;   CharTable : ARRAY OF CHAR[stack] ;
.equ LocServ_ServTable  = 2     ; +2    ;   ServTable : ARRAY OF PROCEDURE()stack] ;
.equ LocServ_ElseServ   = 0     ; +2    ;   ElseServ  : PROCEDURE(CHAR)[stack] ) ;
.equ LocServLocVarSize  = 2             ;VAR
.equ LocServ_Index      = 2     ; +1    ; Index : BYTE[stack] ;
.equ LocServ_TabElem    = 3     ; +1    ; TabElem : BYTE[stack] ;
        rcall   _OpenCallParam          ;
        ldi     acc,LocServLocVarSize   ;
        rcall   _CreateLocalVar         ;BEGIN (* LocateService *)
        ldi     acc,0                           ; Index := 0 ;
        std     y+LocServ_Index,acc     ;
LoSrv_1 :                               ; LOOP
        GetCallParamLink                        ;  TabElem := CharTable [ Index ] ;
        ldd     xl,z+LocServ_CharTable  ;
        ldd     xh,z+LocServ_CharTable+1;
        ldd     acc,y+LocServ_Index     ;
        add     xl,acc                  ;
        ldi     acc,0                           ;
        adc     xh,acc                  ;
        movw    zl,xl                           ;
        lpm     acc,z                           ;
        std     y+LocServ_TabElem,acc   ;
        cpi     acc,0xFF                        ;  IF TabElem = 0xFF THEN
        brne    LoSrv_2                 ;  BEGIN
        GetCallParamLink                        ;   IF ElseServ <> NIL THEN
        ldd     xl,z+LocServ_ElseServ   ;
        ldd     xh,z+LocServ_ElseServ+1 ;
        mov     acc,xl                  ;
        or      acc,xh                  ;
        breq    LoSrv_4                 ;   BEGIN
        ldd     acc,z+LocServ_Code      ;    ElseServ ( Code ) ;
        push    acc                             ;
        movw    zl,xl                           ;
        icall                                   ;
LoSrv_4 :                               ;   END (* IF *) ;
        rjmp    LoSrv_0                 ;   RETURN ;
LoSrv_2 :                               ;  END (* IF *) ;
        GetCallParamLink                        ;  IF TabElem = Code THEN
        ldd     acc2,z+LocServ_Code     ;
        ldd     acc,y+LocServ_TabElem   ;
        cp      acc,acc2                        ;
        brne    LoSrv_3                 ;  BEGIN
        ldd     xl,z+LocServ_ServTable  ;   ServTable [ Index ] ( ) ;
        ldd     xh,z+LocServ_ServTable+1;
        ldd     acc,y+LocServ_Index     ;
        ldi     acc2,0                  ;
        lsl     acc                             ;
        rol     acc2                            ;
        add     xl,acc                  ;
        adc     xh,acc2                 ;
        movw    zl,xl                           ;
        lpm     acc,z+                  ;
        lpm     acc2,z                  ;
        mov     zl,acc                  ;
        mov     zh,acc2                 ;
        icall                                   ;
        rjmp    LoSrv_0                 ;   RETURN ;
LoSrv_3 :                               ;  END (* IF *) ;
        ldd     acc,y+LocServ_Index     ;  Index := Index + 1 ;
        inc     acc                             ;
        std     y+LocServ_Index,acc     ;
        rjmp    LoSrv_1                 ; END (* LOOP *) ;
LoSrv_0 :                               ;
        rcall   _DestrLocalVar          ;END (* LocateService *) ;
        ldi     param,LocServParamSize  ;
        rcall   _CloseCallParam         ;
        ret                                     ;
;-----------------------------------------------------------------------------
                        ; Poszczególne bezparametrowe funkcje reakcji na zdarzenia.
Service0:                                       ;PROCEDURE Service0 ( ) ;
;********                                       ;BEGIN (* Service0 *)
        ret                                     ;END (* Service0 *) ;
;-----------------------------------------------------------------------------
Service1:                                       ;PROCEDURE Service1 ( ) ;
;********                                       ;BEGIN (* Service1 *)
        ret                                     ;END (* Service1 *) ;
;-----------------------------------------------------------------------------
Service2:                                       ;PROCEDURE Service2 ( ) ;
;********                                       ;BEGIN (* Service2 *)
        ret                                     ;END (* Service2 *) ;
;-----------------------------------------------------------------------------
Service3:                                       ;PROCEDURE Service3 ( ) ;
;********                                       ;BEGIN (* Service3 *)
        ret                                     ;END (* Service3 *) ;
;-----------------------------------------------------------------------------
Service4:                                       ;PROCEDURE Service4 ( ) ;
;********                                       ;BEGIN (* Service4 *)
        ret                                     ;END (* Service4 *) ;
;-----------------------------------------------------------------------------
Service5:                                       ;PROCEDURE Service5 ( ) ;
;********                                       ;BEGIN (* Service5 *)
        ret                                     ;END (* Service5 *) ;
;-----------------------------------------------------------------------------
ExtraService0:                          ;PROCEDURE ExtraService0 ( ) ;
;************                           ;BEGIN (* ExtraService0 *)
        ret                                     ;END (* ExtraService0 *) ;
;-----------------------------------------------------------------------------
ExtraService1:                          ;PROCEDURE ExtraService1 ( ) ;
;************                           ;BEGIN (* ExtraService1 *)
        ret                                     ;END (* ExtraService1 *) ;
;-----------------------------------------------------------------------------
ExtraService2:                          ;PROCEDURE ExtraService2 ( ) ;
;************                           ;BEGIN (* ExtraService2 *)
        ret                                     ;END (* ExtraService2 *) ;
;-----------------------------------------------------------------------------
ExtraService3:                          ;PROCEDURE ExtraService3 ( ) ;
;************                           ;BEGIN (* ExtraService3 *)
        ret                                     ;END (* ExtraService3 *) ;
;-----------------------------------------------------------------------------
ExtraService4:                          ;PROCEDURE ExtraService4 ( ) ;
;************                           ;BEGIN (* ExtraService4 *)
        ret                                     ;END (* ExtraService4 *) ;
;-----------------------------------------------------------------------------
ExtraService5:                          ;PROCEDURE ExtraService5 ( ) ;
;************                           ;BEGIN (* ExtraService5 *)
        ret                                     ;END (* ExtraService5 *) ;
;-----------------------------------------------------------------------------
ExtraService6:                          ;PROCEDURE ExtraService6 ( ) ;
;************                           ;BEGIN (* ExtraService6 *)
        ret                                     ;END (* ExtraService6 *) ;
;-----------------------------------------------------------------------------
ExtraService7:                          ;PROCEDURE ExtraService7 ( ) ;
;************                           ;BEGIN (* ExtraService7 *)
        ret                                     ;END (* ExtraService7 *) ;
;-----------------------------------------------------------------------------
ResetProcessor :                                ;
        ldi     acc,HIGH(RAMEND)                ;
        out     SPH,acc                 ;
        ldi     acc,LOW(RAMEND)         ;
        out     SPL,acc                 ;
Main :                                  ;BEGIN (* Main *)
        rcall   ClearRAM                        ; ClearRAM ( ) ;
        rcall   ClearRegs                       ; ClearRegs ( ) ;
        rcall   HardwareInit            ; HardwareInit ( ) ;
        rcall   EnvirInit                       ; EnvirInit ( ) ;
        rcall   SoftwareInit            ; SoftwareInit ( ) ;
        sei                                     ; EI ( ) ;
Main_0  :                               ; LOOP
                        ; Obsługa zdarzeń:
        ldi     acc,'a'                 ;  LocateService ( 'a' ,
        push    acc                             ;
                        ; należy wykonać procedurę związaną ze zdarzeniem o kodzie 'a'
        ldi     acc,HIGH(CodeTable<<1)  ;                  CodeTable ,
        push    acc                             ;
        ldi     acc,LOW(CodeTable<<1)   ;
        push    acc                             ;
                        ; posiłkując się tabelą wzorców CodeTable,
        ldi     acc,HIGH(ServiceTable<<1);                  ServiceTable ,
        push    acc                             ;
        ldi     acc,LOW(ServiceTable<<1);
        push    acc                             ;
                        ; tabelą procedur obsługi ServiceTable,
        ldi     acc,HIGH(ServiceElse)   ;                  ServiceElse ) ;
        push    acc                             ;
        ldi     acc,LOW(ServiceElse)    ;
        push    acc                             ;
                        ; z podaniem procedury awaryjnej ServiceElse.
        rcall   LocateService           ;
                        ; Łatwo zauważyć, że kod 'a' znajduje się tabeli CodeTable, więc powinno
                        ; dojść do wywołania procedury Service1.
                        ; W kolejnym wywołaniu ...
        ldi     acc,'X'                 ;  LocateService ( 'X' ,
        push    acc                             ;
        ldi     acc,HIGH(CodeTable<<1)  ;                  CodeTable ,
        push    acc                             ;
        ldi     acc,LOW(CodeTable<<1)   ;
        push    acc                             ;
        ldi     acc,HIGH(ServiceTable<<1);                  ServiceTable ,
        push    acc                             ;
        ldi     acc,LOW(ServiceTable<<1);
        push    acc                             ;
        ldi     acc,HIGH(ServiceElse)   ;                  ServiceElse ) ;
        push    acc                             ;
        ldi     acc,LOW(ServiceElse)    ;
        push    acc                             ;
        rcall   LocateService           ;
                        ; ... kod zdarzenia 'X' nie występuje we wskazanej w parametrach  wywołania
                        ; tablicy CodeTable, co prowadzi do wywołania „procedury awaryjnej” ServiceElse.
                        ; Patrząc na jej implementację widać, że nastąpi rekurencyjne wywołanie z
                        ; podaniem innej tablicy ExtraCodeTable, która również nie zawiera wzorca
                        ; (nie występuje 'X'), więc dojdzie do kolejnego wywołania „procedury awaryjnej”.
        rjmp    Main_0                  ; END (* LOOP *) ;
                                                ;END (* Main *) ;
;-----------------------------------------------------------------------------
CodeTable:              .db     'A' , 'a'               ; inx = 0, inx = 1
                        .db     'B' , 'b'               ; inx = 2, inx = 3
                        .db     'C' , 'c'               ; inx = 4, inx = 5
                        .db     0xFF , 0                ;
ServiceTable:   .dw     Service0                ; inx = 0
                        .dw     Service1                ; inx = 1
                        .dw     Service2                ; inx = 2
                        .dw     Service3                ; inx = 3
                        .dw     Service4                ; inx = 4
                        .dw     Service5                ; inx = 5
;-----------------------------------------------------------------------------
ExtraCodeTab:   .db     '0' , '1'               ; inx=0, inx=1
                        .db     '2' , '3'               ; inx=2, inx=3
                        .db     '4' , '5'               ; inx=4, inx=5
                        .db     '6' , '7'               ; inx=6, inx=7
                        .db     0xFF , 0                ;
ExtraServiceTab:        .dw     ExtraService0   ; inx = 0
                        .dw     ExtraService1   ; inx = 1
                        .dw     ExtraService2   ; inx = 2
                        .dw     ExtraService3   ; inx = 3
                        .dw     ExtraService4   ; inx = 4
                        .dw     ExtraService5   ; inx = 5
                        .dw     ExtraService6   ; inx = 6
                        .dw     ExtraService7   ; inx = 7
;-----------------------------------------------------------------------------
.exit


Teraz, po napisaniu programu pozostaje go skompilować i odpalić symulator by sprawdzić działanie opisanego wyżej algorytmu. Ja odpaliłem .... zadziałało.

Załącznik:
avrasm_p3.zip
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

PeterB314
Posty: 8
Rejestracja: środa 20 mar 2019, 08:08

Re: [ASM][AVR] Inne spojrzenie

Postautor: PeterB314 » środa 20 mar 2019, 08:23

Witam, to mój pierwszy post.
Dłubie trochę w AVR asm, Świetny artykuł. Wielkie podziękowania!
Głównie AVR8 ASM, Próbowałem PIC32 ale C mi jakoś nie leży...

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

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » czwartek 21 mar 2019, 21:53

Cieszę się niezmiernie, że się podobało. Nawet dla jednej osoby warto się potrudzić.

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

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

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » poniedziałek 21 cze 2021, 17:48

Budowa programu dla AVR'ów

Jak zauważył j23, miksowanie programów napisanych w C i w ASM lub
realizacja wstawek asmowych może być czasami pomocna (właściwie należy
stwierdzić, że czasami jest niezbędna). Wymaga to pewnego zrozumienia
filozofii całości. By móc się poruszać w tym obszarze, to trzeba poznać "przeciwnika".


Program, w sensie kodu binarnego dla pamięci FLASH mikrokontolera, jest sumą kilku różnych kawałków. Jednym z nich jest wygenerowany przez kompilator odpowiednik naszego programu napisanego przykładowo w C. Do pozostałych elementów tego spisu składowych należą między innymi procedury biblioteczne, dołączane jawnie przez twórcę programu jak i elementy niejawne, takie które są, ale ich nie widać. Wręcz klasycznym przykładem takiego składnika jest startup – ta część programu, która stanowi procedurę rozruchową dla tworzonego programu. Finalny produkt, jakim jest kod programu przeznaczony do umieszczenia w pamięci FLASH jest dziełem linkera. To on pracowicie składa wszystkie kawałeczki do kupy. Lista składowych może być całkiem spora.
Wygenerowany przez kompilator plik typu object (taki, który zawiera instrukcje przetłumaczone z języka przykładowo C na język maszynowy dla danego procka) składa się z sekcji. Może być ich kilka, gdyż przede wszystkim każda przestrzeń adresowa to oddzielna sekcja. Tu należy to właściwie zrozumieć. Zapis jakiejś funkcji to ciąg instrukcji, które przetwarzają dane, czyli posiłkują się aktywnie bądź pasywnie informacją w pamięci RAM. Już można tu wyróżnić dwie przestrzenie adresowe: przestrzeń na kod programu oraz przestrzeń na zmienne robocze. W przypadku procków AVR te przestrzenie są technicznie rozdzielone (w końcu to architektura typu Harvard), ale nawet jeżeli nie są, to nie zmienia to koncepcji. Każda przestrzeń jest adresowana niezależnie.
Normalny program, czyli te kawałeczki, które wyszły spod naszej ręki, wchodzą do sekcji określanej jako .text (jakaś nazwa zaczynająca się od znaku kropki). Jeżeli program znajduje się w wielu plikach, to kompilacja każdego z nich generuje ekwiwalentny plik typu object, w którym raczej należy spodziewać się sekcji .text (mogą wystąpić inne, wystarczy, że w pliku wystąpią zmienne globalne). Zadaniem linkera jest pozbieranie w różnych plików object wszystkich takich „sierot” i potworzenie „rodzin”, stworzenie powiązań między nimi i uzyskanie produktu finalnego jakim jest przykładowo plik typu hex. Taką istotną sekcją w programie jest sekcja .init. To ona zawiera kod rozruchowy programu dla procka. Pisząc program nikt nie zawraca sobie głowy przykładowo zainicjowaniem stosu, czy wyzerowaniem zmiennych w pamięci RAM. Tym zajmuje się właśnie część rozruchowa. Mając na względzie mnogość modeli mikrokontrolerów AVR, właściwie dla każdego z nich należy przygotować dedykowaną procedurą inicjującą. Jednak jak przyjrzeć się temu dokładniej, to może nie koniecznie. Jest zrozumiałe, że w niektórych przypadkach muszą wystąpić różnice. Wystarczy wskazać przykładowo na układ ATMEGA8515 i ATTINY2313. Mają one różniej wielkości wskaźniki stosu: dla ATM8515 jest to rejestr 16-bitowy , dla TINY2313 jest to rejestr 8-bitowy. To oczywista wnosi kolejne perturbacje w generacji kodu realizujących czynności zapisane w funkcjach.
Jak się przyjrzeć tematyce, to nie jest tak tragicznie, jak wygląda to na pierwszy rzut oka. Sekcja .init została podzielona na podsekcje: od .init0 do .init9. Linker składa poszczególne podsekcje w rosnącej kolejności. Wystarczy, że gdziekolwiek w objectach wystąpią poszczególne podsekcje, linker to poprzestawia i połączy we właściwej kolejności. Jak wspomniałem wcześniej, w standardowej sekcji .init występują podsekcje. Część z nich ma już przyporządkowana funkcję i tak:
  • sekcja .init0 zawiera kod wykonywany bezpośrednio po sygnale reset,
  • sekcja .init2 zawiera zainicjowanie rejestru stosu,
  • sekcja .init4 zawiera czynności związane z zainicjowaniem zmiennych globalnych i lokalnych zmiennych statycznych na wymaganą wartość początkową,
  • sekcja .init9 zawiera wywołanie funkcji main.
Pozostałe podsekcje .init są wolne i mogą być wykorzystane.
Bardzo podobnie (w sensie koncepcyjnym) jest zorganizowana sekcja .fini kończąca działanie programu. Podobnie składa się z podsekcji od .fini0 do .fini9, z tym, że podsekcje .fini są linkowane w kolejności malejącej. Standardową implementacją jest zatrzymanie programu w nieskończonej pętli i jest to zawarte w podsekcji .fini0.
Dołączenie funkcji napisanej przykładowo w C do procedury inicjującej jest trochę specyficzne: nie jest realizowane jako wywołanie funkcji a wstawienie kodu do odpowiedniego miejsca. To trochę dziwna akcja, ale jak się temu przyjrzeć, wszystko ma swój sens. W ogólnym przypadku, należy sekcję rozruchową traktować jako fragment o ograniczonych możliwościach. Przykładowo w niektórych podsekcjach nie jest zainicjowany stos, więc nie można funkcji wywołać instrukcją call (lub jakąkolwiek jej modyfikacją), gdyż ta okłada na stos adres powrotu. Stos co prawda jest zainicjowany w podsekcji .init2, więc w kolejnych można by już go użyć, ale zasada jest zasadą. Poszczególne kawałeczki procedur inicjujących są składane by tworzyły jednolity, ciągły blok, gdzie sterowanie w sposób naturalny będzie przechodzić z każdej podsekcji do kolejnej. Skoro funkcje sekcji .init nie są wywoływane, to nie mogą zawierać instrukcji ret (powrotu do kolejnej instrukcji za instrukcją call). Występują trochę sprzeczne wymagania i na każdą koncepcję można zaproponować antykoncepcję. Istnieje w języku odpowiednie zaklęcie, które informuje kompilator, by ten pominął w funkcji wygenerowanie prologu (specyficznego kodu związanego z wejściem do funkcji jak choćby zabezpieczenie zawartości wykorzystanych rejestrów) i epilogu (specyficznego kodu związanego z wyjściem z funkcji oraz powrót do poprzedniej zawartości popsutych rejestrów i rzecz jasna instrukcji ret). Tym zaklęciem jest __naked__ atrybut informujący, że funkcja jest goła: zawiera jedynie realizację zapisanych instrukcji bez żadnych fragmentów ukrytych.
Rozpatrzmy przykład, program dla ATMEGA8515, który nic nie robi, ale ma skonfigurowane porty jako porty wyjściowe (po sygnale reset, wszystkie porty są ustawione jako wejściowe). Ta akcja jest zrealizowana przez funkcję napisaną w C o nazwie Init i jest ona dołączona jako podsekcja .init7 do kodu programu. Pomimo, że nigdzie nie występuje jawne jej wywołanie, jest ona wykonana jednorazowo jeszcze zanim zostanie wywołana funkcja main. By to dobrze zadziałało, musi być ona opatrzona atrybutem __naked__ oraz umieszczona w sekcji o nazwie .init7. Komplet wymaganych zaklęć jest umieszczony przy deklaracji prototypu funkcji.

Kod: Zaznacz cały

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


void Init ( void ) __attribute__ ( ( __naked__ ) )
                   __attribute__ ( ( section ( ".init7" ) ) ) ;

void Init ( void )
{
  DDRA = 0xff ;
  DDRB = 0xff ;
  DDRC = 0xff ;
  DDRD = 0xFF ;
}

int main ( void )
{
  uint16_t Variable ;
  /* ------------------- */
  Variable = 0 ;
  for ( ; ; )
  {
    Variable ++ ;
  } /* for */ ;
} /* main */
Po kompilacji można uruchomić symulator procka i puścić program krokowo. Tuż po wejściu do funkcji main, porty są już zdefiniowane jako wyjściowe (rejestry DDRx zawierają wartości 0xFF).
sym.PNG
Po disasemblacji widać, dlaczego tak się stało:

Kod: Zaznacz cały

---- UNKNOWN_FILE ---------------------------------------------------------------------------------
0: File not found
+00000000:   C010        RJMP      PC+0x0011      Relative jump
+00000001:   C027        RJMP      PC+0x0028      Relative jump
+00000002:   C026        RJMP      PC+0x0027      Relative jump
+00000003:   C025        RJMP      PC+0x0026      Relative jump
+00000004:   C024        RJMP      PC+0x0025      Relative jump
+00000005:   C023        RJMP      PC+0x0024      Relative jump
+00000006:   C022        RJMP      PC+0x0023      Relative jump
+00000007:   C021        RJMP      PC+0x0022      Relative jump
+00000008:   C020        RJMP      PC+0x0021      Relative jump
+00000009:   C01F        RJMP      PC+0x0020      Relative jump
+0000000A:   C01E        RJMP      PC+0x001F      Relative jump
+0000000B:   C01D        RJMP      PC+0x001E      Relative jump
+0000000C:   C01C        RJMP      PC+0x001D      Relative jump
+0000000D:   C01B        RJMP      PC+0x001C      Relative jump
+0000000E:   C01A        RJMP      PC+0x001B      Relative jump
+0000000F:   C019        RJMP      PC+0x001A      Relative jump
+00000010:   C018        RJMP      PC+0x0019      Relative jump
+00000011:   2411        CLR       R1             Clear Register
+00000012:   BE1F        OUT       0x3F,R1        Out to I/O location
+00000013:   E5CF        LDI       R28,0x5F       Load immediate
+00000014:   E0D2        LDI       R29,0x02       Load immediate
+00000015:   BFDE        OUT       0x3E,R29       Out to I/O location
+00000016:   BFCD        OUT       0x3D,R28       Out to I/O location
@00000017: Init
---- teset2.c -------------------------------------------------------------------------------------
11:       {
+00000017:   E3EA        LDI       R30,0x3A       Load immediate
+00000018:   E0F0        LDI       R31,0x00       Load immediate
+00000019:   EF8F        SER       R24            Set Register
+0000001A:   8380        STD       Z+0,R24        Store indirect with displacement
13:          DDRB = 0xff ;
+0000001B:   E3E7        LDI       R30,0x37       Load immediate
+0000001C:   E0F0        LDI       R31,0x00       Load immediate
+0000001D:   EF8F        SER       R24            Set Register
+0000001E:   8380        STD       Z+0,R24        Store indirect with displacement
14:          DDRC = 0xff ;
+0000001F:   E3E4        LDI       R30,0x34       Load immediate
+00000020:   E0F0        LDI       R31,0x00       Load immediate
+00000021:   EF8F        SER       R24            Set Register
+00000022:   8380        STD       Z+0,R24        Store indirect with displacement
15:           DDRD = 0xFF ;
+00000023:   E3E1        LDI       R30,0x31       Load immediate
+00000024:   E0F0        LDI       R31,0x00       Load immediate
+00000025:   EF8F        SER       R24            Set Register
+00000026:   8380        STD       Z+0,R24        Store indirect with displacement
16:       }
+00000027:   D002        RCALL     PC+0x0003      Relative call subroutine
+00000028:   C00E        RJMP      PC+0x000F      Relative jump
+00000029:   CFD6        RJMP      PC-0x0029      Relative jump
@0000002A: main
19:       {
+0000002A:   93DF        PUSH      R29            Push register on stack
+0000002B:   93CF        PUSH      R28            Push register on stack
+0000002C:   D000        RCALL     PC+0x0001      Relative call subroutine
+0000002D:   B7CD        IN        R28,0x3D       In from I/O location
+0000002E:   B7DE        IN        R29,0x3E       In from I/O location
23:         Variable = 0 ;
+0000002F:   821A        STD       Y+2,R1         Store indirect with displacement
+00000030:   8219        STD       Y+1,R1         Store indirect with displacement
26:           Variable ++ ;
+00000031:   8189        LDD       R24,Y+1        Load indirect with displacement
+00000032:   819A        LDD       R25,Y+2        Load indirect with displacement
+00000033:   9601        ADIW      R24,0x01       Add immediate to word
+00000034:   839A        STD       Y+2,R25        Store indirect with displacement
+00000035:   8389        STD       Y+1,R24        Store indirect with displacement
+00000036:   CFFA        RJMP      PC-0x0005      Relative jump
26:           Variable ++ ;
+00000037:   94F8        CLI                      Global Interrupt Disable
+00000038:   CFFF        RJMP      PC-0x0000      Relative jump
+00000039:   81FF        LDD       R31,Y+7        Load indirect with displacement
+0000003A:   9601        ADIW      R24,0x01       Add immediate to word
+0000003B:   839A        STD       Y+2,R25        Store indirect with displacement
+0000003C:   8389        STD       Y+1,R24        Store indirect with displacement
+0000003D:   CFFA        RJMP      PC-0x0005      Relative jump
+0000003E:   94F8        CLI                      Global Interrupt Disable
+0000003F:   CFFF        RJMP      PC-0x0000      Relative jump
Tu również łatwo jest przekonać się o bolesności niektórych pomyłek: zapomnienie o atrybucie __naked__ wygenerowało standardowy prolog i epilog dla funkcji Init. Program w postaci:

Kod: Zaznacz cały

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


//void Init ( void ) __attribute__ ( ( __naked__ ) )
//                   __attribute__ ( ( section ( ".init7" ) ) ) ;
void Init ( void ) __attribute__ ( ( section ( ".init7" ) ) ) ;

void Init ( void )
{
   DDRA = 0xff ;
   DDRB = 0xff ;
   DDRC = 0xff ;
    DDRD = 0xFF ;
}


int main ( void )
{
  uint16_t Variable ;
  /* ------------------- */
  Variable = 0 ;
  for ( ; ; )
  {
    Variable ++ ;
  } /* for */ ;
} /* main */
puszczony pod debugerem sprawia wrażenie, że się spętlił: ciągle wykonuje instrukcje procedury Init. Prawdę ujawnia bliższe spojrzenie nawet na plik o rozszerzenie .lss.

Kod: Zaznacz cały

teset1.elf:     file format elf32-avr

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000080  00000000  00000000  00000054  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .debug_aranges 00000028  00000000  00000000  000000d4  2**0
                  CONTENTS, READONLY, DEBUGGING
  2 .debug_pubnames 00000024  00000000  00000000  000000fc  2**0
                  CONTENTS, READONLY, DEBUGGING
  3 .debug_info   000000a8  00000000  00000000  00000120  2**0
                  CONTENTS, READONLY, DEBUGGING
  4 .debug_abbrev 00000074  00000000  00000000  000001c8  2**0
                  CONTENTS, READONLY, DEBUGGING
  5 .debug_line   000000cf  00000000  00000000  0000023c  2**0
                  CONTENTS, READONLY, DEBUGGING
  6 .debug_frame  00000030  00000000  00000000  0000030c  2**2
                  CONTENTS, READONLY, DEBUGGING
  7 .debug_str    00000087  00000000  00000000  0000033c  2**0
                  CONTENTS, READONLY, DEBUGGING
  8 .debug_ranges 00000020  00000000  00000000  000003c3  2**0
                  CONTENTS, READONLY, DEBUGGING

Disassembly of section .text:

00000000 <__vectors>:
   0:   10 c0          rjmp   .+32        ; 0x22 <__ctors_end>
   2:   2e c0          rjmp   .+92        ; 0x60 <__bad_interrupt>
   4:   2d c0          rjmp   .+90        ; 0x60 <__bad_interrupt>
   6:   2c c0          rjmp   .+88        ; 0x60 <__bad_interrupt>
   8:   2b c0          rjmp   .+86        ; 0x60 <__bad_interrupt>
   a:   2a c0          rjmp   .+84        ; 0x60 <__bad_interrupt>
   c:   29 c0          rjmp   .+82        ; 0x60 <__bad_interrupt>
   e:   28 c0          rjmp   .+80        ; 0x60 <__bad_interrupt>
  10:   27 c0          rjmp   .+78        ; 0x60 <__bad_interrupt>
  12:   26 c0          rjmp   .+76        ; 0x60 <__bad_interrupt>
  14:   25 c0          rjmp   .+74        ; 0x60 <__bad_interrupt>
  16:   24 c0          rjmp   .+72        ; 0x60 <__bad_interrupt>
  18:   23 c0          rjmp   .+70        ; 0x60 <__bad_interrupt>
  1a:   22 c0          rjmp   .+68        ; 0x60 <__bad_interrupt>
  1c:   21 c0          rjmp   .+66        ; 0x60 <__bad_interrupt>
  1e:   20 c0          rjmp   .+64        ; 0x60 <__bad_interrupt>
  20:   1f c0          rjmp   .+62        ; 0x60 <__bad_interrupt>

00000022 <__ctors_end>:
  22:   11 24          eor   r1, r1
  24:   1f be          out   0x3f, r1   ; 63
  26:   cf e5          ldi   r28, 0x5F   ; 95
  28:   d2 e0          ldi   r29, 0x02   ; 2
  2a:   de bf          out   0x3e, r29   ; 62
  2c:   cd bf          out   0x3d, r28   ; 61

0000002e <Init>:
//void Init ( void ) __attribute__ ( ( __naked__ ) )
//                   __attribute__ ( ( section ( ".init7" ) ) ) ;
void Init ( void ) __attribute__ ( ( section ( ".init7" ) ) ) ;

void Init ( void )
{
  2e:   df 93          push   r29
  30:   cf 93          push   r28
  32:   cd b7          in   r28, 0x3d   ; 61
  34:   de b7          in   r29, 0x3e   ; 62
   DDRA = 0xff ;
  36:   ea e3          ldi   r30, 0x3A   ; 58
  38:   f0 e0          ldi   r31, 0x00   ; 0
  3a:   8f ef          ldi   r24, 0xFF   ; 255
  3c:   80 83          st   Z, r24
   DDRB = 0xff ;
  3e:   e7 e3          ldi   r30, 0x37   ; 55
  40:   f0 e0          ldi   r31, 0x00   ; 0
  42:   8f ef          ldi   r24, 0xFF   ; 255
  44:   80 83          st   Z, r24
   DDRC = 0xff ;
  46:   e4 e3          ldi   r30, 0x34   ; 52
  48:   f0 e0          ldi   r31, 0x00   ; 0
  4a:   8f ef          ldi   r24, 0xFF   ; 255
  4c:   80 83          st   Z, r24
    DDRD = 0xFF ;
  4e:   e1 e3          ldi   r30, 0x31   ; 49
  50:   f0 e0          ldi   r31, 0x00   ; 0
  52:   8f ef          ldi   r24, 0xFF   ; 255
  54:   80 83          st   Z, r24
}
  56:   cf 91          pop   r28
  58:   df 91          pop   r29
  5a:   08 95          ret
  5c:   02 d0          rcall   .+4         ; 0x62 <main>
  5e:   0e c0          rjmp   .+28        ; 0x7c <_exit>

00000060 <__bad_interrupt>:
  60:   cf cf          rjmp   .-98        ; 0x0 <__vectors>

00000062 <main>:


int main ( void )
{
  62:   df 93          push   r29
  64:   cf 93          push   r28
  66:   00 d0          rcall   .+0         ; 0x68 <main+0x6>
  68:   cd b7          in   r28, 0x3d   ; 61
  6a:   de b7          in   r29, 0x3e   ; 62
  uint16_t Variable ;
  /* ------------------- */
  Variable = 0 ;
  6c:   1a 82          std   Y+2, r1   ; 0x02
  6e:   19 82          std   Y+1, r1   ; 0x01
  for ( ; ; )
  {
    Variable ++ ;
  70:   89 81          ldd   r24, Y+1   ; 0x01
  72:   9a 81          ldd   r25, Y+2   ; 0x02
  74:   01 96          adiw   r24, 0x01   ; 1
  76:   9a 83          std   Y+2, r25   ; 0x02
  78:   89 83          std   Y+1, r24   ; 0x01
  7a:   fa cf          rjmp   .-12        ; 0x70 <main+0xe>

0000007c <_exit>:
  7c:   f8 94          cli

0000007e <__stop_program>:
  7e:   ff cf          rjmp   .-2         ; 0x7e <__stop_program>
Funkcja Init ma swój prolog oraz epilog. O ile prolog nie jest wnosi poważnych perturbacji, to epilog to gwóźdź do trumny: zawiera instrukcję ret. Łatwo zadać sobie pytanie: dokąd wróci wykonanie programu? Znacznie gorzej jest z odpowiedzią. Na stosie nie ma zapisanego adresu powrotu więc... program się resetnie (tak przynajmniej zachowuje się pod kontrolą debugera). Skoro się resetnie, to znowu zaczyna od początku i po wykonaniu akcji zapisanej w Init znowu się resetnie i tak do usr... śmierci.
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: 1259
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » czwartek 24 cze 2021, 23:31

Poczyniłem w temacie jeszcze jeden ciekawy eksperyment: rozbiłem własną procedurę startupową na dwie części i umieściłem je w różnych plikach. By było ciekawiej, w tych plikach dodałem jakieś instrukcje, które standardowo wchodzą w sekcję .text, a ich implementacja znajduje się również w oddzielnych plikach źródłowych (takie pomieszanie z poplątaniem). Nowy wariant programu sprowadził się do 5 plików o następującej treści:

Kod: Zaznacz cały

include <inttypes.h>
#include <avr/io.h>
#include "incl1.h"
#include "incl2.h"

int main ( void )
{
  uint16_t Variable ;
  /* ------------------- */
  Variable = 0 ;
  for ( ; ; )
  {
    Set1 ( ) ;
    Set2 ( ) ;
    Variable ++ ;
  } /* for */ ;
} /* main */


Kod: Zaznacz cały

#ifndef _init1_

#define _init1_

extern void Init1 ( void ) __attribute__ ( ( __naked__ ) )
                           __attribute__ ( ( section ( ".init7" ) ) ) ;

 
extern void Set1 ( void ) ;

#endif


Kod: Zaznacz cały

#include <inttypes.h>
#include <avr/io.h>
#include "incl1.h"


void Init1 ( void )
{
  DDRA = 0xff ;
  DDRB = 0xff ;
}

 
void Set1 ( void )
{
  PORTA = 0x55 ;
  PORTB = 0xAA ;
}


Kod: Zaznacz cały

#ifndef _init2_

#define _init2_

extern void Init2 ( void ) __attribute__ ( ( __naked__ ) )
                           __attribute__ ( ( section ( ".init7" ) ) ) ;

 
extern void Set2 ( void ) ;

#endif


Kod: Zaznacz cały

#include <inttypes.h>
#include <avr/io.h>
#include "incl2.h"


void Init2 ( void )
{
  DDRC = 0xff ;
  DDRD = 0xff ;
}

 
void Set2 ( void )
{
  PORTC = 0x33 ;
  PORTD = 0xCC ;
}


Kompilację całości symbolicznie pokazuje rysunek.
komp1.png
Natomiast linker poskładał to wszystko do kupy we właściwy sposób. Oczywiście kawałeczki podsekcji poskładał w jakiejś swojej kolejności: najpierw wystąpił fragment z pliku incl2.c później z pliku incl1.c (czy istnieje wpływ na tę kolejność?).
Można się o tym przekonać przeglądając plik typu lss.

Kod: Zaznacz cały

teset1.elf:     file format elf32-avr

Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         0000005e  00000000  00000000  00000054  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .debug_aranges 00000070  00000000  00000000  000000b2  2**0
                  CONTENTS, READONLY, DEBUGGING
  2 .debug_pubnames 00000065  00000000  00000000  00000122  2**0
                  CONTENTS, READONLY, DEBUGGING
  3 .debug_info   000001a1  00000000  00000000  00000187  2**0
                  CONTENTS, READONLY, DEBUGGING
  4 .debug_abbrev 000000dd  00000000  00000000  00000328  2**0
                  CONTENTS, READONLY, DEBUGGING
  5 .debug_line   000001c1  00000000  00000000  00000405  2**0
                  CONTENTS, READONLY, DEBUGGING
  6 .debug_frame  00000080  00000000  00000000  000005c8  2**2
                  CONTENTS, READONLY, DEBUGGING
  7 .debug_str    000000ae  00000000  00000000  00000648  2**0
                  CONTENTS, READONLY, DEBUGGING
  8 .debug_ranges 00000040  00000000  00000000  000006f6  2**0
                  CONTENTS, READONLY, DEBUGGING

Disassembly of section .text:

00000000 <__vectors>:
   0:   10 c0          rjmp   .+32        ; 0x22 <__ctors_end>
   2:   1d c0          rjmp   .+58        ; 0x3e <__bad_interrupt>
   4:   1c c0          rjmp   .+56        ; 0x3e <__bad_interrupt>
   6:   1b c0          rjmp   .+54        ; 0x3e <__bad_interrupt>
   8:   1a c0          rjmp   .+52        ; 0x3e <__bad_interrupt>
   a:   19 c0          rjmp   .+50        ; 0x3e <__bad_interrupt>
   c:   18 c0          rjmp   .+48        ; 0x3e <__bad_interrupt>
   e:   17 c0          rjmp   .+46        ; 0x3e <__bad_interrupt>
  10:   16 c0          rjmp   .+44        ; 0x3e <__bad_interrupt>
  12:   15 c0          rjmp   .+42        ; 0x3e <__bad_interrupt>
  14:   14 c0          rjmp   .+40        ; 0x3e <__bad_interrupt>
  16:   13 c0          rjmp   .+38        ; 0x3e <__bad_interrupt>
  18:   12 c0          rjmp   .+36        ; 0x3e <__bad_interrupt>
  1a:   11 c0          rjmp   .+34        ; 0x3e <__bad_interrupt>
  1c:   10 c0          rjmp   .+32        ; 0x3e <__bad_interrupt>
  1e:   0f c0          rjmp   .+30        ; 0x3e <__bad_interrupt>
  20:   0e c0          rjmp   .+28        ; 0x3e <__bad_interrupt>

00000022 <__ctors_end>:
  22:   11 24          eor   r1, r1
  24:   1f be          out   0x3f, r1   ; 63
  26:   cf e5          ldi   r28, 0x5F   ; 95
  28:   d2 e0          ldi   r29, 0x02   ; 2
  2a:   de bf          out   0x3e, r29   ; 62
  2c:   cd bf          out   0x3d, r28   ; 61

0000002e <Init2>:
#include <avr/io.h>
#include "incl2.h"


void Init2 ( void )
{
  2e:   8f ef          ldi   r24, 0xFF   ; 255
  30:   84 bb          out   0x14, r24   ; 20
   DDRC = 0xff ;
   DDRD = 0xff ;
  32:   81 bb          out   0x11, r24   ; 17

00000034 <Init1>:
#include <avr/io.h>
#include "incl1.h"


void Init1 ( void )
{
  34:   8f ef          ldi   r24, 0xFF   ; 255
  36:   8a bb          out   0x1a, r24   ; 26
   DDRA = 0xff ;
   DDRB = 0xff ;
  38:   87 bb          out   0x17, r24   ; 23
  3a:   02 d0          rcall   .+4         ; 0x40 <main>
  3c:   0e c0          rjmp   .+28        ; 0x5a <_exit>

0000003e <__bad_interrupt>:
  3e:   e0 cf          rjmp   .-64        ; 0x0 <__vectors>

00000040 <main>:
  uint16_t Variable ;
  /* ------------------- */
  Variable = 0 ;
  for ( ; ; )
  {
    Set1 ( ) ;
  40:   07 d0          rcall   .+14        ; 0x50 <Set1>
    Set2 ( ) ;
  42:   01 d0          rcall   .+2         ; 0x46 <Set2>
  44:   fd cf          rjmp   .-6         ; 0x40 <main>

00000046 <Set2>:
}

 
void Set2 ( void )
{
  PORTC = 0x33 ;
  46:   83 e3          ldi   r24, 0x33   ; 51
  48:   85 bb          out   0x15, r24   ; 21
  PORTD = 0xCC ;
  4a:   8c ec          ldi   r24, 0xCC   ; 204
  4c:   82 bb          out   0x12, r24   ; 18
}
  4e:   08 95          ret

00000050 <Set1>:
}

 
void Set1 ( void )
{
  PORTA = 0x55 ;
  50:   85 e5          ldi   r24, 0x55   ; 85
  52:   8b bb          out   0x1b, r24   ; 27
  PORTB = 0xAA ;
  54:   8a ea          ldi   r24, 0xAA   ; 170
  56:   88 bb          out   0x18, r24   ; 24
}
  58:   08 95          ret

0000005a <_exit>:
  5a:   f8 94          cli

0000005c <__stop_program>:
  5c:   ff cf          rjmp   .-2         ; 0x5c <__stop_program>

W piśmie obrazkowym to:
komp2.png
Symulacja działania programu dała efekt oczekiwany.

Tak przyglądając się całości naszła mnie taka refleksja: właściwie nie jest potrzebne umieszczanie prototypu funkcji inicjującej w pliku nagłówkowym, przecież nigdzie nie wystąpi jej jawne wywołanie, więc nie musi być widziana na zewnątrz pliku. Tak trochę rozum mi wrócił (lepiej późno niż wcale), ale nie chciało mi się już robić zmian w plikach źródłowych, wystarczy o tym pamiętać na przyszłość.
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: 1259
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » czwartek 01 lip 2021, 11:19

Segmenty pamięci RAM

Opisane wyżej segmenty związane z pamięcią programu nie wyczerpują tematyki dotyczącej segmentacji w programie dla procka AVR. Również podobnym zasadom podlegają dane lokowane w pamięci RAM. Standardowa nazwa segmentu to .data (dotyczy to pamięci RAM, bo również komórki lokowane w pamięci EEPROM również są segmentowane). W przypadku pamięci RAM jest również duża różnorodność:
  • są zmienne globalne mające swoją wartość początkową (należą do segmentu .data),
  • są zmienne, które nie są inicjowane (jawnie) i są lokowane do segmentu .bss (przy generacji kodu jakaś część sekcji .init zeruje te zmienne),
  • są obszary danych nie inicjowane (należą do segmentu .noinit),
To tak z grubsza, tematyka jest bardziej rozległa. W tym miejscu chciałem pokazać sposób wpływania na rzeczywistość, czyli jakiego zaklęcia użyć by zmienna trafiła do właściwego miejsca. Dobrym przykładem jest powołanie zmiennej w pamięci zewnętrznej RAM. Wybrane procki AVR (przykładowo ATMEGA8515) mają taką możliwość, że można przyłączyć zewnętrzną pamięć RAM i jej używać. Niby prosta sprawa, ale jakoś należy poinformować kompilator i linker o swoich zamierzeniach. Rozpatrzmy przykład programu, którego zadaniem jest użycie pamięci zewnętrznej. Segment tam lokowany nazwałem .xram. Tekst programu jest następujący:

Kod: Zaznacz cały

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


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

uint16_t Param1 = 0x1234 ;
uint16_t Param2 = 0x5678 ;
uint16_t Param3 __attribute__ ( ( section (".xram" ) ) ) ;

int main ( void )
{
  /* ------------------- */
  Param3 = Param1 + Param2 ;
  for ( ; ; )
  {
    nop ( ) ;
  } /* for */ ;
} /* main */

Kompilacja nie zapowiada jeszcze porażki, dopiero analiza wygenerowanego kodu ujawnia problemy (jak wiadomo, segment danych w procku ATMEGA8515 zaczyna się od adresu 60hex).
Co wygenerował kompilator w kooperacji z linkerem?
komp3.png
Ano… krzaki i problemy. Jednak zanim zaczniemy urągać narzędziu warto przemyśleć własne postępowanie. Stając tak zupełnie z boku i analizując na chłodno generuje się pytanie: a skąd bidulka kompilator razem z linkerem ma wiedzieć o naszych zamiarach? Należy to jakoś wyartykułować. W opcjach projektu jest zakładka pozwalająca na właściwe poczarowanie (Memory settings).
opcje1.png
Klikamy na ADD i w lokacji Memory Type wybieramy SRAM.
opcje2.png
W kolejnym kroku wpisujemy nazwę segmentu (.xram)
opcje3.png
Na samym końcu pozostało poinformować, że naszym życzeniem jest, by położenie segmentu było na adresie 8000 hex.
opcje4.png
Finał czarowania, jest taki, że znana jest adresacja niesfornego segmentu.
opcje5.png
Tym razem kompilacja (i linkowanie) daje właściwy efekt finalny: tak miało być.
komp4.png
Nie po raz pierwszy okazuje się, że jak zwykle istotne są szczegóły.
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
dambo
Expert
Expert
Posty: 645
Rejestracja: czwartek 17 mar 2016, 17:12

Re: [ASM][AVR] Inne spojrzenie

Postautor: dambo » czwartek 01 lip 2021, 16:35

Nie ma żadnego info, że chcemy wrzucić dane do sekcji o której linker nie ma pojęcia?
Nowy blog o tematyce embedded -> https://www.embedownik.pl/

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

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » piątek 02 lip 2021, 21:52

dambo pisze:Nie ma żadnego info, że chcemy wrzucić dane do sekcji o której linker nie ma pojęcia?

No faktycznie, sprawdziłem jeszcze raz. Jeżeli info o segmencie jest dodane do projektu:
wxram.png
to raport z całego procesu jest

Kod: Zaznacz cały

Build started 2.7.2021 at 21:38:51
avr-gcc  -mmcu=atmega8515 -Wall -gdwarf-2 -std=gnu99 -O0 -funsigned-char -funsigned-bitfields -fpack-struct -fshort-enums -MD -MP -MT teset1.o -MF dep/teset1.o.d  -c  ../teset1.c
avr-gcc -mmcu=atmega8515 -Wl,-Map=teset1.map -Wl,-section-start=.xram=0x808000 teset1.o     -o teset1.elf
avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock -R .signature  teset1.elf teset1.hex
avr-objcopy -j .eeprom --set-section-flags=.eeprom="alloc,load" --change-section-lma .eeprom=0 --no-change-warnings -O ihex teset1.elf teset1.eep || exit 0
avr-objdump -h -S teset1.elf > teset1.lss
Build succeeded with 0 Warnings...

Jeżeli odpowiedniej informacji nie ma:
noxram.png
to raport jest

Kod: Zaznacz cały

Build started 2.7.2021 at 21:36:59
avr-gcc  -mmcu=atmega8515 -Wall -gdwarf-2 -std=gnu99 -O0 -funsigned-char -funsigned-bitfields -fpack-struct -fshort-enums -MD -MP -MT teset1.o -MF dep/teset1.o.d  -c  ../teset1.c
avr-gcc -mmcu=atmega8515 -Wl,-Map=teset1.map teset1.o     -o teset1.elf
avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock -R .signature  teset1.elf teset1.hex
avr-objcopy -j .eeprom --set-section-flags=.eeprom="alloc,load" --change-section-lma .eeprom=0 --no-change-warnings -O ihex teset1.elf teset1.eep || exit 0
avr-objdump -h -S teset1.elf > teset1.lss
Build succeeded with 0 Warnings...
Nawet nieuzbrojonym okiem widać, że żadnego info o problemach nie ma.
Oba raporty różnią się jedynie w:
avr-gcc -mmcu=atmega8515 -Wl,-Map=teset1.map -Wl,-section-start=.xram=0x808000 teset1.o -o teset1.elf
avr-gcc -mmcu=atmega8515 -Wl,-Map=teset1.map teset1.o -o teset1.elf


I tyle...
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: 1259
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » wtorek 06 lip 2021, 13:53

Zewnętrzna pamięć RAM

Eksperymenty z wewnętrzna pamięcią RAM są trochę mało interesujące, o wiele bardziej ciekawa okazała się zabawa z zewnętrzną pamięcią RAM. Już wcześniej pisałem jak utworzyć taki segment. Na temat rozwiązań technicznym nie wspominałem, wystarczy pójść drogą opisaną w odpowiednich datasheetach. Idea jest wręcz identyczna jak w przypadku popularnych mikrokontrolerów z rodziny C51. Odpowiednie porty stanowią szynę danych oraz szynę adresową (tu część adresów jest zatrzaskiwana w rejestrze, gdzie wpis następuje dedykowanym sygnałem pochodzącym od procka). Sam zapis lub odczyt danych jest sterowany kolejnymi dedykowanymi sygnałami generowanymi przez proca - nic nowego pod słońcem. Skupiając się nad istotnymi elementami, koncepcję pokazuje poniższy rysunek.
sch1.png

Oczywiści, by móc używać zewnętrzną pamięć RAM konieczne są odpowiednie zaklęcia do odpowiednich portów konfiguracyjnych mikrokontrolera, ale nie to jest elementem istotnym.
Wracając jednak do meritum...
Jest następujący program:

Kod: Zaznacz cały

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


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

uint16_t Param1 = 0x1234 ;
uint16_t Param2 __attribute__ ( ( section (".xram" ) ) ) = 0x5678 ;
uint16_t Param3 ;


int main ( void )
{
  /* ------------------- */
  Param3 = Param1 + Param2 ;
  for ( ; ; )
  {
    nop ( ) ;
  } /* for */ ;
} /* main */

wraz z kompletem właściwych zaklęć dla linkera:
opcje5.png

W programie występuje zmienna z segmentu .data, segmentu .bss i oczywista zmienna z segmentu .xram. Program puszczony pod debugerem zadziałał inaczej niż się spodziewałem.
debug1.png

Zmienna Param1 (adres 60hex → wewnętrzna pamięć RAM) zawiera 4660 dec = 1234 hex (jest OK).
debug2.png

Zmienna Param2 (adres 8000 hex → zewnętrzna pamięć RAM) zawiera 65535 dec=FFFF hex (WTF!).
debug3.png

Zmienna Param3 (adres 62hex → wewnętrzna pamięć RAM) zawiera 4659 dec = 1233 hex (nawet gołym okiem widać, że suma jest krzywa). Obszar zmiennych o inicjowanych zmiennych w pamięci wewnętrznej został zainicjowany. Zmienna Param3 należąca do segmentu .bss została wyzerowana (widać to przed wykonaniem sumowania). A inicjowany obszar w .xram to co, sierota?
Postanowiłem zrobić dokładniejsze badania w temacie. By mieć absolutną pewność, powołałem do istnienia sekcję .init1 (taką, co się wykona między resetem procka a zainicjowaniem stosu).

Kod: Zaznacz cały

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

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

uint16_t Param1 = 0x1234 ;
uint16_t Param2 __attribute__ ( ( section (".xram" ) ) ) = 0x5678 ;
uint16_t Param3 ;

void Init ( void ) __attribute__ ( ( __naked__ ) )
                   __attribute__ ( ( section ( ".init1" ) ) ) ;
void Init ( void )
{
  uint16_t * xramvar ;
  xramvar = ( uint16_t * ) 0x8000 ;
  * xramvar = 0x1111 ;
}

int main ( void )
{
  /* ------------------- */
  Param3 = Param1 + Param2 ;
  for ( ; ; )
  {
    nop ( ) ;
  } /* for */ ;
} /* main */

Jest pewność, że wykonana wystarczająco wcześnie ustali ściśle określone warunki początkowe dla zmiennej w pamięci zewnętrznej RAM (wpisze do zmiennej wartość 0x1111). Stan symulatora po sygnale reset (w widoku assemblerowym), to
debug4.png

Podglądana pamięć wewnętrzna RAM pokazuje stan zmiennych (Param1 i Param3) jako 0xFFFF. Po wykonaniu iluś tam kroków w symulatorze, zmienne zostały zainicjowane do stanu właściwego.
debug5.png

Po realizacji sumowania, do pamięci został wpisany wynik, który nie wynika z zapisów instrukcji w programie. W pamięci zapisało się 2345hex jako suma 1234 hex (wynikające z zainicjowania sekcji w fazie rozruchowej) + 1111 hex (nadane również w fazie rozruchowej, by później stwierdzić, czy to zostało gdziekolwiek zmienione – nie zostało).
debug6.png

Wniosek nasuwa się sam: zmienne inicjowane w zewnętrznej pamięci RAM to sieroty, którymi (by mieć efekt oczekiwany) należy samemu się zaopiekować. No cóż, nie przypominam sobie, bym o tym gdzieś czytał, także prawdziwa wiedza wynika z własnych eksperymentów i badań.
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
dambo
Expert
Expert
Posty: 645
Rejestracja: czwartek 17 mar 2016, 17:12

Re: [ASM][AVR] Inne spojrzenie

Postautor: dambo » wtorek 06 lip 2021, 14:46

Mega fajne smaczki poruszasz :)

Osobiście z zewnętrznej pamięci korzystałem tylko w celu wrzucenia tam fremebuffera (w STM32F7) i nie potrzebowałem tam inicjalizacji zmiennych w niej umieszczonych.

Inicjalizacja zmiennych w zewnętrznej pamięci to ciekawe zagadnienie - domyślnie inicjalizacją zajmuje się etap "initx" przed mainem - w AVRach to było jakoś fajnie rozbite z różnymi numerami init3 itp z czego niektóre są puste przeznaczone dla użytkowników (pamiętam, że w książce T. Francuza o tym jest - tak samo są sekcje co ma się stać jak mikrokontroler opuści/zakończy funkcję main), ale w przypadku zewnętrznej - potrzebowałby jeszcze mieć już zainicjalizowaną pamięć itp. Tu jest też fajny link: http://www.nongnu.org/avr-libc/user-man ... aq_ext_ram - z informacjami, żeby nie wrzucać tam stosu, bo spowolni to działanie itp - też fajny smaczek.
Nowy blog o tematyce embedded -> https://www.embedownik.pl/

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

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » wtorek 06 lip 2021, 16:52

dambo pisze:Osobiście z zewnętrznej pamięci korzystałem tylko w celu wrzucenia tam fremebuffera (w STM32F7) i nie potrzebowałem tam inicjalizacji zmiennych w niej umieszczonych.

W większości przypadków również nie potrzebuję pamięci zewnętrznej, jednak są tematy, które bez xram nie są możliwe do zrealizowania. Przykładowo buduję system, w którym potrzebuję przechować tak między 20 a 30 kB danych. Żaden proc AVR'owy takiej nie posiada. Oczywista, w tym przypadku to obszar pamięci może być nawet .noinit - nie ma to żadnego znaczenia. Niemniej rozkmina jest rozkminą (nie to bym się czepiał kompilarea), ale czasami jest warto o czymś wiedzieć, bo to może zmniejszyć cierpienia twórcze :D .

dambo pisze:... nie wrzucać tam stosu, bo spowolni to działanie itp - też fajny smaczek.

Jest to silnie uzasadnione, dostęp do XRAM zawiera waitstate, więc spowalnia to pracę.

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

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

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » środa 07 lip 2021, 16:22

Obsługa przerwań w asm

… czyli to co lubią tygryski najbardziej. Nie jest żadną tajemnicą, że optymalność kodu programu jest istotnym elementem. Kompiler C radzi sobie całkiem nieźle z tym problemem, ale czasami można zrobić coś lepiej. Procedury obsługi przerwań czasami można zrealizować lepiej (bardziej optymalnie) niż proponuje to kompiler C. Utworzenie funkcji w asm, która będzie „widoczna” w C nie jest wielkim problemem. Podstawowym wymogiem jest, by nazwa funkcji (ogólnie nazwa czegokolwiek) była elementem globalnym (czyli widoczna dla linkera, który tworzy powiązania do innych plików). Obsługa przerwania jako funkcja (w C wszystko jest funkcją tylko czasami wynik funkcji jest „void” a filozoficznie bardziej adekwatne jest określenie rodem z Pascal'a → procedura, gdyż nie generuje wyniku działania) praktycznie niczym nie różni się od innych funkcji. Jedynym elementem istotnym jest to, że zamiast instrukcji ret (normalnego powrotu z funkcji) musi być zastosowana instrukcja reti (powrót z obsługi przerwania). Każdy, kto trochę dłubie w prockach o tam wie. W przypadku obsługi przerwań w mikście C i ASM (w narzędziu GCC) istnieje jeszcze jeden bardzo istotny element: nazewnictwo. O ile funkcję można nazwać dowolnie, to funkcję do obsługi przerwania już nie.
Jednak zanim zanurzymy się o asmowych odmętach, kilka słów o funkcji main w C. By zaprezentować problematykę, jest program samonapędzający się (w sensie przerwań) → obsługa przerwań od timer0 (to gwarancja nieprzerwanego ciągu zdarzeń). Niech zadaniem obsługi przerwania będzie inkrementacja stanu portu A (takie coś łatwo jest obserwować w symulatorze).
Treść programu (właściwie części programu zapisanego w C) jest następująca:

Kod: Zaznacz cały

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


uint16_t Variable ;

int main ( void )
{
  /* ------------------- */
  TCCR0 = ( 1 << CS00 ) ;
  TIMSK = ( 1 << TOIE0 ) ;
  DDRA = 0xFF ;
  PORTA = 0x5A ;
  Variable = 0 ;
  sei();
  for ( ; ; )
  {
    Variable ++ ;
  } /* for */ ;
} /* main */

Implementacja funkcji main zawiera „włączenie” przerwań od przepełnienia timer0 zliczającego impulsy zegarowe bez preskalera (by było szybciej) i konfigurację portu A jako portu wyjściowego z jakimś tam stanem początkowym. Istotnym elementem jest włączenie możliwości przyjmowania przerwań: wywołanie sei(). Pozostałe instrukcje nie mają znaczenia, ot by stały się zajęciem dla proca bo inaczej nieróbstwo go pochłonie bez reszty (można w pętli użyć instrukcji nop → nic nie rób).
Kawałek w asm to:

Kod: Zaznacz cały

#include <avr/io.h>
    .section  .text
    OutPort = PORTA - 0x20
    .global __vector_7
    .type   __vector_7, @function
__vector_7 :
    push  r16
    in    r16,OutPort
    inc   r16
    out   OutPort,r16
    pop   r16
    reti

Generalnie asm ma odmienną syntaktykę od języka C, ale również podlega przetwarzaniu przez preprocesor, chodzi o definicje elementów zawartych w plikach inkludowych. Jest uzasadnione by nie tworzyć tych samych stałych dla asm i dla C oddzielnie. W asm można użyć plików nagłówkowych od C i preprocesor przerobi to na czysty zapis asmowy (tak jak identycznie nasz zapis w C jest przetwarzany do „prawdziwego” C, który jest dopiero kompilowany).
Jak widać algorytm obsługi jest banalnie prosty: wczytanie stanu portu do rejestru R16, jego inkrementacja i później zapis do portu. W ciągu instrukcji występuje rejestr R16 jako rejestr operacyjny. Z tego powodu jest wcześniej zachowany na stosie i odtworzony przed wyjściem z procedury obsługi przerwania. Rzecz jasna akcja kończy się instrukcją reti.

Kwestia syntaktyki

Kod funkcji musi zawierać się w segmencie związanym z kodem programu, stąd zaklęcie .section .text, musi zawierać nazwę funkcji (de facto nazwę etykiety), czyli__vector_7 :, która jest funkcją obsługi (.type __vector_7, @function) i musi być widoczna dla linkera poza tym plikiem .global __vector_7. Jeszcze słowo komentarza do stałej określającej port:OutPort = PORTA - 0x20. Jak popatrzeć na definicję elementu PORTA (w pliku iom8515.h), to jest tam #define PORTA _SFR_IO8(0x1B) z tym, że sama definicja makra _SRF_IO8 dodaje 20 hex do stałej w nawiasie (finalnie _SFR_IO8(0x1B) ma wartość 0x3B). Z tego powodu, we własnej definicji portu (OutPort = PORTA - 0x20) jest odjęte 20 hex. Dlaczego tak jest? No cóż, jest jak jest. W instrukcjach typu LDS, STS jest to poprawne, w instrukcjach IN, OUT jest o 20 hex za dużo (IN, OUT ma 6-bitowy adres komórki, więc jest utrącony o 20 hex → nie da się IN, OUT sięgnąć do rejestrów – po prostu ten typ tak ma).

Kwesta nazewnictwa

Funkcja musi nazywać się __vector_7 (dwa znaki podkreślenia z przodu, człon vector, jeden znak podkreślenia i numer wektora przerwań). W tym konkretnym przypadku jest to przerwanie numer 7.
Po kompilacji, można sobie pooglądać co z tego wyszło:
int1.png

Obsługa przerwania jest umocowana w tabeli przerwań.
Program puszczony w symulatorze (po resecie):
deb42.png

i puszczony na żywioł („dokręcił” się na PORTA do jakiejś tam liczby).
deb41.png

Jeżeli w programie źródłowym (części asmowej) zmieniona zostanie nazwa funkcji na jakąś, przykładowo

Kod: Zaznacz cały

#include <avr/io.h>
    .section  .text
    OutPort = PORTA - 0x20
    .global jakas_funkcja
    .type   jakas_funkcja, @function
jakas_funkcja :
    push  r16
    in    r16,OutPort
    inc   r16
    out   OutPort,r16
    pop   r16
    reti

to nie włączy się ona jako obsługa przerwania (w kodzie owszem będzie, ale nie będzie umocowana w systemie przerwań), co widać oglądając tabelę wektorów (nie ma jej):

Kod: Zaznacz cały

00000000 <__vectors>:
   0:   10 c0          rjmp   .+32        ; 0x22 <__ctors_end>
   2:   1f c0          rjmp   .+62        ; 0x42 <__bad_interrupt>
   4:   1e c0          rjmp   .+60        ; 0x42 <__bad_interrupt>
   6:   1d c0          rjmp   .+58        ; 0x42 <__bad_interrupt>
   8:   1c c0          rjmp   .+56        ; 0x42 <__bad_interrupt>
   a:   1b c0          rjmp   .+54        ; 0x42 <__bad_interrupt>
   c:   1a c0          rjmp   .+52        ; 0x42 <__bad_interrupt>
   e:   19 c0          rjmp   .+50        ; 0x42 <__bad_interrupt>
  10:   18 c0          rjmp   .+48        ; 0x42 <__bad_interrupt>
  12:   17 c0          rjmp   .+46        ; 0x42 <__bad_interrupt>
  14:   16 c0          rjmp   .+44        ; 0x42 <__bad_interrupt>
  16:   15 c0          rjmp   .+42        ; 0x42 <__bad_interrupt>
  18:   14 c0          rjmp   .+40        ; 0x42 <__bad_interrupt>
  1a:   13 c0          rjmp   .+38        ; 0x42 <__bad_interrupt>
  1c:   12 c0          rjmp   .+36        ; 0x42 <__bad_interrupt>
  1e:   11 c0          rjmp   .+34        ; 0x42 <__bad_interrupt>
  20:   10 c0          rjmp   .+32        ; 0x42 <__bad_interrupt>

Jako nazwy funkcji obsługi przerwania można użyć identyfikatora zdefiniowanego w pliku nagłówkowym dla danego proca, czyli:

Kod: Zaznacz cały

#include <avr/io.h>
    .section  .text
    OutPort = PORTA - 0x20
    .global SIG_OVERFLOW0
    .type   SIG_OVERFLOW0, @function
SIG_OVERFLOW0 :
    push  r16
    in    r16,OutPort
    inc   r16
    out   OutPort,r16
    pop   r16
    reti

zadziała poprawnie, gdyż SIG_OVERFLOW0 jest makrodefinicją sprowadzającą się do __vecotr_7. Identycznie jest z nazwą TIMER0_OVF_vect.
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: 1259
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » sobota 10 lip 2021, 14:33

Segmenty danych
raz jeszcze


Chcę wrócić raz jeszcze do segmentów danych w pamięci RAM. Rozumiem, że starym wyjadaczom to nic nowego nie wnosi, ale pamiętając o formule przyświecającej forum, zadbajmy o tych, co znajdują się na początku drogi. Wcześniej było pisane o segmentach .data oraz .bss. Dlaczego dane w tych segmentach mają już na starcie programu (w rzeczywistości funkcji main) jakieś określone dane?
Rozpatrzmy przykładowy program (celowo zostało to rozbite na kilka plików):

Kod: Zaznacz cały

#include <inttypes.h>
#include <avr/io.h>
#include "src1.h"
#include "src2.h"

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

uint8_t Param3 = 'C' ;
uint8_t Param4 ;


int main ( void )
{
  /* ------------------- */
  Param4 = Param1 + Param2 + Param3 ;
  for ( ; ; )
  {
    Fun1 ( ) ;
    Fun2 ( ) ;
  } /* for */ ;
} /* main */

Kompilując powyższy fragment, kompiler widzi jedynie plik nagłówkowy src1.h i scr2.h. Z nich wynika jedynie, że gdzieś tam na świecie jest zmienna typu uint8_t o nazwie Param1 oraz funkcja void Fun1 ( void ) jak i zmienna Param2 z funkcją Fun2 i nic więcej.

Kod: Zaznacz cały

#include <inttypes.h>

extern uint8_t Param1 ;

void Fun1 ( void ) ;


Kod: Zaznacz cały

#include <inttypes.h>


extern uint8_t Param2 ;


void Fun2 ( void ) ;


Te właśnie informacje pochodzą z pliku nagłówkowego src.h i scr2.h.

Kod: Zaznacz cały

#include "src1.h"


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

uint8_t Param1 = 'A' ;

void Fun1 ( void )
{
  nop();
}


Kod: Zaznacz cały

#include "src2.h"


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

uint8_t Param2 = 'B' ;

void Fun2 ( void )
{
  nop();
}

Natomiast „ciało” dodawanych kawałeczków programu jest zawarte w kolejnych plikach źródłowych. Z nich właśnie wynika, że zmienne Param1 oraz Param2 należą do segmentu .data, gdyż są inicjowane określoną wartością początkową. Z punktu widzenia generacji kodu nie ma to żadnego znaczenia (nie są rozróżnialne dane z .data oraz .bss), gdyż wszystko i tak znajduje się w pamięci RAM oraz jest identyfikowane przez swój adres (każdy element ma inny).
W piśmie obrazkowym to wygląda następująco:
ram1.png

oraz
ram2.png

oraz
ram3.png

Kompiler zrobił co do niego należało, teraz czas na pracę dla linkera. Oczywista (linker zauważył), że występuje sekcja .data oraz sekcja .bss. Poskładał to wszystko do kupy, to znaczy utworzył z wszystkich kawałeczków sekcję .data, czyli lity kawałek przestrzeni adresowej pamięci RAM zawierający komórki zmiennych. Jednocześnie utworzył w pamięci FLASH obszar wzorców. Zmienne w sekcji .data są ułożone w jakiejś kolejności (znanej dla linkera). W identycznej kolejności tworzy blok danych w pamięci FLASH zawierający wzorzec inicjacyjny. W sekcji .init4 ten blok jest przepisany do pamięci RAM. Analogicznie zgrupowane są komórki tworzące sekcję .bss, z tym, że ten obszar jest zerowany. Nie ma potrzeby, by został utworzony obszar wzorca.
Obie te akcje (przepisania obszaru wzorca do pamięci RAM do sekcji .data oraz wyzerowania sekcji .bss) jest realizowane w fazie .init4.
W piśmie obrazkowym to można wyobrazić sobie jako (jeżeli ograniczyć się do sekcji .data i .bss):
ram4.png

Puszczając program pod kontrolą symulatora można sprawdzić przedstawione szczegóły.
ram5.png

Po wartościach adresów sygnalizowanych przez symulator można wyciągnąć wniosek, że linker w pierwszej kolejności zajmuje się .data później .bss. W sumie jest to jego problem.
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: 1259
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » czwartek 15 lip 2021, 23:43

Pamięć zewnętrzna
raz jeszcze


Czasami tak się zdarza, że jakiś problem ciągle „męczy”. Taką „męczyduszą” była problematyka inicjowania na określone wartości początkowe zmiennych umieszczonych w zewnętrznej pamięci RAM. Ciągle powracające pytanie: dlaczego nie wyszło?
Rozwiązanie objawiło się nagle i niespodziewanie, jak jakieś olśnienie. W krótkim błysku świadomości wszystko stało się jasne i zrozumiałe. Przecież zmienne, które są inicjowane na określone wartości początkowe należą do segmentu .data, tak jak zmienne automatycznie zerowane znajdują się w segmencie .bss. Utworzone zmienne należały do segmentu .xram. Jakkolwiek na to nie patrzeć, to nie jest segment .data (nawet pisownia jest inna :o ).
Typowe rozwiązanie podziału przestrzeni pamięci wewnętrznej RAM jest następujące (przykładowo dla procka ATMEGA8515):
xram1.png

Na początku wewnętrznej pamięci RAM znajduje się segment .data i tuż za nim jest lokowany segment .bss. Od końca tej przestrzeni adresowej znajduje się miejsce na stos programu. To miejsce jest określone przez stałą RAMEND (którą można znaleźć w pliku iom8515.h - standardowym pliku określającym zasoby danego procka) i w przypadku ATMEGA8515 jest to 25F hex (adres ostatniej komórki dostępnej w pamięci).
Rozwiązaniem postawionego wcześniej problemu nie jest zmuszanie do działań, które niejako nie wchodzą w grę, lecz przeniesienie całego segmentu .data (i .bss) do zewnętrznej pamięci. Mówiąc inaczej należy wyciągnąć określone elementy z wnętrza „internal RAM” i przenieść je do innego świata: „external RAM”. Dążymy do modelu:
xram2.png

Hołdując zasadzie „kto szuka, ten znajdzie”, to właśnie znalazłem (w dokumentacji dotyczącej linkera). Istnieje odpowiednie zaklęcie, które realizuje przedmiotowe przenosiny. Jest to:
-Wl,--section-start,.data=0x808000. Liczba określająca adres w pamięci może nieco zdziwić: jest większa niż 16 bitów. To taki chwyt stosowany w GCC: adres jest podzielony na detale: 0x808000. Wartość 0x80 określa, że dotyczy on przestrzeni pamięci RAM (nie FLASH) oraz0x8000 oznacza realne położenie w przestrzeni. Pozostaje jedynie problem przekazania go do linkera. Na to również znalazł się sposób. Należy wejść w opcje projektu do zakładki „Custom options”, wybrać „Linkier options” i określić własne życzenie:
xram3.png

w odpowiednim polu wpisać treść dodatkowego zaklęcia (i kliknąć na Add).
xram4.png

Weryfikacja koncepcji (każdą jakoś należy sprawdzić) nie jest skomplikowana. Napisałem program (jest tak prosty i przejrzysty, że nie wymaga komentarza):

Kod: Zaznacz cały

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


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

uint16_t Param1 = 0x1234 ;
uint16_t Param2 = 0x5678 ;
uint16_t Param3 ;

int main ( void )
{
  /* ------------------- */
  Param3 = Param1 + Param2 ;
  for ( ; ; )
  {
    nop ( ) ;
  } /* for */ ;
} /* main */

W treści programu nie ma nic nadzwyczajnego, ale „wydruk” z procesu generacji programu ujawnia prawdę: adresy zmiennych dotyczą przestrzeni zewnętrznej pamięci RAM.
xram5.png

Program puszczony pod kontrolą symulatora potwierdza prawdziwość przypuszczeń.
xram6.png

1234 hex + 5678 hex = 68AC hex = 26796 dec. Nic nie zginęło i wszystko jest tam, gdzie być powinno.
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: 1259
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » sobota 17 lip 2021, 16:18

Funkcje w asemblerze

Dosyć istotnym elementem języka C są funkcje. Funkcje, jak to funkcje, spełniają przeróżną rolę. Każda z nich generuje jakiś wynik, który musi zostać jakoś „wyniesiony” na zewnątrz. Realizowane jest to za pośrednictwem rejestrów. W zależności od typu funkcji (a właściwie bardziej precyzyjnie mówiąc: w zależności od „objętości” wyniku funkcji) są używane różne rejestry. W najbardziej prostym przypadku (wynik funkcji zajmuje 1 bajt) jest to rejestr R24. W zależności od „objętości” wyniku są używane następujące rejestry:
  • R24 dla wyników mieszczących się na 8 bitach,
  • R25:R24 (jako część starsza:część młodsza) dla wyników mieszczących się na 16 bitach,
  • R25:R24:R23:R22 (w kolejności od części najstarszej do najmłodszej) dla wyników mieszczących się na 32 bitach,
  • R25:R24:R23:R22:R21:R20:R19:R18 (w kolejności od części najstarszej do najmłodszej) dla wyników mieszczących się na 64 bitach.
Jako najprostszy przykład ilustrujący rozpatrzmy następujący program. Prezentuje on funkcję napisaną w asemblerze mającą dwa argumenty (jednobajtowe), której zadaniem jest obliczenie sumy argumentów i przekazanie jej jako wynik funkcji (również jako 1 bajt). W przykładzie zastosowane jest użycie zmiennej jako parametru wywołania oraz stałej jako parametru wywołania. Ponieważ część asemblerowa nie posiada pliku nagłówkowego (o nazwie *.h) a kompiler musi wiedzieć (no wiedza to podstawa) jak jest skonstruowana funkcja, jej prototyp jest zdefiniowane przed pierwszym jej użyciem (właściwie to nie ma żadnych ograniczeń, by stworzyć plik nagłówkowy i go dołączyć jako #include - każdy chwyt jest dozwolony). Ponieważ funkcja ma dwa argumenty jednobajtowe i wynik jest jednobajtowy, to jej prototyp może wyglądać następująco:
extern uint8_t SumFun ( uint8_t Arg1 , uint8_t Arg2 ) ;
Tu zostało zastosowane „zaklęcie” extern, by kompiler powstrzymał się od swoich uwag dotyczących ciała owej funkcji (zaklęcie to oznacza, że funkcja jest sobie gdzieś). Przykład jest następujący:

Kod: Zaznacz cały

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

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

uint8_t Param1 ;
uint8_t Param2 ;
uint8_t Param3 ;
uint8_t Param4 ;
uint8_t Param5 ;

extern uint8_t SumFun ( uint8_t Arg1 , uint8_t Arg2 ) ;

int main ( void )
{
  /* ------------------- */
  Param1 = '0' ;
  Param2 = '1' ;
  Param3 = SumFun ( '0' , '1' ) ;
  Param4 = SumFun ( Param1 , Param2 ) ;
  Param5 = Param3 + Param4 ;
  for ( ; ; )
  {
    nop();
  } /* for */ ;
} /* main */

Postać asmowa funkcji jest następująca:

Kod: Zaznacz cały

#include <avr/io.h>
    .section  .text
    .global SumFun
// uint8_t SumFun ( uint8_t Arg1 , uint8_t Arg2 ) ;
    .type   SumFun, @function
//              Arg1 - r24 (+r25)
//              Arg2 - r22 (+r23)
SumFun :
    mov   r0 , r22
    add   r24 , r0
    ret

Parametry do funkcji są wniesione via rejestry. W tym przypadku użyty jest R24 jako Arg1 oraz R22 jako Arg2. Wynik funkcji to R24. Wprawne oko zauważy, że wynik „nadpisuje” argument wywołania. W ogólnym przypadku warto jest powołać odpowiednią zmienną lokalną na wynik funkcji i przed wyjściem z funkcji zostawić wynik w wymaganym zestawie rejestrów (jak wspomniane zostało to wyżej). Po kompilacji i linkowaniu programu pora zmierzyć się z rzeczywistością. Do akcji wchodzi symulator procka, który potwierdza poprawność realizacji.
sumfun1.png

Znak '0' to 30 hex = 48 dec, znak '1' to 31 hex, 49 dec. Suma 97 jest poprawnym wynikiem. Analogicznie dla drugiego wariantu wywołania - wszystko się zgadza.
sumfun2.png

Pora przejść na trochę wyższy level. Niech wynik funkcji będzie 32-bitowy a argumenty 16-bitowe. Użycie funkcji to:

Kod: Zaznacz cały

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

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

uint16_t Param1 ;
uint16_t Param2 ;
uint32_t Param3 ;
uint32_t Param4 ;
uint32_t Param5 ;

extern uint32_t SumFun ( uint16_t Arg1 , uint16_t Arg2 ) ;


int main ( void )
{
  /* ------------------- */
  Param1 = 0x8000 ;
  Param2 = 0xFFFF ;
  Param3 = SumFun ( Param1 , Param2 ) ;
  for ( ; ; )
  {
    nop();
  } /* for */ ;
} /* main */

Wartości parametrów są tak dobrane, by wynik (jako suma) przekroczyły 16 bitów. To implikuje trochę bardziej złożoną realizację w asmie:

Kod: Zaznacz cały

#include <avr/io.h>
    .section  .text
    .global SumFun
// uint32_t SumFun ( uint16_t Arg1 , uint16_t Arg2 ) ;
    .type   SumFun, @function
//              Arg1 - r25:r24 (arg1_h:arg1_l)
//              Arg2 - r23:r22 (arg2_h:arg2_l)
//     wynik funkcji - r25:r24:r23:r22 (od MSB do LSB)
SumFun :
    add   r22 , r24
    adc   r23 , r25
    mov   r24 , r1
    adc   r24 , r1
    mov   r25 , r1
    ret

Rzecz jasna wynik zajmuje rejestry od R25 (część najstarsza) do R22 (część najmłodsza). O ile operacje dodania (i dodania z przeniesieniem) dotyczące części najmłodszej w sensie 16 bitów (R22 i R23) nie stanowią tajemnicy, to dalsze operacje mogą być owiane aurą tajemniczości. Wiadomo, że R1 zawsze zawiera zero, toteż wpis do R24 zawartości R1 jest zrozumiały. Do tego rejestru zostanie dodana z uwzględnieniem wskaźnika przeniesienia zawartość rejestru zawierającego zero. W ten sposób do części wyniku (od bitów 16 do 23) zostanie dodany nadmiar z poprzedniej operacji. Część najstarsza zostaje wyzerowana (żadna suma 16-bitów) nie jest w stanie sięgnąć do tych pozycji. Wynik pozostaje w rejestrach od R25 do R22.
Weryfikacja nie wnosi niespodzianek.
sumfun3.png

8000hex + FFFF hex = 17FFF hex = 98303 dec (i nie ma inaczej).
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
dambo
Expert
Expert
Posty: 645
Rejestracja: czwartek 17 mar 2016, 17:12

Re: [ASM][AVR] Inne spojrzenie

Postautor: dambo » niedziela 18 lip 2021, 19:58

To ja zadam pytanko w temacie - które kieeeedyś mi chodziło po głowie - to w jaki sposób przekazujemy/odbieramy z funkcji dane zależy od ustalonego ABI - w tym przypadku jest to ABI z avr-gcc, tak? A czy inne kompilatory mogą mieć inne zasady i to by powodowało, ze wstawki asemblerowe są przeznaczone i tylko na konkretną platformę (jeśli chodzi o listę rozkazów) i konkretny kompilator (jeśli chodzi o ABI)?
Nowy blog o tematyce embedded -> https://www.embedownik.pl/

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

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » niedziela 18 lip 2021, 20:33

dambo pisze:w tym przypadku jest to ABI z avr-gcc, tak?

Dokładnie, AVRSTUDIO opiera się o kompilator GCC, chociaż system GCC można rozpatrywać szerzej. Jest to kolekcja narzędzi na ileś procesorów i ileś języków. Wracając do tematu, powyższe rozważania dotyczą jedynie jednej rodziny procków: AVR'ów. Nie da się ukryć, że pewne rozwiązania filozofii ABI muszą być jakoś integrowane z prockiem, przecież w końcu każdy jest inny, każdy jest indywidualnością.

dambo pisze:A czy inne kompilatory mogą mieć inne zasady i to by powodowało, ze wstawki asemblerowe są przeznaczone i tylko na konkretną platformę (jeśli chodzi o listę rozkazów) i konkretny kompilator (jeśli chodzi o ABI)?


To dobre pytanie, tylko z odpowiedzią jest gorzej. Jednak z punktu widzenia logiki można zaryzykować stwierdzenie, że ABI musi być jakoś związane z typem procka. Z pewnością już wstawki asemblerowe muszą być dedykowane konkretnemu modelowi procka. Nie każdy dysponuje identycznymi możliwościami, ale każdy (tak sądzę) w jakiś sposób mniej lub bardziej złożony pozwala na realizację celu. Rozkładając zapis języka, przykładowo C, na ciąg operacji (nie mówię instrukcji) których celem jest określona czynność można ją czasami zrobić na kilka sposobów. Przykładowo przekazanie parametrów do funkcji. Na samym początku opisałem własną filozofię, która jest jakaś (nie mówię, że jest lepsza lub gorsza - jest jakaś). Jednak kompiler GCC dla AVR postępuje inaczej (nie znaczy że lepiej lub gorzej). Historycznie rzecz biorąc początkowe rozważania są adaptacją rozważań rodem z Z80 (tylko dla Z80 jest bardziej klarowna, moim zdaniem). Z kolei te zilogowe rozkminy są w jakimś stopniu wzorowane na filozofii PC-ów. W sumie obróciłem wiele kompilatorów (języków), ileś procków (wliczając w to duże komputery) i każdy z nich posługiwał się jakąś własną filozofią. W obecnych czasach powszechnym standardem stają się procki ARM i zapewne one wymuszą jakąś standaryzację filozofii generowania kodu. W końcu wszystko zmierza do jednego celu.

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

Awatar użytkownika
dambo
Expert
Expert
Posty: 645
Rejestracja: czwartek 17 mar 2016, 17:12

Re: [ASM][AVR] Inne spojrzenie

Postautor: dambo » niedziela 18 lip 2021, 22:21

Poszukałem trochę info na ten temat - jakiś super dokumentów od IARa nie znalazłem (możliwe, ze po zakupie ma się dostęp) - ale jest dostępny dokument AVR034 od Atmela i wychodzi na to, że IAR ma inny system przekazywania parametrów - i w sumie jakakolwiek różnica powoduje niekompatybilność binarek - nie połączy się biblioteki skompilowanej IARem do GCC (nie w łatwy sposób, bo pohakować zawsze jakoś można dla zabawy/nauki).

Przepraszam, że robię takie offtopy w twoim wątku - może pisać to w innym?
Nowy blog o tematyce embedded -> https://www.embedownik.pl/

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

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » poniedziałek 19 lip 2021, 15:27

dambo pisze:Przepraszam, że robię takie offtopy w twoim wątku - może pisać to w innym?


Ależ spoko, i tak wszystko kręci się wokół jednego wątku. To, że ostatnio piszę o kompilerze GCC, nie znaczy, że jest to jedyna słuszna koncepcja. Zauważ, że pierwsze posty nie dotyczyły miksowania C z asm, a i ich filozofia jest daleka od narzędzi GCC.

dambo pisze:Poszukałem trochę info na ten temat - jakiś super dokumentów od IARa nie znalazłem (możliwe, ze po zakupie ma się dostęp) - ale jest dostępny dokument AVR034 od Atmela i wychodzi na to, że IAR ma inny system przekazywania parametrów - i w sumie jakakolwiek różnica powoduje niekompatybilność binarek - nie połączy się biblioteki skompilowanej IARem do GCC (nie w łatwy sposób, bo pohakować zawsze jakoś można dla zabawy/nauki)


Właśnie wiedza jest elementem bezcennym. Zapewne z tego powodu firmy o nastawieniu komercyjnym chronią swoje rozwiązania jak tylko mogą. Z drugiej strony chodzi o klasyczne przywiązanie klienta do siebie i nie daj Boże klient zaczął kombinować w konkurencją. To może oznaczać utratę monopolu i doprowadzić do tego, że klient zostanie stracony z punktu widzenia biznesowego. Znane powiedzenie "biznes jest biznes" znacząco inaczej wygląda po obu stronach barykady. Stąd jest szlaban do jakieś dokładniejsze informacje. Ponieważ GCC jest produktem niekomercyjnym, tu sprawa wygląda inaczej.
Z takich ciekawostek, to właśnie sobie przypomniałem. Na ODRA1305 (licencja ICL1900) był ciekawy patent na przenoszenie parametrów. Sama oderka nie była maszyną stosową, więc zwykłe CALL nie odkładało adresu powrotu gdzieś na stosie. Oderka (jako maszyna o słowie 24-bitowym) miała 8 rejestrów (24-bitowych). W instrukcji CALL był wskazywany rejestr, w którym zostawiany jest adres powrotu i typowo to był R1. Instrukcje oderki mające operand w pamięci operacyjnej mogły użyć rejestru R1, R2 i R3 do modyfikacji fizycznego adresu (tak jak to mają rejestry indeksowe przykładowo w Z80). Tuż za instrukcją CALL był ciąg instrukcji, które pobierały do R3 wartość parametru wywołania: ile parametrów, tyle takich instrukcji typu LDI. Więc jak sterowanie było już w wywołanej funkcji, to można było sięgnąć do określonego parametru. Była taka instrukcja OBEY, która wykonywała jedną instrukcję spod podanego adresu, więc OBEY adresowany przez rejestr R1 z dodatkowym przesunięciem przykładowo 2, pobierało do R3 wartość drugiego parametru. Też jest to jakieś rozwiązanie - maszyna bez filozofii stosu jakoś sobie radziła z problemami.
Ważne jest by poznać różne warianty i systemy "filozoficzne" związane z generacją kodu.
Ot taka offtopowa dygresja :D

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

Awatar użytkownika
dambo
Expert
Expert
Posty: 645
Rejestracja: czwartek 17 mar 2016, 17:12

Re: [ASM][AVR] Inne spojrzenie

Postautor: dambo » poniedziałek 19 lip 2021, 15:57

To jeszcze kontynuując temat z przekazywaniem parametrów - jako ciekawostka w rdzeniach Tricore na prockach infineona jest dostępna instrukcja skoku, która z automatu określone rejestry wrzuca na stos bez dodatkowych instrukcji (bez wnikania w szczegóły, bo ta instrukcja ma kilka opcji, różne zestawy rejestrów można tak przerzucać itp) - czyli trochę "sprzętowe wsparcie" do tego typu operacji.

W C to proste zawolajFunckcje(a,b,c) -> a ile smaczków i rozkmin jest pod spodem schowane :)
Nowy blog o tematyce embedded -> https://www.embedownik.pl/

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

Re: [ASM][AVR] Inne spojrzenie

Postautor: gaweł » poniedziałek 19 lip 2021, 16:05

dambo pisze:W C to proste zawolajFunckcje(a,b,c) -> a ile smaczków i rozkmin jest pod spodem schowane :)


No widzisz: ilu inżynierów, tyle rozwiązań :lol:

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 2 gości