Kanał - ATNEL tech-forum
Wszystkie działy
Najnowsze wątki



Teraz jest 22 lis 2024, o 13:32


Strefa czasowa: UTC + 1





Utwórz nowy wątek Odpowiedz w wątku  [ Posty: 24 ] 
Autor Wiadomość
PostNapisane: 14 paź 2016, o 19:11 
Offline
Użytkownik

Dołączył(a): 07 cze 2016
Posty: 563
Pomógł: 143

Obrazek
Prolog

Jakiś czas temu odpowiadałem w wątku traktującym o wstawkach asemblerowych w kodzie napisanym w języku C. Wprawdzie autor wątku jakoś stracił zainteresowanie, jednak ze względu na zapewnienia kolegi Mirka, że inni czytelnicy tego forum też interesują się tym tematem, postanowiłem go nieco dokładniej opisać.

Podstawowe wiadomości

Zakładam, że każdy z czytających ten artykuł posiada podstawową wiedzę z dziedziny programowania zarówno w C jak i w asemblerze, dlatego nie będę tu zbyt szczegółowo wszystkiego opisywał. Przypomnę jednak kilka pojęć istotnych dla omawianego tematu i przedstawię kilka ważnych uwag.

Dla uproszczenia ograniczę się do opisania programów, których objętość danych w pamięci FLASH nie przekracza 64KB, a łącznie z kodem wykonywalnym 128KB. Powyżej tej granicy mieszanie kodu nieco się komplikuje ze względu na konieczność użycia wskaźników o rozmiarze przekraczającym 16 bitów, ale to raczej temat na inny artykuł. Myślę jednak, że to ograniczenie nie będzie większym problemem, gdyż linker umieszcza dane w pamięci FLASH na samym początku, zaraz po wektorach przerwań, dopiero później funkcje. Sytuacje, kiedy zapisujemy do FLASH dane o rozmiarze większym od 64KB nie zdarzają się chyba często, więc w większości przypadków 16-bitowe adresowanie będzie wystarczające.

Opisane tutaj zasady działania kompilatora i sposoby mieszania kodu dotyczą głównie aktualnego toolchain'a Atmela w wersji 3.5.3.1700. Jak powszechnie wiadomo oprogramowanie w dzisiejszych czasach dosyć szybko ewoluuje, więc nie mogę zagwarantować, że dokładnie te same zasady we wszystkich szczegółach dotyczą starszych wersji i będą dotyczyły nowszych wersji toolchain'a.

  • Zasadność mieszania kodu ASM i C

    Właściwie to potrzeba wstawiania kodu asemblera do kodu w języku C nie zdarza się zbyt często. Obecnie kompilatory C potrafią naprawdę bardzo dobrze optymalizować kod. Zwykle robią to równie dobrze lub nawet lepiej niż programista, szczególnie w przypadku tych bardziej skomplikowanych funkcji.

    Zdarzają się jednak sytuacje, kiedy (moim zdaniem) takie wstawki mają sens. Przypuśćmy przykładowo, że potrzebujemy wydajnej (szybkiej) funkcji lub procedury obsługi przerwania, w której optymalnym typem danych będzie liczba całkowita np. 40-bitowa (5 bajtów). Język C nie wspiera wprost operacji na takich liczbach.

    Można wprawdzie zastosować typ nadmiarowy 64-bitowy (8 bajtów zamiast 5), ale to spowoduje (mowa oczywiście o mikrokontrolerze 8-bitowym), że każde ładowanie zmiennej z pamięci do rejestrów i odwrotnie, wszelkie operacje dodawania, odejmowania, porównania i logiczne (nie wspominając o ewentualnej konieczności maskowania nieużywanych bajtów) będą zajmowały kilka (3, 6 lub nawet 9) taktów więcej, niż byłoby to konieczne dla operacji na pięciu bajtach, zamiast na ośmiu. W przypadku operacji mnożenia lub dzielenia te różnice mogą sięgać nawet kilkudziesięciu taktów, szczególnie w mikrokontrolerach nie obsługujących instrukcji mul, muls itp.

    Można też ewentualnie próbować obejść problem stosując jakieś triki ze strukturami i/lub uniami i/lub tablicami, ale to z kolei gmatwa kod i w dodatku wcale nie daje gwarancji, że będzie on optymalny. Dlatego (przynajmniej dla mnie) łatwiej rozwiązać zadanie za pomocą funkcji napisanej w asemblerze.

    Pewnie znalazłyby się jeszcze inne argumenty do stosowania wstawek, ale pozostawmy może te rozważania. Pokażę po prostu jak to technicznie wykonać, a każdy sam zdecyduje, czy i kiedy wstawki stosować. Oczywiście nie polecam nadużywania, ponieważ to na prawdę w niektórych przypadkach może nawet pogorszyć sprawę. Można najpierw poeksperymentować, pisząc takie same funkcje w języku C i w języku ASM sprawdzając, kto wygeneruje wydajniejszy kod wynikowy - programista ASM czy kompilator C.

  • Preprocesor

    W AVR GCC pliki zawierające kod asemblera mają rozszerzenia *.s (małe s) lub *.S (duże S). Aplikacja tłumacząca kod asemblera na wynikowy kod maszynowy (avr-as.exe) podczas procesu budowania nie jest wywoływana bezpośrednio, lecz poprzez aplikację avr-gcc.exe, która przed uruchomieniem avr-as.exe decyduje o użyciu lub nie użyciu preprocesora. Kiedyś decyzja ta podejmowana była właśnie na podstawie rozszerzenia pliku. W przypadku dużego S preprocesor był używany, a w przypadku małego s – nie.

    Podobno w systemach operacyjnych, które nie biorą pod uwagę wielkości liter w nazwach plików (np. Windows) były z tym jakieś problemy, więc z tego i/lub z innych powodów zasada ta została zmieniona. Obecnie opcja programu avr-gcc.exe
    -x assembler
    oznacza przekazanie pliku bezpośrednio do asemblacji, natomiast
    -x assembler-with-cpp
    wymusza wcześniejsze użycie preprocesora.

    Dlaczego właściwie preprocesor C ma przetwarzać plik z kodem ASM? Otóż skoro już miksujemy kod C i ASM, to przecież fajnie byłoby mieć pewne zdefiniowane stałe w programie dostępne jednocześnie w obu kodach. Trochę niewygodnie byłoby definiować stałą w dwóch miejscach (ryzyko pomyłki) i później jeszcze pamiętać, żeby ją w razie potrzeby zmienić w dwóch miejscach. Program asemblujący nie rozumie jednak tych wszystkich komentarzy, dyrektyw typowych dla języka C i będzie generował błędy składni. Jednak dzięki preprocesorowi możemy dołączyć ten sam plik nagłówkowy do obu kodów, a preprocesor przed asemblacją pliku z kodem ASM zamieni wszystkie symbole na ich zdefiniowane wartości, po czym pousuwa dyrektywy preprocesora i komentarze C-style, dzięki czemu asembler będzie mógł prawidłowo wygenerować kod wynikowy.

    Preprocesor będzie więc nam potrzebny. W Atmel Studio 7 opcja użycia preprocesora jest domyślnie włączona zarówno dla plików typu „Assembler File (.s)” jak i dla „Preprocessing Assembler File (.S)”. Zgodnie z moją wiedzą nie można tej opcji wyłączyć inaczej, jak tylko poprzez ręczne edytowanie pliku Makefile. Dopóki więc będziemy używać automatycznie generowanego Makefile, nie powinno być problemów.

    W Eclipse preprocesor dla plików z ASM również powinien być domyślnie włączony, ale tutaj można go wyłączyć (choć my nie powinniśmy tego robić, chyba że mamy uzasadniony powód) wyłączając opcję „Use preprocessor” we właściwościach projektu:
    Project->Properties->C/C++ Build->Settings->zakładka Tool Settings->AVR Assembler->General (jeśli dołączymy kilka plików z kodem ASM, można tę opcję włączyć lub wyłączyć dla każdego pliku osobno we właściwościach pliku). Tutaj też nie powinno być problemu, dopóki ktoś tej opcji nie wyłączy.

    Mimo tego, że w obu środowiskach opcja preprocesora jest domyślnie włączona, postanowiłem o tym napisać, bo moim zdaniem dobrze jest wiedzieć o co chodzi z tym preprocesorem, szczególnie gdyby ktoś korzystał ze starszej wersji avr-gcc (w dodatku w systemie unixowym), ewentualnie uruchamiał kompilację z wiersza poleceń lub zechciał ręcznie edytować plik Makefile.

  • Deklaracje kontra etykiety - używanie słowa kluczowego .extern

    Kompilator języka C wymaga od programisty precyzyjnego zadeklarowania zmiennych i funkcji. Potrzebuje tych informacji głównie w celu wygenerowania poprawnego kodu maszynowego (np. użycie właściwych instrukcji ASM w zależności od tego, czy zmienna całkowita jest ze znakiem, czy bez) oraz analizy semantycznej (np. kontrola typów podczas przypisania wartości jednej zmiennej do innej lub argumentów przekazywanych do funkcji).

    W przypadku asemblera to na programiście spoczywa obowiązek prawidłowej obsługi poszczególnych zmiennych, przekazywania prawidłowych argumentów do funkcji i wykonywania wszystkich innych zadań, które wykonuje kompilator C. Tutaj deklaracja zmiennej czy funkcji odbywa się poprzez nadanie im etykiet, które po zakończeniu budowania aplikacji stają się liczbami reprezentującymi adres komórki pamięci RAM lub FLASH. Etykiety nie mówią nic o typie zmiennej, o ilości i typach argumentów przekazywanych do funkcji, o wartości zwracanej przez funkcję. Ba, na podstawie samej etykiety nie można nawet stwierdzić, czy dotyczy ona zmiennej, stałej w pamięci programu czy może funkcji.

    Piszę o tym, ponieważ czasami spotykam próby deklarowania w pliku ASM konstrukcje tego rodzaju:
    .extern uint8_t nazwa
    co oczywiście nie ma większego sensu z dwóch powodów. Po pierwsze dla asemblera typ uint8_t i tak jest niezrozumiały - liczy się tylko słowo 'nazwa' będące dla asemblera etykietą (symbolem zmiennej). Po drugie samo użycie .extern jest przez GCC assembler akceptowane ze względu na kompatybilność z innymi asemblerami, nie jest jednak wymagane, gdyż program as (w naszym przypadku avr-as) traktuje wszystkie niezdefiniowane (w danym pliku) symbole jako .extern. Jeśli tylko w innym pliku naszego projektu lub w bibliotekach standardowych istnieje taki symbol (nazwa zmiennej lub funkcji), asemblacja przebiegnie bez błędów. Czy kod będzie działał prawidłowo, to już inna sprawa. Zależy to tylko od tego, czy programista nie pomyli np. typów zmiennych lub nazwy funkcji z nazwą zmiennej itp.

  • Kwestia optymalizacji kompilatora C

    W czasie wykonywania każdej funkcji używane są (przynajmniej niektóre) rejestry - nie da się tego uniknąć. Należy więc liczyć się z tym, że zawartość niektórych rejestrów po wykonaniu funkcji może być inna, niż przed jej wykonaniem. Jeśli nie chcemy utracić zawartości jakichś rejestrów, należy je przed wywołaniem funkcji zapamiętać (np. na stosie) lub przechować te wartości w rejestrach, o których wiemy, że nie zostaną zmienione. Zapamiętywanie rejestrów na stosie wiąże się z zaangażowaniem pewnych zasobów (np. czasu procesora, zajętości RAM), więc lepiej więc tego unikać.

    Jedną ze strategii optymalizacyjnych kompilatora C jest właśnie takie dobieranie rejestrów w trakcie wykonywania programu, aby zminimalizować potrzebę zapamiętywania ich wartości. Niestety dołączając do projektu pliki ASM, w których "na sztywno" definiujemy rejestry użyte wewnątrz funkcji, zawężamy mu nieco pole manewru, przez co optymalizacja może być mniej skuteczna. Lepsze efekty pod tym względem daje inline assembler, gdyż pozostawia kompilatorowi dobór rejestrów wykorzystywanych we funkcji, jednak jak dla mnie jego składnia jest stosunkowo trudna, a kod mało czytelny i osobiście nie lubię go stosować, chyba że do definiowana naprawdę prostych funkcji.

    Istnieje jeszcze jeden problem podobnej natury. Zdarza się, że chcąc przyspieszyć dostęp do jakichś newralgicznych danych, zechcemy zarezerwować dla jakiejś zmiennej (lub kilku) rejestr (rejestry) mikrokontrolera. Można to oczywiście zrobić używając słowa kluczowego 'register' deklarując zmienną globalną (stosunkowo bezpiecznie jest używać do tego celu rejestry z zakresu r2-r7):
    Składnia: [ Pobierz ] [ Ukryj ]
    język c
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

    jednak trzeba mieć świadomość, że to zablokuje dostęp do tego rejestru kompilatorowi, również utrudniając mu zadanie optymalizacji.

    Nie należy się tym oczywiście zniechęcać. Dobrze jest jednak o tym wiedzieć i pamiętać, aby nie przesadzać z długością i ilością funkcji pisanych w ASM oraz z ilością używanych w nich rejestrów.

    Na koniec mam też dobrą wiadomość. Przerwania w mikrokontrolerze występują w zupełnie przypadkowych momentach. Skoro nie da się przewidzieć tych momentów, kompilator i tak nie będzie mógł zoptymalizować kodu poprzez taki dobór rejestrów, by nie trzeba było ich zapamiętywać, gdyż (ze względu na tę przypadkowość) procedura obsługi przerwania musi zawsze zapamiętać wszystkie rejestry oraz rejestr statusowy SREG na początku procedury i przywrócić ich stan przed powrotem do programu głównego. Dlatego też napisanie procedury obsługi przerwania w ASM nie wpłynie na możliwości optymalizacyjne kompilatora C. Ewentualne użycie nadmiernej liczby rejestrów wpłynie jedynie na czas wykonania samej procedury, ale nie wpłynie na optymalizację kompilatora C. W związku z tym właśnie w procedurach obsługi przerwań będziemy mieli prawdziwe pole do popisu.

Różnice w składni asemblera

Asembler AVR GCC i ten Atmela różnią się nieco składnią. Oczywiście nie dotyczy to samych instrukcji ASM, a raczej sposobu deklarowania stałych, danych w pamięci RAM lub FLASH, dołączania plików itp. Odnoszę wrażenie, że trochę trudno dotrzeć do szczegółowej dokumentacji, więc przedstawię najważniejsze różnice, które udało mi się znaleźć.

  • Dołączanie plików z definicjami mikrokontrolera:

    Kod:
        Atmel assembler (avrasm2)    |     GCC assembler (avr-as)
    ---------------------------------|--------------------------
    .include "m644pdef.inc"          | #include <avr/io.h>
    #include "m644pdef.inc"          |

  • Nadawanie rejestrom nazw symbolicznych:

    Kod:
        Atmel assembler (avrasm2)    |     GCC assembler (avr-as)
    ---------------------------------|--------------------------
    .def my_reg=r16                  | #define my_reg r16
                                     | my_reg = 16

  • Definiowanie stałych:

    Kod:
        Atmel assembler (avrasm2)      |     GCC assembler (avr-as)
    -----------------------------------|--------------------------
    .equ mask=0x21 ; nie można zmienić | #define mask 0x21
    .set mask=0x21 ; można zmienić     | .equ mask, 0x21
          ; w dalszej części programu  | .set mask, 0x21

  • Wydobywanie poszczególnych bajtów ze stałych wielobajtowych.

    Kiedy zdefiniujemy stałą, której wartość wykracza poza 8 bitów, i będziemy chcieli ją załadować do kilku rejestrów, musimy wydobyć jakoś poszczególne bajty z takiej stałej. Służą do tego specjalne instrukcje dla preprocesora umieszczane w kodzie:

    Kod:
       bity   |    Atmel assembler (avrasm2)    |    GCC assembler (avr-as)
    ----------|---------------------------------|------------------------------
      0 - 7   |   low(expression)               |   lo8(expression)
    ----------|---------------------------------|------------------------------
      8 - 15  |   high(expression)              |   hi8(expression)
              |   byte2(expression)             |
    ----------|---------------------------------|------------------------------
     16 - 23  |   byte3(expression)             |   hh8(expression)
              |                                 |   hlo8(expression)
    ----------|---------------------------------|------------------------------
     24 - 31  |   byte4(expression)             |   hhi8(expression)


    Przykład dla GCC assembler:

    Składnia: [ Pobierz ] [ Ukryj ]
    język asm
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


    AVR GCC assembler oferuje dodatkowo instrukcje, które są przydatne przy pobieraniu adresu, gdyż przetwarzają adres bajtu we flash na adres słowa. Jest to przydatne na przykład, gdy chcemy wywołać funkcję poprzez wskaźnik za pomocą instrukcji ASM icall (indirect call) lub wykonać skok do określonej przez rejestr wskaźnikowy lokalizacji w programie za pomocą instrukcji ijmp (indirect jump).

    Kod:
       bity   |    GCC assembler (avr-as)
    ----------|-----------------------------------------------------------
      0 - 7   |   pm_lo8(expression)
    ----------|-----------------------------------------------------------
      8 - 15  |   pm_hi8(expression)
    ----------|-----------------------------------------------------------
     16 - 23  |   pm_hh8(expression)  ; tylko w przypadku pamięci flash
              |                       ; powyżej 64K słów (128KB)


    Przykład:

    Składnia: [ Pobierz ] [ Ukryj ]
    język asm
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

  • Sekcje / segmenty.

    Kod:
        Atmel assembler (avrasm2)      |     GCC assembler (avr-as)
    -----------------------------------|--------------------------
    .dseg                              | .section .data
    .cseg                              | .section .text
    .eseg                              | .section .eeprom

  • Deklarowanie stałych w pamięci FLASH lub EEPROM:

    Oczywiście deklaracje należy umieszczać w odpowiedniej sekcji (.section .text lub .section .eeprom) oraz przed każdą deklaracją musi znajdować się etykieta, ponieważ tylko w ten sposób będzie można określić adres, pod którym dane się znajdują.

    Kod:
        Atmel assembler (avrasm2)     |     GCC assembler (avr-as)
    ----------------------------------|--------------------------------------
                  ciąg znaków (zakończony znakiem '\0')
    .db „example text”, 0             | .asciz „example text” // znak '\0'
                                      |       // jest dodawany automatycznie
    -------------------------------------------------------------------------
                             stałe 1-bajtowe
    .db 0, 0x26, 0b01101001           | .byte 0, 0x26, 0b01101001
    -------------------------------------------------------------------------
                             stałe 2-bajtowe
    .dw 0, 5846, 0x15AF               | .word  0, 5846, 0x15AF
    -------------------------------------------------------------------------
                             stałe 4-bajtowe
    .dd 15, 158933, 0x056A0D2E        | .long 15, 158933, 0x056A0D2E
    -------------------------------------------------------------------------
                             stałe 8-bajtowe
    .dq 689958741, 0x54A6d32F5011AE1C | .quad 689958741, 0x54A6d32F5011AE1C
    -------------------------------------------------------------------------

  • Deklarowanie zmiennych w pamięci RAM:

    Tak jak poprzednio należy również pamiętać o umieszczeniu deklaracji w odpowiedniej sekcji (tym razem w .section .data) i opatrzeniu ich etykietami.

    Kod:
        Atmel assembler (avrasm2)     |     GCC assembler (avr-as)
    ----------------------------------|--------------------------------------------------
                  ciąg znaków (zakończony znakiem '\0')
    .byte 13  ; liczba oznacza ilość  | .asciz „example text” ; znak '\0'
              ; znaków w ciągu (+1)   |        ; jest dodawany automatycznie
    -------------------------------------------------------------------------------------
                           zmienne 1-bajtowe
    .byte 3  ; rezerwuje 3 bajty      | .byte 0, 0x26, 0b01101001 ;3 bajty
    -------------------------------------------------------------------------------------
                          zmienne 2-bajtowe
    .byte 6  ; rezerwuje 3*2 bajty    | .word  0, 5846, 0x15AF ; 3*2 bajty
    -------------------------------------------------------------------------------------
                          zmienne 32-bitowe
    .byte 12 ; rezerwuje 3*4 bajty    | .long 15, 158933, 0x056A0D2E ; 3*4 bajty
    -------------------------------------------------------------------------------------
                          zmienne 64-bitowe
    .byte 16 ; rezerwuje 2*8 bajtów   | .quad 689958741, 0x54A6d32F5011AE1C ; 2*8 bajtów
    -------------------------------------------------------------------------------------


    Tutaj jednak różnice nie dotyczą tylko składni. W przypadku projektu napisanego tylko w języku asemblera (Atmel assembler), z przyczyn oczywistych na programiście spoczywa obowiązek zainicjowania zmiennych odpowiednimi wartościami przed ich użyciem.

    Jeśli tworzymy projekt w języku C, to kompilator powinien zainicjować za nas zmienne zadeklarowane w kodzie ASM wartościami, które podamy przy deklaracji, np.:
    Składnia: [ Pobierz ] [ Ukryj ]
    język asm
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


    Z moich doświadczeń wynika jednak, że nie zawsze to robi (nie udało mi się znaleźć odpowiedzi na pytanie, czy jest to działanie zamierzone).

    Jeśli w kodzie C będziemy mieli zadeklarowaną co najmniej jedną zmienną statyczną z przypisaniem wartości niezerowej, np.:
    Składnia: [ Pobierz ] [ Ukryj ]
    język c
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

    to kompilator C wygeneruje kod, który podczas startu mikrokontrolera zainicjuje zarówno zmienne zadeklarowane w kodzie C, jak i te zadeklarowane w kodzie ASM.

    Jeśli w kodzie C nie będziemy mieli zadeklarowanej takiej zmiennej, o której mowa powyżej, to kompilator zwyczajnie podczas startu mikrokontrolera wyzeruje tylko wszystkie komórki pamięci przeznaczone na zmienne (zadeklarowane w kodzie C), uwzględni wprawdzie miejsce na zmienne zadeklarowane w ASM, ale pominie tworzenie kodu inicjującego wartości.

    Jeśli wystąpi taka sytuacja, a zależy nam na zainicjowaniu zmiennych zadeklarowanych w kodzie ASM konkretnymi wartościami podczas startu, mamy dwa sposoby rozwiązania problemu:
    1. Przeniesienie deklaracji zmiennej do pliku C i tam przypisanie jej wartości (później opcjonalnie zadeklarowanie w pliku ASM jako .extern, choć nie jest to wymagane). Jeśli jednak nie będzie to zmienna współdzielona przez oba kody, nie jest to (moim zdaniem) dobre rozwiązanie, dlatego że zmienna używana tylko w pliku ASM powinna być tylko dla tego pliku widoczna (osiągalna).
    2. Dopisanie w kodzie ASM procedury wpisującej wartości do odpowiednich komórek pamięci, i umieszczenie jej np. w sekcji .init1, której kod jest wykonywany podczas startu mikrokontrolera, jeszcze przed wejściem do funkcji main().

      Przykład:
      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

  • Operacje na rejestrach wejścia/wyjścia

    Mikrokontrolery posiadają szereg rejestrów specjalnych (tzw. rejestry wejścia/wyjścia, tutaj przyjmijmy skrótową nazwę: rejestry i/o), które służą do sterowania pracą wbudowanych w niego układów peryferyjnych. W mikrokontrolerach AVR 8-bitowych rejestry te są podzielone na dwie grupy.

    Pierwsze 64 rejestry mogą być odczytywane i modyfikowane zarówno przez instrukcje asemblera in oraz out operujące na przestrzeni adresowej i/0 jak i poprzez instrukcje load oraz store operujące na przestrzeni adresowej pamięci danych SRAM. Oczywiście nie ma takich instrukcji asemblera load oraz store. Pod tym pojęciem rozumiem tutaj wszystkie instrukcje czytające z pamięci (np. ld, lds) jak i zapisujące do pamięci (np. st, sts).

    Jeśli mikrokontroler jest rozbudowany na tyle, że musi mieć więcej niż 64 rejestry (maksymalnie może mieć 160 dodatkowych rejestrów), dodatkowe rejestry są osiągalne tylko poprzez instrukcje load oraz store.

    W tej chwili interesuje nas ta pierwsza grupa rejestrów. Ich adresy w przestrzeni i/o mieszczą się w zakresie 0-63 (0x00-0x3F heksadecymalnie), natomiast w przestrzeni adresowej pamięci mają adresy 32-95 (0x20-0x5F heksadecymalnie). Relacja między adresami jest taka, że te w przestrzeni pamięci są większe o 32 (0x20 heksadecymalnie) od tych w przestrzeni i/o.

    Z punktu widzenia szybkości wykonania programu korzystniejsze są instrukcje in oraz out, dlatego dobrze jest korzystać z nich (zamiast load oraz store) kiedy to tylko możliwe. Korzystając z Atmel assembler robimy to zwyczajnie pisząc np.:

    Składnia: [ Pobierz ] [ Ukryj ]
    język asm
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


    GCC assembler korzysta jednak z tego samego pliku nagłówkowego (<avr/io.h>), co kompilator C. Wszystkie adresy rejestrów i/o są tam zdefiniowane jako adresy w przestrzeni adresowej pamięci. Kiedy kompilator uzna, że należy użyć instrukcji operujących na przestrzeni adresowej i/o, automatycznie odejmuje 0x20 od adresu podczas generowania takiej instrukcji. Niestety, jeśli piszemy wstawki asemblerowe musimy sami zadbać o użycie właściwego adresu. Jeśli więc chcemy użyć instrukcji in lub out (ewentualnie którejś z instrukcji operowania na bitach rejestru i/o, np. sbi), powinniśmy użyć do tego makra _SFR_IO_ADDR(), przykładowo:

    Składnia: [ Pobierz ] [ Ukryj ]
    język asm
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


    Może to być nieco kłopotliwe, istnieje jednak sposób, aby pozbyć się tej niedogodności. Polega on na dodaniu na samym początku pliku ASM (jeszcze przed dyrektywą #include <avr/io.h>) definicji:

    Składnia: [ Pobierz ] [ Ukryj ]
    język asm
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


    We wszystkich przedstawionych tu przykładowych fragmentach kodu zostało przyjęte, że powyższa definicja została umieszczona na początku pliku.

Reguły stosowane przez kompilator GCC

Mieszając kod ASM z kodem C musimy stosować takie same reguły co kompilator C. Przykładowo, chcąc wywołać funkcję napisaną w języku C z kodu w języku ASM, musimy wiedzieć, w jakich rejestrach umieścić argumenty przekazywane do funkcji i w jakich rejestrach oczekiwać wartości zwracanych przez funkcję. W odwrotnym przypadku, gdy piszemy w kodzie ASM funkcję, która będzie wywoływana z kodu C, też musimy wiedzieć, w jakich rejestrach kompilator umieści argumenty i w jakich będzie oczekiwał wartości zwracanej przez naszą funkcję. Poza tym kompilator C traktuje niektóre rejestry lub grupy rejestrów w ściśle określony sposób i my też musimy te same zasady stosować.

  • Przekazywanie argumentów do funkcji

    W języku C funkcje mogą (choć nie muszą) przyjmować argumenty, czyli innymi słowami jakieś dane, którymi będą operować (wykonywać jakieś obliczenia, porównania itp.). Przekazanie argumentu do funkcji polega na podaniu ich w nawiasie po nazwie funkcji.

    W języku ASM samo wywołanie jest stosunkowo proste. Służy do tego celu np. instrukcja call, która ma jeden operand. Tym operandem jest etykieta, która wyznacza adres początku funkcji (dokładniej adres słowa w pamięci FLASH, w którym znajduje się pierwsza instrukcja funkcji).

    Skoro instrukcja call ma tylko jeden operand, to jak przekazać argumenty do funkcji?
    Można to zrobić na przykład w taki sposób:
    - najpierw do wybranych rejestrów wprowadzić dane, które chcemy przekazać do funkcji,
    - później wywołać funkcję np. za pomocą instrukcji call,
    - wywołana funkcja odczytuje dane z wybranych rejestrów i wykonuje na nich wymagane operacje.

    Jeśli wywoływana funkcja jest również napisana przez nas w ASM, to możemy do tego celu wyznaczyć dowolne rejestry wedle uznania. Wystarczy, że funkcję napiszemy tak, aby czytała dane z właściwych rejestrów. Jeśli jednak w pliku ASM wywołujemy funkcję napisaną w języku C, musimy wiedzieć, w których rejestrach funkcja ta oczekuje argumentów, ponieważ nie my o tym decydujemy, tylko kompilator.

    Na szczęście kompilator C stosuje pewne stałe reguły określające, w których rejestrach mają być przekazywane argumenty do funkcji. Zostały do tego celu wyznaczone rejestry r25 - r8. Rejestry są pogrupowane w pary. Najmniej znaczący bajt argumentu jest zawsze umieszczany w rejestrze o parzystym numerze. Kolejne bajty argumentu są umieszczane w kolejnych rejestrach o wyższych numerach. Kolejne argumenty są umieszczane w rejestrach poczynając od wyższych numerów, kończąc na niższych. Precyzyjnie sposób wyznaczania rejestrów do przekazania argumentów wygląda następująco:
      Za rejestr (nazwijmy go) "bazowy" przyjmujemy r26 i wykonujemy następujące kroki
    1. Jeśli rozmiar argumentu jest liczbą nieparzystą, dodajemy do rozmiaru 1.
    2. Tak obliczony rozmiar odejmujemy od numeru rejestru bazowego, uzyskując nowy numer rejestru bazowego.
    3. Jeśli nowy numer rejestru bazowego jest większy od r8, będzie do niego wpisany najmniej znaczący bajt naszego argumentu. Kolejne bajty będą wpisane do kolejnych rejestrów (w kierunku wyższych numerów).
    4. Jeśli numer rejestru będzie mniejszy od r8, argument zostanie umieszczony w pamięci RAM. Tutaj nie będziemy rozpatrywać takiego przypadku, ponieważ zakładamy pisanie stosunkowo prostych funkcji jak wstawek ASM. Za pomocą wyznaczonych do tego celu rejestrów możemy do funkcji przekazać maksymalnie 18 bajtów, więc w zdecydowanej większości przypadków będzie to ilość wystarczająca.
    5. Jeśli aktualny argument powinien być umieszczony w pamięci RAM, poprzestajemy na tym, ponieważ to oznacza, że wszystkie następne argumenty również muszą być umieszczone w RAM.
    6. Jeśli mamy do przekazania następny argument, wracamy do punktu 1. (oczywiście przyjmując do obliczeń nowo wyznaczony numer rejestru bazowego).

    Wygląda to może nieco skomplikowanie, więc podam kilka przykładów:

    Przykład 1:

    Składnia: [ Pobierz ] [ Ukryj ]
    język c
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

    arg1: 4 bajty

    1. 4 jest liczbą parzystą, więc pozostaje bez zmian.
    2. 26 - 4 = 22 rejestr bazowy dla pierwszego argumentu to r22.
    3. Numer rejestru r22 jest większy od numeru rejestru r8 więc przypisujemy:
      Kod:
      r22 = arg1[byte0]
      r23 = arg1[byte1]
      r24 = arg1[byte2]
      r25 = arg1[byte3]
    4. Nie dotyczy naszego przypadku.
    5. Nie dotyczy naszego przypadku.
    6. Kontynuujemy z następnym argumentem.

    arg2: 1 bajt

    1. 1 jest liczbą nieparzystą, więc dodajemy 1 i otrzymujemy rozmiar równy 2
    2. 22 - 2 = 20 rejestr bazowy dla drugiego argumentu to r20.
    3. Numer rejestru r20 jest większy od numeru rejestru r8 więc przypisujemy:
      Kod:
      r20 = arg2[byte0]
    4. Nie dotyczy naszego przypadku.
    5. Nie dotyczy naszego przypadku.
    6. Kontynuujemy z następnym argumentem.

    arg3: 2 bajty

    1. 2 jest liczbą parzystą, więc pozostaje bez zmian.
    2. 20 - 2 = 18 rejestr bazowy dla pierwszego argumentu to r18.
    3. Numer rejestru r18 jest większy od numeru rejestru r8 więc przypisujemy:
      Kod:
      r18 = arg3[byte0]
      r19 = arg3[byte1]
    4. Nie dotyczy naszego przypadku.
    5. Nie dotyczy naszego przypadku.
    6. Kończymy - to był ostatni argument.

    Przykład 2:

    Składnia: [ Pobierz ] [ Ukryj ]
    język c
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


    arg1: 2 bajty ( wskaźnik = 16 bit )
    Kod:
    r24 = arg1[byte0]
    r25 = arg1[byte1]

    arg2: 2 bajty
    Kod:
    r22 = arg2[byte0]
    r23 = arg2[byte1]

    arg3: 1 bajt
    Kod:
    r20 = arg3[byte0]



    Przykład 3:

    Składnia: [ Pobierz ] [ Ukryj ]
    język c
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

    Kod:
    r14 = arg3[byte0]
    r15 = arg3[byte1]
    r16 = arg3[byte2]
    r17 = arg3[byte3]
    r18 = arg3[byte4]
    r19 = arg3[byte5]
    r20 = arg3[byte6]
    r21 = arg3[byte7]

    r22 = arg2[byte0]
    r23 = arg2[byte1]

    r24 = arg1[byte0]

  • Wartości zwracane przez funkcje

    Jeżeli pisząc w ASM będziemy wywoływali funkcję napisaną w C lub będziemy pisali funkcję w ASM wywoływaną w pliku C, musimy znać też rejestry, w których - po wykonaniu funkcji - znajdzie się wartość zwracana przez tę funkcję (oczywiście tylko wtedy, gdy jakaś wartość ma być zwracana, bo przecież nie zawsze musi być).

    Tutaj sprawa wygląda prościej, gdyż zwracana wartość jest jedna, a nie kilka. Wartość zwracana przez funkcję jest umieszczana w rejestrach, jeśli jej rozmiar nie przekroczy 8 bajtów, czyli zdecydowanie mniej niż w przypadku argumentów przekazywanych do funkcji, ale i tak w większości przypadków rozmiar 64-bitowy jest wystarczający. Właściwie w avr-libc nie ma np. typu całkowitego o większym rozmiarze, a typy zmiennoprzecinkowe float oraz double mają po 32-bity. Jedynym przypadkiem przekroczenia limitu będzie więc zwrócenie przez funkcję struktury (poprzez wartość) o rozmiarze większym od ośmiu bajtów. Moim zdaniem jednak zarówno przekazywanie do funkcji, jak i zwracanie przez funkcję dużych struktur poprzez wartość w ośmiobitowych mikrokontrolerach, mających stosunkowo małe pojemności pamięci RAM, nie jest dobrą praktyką.

    Wyznaczanie rejestrów wygląda podobnie, jak w przypadku argumentów (rejestr bazowy to także r26), a więc przyporządkowanie rejestrów będzie wyglądać tak:
    1 bajt
    Kod:
    byte0
     r24


    2 bajty
    Kod:
    [byte1]   [byte0]
      r25       r24


    4 bajty
    Kod:
    [byte3]   [byte2]   [byte1]   [byte0]
      r25       r24       r23       r22


    8 bajtów
    Kod:
    [byte7]   [byte6]   [byte5]   [byte4]   [byte3]   [byte2]   [byte1]   [byte0]
      r25       r24       r23       r22       r21       r20       r19       r18


  • Rejestry stałe

    Rejestry stałe to rejestry o specjalnym przeznaczeniu, które nie są alokowane przez kompilator wprost do operacji na danych:
    • r0
      Jest to rejestr do przechowywania tymczasowych danych, którego wartość nie musi być przywracana po jego użyciu. Jedynym wyjątkiem są procedury obsługi przerwań, gdzie rejestr ten - jeśli jest używany wewnątrz procedury, jego zawartość musi być zapamiętana w prologu i przywrócona w epilogu.
      W inline assembler może być używany za pośrednictwem symbolu __tmp_reg__ jako rejestr tymczasowy.
    • r1
      Rejestr ten jest przeznaczony do przechowywania wartości zerowej. Jego wartość musi być zawsze równa zero. Nie zawsze jednak da się to osiągnąć, choćby ze względu na to, że jest to jeden z rejestrów wyjściowych dla instrukcji mul. Jeśli jednak jego wartość zostanie zmieniona przez jakąś funkcję, musi być ponownie wyzerowana (najpóźniej przed jej zakończeniem). Nie należy jednak zerować tego rejestru, kiedy jego wartość została zmieniona w procedurze obsługi przerwania. Należy go zapamiętać w prologu i przywrócić w epilogu, gdyż nigdy nie wiadomo, w jakim momencie wystąpi przerwanie i jaka będzie w tym momencie wartość tego rejestru (po zakończeniu procedury musi być taka sama jak przed).
      Rejestr ten może być przydatny np. przy instrukcjach porównania. Nie wszystkie rejestry mają możliwość porównania z wartością stałą (instrukcja cpi obsługuje tylko rejestry r16-r31). Dzięki temu rejestrowi można szybko porównać dowolny rejestr z wartością 0x00 (przy pomocy instrukcji cp, która obsługuje wszystkie rejestry), bez konieczności wykonania najpierw operacji zerowania. Niestety nie wolno w ten sposób używać rejestru r1 w procedurze obsługi przerwania, gdyż nie można wtedy założyć, że rejestr ten ma wartość zero.
      W inline assembler można go używać za pomocą symbolu __zero_reg__ jako rejestr o wartości zerowej.

  • Rejestry niszczone przez funkcje

    Każda funkcja musi używać rejestrów, co oznacza, że zawartość rejestrów podczas wykonywania funkcji zostaje zmieniona. Kompilator C przyjmuje, że zawartość rejestrów r18-r27, r30-r31, r0, flaga T w SREG może ulec zniszczeniu podczas działania funkcji.

    Jeśli pisząc wstawkę w ASM zależy nam na zachowaniu wartości któregoś z wyżej wymienionych rejestrów po zakończeniu wykonania funkcji napisanej w C, musimy przed jej wywołaniem zapamiętać tę wartość (np. na stosie).

    Z kolei pisząc funkcję w ASM, która będzie wywoływana w kodzie C, możemy dowolnie korzystać z tych rejestrów, nie martwiąc się o to, że zniszczymy ich zawartość. Nie trzeba ich zapamiętywać na początku i przywracać na końcu funkcji.

    Wyjątkiem są tutaj oczywiście (o czym pisałem już wcześniej) procedury obsługi przerwań, które zawsze muszą zapamiętywać wszystkie używane przez siebie rejestry, jak i rejestr statusowy SREG.

  • Rejestry zachowywane przez funkcje

    Odwrotna zasada dotyczy pozostałych rejestrów, czyli r2-r17, r28-r29. Wywołując w pliku ASM funkcję C możemy założyć, że zawartość tych rejestrów nie zostanie zmieniona.

    Pisząc w ASM funkcję wywoływaną później w pliku C, jeśli chcemy skorzystać z tych rejestrów, musimy zadbać o zapamiętanie zawartości tych rejestrów na początku funkcji i przywrócenie na końcu.

    Do tej grupy rejestrów można zaliczyć również rejestr r1, jednak należy pamiętać, że rejestr ten jest rejestrem specjalnym ("zerowym") i obowiązują go trochę inne reguły opisane wcześniej.

Miksowanie

Zaznaczam, że nie jest tutaj moim zamiarem udowadnianie, że kod napisany przez programistę jest wydajniejszy od tego wygenerowanego przez kompilator. Chodzi mi tylko o pokazanie, jak technicznie wykonać miksowanie, więc przedstawione tu przykładowe fragmenty kodu będą stosunkowo banalne i zapewne niepraktyczne. Na koniec podam nieco obszerniejszy przykład przy okazji omawiania wykorzystania wspólnego pliku nagłówkowego.

Jak już wspomniałem wcześniej (podrozdział "Operacje na rejestrach wejścia/wyjścia"), możliwość użycia instrukcji in i out zamiast load i store jest uzależnione od adresu rejestru. W poniższych przykładach przyjąłem adresy rejestrów mikrokontrolera ATmega644P

  • Współdzielenie zmiennych

    Wprawdzie podałem tu przykłady współdzielenia zmiennych przez zwykłe funkcje, jednak bardziej uzasadnione jest współdzielenie zmiennych np. poprzez funkcje C i procedurę obsługi przerwania napisaną w ASM. Moim zdaniem, w przypadku zwykłych funkcji, lepiej przekazywać dane poprzez argumenty i zwracanie wartości.

    Oczywiście w przypadku współdzielenia zmiennej pomiędzy procedurą obsługi przerwania w ASM a funkcjami C, należy pamiętać o dodaniu słowa kluczowego volatile.

    • Zmienna zadeklarowana w pliku C dostępna dla kodu ASM

      W pliku C deklarujemy zmienną globalną:

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      W pliku ASM można zadeklarować zmienną .extern, choć nie jest to konieczne:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Przypominam, że w plikach ASM nie deklarujemy typu zmiennej. Pomimo tego, że deklaracja .extern nie jest konieczna, uważam że warto to zrobić np. w taki sposób (typ można podać w komentarzu):

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Dzięki temu, bez konieczności przełączania się do pliku *.c możemy sobie przypomnieć nazwę, typ i przeznaczenie zmiennej.

      Przykład użycia:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

    • Zmienna zadeklarowana w pliku ASM dostępna dla kodu C

      W pliku ASM deklarujemy zmienną w sekcji .data:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Tutaj należy pamiętać o możliwej konieczności zainicjowania wartości zmiennej, co opisałem w podrozdziale: "Deklarowanie zmiennych w pamięci RAM", gdyż kompilator może za nas tego nie zrobić.

      W pliku C deklarujemy zmienną extern:

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Pomimo tego, że w pliku ASM zmienna counter nie ma typu (tzn. ma tylko domyślnie przyjęty przez programistę), w pliku C musimy bezwzględnie podać typ zmiennej.

  • Wywoływanie funkcji napisanej w ASM z kodu napisanego w C

    W kodzie ASM etykiet używamy nie tylko do oznaczenia zmiennych czy też początku funkcji. Część etykiet, zwykle większa część, jest przeznaczona do oznaczenia miejsc, do których będą wykonywane skoki warunkowe (np. instrukcje brne, breq itp.), skoki bezwarunkowe (np. instrukcje rjmp, jmp itp.) lub do oznaczenia początków funkcji lokalnych, pomocniczych, które używane będą tylko w kodzie ASM. Nie jest wskazane, aby niepotrzebne etykiety były widoczne dla kodu C. Dlatego należy specjalnie oznaczyć tylko etykiety oznaczające wejścia do funkcji, które będą wywoływane z kodu C. Służy do tego słowo kluczowe .global

    • Funkcja bez parametrów nie zwracająca wartości

      Deklaracja i wywołanie funkcji w pliku C:

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Definicja funkcji w pliku ASM:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

    • Funkcja z parametrami nie zwracająca wartości

      Deklaracja i wywołanie funkcji w pliku C:

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Definicja funkcji w pliku ASM:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

    • Funkcja bez parametrów zwracająca wartość

      Deklaracja i wywołanie funkcji w pliku C:

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Definicja funkcji w pliku ASM:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

    • Funkcja z parametrami zwracająca wartość

      Deklaracja i wywołanie funkcji w pliku C:

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Definicja funkcji w pliku ASM:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

    • Procedura obsługi przerwania

      Procedura obsługi przerwania to specjalny rodzaj funkcji. Jest ona wywoływana automatycznie przez mikrokontroler w momencie wystąpienia zdarzenia. W przeciwieństwie do zwykłej funkcji nie da się przewidzieć, w którym momencie wykonywania programu głównego procedura zostanie wywołana, dlatego obowiązują tu inne reguły:
      1. O ile funkcję możemy nazwać dowolnie, o tyle nazwa procedury obsługi przerwania (w przypadku asemblera - etykieta globalna) musi być zgodna z nazwą zdefiniowaną w plikach nagłówkowych mikrokontrolera dla danego przerwania. Prościej mówiąc, musi mieś nazwę, którą wpisalibyśmy w nawiasie używając makra ISR() pisząc procedurę w języku C (oczywiście przy użyciu GCC).
      2. Wszystkie rejestry używane wewnątrz procedury oraz rejestr statusowy SREG muszą zostać zapamiętane na stosie w prologu i odtworzone w epilogu procedury. Dotyczy to również rejestrów, których zniszczenie zawartości jest dopuszczalne w normalnej funkcji jak i rejestrów stałych: tymczasowego r0 (który w normalnej funkcji nie musi być zapamiętywany i odtwarzany) oraz zerowego r1 (który w normalnej funkcji musi zostać na końcu wyzerowany, jeśli jego zawartość uległa zmianie).
        W wyjątkowych przypadkach, jeśli żadna z instrukcji wewnątrz procedury nie zmienia zawartości SREG, możemy pominąć jego zapamiętywanie i przywracanie.
        Z drugiej strony należy pamiętać, że w przypadku wywołania jakiejś funkcji wewnątrz procedury, musimy uwzględnić w prologu i epilogu rejestry przez tę funkcję niszczone.
      3. W odróżnieniu od normalnej funkcji kończonej instrukcją powrotu ret, procedura obsługi przerwania musi być zakończona instrukcją powrotu reti.

      Typowa konstrukcja procedury obsługi przerwania (dla przykładu przyjmijmy przepełnienie timer'a 0) powinna wyglądać tak:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.



      W przypadku, kiedy np. chcemy w procedurze tylko ustawić flagę odpowiednio interpretowaną później w programie głównym, (w niektórych mikrokontrolerach) możemy użyć do tego rejestru i/o - GPIOR. W procedurze obsługi przerwania zmieniamy tylko jeden rejestr, jednak żadna z instrukcji nie zmienia żadnego bitu w rejestrze SREG. Cała procedura mogłaby wtedy wyglądać np. tak:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Tak naprawdę to tutaj będziemy mieli główne pole do popisu. Napisanie procedury obsługi przerwania nie wpływa na jakość optymalizacji kompilatora, o czym pisałem wcześniej. Zauważyłem za to tendencje kompilatora do odkładania (w procedurach obsługi przerwań) na stos niepotrzebnych rejestrów, szczególnie w przypadku bardziej rozbudowanych procedur wywołujących dodatkowo jakieś funkcje. Dzięki temu, pisząc w ASM, można oszczędzić kilka cennych taktów. Dodatkowo mamy szansę np. na skrócenie operacji arytmetycznych poprzez ograniczenie rozmiaru zmiennej do niezbędnego minimum.

  • Wywoływanie funkcji napisanej w C z kodu napisanego w ASM

    • Funkcja bez parametrów nie zwracająca wartości

      Definicja funkcji w pliku C:

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Wywołanie funkcji w pliku ASM:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

    • Funkcja z parametrami nie zwracająca wartości

      Definicja funkcji w pliku C:

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Wywołanie funkcji w pliku ASM:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

    • Funkcja bez parametrów zwracająca wartość

      Definicja funkcji w pliku C:

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Wywołanie funkcji w pliku ASM:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

    • Funkcja z parametrami zwracająca wartość

      Definicja funkcji w pliku C:

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Wywołanie funkcji w pliku ASM:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

    • Funkcja z biblioteki standardowej C

      Pisząc w języku asembler możemy również używać funkcji z bibliotek standardowych C. Nie trzeba w tym celu dołączać plików nagłówkowych dyrektywą #include. Wystarczy tak jak w przypadku naszych funkcji umieścić ewentualne argumenty w odpowiednich rejestrach, wywołać funkcję i odczytać wynik z odpowiednich rejestrów.

      Przykładowe wyliczenie wartości bezwzględnej za pomocą funkcji abs() z biblioteki <stdlib.h>:

      Składnia: [ Pobierz ] [ Ukryj ]
      język asm
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

  • Korzystanie ze wspólnego pliku nagłówkowego

    Na koniec mały projekt przykładowy, pokazujący w jaki sposób korzystać ze wspólnego pliku nagłówkowego zarówno dla C jak i asemblera, dzięki czemu można uniknąć konieczności podwójnego definiowania stałych używanych w programie.

    Projekt napisany został dla mikrokontrolera ATmega644P. Przedstawia wprawdzie obsługę klawiatury z debouncing'iem opartym o timer, ale nie to jest jego głównym celem. Koncentrowałem się przede wszystkim na pokazaniu ogólnej struktury projektu wykorzystującego mieszany kod ze współdzielonym plikiem nagłówkowym, więc choć program powinien działać, nie mogę zagwarantować pełnej niezawodności oraz optymalności kodu.

    Projekt składa się z trzech plików, które przedstawiłem poniżej (dokładniejszy opis w komentarzach):

    plik "main.c"
    Składnia: [ Pobierz ] [ Ukryj ]
    język c
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.



    plik "keyboard.h"
    Składnia: [ Pobierz ] [ Ukryj ]
    język c
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


    plik "timer_isr.S"
    Składnia: [ Pobierz ] [ Ukryj ]
    język asm
    Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

Podsumowanie

Obawiam się trochę, że bardziej się skoncentrowałem na tym, żeby było rzeczowo, niż żeby było ciekawie. Mimo tego liczę na to, że znajdzie się ktoś, kto przeczyta to w całości unikając zaśnięcia. Mam również nadzieję, że udało mi się zebrać w jednym miejscu wszystkie informacje niezbędne do stworzenia projektu w języku C zawierającego wstawki w kodzie ASM (który się skompiluje i będzie działał prawidłowo), że było wyczerpująco i zrozumiale, no i że komuś się to kiedyś do czegoś przyda.

W razie pytań postaram się odpowiedzieć na miarę mojej skromnej wiedzy, jednak proszę o cierpliwość, ponieważ zapewne nie zawsze będę miał czas, żeby zrobić to natychmiast.

Pozdrawiam
      andrews



Ostatnio edytowano 18 gru 2016, o 09:27 przez andrews, łącznie edytowano 1 raz

Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 14 paź 2016, o 19:21 
Offline
Moderator
Avatar użytkownika

Dołączył(a): 03 paź 2011
Posty: 27309
Lokalizacja: Szczecin
Pomógł: 1041

No .... no ... uuups ... aż mnie zatkało ;) .... No! takiego arta to się nie spodziewałem ... REWELACJA i myślę, że będzie MEGA SMAKOWITYM przysmakiem dla każdego konesera dań złożonych z C i ASM :lol:

cosik pięknego ;)

_________________
zapraszam na blog: http://www.mirekk36.blogspot.com (mój nick Skype: mirekk36 ) [ obejrzyj Kurs EAGLE ] [ mój kanał YT TV www.youtube.com/mirekk36 ]



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 14 paź 2016, o 19:21 
Offline
Użytkownik

Dołączył(a): 24 sty 2012
Posty: 1469
Pomógł: 56

No i pięknie to opisałeś.
Kawał dobrej roboty. Gratuluję wiedzy!

_________________
Jestem początkujący i moje porady mogą być błędne



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 14 paź 2016, o 19:33 
Offline
Użytkownik

Dołączył(a): 07 cze 2016
Posty: 563
Pomógł: 143

Dziękuję za słowa uznania :)
Mam nadzieję, że to nie były pochopne opinie ;)
Będzie mi miło, jeśli komuś się przyda to co napisałem.



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 14 paź 2016, o 20:37 
Offline
Użytkownik
Avatar użytkownika

Dołączył(a): 23 wrz 2013
Posty: 58
Zbananowany użytkownik

Pomógł: 10

Podziękowania z mojej strony za ogrom bardzo przydatnej wiedzy, którą nam przekazałeś. Na pewno się przyda niejednokrotnie.



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 14 paź 2016, o 21:13 
Offline
Użytkownik
Avatar użytkownika

Dołączył(a): 28 wrz 2014
Posty: 1530
Lokalizacja: Warszawa
Pomógł: 55

Bardzo dziękuję - super poradnik!

_________________
--... ...-- - --- -- . -.-



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 14 paź 2016, o 21:51 
Offline
Użytkownik

Dołączył(a): 27 lut 2013
Posty: 230
Pomógł: 4

Super! Dzięki !



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 14 paź 2016, o 21:55 
Offline
Użytkownik
Avatar użytkownika

Dołączył(a): 25 mar 2015
Posty: 116
Pomógł: 16

:D :) ;) 8-) :lol: :P Dziękuję baaaaardzo!!!



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 14 paź 2016, o 23:05 
Offline
Użytkownik

Dołączył(a): 16 sty 2012
Posty: 78
Pomógł: 1

doskonały artykuł ... dzięki za olbrzymi wkład pracy żebyśmy mogli pogłębić wiedzę ....



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 15 paź 2016, o 00:35 
Offline
Nowy

Dołączył(a): 05 sty 2014
Posty: 12
Lokalizacja: Malbork
Pomógł: 1

Naprawdę świetny artykuł. Gratuluję!



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 15 paź 2016, o 14:40 
Offline
Użytkownik
Avatar użytkownika

Dołączył(a): 27 mar 2015
Posty: 44
Pomógł: 2

Dzięki :) za czas poświęcony na stworzenie tak fajnego artykułu . Przyda się taka wiedza w ... pigułce ... :)
Wielki szacunek dla wiedzy .



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 15 paź 2016, o 15:17 
Offline
Użytkownik
Avatar użytkownika

Dołączył(a): 01 lis 2015
Posty: 1448
Lokalizacja: okolice Warszawa
Pomógł: 149

Ja cie....
To się chyba mówi na to - elaborat. :D
Zauważyłem że wszystkie Twoje posty zawierają dużo cennych informacji.
Dzięki kolego andrews za poświęcony czas i przekazanie szczypty swojej wiedzy. :)
Tu żadna dobra informacja się nie zmarnuje,
ktoś zawsze gdzieś coś spożytkuje w swoich projektach. ;)



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 15 paź 2016, o 16:49 
Offline
Użytkownik

Dołączył(a): 07 cze 2016
Posty: 563
Pomógł: 143

Nie bardzo wiem, co powiedzieć, a właściwie napisać.
Wypada chyba jeszcze raz podziękować za tak pozytywne oceny.
Szczerze mówiąc nie spodziewałem się nawet, że artykuł wzbudzi tak spore zainteresowanie.
Cieszę się, że się spodobało i że będzie przydatne, bo to oznacza, że mój czas (którego nadmiarem nie dysponuję) nie poszedł na marne.

Pozostaje mi życzyć skutecznego miksowania, żeby się coś dobrego z tego upiekło ;)



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 15 paź 2016, o 17:49 
Offline
Nowy

Dołączył(a): 05 kwi 2016
Posty: 21
Pomógł: 0

Super!!! dzięki za podzielenie się wiedzą.



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 15 paź 2016, o 19:13 
Offline
Użytkownik

Dołączył(a): 06 maja 2014
Posty: 415
Lokalizacja: Kraków
Pomógł: 26

To tyle tekstu można upchnąć w jednym poście ?



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 15 paź 2016, o 20:33 
Offline
Użytkownik

Dołączył(a): 25 lip 2013
Posty: 2584
Pomógł: 128

Bardzo fajny i ciekawy artykul, bo postem to grzech go nazwac. Przyjemnie i rzeczowo opisany. Dziekuje!



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 15 paź 2016, o 21:36 
Offline
Użytkownik
Avatar użytkownika

Dołączył(a): 23 maja 2014
Posty: 317
Pomógł: 19

Konkretny rzetelnie napisany poradnik. Są w nim niektóre informacje (np. na temat konfiguracji środowiska) gdzie w innych poradnikach, albo są albo i nie. Wiedzy jest w sam raz tyle, żeby było wiadomo co z czym się je - czyli nie za dużo, nie za mało (w sam raz).
Ja również dziękuję za poradnik.
Pozdrawiam! j23

_________________
"O sygnałach bez całek" Czesław Frąc



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 15 paź 2016, o 23:05 
Offline
Użytkownik
Avatar użytkownika

Dołączył(a): 14 lut 2014
Posty: 293
Lokalizacja: Jaskółowo k. Warszawy
Pomógł: 9

Cześć.
Pozwolisz, że sobie wydrukuje Twój artykuł, oprawie i położę obok książek Mirka, co by mieć zawsze pod ręką.
Dzięki za super poradnik.

_________________
POZDROWIONKA



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 16 paź 2016, o 08:14 
Offline
Użytkownik
Avatar użytkownika

Dołączył(a): 28 lis 2012
Posty: 298
Pomógł: 13

Ja chyba też wydrukuję i chętnie dokładnie przestudiuję, na pewno się przyda! Dzięki!



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 17 paź 2016, o 07:44 
Offline
Użytkownik
Avatar użytkownika

Dołączył(a): 01 lis 2011
Posty: 265
Lokalizacja: Szczecin
Pomógł: 9

Ale musiales sie napracowac :-) Dzieki!

_________________
www.iuvo.it - Automatyka Budynkowa



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 18 gru 2016, o 09:37 
Offline
Użytkownik

Dołączył(a): 07 cze 2016
Posty: 563
Pomógł: 143

Przepraszam, że dłuższy czas nie odpowiadałem w tym wątku, jednak otrzymałem tyle pochwał, że zabrakło mi słów na podziękowania :)

Teraz piszę, dlatego że edytowałem swój pierwszy post. Chciałem tylko poinformować, że nie wprowadziłem żadnych znaczących zmian, poprawiłem tylko kilka "literówek", które przypadkowo zauważyłem. Mam nadzieję, że teraz już nie ma błędów.

Pozdrawiam,
    andrews



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 4 cze 2017, o 15:21 
Offline
Użytkownik
Avatar użytkownika

Dołączył(a): 01 cze 2013
Posty: 137
Lokalizacja: Kraków
Pomógł: 0

Temat już trochę stary, ale również chciałem się przyłączyć do podziękowań. Świetnie opisane, łatwe do wykorzystania - stukrotne dzięki :). Nie przypuszczałem że wstawki asemblerowe mogą być tak wygodne w użyciu. W projektach zawsze odpuszczałem asemblera przez - jak mi się wtedy wydawało - "niekompatybilność" przyjemnych w użyciu argumentów z C i rejestrów ogólnego przeznaczenia.

_________________
Więcej dziwactw na: www.youtube.com/user/mopsiok



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 7 paź 2018, o 19:30 
Offline
Nowy

Dołączył(a): 01 lut 2015
Posty: 2
Pomógł: 0

Gratulacje za fajny wykład, brakuje mi tylko opisu asemblera online, bo jest trudny i każdy opis jest cenny.
Żeby nie być gołosłowny to daję przykład najkrótszego przerwania od zegara. Ustawia ono jeden bit który
pozniej testujemy w głownej pętli, warunkiem jest żeby bit był ustawiany za pomocą sbi co możemy sprawdzić w pliku *.lss wgenerowanym przez avr studio:

Składnia: [ Pobierz ] [ Ukryj ]
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


a deklaracja zmiennej wygląda tak:
Składnia: [ Pobierz ] [ Ukryj ]
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


Składnia: [ Pobierz ] [ Ukryj ]
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

oczywiscie procek musi być nowszych które mają rejestry GPIOR.

------------------------ [ Dodano po: 2 minutach ]

Zapomniałem, w main testujemy flagę:
Składnia: [ Pobierz ] [ Ukryj ]
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 8 paź 2018, o 18:56 
Offline
Użytkownik

Dołączył(a): 07 cze 2016
Posty: 563
Pomógł: 143

Januszkk napisał(a):
brakuje mi tylko opisu asemblera online

Wstawki "inline assembly" to wbrew pozorom temat odmienny (od tego, co tu przedstawiłem), dość obszerny i na tyle skomplikowany, że wymaga osobnego artykułu, aby go solidnie opisać.

Jeśli chodzi o Twój przykład, to tak naprawdę tam masz tylko jedną instrukcję w asm, reszta to język C.

Podstawowa korzyść ze stosowania "inline assembly" jest taka, że pisząc kod w asm pozostawiamy kompilatorowi C dobór rejestrów, dzięki czemu ma on większe możliwości optymalizacji kodu. Instrukcja reti nie korzysta z żadnych rejestrów, więc nic tu się nie da zoptymalizować. Ponadto, pisząc akurat procedurę obsługi przerwania, kompilator i tak musi zapamiętać na stosie wszystkie używane wewnątrz procedury rejestry, więc tu i tak nic nie da się zoptymalizować.

Oczywiście można zrobić tak, jak to przedstawiłeś, jednak zysku z zastosowania "inline assembly" tutaj nie będzie, a osobiście wydaje mi się, że czytelniejsze byłoby (bez konieczności definiowania struktur, pól bitowych i wskaźników na rejestry) dodanie do projektu pliku *.s z zawartością:
Składnia: [ Pobierz ] [ Ukryj ]
język asm
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

Dodatkowo mamy pewność, że instrukcja SBI na pewno będzie użyta, a w przypadku próby zmiany GPIOR0 np. na GPIOR1 (który jest poza zasięgiem instrukcji SBI), program asemblujący zgłosi błąd. W przypadku Twojego rozwiązania zmiana na GPIOR1 spowodowałaby po prostu wygenerowanie przez kompilator (bez ostrzeżenia) innego kodu (np. z użyciem instrukcji OUT, który byłby w tym przypadku błędny), trzeba więc sprawdzać po kompilacji plik *.lss

Ewentualnie, aby operować nazwami symbolicznymi zamiast wartościami liczbowymi czy też nazwami rejestrów, można też dodać plik nagłówkowy zawierający takie nazwy symboliczne i dołączyć go dyrektywą #include zarówno do pliku *.s, jak i do odpowiedniego pliku źródłowego *.c

Proszę mnie źle nie zrozumieć, ja nie neguję zalet wstawek "inline assembly", jednak moim skromnym zdaniem czytelność kodu z takimi wstawkami drastycznie spada, dlatego stosuję je tylko tam, gdzie faktycznie można odnieść z tego konkretną korzyść. Ale wiadomo, każdy robi jak mu wygodniej ;)



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
Wyświetl posty nie starsze niż:  Sortuj wg  
Utwórz nowy wątek Odpowiedz w wątku  [ Posty: 24 ] 

Strefa czasowa: UTC + 1


Kto przegląda forum

Użytkownicy przeglądający ten dział: Brak zidentyfikowanych użytkowników i 0 gości


Nie możesz rozpoczynać nowych wątków
Nie możesz odpowiadać w wątkach
Nie możesz edytować swoich postów
Nie możesz usuwać swoich postów
Nie możesz dodawać załączników

Szukaj:
Skocz do:  
Sitemap
Technologię dostarcza phpBB® Forum Software © phpBB Group phpBB3.PL
phpBB SEO