|
Użytkownik |
Dołączył(a): 07 cze 2016 Posty: 563
Pomógł: 143
|
PrologJakiś 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ściZakł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):
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 asembleraAsembler 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:
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:
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.:
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.:
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:
- 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).
- 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:
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.:
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:
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:
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 GCCMieszają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
- Jeśli rozmiar argumentu jest liczbą nieparzystą, dodajemy do rozmiaru 1.
- Tak obliczony rozmiar odejmujemy od numeru rejestru bazowego, uzyskując nowy numer rejestru bazowego.
- 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).
- 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.
- 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.
- 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:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod. arg1: 4 bajty
- 4 jest liczbą parzystą, więc pozostaje bez zmian.
- 26 - 4 = 22 rejestr bazowy dla pierwszego argumentu to r22.
- 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] - Nie dotyczy naszego przypadku.
- Nie dotyczy naszego przypadku.
- Kontynuujemy z następnym argumentem.
arg2: 1 bajt
- 1 jest liczbą nieparzystą, więc dodajemy 1 i otrzymujemy rozmiar równy 2
- 22 - 2 = 20 rejestr bazowy dla drugiego argumentu to r20.
- Numer rejestru r20 jest większy od numeru rejestru r8 więc przypisujemy:
Kod: r20 = arg2[byte0] - Nie dotyczy naszego przypadku.
- Nie dotyczy naszego przypadku.
- Kontynuujemy z następnym argumentem.
arg3: 2 bajty
- 2 jest liczbą parzystą, więc pozostaje bez zmian.
- 20 - 2 = 18 rejestr bazowy dla pierwszego argumentu to r18.
- Numer rejestru r18 jest większy od numeru rejestru r8 więc przypisujemy:
Kod: r18 = arg3[byte0] r19 = arg3[byte1] - Nie dotyczy naszego przypadku.
- Nie dotyczy naszego przypadku.
- Kończymy - to był ostatni argument.
Przykład 2:
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:
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. MiksowanieZaznaczam, ż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ą:
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:
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):
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:
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:
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:
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:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Definicja funkcji w pliku ASM:
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:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Definicja funkcji w pliku ASM:
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:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Definicja funkcji w pliku ASM:
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:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Definicja funkcji w pliku ASM:
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:
- 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).
- 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. - 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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>:
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"
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
plik "keyboard.h"
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
plik "timer_isr.S"
język asm
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
PodsumowanieObawiam 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
Ostatnio edytowano 18 gru 2016, o 09:27 przez andrews, łącznie edytowano 1 raz
|
|