Codifica e decodifica dei dati sulle cassette: una questione di impulsi
Articolo di 0scur0, pubblicato il 24-04-2021.
Categoria: Programmazione.

Premetto che questo non è un articolo ideale per i principianti; per comprenderne i punti chiave è infatti necessario avere una conoscenza approfondita dell'hardware della macchina e una buona padronanza dell'assembler del 65xx, perché, tra le altre cose, vedremo del codice per registrare e caricare i nostri programmi su nastro.

A differenza del drive, il registratore non è una periferica "intelligente", nel senso che non può trasferire dati binari al computer; non disponendo né di memoria, né di un bus seriale, esso può solo trasferire e ricevere impulsi verso (dal) C64. Sorge allora spontanea una domanda: come può il datassette caricare e salvare dati binari? La risposta a questa domanda si trova a monte.

Qualsiasi programma binario presente nella memoria del C64 viene salvato su nastro sotto forma di impulsi. Per scrivere un impulso sul nastro è sufficiente commutare due volte il bit 3 del registro $01, che (guarda caso) è collegato alla porta del registratore.

Come forse qualcuno di voi già saprà, quando il datassette invia un impulso al C64 esso trasmette un segnale elettrico attraverso questa porta, provocando la commutazione del bit 3 del suddetto registro (da 0 a 1, e viceversa). Ogni volta che il bit 3 del registro $01 commuta sul fronte di discesa (cioè passa da 1 a 0) il registro 56333 ($DC0D esadecimale) genera una interruzione. Questo tipo di interruzione non avrebbe nessun significato se non venisse catturata dal loader, un programma preregistrato scritto in assembler che ha il compito di determinare se il bit corrispondente vale 0 oppure 1. Questo valore dipende dal tempo intercorso tra gli ultimi due impulsi consecutivi.

Per completezza, occorre notare che è anche possibile leggere il registro $DC0D periodicamente, senza far ricorso alla gestione delle interruzioni; tuttavia, in questo articolo non tratteremo i loader basati su questa modalità alternativa.

Esistono almeno due tecniche per determinare la durata di una pausa tra due impulsi (e quindi il valore del bit corrispondente) nei loader basati su interruzioni:

  • Caricare uno dei timer di cui sono provvisti i chip CIA, dando la possibilità di generare un'interruzione sia a questo che al registro 56333; la prima delle due sorgenti a generare l'interruzione determina il valore del bit attuale;
  • Caricare uno dei timer dei chip CIA e, ad ogni interruzione, andare a vedere quanti cicli macchina sono trascorsi dall'ultimo impulso (in questo modo si determina direttamente il tempo intercorso tra gli ultimi due impulsi; se questo valore è inferiore ad un intervallo prestabilito, chiamato soglia, il bit vale 0, altrimenti 1 (questo è l'approccio più usato e che preferisco).

All'inizio il loader viene caricato in memoria come un normale programma assembler; dopodiché, il C64 gli passa l'esecuzione.

Per avere la certezza che i dati vengano ricevuti correttamente, i byte di programma sono preceduti da una serie di byte ripetuti (il cui valore è arbitrario, e viene stabilito quando si registra il programma stesso) e da uno o più byte di sincronizzazione (anch'essi del tutto arbitrari, dipendono solo dal loader con cui andranno interpretati gli impulsi), che segnalano al loader l'imminente presenza dei byte di programma (quelli che il loader dovrà caricare in memoria).

In sintesi, quindi, ecco la struttura di un nastro del C64

Struttura di un nastro

Abbiamo spiegato (seppur sommariamente) come vengono (de)codificate le informazioni su un nastro, ma non vado più in là con inutili sproloqui, preferendo mostrare un esempio pratico di quanto esposto fino ad ora. Questo sorgente commentato contiene una routine per la registrazione di un programma su nastro.

* = $c000 ; USO: SYS 49152, xxxxx, yyyyy ; dove xxxxx = indirizzo iniziale del programma da registrare ; yyyyy = indirizzo finale del programma da registrare jsr $aefd ; salta la virgola jsr $a96b ; legge il primo parametro ldx $14 ldy $15 stx program+1 stx dati+2 sty program+2 sty dati+3 jsr $aefd ; salta la virgola jsr $a96b ; legge il secondo parametro ldx $14 ldy $15 stx dati+4 sty dati+5 sei lda $d011 and #$ef sta $d011 ; blanking video (per le badlines) ldx #$01 stx $c0 lda $01 and #$de ; per i programmi che occupano parte del BASIC sta $01 ; play premuto, avvia il registratore main ldx dati ; carica il byte di dati save1 stx $02 ; lo memorizza ldy #$08 sty $03 ; bit counter extract ldy #$02 asl $02 ; estrae il bit e lo mette nel Carry ldx #$1e bcc skip1 ldx #$3c skip1 stx leng+1 bne leng count dec $03 ; decrementa bit counter bne extract ; byte non terminato,esamina il prossimo bit write dec towrite bne main dec towrite+1 bne main dec phase ; passa alla fase successiva beq rout inc main+1 bne skip2 inc main+2 skip2 inc towrite inc towrite+1 bne main rout ldx #$60 stx write program ldx $ffff ; registra il programma jsr save1 inc program+1 bne cont1 inc program+2 cont1 ldx program+1 cpx dati+4 bne program ldx program+2 cpx dati+5 bne program trail jsr save1 ; scrive la coda dec $fb bne trail ora #$21 sta $01 jsr $fce2 ; resetta il C64 al termine del salvataggio leng ldx #$1e wait dex bne wait sta $01 eor #$08 ; inverte la linea dati out per scrivere l'impulso sul nastro dey bne leng beq count dati !byte $80,$FF,$ff,$ff,$ff,$ff ; byte ripetuto, byte sincro, indirizzo di partenza e finale (formato basso/alto) towrite !byte $ff,$00 ; numero di repliche dei byte phase !byte $06 ; indicatore della fase raggiunta (byte ripetuti o sincro)

Do per scontata la sintassi assembler, soffermandomi solo sulla routine leng, che determina l'intervallo di tempo tra due impulsi successivi.
Questa routine crea un ciclo di ritardo dipendente dal bit che si vuole registrare; nel caso di un bit 0 il ciclo di ritardo è molto più breve, e la nuova commutazione 1-0 del bit 3 di $01 avviene circa 300 microsecondi dopo la precedente (ricordo che un ciclo macchina, nel C64, dura circa 1.015 microsecondi).
Invece, per registrare i bit 1 viene creato un ciclo di ritardo doppio, per un totale di circa 600 cicli macchina tra una commutazione 1-0 e la successiva.
Nota inoltre che ho scelto come byte ripetuto (255 volte) $80, e come byte di sincronizzazione $FF (un solo byte di sincronizzazione, ma nulla vietava di metterne un'intera sequenza).

Adesso ci vuole un loader che decodifichi correttamente ciò che è stato codificato dalla routine precedente; quello che segue è il suo codice sorgente.

entry lda #$01 sta $c0 lda $01 and #$df sta $01 ; riavvia il registratore lda $d011 and #$ef sta $d011 ; blanking video per evitare le badlines sei ; inibisce le interruzioni dal CIA1 ldx #$c2 ldy #$03 stx $dc06 sty $dc07 ; carica il timer B con la soglia prestabilita lda #$7f ldx #$90 ldy #$19 sta $dc0d ; disattiva le interruzioni del clock stx $dc0d ; attiva le interruzioni del registro $01 sty $dc0f ; avvia il timer B in modalita' one-shot lda #<getbit sta $0314 lda #>getbit sta $0315 ; cambia il vettore di interruzione lda $d020 sta $03 ; memorizza il colore del bordo cli ; riattiva le interruzioni dal CIA1 mloop inc $d020 jmp mloop ; loop principale ; DECODER (PARTE 1) getbit lda #$01 cmp $dc07 ; controlla la soglia rol $02 ; salva il bit lda $02 ; carica l'intero byte sync2 cmp #$80 ; e' un byte ripetuto? bne ret ; no,torna dalla routine ldy #$08 sty $fb ; si', inizializza il bit counter (per la ricezione di un byte completo) lda #<sync1 sta $0314 ; cambia il vettore di interruzione ldy #$60 sty sync2 ; volge in routine ret lda $dc0d lda #$19 sta $dc0f ; riavvia il timer jmp $ea81 ; libera lo stack ed esce ; DECODER (PARTE 2) sync1 jsr getbit dec $fb sync3 bne ret ; byte completo? ldx #$08 stx $fb ; si', ricarica il bit counter cmp #$80 ; e' ancora un byte ripetuto? beq ret ; si', non fare nulla cmp #$ff ; e' il byte di sincronizzazione? beq cont jmp ramfree ; no, ritorna alla fase non sincronizzata cont lda #$60 sta sync3 ; si', volge in routine... lda #<baddr ; ...cambia di nuovo il vettore d'interruzione... sta $0314 bne ret ; ...ed esce ; LOADER (PARTE 1) baddr jsr sync1 bne ret ldx #$08 stx $fb vaddr sta $fc ; memorizza i byte di indirizzo inc vaddr+1 bne ret ; e' l'ultimo byte di indirizzo? ldx #<loader2 ldy #>loader2 stx vaddr+1 sty vaddr+2 lda #$4c sta vaddr bne ret ; si', i byte successivi sono dati endheader ; LOADER (PARTE 2) loader ldx #$c9 ldy #<getbit stx sync2 sty $0314 jmp ret ; evita falsi positivi dovuti alle inerzie meccaniche di un C2N reale pgmdata ldy #$00 sta ($fc),y ; memorizza i byte di dati inc $fc bne skip inc $fd ; incrementa il puntatore al byte di indirizzo skip sec lda $fc sta $2d ; predispone il puntatore di fine programma BASIC (byte basso) sbc $fe lda $fd sta $2e ; predispone il puntatore di fine programma BASIC (byte alto) sbc $ff bcs run jmp ret ; caricamento non completato,esce run lda #<irq sta $0314 lda #>irq sta $0315 ; reimposta il vettore di IRQ standard lda $03 sta $d020 ; ripristina il colore del bordo jsr $fc93 ; ferma il registratore e ripristina le badlines jsr $a660 ; reimposta i puntatori alle aree di memoria usate dal programma BASIC jsr $a68e ; imposta il punto da cui iniziare a leggere il BASIC jsr $e453 ; reimposta il vettore del main loop del BASIC jmp $a7ae ; esegue automaticamente la prima istruzione BASIC (=autostart) !byte $8b,$e3,<entry,>entry ; vettori di sistema

Questo è un semplice esempio di loader. Per stabilire se l'impulso ricevuto è associato a un bit 0 o 1, esamino il contenuto del timer B del chip CIA1 e vedo se sono trascorsi più di 450 cicli macchina dall'ultimo impulso. Non appena il byte ricevuto coincide col byte ripetuto ($80), passo alla seconda fase, che consiste nell'attesa del byte di sincronizzazione (nel nostro caso, $FF). Quando è stato ricevuto anche questo byte, comincia la fase di caricamento dei dati: prima i quattro byte di indirizzo, che definiscono il segmento di memoria in cui caricare i byte del programma (indirizzo iniziale-finale in formato basso/alto); poi i byte di programma veri e propri.

Occorre notare tre cose:

  1. Con l'ultima istruzione (JMP $A7AE) il loader si aspetta che il programma inizi con un'istruzione Basic del tipo SYS [primaistruzione];
  2. Gli ultimi 4 byte costituiscono i vettori di sistema $300-$302; la modifica al vettore che punta al main loop del BASIC ($302) è necessaria per permettere al C64 di mandare in esecuzione il loader; in caso contrario il C64 torna all'interprete BASIC dopo aver terminato di caricarlo.
  3. Al termine del caricamento è necessario ripristinare le badlines, i puntatori alle aree di memoria del BASIC e il puntatore alla prima istruzione, in modo che l'interprete processi il tutto correttamente.

Inoltre, questo loader è (volutamente) limitato; non prevede il caricamento di un byte per la verifica del corretto caricamento dei byte di programma (la cosiddetta checksum), né consente di tornare indietro se viene incontrato un byte che sfasi la canonica sequenza $80-...-$FF; inoltre, è piuttosto lento, per cui la soglia e le pause tra gli impulsi andrebbero ridimensionate. Comunque, a beneficio dei lettori, viene fornito il codice completo del recorder e del loader appena illustrati, oltre alle routines accessorie necessarie per poterli utilizzare su un emulatore o un C64 reale.

Viene lasciato ai lettori più volenterosi il compito di migliorarlo (magari mostrando qualche animazione in interrupt durante il caricamento del programma principale) e di provarlo su un vero C64 corredato di datassette.

Commenti
Commenta gioco Ci sono 0 commenti per questo articolo. Registrati se vuoi lasciarne uno.
Commodore 64
Pannello Utente
15 Visitatori, 0 Utenti

Ciao, ospite!
(Login | Registrati)
Ready64 Store
Ultimo Commento
Clicca per leggere tutti i commenti
Simulgolf
"Uno dei più riusciti minigolf per il nostro beneamato! Tre percorsi differenti, con ostacoli ben studiati. In questi giorni di ferie, in famiglia ci stiamo divertendo a fare tornei con l'emulatore su smartphone. Un altro bel prodotto italiano! ..."
leggi »
- Xandra
Ult. Commento Art.
Intervista a Leopoldo Ghielmetti
"Finalmente ho l'onore di vedere in faccia colui che reputo una delle più grandi menti informatiche del ventesimo secolo (non sto scherzando, anche perché non sono tipo di facili lusinghe). Che dire, complimenti davvero per la geniale invenzione, il ..."
- Scritto da 0scur0
RetroCampus