Comunicazioni Interfacciamento

Reverse engineering dei segnali della porta tape del Commodore 64 con Arduino

Chi si è cimentato almeno una volta nell’ardua impresa di caricare un gioco sul suo Commodore 64 da una cassetta alzi la mano!

Ore e ore a cercare di regolare il maledetto “azimuth”, l’allineamento tra nastro e testina, girando l’altrettanto maledetta vitina a destra e sinistra fino a quando finalmente qualcosa appariva sullo schermo.

Ebbene sì, io sono sicuramente uno di quelli e in un attimo di improvvisa nostalgia per quei tempi ho rispolverato il vecchio amico di tante avventure.

Dopo un po’ di esperimenti però ho iniziato a chiedermi se non ci fosse un modo più affidabile per salvare i preziosissimi giochi contenuti nelle cassette e non dover impazzire ogni volta con i capricci dei sistemi “analogici”.

Google come sempre è nostro amico e non c’è voluto molto per capire che molti prima di me hanno avuto la mia stessa illuminazione. Esiste infatti un dispositivo, peraltro relativamente economico, chiamato “tapuino” che fa esattamente questo! Permette di trasformare i nastri in files di tipo TAP e di rileggerli poi per generare segnali appositi così da far credere al Commodore 64 di parlare con un datassette. Insomma, una sorta di emulatore di datassette.

Preso, provato, perfetto! Tutto secondo programma, ma… come funziona il tutto? Ecco, questo è il punto, ora che ho scoperto che non solo si può ma esiste anche un dispositivo bello e pronto devo capire cosa ci sta dietro!

Online si trova qualcosa ma spesso le informazioni sono parziali e poco comprensibili quindi proviamo a rimettere in fila tutti gli indizi e capire come funziona esattamente il salvataggio di un file su datassette.

Il primo passo: la porta “Cassette”

Per primissima cosa dobbiamo capire che tipo di segnali sono presenti sulla porta del registratore della console.

Il connettore presente sul retro del C64 è di tipo “Edge” a sei contatti maschio (in pratica è costituito dalla scheda madre stessa “tagliata” opportunamente e dotata di apposite piste). Il pin 1 si trova sul lato superiore della scheda al margine destro del connettore come illustrato in Figura 1. Man mano che si procede verso sinistra si incontrano i pin successivi fino al 6. Nella parte inferiore della scheda, partendo dalla medesima estremità, si trovano i pin da A a F.

Figura 1: Connettore porta tape, lato scheda madre


Per collegarvicisi è necessario il corrispettivo connettore femmina con passo pari a 3.96mm a cui saldare un apposto cavo a 6 poli:

Figura 2: Connettore tipo “Edge” passo 3.96mm – 6+6 pin


In alternativa, se si dispone del cavo di un vecchio registratore, come nel mio caso, si può usare direttamente quello.

Figura 3: Cavo recuperato da un vecchio datassette

I sei segnali disponibili sono:

A1 - GND    - Massa
B2 - +5V    - Alimentazione
C3 - MOTOR  - Alimentazione motore
D4 - READ   - Dati in ingresso al C64 dal registratore
E5 - WRITE  - Dati in uscita dal C64 al registratore
F6 - SENSE  - Segnale di pressione dei tasti del registratore

Tra i pin 1 e 2 c’è una tacca di allineamento che impedisce di collegare il connettore al contrario. Nel caso del mio cavo recuperato i colori dei singoli conduttori sono, partendo dall’1: nero, verde, rosso, bianco, marrone e blu ma immagino non ci sia uno standard, quindi ogni produttore potrebbe avere colori differenti.


Il segnale SENSE

Visto che non sappiamo ancora nulla di come i dati vengono trasferiti da e per il registratore a cassette è giunto il momento di fare una rapida ispezione.

L’idea è collegare un oscilloscopio e andare a vedere cosa viene prodotto dal Commodore 64 quando si procede al salvataggio di un semplice programma scritto in BASIC. Il programmino di test sarà il seguente:

10 PRINT "PROVA"


Per sempificare il collegamento dell’oscilloscopio, saldo al termine del cavo del registraore una serie di pin. Sarà anche utilissimo successivamente dato che, come avrete intuito, ho inenzione di collegare il tutto ad un Arduino e mi servirò di una breadbord e degli appositi ponticelli per realizzare tutte le connessioni.

Figura 4. Connettore per breadboard


Ora siamo pronti per catturare il segnale: per avviare il salvataggio è necessario digitare il comando seguente:

 SAVE "TEST",1


Il C64 risponde con un laconico “PRESS RECORD & PLAY ON TAPE” visualizzato a schermo sulla riga successiva a quella sulla quale abbiamo impartito il comando.

Figura 5: Quando si salva il C64 attende la pressione dei tasti


E’ qui che entra in gioco il segnale “SENSE” presente sul pin F6. Solitamente questo pin viene lasciato “flottante” cioè non collegato a nulla e la circuitistica interna, attraverso una resistenza di pull-up da 3.3 KOhm che lo collega ai +5 V, fa in modo che il sistema lo veda ad uno stato logico 1.

Quando viene premuto un tasto sul registratore, questo fa in modo di collegare il pin F6 a massa, portando lo stato logico visto dal sistema a 0. In questo modo la routine di salvataggio capisce che l’utente ha fatto ciò che gli è stato chiesto (nello specifico premere i tasti PLAY+REC).

Per i test farò in modo di simulare questo comportamento su breadboard collegando con un ponticello i piedini 1 e 6 del connettore quando necessario. In effetti dalla schermata di Figura 5, posizionando il ponticello, il video diventa completamente azzurro esattamente come dovrebbe essere durante il salvataggio di un programma basic e torna alla normalità, cioè con la schermata “bicolore” e il testo, non appena il salvataggio termina.

Al termine del salvataggio si può rimuovere con calma il ponticello, simulando la pressione del tasto “STOP” e rilasciando quindi virtualmente i tasti “REC e PLAY”.

Figura 6: Ponticello per il segnale SENSE

Analisi dei dati in uscita

Trovato il modo di simulare correttamente la pressione dei tasti sul registratore per far credere al C64 di aver a che fare con un datassette fisico si può procedere all’analisi di ciò che viene inviato attraverso la linea “WRITE” (pin E5) che trasporta i dati dal computer verso il registratore.

Per farlo collego l’oscilloscopio in modo che la massa sia connessa al pin A1 e il puntale al pin E5:

Figura 7: Collegamento tra oscilloscopio e C64


Per quanto riguarda l’impostazione dell’oscilloscopio seleziono 2V/div per l’asse verticale delle tensioni, dato che mi aspetto valori di tensione in linea con quelli propri delle logiche della famiglia TTL, e 200.0us/div per l’asse orizzontale dei tempi.

Per quanto riguarda la sezione trigger utilizzo la modalità che permette di effettuare un singolo sweep innescato da un impulso la cui durata sia maggiore di un valore impostato. Non so esattamente quale possa essere il valore da utilizzare, per un primo tentativo imposto 50us; se non dovesse scattare nessun trigger proverò a ridurre questo tempo.

Figura 8: impostazioni oscilloscopio


E’ il momento della verità: inserendo il comando di salvataggio SAVE "TEST",1 e premendo il tasto RETURN dovremmo vedere qualcosa comparire sull’oscilloscopio: incrociamo le dita!

E in effetti… qualche cosa succede, qui sotto il risultato della prima anlisi del segnale generato dal Commodore sulla linea WRITE del registratore durante il salvataggio:

Figura 9: Dati in uscita dalla porta tape durante il salvataggio


Scorrendo avanti e indietro nel tempo però si nota che tutti gli impulsi sono di ampiezza identica, circa 4Vpp, e durata simile, intorno ai 190us e questo è strano perchè un treno di impulsi tutti uguali non porta alcuna informazione.

Tuttavia ci offre una interessante prospettiva: date le tensioni e le frequenze in gioco possiamo utilizzare un Arduino, come se fosse un analizatore logico, per analizzare un numero maggiore di impulsi e vedere meglio cosa succede.

Collegamento di Arduino e primo sketch

Il collegamento di Arduino alla porta tape del Commodore 64 è davvero immediato: è sufficiente connettere la massa che si trova al pin A1 del registratore alla massa di Arduino e il segnale WRITE che si trova al pin E5 al pin D2 di Arduino. Fatto!

Ho scelto il pin D2 perchè ci permetterà di utilizzare gli interrupts per rilevare i cambiamenti di stato del segnale che vogliamo analizzare ovvero intercettare quando tale segnale passa da un livello logico basso (0 o false) corrispondente a 0V a un livello logico alto (1 o true) corrispondente a 4V e viceversa.

Ecco dunque il setup su breadboard:

Figua 10: Collegamento di Arduino alla porta tape


Come anticipato, per analizzare i cambiamenti di stato della linea WRITE utilizzeremo gli interrpupt e in particolare collegheremo al pin D2 e al suo cambiamento di stato opportune funzioni tramite l’istruzione:

attachInterrupt(interruptNumber, function, mode);

Il primo parametro è l’interrupt al quale collegarsi. Può essere determinato consultando una apposita tabella nella documentazione del microcontrollore o sul sito di Arduino (AttachInterrupt – Arduino Reference) o, meglio, utilizzando una apposita funzione, digitalPinToInterrupt(pin), a cui passare il numero del pin che si desidera utilizzare.

Il secondo parametro è il nome della funzione che il microcontrollore dovrà eseguire quando si verificherà la condizione di interrupt.

Il terzo e ultimo parametro definiscce la condizione in cui si verifica l’interrupt, e quindi la chiamata alla funzione collegata: può avere i valori RISING, FALLING, CHANGE e LOW che determinano la chiamata della funzione rispettivamente sul fronte di salita del segnale applicato al pin interessato, sul fronte di discesa, ogni volta che vi è una transizione da basso a alto oppure da alto a basso e quando il pin è in stato basso.

Per il nostro “analizzatore logico” fatto in casa utilizzeremo alternativamente RISING e FALLING nel modo che si può vedere nel codice riportato qui sotto.

volatile unsigned int duration=0;
volatile unsigned long prev_micros=0;

void setup() {
  Serial.begin(230400);
  attachInterrupt(digitalPinToInterrupt(2), rising, RISING);
}

void loop() { }

void rising() {
  prev_micros = micros();
  attachInterrupt(digitalPinToInterrupt(2), falling, FALLING);
}

void falling() {
  duration = micros()-prev_micros;
  Serial.println(duration);
  attachInterrupt(digitalPinToInterrupt(2), rising, RISING);
}

E’ un modo estremamente “spartano” di analizzare i cambiamenti che avvengono sul pin 2 e richiede alcuni accorgimenti, tipo alzare i boud a 230400, per funzionare ma… per ora fa il suo lavoro e ci permette di capire alcune cose in più su come vengono codifcati i dati.

Caricando lo sketch e aprendo il terminare seriale di Arduino vengono stampati una discreta quantità di numeri che corrispondono alla durata degli impulsi (parte positiva del segnale) trasmessi dal C64 al datassette. Ne riporto qui sotto una parte significativa:

192
196
192
192
196
200
192
...
356
260
268
184
192
272
188

...
196
196
192
196
196
192

Ritroviamo all’inizio una serie di impulsi molto simili a quelli che avevamo individuato tramite l’oscilloscopio: la loro durata misurata da Arduino varia leggermente a causa del fatto che, con il tipo di approccio che ho utilizzato nel codice, siamo probabilmente al limite delle possibilità della scheda oppure del fatto che il C64 stesso non è precisissimo nella generazione o ancora a causa di come vengono gestiti internamente gli interrupts dal microcontrollore e come viene implementata la funzione micors() di Arduino. Questo potrebbe essere un interessante spunto per una analisi successiva da fare con un analizzatore logico un po’ più performante.

Tornando ai nostri impulsi, vediamo valori che variano tra 192us (esattamente il valore indicato dall’oscilloscopio) e 200us per molti impulsi consecutivi.

Però ad un certo punto la cosa cambia parecchio e viene rilevato un impulso della durata di 356us seguito da impulsi con durata tra 260us e 270us e nuovamente impulsi intorno ai 192us.

Analizzando tutti i valori si riescono ad identificare quindi 3 tipi differenti di impulsi:

  • Impusi brevi: durata tra i 188us e 200us
  • Impusi medi: durata tra i 260us e 280us
  • Impusi lunghi: durata tra i 348us e 356us

Con queste informazioni andiamo a modificare lo sketch in modo da fargli stampare solo “1”,”2″ o “3” in base al tipo di impulso individuato.

volatile unsigned int duration=0;
volatile unsigned long prev_micros=0;
volatile int value = 0;

void setup() {
  Serial.begin(230400);
  attachInterrupt(digitalPinToInterrupt(2), rising, RISING);
}
 
void loop() { }
 
void rising() {
  prev_micros = micros();
  attachInterrupt(digitalPinToInterrupt(2), falling, FALLING);
}
 
void falling() {
  duration = micros()-prev_micros;
  value=0;
  
  if(duration>=10 && duration<220) {
    value = 1;
  }

  if(duration>=220 && duration<300) {
    value = 2;
  }

  if(duration>=300) {
    value = 3;
  }

  Serial.println(value);
    
  attachInterrupt(digitalPinToInterrupt(2), rising, RISING);
}

Caricando questo sketch e avviando il salvataggio otteniamo una luga serie di valori, uno per ciascun impulso, ma che ora, a differenza di prima, sono stati normalizzati.

La stessa sequenza di impulsi mostrata poco più sopra ora appare così:

1 1 1 1 1 1 1 ... 3 2 2 1 1 2 1 ... 1 1 1 1 1 1

Questo potrebbe già bastare per salvare, ad esempio su una SD, la sequenza degli “1”,”2″ e “3” che costituiscono il programma salvato, qualsiasi sia il loro significato sarà poi possibile rigenerare i treni di impulsi delle lunghezze necessarie a partire dal dato memorizzato.

Dagli impusi ai bits e bytes

Il mio obiettivo però è capire fino in fondo cosa viene inviato al datassette dal Commodore 64 durante il salvataggio. Quindi devo capire come le diverse lunghezze degli impulsi siano trasformati in informazioni.

Trascurando per il momento tutti gli “1” che vengono inviati all’inizio del salvataggio si nota facilmente che quando gli “1” terminano si ripetono delle sequenze di “1” e “2” separate da una sequenza ricorrente “3”->”2″.

In sostanza c’è una sequenza “3”->”2″ seguita da 18 “1” o “2” e poi si ripete la coppia “3”->”2″ e ancora 18 “1” o “2” e così via.

...
3 2 2 1 1 2 1 2 2 1 1 2 1 2 1 2 2 1 1 2
3 2 1 2 1 2 1 2 2 1 1 2 1 2 1 2 2 1 2 1
3 2 2 1 2 1 2 1 1 2 1 2 1 2 1 2 2 1 2 1
3 2 1 2 2 1 2 1 1 2 1 2 1 2 1 2 2 1 1 2
3 2 2 1 1 2 2 1 1 2 1 2 1 2 1 2 2 1 1 2
3 2 1 2 1 2 2 1 1 2 1 2 1 2 1 2 2 1 2 1
3 2 2 1 2 1 1 2 1 2 1 2 1 2 1 2 2 1 1 2
3 2 1 2 2 1 1 2 1 2 1 2 1 2 1 2 2 1 2 1
3 2 2 1 1 2 1 2 1 2 1 2 1 2 1 2 2 1 2 1
3 2 2 1 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2
3 2 2 1 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2
3 2 1 2 1 2 1 2 2 1 1 2 1 2 1 2 1 2 1 2
3 2 2 1 1 2 1 2 1 2 2 1 1 2 1 2 1 2 2 1
3 2 1 2 1 2 1 2 2 1 1 2 1 2 1 2 1 2 1 2
3 2 1 2 1 2 2 1 1 2 2 1 1 2 2 1 1 2 1 2
3 2 2 1 1 2 2 1 1 2 1 2 1 2 2 1 1 2 1 2
3 2 2 1 2 1 1 2 1 2 2 1 1 2 2 1 1 2 2 1
3 2 1 2 1 2 2 1 1 2 2 1 1 2 2 1 1 2 1 2
3 2 1 2 1 2 1 2 1 2 1 2 2 1 1 2 1 2 1 2
...

In tutto sono 20 impulsi ogni blocco: i primi due sembrano essere un marcatore di “start” seguito dai dati veri e propri. Dato che il Commodore 64 è un sistema a 8 bit potremmo ipotizzare che anche questi dati siano trasferiti in “pacchetti” di 8 bits.

Se l’ipotesi degli 8 bits fosse vera avremmo una coppia di “1” e “2” per ogni bit con 2 impusi finali che “avanzano”. Ipotizziamo che possano essere relativi a un bit di parità come accade per i protocolli seriali tipo l’RS232C.

Quindi:

  • Coppia 3-2: Start
  • Coppia 1-2: Bit 0
  • Coppia 2-1: Bit 1

Applicando questa regola alle sequenze precedenti (che lavoraccio!!!), tenendo presente che il bit meno significativo è il primo e quindi quello più a sinistra (LSF), trasformando i primi 8 bits in esadecimale e poi in caratteri ASCII si ottiene:

10010001-0 0x89 ⸮
00010001-1 0x88 ⸮
11100001-1 0x87 ⸮
01100001-0 0x86 ⸮
10100001-0 0x85 ⸮
00100001-1 0x84 ⸮
11000001-0 0x83 ⸮
01000001-1 0x82 ⸮
10000001-1 0x81 ⸮
10000000-0 0x01 ⸮
10000000-0 0x01 ⸮
00010000-0 0x08 ⸮
10001000-1 0x11 ⸮
00010000-0 0x08 ⸮
00101010-0 0x54 T
10100010-0 0x45 E
11001010-1 0x53 S
00101010-0 0x54 T
00000100-0 0x20  
...

Wow! Abbiamo trovato 4 bytes che sicuramente sono stati decodificati correttamente perchè corrispondono al nome del file che abbiamo utilizzato nel comando si salvataggio del programma. Ma gli altri bytes? E cosa succede dopo questi primi valori?

Dopo i primi valori c’è una sequenza di 188 bytes tutti con valore 0x20 che segue esattamente lo schema individuato fino ad ora.

Dopo il 201° byte si verifica una situazione non ancora codificata: 1 impulso lungo (3) seguito da un impulso breve (1). Poi una serie di 78 impulsi brevi simili a quelli che vengono inviati all’inizio del salvataggio.

Passati questi impusi di tipo 1 si ripresenta una sequnza di impulsi di tipo “3”->”2″ seguita nuovamente da 9 coppie di “1”->”2″ o “2”->”1″. E alla fine nuovamente la sequenza “3”->”1″.

Sketch per il dump dei dati

Modifichiamo lo sketch per semplificare il dump dei dati:

volatile unsigned int duration;
volatile unsigned long prev_micros;
volatile int value = 0;
volatile int prev_value = 0;
volatile bool secondo = false;
volatile int pos=0;
volatile byte number=0;
volatile byte mode=0;

void setup() {
  Serial.begin(230400);
  attachInterrupt(digitalPinToInterrupt(2), rising, RISING);
}
 
void loop() { }
 
void rising() {
  prev_micros = micros();
  attachInterrupt(digitalPinToInterrupt(2), falling, FALLING);
}
 
void falling() {
  duration = micros()-prev_micros;
  value=0;
  
  if(duration>=10 && duration<220) {
    value = 1;
  }

  if(duration>=220 && duration<300) {
    value = 2;
  }

  if(duration>=300) {
    value = 3;
  }

  if(secondo) {
    if(prev_value==1 && value==1)
    {
      //Serial.print("-- Sync --");
    } 
    else if(prev_value==1 && value==2) {
      
      if(pos<8)
        number = number >> 1;
      else
        Serial.print("-");
      Serial.print("0");
      pos++;
    } 
    else if(prev_value==2 && value==1) {
      if(pos<8) {
        number = number >> 1;
        number |= 128;
      }
      else  {
        Serial.print("-");
      }
      Serial.print("1");
      pos++;
    } 
    else if(prev_value==3 && value==1) {  // End-of-data
      Serial.print(" 0x");
      if(number<16) {
        Serial.print("0");
      }
      Serial.print(number,HEX);
      Serial.print(" ");
      Serial.println(char(number));
      secondo=false;
      Serial.println("-- End --");
      mode=0;
      pos=0;
      number=0;
    } 
    else if(prev_value==3 && value==2) {  // New-data      
      if(mode==0)   // da sync a data
      {
        mode=1;
        Serial.println("-- Start --");
      }
      else if(mode==1)   // Data
      {
        Serial.print(" 0x");
        if(number<16) {
          Serial.print("0");
        }
        Serial.print(number,HEX);
        Serial.print(" ");
        Serial.println(char(number));
      }
      pos=0;
      number=0;
    } 
    else {
      Serial.print("-");
      Serial.print(prev_value);
      Serial.print("|");
      Serial.println(value);
      
    }
  }
  secondo=!secondo;
  prev_value = value;
    
  attachInterrupt(digitalPinToInterrupt(2), rising, RISING);
}

Analisi dei dati ottenuti con il nuovo sketch

Andando a decodificare i dati con il nuovo sketch si individua una sequanza simile alla prima:

10010000-1 0x09 ⸮
00010000-0 0x08 ⸮
11100000-0 0x07 ⸮
01100000-1 0x06 ⸮
10100000-1 0x05 ⸮
00100000-0 0x04 ⸮
11000000-1 0x03 ⸮
01000000-0 0x02 ⸮
10000000-0 0x01 ⸮
10000000-0 0x01 ⸮
10000000-0 0x01 ⸮
00010000-0 0x08 ⸮
10001000-1 0x11 ⸮
00010000-0 0x08 ⸮
00101010-0 0x54 T
10100010-0 0x45 E
11001010-1 0x53 S
00101010-0 0x54 T
00000100-0 0x20  
...

I primi 9 bytes sono simili ma presentano l’ultimo bit impostato a 0 invece che a 1. I bytes dal 10 in avanti sono identici al blocco precedente.

Utilizzando lo stesso metodo si riescono ad individuare altri 2 blocchi:

10010001-0 0x89 ⸮
00010001-1 0x88 ⸮
11100001-1 0x87 ⸮
01100001-0 0x86 ⸮
10100001-0 0x85 ⸮
00100001-1 0x84 ⸮
11000001-0 0x83 ⸮
01000001-1 0x82 ⸮
10000001-1 0x81 ⸮
11110000-1 0x0F ⸮
00010000-0 0x08 ⸮
01010000-1 0x0A ⸮
00000000-1 0x00 ⸮
10011001-1 0x99 ⸮
00000100-0 0x20  
01000100-1 0x22 "
00001010-1 0x50 P
01001010-0 0x52 R
11110010-0 0x4F O
01101010-1 0x56 V
10000010-1 0x41 A
01000100-1 0x22 "
00000000-1 0x00
00000000-1 0x00 
00000000-1 0x00 
10010000-1 0x09 ⸮
00010000-0 0x08 ⸮
11100000-0 0x07 ⸮
01100000-1 0x06 ⸮
10100000-1 0x05 ⸮
00100000-0 0x04 ⸮
11000000-1 0x03 ⸮
01000000-0 0x02 ⸮
10000000-0 0x01 ⸮
11110000-1 0x0F ⸮
00010000-0 0x08 ⸮
01010000-1 0x0A ⸮
00000000-1 0x00 ⸮
10011001-1 0x99 ⸮
00000100-0 0x20  
01000100-1 0x22 "
00001010-1 0x50 P
01001010-0 0x52 R
11110010-0 0x4F O
01101010-1 0x56 V
10000010-1 0x41 A
01000100-1 0x22 "
00000000-1 0x00 
00000000-1 0x00 
00000000-1 0x00 

Anche in questo caso riconosciamo le sequenze iniziali (con l’ultimo bit a 1 prima e a zero dopo) e un po’ di fortuna aiuta: si riconosce la stringa “PROVA” che fa parte del programma di prova che abbiamo utilizzato come cavia per il nostro test di salvataggio.

Questi due blocchi dovrebbero quindi contenere le informazioni vere e proprie del programma che abbiamo salvato.

Struttura dei blocchi salvati

Riassumento tutte le informazioni raccolte fin qui possiamo quindi schematizzare i dati inviati dal C64 al datassette in quattro blocchi principali:

  • Dettagli sul file
  • Ripetizione dettagli sul file
  • Dettagli sul programma
  • Ripetizione dettagli sul programma

I due blocchi “principali” (cioè non ripetizioni) iniziano con una seqenza di 9 bytes fissa: 0x89 0x88 0x87 0x86 0x85 0x84 0x83 0x82 0x81 mentre i due blocchi “ripetuti” presentano la sequenza: 0x09 0x08 0x07 0x06 0x05 0x04 0x03 0x02 0x01.

Il blocco che contiene i dettagli sul file (sia il principale che la copia) contiene poi 5 bytes il cui significato è ancora da capire, seguiti dal nome del file e una serie di spazi (byte 0x20) fino ad arrivare alla lunghezza di 201 bytes (9+5+4+183).

Il blocco che contiene i dettagli del programma (sia il principale che la copia) contiene, dopo l’intestazione, il programma vero e proprio.

Più in generale un blocco è costituito da 9 bytes di intestazione seguiti da 192 bytes di dati strutturati in modo opportuno in base al tipo di blocco.

Struttura di un programma BASIC

Partendo dal presupposto che il file che stiamo salvando è un programma BASIC, andiamo a spulciare un po’ di documentazione del Commodore 64, in particolare quella relativa alle mappe della memoria: scopriamo che i programmi basic sono memorizzati in un area che va dall’indirizzo 0x0801 all’indirizzo 0x9FFF.

$0801-$9FFF     Default BASIC area (38911 bytes).
2049-40959

Questo spiega cosa siano i bytes 11 e 12 presenti nel blocco dettagli file: sono l’indirizzo di partenza del programma basic che abbiamo salvato (il c64 utilizza la notazione LSF cioè per rappresentare valori a 16 bits prima rappresenta la parte meno significativa e poi quella pià significativa; semplificando il valore è dato da 1°byte+256*2°byte).

Andiamo dunque a vedere come viene memorizzato un programma in quell’area di memoria attraverso un dump:

0F 08 0A 00 99 20 22 50      ..... "P
52 4F 56 41 22 00 00 00      ROVA"...

Sempre dalla documentazione si può evincere che i primi due bytes costituiscono un puntatore che indica l’indirizzo di inizio dell’istruzione successiva; in questo caso 0x080F

Il terzo e il quarto byte rappresentano il numero di linea dell’istruzione BASIC che abbiamo inserito nel nostro programma: in questo caso 0x000A (cioè 10, infatti l’istruzione era 10 PRINT ... ).

Segue quindi il valore 0x99 che non è altro il codice istruzione (anche detto “token”, a ogni istruzione BASIC ne corrisponde uno differente) del comando PRINT, poi c’è il paramtero in ASCII e infine il marcatore di fine istruzione 0x00.

Gli ultimi 2 bytes sono relativi alla istruzione successiva: se ci fosse una seconda istruzione questi due bytes indicherebbero la locazione di memoria dove trovare la terza istruzione. Dato che sono entrambe a 0x00 significa che il programma termina qui.

Da questo possiamo anche determinare che il programma è lungo 16 bytes e quindi l’indirizzo dell’ultimo byte del programma è 0x0811 che è esattamente il valore dei bytes 13 e 14 dei blocchi dei dettagli del file!

Altri tipi di file

Fino ad ora abbiamo provato a salvare un programma su nastro ma il BASIC del Commodore 64 permette di salvare anche altri tipi di dati su nastro, ad esempio i files sequenziali.

I files sequenziali

Per creare un file sequenziale su nastro e scrivere al suo interno dei dati è sufficiente utlizzare i comandi OPEN, PRINT# e CLOSE. Ecco un esempio:

OPEN 1,1,1,"TEST SEQUENZIALE"
PRINT#1,"PROVA FILE SEQUENZIALE"
CLOSE 1

Nel momento in cui si conferma il comando OPEN il Commodore 64 risponde con la classica richiesta “PRESS RECORD & PLAY ON TAPE”. Attivando il segnale SENSE si avvia la trasmissione dei dati.

Anche in questo caso la trasmissione inizia con un treno di impulsi di pari durata (192us) fino a una coppia impulso lungo/impulso medio che rappresenta l’inizio di un blocco dati. Qui sotto il dump del blocco che viene inviato all’apertura del file (per questioni di spazio riporto solo il principale, ma come per i programmi BASIC questo stesso blocco è ripetuto con i primi 9 bytes modificati come prima):

10010001-0 0x89 ⸮
00010001-1 0x88 ⸮
11100001-1 0x87 ⸮
01100001-0 0x86 ⸮
10100001-0 0x85 ⸮
00100001-1 0x84 ⸮
11000001-0 0x83 ⸮
01000001-1 0x82 ⸮
10000001-1 0x81 ⸮
00100000-0 0x04 
00111100-1 0x3C <
11000000-1 0x03 
00111111-1 0xFC ⸮
11000000-1 0x03 
00101010-0 0x54 T
10100010-0 0x45 E
11001010-1 0x53 S
00101010-0 0x54 T
00000100-0 0x20  
11001010-1 0x53 S
10100010-0 0x45 E
10001010-0 0x51 Q
10101010-1 0x55 U
10100010-0 0x45 E
01110010-1 0x4E N
01011010-1 0x5A Z
10010010-0 0x49 I
10000010-1 0x41 A
00110010-0 0x4C L
10100010-0 0x45 E
00000100-0 0x20  
00000100-0 0x20  
...

La struttura è dunque molto simile alla precedente: 9 bytes di intestazione, 5 bytes di informazioni e il nome del file appena aperto seguito da un numero di 0x20 sufficiente a portare la lunghezza totale a 201 bytes.

Procedendo con i comandi, dopo aver confermato il comando PRINT# non succede nulla di evidente. In sostanza i dati che abbiamo inviato con PRINT# sono stati accodati nel buffer di uscita in attesa di essere scritti. Confermando il comando CLOSE invece avviene un nuovo invio di dati verso il registratore (anche in questo caso riporto solo il blocco principale):

10010001-0 0x89 ⸮
00010001-1 0x88 ⸮
11100001-1 0x87 ⸮
01100001-0 0x86 ⸮
10100001-0 0x85 ⸮
00100001-1 0x84 ⸮
11000001-0 0x83 ⸮
01000001-1 0x82 ⸮
10000001-1 0x81 ⸮
01000000-0 0x02 
00001010-1 0x50 P
01001010-0 0x52 R
11110010-0 0x4F O
01101010-1 0x56 V
10000010-1 0x41 A
00000100-0 0x20  
01100010-0 0x46 F
10010010-0 0x49 I
00110010-0 0x4C L
10100010-0 0x45 E
00000100-0 0x20  
11001010-1 0x53 S
10100010-0 0x45 E
10001010-0 0x51 Q
10101010-1 0x55 U
10100010-0 0x45 E
01110010-1 0x4E N
01011010-1 0x5A Z
10010010-0 0x49 I
10000010-1 0x41 A
00110010-0 0x4C L
10100010-0 0x45 E
10110000-0 0x0D 
00000000-1 0x00 
00000100-0 0x20 
...

In questo caso dopo i 9 bytes di intestazione c'è un unico byte di informazioni sul blocco seguito subito dopo dai dati che ho inviato con il PRINT#.

Tipo di blocco

Il decimo byte sembra quindi un indicatore del tipo di blocco. Facendo un po' di ulteriori prove si riescono ad individuare i valori seguenti:

  • $01= Programma BASIC
  • $02= Blocco dati di un file SEQ
  • $03= Programma PRG
  • $04= Intestazione di un file SEQ
  • $05= Marcatore di fine nastro

Parità e checksums

Una ultima analisi va fatta sui bit di parità a livello di singolo byte e sui valori di checksum presenti a livello di blocco.

Il bit di parità

Come abbiamo visto, ciascun byte è codificato al livello più basso con un marcatore di start rappresentato da una coppia di impulsi lungo/medio (3->2) seguiti da 8 coppie di impulsi medi/corti (1->2 o 2->1) che rappresentano i bits a partire da quello meno significativo e infine due impulsi medi/corti (1->2 o 2->1) che rappresentano il bit di parità.

Ci sono due varianti del bit di parità: bit di parità pari e bit di parità dispari. Quando si usa un bit di parità pari, si pone tale bit uguale a 1 se il numero di "1" in un certo insieme di bit è dispari (facendo diventare il numero totale di "1", incluso il bit di parità, pari). Quando invece si usa un bit di parità dispari, si pone tale bit uguale a 1 se il numero di "1" in un certo insieme di bit è pari (facendo diventare il numero totale di "1", incluso il bit di parità, dispari).

Nel nostro caso quindi si applica la variante parità dispari (cioè il numero di 1 compreso dil bit di parità deve essere dispari) e viene calcolata così:

1 XOR bit0 XOR bit1 XOR bit2 XOR bit3 XOR bit4 XOR bit5 XOR bit6 XOR bit7

Il checksum di blocco

L'ultimo byte di ogni blocco è un byte di checksum calcolato sui 192 bytes di "payload" del blocco stesso (cioè non vengono considerati i primi 9 bytes di intestazione) e viene calcolato così:

0 XOR byte1 XOR byte2 XOR ... XOR byte 192

Conclusioni

Il primo passo per capire come funziona l'interfaccia del registratore del Commodore 64 è fatto ma, visto che l'appetito vien mangiando... mi sono già venuti in mente un po' di esperimenti da fare tipo trovare il modo di caricare direttamente da Arduino un programma basic nel commodore 64 in modo da poter utilizzare un editor esterno per scrivere i programmi e poi farli eseguire su un C64 reale.

Esisterà già qualcosa del genere? Non lo so ancora... mi guarderò in giro! Nel frattempo, se avete qualche domanda, curiosità, informazione aggiuntiva oppure quale idea da realizzare, lasciate un commento qui sotto.

Alla prossima!



Commenti

04.09.2020 Simone

Grande processo di reverse engeneering!

06.07.2022 Marino

buondi e grazie del lavoro svolto, voglio farlo per un olivetti M10. alla prossima Marino

25.04.2023 Daniele

molto interessante, volevo fare un progetto simile per creare dei dump in formato tap. Volevo segnalarti che forse c'è un errore: i tempi basta misurarli tra un impulso di discesa e l'altro per cui si trovano 3 tempi diversi cieca doppi rispetti a quelli da te misurati. Confronta qui: https://web.archive.org/web/20180709173001/http://c64tapes.org/dokuwiki/doku.php?id=analyzing_loaders#tap_format

20.05.2023 Silvio Marzotto

Ciao Daniele, grazie per il link, molto utile! Lo lascio qui nei commenti così diamo un po' di visibilità a questa documentazione che non è facilissima da trovare online. Rispetto ai timings, per me è interessante soprattutto poter verificare dopo il reverse engineering del protocollo (fatto apposta senza andare a cercare prima alcuna informazione) quali fossero in realtà le reali specifiche del protocollo e quanto la mia approssimazione fosse vicina o lontana dalle specifiche effettive. Per tutti gli utenti che volessero cimentarsi con la realizzazione di hardware da interfacciare alla porta tape del C64 e/o gestire files TAP sicuramente una importante informazione così che non utilizzino i timing che ho trovato io in questo esperimento ma quelli "da specifica"! Ciao, Silvio

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

In questo articolo abbiamo parlato di

Potrebbe interessarti anche...

European Cookies Policy Law e la legge italiana

Strumenti

European Cookies Policy Law e la legge italiana

17.11.2014 di Silvio Marzotto3 commenti

Ultimamente sembra che la maggior parte del tempo, in Europa e in particolar modo in Italia, la si debba passare a risolvere grattacapi burocratici piuttosto che impegnarla in attività costruttive […]

Alimentare correttamente ATmega328

Hardware

Alimentare correttamente ATmega328

12.02.2012 di Silvio Marzotto19 commenti

Uno dei principali aspetti da tenere in considerazione quando si lavora con microcontrollori e circuiti digitali è la corretta alimentazione. I problemi più comuni che si potrebbero verificare sono legati innanzitutto […]