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.
Sodobni ABI dodeli parametre registrom od leve proti desni:
ABI dodeli registre glede na kombinacijo tipov. V registrih sta lahko največ dva parametra:
| 1. parameter | Register | 2. parameter | Register |
|---|---|---|---|
| uint8_t | A | uint8_t | L |
| uint8_t | A | uint16_t / kazalec | DE |
| uint16_t / kazalec | HL | uint16_t / kazalec | DE |
| uint16_t / kazalec | HL | uint8_t | sklad |
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.
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.
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);
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);
DJNZUkaz 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
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);
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).
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:
ESC (27) in eno ali več dodatnih kod.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”:
#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.