Categorieën

Category Algemeen

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.

Terug van weggeweest

Het is inmiddels een paar jaar geleden dat ik hier het voorlopig laatste artikel ophing. Berry en ik hadden een passend 6510.nl-forum opgetuigd maar door drukte hebben we nooit de schakelaar omgezet.

Ondanks het uitblijven van vervolgartikelen wordt 6510.nl nog steeds goed bezocht. Dat is niet gek: de hele demoscene kent veel beweging en met name de Amigascene in Nederland ziet een groeiende populatie.

Ingewanden van een Amiga

Het is goed mogelijk dat je inmiddels een Amiga in het wild hebt gezien. Je hebt wat spellen gespeeld en demo’s bekeken. Misschien heb je jezelf een weg gebaand door het besturingssysteem Workbench. Laten we de Amiga open zagen om te kijken wat er in zit. Je wilt vast weten wat er technisch mogelijk is.

C64 versus Amiga: choose your warrior!

Commodore 64 of Commodore Amiga? Beiden waren bij de introductie hun tijd ver vooruit. Maar dat is tientallen jaren geleden en als je ze gaat vergelijken met de snelheid van moderne computers ben je snel klaar; de processor in je wasmachine is vermoedelijk sneller. Dat doen we dus niet. Een echte computer selecteer je op gevoel.

Heb je ooit twintig minuten lang een spel geladen enkel om de hele zondagmiddag naar de intromuziek te luisteren? Spaarde je samen met je vriendjes 59 gulden voor een tweede Suzo Arcade joystick? Besteedde je ooit twee dagen aan het overtypen van een programma uit een computermagazine? Gaat je hart sneller kloppen bij het zien van een TDK D60 zonder label? Fietste je uit school vlug naar huis om te kijken of er post was? Dan heb je de keus misschien al gemaakt.

Hexadecimaal

In het vorige artikel heb je je kunnen wentelen in het binaire talstelsel. Het is geen probleem als dit nieuwe concept nog wat ongemakkelijk voelt; het decimale stelsel heb je tenslotte ook niet binnen een uur geleerd.

Je zult die bits snel genoeg met andere ogen gaan bekijken; het worden getallen, programma’s, beeld en geluid. Kijk eens! We gooien er nog een talstelsel overheen!

Praten met 6510.nl

Spreek je bericht na de toon

We merken dat het drukker wordt in de gangen van 6510.nl. Daarom zijn we druk bezig met het bouwen van een communicatiemiddel waarmee je gemakkelijk kunt overleggen met 6510.nl en je mede-aspirant coders.

USRobotics HST
USRobotics FTW!

We verwachten voor oktober november eind van het jaar sint-juttemis klaar te zijn met de implementatie. Tot die tijd kun je contact leggen via een aantal alternatieven. Bekijk het colofon.

Binair, bits en bytes

Geluid is (altijd) analoog en computers verwerken informatie digitaal. Wat analoog betekent, leggen audiofielen en degenen die ooit een langspeelplaat hebben vastgehouden je met liefde uit. Wij nemen digitaal voor onze rekening.

Als je in de Van Dale de woorden digitaal en analoog opzoekt, vind je een tegenstelling:

Zo werkt een computer


Wanneer je een computer wilt programmeren, is het natuurlijk praktisch dat je in grote lijnen weet hoe een computer werkt. Dat leggen we in klare taal uit.

Rekenaar

Het van oorsprong Engelse woord ‘computer’ was de naam voor iemand die — al dan niet met mechanische hulpmiddelen — gecompliceerde berekeningen uitvoerde. Datzelfde geldt voor het Duitse woord ‘Rechner’.

De wegwijzer is geïnstalleerd

De wegwijzer die je in de toekomst door de diepste krochten van 6510.nl zal leiden, staat klaar. Omdat er regelmatig hoofdstukken bijkomen, zal de wegwijzer vaak worden bijgewerkt. We beginnen met een drietal algemene artikelen waarmee je je kunt voorbereiden op het maken van de eerste keuze: Commodore 64 of Amiga. Werp een blik op de wegwijzer!

Over de demoscene

Nieuw leesvoer

De volgende pagina van de introductiereeks is gereed: een beschrijving van de demoscene. We liggen op schema.