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:
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
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:
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.