- Drukuj
- 27 Dec 2012
- Programowanie
- 33380 czytań
- 0 komentarzy
Autor: tsulej
Celem poniższego tekstu jest przybliżenie Wam kompilatora SDCC jako narzędzia do tworzenia oprogramowania na ZX Spectrum (a w zasadzie na procesor Z80).
Od razu zaznaczam, że jestem świeżym użytkownikiem SDCC i na rozpoznanie go poświęciłem kilkanaście godzin. Jednak z racji tego, że udało mi się wykuć kawałek działającego softu, postanowiłem się moimi odkryciami podzielić.
Zapraszam i z góry przepraszam za wszelkie błędy, niezgodności czy uproszczenia.
Ze względu na, jak to się mówi w biznesie, "time to market". Nie programowałem na ZX Spectrum ponad 20 lat. Podejście do assemblera mi nie wyszło - zabrakło cierpliwości i rejestrów. Przeglądałem też z88dk, ale brak sensownej dokumentacji (szczególnie do splib2 czy sp1) i parę nieudanych prób kompilacji nieco mnie zniechęciły. W przypadku SDCC jest dokumentacja, znalazłem grę o bardzo czytelnych źródłach i na dodatek szybko uruchomiłem sobie testowy programik, który coś tam rysował po ekranie. Szalę przeważyło porównanie SDCC i z88dk. Rozmiar kodu i szybkość wykonywania programów jest na korzyść SDCC.
- Porównanie znajduje się pod adresem: http://www.cpcmania.com/Docs/Programming/SDCC_vs_z88dk_Comparing_size_and_speed.htm
- Magic Tokens - gra ze źródłami pod SDCC: http://www.worldofspectrum.org/infoseekid.cgi?id=0025075
- Mój program - scroll na atrybutach http://www.speccy.pl/archive/prod.php?id=223
Zacznę od tego jak sobie przygotować narzędziówkę do sprawnej pracy. Potrzebne są:
- SDCC - http://sdcc.sourceforge.net/, ja używam wersji pod Windows (z instalera jak i samodzielnie kompilowanej pod Cygwin). Są jeszcze wersje pod Linux jak i Mac OS X. Uwaga: wersja z repozytorium (dev) i wersja stabilna nieco się różnią i generują nieco odmienny kod.
- źródeł SDCC - do podglądu dostępnych funkcji i ich implementacji. To co jest interesujące znajduje się w katalogach device/include i device/lib.
- hex2bin - http://hex2bin.sourceforge.net/; narzędzie do konwersji plików wynikowych SDCC do binarnego formatu. SDCC produkuje kompilat w formie tekstowych plików hex w formacie Intela. Pod Linux-y są zamienniki (np. SRecord - http://srecord.sourceforge.net/).
- bin2tap - http://zeroteam.sk/bin2tap.html; narzędzie do generowania TAPa.
To jeszcze niestety nie wszystko. Kompilatorowi trzeba dostarczyć szablon aplikacji, w którym zaznaczymy organizację kodu - adres startowy, miejsce na kod, dane, stos, wstawimy kod inicjujący itp. Standardowy szablon dostarczany z SDCC kod ustawia pod adresem 0x100. To trzeba zmienić. Poniżej przykład przygotowany przeze mnie.
Słowo komentarza:
- sekcja gsinit - to jest miejsce dla kompilatora, z tego co zaobserwowałem nic tam docelowo nie ląduje. Podejrzewam, że jest to miejsce na inicjowanie zmiennych globalnych, które w SDCC dla Z80 nie działa.
- sekcja init - tu startujemy program, jp _main jest konieczne gdyż kompilator wrzuca nasz kod jak leci. Funkcje wstawiane są wg kolejności z kodu.
- sekcja code_and_data - tu ląduje nasz kod i zmienne globalne.
- sekcja heap - dostępna dla malloc/balloc - nie testowałem.
; minimal template for sdcc for zx spectrum programs ; init code starts at 0x7ff6 ; main code starts at 0x8000 ; stack is set to 0xfffe .globl _main .area _HEADER (ABS) .org 0x7ff6 ; 10 bytes in init section init: di ld sp,#0xfffe ; stack set to end of RAM call gsinit ; sdcc init jp _main ; just to be sure we start at main code_and_data: .org 0x8000 .area _CODE ; code and main function lands here .area _DATA ; global data section gsinit: .area _GSINIT ; sdcc startup code .area _GSFINAL ret heap: .area _HEAP _HEAP_start::
Nic nie stoi na przeszkodzie aby umieścić tu inny kod wspólny dla wszystkich naszych programów, np. tablicę wektorów pod IM2
Plik ten kompilujemy poleceniem (sdasz80 jest dostępny w pakiecie SDCC):
sdasz80.exe -o crt0.rel crt0.s
Załóżmy, że mamy już program napisany (dla przykładu: scroll.c) i chcemy skompilować. Opiszę tutaj parametry wykorzystywane przeze mnie. Linia poleceń wygląda tak:
sdcc -mz80 --reserve-regs-iy --opt-code-speed --max-allocs-per-node 100000 --code-loc 0x8000 --data-loc 0 --no-std-crt0 crt0.rel scroll.c -o scroll.ihx
Lecąc po kolei:
- -mz80 - informujemy kompilator, że ma generować kod pod z80. Na marginesie dodam, że sam kompilator może generować kod dla kilkunastu procesorów.
- --reserve-regs-iy - ta opcja kosztowała mnie kilka godzin. W trybie przerwań IM1 wywoływany jest kod z ROM, a ROM ZX Spectrum ustawia sobie rejestr IY na stałą wartość i namiętnie z niego korzysta. Aby kompilator nie używał IY stosujemy tę opcję.
- --opt-code-speed - nastawienie na szybkość kodu. Bliźniacza opcja --opt-code-size - nastawienie na rozmiar. W moim przypadku różnica wynosi... 1 bajt.
- --max-allocs-per-node 100000 - jeśli dobrze rozumiem - optymalizator. Im większa wartość tym lepszy kod. Przejście z wartości 10000 na 100000 w moim przypadku spowodowało, że mój przykład wykonuje się w 1,5 ramki zamiast w 2,5. Im większa wartość tym czas kompilacji się wydłuża.
- --code-loc 0x8000 i --data-loc 0 - w przeciwieństwie do crt0, tutaj podajemy sekcje dla linkera. Po paru eksperymentach dochodzę do wniosku, że code-loc wstawia funkcje biblioteczne tam gdzie wskazujemy, chyba że interferuje to z naszym kodem, wtedy linker wrzuca biblioteki na w pierwsze wolne miejsce. data-loc ustawiona na 0 działa dobrze - choć nie jest to logiczne, zmienne funkcji bibliotecznych lądują tam gdzie trzeba (chyba za kod bibliotek).
- --no-std-crt0 - mówimy kompilatorowi aby nie korzystał ze standardowego crt0. Oznacza to, że musimy podać stworzony przez nas i powinien to być pierwszy plik dla kompilatora.
Kompilator generuje całą masę plików, najważniejsze to:
- .asm - kod assemblera wygenerowany z pliku c. Przed linkowaniem. Bardzo dobre miejsce na obejrzenie jak sobie poradził kompilator. Bardzo dobrze okomentowane.
- .lst - j.w. + wstawione opcody i co ciekawe ile cykli procesora dana instrukcja kosztuje.
- .map/.noi - dokładne adresy wszystkich funkcji i zmiennych, a także wykorzystane i zlinkowane funkcje biblioteczne (.map).
- .ihx - plik wynikowy.
Dodatkowe informacje:
- dokumentacja SDCC: http://sdcc.sourceforge.net/doc/sdccman.pdf
- strona wiki na temat portu na procesor z80: http://sdcc.sourceforge.net/mediawiki/index.php/Z80_port
- strona wiki na temat optymalizatora (--max-allocs-per-node): http://sdcc.sourceforge.net/mediawiki/index.php/Z80_code_size
Przejdę teraz do samego kodu i pewnych aspektów specyficznych dla SDCC. Wymienię głównie te, które przetestowałem i jestem ich w miarę pewien.
Z tego co doczytałem, jest wzięty z GCC, więc można korzystać bez ograniczeń te same komendy co w "dużych" kompilatorach C.
Wystarczy, że damy #include i wykorzystamy wybraną funkcję w kodzie. SDCC sam dolinkuje odpowiednie biblioteki. Tutaj mała uwaga, kompilator pozwala na użycie liczb całkowitych 32-bitowych. Przy ich wykorzystywaniu może się zdarzyć, że zostaniemy obciążeni 2kb bagażem funkcji arytmetycznych do dużych liczb. Co dokładnie możemy załączyć, można dowiedzieć się ze źródeł (katalog device).
Do dyspozycji mamy typy: * (wskaźnik), float, char, int (short), long. Trzy ostatnie także w wersji unsigned. Występuje jeszcze typ bool - ale nie wykorzystywałem. Tablica i tekst to wskaźniki na odpowiedni obszar pamięci.>
Zmienne globalne są inicjalizowane pod warunkiem, że są const. W przeciwnym wypadku nie są inicjowane - to jest chyba bug SDCC, bo kod się kompiluje a efektu nie ma. Twórcy SDCC sugerują aby tam gdzie się da stosować zmienne lokalne, bo wtedy pakowane są do rejestrów lub załatwiane organizacją kodu. Oczywiście o ile się da. Bo jeśli się nie da to pakowane są na stos i odwołania do nich są wykonywane przez rejestr IX. Do zmiennych globalnych kompilator zazwyczaj dostaje się przez rejestr HL.
Zmienną można oznaczyć prefixem __at aby wskazać konkretny adres w pamięci. Przykład:
#define u8 unsigned char #define FRAMES 23672 // number of frames address __at FRAMES u8 srand1; // ROM number of frames variables __at FRAMES+1 u8 srand2; __at FRAMES+2 u8 srand3;
Odwołanie się do takiej zmiennej powoduje odczyt bajtu spod wskazanego adresu (przypisanie to zapis).
Jeszcze uwaga na temat liczb, można je podawać w systemie dziesiętnym, hex (np. 0xff) lub binarnym (np. 0b01011010) i zakończyć suffixem typu (np 10L, 100UL). W przypadku wstawek assemblerowych liczby poprzedzamy znakiem '#'.
Tutaj ułatwienie polega, że możemy oznaczyć sobie zmienną w odpowiedni sposób, tak że zapis lub odczyt zmiennej zamieniany jest na odpowiednie operacje na porcie. Przykładowa deklaracja (globalna):
__sfr __at 0xfe border; // port for border colour setting __sfr __banked __at 0x7ffe keyboard; // port for reading keyboard (space)Wykorzystanie:
a = keyboard & 1; if(a) { border = 0; } else { border = 7; }Generowany kod assemblera:
;test.c:41: a = keyboard & 1; ld a,#>(_keyboard) in a,(#<(_keyboard)) rrca jr NC,00102$ ;test.c:42: if(a) { ;test.c:43: border = 0; ld a,#0x00 out (_border),a jr 00103$ 00102$: ;test.c:45: border = 7; ld a,#0x07 out (_border),a 00103$:
Jest to odpowiednik assemblerowego LDIR. Kod:
#includejest bezpośrednio zamieniany na:memcpy((void *)ATTR2,buff,256);
;scroll.c:235: memcpy((void *)ATTR2,buff,256); ld hl,#_buff ld de,#0x5900 ld bc,#0x0100 ldir
Parametry funkcji przekazywane są przez stos, wartości zwracane są za pośrednictwem rejestrów: L (bajt), HL (słowo) lub DEHL (dwa słowa). Parametrów nie ściągamy ze stosu poprzez POP, tylko dostajemy się poprzez adres. Kod funkcji przez kompilator otaczany jest następująco:
push ix ld ix,#0 add ix,sp ; tutaj kod funkcji, do parametrów odwołujemy się przez IX+nn pop ix ret
W przypadku oznaczenia funkcji jako __naked kompilator wrzuca tylko ciało funkcji bez powyższego kodu. Oznacza to, że powinniśmy zatroszczyć się przynajmniej od RET.
Gdy funkcję oznaczymy __critical, na początku wywołania wyłączane są przerwania (di) i włączane na koniec (ei).
__interrupt stosowane jest do oznaczenia funkcji wykonywanej podczas przerwań. Kompilator kończy ją RETI. Jeśli dodatkowo taką funkcję oznaczymy __critical, kompilator zakończy ją RETM. Niestety nie testowałem tej funkcjonalności.
Assemblera możemy uzywać w dwóch trybach. Inline i jako blok. W pierwszym przypadku robimy to otaczając naszą instrukcję słówkiem kluczowym __asm__ i instrukcją jako parametr. Na przykład: __asm__ (" halt "); W drugim przypadku blok assemblera otaczamy słówkami __asm i __endasm; Dokumentacja assemblera znajduje się pod adresem: http://sdcc.svn.sourceforge.net/viewvc/sdcc/trunk/sdcc/sdas/doc/asxhtm.html Garść informacji:
- stałe należy poprzedzić znakiem '#'. Np. ld a,#8 (ładuje 8 do akumulatora)
- do zmiennych globalnych dostajemy się poprzedzając nazwę zmiennej znakiem podkreślenia '_'. Np. ld hl,#_textbuf (załaduj adres zmiennej textbuf do rejestru HL)
- młodszy/starszy bajt adresu wyznacza się wyrażeniami #<(_textbuf) i #>(_textbuf). Np. ld a,#<(_textbuf) (załaduj młodszy bajt adresu _textbuf do akumulatora)
- można wykonywać standardowe operacje arytmetyczne i logiczne (+,-,*,/,%,<<,>>, &,|,^,~)
- aby dobrać się do bieżącego adresu używami kropki '.'. Np. ld a,7; ld hl,#.-1 (załaduj adres liczby 7 do HL)
- labelki możemy oznaczać albo tekstem albo liczbą zakończoną znakiem dolara '$'. Np. 00101$: jp 00101$
Pod tym linkiem załączam przykład wraz z kodem źródłowym i wszystkimi plikami generowanymi przez kompilator. Przykład to prosty scroll na atrybutach z losowo zmieniającym się tłem. Po naciśnięciu spacji zmienia się kolor scrolla. Możecie tam sobie obejrzeć większość opisanych przeze mnie elementów. Kompilujemy przez wywołanie make.bat. Kod wynikowy ma 1420 bajtów z czego 896 bajtów to mój kod, 88 funkcje biblioteczne (obliczanie modulo), a reszta to zmienne globalne i stałe. W pierwotnej wersji używałem bibliotecznej funkcji rand(), jednak jak kompilator dorzucił mi 2kb kodu to zrezygnowałem.
Pytania i uwagi zostawiajcie w komentarzach pod artykułem lub na forum. Życzę miłego kodowania!
Zaloguj się , żeby móc zagłosować.