[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ł
Expert
Expert
Posty: 614
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ł
Expert
Expert
Posty: 614
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
User
User
Posty: 369
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

Awatar użytkownika
gaweł
Expert
Expert
Posty: 614
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ł
Expert
Expert
Posty: 614
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


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

Kto jest online

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