CC65: Cross compilatie voor de C64

CC65 is een cross development omgeving wat het mogelijk maakt om C programma’s te schrijven voor systemen die een 6502, 6507, 6510 of ricoh 2a03 gebruiken. In dit artikel bekijken we hoe dit pakket werkt en hoe we een simpel snake-spel kunnen maken voor de Commodore 64 gebruikmakend van een joystick.

Programmeren op de C64 is leuk, maar desalniettemin erg bewerkelijk en tijdrovend wanneer we alles in 6510 assembly moeten maken. Daarom zijn er diverse pakketten gemaakt om in een andere programmeertaal te kunnen werken. De C64 biedt zelf al een mogelijkheid: cbm-basic. Hoewel dit erg toegankelijk en redelijk eenvoudig te gebruiken is, is het niet geschikt voor serieuzer werk: bijvoorbeeld voor het maken van demo’s.

Toen de C64 nog verkocht werd, waren er diverse pakketten om in talen als Pascal en C te kunnen programmeren, maar wie wil nu Pascal gebruiken? Dit is ook alleen mogelijk op de C64 zelf, dus dan zijn we gebonden aan 40 kolommen per regel… niet heel erg handig… De Aztec C compiler maakte het mogelijk in C te programmeren op de C64, maar er is alleen een klein probleem… laten we eens kijken naar het toetsenbord van de c64.

Commodore 64 toetsenbord zonder typografische tekens zoals krulaccolades

Waar zijn onze krulaccolades { en }? En het liggend streepje? Juist… die zijn er niet! Maar niet getreurd… C ondersteunt trigraphs voor toetsenbordindelingen die het C-alfabet niet ondersteunen:

TrigraphResultaat
??=#
??([
??)]
??<{
??>}
??/\
??'^
??!`
??-~

Zoals u waarschijnlijk al aanvoelt, zijn trigraphs niet ideaal... veel C-programmeurs weten niet eens dat trigraphs bestaan en zullen bij het zien van deze code:

??=include <stdio.h>

int main(void)
??<
    puts("true ??!??! false == %d??/n", 1 ??!??! 0);
??>

waarschijnlijk denken dat de broncode corrupt is geraakt. Laten we kijken hoe we cc65 kunnen opzetten. Wanneer u het gemaakte programma ook wil testen, adviseren we om VICE te gebruiken (of natuurlijk een echte C64).

Voorbereiding

Het is aanbevolen dat u over redelijke kennis beschikt van de C-programmeertaal. De werking van diverse codefragmenten wordt wel uitgelegd, maar zonder enige programmeerachtergrond is dit erg lastig te volgen. Het is niet noodzakelijk om eerder op de C64 geprogrammeerd te hebben, alhoewel dit wel helpt met het begrijpen van de codefragmenten.

CC65 opzetten

Als u Windows gebruikt, kunt u het pakket hier downloaden. Als u Linux gebruikt dan is het mogelijk dat uw packagemanager cc65 al aanbiedt. Bijvoorbeeld op Ubuntu:

sudo apt install cc65

Zie Getting Started van de officiële website voor de recentste informatie.

De cross compiler bestaat uit verschillende tools. Laten we kijken hoe we deze kunnen gebruiken.

Compilatieprocess

In tegenstelling tot c-compilers zoals gcc en clang moeten alle stappen van het compilatieprocess stap voor stap met een ander cc65 tool uitgevoerd worden. Dat wil zeggen dat de C-code eerst naar assembly wordt omgezet met cc65, vervolgens naar een object file met as65 en als laatste naar een uitvoerbaar bestand met ld65.

CC65 ondersteunt slechts de oudste C-standaard C89 en deels C99 (de C standaard van 1999). Het is bijvoorbeeld niet mogelijk om in de initializer van een for-loop een variabele te declareren zoals in c99:

#include <stdio.h>

int main(void) {
    for (int i = 0; i < 10; ++i)
        printf("i=%d\n", i);
    return 0;
}

In C89 kan i niet in een block scope aangemaakt worden en moet het op deze manier:

#include <stdio.h>

int main(void) {
    int i;
    for (i = 0; i < 10; ++i)
        printf("i=%d\n", i);
    return 0;
}

Zie verschillen met de ISO-standaard voor meer details.

Laten we als eerste kijken of we een hello world-programma kunnen maken. Plak deze code in hallo.c:

#include <stdio.h>

int main(void)
{
    puts("hallo, c64!");
    return 0;
}

Bouw en run het programma als volgt:

cc65 -t c64 hallo.c
ca65 hallo.s
ld65 -o hallo.prg -t c64 hallo.o c64.lib
x64 hallo.prg

De laatste regel start de emulator op. Als u deze niet geïnstalleerd heeft of het commando anders heet, dan zal deze regel een foutmelding geven. In dat geval dient u te achterhalen of u de emulator correct geïnstalleerd heeft en hoe u de emulator met een prg kan starten.

Snake

Laten we een simpel spel maken waar we een slang besturen die zoveel mogelijk voedsel tracht te nuttigen opdat het zo lang mogelijk wordt. Wanneer de slang in zichzelf bijt, is het spel afgelopen. Hiervoor gebruiken we de joystick als besturing omdat dit verreweg het makkelijkst aan de praat te krijgen is. Als eerste gaan we een programma maken wat de joystick uitleest om het daarna stapsgewijs tot een spel uit te bouwen.

Joystick

De C64 heeft twee joystickpoorten. De meeste spellen gebruikten joy2 voor speler één, maar waarom eigenlijk? Waarom gebruikten ze niet joy1? Stop de joystick maar eens in poort 1 en beweeg de joystick... u zult zien dat er rare tekens op het scherm zullen verschijnen. Dit komt omdat de C64 denkt dat het toetsenbord gebruikt wordt, omdat deze poort gedeeld wordt met het toetsenbord. Nu is er een manier om dit probleem te verhelpen, maar de meeste programmeurs vonden dit teveel moeite.

Hoe gebruiken we nu precies de joystickpoort? Met behulp van dit overzicht zien we bij CIA#1 dat we Port A (adres $dc00) nodighebben. Maar wacht even... hoe weten we zeker dat we zomaar van dit register kunnen lezen? Als we iets verder kijken, zien we dat er ook een Port A Data Direction Register is. Aangezien we alleen willen lezen van dit register is het niet noodzakelijk om dit datarichtingsregister in te stellen, maar het is wel verstandig om dit te doen. Wanneer we het toetsenbord willen gebruiken, is dit zelfs noodzakelijk!

Indien u in CBM-BASIC heeft geprogrammeerd, weet u dat we geheugen kunnen lezen en schrijven op de C64 met PEEKs en POKEs. Deze kunnen we ook in C namaken, en wel met deze macro's:

#define POKE(addr, val) (*((unsigned char*)addr) = (val))
#define PEEK(addr) (*((unsigned char*)addr))

Vervolgens kunnen we bovengenoemde CIA registers ook definiëren:

#define CIA1_PORT1 0xDC00
#define CIA1_DDR1 0xDC02

Het hele programma kan er bijvoorbeeld zo uitzien:

#include <stdio.h>

// Controller Interface Adapter registers
// Zie: http://sta.c64.org/cbm64mem.html
#define CIA1_PORT1 0xDC00
#define CIA1_DDR1 0xDC02

#define POKE(addr, val) (*((unsigned char*)addr) = (val))
#define PEEK(addr) (*((unsigned char*)addr))

int main(void)
{
    // Stel joy2 zo in dat we alleen kunnen lezen.
    POKE(CIA1_DDR1, 0x0);

    // Dump joy2 status in hexadecimaal.
    while (1) {
        printf("%x", PEEK(CIA1_PORT1));
    }

    return 0;
}

Als we dit programma vervolgens compileren naar een prg, kunnen we het uitvoeren op de C64.

Joystick port 2 continue statusweergave

Hmmmmm, het doet wel wat we willen, maar de presentatie is niet heel duidelijk. Laten we het wat fraaier maken.

CC65 heeft een grote bibliotheek, ofwel Application Programming Interface (hierna: API), die allerlei low-level taken voor ons uit handen neemt. Zo is er bijvoorbeeld een API wat de cursor kan bewegen, waardoor we de toestand van joy2 op een vaste plek kunnen tonen. Deze heet conio.h en laat bij menig MS-DOS programmeur ongetwijfeld een belletje rinkelen. We vervangen stdio.h met conio.h en vervangen de printf regel met:

gotoxy(0, 0); cprintf("joy2=%x", PEEK(CIA1_PORT1));

Het is ook handig als we alle tekst die op al op het scherm staat voordat ons programma begint verwijderen. Dit doen we door voor de POKE een clrscr(); te plaatsen.

Joystick port 2 status op vaste plek in linkerbovenhoek van het scherm

Kijk, dat lijkt er al meer op. Nu kunnen we de kop van de slang maken en zorgen dat deze met joy2 bestuurd kan worden. Hiervoor moeten we aardig wat code gaan schrijven. Zet de volgende code tussen #include <conio.h> en #define CIA1_PORT1 0xDC00:

#include <stdlib.h>
#include <time.h>

#define ROWS 25
#define COLS 40

Plak daaronder de volgende globale variabelen:

unsigned score = 0;

unsigned char joy2, richting, richting_nieuw;
unsigned char kop_x = 10, kop_y = 10;
unsigned char staart_x, staart_y;
unsigned char food_x, food_y;
unsigned char timer = 0, stappen = 6;

Deze variabelen gaan ons helpen de toestand van het spel bij te houden. Maak daarna deze functie:

void next_food(void)
{
    food_x = rand() % COLS;
    food_y = 2 + (rand() % (ROWS - 2));
}

Deze functie zorgt ervoor dat er voedsel op een willekeurige plek geplaatst wordt. De bovenste twee regels van het scherm worden overgeslagen omdat hier de score komt te staan.

Vervang de main functie met het volgende:

int main(void)
{
    srand(time(NULL));

    cursor(0);
    clrscr();

    // Plaats objecten
    next_food();

    // Maak balk tussen scorebord en speelveld
    gotoxy(0, 1); chline(COLS);

    // Dump joy2 status in hexadecimaal.
    while (1) {
        joy2 = PEEK(CIA1_PORT1);

        gotoxy(0, 0); cprintf("Score %d", score);
        cputcxy(staart_x, staart_y, ' ');
        cputcxy(kop_x, kop_y, '@');
        cputcxy(food_x, food_y, '#');
    }

    return 0;
}

Zo kan het er bijvoorbeeld uitzien:

Leeg veld met slang en voedsel met een puntenteller in de linkerbovenhoek

Dit is al aardig, maar we kunnen nu nog niets besturen! Voeg daarom deze twee regels toe na while (1) {:

staart_x = kop_x;
staart_y = kop_y;

Zet vervolgens onder joy2 = PEEK(CIA1_PORT1);:

// Bepaal de richting waarin joy2 wijst
// 0 = midden, 1 = rechts, 2 = omhoog, 3 = links, 4 = beneden
if (!(joy2 & 0x01))
    richting = 2;
else if (!(joy2 & 0x02))
    richting = 4;
else if (!(joy2 & 0x04))
    richting = 3;
else if (!(joy2 & 0x08))
    richting = 1;

// Beweeg de kop als de timer verstreken is
if (!timer) {
    switch (richting) {
    case 1: if (kop_x == COLS - 1) kop_x = 0; else ++kop_x; break;
    case 2: if (kop_y == 2) kop_y = ROWS - 1; else --kop_y; break;
    case 3: if (kop_x == 0) kop_x = COLS - 1; else --kop_x; break;
    case 4: if (kop_y == ROWS - 1) kop_y = 2; else ++kop_y; break;
    }

    timer = stappen;
} else {
    --timer;
}

// Kijk of we een stuk voedsel gegeten hebben
if (kop_x == food_x && kop_y == food_y) {
    score += 50;
    next_food();
}

Als we het programma nu uitvoeren, kunnen we met de joystick de kop van de slang bewegen en voedsel eten. Wanneer de slang het speelveld verlaat, komt het aan de andere kant weer terug.

Het maken en bewegen van de staart van de slang laten we over als een oefening voor de lezer ;-) Voor een idee hoe het eruit kan zien, kunt u de volledige broncode downloaden en compileren.

Hier is nog een paar ideeën om het spel beter te maken:

  • Obstakels toevoegen die de slang moet mijden
  • Voedsel levert steeds minder punten op als het later opgepakt wordt
  • Teken de segmenten van de slang met verschillende tekens
  • Collision detection is nu erg inefficiënt, omdat het telkens elke cel moet testen. Is hier een slimmere manier voor?

Tips voor cc65

Nu u een beetje bekend met cc65, hebben we nog een aantal tips die u wellicht kunnen helpen met programmeren met cc65.

Algemeen

  • Variabelen die u veel gebruikt kunnen beter als globale variabelen in een bestand gedeclareerd worden om zo de overhead bij het doorgeven aan functies te minimaliseren.
  • Code die veel berekeningen moet doen kunnen het best, indien mogelijk, vantevoren door een extern programma uitgerekend worden dan door ze steeds opnieuw te berekenen. Een voorbeeld is het gebruiken van sinustabellen voor het bewegen van movable objects (ofwel sprites).

Demoprogrammeren

Voor serieus demoprogrammeren is het ook mogelijk cc65 te gebruiken, alhoewel de gegenereerde code veel extra ondersteuning biedt wat de performance niet ten goede komt. In dat geval is het wellicht verstandig om het idee eerst in C uit te werken om vervolgens de complexe en langzame operaties te optimaliseren door ze weer om te zetten naar 6510 assembly.

Conclusie

We hebben geleerd hoe we cc65 opzetten en kunnen gebruiken. CC65 biedt veel mogelijkheden en maakt het programmeren voor de C64 een stuk eenvoudiger, maar hierbij moet rekening gehouden worden met de beperkte ondersteuning van C99, de C standaard van 1999. Desalniettemin hebben we een eenvoudig spel gemaakt met een paar regels C code.

%d bloggers liken dit: