[SDCC] Kompilator języka C dla Z80

Kącik dla elektroniki retro - układy, urządzenia, podzespoły, literatura itp.
Awatar użytkownika
gaweł
Geek
Geek
Posty: 1262
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

[SDCC] Kompilator języka C dla Z80

Postautor: gaweł » środa 19 lip 2017, 15:23

Pakiet SDCC
Istnieje niekomercyjny pakiet oprogramowania pozwalający na tworzenie programów między innymi dla mikrokontrolerów Z80. Jest to pakiet SDCC (ang Small Device C Compiler) i pozwala na tworzenie kodu binarnego dla kilku mikroprocesorów oraz mikrokontrolerów. Pakiet ten (plik o nazwie sdcc-3.6.0-setup.exe) można pobrać ze strony http://sdcc.sourceforge.net/snap.php. Instalacja pakietu pod windozą nie sprawia żadnych problemów. Jest tylko jedna sugestia: warto przy instalacji określić inne miejsce docelowe niż jest proponowany (przy instalacji proponowany jest katalog c:\Program files\sdcc). Ja podałem następujące miejsce docelowe: e:\sdcc (wynika to z tego, że w pakiecie nie ma elementu typu IDE i, w fazie eksperymentów posługiwałem się plikami typu BAT do uruchamiania kompilatora; w przyszłości trzeba będzie pochylić się nad systemem MAKEFILE lub czymś podobnym, obecnie w fazie eksperymentów posługuję się plikami typu BAT a spacja w nazwie kartoteki C:\Program Files znaczące w tym przeszkadza). Po instalacji w e:\sdcc\doc znajduje się elementarna dokumentacja sdccman.pdf.
O liście dostępnych procków można przekonać się realizując „zapytanie do pakietu”: sdcc –version.
ilu01-01.png

Na liście znajduje się między innymi Z80 → główny powód zainteresowania pakietem.
Pierwszy program dla proca Z80. Za pomocą dowolnego edytora plików tekstowych utworzony zostaje plik o nazwie test1.c o następującej zawartości.

Kod: Zaznacz cały

int GloblVar ;

main ( )
{
  unsigned short j ;
  unsigned short n ;
/*---------------------------------------------------*/
  while ( 1 )
  {
    n = 1 ;
    for ( j = 0 ; j < 4 ; j++ )
    {
      GloblVar = n ;
      n <<= 1 ;
    } /* for */ ;
    n = 0x80 ;
    for ( j = 0 ; j < 4 ; j++ )
    {
      GloblVar = n ;
      n >>= 1 ;
    } /* for */ ;
  } /* while */ ;
} /* main */

Nie jest istotne co program robi, im jest prostszy, tym łatwiej poznać filozofię pakietu. Głównym celem takiego program jest poznanie co i jak zostało zrobione.
Do kompilacji utworzony jest plik o nazwie cc.bat o następującej zawartości:

Kod: Zaznacz cały

e:\sdcc\bin\sdcc -mz80 --data-loc 0x8000 test1.c

Polecenie to nakazuje wygenerować binarną wersję program dla mikroprocesora z80 (parametr: -mz80), w którym pamięć ram znajduje się w przestrzeni adresowej od adresu 8000H do adresu FFFFH → będziemy udawać, że w systemie z Z80 znajduje się RAM typu 62256 (Static RAM o wielkości 32kB), tekst programu znajduje się w pliku o nazwie test1.c. Ponieważ Z80 po resecie rusza od wykonania instrukcji spod adresu 0000H, w domyśle z przestrzeni od adresu 0000H do adresu 7FFFH znajduje się pamięć EPROM przeznaczona na kod programu. By uruchomić kompilację, należy uruchomić w kartotece program cmd.exe (gdzie znajduje się program źródłowy test1.c). Tam należy uruchomić makropolecenie cc.bat.
ilu01-02.png

Po pierwszej kompilacji widać, że pakiet działa w ten sposób, że przetwarza program napisany w języku C na program napisany w języku assebler i ten docelowo jest kompilowany i linkowany (do programu jest dolinkowana część rozruchowa o nazwie crt0.rel, którą można odnaleźć w strukturze kartoteki e:\sdcc.
W wyniku działania kompilatora powstają między innymi pliki:
  • test1.asm – przetłumaczony na asembler program w języku C,
  • test1.ihx – wynikowy program w zapisie intel-hex (pliki o rozszerzeniu IHX zawierają wygenerowany kod w zapisie intel-hex),
  • test1.lst – listing z kompilacji programu asemblerowego,
  • test1.map – wydruk zawierający informacje o przydzielonych adresach dla poszczególnych elementów programu (w sensie nazw segmentów),
  • test1.rel – wynik kompilacji (jako półkompilat), który jest „obrabiany” przez linker (pliki o rozszerzeniu REL są wynikiem kompilacji i są w formacie tekstowym i można je sobie przeglądać).
Przeglądając plik test1.map mamy:

Kod: Zaznacz cały

ASxxxx Linker V03.00 + NoICE + sdld,  page 1.
Hexadecimal  [32-Bits]

Area                                    Addr        Size        Decimal Bytes (Attributes)
--------------------------------        ----        ----        ------- ----- ------------
.  .ABS.                            00000000    00000000 =           0. bytes (ABS,CON)

      Value  Global                              Global Defined In Module
      -----  --------------------------------   ------------------------
     00000000  .__.ABS.                           test1
     00000000  l__BSEG                         
     00000000  l__BSS                         
     00000000  l__CABS                         
     00000000  l__DABS                         
     00000000  l__HEADER                       
     00000000  l__HEAP                         
     00000000  l__HOME                         
     00000000  l__INITIALIZED                 
     00000000  l__INITIALIZER                 
     00000000  s__CABS                         
     00000000  s__DABS                         
     00000000  s__HEADER                       
     00000000  s__HEADER0                     
     00000000  s__HEADER1                     
     00000000  s__HEADER2                     
     00000000  s__HEADER3                     
     00000000  s__HEADER4                     
     00000000  s__HEADER5                     
     00000000  s__HEADER6                     
     00000000  s__HEADER7                     
     00000000  s__HEADER8                     
     00000001  l__GSFINAL                     
     00000002  l__DATA       <<< wielkość segmentu DATA (obszaru zmiennych globalnych)
     00000002  l__HEADER1                     
     00000002  l__HEADER2                     
     00000002  l__HEADER3                     
     00000002  l__HEADER4                     
     00000002  l__HEADER5                     
     00000002  l__HEADER6                     
     00000002  l__HEADER7                     
     00000003  l__HEADER0                     
     0000000C  l__HEADER8                     
     0000000F  l__GSINIT                       
     00000032  l__CODE                         
     00000200  s__CODE                         
     00000232  s__GSINIT                       
     00000232  s__HOME                         
     00000232  s__INITIALIZER                 
     00000241  s__GSFINAL                     
     00008000  s__DATA       <<< początek segmentu DATA (adres w przestrzeni adresowej)
     00008002  s__BSEG                         
     00008002  s__BSS                         
     00008002  s__HEAP                         
     00008002  s__INITIALIZED                 

ASxxxx Linker V03.00 + NoICE + sdld,  page 2.
Hexadecimal  [32-Bits]

Area                                    Addr        Size        Decimal Bytes (Attributes)
--------------------------------        ----        ----        ------- ----- ----------
_CODE                               00000200    00000032 =          50. bytes (REL,CON)

      Value  Global                              Global Defined In Module
      -----  --------------------------------   ------------------------
     00000200  __clock                            crt0
     00000204  _exit                              crt0
     0000020A  _main                              test1

(…)
User Base Address Definitions

_CODE = 0x0200
_DATA = 0x8000

Z grubsza wszystko się zgadza. Pozostało sprawdzić działanie programu. W tym celu został użyty pakiet AVSIMZ80. W okienku program cmd.exe uruchomiony zostaje program avsimz80.exe.
ilu01-03.png

Do symulacji należy wybrać wariant A (sam procesor, bez urządzeń peryferalnych). W pierwszej kolejności należy załadować symulowany program L [od Load] P (od Program) i podać nazwę pliku test1.ihx. Później „podłączyć” pamięć RAM: S [od Set], M [od Memory-map], A [od random-Access] i wpisać: m:8000h <enter> oraz później m:ffffh <enter>). To spowoduje utworzenie przestrzeni pamięci RAM od adresu 8000 hex do FFFF hex. Program gotowy jest do symulacji.
ilu01-04.png

Jak widać, po resecie, procesor Z80 ma do wykonania instrukcję bezwarunkowego skoku pod adres 100 hex. Klawiszem F10 można wykonać pojedynczą instrukcję symulowanego programu.
ilu01-05.png

Kolejną instrukcją jest załadowanie wskaźnika SP stałą o wartości 0. Tutaj sięgam do opisu instrukcji mikroprocesora Z80. W dokumentacji jest napisane, że instrukcja CALL (również wszelkie instrukcje PUSH) najpierw dekrementują wskaźnik stosu a następnie realizują zapis odpowiednich danych na stosie. Implikuje to, że to rozwiązanie zadziała poprawnie w każdym przypadku, gdzie pamięć RAM kończy się na adresie FFFF hex, czyli standardowa część rozruchowa (crt0.rel) jest właściwa w takich przypadkach. Kolejna instrukcja (CALL 232 hex) ma za zadanie zainicjowanie stałych obszarów zmiennych (w tym konkretnym przypadku nie ma inicjowanego obszaru, więc następuje szybki powrót). Kolejna instrukcja CALL 20A hex jest wywołaniem funkcji main. Rzeczywiście, program zrobił 4 obroty pierwszej pętli i 4 obroty drugiej pętli. Zadziałał, jak było napisane.

Załącznik: test1.zip
test1.zip
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse

Awatar użytkownika
gaweł
Geek
Geek
Posty: 1262
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [SDCC] Kompilator języka C dla Z80

Postautor: gaweł » środa 19 lip 2017, 15:46

Grzebanie po portach
Ponieważ trudno sobie wyobrazić procesor Z80 bez „grzebania” po portach, to powstał drugi program do eksperymentów. W programie w języku C istnieje możliwość opisu przestrzeni portów. Zapis jest następujący:
__sfr __at <adres> <nazwa portu>
Program testowy ma następującą treść (maksymalnie prosty jednak dający możliwość weryfikacji działania poprzez symulację działania w programie AVSIMZ80). Wybrana została lokacja o adresie 0 ze względu na to, że AVSIMZ80 w wariancie symulacji samego procesora (wariant A określany po uruchomieniu programu symulacyjnego) pokazuje porty o adresach od 0 do 3 na swoim ekranie.

Kod: Zaznacz cały

__sfr __at 0x00 IO_Port ;

main ( )
{
  unsigned short j ;
  unsigned short n ;
/*---------------------------------------------------*/
  while ( 1 )
  {
    n = 1 ;
    for ( j = 0 ; j < 4 ; j++ )
    {
      IO_Port = n ;
      n <<= 1 ;
    } /* for */ ;
    n = 0x80 ;
    for ( j = 0 ; j < 4 ; j++ )
    {
      IO_Port = n ;
      n >>= 1 ;
    } /* for */ ;
  } /* while */ ;
} /* main */

Po kompilacji:
ilu02-01.png
można przejrzeć wariant assemblerowy programu. Rzeczywiście, widać, że został wygenerowany kod, który wysyła odpowiednie dane do portu.

Kod: Zaznacz cały

;test2.c:110: IO_Port = n ;
   ld   a,c
   out   (_IO_Port),a
Zostaje uruchomiony symulator procesora Z80. Oprócz załadowania programu do symulacji (test2.ihx), „podłączeniu” pamięci RAM (S M A m:8000h <enter> m:ffffh <enter>) należy „podłączyć” przestrzeń portów (robi się to identycznie jak dla pamięci z tym, że podaje się „kwalifikator” „i” na określenie przestrzeni we/wy: S M A i:0 <enter> i:0 <enter>). To określa dla programu symulacyjnego, że jest używany port we/wy o adresie 0 (w ogólności można utworzyć całą przestrzeń adresową specyfikując i:0 <enter< i:255 <enter>). Przestrzeń adresową można przejrzeć wydając polecenie: V [od View], M [od Memory-map]. Efekt jest następujący:
ilu02-02.png
Symulacja działania programu przebiegła zgodnie z oczekiwaniami.
ilu02-03.png
Zapisywane do portu dane są wyświetlane przez program AVSIMZ80.
Bazując na „dotychczasowych sukcesach” i dokumentacji sdccman.pdf, wzrosła chęć „bardziej finezyjnych” eksperymentów. Powstał kolejny program:

Kod: Zaznacz cały

#define PORTA_8255_Base 0x10

__sfr __at (PORTA_8255_Base+0) PORTA_8255 ;
__sfr __at (PORTA_8255_Base+1) PORTB_8255 ;
__sfr __at (PORTA_8255_Base+2) PORTC_8255 ;
__sfr __at (PORTA_8255_Base+3) PORTCONTROL_8255 ;

__sfr __banked __at 0x23 IoPort1;
__sfr __at 0x24 IoPort2;
__sfr __banked __at 0x25 IoPort3;
__sfr __banked __at 0x26 IoPort4;

main ( )
{
  char Ch ;
/*---------------------------------------------------*/
  while ( 1 )
  {
    PORTA_8255 = 0x01 ;
    PORTB_8255 = 0x02 ;
    PORTC_8255 = 0x03 ;
    PORTCONTROL_8255 = 0x04 ;
    IoPort1 += 1 ;
    IoPort2 += 1 ;
    IoPort3 = 0x5a ;
    Ch = IoPort4 ;
  } /* while */ ;
} /* main */
Często istnieje potrzeba opisania portów zawierających kilka komórek. Rozsądnym jest określenie adresu bazowego oraz później relatywnie kilka komórek. Przykładowo: port intela 8255 zawiera trzy porty oraz rejestr kontrolny. Można to opisać w następujący sposób:

Kod: Zaznacz cały

#define PORTA_8255_Base 0x10

__sfr __at (PORTA_8255_Base+0) PORTA_8255 ;
__sfr __at (PORTA_8255_Base+1) PORTB_8255 ;
__sfr __at (PORTA_8255_Base+2) PORTC_8255 ;
__sfr __at (PORTA_8255_Base+3) PORTCONTROL_8255 ;
Kompilator to łyknął i wygenerował właściwy kod:

Kod: Zaznacz cały

;test3.c:115: PORTA_8255 = 0x01 ;
   ld   a,#0x01
   out   (_PORTA_8255),a
;test3.c:116: PORTB_8255 = 0x02 ;
   ld   a,#0x02
   out   (_PORTB_8255),a
;test3.c:117: PORTC_8255 = 0x03 ;
   ld   a,#0x03
   out   (_PORTC_8255),a
;test3.c:118: PORTCONTROL_8255 = 0x04 ;
   ld   a,#0x04
   out   (_PORTCONTROL_8255),a
Przy okazji zostało zbadane zachowanie się kompilatora w przypadkach bardziej złożonych (typowych przykładowo w programach dla mikrokontroleró AVR): zapis oraz odczyt. Ciąg instrukcji:

Kod: Zaznacz cały

    IoPort1 += 1 ;
    IoPort2 += 1 ;
    IoPort3 = 0x5a ;
    Ch = IoPort4 ;
w złożeniu z dodatkowym kwalifikatorem (__banked, z dokumentacji sdccman.pdf wynika, że służy do określenia 16-bitowego adresu dla portu) został przetłumaczony następująco:

Kod: Zaznacz cały

;test3.c:119: IoPort1 += 1 ;
   ld   a,#>(_IoPort1)
   in   a,(#<(_IoPort1))
   inc   a
   ld   bc,#_IoPort1
   out   (c),a
;test3.c:120: IoPort2 += 1 ;
   in   a,(_IoPort2)
   inc   a
   out   (_IoPort2),a
;test3.c:121: IoPort3 = 0x5a ;
   ld   a,#0x5a
   ld   bc,#_IoPort3
   out   (c),a
;test3.c:122: Ch = IoPort4 ;
   ld   a,#>(_IoPort4)
   in   a,(#<(_IoPort4))
Zwrócił moją uwagę zapis (przykładowo): ld a,#>(_IoPort1) oraz in a,(#<(_IoPort1)). Występuje tu znak „>” oraz „<”. W dokumentacji nie znalazłem znaczenia tych „znaczków”, jednak w drodze różnych eksperymentów doszedłem do wniosku, że znaczek „>” oznacza część starszą oraz „<” oznacza część młodszą odpowiedniego operandu. To przypuszczenie okazuje się potwierdzać w wyniku różnych eksperymentów badawczych. Dla Z80 zapis do portu zawierającego w opisie kwalifikator __banked, w przypadku instrukcji OUT jest realizowany jako ciąg instrukcji:
LD A,......
LD BC,<adres portu>
OUT (C),A

w przypadku instrukcji IN, translacja na assembler jest odmienna:
LD A, <część starsza adresu portu>
IN A,(<część młodsza adresu portu>)

Trochę to dziwne. Dlaczego nie:
LD BC,<adres portu>
IN A,(C)

Może ma to jakieś uzasadnienie. W ogólnym przypadku sens znaczenia kwalifikatora __banked może być taki, że przykładowo w mikroprocesorze MC6800 nie ma pojęcia przestrzeni we/wy (nie ma instrukcji IN i OUT) i wtedy komórki portów są widziane jako wybrane miejsca w przestrzeni adresowej (z konieczności 16-bitowej).

Załącznik: test2.zip, test3.zip
test2.zip
test3.zip
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse

Awatar użytkownika
gaweł
Geek
Geek
Posty: 1262
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [SDCC] Kompilator języka C dla Z80

Postautor: gaweł » środa 19 lip 2017, 15:55

Funkcje do obsługi przerwań
Nieodłącznym elementem systemów z użyciem mikroprocesorów (mikrokontrolerów) jest obsługa przerwań. Funkcje przeznaczone do ich obsługi muszą posiadać przede wszystkim kilka specyficznych cech. Są to:
  • funkcje jest bezparametrowa i nie generuje wyniku, wynika to z tego, że wywołanie takiej funkcji jest realizowane przez sprzęt w sposób asynchroniczny (sprzęt nie przekazuje do funkcji żadnych parametrów i nie oczekuje żadnego wyniku, poza tym przekazanie wyniku wymaga użycia rejestru, a te nie mogą być modyfikowane),
  • funkcja nie może zmienić zawartości żadnego rejestru ani żadnego wskaźnika we flagach (w funkcji muszą być zawarte instrukcje, które zadbają o tę cechę),
  • wyjście z funkcji obsługi przerwania jest realizowane poprzez inne instrukcje (w mikroprocesorze Z80 są trzy rodzaje instrukcji kończących działanie funkcji: RET, RETI i RETN używane odpowiednio: do zakończenia działania funkcji, zakończenia obsługi przerwania maskowalnego oraz zakończenia działania przerwania niemaskowalnego).
By „zmusić” kompilator do właściwego zachowania, należy go odpowiednio o tym poinformować. W przypadku tworzenia funkcji dla procesora Z80 należy w nagłówku funkcji przeznaczonej do obsługi przerwania użyć odpowiedniego „zaklęcia”. Do utworzenia funkcji obsługującej przerwanie maskowalne należy napisać:

Kod: Zaznacz cały

void <nazwa funkcji> ( void ) __interrupt
Zapis taki informuje kompilator o potrzebie zadbania o zawartość wszystkich rejestrów.
Przykładowa funkcja do obsługi przerwania maskowalnego jest następująca:

Kod: Zaznacz cały

void int_isr ( void ) __interrupt
{
  Int_Cnt ++ ;
} /* int_isr */
Jej rozwinięcie w języku assembler jest następujące:

Kod: Zaznacz cały

;test4.c:108: void int_isr ( void ) __interrupt
;   ---------------------------------
; Function int_isr
; ---------------------------------
_int_isr::
   ei
   push   af
   push   bc
   push   de
   push   hl
   push   iy
;test4.c:110: Int_Cnt ++ ;
   ld   iy,#_Int_Cnt
   inc   0 (iy)
   jr   NZ,00103$
   inc   1 (iy)
00103$:
   pop   iy
   pop   hl
   pop   de
   pop   bc
   pop   af
   reti
Jak łatwo zauważyć, w implementacji funkcji zawarte są instrukcje przeznaczone do zachowania stanu rejestrów oraz zastosowana jest właściwa instrukcja przeznaczona do zakończenia działania obsługi przerwania. Tu warto zauważyć, że na początku zastosowana jest instrukcja zezwalająca na przyjmowania kolejnych (zagnieżdżonych) przerwań. Samo przyjęcie przerwania blokuje możliwość przyjmowania kolejnych. Z tego powodu przed zakończeniem obsługi przerwania należy wykonać instrukcję zezwalającą na przyjmowanie kolejnych przerwań (instrukcja RETI nie odtwarza wskaźnika zezwolenia na przyjmowanie przerwań jak jest to praktykowane w innych mikroprocesorach).
Do obsługi przerwań niemaskowalnych należy zastosować inne „zaklęcie” w nagłówku funkcji przewidzianej do obsługi przerwania niemaskowalnego. Zaklęcie jest następujące:

Kod: Zaznacz cały

void <nazwa funkcji> ( void ) __critical __interrupt
W tym przypadku kompilator zastosuje właściwą dla przerwania instrukcję zamykającą obsługę (RETN). Przykładowa funkcja:

Kod: Zaznacz cały

void nmi_isr ( void ) __critical __interrupt
{
  Nmi_Cnt ++ ;
} /* nmi_isr */
jest rozwinięta do następującej postaci w języku assembler:

Kod: Zaznacz cały

;test4.c:103: void nmi_isr ( void ) __critical __interrupt
;   ---------------------------------
; Function nmi_isr
; ---------------------------------
_nmi_isr::
   push   af
   push   bc
   push   de
   push   hl
   push   iy
;test4.c:105: Nmi_Cnt ++ ;
   ld   hl, #_Nmi_Cnt+0
   inc   (hl)
   pop   iy
   pop   hl
   pop   de
   pop   bc
   pop   af
   retn
Tu należy pamiętać, że samo utworzenie funkcji przeznaczonej do obsługi przerwania nie jest równoznaczne z tym, że dana funkcja będzie je obsługiwać. Rzeczywiste związanie przerwania z konkretną funkcją przeznaczoną do jego obsługi jest bardziej złożone niż w wielu innych mikrokontrolerach (będzie to rozpatrzone w kolejnych rozważaniach). Tu na zakończenie warto zauważyć ciekawą możliwość, określaną w systemie SDCC jako sekcje krytyczne. W tworzeniu oprogramowania jako sekcje krytyczne są określane takie fragmenty kodu programu, dla których wymagana jest „jednorazowość” (często niezbędna w realizacjach wielowątkowych, a wręcz klasycznym przykładem jest „podnoszenie i opuszczanie semafora”). Typową realizacją jest objęcie fragmentu kodu (lub całej funkcji) instrukcją blokującą i zezwalającą na przyjmowanie przerwań niemaskowalnych. Przy zablokowanej możliwości obsługi przerwań jest gwarancja, że dany fragment kodu programu zostanie wykonany w sposób atomowy (niepodzielny), nic nie wpłynie na „spokojną realizację” ciągu instrukcji. Jeżeli nagłowek funkcji zostanie opatrzony odpowiednim zaklęciem, to cała funkcja jest traktowana jako sekcja krytyczna. Tym zaklęciem jest kwalifikator __critical mieszczony za definicją funkcji. Przykładowo:

Kod: Zaznacz cały

void procedure ( void ) __critical
{
  Int_Cnt ++ ;
} /* procedure */
Zostało to rozwinięte przez kompilator do następującej postaci:

Kod: Zaznacz cały

;test4.c:116: void procedure ( void ) __critical
;   ---------------------------------
; Function procedure
; ---------------------------------
_procedure::
   ld   a,i
   di
   push   af
;test4.c:118: Int_Cnt ++ ;
   ld   iy,#_Int_Cnt
   inc   0 (iy)
   jr   NZ,00103$
   inc   1 (iy)
00103$:
   pop   af
   ret   PO
   ei
   ret
Zainteresowało mnie to, w jaki sposób i dlaczego zostało to tak zrealizowane. Sięgnąłem do właściwej dokumentacji: „Z80 Family CPU User Manual.pdf”. Instrukcja LD A,I oprócz przepisania zawartości rejestru I do rejestru A umieszcza we flagach (we wskaźniku parzystości) aktualny stan sprzętowego wskaźnika określającego, czy procesor może obsługiwać przerwania). Ciąg instrukcji:

Kod: Zaznacz cały

LD   A,I
   DI
   PUSH   AF
Zapisuje na stosie informację, czy w danej chwili istniało zezwolenie na obsługę przerwań oraz blokuje możliwość ich przyjmowania. Na zakończenie ciąg instrukcji:

Kod: Zaznacz cały

POP   AF
   RET   PO
   EI
   RET
Ma za zadanie odtworzyć stan zezwolenia na przyjmowanie przerwań. Po wyjściu z funkcji stan możliwości przyjmowania przerwań będzie identyczny jak w chwili wejścia do funkcji. Istnieje możliwość objęcia sekcją krytyczną również fragmentu kodu (zamiast całej funkcji). Odpowiedni ciąg instrukcji należy „objąć nawiasami sekcji krytycznej”. Ilustruje to następujący przykład:

Kod: Zaznacz cały

main ( )
{
  unsigned char Ch ;
/*---------------------------------------------------*/
  __critical
  {
    AnyCnt = 0 ;
    Int_Cnt = 0 ;
    Nmi_Cnt = 0 ;
  } /* critical */ ;
  Ch = 0 ;
  while ( 1 )
  {
    Any_IO_Port = Ch ++ ;
    procedure ( ) ;
  } /* while */ ;
} /* main */
Rozwinięcie tego przez kompilator jest następujące:

Kod: Zaznacz cały

;test4.c:122: main ( )
;   ---------------------------------
; Function main
; ---------------------------------
_main::
;test4.c:131: } /* critical */ ;
   ld   a,i
   di
   push   af
;test4.c:128: AnyCnt = 0 ;
   ld   hl,#_AnyCnt + 0
   ld   (hl), #0x00
;test4.c:129: Int_Cnt = 0 ;
   ld   hl,#0x0000
   ld   (_Int_Cnt),hl
;test4.c:130: Nmi_Cnt = 0 ;
   ld   hl,#_Nmi_Cnt + 0
   ld   (hl), #0x00
   pop   af
   jp   PO,00111$
   ei
00111$:
;test4.c:133: while ( 1 )
   ld   c,#0x00
00102$:
;test4.c:135: Any_IO_Port = Ch ++ ;
   ld   a,c
   out   (_Any_IO_Port),a
   inc   c
;test4.c:136: procedure ( ) ;
   push   bc
   call   _procedure
   pop   bc
   jr   00102$
Można tu dostrzec identyczną inwokację, ale odmienna jest realizacja zakończenia sekcji krytycznej (co nie jest dziwne). Zamiast instrukcji RET (ewentualnie RET PO) użyta jest instrukcja odpowiedniego skoku warunkowego. Reasumując, na zakończenie sekcji krytycznej stan wskaźnika zezwolenia na przyjęcie przerwania jest identyczny jak przed sekcją krytyczną. Ciekawe odkrycie, kiedyś zastanawiałem się nad takim elementem programów, teraz objawiło się samo. Człowiek uczy się przez całe życie :) .

Załącznik: test4.zip
test4.zip
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse

Awatar użytkownika
gaweł
Geek
Geek
Posty: 1262
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [SDCC] Kompilator języka C dla Z80

Postautor: gaweł » środa 19 lip 2017, 16:18

Modyfikacja części rozruchowej
Załóżmy, że zaistniała potrzeba modyfikacji części rozruchowej programu dla Z80. W strukturze kartotek można odnaleźć źródłowa postać tego fragmentu programu. Pakiet SDCC pozwala na użycie innego kodu programu niż standardowy. Część ta jest napisana w języku assembler. Użycie w wywołaniu kompilatora parametru --no-std-crt0 oznacza, że linker nie zastosuje standardowej wersji części rozruchowej. W takim razie konieczne jest „podanie” alternatywnej wersji tej części programu. Można przekopiować standardową wersję do lokalnej kartoteki przeznaczonej na projekt i dokonać właściwej modyfikacji (plik jest umieszczony w E:\SDCC\LIB\SRC\Z80 i ma nazwę CTR0.S). Przykładowo zostanie przemieszczony stos w inny obszar pamięci RAM. Dokonana zostaje modyfikacja, nowa postać pliku jest następująca:

Kod: Zaznacz cały

;--------------------------------------------------------------------------
;  crt0.s - Generic crt0.s for a Z80
;
;  Copyright (C) 2000, Michael Hope
;
;  This library is free software; you can redistribute it and/or modify it
;  under the terms of the GNU General Public License as published by the
;  Free Software Foundation; either version 2, or (at your option) any
;  later version.
;
;  This library is distributed in the hope that it will be useful,
;  but WITHOUT ANY WARRANTY; without even the implied warranty of
;  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;  GNU General Public License for more details.
;
;  You should have received a copy of the GNU General Public License
;  along with this library; see the file COPYING. If not, write to the
;  Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston,
;   MA 02110-1301, USA.
;
;  As a special exception, if you link this library with other files,
;  some of which are compiled with SDCC, to produce an executable,
;  this library does not by itself cause the resulting executable to
;  be covered by the GNU General Public License. This exception does
;  not however invalidate any other reasons why the executable file
;   might be covered by the GNU General Public License.
;--------------------------------------------------------------------------

   .module crt0
   .globl   _main

   .area   _HEADER (ABS)
   ;; Reset vector
   .org    0
   jp   init

   .org   0x08
   reti
   .org   0x10
   reti
   .org   0x18
   reti
   .org   0x20
   reti
   .org   0x28
   reti
   .org   0x30
   reti
   .org   0x38
   reti

   .org   0x100
init:
   ;; Set stack pointer directly above top of memory.
;***************************************************************************
;***************************************************************************
;***************************************************************************
;   ld   sp,#00C000      <<< było,
;***************************************************************************
;***************************************************************************
;***************************************************************************
   ld   sp,#0xC000      ; <<< jest
;***************************************************************************
;***************************************************************************
;***************************************************************************
   ;; Initialise global variables
   call   gsinit
   call   _main
   jp   _exit

   ;; Ordering of segments for the linker.
   .area   _HOME
   .area   _CODE
   .area   _INITIALIZER
   .area _GSINIT
   .area _GSFINAL

   .area   _DATA
   .area   _INITIALIZED
   .area   _BSEG
   .area _BSS
   .area _HEAP

   .area _CODE
__clock::
   ld   a,#2
   rst   0x08
   ret

_exit::
   ;; Exit - special code to the emulator
   ld   a,#0
   rst   0x08
1$:
   halt
   jr   1$

   .area   _GSINIT
gsinit::
   ld   bc, #l__INITIALIZER
   ld   a, b
   or   a, c
   jr   Z, gsinit_next
   ld   de, #s__INITIALIZED
   ld   hl, #s__INITIALIZER
   ldir
gsinit_next:

   .area   _GSFINAL
   ret
Zostaje uruchomiona kompilacja w/w tekstu programu i tu następuje pierwsza niespodzianka (należy uruchomić program SDASZ80.EXE). Plik nie kompiluje się:
ilu03-01.png
WTF? Długie rozkminy: a o co chodzi? Pomogło dodanie trzech wierszy w programie (własna wersja dostaje nazwę MYCRT0.S i taką nazwę będą miały wszystkie indywidualne zbiory związane ze specyfiką danego projektu i przechowywane lokalnie razem z projektem). Nowa postać (kompilowalna) jest następująca:

Kod: Zaznacz cały

;--------------------------------------------------------------------------
;  crt0.s - Generic crt0.s for a Z80
;
;  Copyright (C) 2000, Michael Hope
;
;  This library is free software; you can redistribute it and/or modify it
;  under the terms of the GNU General Public License as published by the
;  Free Software Foundation; either version 2, or (at your option) any
;  later version.
;
;  This library is distributed in the hope that it will be useful,
;  but WITHOUT ANY WARRANTY; without even the implied warranty of
;  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;  GNU General Public License for more details.
;
;  You should have received a copy of the GNU General Public License
;  along with this library; see the file COPYING. If not, write to the
;  Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston,
;   MA 02110-1301, USA.
;
;  As a special exception, if you link this library with other files,
;  some of which are compiled with SDCC, to produce an executable,
;  this library does not by itself cause the resulting executable to
;  be covered by the GNU General Public License. This exception does
;  not however invalidate any other reasons why the executable file
;   might be covered by the GNU General Public License.
;--------------------------------------------------------------------------
;
;  Zmodyfikowana wersja VII.2017 (A.Pawluczuk) 
;
;--------------------------------------------------------------------------

   .module crt0
   .globl   _main

   .area   _HEADER (ABS)
   ;; Reset vector
   .org    0
   jp   init
   .org   0x08
   reti
   .org   0x10
   reti
   .org   0x18
   reti
   .org   0x20
   reti
   .org   0x28
   reti
   .org   0x30
   reti
   .org   0x38
   reti
   .org   0x100
init:
   ;; Set stack pointer directly above top of memory.
   ld   sp,#0xC000
;   ^^^^^^^^ zmiana polozenia stosu
;  ****************************************************** A.Pawluczuk VI.2017
   ;; Initialise global variables
   call   gsinit
   call   _main
   jp   _exit
   ;; Ordering of segments for the linker.
   .area   _HOME
   .area   _CODE
   .area   _INITIALIZER
   .area   _GSINIT
   .area   _GSFINAL
   .area   _DATA
   .area   _INITIALIZED
   .area   _BSEG
   .area   _BSS
   .area   _HEAP
   .area   _CODE
__clock::
   ld   a,#2
   rst   0x08
   ret
_exit::
   ;; Exit - special code to the emulator
   ld   a,#0
   rst   0x08
1$:
   halt
   jr   1$
   .globl   l__INITIALIZER
   .globl   s__INITIALIZED
   .globl   s__INITIALIZER
;   dodane trzy wiersze: .globl ....
;  ****************************************************** A.Pawluczuk VI.2017
   .area   _GSINIT
gsinit::
   ld   bc, #l__INITIALIZER
   ld   a, b
   or   a, c
   jr   Z, gsinit_next
   ld   de, #s__INITIALIZED
   ld   hl, #s__INITIALIZER
   ldir
gsinit_next:
   .area   _GSFINAL
   ret
Do wielokrotnej kompilacji zostało utworzone makropolecenie CCASM.BAT o następującej treści:

Kod: Zaznacz cały

e:\sdcc\bin\sdasz80 -o crt0.rel mycrt0.s
którego zadaniem jest skompilowanie własnej wersji części startup'owej, wynik kompilacji należy umieścić w pliku o nazwie: crt0.rel (lokalnie w kartotece projektu). Kompilacja zostaje zakończona powodzeniem.
ilu03-02.png
No to pozostaje skompilowanie programu napisanego w języku C. Program (nieskomplikowany) ma następującą postać:

Kod: Zaznacz cały

unsigned char Pattern [ 16 ] = "Ala ma kota" ;

const unsigned char Pattern2 [ 16 ] = "Ola ma psa" ;

unsigned char Sum ;

main ( )
{
  unsigned char Ch1 ;
  unsigned char Ch2 ;
/*---------------------------------------------------*/
  Ch1 = Pattern [ 2 ] ;
  Ch2 = Pattern2 [ 3 ] ;
  Sum = Ch1 + Ch2 ;
  for ( ; ; )
  {
  } /* for */ ;
} /* main */
Standardowo, realizowana jest kompilacja, utworzone makropolecenie o nazwie CC.BAT zawierające:

Kod: Zaznacz cały

e:\sdcc\bin\sdcc -mz80 --data-loc 0x8000 --no-std-crt0 crt0.rel test5.c
Nic nie wskazuje na „katastrofę”.
ilu03-03.png
Zostaje uruchomiony symulator procesora, załadowany program do symulacji, „podłączona pamięć RAM” i … jedziemy.
ilu03-04.png
Po kilku krokach napotykamy problem: kod programu nie jest właściwy. Nie może tak być, by wystąpił skok (wywołanie) do niezainicjowanego obszaru w pamięci RAM. Tam jest przypadkowa zawartość. Te dziwności zostają potwierdzone w plikach powstałych z procesu kompilacji. W pliku test5.map jest napisane:

Kod: Zaznacz cały

00000010  l__INITIALIZED 
00000010  l__INITIALIZER 
0000002A  l__CODE         
00000200  s__CODE         
00008000  s__DATA         <-- obszar pamięci RAM: OK
00008001  s__INITIALIZED  <-- obszar pamięci RAM do zainicjowania: możliwe
00008011  s__GSINIT       <-- obszar zawierający kod inicjujący: ERROR !!!
00008011  s__HOME         
00008020  s__GSFINAL     
00008021  s__INITIALIZER  <-- obszar wzorca do zainicjowania obszarów:ERROR !!!
00008031  s__BSEG         
00008031  s__BSS         
00008031  s__HEAP         
Nie może tak być, by kod programu mający za zadanie zainicjowanie początkowych wartości zmiennych (a w programie jest taki obszar):
unsigned char Pattern [ 16 ] = "Ala ma kota" ;
znajdował się w obszarze pamięci RAM, oraz wzorzec (zawartość początkowa) również znajdował się w RAM. Zrobionych zostało „setka” różnych eksperymentów badawczych w celu ustalenia WTF?
Zajęło mi to trzy dni i jak zwykle pomógł trochę przypadek (ponoć przypadków nie ma). Jeżeli własny kod zastąpi kod standardowy (umieszczony w strukturze kartoteki E:\SDCC), jest poprawnie wygenerowany kod programu, jeżeli ma być dolinkowany kod znajdujący się lokalnie w kartotece projektu, są krzaki. Tak jakby efekt finalny był zależny od miejsca, gdzie znajduje się kod programu części startupowej. Przeglądając pliki wygenerowany zauważyłem jedną różnicę (plik test5.lk zawierający zapewne informacje dla linkera gdzie kod jest poprawnie wygenerowany):

Kod: Zaznacz cały

-mjwx
-i test3.ihx
-b _CODE = 0x0200
-b _DATA = 0x8000
-k e:\sdcc\bin\..\lib\z80
-l z80
e:\sdcc\bin\..\lib\z80\crt0.rel
test5.rel

-e
Ten sam plik w przypadku błędnego linkowania:

Kod: Zaznacz cały

-mjwx
-i test5.ihx
-b _CODE = 0x0200
-b _DATA = 0x8000
-k e:\sdcc\bin\..\lib\z80
-l z80
test5.rel
crt0.rel

-e
Różni się kolejnością półkomilatów wchodzących do linkowania. Powstała sugestia, że musi to mieć znaczenie. Ponieważ do tej pory użycie kompilatora SDCC sprowadzało się do skompilowania i jednocześnie zlinkowania programu, to może należy te części rozdzielić (różne kombinacje związane z kolejnością parametrów wywołania programu SDCC nie przyniosły oczekiwanych rezultatów). Postanowiłem niezależnie wykonać kompilację i niezależnie zrealizować linkowanie. Utworzona zostaje nowa wersja makropolecenia, obecnie ma postać:

Kod: Zaznacz cały

rem e:\sdcc\bin\sdcc -mz80 --data-loc 0x8000 --no-std-crt0 crt0.rel test5.c
e:\sdcc\bin\sdcc -mz80 -c test5.c
e:\sdcc\bin\sdcc -mz80 --data-loc 0x8000 --no-std-crt0 -o test5.ihx crt0.rel test5.rel
Kompilacja:
ilu03-05.png
W takim przypadku kod został wygenerowany poprawnie (ważna jest kolejność półkompilatów wymieniona na liście parametrów; ctr0.rel ma być pierwszym półkompilatem). Wywołanie program SDCC z parametrem -c oznacza wykonanie tylko kompilacji. Drugie wywołanie ma za zadanie wykonać linkowanie. Symulacja za pomocą AVSIMZ80 potwierdza poprawność kodu. Również w pliku test5.map jest informacja o właściwej adresacji:

Kod: Zaznacz cały

00000010  l__INITIALIZED                 
00000010  l__INITIALIZER                 
0000002A  l__CODE                         
00000200  s__CODE                         
0000022A  s__HOME                         
0000022A  s__INITIALIZER                 
0000023A  s__GSINIT                       
00000249  s__GSFINAL                     
00008000  s__DATA                         
00008001  s__INITIALIZED                 
00008011  s__BSEG                         
00008011  s__BSS                         
00008011  s__HEAP                         
Przy okazji:

Kod: Zaznacz cały

.globl   l__INITIALIZER
   .globl   s__INITIALIZED
   .globl   s__INITIALIZER
   .area   _GSINIT
gsinit::
   ld   bc, #l__INITIALIZER
   ld   a, b
   or   a, c
   jr   Z, gsinit_next
   ld   de, #s__INITIALIZED
   ld   hl, #s__INITIALIZER
   ldir
gsinit_next:
  • l__INITIALIZER – jest wielkością obszaru wzorca do zainicjowania zmiennych,
  • s__INITIALIZER – jest położeniem w pamięci (adresem) obszaru wzorca,
  • s__INITIALIZED – jest położeniem w pamięci (adresem) obszaru inicjowanego (docelowego)
Z innego pliku (test5.noi) pochodzi informacja:

Kod: Zaznacz cały

DEF l__INITIALIZED 0x10
DEF l__INITIALIZER 0x10
DEF s__INITIALIZER 0x22A
DEF s__GSINIT 0x23A
DEF s__GSFINAL 0x249
DEF s__DATA 0x8000
DEF s__INITIALIZED 0x8001
wielkość obszaru wzorca (l_INITIALIZER=0x10), jak również wielkość obszaru do zainicjowania (l_INITIALIZED=0x10) są równe, co chyba nie powinno dziwić. Patrząc w tekst programu mamy zmienną o wielkości 16 bajtów (zmienna Pattern). Zmienna Pattern2 jest umieszczona w obszarze kodu programu (kwalifikator const), więc nie wchodzi do zainicjowania. Na sztuki wszystko się zgadza.

Kod: Zaznacz cały

unsigned char Pattern [ 16 ] = "Ala ma kota" ;
const unsigned char Pattern2 [ 16 ] = "Ola ma psa" ;
unsigned char Sum ;
Położenie obszaru inicjowanego (zmienna Pattern) również jest właściwe (obszar s_INITIALIZED ma adres 8001 hex). Obszary zmiennych zostały w kodzie „poprzestawiane”, najpierw umieszczane są zmienne nieinicjowane (jest taka jedna o wielkości 1 bajtu [zmienna Sum]), w dalszej kolejności są lokowane zmienne inicjowane. Na sztuki wszystko się zgadza.
Powstałe sugestie potwierdza kolejny przykład programu:

Kod: Zaznacz cały

_sfr __at 0x03 Any_IO_Port ;

unsigned int anyvariable ;
unsigned char Pattern [ 12 ] = "Ala ma kota" ;
unsigned char Pattern2 [ 16 ] = "Ola ma psa" ;
unsigned char variable [ 32 ] ;
unsigned char Pattern3 [ 38 ] = "Ala i Ola maja po jednym zwierzu" ;

main ( )
{
  unsigned char * Ch ;
/*---------------------------------------------------*/
  Ch = Pattern ;
  for ( ; ; )
  {
    Any_IO_Port = * Ch ++ ;
    if ( * Ch == 0 )
      Ch = Pattern ;
  } /* for */ ;
} /* main */
Ponowne przejrzenie plików z linkowania potwierdza wcześniejsze przypuszczenia.

Załącznik: test5.zip, test6.zip

test5.zip
test6.zip
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse

Awatar użytkownika
gaweł
Geek
Geek
Posty: 1262
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [SDCC] Kompilator języka C dla Z80

Postautor: gaweł » środa 19 lip 2017, 16:28

Obsługa przerwań maskowalnych i niemaskowalnych
Mając opanowaną sztukę tworzenia funkcji do obsługi przerwań pozostało związać daną funkcję z konkretną funkcją obsługi. W przypadku przerwań niemaskowalnych związanie tego przerwania z funkcją jej obsługi jest nieskomplikowane. Wiadomo, że obsługa przerwania niemaskowalnego rozpoczyna się zawsze w ściśle określonym miejscu: pod adresem 66 hex. Oznacza to, że w części startup'owej należy zawrzeć w określonym miejscu skok do odpowiedniej funkcji obsługi. Lokalnie w kartotece projektu utworzony jest plik mycrt0.s, w którym dodane są następujące zapisy:

Kod: Zaznacz cały

.globl   _Rst7Service
   .globl   _NMIService
(...)
   .org   0x38
   jp   _Rst7Service
   .org   0x66
   jp   _NMIService
Zapis .globl jest odpowiednikiem c'owego zapisu extern i służy do zgłoszenia etykiety użytej z tekście programu ale której implementacja zawarta jest w innej części. Tu należy pamiętać, że kompilator C przekształca wszystkie nazwy elementów utworzone przez programistę dodając znak podkreślenia przed nazwą (po prostu tak mają kompilatory C i należy to zaakceptować). W ściśle określonym miejscu przestrzeni adresowej (zapis .org) umieszczony jest skok do właściwej obsługi. Program prezentuje obsługę przerwania niemaskowalnego i maskowalnego przy trybie obsługi przerwań wynikający z wykonania instrukcji IM 1 (obsługa każdego przerwania jest rozpoczynana spod adresu 38 hex → tak jak działa instrukcja RST 0x38). W programie (w języku C) znajdują się funkcje użyte do obsługi przerwania. Cały przykładowy program prezentujący działanie jest następujący:

Kod: Zaznacz cały

__sfr __at 0x03 Any_IO_Port ;

#define EI() __asm__ ("ei")
#define IM1() __asm__ ("im 1")

unsigned char Pattern [ 12 ] = "Ala ma kota" ;
unsigned int Rst7Counter ;
unsigned char MNICounter ;

void Rst7Service ( void ) __interrupt
{
  Rst7Counter ++ ;
}

void NMIService ( void ) __critical __interrupt
{
  MNICounter += 1 ;
}

main ( )
{
  unsigned char * Ch ;
/*---------------------------------------------------*/
  Rst7Counter = 0 ;
  MNICounter = 0 ;
  IM1 ( ) ;
  EI ( ) ;
  Ch = Pattern ;
  for ( ; ; )
  {
    Any_IO_Port = * Ch ++ ;
    if ( * Ch == 0 )
    {
      Ch = Pattern ;
    } ;
  } /* for */ ;
} /* main */
By zadziałał wybrany system obsługi przerwań maskowalnych, należy umieścić w programie instrukcję IM 1 (definiującą określony tryb obsługi przerwań). Kompilacja nie sprawia problemów:
ilu04-01.png
Do sprawdzenia działania użyty zostaje symulator AVSIMZ80. Po uruchomieniu programu zostaje załadowany kod binarny z pliku test7.ihx, dodana przestrzeń pamięci RAM (m:8000h, m:ffffh) oraz przestrzeń we/wy (i:0, i:255). W symulatorze dojeżdżamy poza instrukcję IM2 i EI.
ilu04-02.png
Teraz można „wygenerować” przerwanie niemaskowalne. W tym celu należy przejść do modyfikacji stanu procesora, która jest udostępniona po naciśnięciu klawisze ESC (zmienia się kursor i za pomocą klawiszy kursora: lewo, prawo, góra, dół) dotrzeć do miejsca, gdzie jest sygnalizowany stan wejścia NMI i zmienić „1” na „0”.
ilu04-03.png
Po tej zmianie wykonanie kolejnego kroku (klawisz F10) powoduje przejście do obsługi przerwania.
ilu04-04.png
Po wejściu do obsługi przerwania należy zmienić stan wejścia NMI ponownie na wartość „1” i poprzez klawisz F10 „przekonać się” o obsłudze danego przerwania:
ilu04-05.png
Na identycznej zasadzie można wygenerować przerwanie maskowalne zmieniając „1” na „0” w polu Int.
ilu04-06.png
Po przyjęciu przerwania należy zmienić ponownie wskaźnik Int z „0” na „1” (po wykonaniu instrukcji EI zawartej w obsłudze dojdzie do wykonania przerwania zagnieżdżonego [przerwania w przerwaniu]). I tak można bawić się w kółko, działa.

Załącznik: test7.zip
test7.zip
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse

Awatar użytkownika
gaweł
Geek
Geek
Posty: 1262
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [SDCC] Kompilator języka C dla Z80

Postautor: gaweł » środa 19 lip 2017, 16:52

Obsługa przerwań maskowalnych „zilogowych”
Nadszedł czas na obsługę „prawdziwych” zilogowych przerwań. By je uzyskać należy zastosować instrukcję IM 2. W tym przypadku uzyskuje się dużą elastyczność liczby przerwań jak i umiejscowienia ich programie. Położenie funkcji obsługi przerwania może być dowolne w przestrzeni adresowej (ma 16-bitowy adres). Również ich liczba nie jest ściśle określona (chociaż w gruncie rzeczy jest ograniczona do 128 sztuk, co w przeciętnym systemie mikroprocesorowym jest „ogromna”).
Filozofia ich jest następująca: w dowolnym miejscu przestrzeni adresowej na adresie parzystym znajdują się adresy poszczególnych funkcji przewidzianych do obsługi przerwania. Przyjęcie przerwania sprowadza się do „złożenia” adresu z części zawartej w rejestrze I (jako część starsza adresu) oraz części adresu podanego przez przerywający peryferal (jako część młodsza adresu, co jednocześnie implikuje, że tym peryferalem musi był coś z rodziny Z80). Tak złożony adres jest miejscem w przestrzeni pamięci zawierający adres rzeczywistej obsługi przerwania. Tą tabele (tabelę wektorów przerwań) można umieścić w pamięci stałej (EPROM) lub pamięci operacyjnej (RAM).
Do pokazania działania tego typu przerwań zostanie użyty układ PIO pracujący jako port wejściowy (wpis danych do portu generuje przerwanie, które mikroprocesor ma odczytać). AVSIMZ80 ma możliwość symulowania takiego zestawu (po uruchomieniu program należy wybrać wariant C → z dodanym układem Z80-PIO).
Poniższy program prezentuje rozwiązanie z tablicą wektorów przerwań znajdującą się w pamięci EPROM. Wymaga to zmodyfikowania części startup'owej. Postać programu jest następująca (we fragmentach):

Kod: Zaznacz cały

(...)
   .module crt0
   .globl   _main
   .globl   _IntService0
   .globl   _IntService1
   .globl   _IntService2
   .globl   _IntService3
   .globl   _IntTab

(...)

   .area   _HEADER (ABS)
   ;; Reset vector
   .org    0
   jp   init
   .org   0x08
   reti
   .org   0x10
   reti
   .org   0x18
   reti
   .org   0x20
   reti
   .org   0x28
   reti
   .org   0x30
   reti
   .org   0x38
   reti
   .org   0x70
_IntTab:   
   .dw   _IntService0
   .dw   _IntService1
   .dw   _IntService2
   .dw   _IntService3
   .org   0x100
init:
   ;; Set stack pointer directly above top of memory.
   ld   sp,#0x0000
   ;; Initialise global variables
   ld   a,#_IntTab>>8
   ld   i,a
   call   gsinit
   call   _main
(...)

Adres położenia tablicy wektorów przerwań _IntTab wynosi 70 hex (jest parzysty). W tej tablicy zawarte są cztery wskazania do funkcji obsługi przerwania (nie wynika z tego jeszcze które przerwanie jest związane z określoną obsługą). Obszar _IntTab jest eksportowany na zewnątrz (.globl) i może być użyty w inncy częściach programu. Przykladowy program w języku C przedstawia się następująco:

Kod: Zaznacz cały

__sfr __at 0x04 PORTADATA ;
__sfr __at 0x05 PORTBDATA ;
__sfr __at 0x06 PORTACONTROL ;
__sfr __at 0x07 PORTBCONTROL ;

#define EI() __asm__ ("ei")
#define IM2() __asm__ ("im 2")

extern unsigned short IntTab [ ] ;

unsigned char Pattern [ 12 ] = "Ala ma kota" ;
static unsigned char IRQIdentification ;
static unsigned char PortData ;
static unsigned short Sum ;


void IntService0 ( void ) __interrupt
{
  IRQIdentification = 0x80 ;
} /* IntService0 */


void IntService1 ( void ) __interrupt
{
  IRQIdentification = 0x81 ;
  PortData = PORTADATA ;
} /* IntService1 */


void IntService2 ( void ) __interrupt
{
  IRQIdentification = 0x82 ;
  PortData = PORTBDATA ;
} /* IntService2 */


void IntService3 ( void ) __interrupt
{
  IRQIdentification = 0x83 ;
} /* IntService3 */


void SetInterruptVectors ( void )
{
  PORTACONTROL = ( unsigned char ) ( IntTab + 1 ) ;
  PORTACONTROL = 0x4F ;
  PORTACONTROL = 0x87 ;
  PORTBCONTROL = ( unsigned char ) ( IntTab + 2 ) ;
  PORTBCONTROL = 0x4F ;
  PORTBCONTROL = 0x87 ;
} /* SetInterruptVectors */


main ( )
{
  unsigned char * Ch ;
/*---------------------------------------------------*/
  SetInterruptVectors ( ) ;
  IRQIdentification = 0xFF ;
  PortData = 0xFF ;
  IM2 ( ) ;
  EI ( ) ;
  Ch = Pattern ;
  Sum = 0 ;
  for ( ; ; )
  {
    Sum += * Ch ++ ;
    if ( * Ch == 0 )
    {
      Ch = Pattern ;
      Sum = 0 ;
    } ;
  } /* for */ ;
} /* main */

Zawiera on opis portów (w sensie przestrzeni adresowej) Z80-PIO (takie wartości adresów są „wbite” w program AVSIMZ80). Zapis:
extern unsigned short IntTab [ ] ;
informuje, że gdzieś jest obszar tablicy elementów 16-bitowych (jako obszar o nieokreślonej liczbie elementów). Jest to eksportowana z części startup'owej tablica wektorów przerwań. W rzeczywistości ta tablica zawiera 4 elementy będące adresami odpowiednich funkcji obsługi przerwań w następujący sposób:
  • IntTab [ 0 ] wskazuje na IntService0
  • IntTab [ 1] wskazuje na IntService1
  • IntTab [ 2 ] wskazuje na IntService2
  • IntTab [ 3 ] wskazuje na IntService3
Związanie określonych przerwań z tablicą wektorów zawarte jest w funkcji SetInterruptVectors. Ciąg instrukcji

Kod: Zaznacz cały

 PORTACONTROL = ( unsigned char ) ( IntTab + 1 ) ;
  PORTACONTROL = 0x4F ;
  PORTACONTROL = 0x87 ;

powoduje zapis młodszej części adresu do odpowiedniego rejestru w Z80-PIO (jako młodszej części adresu elementu [nie zawartości]). Ponieważ tablica wektorów ma parzysty adres, również każdy jej element ma adres parzysty, a to implikuje, że najmłodszy bit adresu ma wartość 0. Po tym bicie układ PIO rozpoznaje, że dany zapis dotyczy młodszej części wektora przerwań. Kolejny zapis (0x4F) jest identyfikowany jako określenie trybu pracy portu. W tym przypadku port A układu PIO jest skonfigurowany do pracy jako wejście. Kolejny zapis (0x87) jest określeniem dla układu PIO, że jest zezwolenie dla tego układu na generowanie przerwań. Identyczna sekwencja, w sensie funkcjonalności, jest zapisana do rejestru konfiguracyjnego portu B układu Z80-PIO (jedynie dotyczy innego wektora przerwań).
Pozostało skompilować program i sprawdzić jego działanie.
Kompilacja:
ilu05-01.png
Symulacja działania (po uruchomieniu programu AVSIMZ80 należy wybrać wariant „C” symulacja działania procesora wraz z układem PIO):
ilu05-02.png
Po załadowaniu programu (test8.ihx), utworzeniu pamięci RAM (m:8000h, m:ffffh), utworzeniu przestrzeni we/wy (i:0, i:255) można przystąpić do symulacji działania programu. Po "przejechaniu" instrukcji programujących zespół PIO mamy:
ilu05-03.png

Po „dojechaniu” za instrukcję DI zostają określone dane na wejściu portu A i B (przykładowo 55 hex oraz AA hex).
ilu05-04.png
Następnie w polu stb portu A zostaje wpisana „1”, co spowoduje wygenerowanie przerwania (dokładnie, to musi zaistnieć zmiana z „0” na „1” w polu stb, gdyż wpis oraz wygenerowanie przerwania następuje na skutek dodatniego zbocza sygnału strobu dla portu). Po wygenerowaniu przerwania i wykonaniu kolejnego kroku w programie (klawisz F10), następuje reakcja na przerwanie.
ilu05-05.png
Wykonanie programu weszło do funkcji obsługi przerwania i po odczytaniu danych, jak widać właściwych), następuje zapis ich w pamięci RAM. Identyczne postępowanie pozwala zaobserwować działanie przerwania po wpisie do portu B (inny wektor przerwania).
Tablicę wektorów przerwań można również umieścić w przestrzeni pamięci RAM. Wymaga to trochę więcej zachodu, gdyż nie ma gwarancji, że utworzona w programie tablica będzie miała parzysty adres (konieczne jest uparzyścienie). Ten wariant nie wymaga modyfikacji części startup'owej, więc pojedzie na standardowej postaci tego fragmentu programu. Przykładowy program w języku C jest następujący:

Kod: Zaznacz cały

__sfr __at 0x04 PORTADATA ;
__sfr __at 0x05 PORTBDATA ;
__sfr __at 0x06 PORTACONTROL ;
__sfr __at 0x07 PORTBCONTROL ;

#define EI() __asm__ ("ei")
#define IM2() __asm__ ("im 2")


unsigned char Pattern [ 13 ] = "Ala ma kota" ;
static unsigned char PortData ;
unsigned short InterruptTableSpace [ 32 ] ;
static unsigned char IRQIdentification ;
static unsigned short Sum ;

typedef void ( * IrqServiceProcT ) ( void ) __interrupt ;


void IntService0 ( void ) __interrupt
{
  IRQIdentification = 0x80 ;
  PortData = PORTADATA ;
} /* IntService0 */


void IntService1 ( void ) __interrupt
{
  IRQIdentification = 0x81 ;
  PortData = PORTBDATA ;
} /* IntService1 */


unsigned char HiPart ;
unsigned char LoPart ;
void SetInterruptVectors ( void )
{
  unsigned short IrqTableSpacePtr ;
  IrqServiceProcT * IsrService ;
/*---------------------------------------------------*/
  IrqTableSpacePtr = ( ( unsigned short ) ( & InterruptTableSpace [ 0 ] ) & 0xFFFE ) + 2 ;
  HiPart = ( unsigned char ) ( IrqTableSpacePtr >> 8 ) ;
  LoPart = ( unsigned char ) ( IrqTableSpacePtr & 0xFF ) ;
//  __asm__ ( "ld a,(_HiPart) \n ld i,a");
  __asm
      ld a,(_HiPart)
      ld i,a
  __endasm ;
  IsrService = ( IrqServiceProcT * ) IrqTableSpacePtr ;
  * IsrService = IntService0 ;
  PORTACONTROL = LoPart ;
  PORTACONTROL = 0x4F ;
  PORTACONTROL = 0x87 ;
  IrqTableSpacePtr += 2 ;
  IsrService = ( IrqServiceProcT * ) IrqTableSpacePtr ;
  * IsrService = IntService1 ;
  LoPart = ( unsigned char ) ( IrqTableSpacePtr & 0xFF ) ;
  PORTBCONTROL = LoPart ;
  PORTBCONTROL = 0x4F ;
  PORTBCONTROL = 0x87 ;
} /* SetInterruptVectors */


main ( )
{
  unsigned char * Ch ;
/*---------------------------------------------------*/
  SetInterruptVectors ( ) ;
  IRQIdentification = 0xFF ;
  PortData = 0xFF ;
  IM2 ( ) ;
  EI ( ) ;
  Ch = Pattern ;
  Sum = 0 ;
  for ( ; ; )
  {
    Sum += * Ch ++ ;
    if ( * Ch == 0 )
    {
      Ch = Pattern ;
      Sum = 0 ;
    } ;
  } /* for */ ;
} /* main */
Koncepcja działania programu jest identyczna jak opisana wyżej. Jedyną różnicą jest umieszczenie tablicy wektorów przerwań w pamięci RAM. Implikuje to parę dodatkowych działań, jak uparzyścienie jej położenia (w przykładowym programie jest zadbane, by sam obszar w pamięci RAM przeznaczony na tablicę wektorów przerwań był nieparzysty). Przy okazji, w programie pokazany jest zapis wstawek assemblerowych w innej konwencji (można napisać: __asm__ ( "<ciąg instrukcji>” ), gdzie kolejne instrukcje są rozdzielone znakiem \n lub napisać:
__asm
<instrukcja>
<instrukcja>
<instrukcja>
__endasm;
Kompilacja programu nie sprawia kłopotów, symulacja działania programu jest identyczna jak opisana wyżej.

Załącznik: test8.zip, test9.zip


test8.zip
test9.zip
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse

Awatar użytkownika
tasza
Geek
Geek
Posty: 1082
Rejestracja: czwartek 12 sty 2017, 10:24
Kontaktowanie:

Re: [SDCC] Kompilator języka C dla Z80

Postautor: tasza » piątek 21 lip 2017, 10:38

gaweł pisze:w przypadku instrukcji IN, translacja na assembler jest odmienna:
LD A, <część starsza adresu portu>
IN A,(<część młodsza adresu portu>)
Trochę to dziwne. Dlaczego nie:
LD BC,<adres portu>
IN A,(C)
Może ma to jakieś uzasadnienie.


To ma uzadadnienie i to duże, ponieważ to jest optymalizacja wykorzystania rejestrów w ramach dostępnej puli, a 16-bitowa para BC dość cenną jest.
SDCC jest po prostu w tym zakresie gospodarny.

Wersja translacji typu

Kod: Zaznacz cały

ld A, hi ( nnnn )
in A,( lo( nnnn ) )


skrzętnie korzysta z faktu, że podczas wykonywania instrukcji in A,(nn) procesor Z80 wystawia na magistrali adresowej na dolnych bitach (A7..A0) adres portu nn, ale na górnych (A15..A8) - zawartość akumulatora, no taką przypadłość ma i już.
Można zatem wykorzystać to zjawisko do ustawiania starszej cześci adresu, ale bez angażowania pary BC.

Zauważmy, że w strzępku kodu co ja napisałam:
* akumulator i tak jest na straty, bo zaraz zamaże go zawartość pobrana z portu
* przesłanie A <- nn jest mniej kosztowne czasowo niż BC <- nnnn, a z punktu widzenia sygnałów na magistralach - efekt ten sam.
* nie trzeba chronić pary BC, a 16-bit push/pop też sporo kosztują

o, a w źródłach SDCC widać ładnie jak on tam kombinuje pod spodem:
\sdcc\src\z80\gen.c pisze:

Kod: Zaznacz cały

/*.p.t.20030716 handling for i/o port read access for Z80 */
if (aop->paged)
{
/* banked mode */
/* reg A goes to address bits 15-8 during "in a,(x)" instruction */
emit2 ("ld a, !msbimmeds", aop->aopu.aop_dir);
emit2 ("in a, (!lsbimmeds)", aop->aopu.aop_dir);
}
else



dla translacji out-a wystaczy poszukac jak korzysta on ze zmiennej bcInUSe, widac też ochronę rej. BC

\sdcc\src\z80\gen.c pisze:

Kod: Zaznacz cały

/*.p.t.20030716 handling for i/o port read access for Z80 */
          if (aop->paged)
            {
              /* banked mode */
              if (aop->bcInUse)
                emit2 ("push bc");
              if (strlen (s) != 1 || (s[0] != 'a' && s[0] != 'd' && s[0] != 'e' && s[0] != 'h' && s[0] != 'l'))
                {
                  emit2 ("ld a, %s", s);
                  s = "a";
                }
              emit2 ("ld bc,#%s", aop->aopu.aop_dir);
              emit2 ("out (c),%s", s);
              if (aop->bcInUse)
                emit2 ("pop bc");
              else
                spillPair (PAIR_BC);
            }
          else



no to tyle, wracam do swoich krokodyli
______________________________________________ ____ ___ __ _ _ _ _
Kończysz tworzyć dopiero, gdy umierasz. (Marina Abramović)

Awatar użytkownika
gaweł
Geek
Geek
Posty: 1262
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [SDCC] Kompilator języka C dla Z80

Postautor: gaweł » sobota 22 lip 2017, 11:15

tasza pisze:Wersja translacji typu

Kod: Zaznacz cały

ld A, hi ( nnnn )
in A,( lo( nnnn ) )
skrzętnie korzysta z faktu, że podczas wykonywania instrukcji in A,(nn) procesor Z80 wystawia na magistrali adresowej na dolnych bitach (A7..A0) adres portu nn, ale na górnych (A15..A8) - zawartość akumulatora, no taką przypadłość ma i już.

O tym to jak raz wiedziałem.

tasza pisze:Zauważmy, że w strzępku kodu co ja napisałam:
* akumulator i tak jest na straty, bo zaraz zamaże go zawartość pobrana z portu
* przesłanie A <- nn jest mniej kosztowne czasowo niż BC <- nnnn, a z punktu widzenia sygnałów na magistralach - efekt ten sam.
* nie trzeba chronić pary BC, a 16-bit push/pop też sporo kosztują

No o tym nie pomyślałem :) . Dziękuję.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse

Awatar użytkownika
gaweł
Geek
Geek
Posty: 1262
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [SDCC] Kompilator języka C dla Z80

Postautor: gaweł » niedziela 23 lip 2017, 02:10

Obsługa przerwań maskowalnych „zilogowych”

Istotnym elementem w oprogramowaniu jest „świadomość” upływającego czasu. Zagadnienie nie jest zbyt skomplikowane. W każdym mikrokontrolerze znajduje się zespół zegara/licznika, który można skonfigurować w ten sposób, że po odliczeniu określonej liczby impulsów taktujących generowane jest przerwanie. Jeżeli impulsy taktujące mają określoną częstotliwość, to uzyskuje się w ten sposób „strumień” przerwań ściśle określonych interwałów czasowych. Zapewne „geny” tego rozwiązania pochodzą od układów takich jak Intel 8253 (ulepszony I8254) lub Z80-CTC. Nie jest problemem stworzyć kawałek programu, który będzie obsługiwał ten peryferal. Dodatkowo symulator AVSIMZ80 ma możliwość określenia, że będzie symulowane działanie mikroprocesora wraz z układem CTC (wariant B na początku działania programu) i nawet to połączenie jest sensownie przedstawione. Trochę dopadło mnie lenistwo w czekaniu na rezultat przy krokowej symulacji działania programu, więc postanowiłem „puścić program na żywioł”. Implikuje to, że symulacja „przeleci” interesujące miejsca, toteż „wykombinowałem” sobie pośredni test. Program po „upłynie” iluś tam impulsów z zegara systemowego, stanie w „twardej pętli” (przy zablokowanych przerwaniach stanie na instrukcji HALT). To taki zilogowy odpowiednik stanu uśpienia. Z tego może go wyrwać przykładowo przerwanie niemaskowalne. Więc fragment startup'owy zawiera w sobie miejsce na tabelę wektorów przerwań oraz elementarną obsługę przerwania niemaskowalnego.

Kod: Zaznacz cały

   .org   0x66
   retn
   .org   0x70
_IntTab:   
   .dw   _IntService0
   .dw   _IntService1
   .dw   _IntService2
   .dw   _IntService3

Wektory przerwań prowadzą do funkcji zaimplementowanych w C. Przykładowa funkcja do obsługi przerwania dla kanału 0 jest następująca (po iluś tam przerwaniach ustawi flagę, że upłynął określony interwał czasowy):

Kod: Zaznacz cały

void IntService0 ( void ) __interrupt
{
  IRQIdentification = 0x80 ;
  IrqChannel0Counter ++ ;
  if ( IrqChannel0Counter >= 5 )
  {
    Timer0Flag = 1 ;
    IrqChannel0Counter = 0 ;
  } /* if */ ;
} /* IntService0 */

Program w funkcji main będzie monitorował odpowiednią flagę i zatrzyma się w pułapce (w której można go wyszarpnąć poprzez przerwanie niemaskowalne).

Kod: Zaznacz cały

  for ( ; ; )
  {
    NOP ( ) ;
    NOP ( ) ;
    NOP ( ) ;
    NOP ( ) ;
    NOP ( ) ;
    NOP ( ) ;
    if ( Timer0Flag || Timer1Flag || Timer2Flag || Timer3Flag )
    {
      DI ( ) ;
      HALT ( ) ;
    } /* if */ ;
  } /* for */ ;

Konfiguracja układu CTC do wymaganego trybu pracy jest następująca:

Kod: Zaznacz cały

void SetInterruptVectors ( void )
{
  CTC_Channel0 = ( unsigned char ) ( IntTab ) ;
  CTC_Channel0 = 0x85 ;      // 1000101
  CTC_Channel0 = 0x50 ;
  CTC_Channel1 = 0x85 ;
  CTC_Channel1 = 0x60 ;
  CTC_Channel2 = 0x85 ;
  CTC_Channel2 = 0x70 ;
  CTC_Channel3 = 0x85 ;
  CTC_Channel3 = 0x80 ;
} /* SetInterruptVectors */

Po wygenerowaniu wersji binarnej, za pomocą programu AVSIMZ80 puszczonego „na żywioł” (klawisz F1), okazało się, że program „wpadł w zastawioną” pułapkę.
ilu06-01.PNG


Załącznik: test10.zip
test10.zip
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse

Awatar użytkownika
gaweł
Geek
Geek
Posty: 1262
Rejestracja: wtorek 24 sty 2017, 22:05
Lokalizacja: Białystok

Re: [SDCC] Kompilator języka C dla Z80

Postautor: gaweł » środa 08 maja 2019, 13:16

Te niejako akademickie rozważania doczekały się ciągu dalszego jako praktyczna realizacja w realnym świecie. Dalsza lektura godna uwagi znajduje się tutaj. Można rzecz, że klasyka jest wiecznie młoda, bo Z80 to już jest absolutna klasyka gatunku.
Klasyka wiecznie młodahttps://www.youtube.com/watch?v=WeDDLsLkao8

Prawdziwe słowa nie są przyjemne. Przyjemne słowa nie są prawdziwe.
Lao Tse


Wróć do „Retro”

Kto jest online

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