Na článek Překladače jazyka C pro osmibitové domácí mikropočítače Atari dnes navážeme. Popíšeme si některé další důležité vlastnosti céčkového překladače cc65, který je určen pro cross překlad programů do assembleru osmibitových mikroprocesorů MOS 6502. Výsledek překladu s využitím cc65 je následně přeložen assemblerem ca65 a objektové soubory, které tímto postupem vzniknou, jsou slinkovány linkerem ld65 nebo utility cl65 (my dnes použijeme tento druhý nástroj, který z pohledu uživatele dokáže nahradit celý řetězec překladu a linkování).
Co se dozvíte v článku
- Krátké zopakování: příklad, který použijeme pro studium vlastností překladače cc65
- Překlad do assembleru s poznámkami obsahujícími původní zdrojový kód
- Zapnutí optimalizací prováděných při překladu
- Statické lokální proměnné
- Lokální proměnné označené jako register
- Přímý přístup k akumulátoru s využitím identifikátoru __A__
- Úprava funkce wsync tak, že se explicitně použije akumulátor
- Výsledek překladu do assembleru
- Základní datové typy, s nimiž překladač cc65 pracuje
- Vyplnění části obrazovky zvoleným znakem
- Varianty ručně psané v assembleru
- První céčková verze s ukazatelem
- Počitadlo smyčky i ukazatel uložený v nulté stránce paměti
- Využití makra POKE
- Kombinace céčka a assembleru
- Výsledek poslední varianty po překladu
- Soubor Makefile pro překlad příkladů z C do assembleru
- Repositář s demonstračními příklady
- Odkazy na Internetu
Jednotlivé kroky překladu si můžeme naznačit takto:
- test.c → test.s (či test.asm, pokud preferujete koncovku .asm)
- test.s nebo test.asm → test.o
- test.o → test.xex (platí jen v případě, že cílem jsou osmibitové počítače Atari)
Největší problém celého naznačeného postupu spočívá v tom, že osmibitový mikroprocesor MOS 6502 má poměrně specifickou architekturu s naprostým minimem pracovních registrů a adresovacími režimy, které jsou určeny spíše pro programátory, kteří použijí přímo assembler a nikoli (z tohoto pohledu) vysokoúrovňový jazyk C. Mnohé céčkové programové konstrukce, které lze na moderních architekturách přeložit optimálním způsobem, dopadnou v případě kombinace cc65+MOS 6502 poměrně špatně a mnohdy až katastrofálně (a to i při porovnání s dobovým překladačem jazyka Action!, což ukazuje na kvalitu software, který byl vyvinut před více než čtyřiceti lety).
V okamžiku, kdy samotnému překladači cc65 „dojde dech“, je mu zapotřebí nějakým způsobem pomoci. Vývojáři přitom mají k dispozici různé metody:
- Překladači je možné napovědět modifikátorem register
- Můžeme se pokusit o přepis konstrukcí, které přistupují do operační paměti
- Překladači lze nařídit, aby namísto běžných lokálních proměnných používal proměnné statické
- Namísto předávání parametrů do funkcí se použijí globální proměnné (což je škaredé, ale je to daň za optimální chod)
- Lze přímo manipulovat s akumulátorem A
- Části programu lze zapsat v assembleru
Krátké zopakování: příklad, který použijeme pro studium vlastností překladače cc65
V úvodním článku o překladačích jazyka C pro osmibitový mikroprocesor MOS 6502 jsme si mj. ukázali i následující příklad, který po svém překladu a spuštění zobrazí sprite (konkrétně prvního hráče) a následně postupně mění horizontální pozici tohoto spritu na obrazovce. Po každém posunu se čeká na další „tik“ hodin reálného času, které mají frekvenci 50 nebo 60Hz. Realizace příkladu v jazyku C může vypadat následovně:
#include <atari.h>
#include <peekpoke.h>
#define RTCLK 0x14
void wsync(void) {
unsigned char old = PEEK(RTCLK);
while (old == PEEK(RTCLK)) {
}
}
void main(void) {
unsigned char x = 128;
GTIA_WRITE.hposp0 = x;
OS.pcolr0 = (5 << 4) + 8;
GTIA_WRITE.grafp0 = 0xFF;
GTIA_WRITE.gractl = 0;
while (1) {
x++;
GTIA_WRITE.hposp0 = x;
wsync();
}
}
Překlad výše uvedeného zdrojového kódu do assembleru provedeme (prozatím) s vypnutými optimalizacemi, a to konkrétně následujícím příkazem:
$ cc65 -t atari sprite5.c -o sprite5.asm
Výsledkem překladu bude zdrojový kód reprezentovaný v assembleru ca65, který by měl vypadat následovně:
;
; File generated by cc65 v 2.18 - Fedora 2.19-15.fc44
;
.fopt compiler,"cc65 v 2.18 - Fedora 2.19-15.fc44"
.setcpu "6502"
.smart on
.autoimport on
.case on
.debuginfo off
.importzp sp, sreg, regsave, regbank
.importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4
.macpack longbranch
.forceimport __STARTUP__
.export _wsync
.export _main
; ---------------------------------------------------------------
; void __near__ wsync (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _wsync: near
.segment "CODE"
ldx #$00
lda $0014
jsr pusha
jmp L0024
L0024: ldy #$00
ldx #$00
lda (sp),y
jsr pushax
ldx #$00
lda $0014
jsr toseqax
jne L0024
jsr incsp1
rts
.endproc
; ---------------------------------------------------------------
; void __near__ main (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _main: near
.segment "CODE"
lda #$80
jsr pusha
ldy #$00
ldx #$00
lda (sp),y
sta $D000
ldx #$00
lda #$58
sta $02C0
ldx #$00
lda #$FF
sta $D00D
ldx #$00
lda #$00
sta $D01D
jmp L003B
L0039: ldy #$00
ldx #$00
lda (sp),y
pha
clc
adc #$01
ldy #$00
sta (sp),y
pla
ldy #$00
ldx #$00
lda (sp),y
sta $D000
jsr _wsync
L003B: jmp L0039
jsr incsp1
rts
.endproc
Obrázek 1: Pohyblivý sprite vytvořený programem napsaným v jazyce C.
Překlad do assembleru s poznámkami obsahujícími původní zdrojový kód
Zejména při zkoumání delšího kódu přeloženého z céčka do assembleru může být výhodné, pokud jsou ve vygenerovaném assembleru uvedeny odkazy na původní céčkový zdrojový kód. V praxi to vypadá tak, že je nejdříve v poznámce zapsán (typicky) jeden původní programový řádek v céčku, za nímž následuje sekvence instrukcí mikroprocesoru MOS 6502, do kterých je tento řádek přeložen.
Odkazy na původní zdrojový kód jsou do assembleru přidány po použití přepínače -T:
$ cc65 -t atari -T sprite5.c
Výsledek bude sice stále neoptimální, ale budeme lépe vědět, které části vyžadují naši pozornost:
;
; File generated by cc65 v 2.18 - Fedora 2.19-15.fc44
;
.fopt compiler,"cc65 v 2.18 - Fedora 2.19-15.fc44"
.setcpu "6502"
.smart on
.autoimport on
.case on
.debuginfo off
.importzp sp, sreg, regsave, regbank
.importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4
.macpack longbranch
.forceimport __STARTUP__
.export _wsync
.export _main
; ---------------------------------------------------------------
; void __near__ wsync (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _wsync: near
.segment "CODE"
;
; unsigned char old = PEEK(RTCLK);
;
ldx #$00
lda $0014
jsr pusha
;
; while (old == PEEK(RTCLK)) {
;
jmp L0024
L0024: ldy #$00
ldx #$00
lda (sp),y
jsr pushax
ldx #$00
lda $0014
jsr toseqax
jne L0024
;
; }
;
jsr incsp1
rts
.endproc
; ---------------------------------------------------------------
; void __near__ main (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _main: near
.segment "CODE"
;
; unsigned char x = 128;
;
lda #$80
jsr pusha
;
; GTIA_WRITE.hposp0 = x;
;
ldy #$00
ldx #$00
lda (sp),y
sta $D000
;
; OS.pcolr0 = (5 << 4) + 8;
;
ldx #$00
lda #$58
sta $02C0
;
; GTIA_WRITE.grafp0 = 0xFF;
;
ldx #$00
lda #$FF
sta $D00D
;
; GTIA_WRITE.gractl = 0;
;
ldx #$00
lda #$00
sta $D01D
;
; while (1) {
;
jmp L003B
;
; x++;
;
L0039: ldy #$00
ldx #$00
lda (sp),y
pha
clc
adc #$01
ldy #$00
sta (sp),y
pla
;
; GTIA_WRITE.hposp0 = x;
;
ldy #$00
ldx #$00
lda (sp),y
sta $D000
;
; wsync();
;
jsr _wsync
;
; while (1) {
;
L003B: jmp L0039
;
; }
;
jsr incsp1
rts
.endproc
Zapnutí optimalizací prováděných při překladu
V předchozích dvou kapitolách jsme mohli vidět, že generovaný kód není v žádném případě dokonalý. Překladač cc65 nabízí přepínač -O pro zapnutí základních optimalizací. Ovšem nečekejme žádné zázraky, nejedná se o clang/LLVM ani o gcc. Navíc je ještě možné dalšími znaky přesněji určit, jaké optimalizace se mají provádět:
| Přepínač | Význam |
|---|---|
| i | zapne inlining delších částí kódu |
| r | proměnné označené jako register mohou být uloženy v nulté stránce paměti |
| s | povolení inliningu standardních funkcí |
Většinou se zapínají všechny optimalizační volby současně (-Oirs), takže typický příkaz pro překlad vypadá takto:
$ cc65 -Oirs -t atari -T sprite5.c
Výsledek je již lepší, ale stále není dokonalý:
;
; File generated by cc65 v 2.18 - Fedora 2.19-15.fc44
;
.fopt compiler,"cc65 v 2.18 - Fedora 2.19-15.fc44"
.setcpu "6502"
.smart on
.autoimport on
.case on
.debuginfo off
.importzp sp, sreg, regsave, regbank
.importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4
.macpack longbranch
.forceimport __STARTUP__
.export _wsync
.export _main
; ---------------------------------------------------------------
; void __near__ wsync (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _wsync: near
.segment "CODE"
;
; unsigned char old = PEEK(RTCLK);
;
lda $0014
jsr pusha
;
; while (old == PEEK(RTCLK)) {
;
ldy #$00
L0044: lda (sp),y
cmp $0014
beq L0044
;
; }
;
jmp incsp1
.endproc
; ---------------------------------------------------------------
; void __near__ main (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _main: near
.segment "CODE"
;
; unsigned char x = 128;
;
lda #$80
jsr pusha
;
; GTIA_WRITE.hposp0 = x;
;
ldy #$00
lda (sp),y
sta $D000
;
; OS.pcolr0 = (5 << 4) + 8;
;
lda #$58
sta $02C0
;
; GTIA_WRITE.grafp0 = 0xFF;
;
lda #$FF
sta $D00D
;
; GTIA_WRITE.gractl = 0;
;
sty $D01D
;
; x++;
;
L0039: ldy #$00
lda (sp),y
clc
adc #$01
sta (sp),y
;
; GTIA_WRITE.hposp0 = x;
;
sta $D000
;
; wsync();
;
jsr _wsync
;
; while (1) {
;
jmp L0039
.endproc
Statické lokální proměnné
U mnoha architektur mikroprocesorů se můžeme setkat s tím, že se parametry funkcí i lokální proměnné ukládají do zásobníkových rámců (stack frame). Pokud se o podobný přístup pokusíme na architektuře MOS 6502, je nutné zásobníkový rámec simulovat přes adresu (uloženou v operační paměti), která napodobuje registr SP, jenž na MOS 6502 v plnohodnotné podobě neexistuje (zásobník zde má jen 256 adres a navíc není umožněno relativní adresování přes offset – což je možná největší nedostatek MOS 6502). Z tohoto důvodu jsme také mohli vidět snahu o simulaci klasického zásobníkového rámce:
; unsigned char old = PEEK(RTCLK);
;
lda $0014
jsr pusha
;
; while (old == PEEK(RTCLK)) {
;
ldy #$00
L0044: lda (sp),y
cmp $0014
beq L0044
Alternativně je možné používat statické lokální proměnné, což jsou vlastně globální proměnné, které jsou ovšem z pohledu programátora dostupné pouze v jedné funkci. Generovaný kód by měl být nepatrně menší i rychlejší. Přepínačem -Cl lze použití statických lokálních proměnných vynutit, a to i tehdy, pokud není použito klíčové slovo static:
$ cc65 -Oirs -Cl -t atari -T sprite5.c
Výsledek již nevypadá tak špatně. Příkladem je funkce wsync, která se přeloží do následující podoby:
;
; unsigned char old = PEEK(RTCLK);
;
lda $0014
sta L0021
;
; while (old == PEEK(RTCLK)) {
;
L0045: lda $0014
cmp L0021
beq L0045
Celý příklad byl přeložen do této podoby:
;
; File generated by cc65 v 2.18 - Fedora 2.19-15.fc44
;
.fopt compiler,"cc65 v 2.18 - Fedora 2.19-15.fc44"
.setcpu "6502"
.smart on
.autoimport on
.case on
.debuginfo off
.importzp sp, sreg, regsave, regbank
.importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4
.macpack longbranch
.forceimport __STARTUP__
.export _wsync
.export _main
; ---------------------------------------------------------------
; void __near__ wsync (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _wsync: near
.segment "BSS"
L0021:
.res 1,$00
.segment "CODE"
;
; unsigned char old = PEEK(RTCLK);
;
lda $0014
sta L0021
;
; while (old == PEEK(RTCLK)) {
;
L0045: lda $0014
cmp L0021
beq L0045
;
; }
;
rts
.endproc
; ---------------------------------------------------------------
; void __near__ main (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _main: near
.segment "BSS"
L002C:
.res 1,$00
.segment "CODE"
;
; unsigned char x = 128;
;
lda #$80
sta L002C
;
; GTIA_WRITE.hposp0 = x;
;
sta $D000
;
; OS.pcolr0 = (5 << 4) + 8;
;
lda #$58
sta $02C0
;
; GTIA_WRITE.grafp0 = 0xFF;
;
lda #$FF
sta $D00D
;
; GTIA_WRITE.gractl = 0;
;
lda #$00
sta $D01D
;
; x++;
;
L003B: inc L002C
;
; GTIA_WRITE.hposp0 = x;
;
lda L002C
sta $D000
;
; wsync();
;
jsr _wsync
;
; while (1) {
;
jmp L003B
.endproc
Lokální proměnné označené jako register
Proměnnou použitou ve funkci wsync lze označit slovem register. Taková proměnná bude při použití přepínače -Or uložena na nulté stránce paměti (kde je ovšem místo pouze pro několik takových proměnných):
#include <atari.h>
#include <peekpoke.h>
#define RTCLK 0x14
void wsync(void) {
register unsigned char old = PEEK(RTCLK);
while (old == PEEK(RTCLK)) {
}
}
void main(void) {
unsigned char x = 128;
GTIA_WRITE.hposp0 = x;
OS.pcolr0 = (5 << 4) + 8;
GTIA_WRITE.grafp0 = 0xFF;
GTIA_WRITE.gractl = 0;
while (1) {
x++;
GTIA_WRITE.hposp0 = x;
wsync();
}
}
Povšimněte si, že se nyní pro uložení proměnné používá adresa regbank+5:
; ---------------------------------------------------------------
; void __near__ wsync (void)
; ---------------------------------------------------------------
;
; register unsigned char old = PEEK(RTCLK);
;
lda regbank+5
jsr pusha
lda $0014
sta regbank+5
;
; while (old == PEEK(RTCLK)) {
;
L0044: lda $0014
cmp regbank+5
beq L0044
;
; }
;
ldy #$00
lda (sp),y
sta regbank+5
jmp incsp1
.endproc
; ---------------------------------------------------------------
; void __near__ main (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _main: near
.segment "BSS"
L002B:
.res 1,$00
.segment "CODE"
;
; unsigned char x = 128;
;
lda #$80
sta L002B
;
; GTIA_WRITE.hposp0 = x;
;
sta $D000
;
; OS.pcolr0 = (5 << 4) + 8;
;
lda #$58
sta $02C0
;
; GTIA_WRITE.grafp0 = 0xFF;
;
lda #$FF
sta $D00D
;
; GTIA_WRITE.gractl = 0;
;
lda #$00
sta $D01D
;
; x++;
;
L003A: inc L002B
;
; GTIA_WRITE.hposp0 = x;
;
lda L002B
sta $D000
;
; wsync();
;
jsr _wsync
;
; while (1) {
;
jmp L003A
.endproc
Přímý přístup k akumulátoru s využitím identifikátoru __A__
V okamžiku, kdy již selhávají jak možnosti překladače, tak i způsob vyjádření sémantiky programu v čistém céčku, musí nastoupit další fáze – sestoupíme o jednu úroveň níže. Prozatím sice nikoli až na úroveň assembleru, ale již budeme muset znát možnosti a schopnosti samotného mikroprocesoru MOS 6502, zejména omezení, která se týkají použití pracovních registrů. Můžeme si například „vynutit“, aby se určitá operace neprováděla s hodnotou uloženou v operační paměti, ale přímo s akumulátorem A.
Překladač cc65 skutečně umožňuje přímou manipulaci s akumulátorem s využitím identifikátoru __A__. Z pohledu programátora se jedná o proměnnou typu unsigned char. Podívejme se na jednoduchý příklad manipulace s akumulátorem. Nejdříve do něj přeneseme konstantu a posléze jeho hodnotu zvýšíme o jedničku:
void main(void)
{
__A__ = 42;
++__A__;
}
Po překladu tohoto jednoduchého programu z céčka do assembleru uvidíme, že výsledkem je do značné míry optimální kód – viz zvýrazněné instrukce, které přímo manipulují s akumulátorem:
;
; File generated by cc65 v 2.18 - Ubuntu 2.18-1
;
.fopt compiler,"cc65 v 2.18 - Ubuntu 2.18-1"
.setcpu "6502"
.smart on
.autoimport on
.case on
.debuginfo off
.importzp sp, sreg, regsave, regbank
.importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4
.macpack longbranch
.forceimport __STARTUP__
.export _main
; ---------------------------------------------------------------
; void __near__ main (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _main: near
.segment "CODE"
;
; __A__ = 42;
;
lda #$2A
;
; ++__A__;
;
clc
adc #$01
;
; }
;
rts
.endproc
Úprava funkce wsync tak, že se explicitně použije akumulátor
Připomeňme si, že původně měla funkce wsync definovanou lokální proměnnou old, která se nejdříve nastavila na aktuální hodnotu čítače a následně byla použita i v programové smyčce s podmínkou:
void wsync(void) {
unsigned char old = PEEK(RTCLK);
while (old == PEEK(RTCLK)) {
}
}
Můžeme se pokusit o přepis funkce tak, aby se namísto lokální proměnné přímo používal akumulátor. To sice obecně není vždy možné, ale u takto jednoduché funkce můžeme přepis provést:
void wsync(void) {
__A__ = PEEK(RTCLK);
while (__A__ == PEEK(RTCLK)) {
}
}
Překlad této funkce (pochopitelně se zapnutými optimalizacemi) dopadne následovně:
.proc _wsync: near
lda $0014
L0025: cmp $0014
beq L0025
rts
Povšimněte si, že se v tomto případě skutečně jedná o optimální kód, který je již prakticky nemožné přepsat do kratší či rychlejší podoby.
Pro úplnost si uvedeme i celý zdrojový kód takto upraveného příkladu:
#include <atari.h>
#include <peekpoke.h>
#define RTCLK 0x14
void wsync(void) {
__A__ = PEEK(RTCLK);
while (__A__ == PEEK(RTCLK)) {
}
}
void main(void) {
unsigned char x = 128;
GTIA_WRITE.hposp0 = x;
OS.pcolr0 = (5 << 4) + 8;
GTIA_WRITE.grafp0 = 0xFF;
GTIA_WRITE.gractl = 0;
while (1) {
x++;
GTIA_WRITE.hposp0 = x;
wsync();
}
}
Výsledek překladu do assembleru
Teprve poslední variantu přeloženého kódu můžeme považovat za optimální – kód nyní vypadá prakticky stejně, jako kdybychom ho přímo ručně zapsali v assembleru:
;
; File generated by cc65 v 2.18 - Fedora 2.19-15.fc44
;
.fopt compiler,"cc65 v 2.18 - Fedora 2.19-15.fc44"
.setcpu "6502"
.smart on
.autoimport on
.case on
.debuginfo off
.importzp sp, sreg, regsave, regbank
.importzp tmp1, tmp2, tmp3, tmp4, ptr1, ptr2, ptr3, ptr4
.macpack longbranch
.forceimport __STARTUP__
.export _wsync
.export _main
; ---------------------------------------------------------------
; void __near__ wsync (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _wsync: near
.segment "CODE"
;
; __A__ = PEEK(RTCLK);
;
lda $0014
;
; while (__A__ == PEEK(RTCLK)) {
;
L0025: cmp $0014
beq L0025
;
; }
;
rts
.endproc
; ---------------------------------------------------------------
; void __near__ main (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _main: near
.segment "BSS"
L002C:
.res 1,$00
.segment "CODE"
;
; unsigned char x = 128;
;
lda #$80
sta L002C
;
; GTIA_WRITE.hposp0 = x;
;
sta $D000
;
; OS.pcolr0 = (5 << 4) + 8;
;
lda #$58
sta $02C0
;
; GTIA_WRITE.grafp0 = 0xFF;
;
lda #$FF
sta $D00D
;
; GTIA_WRITE.gractl = 0;
;
lda #$00
sta $D01D
;
; x++;
;
L003B: inc L002C
;
; GTIA_WRITE.hposp0 = x;
;
lda L002C
sta $D000
;
; wsync();
;
jsr _wsync
;
; while (1) {
;
jmp L003B
.endproc
Základní datové typy, s nimiž překladač cc65 pracuje
Ve druhé polovině článku si ukážeme několik variant demonstračního příkladu, ve kterém jsou definovány proměnné typu uint8_t (tedy bajt) a uint8_t * (ukazatel na bajt). Typ uint8_t je definován, pochopitelně společně s dalšími typy, v hlavičkovém souboru stdint.h:
/* Exact-width integer types */ typedef signed char int8_t; typedef int int16_t; typedef long int32_t; typedef unsigned char uint8_t; typedef unsigned uint16_t; typedef unsigned long uint32_t;
Nalezneme zde i maximální a minimální hodnoty pro jednotlivé typy:
#define INT8_MIN ((int8_t) 0x80) #define INT8_MAX ((int8_t) 0x7F) #define INT16_MIN ((int16_t) 0x8000) #define INT16_MAX ((int16_t) 0x7FFF) #define INT32_MIN ((int32_t) 0x80000000) #define INT32_MAX ((int32_t) 0x7FFFFFFF) #define UINT8_MAX ((uint8_t) 0xFF) #define UINT16_MAX ((uint16_t) 0xFFFF) #define UINT32_MAX ((uint32_t) 0xFFFFFFFF)
Definovány jsou i dva typy, které lze použít pro uložení adresy:
/* Integer types capable of holding object pointers */ typedef int intptr_t; typedef unsigned uintptr_t;
Vyplnění části obrazovky zvoleným znakem
Ve druhé části dnešního článku se pokusíme vytvořit jednoduchý program, který po svém spuštění vyplní prvních pět textových řádků ve standardním textovém režimu. Každý řádek obsahuje čtyřicet znaků, takže je nutné vyplnit pouze 5×40=200 bajtů. A to je „pěkná“ hodnota, protože je menší než 256 (dokonce menší než 255 – to při implementaci v céčku), což celý algoritmus zjednoduší. Ukážeme si několik způsobů implementace:
- Verze v assembleru se dvěma index registry X i Y
- Verze v assembleru s jediným index registrem Y (ten je zde výhodnější)
- Vyplňování od nižších adres k vyšším
- Verze napsaná v idiomatickém C
- Úprava předchozí varianty pro použití proměnných uložených v nulté stránce paměti
- Pokus o zápis do paměti makrem POKE
- Kombinace céčka s assemblerem
Varianty ručně psané v assembleru
První varianta programu pro vyplnění pěti textových řádků bude používat index registr Y ve funkci offsetu a index registr X bude počitadlo smyčky. Offset se zvyšuje instrukcí INY (INcrement Y) a adresa pro zápis je vypočtena na základě adresy uložené v buňkách 88 a 89, ke které je připočten právě obsah registru Y. Počitadlo budeme postupně snižovat instrukcí DEX (DEcrement X), která mj. nastavuje i příznakový bit zero. Ten je testován instrukcí BNE (Branch if Not Equal), čímž je realizována celá programová smyčka:
.include "atari.inc"
.CODE
.proc main
lda #33 ; kod znaku, ktery se bude tisknout
ldx #40*5 ; počet zápisů
ldy #0 ; offset
clear:
sta (88), y ; tisk znaku "A" na zvolené místo na obrazovce
; (adresa Video RAM je na adresách 88 a 89)
iny ; zvýšit hodnotu offsetu
dex ; zmenšit hodnotu počitadla
bne clear ; skok, pokud X>0
loop: jmp loop
end:
.endproc
.segment "EXEHDR"
.word $ffff ; uvodni sekvence bajtu v souboru XEX
.word main ; zacatek kodoveho segmentu
.word main::end - 1 ; konec kodoveho segmentu
.segment "AUTOSTRT" ; segment s pocatecni adresou
.word RUNAD ; naplni se pouze adresy RUNAD a RUNAD+1
.word RUNAD+1
.word main ; adresa vstupniho bodu do programu
; finito
Po překladu a spuštění tohoto programu by měla mít obrazovka následující obsah:
Obrázek 2: Obrazovka ve standardním textovém režimu; prvních pět řádků je vyplněno jednoduchou programovou smyčkou.
Program lze ovšem zjednodušit – zmenšit počet použitých registrů, přeorganizovat smyčku atd.:
.include "atari.inc"
.CODE
.proc main
lda #33 ; kod znaku, ktery se bude tisknout
ldy #40*5 ; počet zápisů
clear:
dey ; zmenšit hodnotu počitadla a offsetu
sta (88), y ; tisk znaku "A" na zvolené místo na obrazovce
; (adresa Video RAM je na adresách 88 a 89)
bne clear ; skok, pokud Y>0
loop: jmp loop
end:
.endproc
.segment "EXEHDR"
.word $ffff ; uvodni sekvence bajtu v souboru XEX
.word main ; zacatek kodoveho segmentu
.word main::end - 1 ; konec kodoveho segmentu
.segment "AUTOSTRT" ; segment s pocatecni adresou
.word RUNAD ; naplni se pouze adresy RUNAD a RUNAD+1
.word RUNAD+1
.word main ; adresa vstupniho bodu do programu
; finito
Samozřejmě se můžeme pokusit o úpravu počítané programové smyčky takovým způsobem, že budeme provádět vyplňování od prvního znaku a nikoli od znaku posledního. Počitadlo a současně i offset reprezentovaný index registrem Y tedy bude začínat hodnotou 0 a bude postupně zvyšováno až do hodnoty 40×5=200. Po dosažení této hodnoty bude počítaná programová smyčka ukončena:
.include "atari.inc"
.CODE
.proc main
lda #33 ; kod znaku, ktery se bude tisknout
ldy #0 ; počitadlo zápisů
clear:
sta (88), y ; tisk znaku "A" na zvolené místo na obrazovce
; (adresa Video RAM je na adresách 88 a 89)
iny ; zvětšit hodnotu počitadla a offsetu
cpy #40*5 ; test na koncovou hodnotu počitadla
bne clear ; skok, pokud Y<40*5
loop: jmp loop
end:
.endproc
.segment "EXEHDR"
.word $ffff ; uvodni sekvence bajtu v souboru XEX
.word main ; zacatek kodoveho segmentu
.word main::end - 1 ; konec kodoveho segmentu
.segment "AUTOSTRT" ; segment s pocatecni adresou
.word RUNAD ; naplni se pouze adresy RUNAD a RUNAD+1
.word RUNAD+1
.word main ; adresa vstupniho bodu do programu
; finito
První céčková verze s ukazatelem
První varianta céčkové verze programu, který po svém překladu a spuštění vyplní pět řádků textové obrazovky vybraným znakem, využívá počitadlo smyčky a ukazatel, který se po zápisu zvýší o jedničku. Jedná se o pravděpodobně nejvíce idiomatický způsob zápisu:
#include <stdint.h>
#include <peekpoke.h>
void main(void) {
uint8_t *dest = (uint8_t *)PEEKW(88);
uint8_t i = 0;
for (i=0; i<40*5; i++) {
*dest++ = 33;
}
while (1) {
}
}
Vlastnosti překladače c65 již poměrně dobře známe, takže pro nás asi nebude překvapující, že výsledek překladu (i se zapnutými optimalizacemi) není příliš oslnivý. Spíše je tomu přesně naopak – prakticky se zde nevyužívají specifické vlastnosti index registrů atd.:
; ---------------------------------------------------------------
; void __near__ main (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _main: near
.segment "BSS"
L0002:
.res 2,$00
L0006:
.res 1,$00
.segment "CODE"
;
; uint8_t *dest = (uint8_t *)PEEKW(88);
;
lda $0058+1
sta L0002+1
lda $0058
sta L0002
;
; uint8_t i = 0;
;
lda #$00
sta L0006
;
; for (i=0; i<40*5; i++) {
;
sta L0006
L0018: lda L0006
cmp #$C8
L0019: bcs L0019
;
; *dest++ = 33;
;
lda L0002
ldx L0002+1
sta regsave
stx regsave+1
clc
adc #$01
bcc L0011
inx
L0011: sta L0002
stx L0002+1
lda #$21
ldy #$00
sta (regsave),y
;
; for (i=0; i<40*5; i++) {
;
inc L0006
jmp L0018
.endproc
Počitadlo smyčky i ukazatel uložený v nulté stránce paměti
Ve druhém kroku se pokusíme lokální proměnné obsahující ukazatel na zapisovaný znak i počitadlo zapsaných znaků označit jako register, což pro cc65 znamená, že se tyto dvě proměnné pokusí umístit do několika volných bajtů v nulté stránce paměti. Funkci main už nebude možné (korektně) volat rekurzivně, ale to je v tomto případě rozumná cena za obecně rychlejší program:
#include <stdint.h>
#include <peekpoke.h>
void main(void) {
register uint8_t *dest = (uint8_t *)PEEKW(88);
register uint8_t i = 0;
for (i=0; i<40*5; i++) {
*dest++ = 33;
}
while (1) {
}
}
Opět se podívejme na výsledek překladu, z něhož je patrné, že se používají adresy regbank+3 až regbank+5 (ukazatel je uložen ve dvou bajtech, počitadlo v bajtu jednom):
; ---------------------------------------------------------------
; void __near__ main (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _main: near
.segment "CODE"
lda regbank+4
ldx regbank+5
jsr pushax
lda $0058+1
sta regbank+4+1
lda $0058
sta regbank+4
lda regbank+3
jsr pusha
lda #$00
sta regbank+3
sta regbank+3
L0016: lda regbank+3
cmp #$C8
L0017: bcs L0017
lda regbank+4
ldx regbank+4+1
sta regsave
stx regsave+1
clc
adc #$01
bcc L000F
inx
L000F: sta regbank+4
stx regbank+4+1
lda #$21
ldy #$00
sta (regsave),y
inc regbank+3
jmp L0016
.endproc
Využití makra POKE
Dále by nás mohlo napadnout, jestli nakonec nebude výhodnější provést zápis do obrazové paměti makrem POKE a napodobit tak BASIC. Není nic jednoduššího, než si tuto variantu otestovat a přepsat program do následující podoby:
#include <stdint.h>
#include <peekpoke.h>
void main(void) {
uint16_t scr = PEEKW(88);
register uint8_t i = 0;
for (i=0; i<40*5; i++) {
POKE(scr, 33);
scr++;
}
while (1) {
}
}
Výsledek, který získáme překladem do assembleru, ovšem nebude v žádném případě ohromující, a to přesto, že alespoň počitadlo smyčky bude uložené v nulté stránce paměti:
; ---------------------------------------------------------------
; void __near__ main (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _main: near
.segment "CODE"
lda $0058
ldx $0058+1
jsr pushax
lda regbank+5
jsr pusha
lda #$00
sta regbank+5
sta regbank+5
L001A: lda regbank+5
cmp #$C8
L001B: bcs L001B
ldy #$02
lda (sp),y
sta ptr1+1
dey
lda (sp),y
sta ptr1
lda #$21
dey
sta (ptr1),y
ldy #$02
lda (sp),y
tax
dey
lda (sp),y
clc
adc #$01
bcc L0014
inx
L0014: jsr staxysp
inc regbank+5
jmp L001A
.endproc
Kombinace céčka a assembleru
V některých situacích je vhodné si přiznat, že možnosti poskytované překladačem cc65 nedostačují nárokům vývojáře a je čas přejít na nižší úroveň abstrakce – k assembleru mikroprocesorů MOS 6502. Zde nám může cc65 pomoci, protože umožňuje do céčkového kódu vkládat instrukce assembleru pomocí __asm__(„instrukce“, případné parametry). Je zde jedno omezení – návěští (tedy cíle skoků) se definují přímo v céčku a navíc se takové návěští musí do příslušné instrukce skoku (či instrukce LD? nebo ST?) předat jako parametr, což je poněkud zvláštní (a určitě se nejedná o standardní konstrukci jazyka C).
Podívejme se, jak lze náš program pro vyplnění pěti řádků textové obrazovky přepsat do kombinace céčka a assembleru. Pravděpodobně nejkomplikovanější je poslední přímo zapsaná instrukce skoku, které předáme návěští:
#include <stdint.h>
#include <peekpoke.h>
void main(void) {
__asm__("lda #33");
__asm__("ldy #40*5");
clear:
__asm__("dey");
__asm__("sta (88), y");
__asm__("bne %g", clear);
while (1) {
}
}
Tento postup sice umožňuje tvořit tak efektivní kód, jaký by byl zapsaný v čistém assembleru, ale současně je zde nutné upozornit na několik pastí. Především vůbec nedochází ke kontrolám, zda například nepoužíváme stejný registr ve dvou různých rolích (zejména tehdy, pokud céčková část přímo přistupuje k __A__. Navíc můžeme do __asm__ vlastně zapsat jakékoli instrukce, i ty neexistující. Kontrola bude provedena až v dalším kroku, tedy při překladu assembleru do objektového kódu.
Výsledek poslední varianty po překladu
Z výpisu obsahu souboru, který vznikl překladem z céčka do assembleru, je zřejmé, že tentokrát je výsledný kód přesně takový, jaký požadujeme. Pochopitelně jsme za to ale zaplatili tím, že jsme museli část programu napsat přímo v assembleru:
; ---------------------------------------------------------------
; void __near__ main (void)
; ---------------------------------------------------------------
.segment "CODE"
.proc _main: near
.segment "CODE"
lda #33
ldy #40*5
L0006: dey
sta (88),y
bne L0006
L0013: jmp L0013
.endproc
Soubor Makefile pro překlad příkladů z C do assembleru
Pro překlad všech minule i dnes ukázaných příkladů byl použit následující soubor Makefile:
INCLUDE_PATH=/usr/share/cc65/include
execs := background_color.xex \
poke.xex \
sprite1.xex \
sprite2.xex \
sprite3.xex \
sprite4.xex \
sprite5.xex \
sprite5_1.xex \
sprite5_2.xex \
sprite5_3.xex \
sprite5_4.xex \
sprite6.xex \
sprite7.xex \
print_1.xex \
print_2.xex \
print_3.xex \
print_4.xex
all: $(execs)
clean:
rm -f *.o
rm -f *.xex
.PHONY: all clean
%.o: %.c
%.xex: %.o
cl65 -t atari -Oirs $< -o $@ -m $(basename $<).map
%.o: %.asm
ca65 $< -o $@
%.asm: %.c
cc65 -I ${INCLUDE_PATH} -Oirs -t atari $< -o $@
sprite5_1.asm: sprite5.c
cc65 -t atari $< -o $@
sprite5_2.asm: sprite5.c
cc65 -t atari -T $< -o $@
sprite5_3.asm: sprite5.c
cc65 -Oirs -t atari -T $< -o $@
sprite5_4.asm: sprite5.c
cc65 -Oirs -t atari -T -Cl $< -o $@
sprite6.asm: sprite6.c
cc65 -Oirs -t atari -T -Cl $< -o $@
sprite7.asm: sprite7.c
cc65 -Oirs -t atari -T $< -o $@
print_1.asm: print_1.c
cc65 -Oirs -t atari -Cl -T $< -o $@
Repositář s demonstračními příklady
Všechny demonstrační příklady, s nimiž jsme se v předchozích článcích i v článku dnešním seznámili a které jsou určeny pro překlad s využitím assembleru ca65, jsou dostupné, jak je zvykem, na GitHubu. V tabulce níže jsou uvedeny odkazy na jednotlivé zdrojové kódy příkladů psané v assembleru i „listingy“ vygenerované samotným assemblerem, ze kterých je patrné, jakým způsobem se jednotlivé příklady přeložily do výsledného XEX souboru:
Odkazy na Internetu
- MOS 6502 instruction set
http://www.6502.org/users/obelisk/6502/instructions.html - EXE File Format Description
https://gury.atari8.info/refs/file_formats_exe.php - XEX Filter – A toolkit to analyze and manipulate Atari binary files
https://www.vitoco.cl/atari/xex-filter/index.html - chkxex.py
https://raw.githubusercontent.com/seban-slt/tcx_tools/refs/heads/master/chkxex.py - ca65 Users Guide
https://cc65.github.io/doc/ca65.html - cc65 Users Guide
https://cc65.github.io/doc/cc65.html - ld65 Users Guide
https://cc65.github.io/doc/ld65.html - da65 Users Guide
https://cc65.github.io/doc/da65.html - Překladače jazyka C pro historické osmibitové mikroprocesory
https://www.root.cz/clanky/prekladace-jazyka-c-pro-historicke-osmibitove-mikroprocesory/ - Překladače programovacího jazyka C pro historické osmibitové mikroprocesory (2)
https://www.root.cz/clanky/prekladace-programovaciho-jazyka-c-pro-historicke-osmibitove-mikroprocesory-2/ - Getting Started Programming in C: Coding a Retro Game with C Part 2
https://retrogamecoders.com/getting-started-with-c-cc65/ - NES game development in 6502 assembly – Part 1
https://kibrit.tech/en/blog/nes-game-development-part-1 - NES 6502 Programming Tutorial – Part 1: Getting Started
https://dev.xenforo.relay.cool/index.php?threads/nes-6502-programming-tutorial-part-1-getting-started.858389/ - Minimal NES example using ca65
https://github.com/bbbradsmith/NES-ca65-example - List of 6502-based Computers and Consoles
https://www.retrocompute.co.uk/list-of-6502-based-computers-and-consoles/ - 6502 – the first RISC µP
http://ericclever.com/6500/ - 3 Generations of Game Machine Architecture
http://www.atariarchives.org/dev/CGEXPO99.html - “Hello, world” from scratch on a 6502 — Part 1
https://www.youtube.com/watch?v=LnzuMJLZRdU - A Tour of 6502 Cross-Assemblers
https://bumbershootsoft.wordpress.com/2016/01/31/a-tour-of-6502-cross-assemblers/ - Adventures with ca65
https://atariage.com/forums/topic/312451-adventures-with-ca65/ - example ca65 startup code
https://atariage.com/forums/topic/209776-example-ca65-startup-code/ - 6502 PRIMER: Building your own 6502 computer
http://wilsonminesco.com/6502primer/ - 6502 Instruction Set
https://www.masswerk.at/6502/6502_instruction_set.html - Chip Hall of Fame: MOS Technology 6502 Microprocessor
https://spectrum.ieee.org/tech-history/silicon-revolution/chip-hall-of-fame-mos-technology-6502-microprocessor - Single-board computer
https://en.wikipedia.org/wiki/Single-board_computer - www.6502.org
http://www.6502.org/ - 6502 PRIMER: Building your own 6502 computer – clock generator
http://wilsonminesco.com/6502primer/ClkGen.html - Great Microprocessors of the Past and Present (V 13.4.0)
http://www.cpushack.com/CPU/cpu.html - Jak se zrodil procesor?
https://www.root.cz/clanky/jak-se-zrodil-procesor/ - Osmibitové mikroprocesory a mikrořadiče firmy Motorola (1)
https://www.root.cz/clanky/osmibitove-mikroprocesory-a-mikroradice-firmy-motorola-1/ - Mikrořadiče a jejich použití v jednoduchých mikropočítačích
https://www.root.cz/clanky/mikroradice-a-jejich-pouziti-v-jednoduchych-mikropocitacich/ - Mikrořadiče a jejich aplikace v jednoduchých mikropočítačích (2)
https://www.root.cz/clanky/mikroradice-a-jejich-aplikace-v-jednoduchych-mikropocitacich-2/ - 25 Microchips That Shook the World
https://spectrum.ieee.org/tech-history/silicon-revolution/25-microchips-that-shook-the-world - Comparison of instruction set architectures
https://en.wikipedia.org/wiki/Comparison_of_instruction_set_architectures - How To Start Learning Atari 8 Bit Assembly For Free
https://forums.atariage.com/topic/300732-how-to-start-learning-atari-8-bit-assembly-for-free/ - WUDSN (Demo Group)
https://www.wudsn.com/ - Machine Language For Beginners
https://www.atariarchives.org/mlb/ - Assembly language: all about I/O
https://www.atarimagazines.com/v3n8/AllAbout_IO.html - Sedmdesátiny assemblerů: lidsky čitelný strojový kód
https://www.root.cz/clanky/sedmdesatiny-assembleru-lidsky-citelny-strojovy-kod/ - Color names
https://atariwiki.org/wiki/Wiki.jsp?page=Color%20names - ATASCII
https://en.wikipedia.org/wiki/ATASCII - Put characters in display ram isn't ATASCII?
https://forums.atariage.com/topic/359973-put-characters-in-display-ram-isnt-atascii/ - ATASCII And Internal Character Code Values
https://www.atariarchives.org/mapping/appendix10.php - Reading ATASCII from the keyboard in assembly
https://forums.atariage.com/topic/361733-reading-atascii-from-the-keyboard-in-assembly/ - Why does the 6502 JSR instruction only increment the return address by 2 bytes?
https://retrocomputing.stackexchange.com/questions/19543/why-does-the-6502-jsr-instruction-only-increment-the-return-address-by-2-bytes - Pushing return address to stack off by 1 byte
https://forums.atariage.com/topic/378206-pushing-return-address-to-stack-off-by-1-byte/ - Intel x86 documentation has more pages than the 6502 has transistors
https://www.righto.com/2013/09/intel-x86-documentation-has-more-pages.html - Clearing a Section of Memory
http://www.6502.org/source/general/clearmem.htm - Practical Memory Move Routines by Bruce Clark
http://www.6502.org/source/general/memory_move.html - 6502 Assembly Programming Guide
https://neumont-gamedev.github.io/posts/retrogamedev-6502-guide/ - Off-by-one error
https://en.wikipedia.org/wiki/Off-by-one_error - 6502 cycle times
https://www.nesdev.org/wiki/6502_cycle_times - Atari TIA
http://www.atarihq.com/danb/tia.shtml - TIA Playfield
http://www.atarihq.com/danb/TIA/Playfield.shtml - Atari Inc.:
ANTIC C012296 (NTSC) Revision D
Atari Incorporated, Sunnyvale CA, 1982 - Atari Inc.:
GTIA C014805 (NTSC) Revision A
Atari Incorporated, Sunnyvale CA, 1982 - Atari 5200
http://www.atariage.com/software_search.html?SystemID=5200 - Atari 5200 Hardware and Accessories
http://www.atariage.com/5200/archives/hardware.html - Atari 5200 Screenshots
http://www.atariage.com/system_items.html?SystemID=5200&ItemTypeID=SCREENSHOT - History of video game consoles (second generation): Wikipedia
http://en.wikipedia.org/wiki/History_of_video_game_consoles_(second_generation) - Atari 5200: Wikipedia
http://en.wikipedia.org/wiki/Atari_5200 - Player-Missile Graphics
https://www.atariarchives.org/agagd/chapter5.php - Sprite (computer graphics)
https://en.wikipedia.org/wiki/Sprite_(computer_graphics) - Atari Graphics Demonstrations by Underground Software, 1985 | Atari 8 bit Demo
https://www.youtube.com/watch?v=h7N9EYSyCkw - Atari 8-bit Display List Interrupts: A Complete(ish) Tutorial
https://playermissile.com/dli_tutorial/ - Atari Assembler Editor manual
https://atariwiki.org/wiki/attach/Atari%20Assembler%20Editor/ATARI%20Assembler%20Editor%20User-s%20Manual-OCR.pdf - Atari 8-bit Fine Scrolling: A Complete(ish) Tutorial
https://playermissile.com/scrolling_tutorial/index.html - Atari Fine Scrolling
https://www.atarimagazines.com/compute/issue67/338_1_Atari_Fine_Scrolling.php - CTIA / GTIA Pinout Diagram
https://user.xmission.com/~trevin/atari/gtia_pinout.html - GTIA Modes
https://page6.org/archive/issue_02/page_10.htm - 56 graphic modes
https://www.atari800xl.eu/docs/kb/kb-hardware-0005-atari-8bit-56-graphic-modes.html - UNLOCKING THE 56 GRAPHIC MODES
https://www.atarimagazines.com/v3n5/allmodes.html - GTIA Modes 9, 10 & 11 + ANTIC data and color clocks
https://forums.atariage.com/topic/366256-gtia-modes-9–10–11-antic-data-and-color-clocks/ - ANTIC, GTIA and timing info
https://atarimax.com/jindroush.atari.org/atanttim.html - 6.10 Cycle timing
https://github.com/AnimaInCorpore/A8E/blob/main/AHRM/6.%20CTIA-GTIA/10.%20Cycle%20timing.md - Doug Neubauer
https://en.wikipedia.org/wiki/Doug_Neubauer - POKEY
https://en.wikipedia.org/wiki/POKEY - Inside the Atari 800XL
https://www.goto10retro.com/p/800xl-inside - Grayscale Project – „Jam Session“ – Chiptune Visualization / Atari SAP
https://www.youtube.com/watch?v=Qx-AHgvwrHo&list=PL92E73FD91764173B - POKEY MUSIC ( ATARI XL / XE ) =+ MUSIC FROM POLAND += DEMO
https://www.youtube.com/watch?v=5PswfMjMop4&list=RD5PswfMjMop4&start_radio=1 - Atari.org
http://www.atari.org/ - Atari POKEY
http://www.absoluteastronomy.com/topics/Atari_POKEY - Chiptune
http://www.absoluteastronomy.com/topics/Chiptune - ASAP – Another Slight Atari Player
http://asap.sourceforge.net/ - Atari SAP music archive
http://asma.atari.org/ - RASTER Music Tracker (RMT)
http://raster.infos.cz/atari/rmt/rmt.htm - POKEY explorer
https://github.com/ivop/pokey-explorer - POKEY C012294 Documentation
https://7800.8bitdev.org/index.php/POKEY_C012294_Documentation - Pokey Registers
https://user.xmission.com/~trevin/atari/pokey_regs.html - POKEY CO12294
http://krap.pl/mirrorz/atari/homepage.ntlworld.com/kryten_droid/Atari/800XL/atari_hw/pokey.htm - De Re Atari: SOUND
https://www.atariarchives.org/dere/chapt07.php#H7_3_5 - Paddle (game controller)
https://en.wikipedia.org/wiki/Paddle_(game_controller) - Interfacing Your Atari
https://www.atariarchives.org/creativeatari/Interfacing_Your_Atari.php - Atari SIO
https://en.wikipedia.org/wiki/Atari_SIO - Atari Chips
https://atarimax.com/jindroush.atari.org/achip.html - Table 4: Keyboard Code Index, KEYCODE: ATASCII
https://www.atariarchives.org/c3ba/kcindex.php - SIO2PC
http://raster.atariportal.cz/hw/sio2pc.htm - Using Serial Input/Output
https://www.atarimagazines.com/compute/issue64/insight_atari.php - SIO ROUTINES
https://www.atariarchives.org/mmm/sio.php - ATARI 8 bit Serial Input/Output (SIO)
https://allpinouts.org/pinouts/connectors/serial/atari-8-bit-serial-input-output-sio/ - SIOspecs
https://archive.org/details/SIOspecs - Interview with Joe Decuir
http://www.atarimuseum.com/articles/joedecuir.html - USB AND OS/2
http://www.os2voice.org/VNL/past_issues/VNL1199H/vnewsf5.htm - Standards and specs: The ins and outs of USB
http://www.ibm.com/developerworks/power/library/pa-spec7.html - USB Class Codes
http://www.usb.org/developers/defined_class - Universal Serial Bus
http://en.wikipedia.org/wiki/Usb - SMBus.org
http://www.smbus.org - SMBus FAQ
http://www.smbus.org/faq/faq.htm - System Management Bus
http://en.wikipedia.org/wiki/System_Management_Bus - SMBus Specifications
http://www.smbus.org/specs - SMBus Control Method Interface Specification Version 1.0
http://www.smbus.org/specs/smbus_cmi10.pdf - System Management Bus (SMBus) Specification Version 2.0
http://www.smbus.org/specs/smbus20.pdf - Atari Connection Machine
http://acm.atariportal.cz/s/con8bit/8bits - Atari Program Recorder
https://en.wikipedia.org/wiki/Atari_Program_Recorder - THE ATARI CASSETTE
https://www.atariarchives.org/dere/chaptC.php - Turbo-2000
https://www.atari-800.cz/turbo-2000 - Atari XC12 FSK Decoder
https://zeninstruments.blogspot.com/2024/08/atari-xc12-fsk-decoder.html - Clock recovery
https://en.wikipedia.org/wiki/Clock_recovery - Jiří Richter (programátor)
https://cs.wikipedia.org/wiki/Ji%C5%99%C3%AD_Richter_(program%C3%A1tor) - Spectrum tape interface
https://sinclair.wiki.zxnet.co.uk/wiki/Spectrum_tape_interface - How Tape Loading Works
https://lemmings.info/how-tape-loading-works/ - CIO Reference
https://atarifoundry.com/docs/forge/advanced-topics/cio-reference - The central input-output system in Atari computers
https://www.atariarchives.org/alp/chapter_9.php - ATR: chpt.1: Central Input/Output (CIO)
https://www.atariarchives.org/cfn/12/02/0010.php - NOTE and POINT
https://atarifoundry.com/docs/forge/advanced-topics/note-point - Customizing the Atari Operating System Device Handlers: Part 1
https://www.atarimagazines.com/v8n2/customhandlers.html - Customizing the Atari Operating System Device Handlers: Part II
https://www.atarimagazines.com/v8n3/OperatingSystemHandlers.html - Spectrum tape interface
https://sinclair.wiki.zxnet.co.uk/wiki/Spectrum_tape_interface - Turbo 2000
http://wiki.oldcomp.cz/index.php?title=Turbo_2000 - Původní manuál k systému Turbo 2000
http://wiki.oldcomp.cz/images/6/65/Zak_praha_1987_p2_turbo2000.pdf - Mark and space
https://en.wikipedia.org/wiki/Mark_and_space - Obdélníkový průběh
https://cs.wikipedia.org/wiki/Obd%C3%A9ln%C3%ADkov%C3%BD_pr%C5%AFb%C4%9Bh - All you ever wanted to know about: Atari Cartridges
https://atariwiki.org/wiki/Wiki.jsp?page=Cartridges - Manuál k MAC/65
https://data.atariwiki.org/DOC/MAC-65_and_BUG-65_Rev._1.1-OSS-OCR.pdf - Manuál k DDT
https://atariwiki.org/wiki/attach/DDT/DDT_1.0_Manual.pdf - MAC/65 na Atari wiki
https://www.atariwiki.org/wiki/Wiki.jsp?page=Mac65 - Madass – home page
https://mads.atari8.info/ - Mad-Assembler (GitHub)
https://github.com/tebe6502/Mad-Assembler - The 6502/65C02/65C816 Instruction Set Decoded
https://llx.com/Neil/a2/opcodes.html - Action
https://atariwiki.org/wiki/Wiki.jsp?page=Action#top - Action: manuál
https://www.atarimania.com/documents/atari-action-reference-manual.pdf - Getting in on the Action 1
https://atariwiki.org/wiki/Wiki.jsp?page=Getting%20in%20on%20the%20Action%201 - Getting in on the Action 2
https://atariwiki.org/wiki/Wiki.jsp?page=Getting%20in%20on%20the%20Action%202 - Category:Action! na RosettaCode
https://rosettacode.org/wiki/Category:Action! - Deep Blue C Compiler
https://www.atarimania.com/games/atari-400–800-xl-xe-utilities-deep-blue-c-compiler-12478 - DEEP BLUE C Manual
https://www.atariarchives.org/APX/showdocs.php?cat=20166 - C/65
https://www.atarimania.com/games/atari-400–800-xl-xe-utilities-c-65–19204 - Getting Started Programming in C: Coding a Retro Game with C Part 2
https://retrogamecoders.com/getting-started-with-c-cc65/ - CC65 Advanced optimizations
https://github.com/ilmenit/CC65-Advanced-Optimizations/tree/master/04-get%20rid%20of%20C%20stack
