[CA80] Programowanie z wykorzystaniem SDCC , C + asm + własne crt0

Kącik dla elektroniki retro - układy, urządzenia, podzespoły, literatura itp.
Awatar użytkownika
tasza
Geek
Geek
Posty: 1082
Rejestracja: czwartek 12 sty 2017, 10:24
Kontaktowanie:

[CA80] Programowanie z wykorzystaniem SDCC , C + asm + własne crt0

Postautor: tasza » poniedziałek 06 maja 2019, 23:31

♬ ☘ Moja muzyka do kodowania ♬ ♬ ♬ ☘
♫ ♩ ♪ TSA ⚡ ☘ ⚡ Trzy zapałki ♪ ♩ ♫
https://youtu.be/OT-wW06LCqY


Temat komputerka CA80 jest u mnie teraz bardzo aktualny, a wyśmienicie działająca `Ładowarka Do Pamięci` sprawia, że w końcu mogę sobie używać i klecić programy dłuższe niż kilkaset bajtów, nie dostając mdłości na myśl o tysiącach kliknięć zleceniem *D. Translator SB-Assembler jest narzędziem całkiem OK, ale skoro już tyle napisano o SDCC, to może czas wykorzystać tę wiedzę w jakiś konkretny sposób.

Poniższa opowiastka powstała na podstawie lektury wypracowania :arrow: Kompilator języka C dla Z80 , które popełnił gaweł w zeszłe,zeszłe wakacje. Zacznę od tego, że we wspomnianym artykule o SDCC znajdują się wszelkie konieczne do zabawy informacje i chcąc sensownie pożenić CA80 z pakietem SDCC należy przez owe przebrnąć i tu nie ma zlituj. Dodatkowe detale wyczytałam sobie z :arrow: SDCC - Interfacing with Z80 assembler code , choć zanim osiągnęłam w miarę zadowalające rezultaty odbyła się seria skoków na bungee z analizą assemblerowych listingów w tle. A zatem...

Komputerek CA80 nie jest trywialny do melanżu z SDCC z kilku powodów. Pierwszy i podstawowy to fakt, że nasz program będzie uruchamiał się w dedykowanych dla aplikacji użytkownika lokalizacjach, w jednym z banków obsadzonych RAM (od 0x4000, 0x8000 lub 0xC000), to automatycznie oznacza konieczność podpowiedzenia linkerowi jak uplasować kod i dane. Po drugie - zupełna rezygnacja z kodu rozruchowego (crt0) jak proponują niektóre źródła oznacza jazdę ze stosem (czy raczej brakiem jego inicjacji na potrzeby CA) oraz konieczność własnoręcznego zadbania o wstępne poustawianie wartości zmiennych. O kodzie rozruchowym gaweł sporo napisał, więc aby się nie powtarzać - poniżej moja bojowa wersja:

crt0ca80_template.s pisze:

Kod: Zaznacz cały

    .module crt0
    .globl  _main
    .area   _HEADER (ABS)
    ;; Reset vector
    .org    __CA80_CODE_BASE__       
init:
    ld  sp,#0xFF66       ; CA80 typical
    call    gsinit
    call    _main
1$: jp  1$     
    .area   _HOME
    .area   _CODE
    .area   _INITIALIZER
    .area   _GSINIT
    .area   _GSFINAL
    .area   _DATA
    .area   _INITIALIZED
    .area   _BSEG
    .area   _BSS
    .area   _HEAP
    .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


I tu pierwsza modernizacja względem Andrzejowego tutoriala - to jest plik-szablon. Właściwy plik do assemblacji powstaje w locie, na skutek wykonania skryptu, który przy okazji zamienia mi symbol `__CA80_CODE_BASE__` na ustawioną zawczasu wartość liczbową. Mam tym prostym sposobem bootstrap ruszający od 4000, od 8000 czy C000, zależnie od kaprysu.
W tutorialu wspominano o problemach z assemblacją kodu rozruchowego, no fakt - sdasz80 płacze komunikatem Error: <u> undefined symbol encountered during assembly bo nie wie co to #l__INITIALIZER i reszta. Znacząco pomaga mu dodanie opcji `-g` Undefined symbols made global. Całość preparująca własny rozrusznik wygląda tak:

Kod: Zaznacz cały

CODE_ORG="0x8000"
cat crt0ca80_template.s | sed -e "s/__CA80_CODE_BASE__/$CODE_ORG/" > crt0ca80.s
sdasz80 -g -l -o crt0ca80.rel crt0ca80.s


Tym oto prostym sposobem mam starter naturalnie osadzony od adresu 8000, z kodem inicjującym zmienne globalne w RAM (od wskazanego później adresu), przy okazji kod rozruchowy wyczyściłam ze zbędnych zaślepek na programowe przerwania RSTxx, bo to przecież mam w systemowym EPROM.

A teraz programik w C, jak łatwo zgadnąć będzie to migadełko do ledów i to symulowanych wyświetlaczem z WaveForms i pudełkiem AD2. Program robi co następuje: z nieskończonej pętli wysyła na port PA układu 8255 wybrany z tabeli wzorek, wartość wzorka oraz jego adres (tzn. adres bieżącego elementu tabeli) pokazuje na systemowym wyświetlaczu i przy pomocy opakowanych w C procedur systemowych z EPROM-u MIK, no właśnie.

Z takich sympatyczniejszych kawałków - przepisane kiedyś z sieci makro BIN(), przydaje się do definiowania stałych-wzorców (np. fontów), aby były czytelne dla człowieka:

simple_leds_1.c pisze:

Kod: Zaznacz cały

define BIN(x) \
    ( ((0x##x##L & 0x00000001L) ? 0x01 : 0) \
    | ((0x##x##L & 0x00000010L) ? 0x02 : 0) \
    | ((0x##x##L & 0x00000100L) ? 0x04 : 0) \
    | ((0x##x##L & 0x00001000L) ? 0x08 : 0) \
    | ((0x##x##L & 0x00010000L) ? 0x10 : 0) \
    | ((0x##x##L & 0x00100000L) ? 0x20 : 0) \
    | ((0x##x##L & 0x01000000L) ? 0x40 : 0) \
    | ((0x##x##L & 0x10000000L) ? 0x80 : 0))


Jego wykorzystanie:

simple_leds_1.c pisze:

Kod: Zaznacz cały

/*const*/ unsigned char ledPattern[ 8 ] = { 
    BIN( 00000001 ),
    BIN( 00000010 ),
    BIN( 00000100 ),
    BIN( 00001000 ),
    BIN( 00010000 ),
    BIN( 00100000 ),
    BIN( 01000000 ),
    BIN( 10000000 )
};


O `const` w komentarzu powiem dalej.

Jeden z wrapperów na systemową procedurę, tu LADR - pokazuje zawartość HL w/g zadanej pozycji wyświetlacza.

simple_leds_1.c pisze:

Kod: Zaznacz cały

void mikLADR( unsigned char pwys, unsigned short value ) __naked {
    __asm           
        ld IY,#2
        add IY,SP
        ld A,0(IY)      ; mamy PWYS
        ;
        ld HL,#0xFFF6   ; APWYS
        ld (HL),A       ; ustaw!
        ;
        ld L,1(IY)      ;value (low)
        ld H,2(IY)      ;value (high)   
        ;
        call 0x0021     ; systemowa LADR1
        ;
        ret
    __endasm;
}


I tu może spostrzegawcze oko (co zerkało na pisaninę o CA80 na bienata) zauważy - wołam nie LADR a LADR1, co oznacza, że parametr PWYS musi zostać ustalony osobno, przed wywołaniem procedury systemowej. Tak więc zostaje on wprowadzony jako pierwszy do funkcji-wrappera. Parametry SDCC przekazuje przez stos, przed wywołaniem funkcji warstwa wyższa upycha kolanem wartości parametrów na stosie, wewnątrz funkcji trzeba to sobie pozbierać i to tak aby nie naruszyć adresu powrotu, który też na stosie leży. I stąd te wygibasy z rejestrem indeksowym IY, przy okazji musimy polubić taki dziwny zapis np.`LD A,9(IY)` zamiast typowo spotykanego `LD A,(IY+9)`. Ja choć to formalnie zbędne dopisuje sobie dla zerowego offsetu właśnie 0 (widać przy PWYS), mam na otarcie łez przynajmniej jakąś konsekwencję w tych dziwnych numerkach. Magiczny parametr PWYS, o którym napisano kilka kartek w MIK05 ubrałam sobie w makro:

simple_leds_1.c pisze:

Kod: Zaznacz cały

#define PWYS(digitsused/*8-1*/,startfrom/*7-0*/)  ((digitsused<<4)|startfrom)


I dzięki temu już mi się nie merda, który nibble tego parametru jest do czego i jakie ma zalecane wartości.

Pętla główna tego prostego demka wygląda następująco:
simple_leds_1.c pisze:

Kod: Zaznacz cały

void main( void ) {   
    unsigned char cntr = 0;
    USER8255_CTRL = 0x80;   // same wyjscia   
    while ( 1 ) {
        for ( cntr = 0; cntr < sizeof( ledPattern ); cntr++ ) {
            USER8255_PA = ledPattern[ cntr ];               // wzorek na wyjścia
            mikCLR( PWYS(8,0) );                            // skasuj wyswietlacz
            mikLBYTE( PWYS(2,6), ledPattern[ cntr ] );      // pokaz daną (lewo)                 
            mikLADR( PWYS(4,0), (unsigned short)&ledPattern[ cntr ] ); // i adres (prawo)           
            delay();           
        }       
    }   
}


Wywołania systemowe są chyba dobrze wyeksponowane, ale odnośnie nazw to mam rozterki - mikLADR() czy może sysLADR(), a może sys_LADR() a może inaczej, powinny przecież nawiązywać do literałów, którymi operują MIK-i. Póki co opakowałam pisanie po wyświetlaczu, kolejne atrakcje to będą funkcje czytające klawiaturę i pobierające parametry. Podobnie z identyfikacją peryferiów w CA80, choćby tych podstawowych CTC i 8255...

Do kompilacji i linkowania prosty skrypcik, jego pierwsze linie już pokazywałam, tu dalej:

Kod: Zaznacz cały

CODE_LOC="0x8010"
DATA_LOC="0xC000"
sdcc --verbose -mz80 -c simple_leds_1.c
sdcc --verbose -mz80 --code-loc ${CODE_LOC} --data-loc ${DATA_LOC} --no-std-crt0 -o simple_leds_1.ihx crt0ca80.rel simple_leds_1.rel


Cóż więcej, aha - ten `const` przy deklaracji wzorka:

Kod: Zaznacz cały

const unsigned char ledPattern [8]

Powyższa deklaracja spowoduje, że wzorek będzie pobierany z segmentu programu, na filmiku będzie widać zmieniające się adresy 80xx.

Kod: Zaznacz cały

unsigned char ledPattern [8]


Bez const-a, jak wyżej, będzie to zwykła zmienna globalna w RAM, w segmencie danych wskazanym adresem C000, zawartość na podstawie wzorca z segmentu kodu ustali gsinit podczas rozruchu aplikacji i to też można zobaczyć na wyświetlaczu po rekompilacji programiku (adresy C0xx).

W podsumowaniu - na CA80 da się klecić w C, choć wymaga to zapoznania się ze specyfiką SDCC, szczęściem jest z czego się uczyć. W nieco dalszej perspektywie zostaje dobranie się do przerwań NMI oraz wektorowego systemu przerwań maskowalnych w trybie IM2, no tu może być całkiem ciekawie.

Na dobranoc filmik:
https://youtu.be/7aGTgmdL3xo

#slowanawiatr
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
______________________________________________ ____ ___ __ _ _ _ _
Kończysz tworzyć dopiero, gdy umierasz. (Marina Abramović)

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

[CA80] SDCC, wykorzystanie przerwań NMI, własne moduły assemblerowe

Postautor: tasza » wtorek 07 maja 2019, 21:53

♬ ☘ Moja muzyka do kodowania ♬ ♬ ♬ ☘
♫ ♩ ♪ TSA ⚡ ☘ ⚡ 51 ♪ ♩ ♫
https://youtu.be/fpAr4Xd8sbo


Oswajania pakietu SDCC ciąg dalszy, tym razem eksperyment, który chciałam kiedyś wykonać przy pomocy SB-Assembler, ale...jakoś zabrakło motywacji. A teraz, na okazję nowej zabaweczki pomysł wrócił, a polega on na gościnnym wpięciu się w obsługę przerwań NMI systemu CA80 i jak zaraz zobaczymy literacka przenośnia `pomiędzy wódkę i zakąskę` będzie jak najbardziej zasadna. Inny aspekt tej pisanki to drobna prezentacja dołączania własnych modułów assemblerowych. Niby żadne halo, no przecież własne ctr0 dokleiłam, ale co innego modyfikowany gotowiec, a co inne - własny, choćby najbiedniejszy modułek. No to lecimy.

NMI w systemie CA80

Przerwania NMI w systemie CA80 są zgłaszane z częstotliwością 500Hz (co 2ms) i dla działania komputerka są krytyczne. W nich właśnie obsługiwany jest wyświetlacz (multipleksowanie), skanowana klawiatura (szczególnie traktowana detekcja klawisza [M]) oraz odliczane są kolejne kwanty czasu programowego zegara. Wywołanie NMI skutkuje przekazaniem sterowania do lokalizacji 0x0066, oczekiwanie jest takie, że ciąg rozkazów stanowiących obsługę przerwania zakończy się przy pomocy instrukcji RETN. Warto wiedzieć, ze NMI ma najwyższy możliwy priorytet, jego obsługa nie może zostać przerwana przez inne (maskowalne) przerwanie, a obsługa nowego zgłoszenia może rozpocząć się dopiero po zakończeniu poprzedniego. No i oczywiście garstka smuteczków - w systemie CA80 całość obsługi NMI znajduje się w systemowym EPROM i na pierwszy rzut oka nie mamy nic do gadania - całość zagadnienia zawłaszcza sobie program Monitora. Ale...

Jak się dobrze wczytać w MIK08, w listing programu Monitora, to na sam koniuszek obsługi NMI znajdziemy bardzo ciekawe miejsce:

MIK08 str.6-8 pisze:

Kod: Zaznacz cały

NMI:
    PUSH AF
    PUSH HL
    PUSH DE
    ; ... i tak dalej....
    CALL NMIU           ; <--- whooaaa!!
    POP DE
    POP HL
    POP AF
    RETN


Wywołanie `CALL NMIU` to call pod adres 0xFFCC, zobaczmy zatem na listingu, co tam się dzieje. Tym razem sięgamy do MIK05, do opisu obszaru RAM wykorzystywanego przez Monitor i proszę:

MIK05 str.148 pisze:

Kod: Zaznacz cały

                ; ....
FFCC    C9      NMIU:   RET
FFCD    0000            DW 0
                ; ....


No proszę, proszę - w systemie CA80 celowo zostawiono furtkę, aby można było wpiąć się w ciąg instrukcji wywoływanych na okazję nadchodzącego co 2ms przerwania NMI, ale super!
Aby mieć obraz całości to tylko wspomnę, że te trzy komórki od adresu 0xFFCC są inicjowane wraz z programem Monitora, aby wywołanie NMIU było transparentne na dobry początek pod ten adres wpisywane są wartości 0xC9, 0x00, 0x00 co daje RET oraz dwa NOP-y.
Pierwsza myśl i w sumie jedynie słuszna - chcąc załapać się na tamtędy biegnący ciąg wywołań należy zamiast tych trzech bajtów wstawić kod instrukcji `JP nn` (0xC3,LO,HI) i będzie fajnie. No niby tak, ale pamiętajmy, że dzielniki zegara CA80 generują impulsy dla NMI z szybkością dzięcioła na poczwórnej espresso i przerwanie niemaskowalne może nas dziobnąć w dowolnym momencie ustawiania tych trzech bajtów. Stąd najbezpieczniejsze jest najpierw ustawienie bajtów adresu skoku (komórki 0xFFCD, 0xFFCE), a potem tej odpowiedzialnej za sam rozkaz - 0xFFCC, czyli przełączenie z RET na JP.

NMI wysokopoziomowo

No i tu wracamy do SDCC, najpierw stwórzmy sobie strukturę danych, która odwzoruje nam punkt przesiadkowy w wywołaniu NMI:

nmi_spy_1.c pisze:

Kod: Zaznacz cały

typedef struct {
    unsigned char   cpuOpCode;  // call/jump/ret
    unsigned short  handlerAddress;   
} NMIU;


W programie możemy zmaterializować ten twór następująco:

nmi_spy_1.c pisze:

Kod: Zaznacz cały

NMIU *pNMIU = (NMIU*)0xFFCC;


I to daje nam w miarę komfortowy dostęp zarówno do kodu rozkazu jak i jego parametru.

Teraz zastanówmy się co ma robić demko - ja dla uproszczenia wymyśliłam sobie tak, że powstaną dwie konkurencyjne procedurki oddziaływające na port PA układu 8255 użytkownika, jedna będzie inkrementować wystawioną wartość, a druga na opak. Ponieważ spodziewana częstotliwość wywołań będzie znaczna, obie procedurki pracują z prywatnym licznikiem ich wywołań - fizyczne efekty są widoczne po zdefiniowanej ilości uruchomień.

nmi_spy_1.c pisze:

Kod: Zaznacz cały

unsigned char privateNMIhandlerCounter = 0;
void handleNMI_up(void) {
    privateNMIhandlerCounter++;
    if ( privateNMIhandlerCounter == 150 ) {
        privateNMIhandlerCounter = 0;
        USER8255_PA++;
    }
}
void handleNMI_down(void) {
    privateNMIhandlerCounter++;
    if ( privateNMIhandlerCounter == 150 ) {
        privateNMIhandlerCounter = 0;
        USER8255_PA--;
    }
}


No i teraz - jak zabrać się za chytre podstawienie tych procedurek systemowi, aby CA80 w łaskawości swej zechciał je wykonać. Piszemy sobie dwie kolejne procedurki do instalacji i deinstalacji naszego handlera:

nmi_spy_1.c pisze:

Kod: Zaznacz cały

typedef void (*TNMIUserHandler)(void);
#define Z80_JP_OPC      0xC3
#define Z80_RET_OPC     0xC9
void installNMIhandler( NMIU *pNMIU, TNMIUserHandler hdlr ){
    pNMIU->cpuOpCode = Z80_RET_OPC;   
    pNMIU->handlerAddress = (unsigned short)hdlr;   // fuj!
    pNMIU->cpuOpCode = Z80_JP_OPC;
}
void uninstallNMIhandler( NMIU *pNMIU ){
    pNMIU->cpuOpCode = Z80_RET_OPC;   
    pNMIU->handlerAddress = 0x0000;   
}


Zacznę od tego, co mnie w tym mierzi - rzutowanie wskaźnika na funkcję na 16-bit liczbę. No niby w tym wypadku nie ma innego wyjścia, a procesor Z80 operuje 16-bit adresacją, ale jakoś brzydko to wygląda. Jak coś wymyślę to oczywiście napiszę. A póki co - installNMIhandler() - profilaktycznie utransparetnia wywołania NMIU rozkazem RET, potem modyfikuje adres procedury obsługi, na koniec uaktywnia zabawkę przez wstawienie zamiast RET rozkazu JP. Procedurka uninstallNMIhandler() sprowadza całość do stanu domyślnego, jak po inicjacji Monitora, rozkaz RET i wyzerowany adres docelowego skoku. Aha, no i poczemu korzystam z JP a nie CALL - inicjalnie CA80 wstawia rozkaz powrotu z procedury RET. Mój handler też ma na końcu RET, stąd aby bilans się zgadzał należy wykonać płaski skok, bez afektowania stosu, jak to ma miejsce w przypadku CALL-a, ot co.

No i czas na wykorzystanie tak przygotowanych komponentów - w pętli głównej odpytujemy klawiaturkę CA80, zależnie czy guzik [1] czy [2] podstawiany jest odpowiedni handler NMI użytkownika, dla pozostałych klawiszy - jest czyszczony. Zawartość portu PA pokazujemy na wyświetlaczu, całość działa dzięki ogólnie znanej właściwości kostki 8255, że odczyt z portu ustawionego na OUT zwraca nie stan pinów, ale wartość wpisaną do rejestru wyjściowego. Część główna:

nmi_spy_1.c pisze:

Kod: Zaznacz cały

void main( void ) {
    unsigned char key = NO_KEY;   
    NMIU *pNMIU = (NMIU*)0xFFCC;        // zgodnie z MIK08   
    USER8255_CTRL = 0x80;   // same wyjscia   
    USER8255_PA = 0x00;                 
    while ( 1 ) {
        key = keyPressed();
        if ( key != NO_KEY ) {           
            switch ( key ) {
                case 1:
                    installNMIhandler( pNMIU, &handleNMI_down );               
                    break;
                case 2:           
                    installNMIhandler( pNMIU, &handleNMI_up );               
                    break;
                default:               
                    uninstallNMIhandler( pNMIU );               
                    USER8255_PA = 0x00;
                    break;
            }
        }
        mikCLR( PWYS(8,0) );                     // skasuj wyswietlacz
        mikLBYTE( PWYS(2,0), USER8255_PA );      // pokaz bajt z portu               
        delay( 1 );
    }   
}


A na żywo działa to następująco:

https://youtu.be/pR2bdVeMcWo

Na koniec drobiazg, ale jak dla mnie fajny - napisana w czystym assemblerze funkcyjka-wrapper do obsługi klawiatury. Oto jej prosty kod:

ca80syscalls.s pisze:

Kod: Zaznacz cały

        .module ca80syscalls       
        ; exported stuff
        .globl _keyPressed
        ; CA80 native procedures
CSTS    .equ    0xFFC3
        ;
NO_KEY  .equ    0xFF
        ;
        .area _CODE       
_keyPressed:
        call CSTS
        ld L,#NO_KEY
        ret NC       ; CY == 0 -> no key
        ld L,A
        ret         
        ;


Jak widać, jest to nieskomplikowana pokrywka systemowej procedury CA80 o nazwie CSTS, która jednorazowo przebiega klawiaturę w poszukiwaniu wciśniętego klawisza. Kody klawiszy CA80 zaczynają się od 0x00 zatem sygnalizacja braku wciśnięcia jest symbolem NO_KEY, o wartości 0xFF. Do kompletu jest plik nagłówkowy:

ca80syscalls.h pisze:

Kod: Zaznacz cały

#ifndef __CA80SYSCALLS_H__
    #define __CA80SYSCALLS_H__

#define NO_KEY  0xFF   
extern unsigned char keyPressed( void );
   
#endif // of __CA80SYSCALLS_H__


No i oczywiście dołączenie tego cudaka co całej aplikacji - skrypcikiem powłoki. Translację robię póki co tak, jak crt0 - narzędziem sdasz80, oto wywołania:

do.sh pisze:

Kod: Zaznacz cały

#!/bin/bash
APP="nmi_spy_1"
CODE_ORG="0x8000"
CODE_LOC="0x8010"
DATA_LOC="0xC000"
cat crt0ca80_template.s | sed -e "s/__CA80_CODE_BASE__/$CODE_ORG/" > crt0ca80.s
sdasz80 -g -l -o crt0ca80.rel crt0ca80.s
sdasz80 -g -l -o ca80syscalls.rel ca80syscalls.s
sdcc --verbose -mz80 -c ${APP}.c
sdcc --verbose -mz80 --code-loc ${CODE_LOC} --data-loc ${DATA_LOC} --no-std-crt0 -o ${APP}.ihx crt0ca80.rel ca80syscalls.rel ${APP}.rel
mv ${APP}.ihx ${APP}.hex


Aha, no i *.ihx przechrzciłam sobie na *.hex...

W lokalnym podsumowaniu - no z SDCC to da się żyć, serio, jest megawdzięcznym poletkiem doświadczalnym, choć cały czas mu nie ufam i ciągle weryfikuje assemblerowe listingi, co on tam nawywijał.

W zip do tej opowiastki, w ca80syscalls.s|h jest też wrapperek na systemową procedurę CI (console input). No, tu nie ukrywam - poległam. Chytra myśl była taka, że zrobię z niej C-ową funkcję getKey() i prawie się udało. Tylko, że w CI system CA80 czeka na zwolnienie i naciśnięcie klawisza i nijak się nie mogłam z tym dogadać, temat klawiatury pozostaje zatem szeroko otwarty.

#slowanawiatr
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
______________________________________________ ____ ___ __ _ _ _ _
Kończysz tworzyć dopiero, gdy umierasz. (Marina Abramović)

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

Re: [CA80] Programowanie z wykorzystaniem SDCC , C + asm + własne crt0

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

Serce rośnie jak widzę niezależną kontynuację prac. Normalny progress.

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:

[CA80] migający wąż czyli SDCC i przerwania maskowalne od Z80-CTC

Postautor: tasza » piątek 10 maja 2019, 14:20

♬ ☘ Moja muzyka do kodowania ♬ ♬ ♬ ☘
♫ ♩ ♪ TSA ⚡ ☘ ⚡ Na Co Cię Stać? ♪ ♩ ♫
https://youtu.be/OyMVfJLiSMc

gaweł pisze:Serce rośnie jak widzę ...


Tak, parafrazując klasyków - `serce roście choć wzrok dziki i suknia plugawa`. No, tym razem kostka Z80-CTC to mi dała do wiwatu i w sumie cały jeden wieczór mam w plecy na rozkminę, dlaczego nie działało. Finalnie sztuczka się udała, zatem tradycyjnie obciążę Panów swymi przemyśleniami w materii pakietu SDCC, układu Z80-CTC oraz trybu przerwań IM2 procesora Z80. Ten odcinek mocno nawiązuje do części :arrow: Obsługa przerwań maskowalnych „zilogowych” i dobrze sobie ten materiał pierwej odświeżyć. Zaczynamy...

IM2

... od koncentratu faktów na temat IM2. Interrupt Mode 2 to tryb obsługi przerwań maskowalnych Z80, w których urządzenie zgłaszające przerwanie i żądające atencji jest automatycznie zobowiązane do dostarczenia procesorowi młodszej części adresu wektora swojego przerwania. Wektor przerwania, to adres pod który trzeba skoczyć, celem obsłużenia zgłoszenia. Adres jest tworem fizycznym, zajmuje dwa bajty, one mają swoją lokalizację w pamięci (czyli...swój adres). Starsza cześć tego adresu wpisana jest do rejestru I podczas inicjalizacji aplikacji, młodszą dostarczy nam zgłaszające urządzenie. Aby nie było śmietnika wprowadzono pojęcie tabeli wektorów przerwań, o tyle to słuszne, że ośmiobitowy rejestr I niejako grupuje swoją maksymalną wartością 0xFF komórek pamięci (stronę), co można potraktować jako tabelę 128 szesnastobitowych słów-adresów (i tyle teoretycznie, bez sztuczek możemy mieć handlerów). W opisie gawła jest wyraźnie zaznaczone, że adresy wektorów przerwań muszą być parzyste, aby ową parzystość zapewnić wykonano w kodzie szereg specjalnych zabiegów. Chęć skorzystania z układu Z80-CTC ujawniła dodatkowe obostrzenia odnośnie adresacji wektorów i od tego zaczniemy.

Krótko o kostce Z80-CTC: :arrow: http://www.z80.info/zip/um0081.pdf (od strony 17, Programming). Parzystość adresu (wyzerowany najmłodszy bit) jest wymagana, aby rozróżnić ładowanie wektora IRQ od załadunku zwykłego słowa sterującego (gdzie bit zerowy jest dla odmiany zawsze 1), to cecha Zilogowych kostek. Dodatkowo, w przypadku CTC zachodzi ciekawe zjawisko, że kostka sama ustawia sobie bity 2,1 adresu wektora, skoro mamy stale wyzerowany bit 0 to tak naprawdę jesteśmy odpowiedzialni za podstawienie pozostałych bitów 7-3 wektora. Tu prosze zerknąć na tabelę 7 (strona 23) dokumentacji Ziloga. A w praktyce oznacza to tyle, że adres tabeli wektorów przerwań musi być nie tylko parzysty, ale musi być wielokrotnością liczby 8. Na własnej skórze wczoraj przekonałam się, jak cholernie trudno złapać taki błąd koncepcyjny w organizacji programu - program działa ale niedeterministycznie, co jest w sumie najgorsze do diagnozy, bo jak chcemy zrekreować problem to akurat jest dobrze. Po przebojach stanęło zatem na tym, że tabela wektorów przerwań została upchnięta w segmencie DATA, w assemblerowym modułku ca80syscalls.s i prezentuje się tak:

ca80syscalls.s pisze:

Kod: Zaznacz cały

   .globl _annoyingData
   .globl _irqHandlersTable       
   .globl _alignedByte       
   .area  _DATA   
_annoyingData:       
    .ds 3       
    .even
_alignedByte:
    .ds 1       
    .bndry 8     ; co 8 bajtów
_irqHandlersTable:
    .ds 2        ; CTC, kanal 0
    .ds 2        ; CTC, kanal 1
    .ds 2        ; CTC, kanal 2
    .ds 2        ; CTC, kanal 3       


Adres segmentu DATA jest podczas linkowania ustawiany na 0xC000, a więc na wartość parzystą i podzielną przez 8. Aby sprawę utrudnić, zarezerwowałam trzy bajty-śmiećki (annoyingData). Jeżeli teraz potrzebujemy wymusić parzystość kolejnej alokacji możemy skorzystać z dyrektywy .even assemblera (odsyłam do manuala: :arrow: https://github.com/ixaxaar/sdcc/blob/ma ... asmlnk.txt ). Oczywiście mogą powstać perwersyjne wymagania na nieparzystość adresu - tu mamy na podorędziu dyrektywę .odd, warto to znać. U mnie wystąpiła konieczność wycyrklowania tabeli na modulo 8 bajtów, z pomocą przychodzi dyrektywa .bndry ułatwiająca ustawienie adresu na podzielną przez n wartość. Mega sprawa. A jak powyższe ma się do CTC w CA80 - proszę, oto mała rozpiska, która funkcjonuje u mnie:

Kod: Zaznacz cały

rejestr I      adres wektora podawany z CTC       efektywny adres
hex            b7  b6  b5  b4  b3  b2  b1  b0     
0xC0           0   0   0   0   1   0   0   0      0xC008 - kanał 0
0xC0           0   0   0   0   1   0   1   0      0xC00A - kanał 1
0xC0           0   0   0   0   1   1   0   0      0xC00C - kanał 2
0xC0           0   0   0   0   1   1   1   0      0xC00E - kanał 3


Widać, że aby zachować niezmienną kombinację bitów b2-b0 - tabelkę możemy ustawić gdziekolwiek w pamięci, pod warunkiem że adres tego gdziekolwiek dzieli się przez 8.

Oczywiście, aby program w C był w stanie skorzystać z tej tabeli, trzeba jej nazwę opublikować, potraktowawszy ją literalnie - jako tabelę słów - do pliczku ca80syscalls.h doszła deklaracja, o tyle już wygodna, że nie muszę zajmować się fizyczną lokalizacją tej porcji danych, to mam załatwione niskopoziomowo, w module *.s:

ca80syscalls.h pisze:

Kod: Zaznacz cały

extern unsigned short irqHandlersTable[4];


Tę cześć wywnętrzeń zakończmy może rzutem oka w przygotowaną przez linker mapę zajętości pamięci:

ca80syscalls.s pisze:

Kod: Zaznacz cały

Area                                    Addr        Size        Decimal Bytes (Attributes)
--------------------------------        ----        ----        ------- ----- ------------
_DATA                               0000C000    00000010 =          16. bytes (REL,CON)

      Value  Global                              Global Defined In Module
      -----  --------------------------------   ------------------------
     0000C000  _annoyingData                      ca80syscalls
     0000C004  _alignedByte                       ca80syscalls
     0000C008  _irqHandlersTable                  ca80syscalls


Naprawdę, warto weryfikować te pliki, a już szczególnie w przypadku stwierdzenia anomalii w działaniu programu, czasem może nam się przytrafić jak to mawiają `czeski błąd`. zamiast .ds napiszemy .db i cuda potrafią się zadziać.

CTC

Teraz przydaś, który ułatwia mi konfigurowanie kostki Z80-CTC. Oczywiście każdy guru od razu pozna, że wartość 0xD7 ustawia kanał CTC w tryb licznik, z przerwaniami, czuły na narastające zbocze, z autostartem i inicjalnym załadowaniem stałej, a stała 0xC7 da to samo, ale z opadającym zboczem. Ja dla odmiany zrobiłam tak:
ca80syscalls.h pisze:

Kod: Zaznacz cały

#define CTC_INTERRUPT_ENABLED   1<<7
#define CTC_INTERRUPT_DISABLED  0
#define CTC_COUNTER_MODE        1<<6
#define CTC_TIMER_MODE          0
#define CTC_PRESCALER_256       1<<5    // tylko TIMER
#define CTC_PRESCALER_16        0
#define CTC_PRESCALER_ANY       0       // nieważne
#define CTC_TRG_RISING_EDGE     1<<4   
#define CTC_TRG_FALLING_EDGE    0
#define CTC_TIMER_START_PULSE   1<<3    // tylko TIMER
#define CTC_TIMER_START_AUTO    0
#define CTC_TIMER_START_ANY     0
#define CTC_NEXT_BYTE_CONST     1<<2
#define CTC_NO_NEXT_BYTE        0
#define CTC_SOFTWARE_RESET      1<<1
#define CTC_CONTINUE_RUN        0

#define CTC_CRTL_BYTE(b7,b6,b5,b4,b3,b2,b1) (b7|b6|b5|b4|b3|b2|b1|1)


I to mi daje w miarę czytelny obraz tego, co ma być ustawione w układzie, zobaczmy to realnym przykładzie programiku, który realizuje dwa sprzętowo-programowe liczniki w przerwaniach maskowalnych od zegara.

irq_ctc_snake_1.c pisze:

Kod: Zaznacz cały

    irqHandlersTable[ 0 ] = (unsigned short)&irqCTCchannelHandler0;
    irqHandlersTable[ 1 ] = (unsigned short)&irqCTCchannelHandler1;       
    irqHandlersTable[ 2 ] = 0x0000;
    irqHandlersTable[ 3 ] = 0x0000;       
       
    setIrqHandlersTable( HIBYTE( irqHandlersTable ) );
   
    //inicjacja CTC, kanal 0 - licznik, narastające zbocza, z przerwaniami
    USER_CTC_CHAN0 = CTC_CRTL_BYTE(
        CTC_INTERRUPT_ENABLED,      // B7
        CTC_COUNTER_MODE,           // B6
        CTC_PRESCALER_ANY,          // B5
        CTC_TRG_RISING_EDGE,        // B4
        CTC_TIMER_START_ANY,        // B3
        CTC_NEXT_BYTE_CONST,        // B2
        CTC_SOFTWARE_RESET          // B1
    );   
    USER_CTC_CHAN0 = 0x3;   // 3->0
    USER_CTC_CHAN0 = LOBYTE( irqHandlersTable );
   
   // drugi kanał podobnie ...   


Część główna - włączenie przerwań w IM2 oraz wyświetlanie wartości liczników:

irq_ctc_snake_1.c pisze:

Kod: Zaznacz cały

   IM2();
    EI();   
    while (1) {
        mikLBYTE( PWYS(2,6), anyCntr0 );
        mikLBYTE( PWYS(2,4), USER_CTC_CHAN0 );   
        mikLBYTE( PWYS(2,2), anyCntr1 );                       
        mikLBYTE( PWYS(2,0), USER_CTC_CHAN1 );           
        delay( 1 );       
    }


Oczywiście megaważne jest zainicjowanie rejestru I, który przechowuje starszą cześć adresu tabeli skoków - do tego powstała dedykowana procedurka, choć można to zrobić tak, jak pokazywał Andrzej - wstawką assemblerową na żywca, w locie.

irq_ctc_snake_1.c pisze:

Kod: Zaznacz cały

void setIrqHandlersTable( unsigned char irqTabHighAddr ) __naked {
    __asm           
        ld IY,#2
        add IY,SP
        ld A,0(IY)      ; mamy górny adres
        ld I,A
        ret
    __endasm;
}


Same procedury obsługujące zgłoszenia z kanałów 0 i 1 są następujące - pierwsza to assemblerowy golasek, druga w C.

irq_ctc_snake_1.c pisze:

Kod: Zaznacz cały

volatile unsigned char anyCntr0 = 0;

void irqCTCchannelHandler0(void) __naked {
    __asm
        push AF
        push HL
        ld HL,#_anyCntr0
        ld A,(HL)
        inc A           ; _anyCntr0++
        ld (HL),A
        pop  HL
        pop  AF
        ei
        reti
    __endasm;
}


irq_ctc_snake_1.c pisze:

Kod: Zaznacz cały

volatile unsigned char anyCntr1 = 0;

void irqCTCchannelHandler1(void) __interrupt {   
    anyCntr1--;
    EI();
}


Wniosek z powyższego ćwiczenia taki, że procedurka z __naked choć wymaga więcej zachodu to daje w sumie pełną kontrolę nad narzutem czasowym (chronimy tylko te rejestry, które potrzeba) oraz nad tym, czy mamy jedno czy wielopoziomową obsługę przerwań - chodzi o położenie rozkazu EI względem RETI. Który wariant wybrać, to zależy od bieżących wymagań, analiza musi być że tak ujmę `per case`.

Pamiątkowy filmik prezentujący CA80 idący w maliny podczas pierwszych etapów walki z lokalizacją tabeli przerwań:

https://youtu.be/DeOy9W5urtA

Finalnie udało mi się uzyskać poprawnie pracujące demko:

https://youtu.be/TNDQgGFIiZ8

Dla wyjaśnienia: od lewej patrząc - widzimy licznik programowy kanału 0, inkrementowany po każdym przerwaniu zgłoszonym z CTC. Przerwanie zgłaszane jest po odliczeniu trzech impulsów (z generatora w AD2). Liczenie również widać na żywo (kolejny hex po lewo), ponieważ CTC ma taką przyjemną cechę, że umożliwia odczyt bieżącej zawartości licznika w locie. Następnie, idąc w prawo widzimy programowy licznik przerwań z kanału 1. On dla odmiany jest dekrementowany, a wyzwala go fakt odliczenia przez kanał 1 zadanej ilości taktów, napęd ponownie z AD2, drugim generatorem. Cała zabawka ładnie i stabilnie pracuje, eksperymentalnie podałam przez chwilę impulsy 5MHz, możliwe że CTC już tego nie zauważał (jest wymaganie na minimalny czas impulsu na TRGx) ale powolne zmniejszanie częstotliwości (grube setki kHz) nie spowodowały żadnych anomalii - CA80 dawał sobie radę.

snake

Kolejne demko ma już zupełnie zabawowy charakter i technologicznie nie było wyzwaniem, no może poza pokonaniem znużenia...
Program obsługuje znane już nam dwa przerwania licznikowe, pierwsze zapewnia zmieniająca się w czasie liczbę, którą możemy pokazywać na wyświetlaczu. Drugie przerwanie zapewnia podmianę funkcji odpowiedzialnej na to pokazywanie, co w efekcie da nam albo liczydełko albo węża-kręciołka z kolejnych segmentów. Oto funkcje wyświetlające, czyli (pojadę na fachowo) - warstwa prezentacyjna:

irq_ctc_snake_2.c pisze:

Kod: Zaznacz cały

volatile TDisplayProc   currentDisplayProc = 0x0000;

void showHexCounter (void) {
    mikLBYTE ( PWYS(2,6), anyCntr0 );
}

const unsigned char snake [ 8 ][ 3 ] = {
    {0x00,0x01,0xFF}, {0x00,0x02,0xFF}, {0x00,0x04,0xFF}, {0x00,0x08,0xFF},
    {0x08,0x00,0xFF}, {0x10,0x00,0xFF}, {0x20,0x00,0xFF}, {0x01,0x00,0xFF}       
};
void showSnake (void) {   
    mikPRINT ( PWYS(2,6), (unsigned short)(snake[ anyCntr0 & 0x7 ]) );
}


Samo przełączenie funkcji odbywa się zupełnie znienacka, po odliczeniu przez kanał 1 zadanej ilości impulsów z pudełeczka AD2.

irq_ctc_snake_2.c pisze:

Kod: Zaznacz cały

void irqCTCchannelHandler1(void) __interrupt {   
    anyCntr1--;
    currentDisplayProc = (anyCntr1 % 2) ? &showSnake : &showHexCounter;   
    EI();
}


Program główny po zainicjowaniu tej całej dziwacznej maszynerii w sumie nic wielkiego nie robi - w pętli woła sobie funkcję zdefiniowaną wskaźnikiem, co tam się dzieje dalej - ma to w nosie, każą odpalać to odpala. Ten warunek na sprawdzanie czy wskaźnik jest pusty jest nieco nadmiarowy, to pozostałość po innych testach i rozmyślaniach, o tym dalej.

irq_ctc_snake_2.c pisze:

Kod: Zaznacz cały

currentDisplayProc = &showHexCounter;       
IM2();
EI();   
mikCLR( PWYS(8,0) );
while (1) {
   if ( currentDisplayProc ) {
      currentDisplayProc();
    }
    delay( 1 );       
}


No i tradycyjnie filmik z liczydło-wężem:

https://youtu.be/9hi0Z6Ckb9A

rywalizacja

A teraz kilka spostrzeżeń na temat ewentualnej rywalizacji o dostęp do żywotnych zasobów aplikacji, tu: licznika anyCntr0 oraz wskaźnika na pocedurę wyświetlającą - currentDisplayProc.

Licznik anyCntr0 jest wartością ośmiobitową, modyfikowaną w przerwaniu, asynchronicznie do programu głównego. Jego wartość wykorzystywana jest wprost do wyświetlenia liczby lub jako indeks semigrafiki. Czy istnieje tu ryzyko jakichkolwiek zaburzeń? Raczej nie. Z maszynowego punktu widzenia inkrementacja licznika jest operacją atomową, niepodzielną (rozkaz INC A) i nie występuje moment czasowy, gdzie cześć wartości jest stara, a reszta nowa. Co innego, gdyby licznik był składowany na przykładowo czterech bajtach (unsigned long). U nas szczęściem tak nie jest.

Teraz wskaźnik na procedurkę wyświetlającą. Zauważmy, że wskaźnik ten jest wykorzystywany w programie głównym, pomijając już tego if-a, jest on wołany na żywioł w kilkumilisekundowych odstępach. Wartość przekazywana jest tu przez operacje szesnastobitowe, dla Z80 także niepodzielne, przykład z pętli głównej:

irq_ctc_snake_2.lst pisze:

Kod: Zaznacz cały

                            375 ;irq_ctc_snake_2.c:182: currentDisplayProc();
   014A 2Ar02r00      [16]  376    ld   hl,(_currentDisplayProc)
   014D CDr00r00      [17]  377    call   __sdcc_call_hl


Jak widać, nie ma tu ryzyka, że do HL zostanie wpisana częściowo stara, a częściowo nowa wartość adresu.
Przykład miejsca, gdzie jest taka teoretyczna możliwość znajduje się zaraz przed głównym while(1). O, proszę:

irq_ctc_snake_2.lst pisze:

Kod: Zaznacz cały

                            353 ;irq_ctc_snake_2.c:174: currentDisplayProc = &showHexCounter;
   0126 FD 21r02r00   [14]  354    ld   iy,#_currentDisplayProc
   012A FD 36 00r72   [19]  355    ld   0 (iy),#<(_showHexCounter)
                                    ; oo, tu! :(
   012E FD 21r02r00   [14]  356    ld   iy,#_currentDisplayProc
   0132 FD 36 01s00   [19]  357    ld   1 (iy),#>(_showHexCounter)


Pomijając realia programu - wyobraźmy sobie, że dwubajtowa zmienna showHexCounter ulega modyfikacji w tle - gdy zajdzie to w zaznaczonym na listingu momencie - currentDisplayProc zostanie w połowie taka niedorobiona, młodszy bajt stary, starszy bajt - już nowy i w przypadku skoku pod ten adres mamy kłopot.

Przejdźmy teraz do handlera drugiego przerwania, który w tle, w tylko sobie znanych momentach przełącza wskaźnik currentDisplayProc, interesuje nas:

irq_ctc_snake_2.lst pisze:

Kod: Zaznacz cały

   00C5 21r7Er00      [10]  279    ld   hl,#_showSnake
   00C8 18 03         [12]  280    jr   00104$
   00CA                     281 00103$:
   00CA 21r72r00      [10]  282    ld   hl,#_showHexCounter
   00CD                     283 00104$:
   00CD 22r02r00      [16]  284    ld   (_currentDisplayProc),hl


Ważna jest ostatnia linijka cytowanego fragmentu - zauważmy, że finalne załadowanie wartości z HL do lokalizacji wskazywanej przez currentDisplayProc jest wykonywane jednym rozkazem transferu. Docelowa wartość będzie tu zawsze spójna.

Tego typu, w sumie nieskomplikowany przegląd kodu warto według mnie wykonać, szczególnie gdy pracujemy z przerwaniami. Jeżeli coś za plecami zmieni nam dane, a nie jest to synchronizowane z ich wykorzystaniem - łatwo o pad aplikacji, a prosze mi wierzyć - takie błędy są upiornie trudne do odtworzenia. Panaceum są rozwiązania dostarczane przez narzędzia - ot, choćby __critical sprytnie wyciszający system przerwań w zadanym bloku kodu lub odpowiedni projekt programu - przykładowo sztuczka z dopisywaniem się do systemowych NMI, która została zaimplementowana w monitorze CA80.

#slowanawiatr
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
______________________________________________ ____ ___ __ _ _ _ _
Kończysz tworzyć dopiero, gdy umierasz. (Marina Abramović)

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

Re: [CA80] migający wąż czyli SDCC i przerwania maskowalne od Z80-CTC

Postautor: gaweł » sobota 11 maja 2019, 03:50

tasza pisze:U mnie wystąpiła konieczność wycyrklowania tabeli na modulo 8 bajtów, z pomocą przychodzi dyrektywa .bndry ułatwiająca ustawienie adresu na podzielną przez n wartość. Mega sprawa.

Nie ma to jak porządne zaklęcie... wszystko da się wtedy zrobić.

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:

[CA80] SDCC i priorytetowy system przerwań Z80 - IM2 odczarowany

Postautor: tasza » niedziela 19 maja 2019, 15:01

♬ ☘ Moja muzyka do kodowania ♬ ♬ ♬ ☘
♫ ♩ ♪ TSA ⚡ ☘ ⚡ Ciągle Walcz ♪ ♩ ♫
https://youtu.be/k5E_l_xkbVo


Tematyka obsługi przerwań maskowalnych została dość dobrze omówiona w tomiku MIK02 dokumentacji CA80. W tomiku MIK04, przy okazji opisu kostki Z80-CTC wspomniano także o priorytetowym trybie pracy systemu przerwań. Opis opisem, ale mi osobiście w tym wszystkim zabrakło konkretnych, zwięzłych fragmentów kodu, możliwych do wpisania do CA80, aby na własne oczy zobaczyć jak działa priorytetyzacja przerwań i jak w sposób programowy zarządzać hierarchią wywołań. Tekst dalej jest próbą uzupełnienia tej drobnej luki, modyfikując przedstawioną aplikację CA80 możemy wykreować praktycznie dowolny wariant obsługi przerwania w IM2, no to zaczynamy...

drobne wsparcie sprzętowe

Z poprzednich pisanek wiemy, że kostka Z80-CTC w sposób naturalny i bez żadnych sprzętowych sztuczek współpracuje z systemem CA80, jako że pochodzi ze `stajni Zilog` doskonale integruje się z łańcuchowym systemem przerwań procesora Z80. Możliwości to jedno, a ich wykorzystanie to drugie. Aby ułatwić sobie obserwację zachowania systemu przerwań skleciłam układzik jak na schemacie poniżej:

000_schemat_pulse_del.png


Sygnał prostokątny (a konkretnie narastające zbocze) z magicznego pudełeczka AD2 podawany na wejście CLK generuje nam dodatkowo dwa narastające zbocza, ale opóźnione o stałe czasowe określone wartościami R1C1,R2C2. Ot, układzik ów niewielki ma wbudowaną skłonność do prokastynacji i dla moich wartości jest ona na poziomie około 80us, wartości dobrałam że tak ujmę `na czuja` i z elementów z łapanki. Ważne jest natomiast to, że sygnały podajemy na wejścia TRG_x w specyficznej kolejności. Wiemy że Z80-CTC potrafi zgłaszać przerwania z kanałów liczących i że kanał 0 ma priorytet najwyższy, kanał ostatni, numer 3 - ma priorytet najniższy. W tym układzie zasilimy impulsami wejścia TRG_x w kolejności od najniższego do najwyższego (TRG_2,TRG_1,TRG_0) i jest to zabieg celowy, aby mieć szanse na zaobserwowanie hierarchii przerwań. Realizacja układu na zdjęciach poniżej, całość w oparciu o jeden układ 4xNAND czyli 7400,37 lub 74132.

001_del_real.jpg


wsparcie programowe

Aplikacja demonstracyjna jest dość prosta i polega na zainicjowaniu układu CTC tak, aby dla trzech kolejnych kanałów generował przerwania przy każdym narastającym zboczu sygnału TRG_x. Wymagane ustawienie kanału realizuje przykładowy kod:
irq_demo_1.c pisze:

Kod: Zaznacz cały

    USER_CTC_CHAN2 = CTC_CRTL_BYTE(
        CTC_INTERRUPT_ENABLED,      // B7
        CTC_COUNTER_MODE,           // B6
        CTC_PRESCALER_ANY,          // B5
        CTC_TRG_RISING_EDGE,       // B4
        CTC_TIMER_START_ANY,        // B3
        CTC_NEXT_BYTE_CONST,        // B2
        CTC_SOFTWARE_RESET          // B1
    );
    USER_CTC_CHAN2 = 1;    // pierwsze _/- zgłosi przerwanie
    USER_CTC_CHAN2 = LOBYTE( irqHandlersTable );


Aby zaobserwować moment, w którym CA80 zacznie wykonywać kod procedury obsługi przerwania wykorzystałam pokładowy 8255, kolejne porty PA,PB,PC służą jako indykatory działania handlerów. Inicjacja kostki 8255 pod to zagadnienie jest banalna i jak widać eksploatuje moje cudaczne makro do konfigurowania peryferiów:

irq_demo_1.c pisze:

Kod: Zaznacz cały

    USER8255_CTRL = P8255_MODE_CTRL_BYTE(
        P8255_GROUP_A_MODE_0,
        P8255_PA_OUTPUT,
        P8255_PC74_OUTPUT,
        P8255_GROUP_B_MODE_0,
        P8255_PB_OUTPUT,
        P8255_PC30_OUTPUT
    );


Przykładowy kod jednej z procedurek obsługi IRQ widzimy poniżej:

irq_demo_1.c pisze:

Kod: Zaznacz cały

void irqCTCchannelHandler1_middle(void) __naked {
    __asm
        ;ei         ; <-- (od|za)komentować jeżeli potrzeba
        push AF
        push BC
        ld A,#0xFF 
        out (USER8255_PB_ADDR),A
        ld B,#100   ; <-- zmienić zależnie od potrzeb, 10,50,100
    tu1:
        nop
        djnz tu1
        ld A,#0x00
        out (USER8255_PB_ADDR),A       
        pop BC       
        pop  AF
        ei          ; <-- (od|za)komentować jeżeli potrzeba
        reti
    __endasm;
}


Pozostałe procedury, obsługujące kanały 0 oraz 2 są identyczne z dokładnością do afektowanych portów układu 8255, gdzie przypisanie jest: przerwanie z kanału 2 - port PC, kanał 1 - port PB, kanał 0 - port PA. Manewrując położeniem rozkazu EI (enable interrupts) w ciele procedury oraz wartością rejestru B, określająca czas wykonania handlera, możemy wygenerować różne przypadki dla obsługi płaskiej i kaskadowej.

Aby już nie motać dalej `prawie identycznie` wyglądającymi procedurami, pozwólcie że będę posługiwać się takim jakby pseudokodem, którego przełożenie na rzeczywisty zapis mnemoniczny w assemblerze jest (mam nadzieję) oczywiste:

irq_demo_1.c pisze:

Kod: Zaznacz cały

hndlr_ch_1:
    PB=1
    DELAY short
    PB=0
    EI



warianty

Przypadek 1 - to ewidentnie strefa komfortu i chillout. Procedurki obsługi przerwań są krótkie, rozkaz EI jest na samym końcu. Nie zachodzi tu zjawisko nakładania na siebie wykonania handlerów i każdy zdąży wykonać swoja pracę przed nadejściem kolejnego przerwania, ich pseudokod taki:

pseudokod pisze:

Kod: Zaznacz cały

hndlr_ch_0:
    PA=1
    DELAY short
    PA=0
    EI

hndlr_ch_1:
    PB=1
    DELAY short
    PB=0
    EI

hndlr_ch_2:
    PC=1
    DELAY short
    PC=0
    EI   


Przebiegi obserwowane w CA80 wyglądają następująco:

002_flat_0_zrzut9.png


Przypadek 2 - komplikujemy sytuację, handlery przerwań wykonują się znacznie dłużej. Kolejne przerwania (od kanałów 1 oraz 0) przychodzą w trakcie gdy pracuje obsługa przerwania 2 (najniższy priorytet). Przerywać nawzajem się nie mogą ponieważ rozkaz EI ciągle znajduje się na końcu:

pseudokod pisze:

Kod: Zaznacz cały

hndlr_ch_0:
    PA=1
    DELAY long
    PA=0
    EI

hndlr_ch_1:
    PB=1
    DELAY long
    PB=0
    EI

hndlr_ch_2:
    PC=1
    DELAY long
    PC=0
    EI   


Obserwowane przebiegi widzimy poniżej:

003_flat_1_zrzut13.png


I tu pierwsza ciekawostka - zwróćmy uwagę na kolejność wywołań handlerów dla kanałów 1 oraz 0. Pomimo, że przerwania zostały zgłoszone w kolejności: kanał 2, kanał 1, kanał 0 to procedury obsługi wywołane zostały w kolejności: handler 2 (to oczywiste), potem handler 0 - ponieważ kanał zero ma wyższy priorytet niż kanał jeden, finalnie wykonano handler 1, wszystko to szeregowo po sobie (sekwencyjnie).

Pewną mutacją tego przypadku byłoby nieprzyzwoite wydłużenie czasu trwania handlera 2 i skrócenie handlerów 1 oraz 0 - działanie będzie takie samo - rysunek niżej:

004_flat_2_zrzut10.png


Jak widzimy, w płaskim wariancie obsługi zapamiętane zgłoszenia przerwań zostaną zrealizowane zgodnie z przydzielonymi im sprzętowo priorytetami.

Przypadek 3 - obsługa priorytetowa. Implementacyjnie różnica jest drobna, aczkolwiek znacząca - rozkaz EI przenosimy na sam początek ciała handlera, co skutkuje tym, że jego wykonanie może zostać przerwane przez zgłoszenie o wyższym priorytecie.

pseudokod pisze:

Kod: Zaznacz cały

hndlr_ch_0:
    EI
    PA=1
    DELAY short
    PA=0

hndlr_ch_1:
    EI
    PB=1
    DELAY short
    PB=0
   
hndlr_ch_2:
    EI   
    PC=1
    DELAY long
    PC=0


Pseudokod powyżej zapewnia, że obsługa kanału 2 (najniższy priorytet) trwa dość długo, handlery kanałów 1 i 0 są dość krótkie i z racji położenia rozkazu EI mogą wciąć się w obsługę handlera 2. Obsługa handlerów 1 i 0 jest zwięzła, wykonanie nie nachodzi na siebie stąd jest ono zgodne z kolejnością zgłoszeń przerwań - najpierw handler 1, potem handler 0, jak na rysunku poniżej:

005_prio_0_zrzut11.png


Przypadek 4 - obsługa priorytetowa, wielopoziomowa. Tu zadbałam o to, aby handlery przerwań o niższych priorytetach (1,2) trwały stosunkowo długo, przerwanie o priorytecie najwyższym (kanał 0) jest krótkie i zwinne. Rozkaz EI oczywiście na początku kodu procedur obsługi:

pseudokod pisze:

Kod: Zaznacz cały

hndlr_ch_0:
    EI
    PA=1
    DELAY short
    PA=0

hndlr_ch_1:
    EI
    PB=1
    DELAY long
    PB=0
   
hndlr_ch_2:
    EI   
    PC=1
    DELAY long
    PC=0


CA80 zachowywał się zgodnie z obrazkiem:

006_prio_1_zrzut12.png


I to chyba kliniczny przypadek obsługi wielopoziomowej - przerwanie o priorytecie wyższym ma prawo przerwać obsługę innych przerwań, niżej uprzywilejowanych, na zasadzie kaskady. Pozycji tej oczywiście nie należy nadużywać, stąd kod obsługi powinien być maksymalnie krótki, aby zapewnić sensowne wykonanie zawieszonych na okazję takiego incydentu pozostałych procedur obsługi przerwań o niższych priorytetach.

Na koniec drobiazg niewielki, w końcu zmęczyłam (to najbardziej adekwatne określenie) makefile do tych programików w SDCC, nie jest to jakieś cudo, ale ma niewątpliwą zaletę - (u mnie) działa.

makefile pisze:

Kod: Zaznacz cały

TARGET      = irq_demo_1.hex
# lokalizacja segmentu kodu (EPROM), do wyboru
#CODE_LOC   = 0000
#CODE_LOC   = 4000
CODE_ORG    = 8000
#lokalizacja segmentu danych (RAM), != kod
#DATA_LOC   = 4000
#DATA_LOC   = 8000
DATA_LOC    = C000
# pliki C, kolejne po spacji
C_FILES     = irq_demo_1.c
# pliki S, startup jako pierwszy!, kolejne po spacji
ASM_FILES   = crt0ca80.s ca80sys.s
#------------------------------------------------------
# start aplikacji po 16 bajtach
CODE_LOC    = $(shell echo ${CODE_ORG}+10|bc)
#ladowarka do pamięci   
CA80MEM     =/home/otoja/PROJECTS/CA80/ca80mem/ca80mem
CC      = sdcc
CC_FLAGS    = --verbose -mz80
AS          = sdasz80
AS_FLAGS    = -g -l
AOBJ_FILES  = $(ASM_FILES:%.s=%.rel)
COBJ_FILES  = $(C_FILES:%.c=%.rel)
OBJ_FILES   = $(AOBJ_FILES) $(COBJ_FILES)

all: $(TARGET)

#linkowanie
$(TARGET): $(OBJ_FILES)
    $(CC) $(CC_FLAGS) --code-loc 0x$(CODE_LOC) --data-loc 0x$(DATA_LOC) --no-std-crt0 -o $(TARGET) $(OBJ_FILES)

#c-ompilacja   
%.rel: %.c
    $(CC) $(CC_FLAGS) -c $^
   
#assemblacja   
%.rel: %.s
    $(AS) $(AS_FLAGS) -o $@  $^

#startup
crt0ca80.s: crt0ca80.template
    cat crt0ca80.template | sed -e "s/__CA80_CODE_BASE__/0x${CODE_ORG}/" > crt0ca80.s   

load:   
    $(CA80MEM) --port=/dev/ttyUSB0 --file=$(TARGET) --reset --verbose

clean:
    rm crt0ca80.s
    rm *.rel
    rm *.hex   


W krótkim podsumowaniu - narzędzie SDCC nie wnosi żadnych ograniczeń, jeżeli chodzi o obsługę systemu przerwań, choć czasem trzeba będzie zejść na poziom assemblera. Opisywane tu przypadki nie wyczerpują zagadnienia, bo możliwych wariantów jest tyle ile konkretnych wymagań odnośnie reakcji systemu na bodźce zewnętrzne, ale jak zauważamy - odpowiednio projektując aplikację i z głową organizując obsługę przerwań (położenie EI) możemy maksymalnie wykorzystać dobrodziejstwa trybu IM2 i wsparcia sprzętowego jakie świadczą kostki Zilog-a - układ CTC, SIO lub PIO.

#slowanawiatr
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
______________________________________________ ____ ___ __ _ _ _ _
Kończysz tworzyć dopiero, gdy umierasz. (Marina Abramović)

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

Re: [CA80] SDCC i priorytetowy system przerwań Z80 - IM2 odczarowany

Postautor: gaweł » niedziela 19 maja 2019, 23:23

tasza pisze:Możliwości to jedno, a ich wykorzystanie to drugie.

Bardzo mądre zdanie.

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 10 gości