speccy.pl
Facebook Like


SPECCY.PL

[SPECCY.PL PARTY 2023.1]

[WIKI SPECCY.PL]
Polecamy

KOMITET SPOŁECZNY KRONIKA POLSKIEJ DEMOSCENY
PIXEL HEAVEN 2023
AYGOR
Forum ZX Spectrum
Zawartość panelu chwilowo niedostępna
Archiwum plików ZX Spectrum
Nawigacja
Programowanie w C z użyciem SDCC
Kompilator SDCC

Autor: tsulej

Wstęp

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.

Dlaczego SDCC?

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.

Narzędzia

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.
Jak widać sprawa nie jest tak prosta jak w przypadku z88dk, który TAPy generuje z marszu. Do samego programowania, dowolny edytor tekstowy i w przypadku Windows proponuję instalację Cygwin (terminal z bash + narzędzia z systemów Unix).

crt0.s

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

SDCC

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.
Potem podajemy swoje pliki i nazwę pliku wynikowego (opcja -o). Funkcje biblioteczne kompilator sam sobie pobiera do linkowania na podstawie kodu i plików nagłówków.

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:

Razem w pakiecie z samym kompilatorem dostajemy jeszcze assembler, preprocesor, linker, binutils, debugger, tester i symulator - poza assemblerem nie stosowałem.

Kod

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.

Preprocesor C

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.

Funkcje biblioteczne

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).

Zmienne i wartości

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 '#'.

Porty

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$:

memcpy()

Jest to odpowiednik assemblerowego LDIR. Kod:

	#include 
	memcpy((void *)ATTR2,buff,256);

jest bezpośrednio zamieniany na:
	;scroll.c:235: memcpy((void *)ATTR2,buff,256);
	ld	hl,#_buff
	ld	de,#0x5900
	ld	bc,#0x0100
	ldir

Funkcje, __naked, __critical, __interrupt

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.

Assembler

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$

Przykład

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.

Koniec

Pytania i uwagi zostawiajcie w komentarzach pod artykułem lub na forum. Życzę miłego kodowania!

Brak komentarzy. Może czas dodać swój?
Dodaj komentarz
Zaloguj się, aby móc dodać komentarz.
Oceny
Tylko zarejestrowani użytkownicy mogą oceniać zawartość strony
Zaloguj się , żeby móc zagłosować.

Brak ocen. Może czas dodać swoją?