Questa è la prima parte di una serie di tre articoli che descrivono tecniche per scrivere codice portabile e ottimizzato in ANSI C per tutti i sistemi 8-bit vintage, cioè computer, console, handheld, calcolatrici scientifiche e microcontrollori dalla fine degli anni 70 fino a metà degli anni 90.
L’articolo completo è disponibile on-line su https://github.com/Fabrizio-Caruso/8bitC/blob/master/8bitC.md
Il contenuto dell’articolo originale sarà trattato in tre parti:
- Introduzione e Scrittura di codice C portabile per 8-bit
- Tecniche per ottimizzare il codice C per 8-bit
- Tecniche avanzate per ottimizzare il codice C per 8-bit
In particolare ci occuperemo dei sistemi basati sulle seguenti architetture (e architetture derivate e retrocompatibili):
- Intel 8080 (*)
- MOS 6502
- Motorola 6809
- Zilog Z80 (*)
(*) Lo Zilog Z80 è una estensione dell’Intel 8080, quindi un binario Intel 8080 sarà utilizzabile anche su un sistema con Z80 ma il viceversa non è vero.
Buona parte di queste tecniche sono valide su altre architetture 8-bit come quella del COSMAC 1802 e quella del microcontrollore Intel 8051.
Lo scopo di questa serie di articoli è duplice:
- descrivere tecniche generiche per scrivere codice portabile, cioè valido e compilabile su tutti i sistemi 8-bit indipendentemente dal fatto che un sistema sia supportato esplicitamente da un compilatore o meno
- descrivere tecniche generali per ottimizzare il codice C su tutti i sistemi 8-bit
Questa serie di articoli non è un manuale introduttivo al linguaggio C e richiede
- conoscenza del linguaggio C;
- conoscenza della programmazione strutturata e a oggetti;
- conoscenza dell’uso di compilatori e linker.
Inoltre non ci occuperemo in profondità di alcuni argomenti avanzati quali:
- alcuni ambiti specifici della programmazione come grafica, suono, input/output, etc.
- l’interazione tra C e Assembly.
Questi argomenti avanzati sono molto importanti e meriterebbero degli articoli separati a loro dedicati.
Introduciamo alcuni termini che saranno ricorrenti in questa serie di articoli:
- Un sistema è un qualunque tipo di macchina dotata di processore come computer, console, handheld, calcolatrici, sistemi embedded, etc.
- Un target di un compilatore è un sistema supportato dal compilatore, cioè un sistema per il quale il compilatore mette a disposizione supporto specifico come librerie e la generazione di un binario in formato specifico.
- Un architettura è un tipo di processore (Intel 8080, 6502, Zilog Z80, Motorola 6809, etc.) . Un target appartiene quindi ad una sola architettura data dal suo processore (con rare eccezioni come il Commodore 128 che ha sia un processore derivato dal 6502 e uno Zilog Z80).
Per produrre i nostri binari 8-bit consigliamo l’uso di cross compilatori multi-target (cioè compilatori eseguiti su PC che producono binari per diversi target). Non consigliamo l’uso di compilatori nativi perché sarebbero molto scomodi (anche se usati all’interno di un emulatore accelerato al massimo) e non potrebbero mai produrre codice ottimizzato perché l’ottimizzatore sarebbe limitato dalla risorse della macchina 8-bit.
Faremo particolare riferimento ai seguenti cross compilatori multi-target:
Architettura | Compilatore/Dev-Kit | Pagina |
---|---|---|
Intel 8080 | ACK | https://github.com/davidgiven/ack |
MOS 6502 | CC65 | https://github.com/cc65/cc65 |
Motorola 6809 | CMOC | https://perso.b2b2c.ca/~sarrazip/dev/cmoc.html |
Zilog 80 | SCCZ80/ZSDCC (Z88DK) | https://github.com/z88dk/z88dk |
Inoltre esistono altri cross compilatori C multi-target che non tratteremo qui ma per i quali buona parte delle stesse tecniche generiche rimangono valide:
- LCC1802 (https://sites.google.com/site/lcc1802/) per il COSMAC 1802;
- SDCC (http://sdcc.sourceforge.net/) per svariate architetture di microprocessori come lo Z80 e di microcontrollori come l’Intel 8051;
- GCC-6809 (https://github.com/bcd/gcc) per 6809 (adattamento di GCC);
- GCC-6502 (https://github.com/itszor/gcc-6502-bits) per 6502 (adattamento di GCC);
- SmallC-85 (https://github.com/ncb85/SmallC-85) per Intel 8080/8085 ;
- devkitSMS (https://github.com/sverx/devkitSMS) per le console Sega basate su Z80 come Sega Master System, Sega Game Gear e Sega SG-1000.
Si noti come il dev-kit Z88DK disponga di due compilatori:
- l’affidabile SCCZ80 che è anche molto veloce nelle compilazione,
- lo sperimentale ZSDCC (versione ottimizzata per Z80 di SDCC sopracitato) che però può produrre codice più efficiente e compatto di SCCZ80 a costo di compilazione più lenta e rischio di introdurre bug.
Quasi tutti i compilatori che stiamo prendendo in considerazione generano codice per una sola architettura (sono mono-architettura) pur essendo multi-target. ACK è una eccezione essendo anche multi-architettura (con supporto per Intel 8080, Intel 8088/8086, I386, 68K, MIPS, PDP11, etc.).
Questo articolo non è né una introduzione né un manuale d’uso di questi compilatori e non tratterà:
- l’istallazione dei compilatori;
- elementi specifici per l’uso di base di un compilatore
Per i dettagli sull’istallazione e l’uso di base dei compilatori in questione, facciamo riferimento ai manuali e alle pagine web dei relativi compilatori.
Un articolo sul Cross Development (di Giorgio Balestrieri) sul numero 2 anno 1 di RetroMagazine ha presentato CC65 assieme ad altri tools di sviluppo (http://www.retromagazine.net/download/RetroMagazine_02.pdf). Una introduzione all’uso di CC65 (di Fabrizio Lodi) è disponibile su https://www.retroacademy.it/author/fabrizio-lodi/ .
Sottoinsieme di ANSI C
In questa serie di articoli per ANSI C intendiamo sostanzialmente un grosso sotto-insieme dello standard C89 in cui i float
e i long long
sono opzionali ma i puntatori a funzioni e puntatori a struct
sono presenti.
Non stiamo considerando versioni precedenti del C come per esempio C in sintassi K&R.
Per quale motivo dovremmo usare il C per programmare dei sistemi 8-bit?
Tradizionalmente queste macchine vengono programmate in Assembly o in BASIC interpretato o in un mix dei due.
Data la limitatezza delle risorse è spesso necessario ricorrere all’Assembly. Il BASIC è invece comodo per la sua semplicità e perché spesso un interprete è già presente sulla macchina.
Volendo limitare il confronto a questi soli tre linguaggi il seguente schema ci dà una idea delle ragione per l’uso del C.
facilità | portabilità | efficienza | |
---|---|---|---|
BASIC | SI | parziale | poca |
Assembly | NO | NO | ottima |
C | SI | SI | buona |
Quindi la ragione principale per l’uso del C è la sua portabilità assoluta. In particolare se si usa un sottoinsieme dell’ANSI C che è uno standard. In particolare l’ANSI C ci pemette di:
- fare porting semplificato tra una architettura all’altra,
- scrivere codice “universale”, cioè valido per diversi target senza alcuna modifica.
Qualcuno si spinge a dichiarare che il C sia una sorta di Assembly universale. Questa è una affermazione un po’ troppo ottimistica perché del C scritto molto bene non batterà mai dell’Assembly scritto bene. Ciò nonostante il C è probabilmente il linguaggio più vicino all’Assembly tra i linguaggi che permettono anche la programmazione ad alto livello.
Una ragione non-razionale ma “sentimentale” per non usare il C sarebbe data dal fatto che il C è sicuramente meno vintage del BASIC e Assembly perché non era un linguaggio comune sugli home computer degli anni 80 (ma lo era sui computer professionali 8-bit come sulle macchine che usavano il sistema operativo CP/M).
Credo che la programmazione in C abbia però il grosso vantaggio di poterci fare programmare l’hardware di quasi tutti i sistemi 8-bit.
Scrivere codice facilmente portabile o addirittura direttamente compilabile per diverse piattaforme è possibile in C attraverso varie strategie:
- Scrivere codice agnostico dell’hardware e che quindi usi interfacce astratte (cioè delle API indipendenti dall’hardware).
- Usare implementazioni diverse per le interfacce comuni da selezionare al momento della compilazione (per esempio attraverso direttive al precompilatore o fornendo file diversi al momento del linking).
Questo diventa banale se il nostro dev-kit multi-target mette a disposizione una libreria multi-target o se ci si limita a usare le librerie standard del C (stdio, stdlib, etc.). Se si è in queste condizioni, allora basterà ricompilare il codice per ogni target e la libreria multi-target del del dev-kit farà la “magia” per noi.
Solo CC65 e Z88DK propongono interfacce multi-target per input e output oltre le librerie C standard:
Dev-Kit | Architettura | librerie multi-target |
---|---|---|
Z88DK | Zilog Z80 | standard C lib, CONIO(testuale), vt52 (testuale), vt100 (testuale), sprite software, UDG, bitmap, joystick Z88DK |
CC65 | MOS 6502 | standard C lib, CONIO(testuale), TGI (bitmap), joystick CC65 |
CMOC | Motorola 6809 | standard C lib |
ACK | Intel 8080 | standard C lib |
CC65 fornisce la libreria CONIO per la visualizzazione di testo e la libreria grafica multi-target TGI per grafica bit-map tra alcuni dei suoi target e API proprie per leggere lo stato dei joystick.
Anche Z88DK fornisce la libreria CONIO e API proprie per leggere lo stato dei joystick. Inoltre fornisce molti strumenti per la grafica multi-target tra cui API per grafica bitmap, per gli sprite software (https://github.com/z88dk/z88dk/wiki/monographics) e per caratteri ridefiniti per buona parte dei suoi 80 target.
Esempio: Il gioco multi-piattaforma H-Tron (di RobertK) è un esempio (https://sourceforge.net/projects/h-tron/) in cui si usano le API previste dal dev-kit Z88DK per creare un gioco su molti sistemi basati sull’architettura Z80.
In tutti i casi in cui le librerie a disposizione non facciano al caso nostro bisognerà crearsi una libreria multi-target sfruttando eventualmente tutto quello che lo specifico dev-kit mette a disposizione.
Per potere avere codice portabile su target e eventualmente anche su architetture diverse bisogna usare eventuali librerie comuni a dev-kit diversi o scriversi una libreria multi-architettura e quindi anche compilabile da compilatori di dev-kit diversi.
Se per l’input/output usassimo esclusivamente le librerie C standard (come stdio.h
) potremmo avere codice compilabile con ACK, CMOC, CC65 e Z88DK ma saremmo limitati a input e output testaule limitato a comandi come printf
e scanf
senza controllo preciso della posizione del testo.
Se per l’input/output, oltre alle librerie standard C, usassimo solo la libreria CONIO (che nasce per input/output testuale su MS-DOS) avremmo codice compilabile con CC65 e Z88DK ma avremmo input e output testuale limitato a comandi come cprintf
, cgetc
, gotoxy
che consentono il controllo della posizione del testo. Per maggiori dettagli facciamo riferimento a https://www.cc65.org/doc/funcref-14.html .
In tutti gli altri casi se vogliamo scrivere codice portabile su architetture diverse bisognerà crearsi una libreria multi-target e multi-architettura che quindi non avrà alcuna dipendenza da un dev-kit.
Scrivere una libreria multi-target o addirittura multi-architettura significa creare un hardware abstraction layer il cui scopo e di permettere la separazione tra:
- il codice che non dipende dall’hardware (per esempio la logica di un gioco) che usa l’interfaccia della libreria
- dal codice della libreria la cui implementazione dipende dall’hardware (per esempio le funzioni per l’input, output in un gioco) ma la cui interfaccia non dipende dall’hardware.
Questo pattern è assai comune nella programmazione moderna. Il C fornisce una serie di strumenti utili per implementare questo pattern in maniera che che si possano supportare hardware diversi da selezione al momento della compilazione. In particolare il C prevede un potente precompilatore con comandi come:
#define
per definire una macro,#if
…defined(...)
…#elif
…#else
…#endif
per selezione porzioni di codice che dipendono dal valore o esistenza di una macro.
Inoltre tutti i compilatori prevedono l’opzione -D
per passare una variabile al precompilatore con eventuale valore. Alcuni compilatori come CC65 implicitamente definiscono una variabile col nome del target (per esempio VIC20) per il quale si intende compilare.
Nel codice avremo qualcosa come:
...
#if defined(__PV1000__)
#define XSize 28
#elif defined(__OSIC1P__) || defined(__G800__) || defined(__RX78__)
#define XSize 24
#elif defined(__VIC20__)
#define XSize 22
...
#endif
...
e al momento di compilare per il Vic 20 il precompilatore selezionerà per noi la definizione di XSize
specifica del Vic 20.
Questo permette al precompilatore non solo di selezionare le parti di codice specifiche per una macchina, ma anche di selezionare opzioni specifiche per configurazione delle macchina (memoria aggiuntiva, modo grafico, debug, etc.).
Il progetto Cross-Chase (https://github.com/Fabrizio-Caruso/CROSS-CHASE) propone il codice di un gioco compilabile su più di 100 sistemi 8-bit diversi con circa 200 configurazioni diverse usando sempre lo stesso codice del gioco grazie alla libreria universale CrossLib.
Se guardiamo il codice vediamo come sia stato separato in:
- codice del gioco (directory
src/chase
) totalmente indipendente dall’hardware, - codice della libreria CrossLib (directory
src/cross_lib
) che implementa i dettagli di ogni hardware possibile.
I nostri dev-kit supportano una lista di target per ogni architettura attraverso la presenza di librerie specifiche per l’hardware. E’ comunque possibile sfruttare un dato dev-kit per altri sistemi con la stessa architettura ma saremo dovremo implementare tutta la parte di codice specifica del target:
- codice necessario per gestire l’input/output (grafica, tastiera, joystick, suoni, etc.),
- codice necessario per inizializzare il sistema.
Per fare ciò potremo in molti casi usare le routine già presenti nella ROM (nel terzo articolo di questa serie diamo un semplice esempio che è anche su https://github.com/Fabrizio-Caruso/8bitC/blob/master/8bitC.md).
Inoltre dovremo procurarci o scrivere un convertitore del binario in un formato accettabile per il nuovo sistema.
Per esempio CC65 non supporta né il BBC Micro né l’Atari 7800 e CMOC non supporta l’Olivetti Prodest PC128 ma è comunque possibile usare (o estendere) questi dev-kit per produrre binari per questi target:
- Cross Chase (https://github.com/Fabrizio-Caruso/CROSS-CHASE) supporta (in principio) qualunque architettura anche non supportata direttamente dai compilatori come per esempio l’Olivetti Prodest PC128.
- Il gioco Robotsfindskitten è stato portato per l’Atari 7800 usando CC65 (https://sourceforge.net/projects/rfk7800/files/rfk7800/).
- BBC è stato aggiunto come target sperimentale su CC65 (https://github.com/dominicbeesley/cc65).
Per potere compilare per un sistema non supportato dobbiamo usare alcune opzioni che servono ad eliminare le dipendenze da un target specifico. Qui diamo una lista di alcune opzioni utili a questo scopo. Per maggiori dettagli facciamo riferimento ai rispettivi manuali.
Architettura | Dev-Kit | Opzione |
---|---|---|
Intel 8080 | ACK | (*) |
MOS 6502 | CC65 | +none |
Motorola 6809 | CMOC | --nodefaultlibs |
Zilog 80 | SCCZ80/ZSDCC (Z88DK) | +test (generico), +embedded (generico con nuove librerie), +cpm (solo per generico CP/M) |
(*) ACK prevede solo il target CP/M-80 per l’architettura Intel 8080 ma è possibile almeno in principio usare ACK per produrre binari generici per Intel 8080 ma non è semplice in quanto ACK usa una sequenze di comandi per produrre il Intel 8080 partendo dal C e passando da vari stai intermedi compreso un byte-code “EM”:
ccp.ansi
: precompilatoreem_cemcom.ansi
: compila C preprocompilato producendo bytecodeem_opt
: ottimizza il bytecodecpm/ncg
: genera Assembly da bytecodecpm/as
: genera codice Intel 80 da Assemblyem_led
: linker
Un’alternativa a ACK per generare binari Intel 8080 generici è data dal già citato compilatore SmallC-85.