<< powrót | Cz/B

Atari CC65 tutorial

Wstęp

Yaron Nir zrobił tutorial cc65 na Atari, niestety po hebrajsku.

[Youtube] Tutorial Yarona Nira 1

Pobrałem filmy yt-dlp, odseparowałem audio, wrzuciłem mp3 do goodtape.io, a hebrajski srt do tłumacza google, co dało mi napisy po polsku:

[Zip] Napisy

Dwie warstwy AI, jakość jest fatalna i często bezsensowna. Ale jeśli ktoś czytał poniższe, to coś dla siebie wyciągnie:

[Link] De re Atari po polsku

"Mapping the Atari" Ian Chadwick

Nie jestem ekspertem od C ani od Atari, nie znam asemblera, to jest zapis moich eksperymentów. Komputer: Windows, Atari: 800XL.

Środowisko

[Link] https://github.com/cc65/cc65

Jeśli mamy gita i MS Visual Studio 2022 to klonujemy repo, w katalogu `C:\cc65\src` otwieramy cc65.sln i kompilujemy. Może być potrzeba podbicia wersji WinSDK, na co pozwalamy.

Jeśli nie zamierzamy kompilować to pozostaje pobrać starą wersję w Releases i gdzieś wypakować zipa.

Do zmiennej path dodajemy C:\cc65\bin i to już powinno działać w terminalu: cc65 --version

Jako edytor polecam Visual Studio Code, ale notepad++ też wystarczy. Kompilować będziemy w terminalu.

Jako emulator polecam Altirrę do uruchamiania i debugowania kodu.

[Link] Altirra

Nie potrzebujemy dodatkowych romów, ale możemy je pobrać ze starym emulatorem X-Former 2.5

[Link] xf25

Mad-Studio do grafiki

[Link] Mad-Studio

Może się też przydać hex edytor do oglądania plików xex, notepad++ ma wtyczkę HEX-Editor.

Hello world

Napiszmy hello world:

#include <stdio.h>
#include <conio.h>

int main (void)
{
    printf("Hello, world!");
    while(!kbhit());
    return 0;
}			

Skompilujmy cl65 -t atari hello_printf.c -o hello_printf.xex

[Link] Inne parametry cl65

Plik hello_printf.xex przeciągamy na okno Altirry.

[Obrazek] Screen

Rozmiar pliku xex to 2937 bajtów. Czyli strasznie dużo z naszego 64kB, a tak naprawdę z 62kB(bo Hardware), a tak naprawdę 48kB(bo OS i DOS). Zastąpmy printf użyciem puts(): puts("Hello, world!"); Skompilujmy, rozmiar u mnie to: 1068 bajtów. Albo użyciem cputs:

#include <conio.h>

int main (void)
{
    cputs("Hello, world!");
    while(!kbhit());
    return 0;
}

844 bajtów.

A może piszmy prosto do pamięci RAM obrazu:

#include <conio.h>
#include <atari.h>
#include <string.h>

#include <atari_screen_charmap.h>
char* hello = "Hello, world!";
#include <atari_atascii_charmap.h>

int main (void)
{
    memcpy(OS.savmsc, hello, 13);
    while(!kbhit());
    return 0;
}

677 bajtów.

Albo użyjmy procedury OSa CIOV we wstawce asemblerowej:

#include <atari.h>
#include <conio.h>

#define CIOV 0xE456
#define CHANNEL 0

void ciov(void){
    asm("LDX #%b", CHANNEL * 16);
    asm("JSR %w", CIOV);
    asm("RTS");
}

void printl(void* c, int l)
{
  OS.iocb[CHANNEL].buffer=c;
  OS.iocb[CHANNEL].buflen=l;
  OS.iocb[CHANNEL].command=IOCB_PUTCHR;
  ciov();
}

int main (void)
{
    printl("Hello, world!", 13);
    while(!kbhit());
    return 0;
}

633 bajty.

Jak widać cc65 pożera RAM jak szalony. Której metody używać? Żadnej, każdej, to zależy. printf("Wynik=%d", zmienna) przydaje się do formatowania danych. puts jest napisana w C i używa procedury write(C:\cc65\libsrc\common), być może jest bardziej przenośna na inne platformy. cputs jest napisana w asemblerze(C:\cc65\libsrc\conio) i używa cputc.s(C:\cc65\libsrc\atari). Pisanie bezpośrednio do pamięci ekranu przyda się w grach, zamiast sztywnego #include <atari_screen_charmap.h> możemy też dynamicznie konwertować ATASCII na internal screen codes. CIOV to już typowa funkcja Atari OS. Nie warto zbyt wcześnie optymalizować, używajmy printf lub puts, a w innych trybach ekranowych i tak będziemy pisać do pamięci.

Optymalizacje

Ogólnie chodzi o to żeby nie pisać zbyt wysokopoziomowo, należy traktować cc65 jako zaawansowany asembler.

[Link] ilmenit / CC65-Advanced-Optimizations

ilmenit pisze żeby nie optymalizować zbyt wcześnie, ale niektóre punkty według mnie warto stosować od razu.

[Link] cc65 coding hints

Mapa pamięci

[Obrazek] Mapa pamięci z De re Atari [Link] Prezentacja Yarona Nira [Link] Mapa pamięci

$00 = 0x00 = hex zapis asemblera, 1 bajt

$0000 = 2 bajty, word

$00 - $7Fstrona zerowa OSa - nie używać
$80 - $FFstrona zerowa której możesz użyć
$100 - 1FFstos 6502
$200 - $2FFwektory i rejestry OSa
$300 - $3FFrejestry OSa, IOCB, HATABS, CIO
$400 - $5FFbufory OSa, magnetofon, QMEG
$600 - $6FFstrona 6 dla programów Basic - nie używać
$700 - $1FFFDOS - nie używać
$2000 - RAMTOPnasz kod
$8000 - $9FFFcartridge albo RAM
$A000 - $BFFFdrugi kartridż lub wysoka połowa kartridźa 16k lub RAM
$C000 - $CFFFOS część 1
$CC00 - $CFFFmiędzynarodowy zestaw znaków
$D800 - $DFFFFPP operacje zmiennoprzecinkowe Basica, PBI
$E000 - $FFFFOS część 2
$E000 - $E3FFdomyślny zestaw znaków

$D000 - $D7FF - czipy Atari, dodatkowe wyspecjalizowane procesory, każdy zajmuje 1 stronę(256 bajtów), ale niekoniecznie całej używa

$D000 - $D0FFGTIA "druga karta graficzna" odpowiada za piksele, kolory, sprajty/graczy pociski, wprowadzanie znaków z klawiatury
$D100 - $D1FFPBI port równoległy, gniazdo rozszerzeń
$D200 - $D2FFPOKEY "karta muzyczna" odpowiada za paddle(wiosełko), klawiaturę, transmisję szeregową SIO i dźwięk
$D300 - $D3FFPIA odpowiada za joystick, zarządzanie pamięcią, np wyłączenie OSa i Basica
$D400 - $D4FFANTIC "karta graficzna" odpowiada za DMA dostarcza GTIA danych
$D500 - $D5FFRejestry kartridży
$D600 - $D7FFPBI, Zarezerwowane dla wbudowanych rozszerzeń np: VBXE

Tutorial właściwy

W nowym folderze tworzymy pliki

build.bat

cl65 -t atari -O -S main.c
move main.s main.asm

cl65 --debug-info -Wl --dbgfile,guess.lab -m guess.map -Ln guess.lbl -t atari -Oi main.c -o guess.xex -C atari.cfg

guess.xex

atari.cfg

Z folderu C:\cc65\cfg kopiujemy sobie plik atari.cfg do naszego folderu. W przyszłości użyjemy go do organizacji pamięci naszego programu.

FEATURES {
    STARTADDRESS: default = $2000;
}

Gdzie zaczyna się nasz kod.

[Link] MEMLO różnych DOSów
SYMBOLS {
    __EXEHDR__:          type = import;
    __SYSTEM_CHECK__:    type = import;  # force inclusion of "system check" load chunk
    __AUTOSTART__:       type = import;  # force inclusion of autostart "trailer"
    __STACKSIZE__:       type = weak, value = $0800; # 2k stack
    __STARTADDRESS__:    type = export, value = %S;
    __RESERVED_MEMORY__: type = weak, value = $0000;
}
[Link] Sekcja symboli

Widoczne symbole generowane dla asemblera. import - symbol jest zdefiniowany gdzieś indziej i jego definicja będzie ładowana na etapie linkowania. export - definiujemy i udostępniamy symbol. weak - jak export, ale tylko, jeśli nikt inny nie zdefiniował symbolu.

[Link] Atari config files

__STACKSIZE__ = rozmiar stosu C, można zmniejszyć jeśli zastosujemy optymalizację ilmenita 04 - ograniczenie parametrów funkcji.

%S = STARTADDRESS z FEATURES w poprzednim akapicie. Musi być zdefiniowane wcześniej niż sekcja SYMBOLS.

__RESERVED_MEMORY__ = RAM na pamięć obrazu dla -t atari. Dla -t atarixl nie używane, zamiast tego zwiększamy STARTADDRESS.

MEMORY {
    ZP:         file = "", define = yes, start = $0082, size = $007E;

# file header, just $FFFF
    HEADER:     file = %O,               start = $0000, size = $0002;

# "system check" load chunk
    SYSCHKHDR:  file = %O,               start = $0000, size = $0004;
    SYSCHKCHNK: file = %O,               start = $2E00, size = $0300;
    SYSCHKTRL:  file = %O,               start = $0000, size = $0006;

# "main program" load chunk
    MAINHDR:    file = %O,               start = $0000, size = $0004;
    MAIN:       file = %O, define = yes, start = %S,    size = $BC20 - __STACKSIZE__ - __RESERVED_MEMORY__ - %S;
    TRAILER:    file = %O,               start = $0000, size = $0006;
}
[Link] Memory areas

Nazywamy sobie obszary pamięci. Do nich przypiszemy segmenty w sekcji poniżej. start - początek nazwanego obszaru pamięci, size - rozmiar.

MAIN = 48160 - 2048 - 0 - 8192 = 37920 bajtów na kod i dane.

define - generuj etykiety asemblera dla obszaru pamięci

[Link] file
SEGMENTS {
    ZEROPAGE:  load = ZP,         type = zp;
    EXTZP:     load = ZP,         type = zp,                optional = yes;
    EXEHDR:    load = HEADER,     type = ro;
    SYSCHKHDR: load = SYSCHKHDR,  type = ro,                optional = yes;
    SYSCHK:    load = SYSCHKCHNK, type = rw,  define = yes, optional = yes;
    SYSCHKTRL: load = SYSCHKTRL,  type = ro,                optional = yes;
    MAINHDR:   load = MAINHDR,    type = ro;
    STARTUP:   load = MAIN,       type = ro,  define = yes;
    LOWBSS:    load = MAIN,       type = rw,                optional = yes;  # not zero initialized
    LOWCODE:   load = MAIN,       type = ro,  define = yes, optional = yes;
    ONCE:      load = MAIN,       type = ro,                optional = yes;
    CODE:      load = MAIN,       type = ro,  define = yes;
    RODATA:    load = MAIN,       type = ro;
    DATA:      load = MAIN,       type = rw;
    INIT:      load = MAIN,       type = rw,                optional = yes;
    BSS:       load = MAIN,       type = bss, define = yes;
    AUTOSTRT:  load = TRAILER,    type = ro;
}

Jakie segmenty trafiają kolejno do których obszarów pamięci. ZEROPAGE do obszaru ZP, itd.

[Link] Segments
FEATURES {
    CONDES: type    = constructor,
            label   = __CONSTRUCTOR_TABLE__,
            count   = __CONSTRUCTOR_COUNT__,
            segment = ONCE;
    CONDES: type    = destructor,
            label   = __DESTRUCTOR_TABLE__,
            count   = __DESTRUCTOR_COUNT__,
            segment = RODATA;
    CONDES: type    = interruptor,
            label   = __INTERRUPTOR_TABLE__,
            count   = __INTERRUPTOR_COUNT__,
            segment = RODATA,
            import  = __CALLIRQ__;
}
[Link] Features]

Pliki cfg od zera fajnie omawia Ben Eater:

[Youtube] A simple BIOS for my breadboard computer [Youtube] Running MSBASIC on my breadboard 6502 computer

guess.c

#include <atari.h>

void main(void){

}

Kompilujemy naszym build.bat w terminalu w naszym katalogu.

Plik guess.xex nic ciekawego nie robi.

Plik guess.map zawiera mapę pamięci naszego programu.

Zedytujmy guess.c:


#include <stdio.h>

void main (void)
{
  while(1)
    printf("Hello, world!");
}

Kompilujemy naszym build.bat a w Altirze wybieramy Debug -> Enable Debugger.

F8 - zatrzymuje/wznawia program. W zakładce Disassembly widzimy gdzie w kodzie ASM jesteśmy.

[Obrazek] Altirra debugger

W linii poleceń na dole(Altirra>) możemy wpisać .help żeby dostać listę komend debuggera. Komenda lm powinna wyświelić załadowane symbole, w tym nasze pliki guess.lab i guess.lbl. Sprawdźmy jaki adres w pliku guess.lbl ma funkcja _main.

al 00207B .L0005
al 0028D4 .S0001
al 00206F ._main

00206F - wpiszmy do pola tekstowego Disassembly:

[Obrazek] 00206F

Klikamy PPM Go to source:

[Obrazek] Go to source

Powinien się nam załadować nasz plik guess.c. Możemy to sprawdzić w menu Debug -> Source file list. Klikamy prawym w kodzie i wybieramy Toggle breakpoint.

[Obrazek] Toggle breakpoint

F8 - zatrzymujemy/wznawiamy kod. Czerwony kolor oznacza breakpoint, żółty że się w nim zatrzymaliśmy.

F10 - Step over

W oknie Console na dole widzimy rejestry A, X i Y 6502 dla zatrzymanego breakpointa:

Breakpoint 0 hit
(359: 83, 52) A=0E X=00 Y=02 S=F7 P=30 (      )  206F: A9 D4     _main   LDA #$D4
(360:304, 71) A=0E X=00 Y=02 S=F7 P=30 (      )  207B: 4C 6F 20          JMP _main    [$206F] = $A9

Usuńmy breakpoint - PPM na czerwonym podświetleniu i Toggle breakpoint. Możemy też je wyświetlić Altirra> bl i skasować bc 0:

Altirra> bl
Breakpoints:
  0     PC  206F (_main)       `hello.c:6`
Altirra> bc 0
Breakpoint 0 cleared.

Otwieramy zakładkę Memory 1(można ją przywołać Alt+7 lub Debug -> Window -> Memory -> Memory 1). W mapie pamięci Atari poszukajmy adresu z zegarem czasu rzeczywistego.

[Link] Mapa pamięci
18,19,20          $12,$13,$14            RTCLOK
Internal realtime clock. Location 20 increments every stage one
VBLANK interrupt (1/60 second = one jiffy) until it reaches 255
($FF); then location 19 is incremented by one and 20 is reset to
zero (every 4.27 seconds). When location 19 reaches 255, it and
20 are reset to zero and location 18 is incremented by one (every
18.2 minutes or 65536 TV frames).

Użyjmy komendy Altirra> wb - watch byte:

Altirra> wb 14
[Obrazek] wb 14

Pojawi nam się mały czarny prostokąt z aktualną wartością komórki pamięci. Aby wyświetlić watche użyj wl, aby usunąć wc 0. Pamiętaj o F8.

Zmieńmy kod:

unsigned char my_char;

void main (void)
{
    my_char = 1;
    while(1)
        ;
}

Skompilujmy, i obejrzyjmy guess.lbl

al 002074 .L0005
al 00206F ._main
al 002100 ._my_char

Po uruchomieniu xex, możemy w zakładce Memory 1 skoczyć pod adres $2100 i sprawdzić, że rzeczywiście jest tam 1. Zapiszmy sobie zrzut pamięci do pliku:

Altirra> .writemem mem.txt $2100 L4
Wrote 2100-2103 to mem.txt

Folder zapisu to nasz folder z plikiem xex, od adresu $2100, 4 bajty. Plik mem.txt możemy podejrzeć w notepad++ wtyczka HEX-Editor.

Debug gier

Załadujmy sobie jakąś grę do Altirry np: Crownland. W Altirze trzeba zwiększyć rozmiar RAMu: System -> Configure system -> Memory -> Memory type -> wystarczy 128k(130XE).

[Link] Crownland (v3,128).xex

Dajmy F8 i polecenie Altirra> .gtia

Altirra> .gtia
Player  0: color = 04, pos = 40, size=0, data = 00
...
Playfield colors: 00 | 94 96 92 2c
PRIOR:  21 (pri= 1, multicolor , normal)
VDELAY: 00
GRACTL: 03, player DMA, missile DMA
CONSOL: 08 set <-> 0f input, speaker
M0PF: PF0 PF1 PF3
M1PF: PF0 PF1 PF3
M2PF:
M3PF:
...			

Widzimy kolejno dane graczy PMG min. ich kolor i pozycję. Kolory pola gry w sztuce pięciu. PRIOR priorytety wyświetlania duchów i pola gry. GRACTL DMA. M0PF kolizje pocisków i graczy z polem gry i innymi graczami.

Dajmy Altirra> .antic

Altirra> .antic
DMACTL = 3d  : narrow missiles players 1-line dlist
CHACTL = 02  : invert
DLIST  = ff80
HSCROL = 0f
VSCROL = 00
PMBASE = f0
CHBASE = b8
NMIEN  = c0  : dli vbi
NMIST  = 9f  : dli
PENH/V = 0c 98

Skoczmy do CHBASE $b800 i zrzućmy czcionkę do pliku .writemem mem.fnt $B800 L1024 otwórzmy Mad-Studio i wybierzmy Character Set Editor -> otwórzmy nasz plik mem.fnt. Te dane powinny nabrać dla nas teraz więcej sensu.

Dajmy Altirra> .dumpdlist

Altirra> .dumpdlist
  FF80: x2   blank 8
  FF82:      blank.i 8
  FF83:      mode.i 4 @ FF40
  FF86:      mode.i 4 @ FF60
  FF89:      mode.i.h 4 @ BC02
  FF8C:      mode.i.h 4 @ BE02
  FF8F:      mode.i.h 4 @ C002
  FF92:      mode.i.h 4 @ C202
  ...
  FFC5:      mode.i.h 4 @ EC02
  FFC8:      mode.h 4 @ EE02
  FFCB:      waitvbl FF80

Widzimy kolejne linie trybowe wraz z ładowaniem pamięci co $200(512 DEC) bajtów.

Edit 2024.12.29 - korekta, mapa pamięci, linki.

Edit 2025.04.06 - HTML