Partner WFG, 2FG, 1FG – PRIROČNIK ZA PROGRAMIRANJE

7. NAPREDNE PROGRAMSKE TEHNIKE

==============================

7.1. KLIC ZBIRNIŠKE KODE IZ C

Kadar C prevajalnik ne more doseči zadostne hitrosti ali kadar moramo dostopati do procesorskih ukazov, ki nimajo ustreznika v C (npr. ukazi za V/I vrata IN/OUT), napišemo funkcijo v zbirnem jeziku. SDCC za Z80 podpira sodobni ABI (__sdcccall(1)), ki parametre posreduje prek registrov – brez dražjega klicnega okvirja na skladu.

Dodelitev registrov

Sodobni ABI dodeli parametre registrom od leve proti desni:

ABI dodeli registre glede na kombinacijo tipov. V registrih sta lahko največ dva parametra:

1. parameterRegister2. parameterRegister
uint8_tAuint8_tL
uint8_tAuint16_t / kazalecDE
uint16_t / kazalecHLuint16_t / kazalecDE
uint16_t / kazalecHLuint8_tsklad

Tretji in nadaljnji parametri vedno gredo na sklad (klicanec jih ne čisti – to naredi klicatelj). Povratna vrednost: uint8_t → A, uint16_t/kazalec → DE.

Opazna asimetrija: kombinacija (uint16_t, uint8_t) ne dobi drugega registra – za 8-bitni drugi parameter po HL-u ni prostega registrskega reže, zato gre na sklad. Kombinacija (uint8_t, uint16_t) pa se v celoti prenese v registrih (A in DE).

Registri AF, BC, DE, HL, IY so registri klicatelja (caller-saved) – zbirniška funkcija jih sme uničiti. Register IX je register klicanca (callee-saved) – če ga funkcija uporablja, ga mora obnoviti pred RET.

Oblika zbirniške datoteke

SDCC prevede C simbol foo v zbirniško ime _foo. Globalni simbol razglasimo z dvojno dvopičje (::):

; Datoteka: myfunc.s
.module myfunc
.globl _myfunc

; uint16_t myfunc(uint16_t a, uint16_t b) __sdcccall(1)
; Vstop: HL = a,  DE = b
; Izhod: DE = rezultat
_myfunc::
    add     hl, de      ; HL = a + b
    ex      de, hl      ; DE = rezultat (povratna vrednost uint16_t gre v DE)
    ret

Datoteko dodamo med vire v src/Makefile enako kot .c datoteke. V C jo razglasimo z atributom __sdcccall(1):

uint16_t myfunc(uint16_t a, uint16_t b) __sdcccall(1);

Kadar celoten projekt gradi z zastavico --sdcccall=1 (dodamo jo v CFLAGS v Makefile), atribut ni potreben – sodobni ABI velja za vse funkcije.

Partner WFG, 2FG, 1FG – PRIROČNIK ZA PROGRAMIRANJE

Primer 1 – branje V/I vrat (IN)

CP/M ne izpostavlja neposrednega dostopa do V/I vrat. Za branje strojnega porta napišemo tanko ovojnico v zbirnem jeziku. Ukaz IN A,(C) prebere bajt iz vrat, katerih naslov je v registru C.

; Datoteka: portio.s
.module portio
.globl _inp
.globl _outp

; uint8_t inp(uint8_t port) __sdcccall(1)
; A = naslov vrat
; Vrne: A = prebrani bajt
_inp::
    ld      c, a        ; naslov vrat iz A v C
    in      a, (c)      ; preberi bajt iz vrat (C) -> A
    ret                 ; rezultat ze v A

; void outp(uint8_t port, uint8_t val) __sdcccall(1)
; A = naslov vrat,  L = vrednost za zapis
_outp::
    ld      c, a        ; naslov vrat iz A v C (A bo prepisan)
    ld      a, l        ; vrednost iz L v A
    out     (c), a      ; zapisi A v vrata (C)
    ret

Razglasitev in uporaba v C:

#include <stdint.h>

uint8_t inp (uint8_t port)            __sdcccall(1);
void    outp(uint8_t port, uint8_t v) __sdcccall(1);

/* Primer: preberi statusni register na naslovu 0x50 */
uint8_t status = inp(0x50);

/* Primer: nastavi bit 0 na vratih 0x51 */
outp(0x51, status | 0x01);
Partner WFG, 2FG, 1FG – PRIROČNIK ZA PROGRAMIRANJE

Primer 2 – branje in pisanje pomnilnika (peek / poke)

Primer prikazuje kombinaciji (uint16_t)→HL in (uint8_t, uint16_t)→A,DE. Povratna vrednost uint8_t gre v A.

; Datoteka: peekpoke.s
.module peekpoke
.globl _peek
.globl _poke

; uint8_t peek(uint16_t addr) __sdcccall(1)
; HL = addr
; Vrne: A = prebrani bajt
_peek::
    ld      a, (hl)     ; preberi bajt na naslovu HL
    ret                 ; rezultat v A

; void poke(uint8_t val, uint16_t addr) __sdcccall(1)
; A = val (ker je 1. param uint8_t)
; DE = addr (ker je 2. param uint16_t po uint8_t -> DE)
_poke::
    ld      h, d        ; HL = addr (iz DE)
    ld      l, e
    ld      (hl), a     ; zapisi val na naslov
    ret

Razglasitev in uporaba v C:

#include <stdint.h>

uint8_t peek(uint16_t addr)             __sdcccall(1);
void    poke(uint8_t val, uint16_t addr) __sdcccall(1);

/* Preberi bajt na naslovu 0xC000 */
uint8_t b = peek(0xC000);

/* Zapisi 0xFF na naslov 0xC000 */
poke(0xFF, 0xC000);

Primer 3 – programska zakasnitev z zanko DJNZ

Ukaz DJNZ zmanjša B za 1 in skoči, dokler B ≠ 0. Daje natančno časovno zanko brez overhead-a C zanke.

; void delay_ms(uint8_t ms) __sdcccall(1)
; A = stevilo milisekund (priblizno, pri 4 MHz)
.globl _delay_ms
_delay_ms::
    ld      b, a        ; stevec ms v B
00001$:
    push    bc
    ld      b, #200     ; notranja zanka: ~1 ms pri 4 MHz
00002$:
    djnz    00002$
    pop     bc
    djnz    00001$
    ret
Partner WFG, 2FG, 1FG – PRIROČNIK ZA PROGRAMIRANJE

7.2. BRANJE TIPKOVNICE

Dva načina zaznave tipke

Funkcija kbhit() preveri, ali čaka neprebrana tipka, in se takoj vrne – vrne 0, če tipke ni, ali neničelno vrednost, če je. Privzeto deluje prek CP/M BDOS in zazna samo znake, ki jih je CP/M že shranil v svojo medpomnilniško vrsto.

#include <partner/conio.h>

/* Neblokirajoce branje v zanki */
while (1) {
    if (kbhit()) {
        char c = getchar();   /* preberi znak, ki ga je BDOS ze zaznal */
        /* obdelaj tipko c ... */
    }
    /* ostalo delo programa ... */
}

Obnašanje kbhit() spremenimo s klicem kbhit_set_bdos():

/* Privzeto: tipkovnica prek CP/M BDOS */
kbhit_set_bdos(true);

/* Alternativa: neposredno anketiranje serijskega vmesnika SIO */
kbhit_set_bdos(false);

Opozorilo: preusmeritev SIO iz prekinitvenega načina

Ko je kbhit_set_bdos(false), knjižnica anketira serijski vmesnik SIO neposredno prek funkcije kbd_poll_key(). Ta pristop ne bo deloval zanesljivo, dokler CP/M tipkovničnega gonilnika ne preusmerimo iz prekinitvenega v anketni način.

Razlog je arhitekturni: CP/M konfigurira čip SIO za delovanje z prekinitvami (interrupt-driven). Ko prispe znak, SIO sproži prekinitev in CP/M ga shrani v svojo vrsto. Če hkrati anketiramo SIO neposredno, se nam in CP/M gonilniku podijo za isti bajt – eden od obeh ga bo izgubil.

Pravilna rešitev je, da pred direktnim anketiranjem onemogočimo prekinitev SIO in čip prekonfiguriramo v anketni način. Po koncu neposrednega branja je treba prekinitev znova omogočiti, sicer CP/M ne bo več prejemal znakov s tipkovnice. Ta postopek je napredna tema in presega obseg tega priročnika; za večino primerov zadošča privzeti CP/M način z kbhit_set_bdos(true).

Partner WFG, 2FG, 1FG – PRIROČNIK ZA PROGRAMIRANJE

7.3. TIPKOVNICA V IGRAH

Tipkovnica Partnerja je neodvisna serijska naprava, ki deluje kot terminal – ob pritisku tipke pošlje ASCII kodo znaka, ob njeni sprostitvi pa ne pošlje ničesar. Zato:

  • Ne obstaja razlika med key down in key up – dobimo samo en znak ob pritisku.
  • Tipk Shift, Ctrl ipd. ni mogoče zaznati samostojno; tipkovnica jih kombinira in pošlje prirejeni znak.
  • Specialne tipke (puščice, funkcijske tipke) pošljejo zaporedje ESC (27) in eno ali več dodatnih kod.

Simulacija tipke “držane dol” z auto-repeat

Partner ima strojni auto-repeat: ko držimo tipko, jo tipkovnica po kratkem zamiku začne samodejno ponavljati. Ta lastnost omogoča preprost postopek za zaznavo “tipka je še pritisnjena”:

  1. Ko prejmemo tipko, si jo zapomnimo kot trenutno pritisnjeno in zabeležimo čas.
  2. V vsaki ponovitvi glavne zanke preverimo, ali je prispela ista tipka znova (auto-repeat). Če ja, ponastavimo čas.
  3. Če do naslednjega prihoda tipke mine več kot auto-repeat interval (pribl. 100–150 ms), zaključimo, da je tipka sproščena.
#include <partner/conio.h>
#include <partner/timer.h>

#define KEY_HELD_MS 150   /* nekoliko vec kot auto-repeat interval */

static char  held_key  = 0;
static uint16_t held_t = 0;

/* Pokliči enkrat na iteracijo igricne zanke.
   Vrne trenutno "drzano" tipko ali 0. */
char game_key(void) {
    char k = kbhit();
    if (k) {
        held_key = k;
        held_t   = timer_ms();
    } else if (held_key) {
        if (timer_diff(held_t, KEY_HELD_MS) > 0)
            held_key = 0;    /* preteklo prevec casa: tipka sproscena */
    }
    return held_key;
}

/* Primer: premikanje lika */
int x = 40;
while (1) {
    char k = game_key();
    if (k == 'a') x--;
    if (k == 'd') x++;
    /* ... risanje ... */
}

Postopek deluje, ker je interval preverjanja v igričini zanki krajši od auto-repeat zamika tipkovnice. Čas KEY_HELD_MS nastavimo na vrednost, ki je večja od auto-repeat periode (da tipka ostane “dol” med ponovitvami), a dovolj kratka, da ob sprostitvi reagiramo hitro.