Commodore 64
Pannello Utente
592 Visitatori, 2 Utenti (1 Nascosti)
MarcomTO
Ciao, ospite!
(Login | Registrati)

Codifica e decodifica dei dati sulle cassette: una questione di impulsi

🤵 0scur0 📅 24 aprile 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 $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 #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 #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  ; 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: "Codifica e decodifica dei dati sulle cassette: una questione di impulsi"

💬 Ci sono 0 commenti per questo articolo!
🔒 Accedi o Registrati per commentare.
Ultimo Commento
Clicca per leggere tutti i commenti
Robot Jet Action
Se vi piace Bomb Jack, questo gioco vi esalterà. E' vero che è difficile, questo pero' ne aumenta la longevità. Bellissima grafica, ben 35 livelli tutti diversi, trovate anche il trainer su CSDb...voto 10

🤵 Alex da Parma
Icona Lo schermo del C64 e il raster
Grazie mille per questo contributo! Sembra un'ottima introduzione al discorso. Vedrò di leggerlo con calma prossimamente!
🤵 marco_b
Superlink