Well, the project is done, I’ve put down my soldering iron, I’ve stopped writing code, and I’m in the middle of editing a demo video (to be posted later, when I’m a little less tired).
I’m mostly happy with how things turned out this time around. I was successful with my project, I’ve met my goal of having usable tape storage. I should probably put the word usable in quotes, though, like this: “usable”. Because, to be honest, it is somewhat flakey. I have about a 70% success rate with saving programs that can be read back later. I think my main problem is just how much noise my circuit injects into the signal. I really did not use best engineering practices: I have no ground plane, my cables are unshielded, my leads are too long, I’m just begging for noise. So, really, I’d say 70% success is not so bad, all things considered.
I learned a lot, especially about how the Apple II did things. I pored through Woz’s code and really tried to understand what it was doing. I enjoyed the process.
But I confess I don’t know how much I’m actually going to use what I’ve built. The unreliability is a problem (as it was for the Apple I, incidentally. At least I’m not alone!). I think my next project will involve writing 6502 SPI code so I can use an SD card for mass storage. It should be perfectly reliable, and provide essentially limitless storage.
Anyway, for those interested, here’s the code I came up with. I include this into EhBASIC and point the SAVE and LOAD vectors at TSAVE and TLOAD, respectively. My code is not exactly elegant and I’m sure a seasoned 6502 veteran will sniff at it, but it gets the job done.
VIA_BASE = $8000
IOB = VIA_BASE
IOA = VIA_BASE+1
DDRB = VIA_BASE+2
DDRA = VIA_BASE+3
TAPEOUT = IOA
TAPEIN = IOA
;***************************************************************************
; Load a BASIC program from tape. Handles the "LOAD" BASIC command.
;
; Modifies A, X, Y
;***************************************************************************
TLOAD LDA #TLOAD_mess
JSR LAB_18C3 ; String Out
LDA #$18 ; Wait for the user to fumble around
JSR TDELAY ; (delay $18 * $FF * 650us = 4 s)
LDA #TLOAD_start
JSR LAB_18C3
@doload LDA #TLL
LDY #$00
STA A1L
STY A1H
LDA #TLH
STA A2L
STY A2H
JSR READ ; Read in the program length.
BNE TPERR ; Checksum mismatch? Error out.
;
; At this point, the program length is in TLL,TLH.
; The goal is to use this length to compute A1L,A1H
; and A2L,A2H for the program length. Starting address
; A1L,A1H should be the start of memory (Smeml,Smemh).
; The program length is then added to this to arrive
; at A2L,A2H (and consequently Svarl,Svarh)
;
LDA TLL ; Decrement program length
BNE @skip0
DEC TLH
@skip0 DEC TLL
CLC ; Now add length to memory base,
LDA Smeml ; giving the end address.
STA A1L
ADC TLL
STA A2L
LDA Smemh
STA A1H
ADC TLH
STA A2H
JSR READ
BNE TPERR ; Checksum mismatch? Error out.
LDA A2L ; Now set Svarl and Svarh to top of prog
LDY A2H
STA Svarl
STY Svarh
;
; Increment (Svarl,Svarh), since they should really be
; end of program + 1
;
INC Svarl
BNE @skip1
INC Svarh
@skip1 LDA #TLOAD_succ
JSR LAB_18C3
JMP LAB_1319 ; Reset execution, Clear vars
;; Print 'TAPE ERROR' and return
TPERR LDY #$00
@loop LDA TLOAD_err,Y
BEQ @done
JSR V_OUTP
INY
BNE @loop
@done RTS
;***************************************************************************
; Save a BASIC program to tape. Handles the "SAVE" BASIC command.
;
; Modifies A, X, Y
;***************************************************************************
TSAVE LDA #TSAVE_mess
JSR LAB_18C3 ; String Out
LDA #$18 ; Wait for the user to fumble around
JSR TDELAY ; (delay $18 * $FF * 650us = 4 s)
LDA #TSAVE_start
JSR LAB_18C3
@dosave SEC
LDA Svarl ; Subtract start of memory from
SBC Smeml ; start of vars to get program length.
STA TLL ; Store result in TLL,TLH.
LDA Svarh
SBC Smemh
STA TLH
LDA #TLL ; Set up to write contents of TLL,TLH
LDY #$00 ; to tape.
STA A1L
STY A1H
LDA #TLH
STA A2L
STY A2H
LDX #$3C ; Set up for a header of 10 seconds.
JSR WRITE ; Save program length to tape.
LDA Smeml ; Set up to save actual program code
LDY Smemh ; by putting Smeml,Smemh into A1L,A1H,
STA A1L ;
STY A1H ;
LDY Svarh ; and (Svarl,Svarh)-1 into A2L,A2H.
LDA Svarl
STY A2H
STA A2L
BNE @nodech ; Decrement high byte only if needed
DEC A2H
@nodech DEC A2L
LDX #$30 ; Set up for a shorter header (about 8 sec)
JSR WRITE ; Save (Smeml,Smemh) to (Svarl,Svarh)-1
LDA #TSAVE_succ
JSR LAB_18C3
RTS
;***************************************************************************
; Read Memory from Tape
; Start Address: A1L,A1H
; End Address: A2L,A2H
;***************************************************************************
READ JSR RD2BIT ; Find tape edge
LDY #$1F
JSR TDELAY ; Busy loop for 5 sec ($1F * $FF * 650us)
LDY #$00 ; Clear X and Y
LDX #$00
JSR RD2BIT ; Align with transition edge again
RD4 LDY #$22 ; Wait for the sync bit to appear
JSR RDBIT
BCS RD4
JSR RDBIT ; Found sync bit, consume other half
LDA #$FF ; Init checksum
STA CHKSUM
LDY #$3B ; Transitions < $3B = 0. > $3B = 1
RD5 JSR RDBYTE
STA (A1L,X)
EOR CHKSUM
STA CHKSUM ; Update the checksum as we read
JSR INCADR
LDY #$35 ; $3B, compensated for extra work
BCC RD5 ; If more to do, keep going
JSR RDBYTE ; Read in the final checksum byte
CMP CHKSUM ; Set Z if checksum matched.
RTS
;***************************************************************************
; Read 8 bits into accumulator
;***************************************************************************
RDBYTE LDX #$08 ; Set up bit read counter (8 bits)
RDBYT2 PHA ; Preserve A (RD2BIT clobbers it)
JSR RD2BIT ; Look for two tape state transitions
PLA
ROL ; Roll the read bit into A (from carry)
LDY #$3A ; Set the compensated read width
DEX
BNE RDBYT2 ; Keep going until 8 bits read.
RTS
;***************************************************************************
; Read 2 transitions
;***************************************************************************
RD2BIT JSR RDBIT ; Recursive call to self (two transitions)
RDBIT DEY
LDA TAPEIN
EOR TAPEST
BPL RDBIT ; Keep looping until state changes.
EOR TAPEST
STA TAPEST
CPY #$80 ; If Y went negative, set carry (this is a '1')
RTS
;***************************************************************************
; Write Memory to Tape
; Start Address: A1L,A1H
; End Address: A2L,A2H
;***************************************************************************
WRITE LDX #$48 ; Header length of $48 times $FF half cycles
JSR HEADR
JSR WRSYNC
JSR WRDATA
RTS
;***************************************************************************
; Write the Tape Header.
; This outputs 10 seconds of 750 Hz square wave.
;***************************************************************************
HEADR LDY #$FF
HD0 JSR WRTAP
LDA #$7E ; (122 * 5 us) + overhead = 650 uS width
HD1 SBC #$01 ; Delay (using A, because X and Y are busy)
BNE HD1
DEY
BNE HD0 ; Inner Loop
DEX
BNE HEADR ; Outer Loop
RTS
;***************************************************************************
; Write a single Sync Bit.
;***************************************************************************
WRSYNC LDX #$24
JSR WS0 ; Recursive call to toggle bit twice
WS0 JSR WRTAP
WS1 DEX
BNE WS1
LDX #$28
RTS
;***************************************************************************
; Read the memory between A1L,A1H and A2L,A2H, one byte at a
; time, and send it to tape.
;
; Values #$34 was chosen as a perfect match for a 250us
; delay. #$2A is the same delay, but compensated.
;***************************************************************************
WRDATA LDA #$FF ; Initialize the checksum
WD0 LDX #$00
EOR (A1L,X) ; Update checksum with data
PHA ; Save the checksum on the stack
LDA (A1L,X) ; Get first byte to send to tape.
JSR WRBYTE ; Write it.
JSR INCADR ; Point to next byte.
PLA ; Restore the checksum
BCC WD0 ; If more bytes to do, loop.
JSR WRBYTE ; No more to do, write checksum
RTS
;***************************************************************************
; Write the byte in the accumulator to tape. Modifies Accumulator, X, Y
;***************************************************************************
WRBYTE LDY #$08 ; 8 bits to do
WRBIT ROL ; Shift leftmost bit into C
PHA ; Save the shifted accumulator
WD1 JSR WRTAP ; Toggle output state
BCC ZERDLY ; Is C a '0'? Skip '1' delay
LDX #$34
ONEDLY DEX ; Delay for 260 clock cycles.
BNE ONEDLY
ZERDLY LDX #$2A
ZD0 DEX ; Delay for 210 clock cycles.
BNE ZD0
; Bit test does not affect 'Carry' flag, so it will
; still be set from the ROL
BIT TAPEST ; Check to see if the cycle is done.
BNE WD1 ; If not, write second half.
PLA ; Restore the shifted accumulator
DEY ; If not done with bits, do
BNE WRBIT ; next bit...
RTS
;***************************************************************************
; Toggle the current tape output state. Modifies Accumulator.
;***************************************************************************
WRTAP LDA TAPEST ; Load current tape state
EOR #$01 ; Toggle it
STA TAPEST ; Store back into tape state,
STA TAPEOUT ; and out to tape. (Tape out is PORTA.1)
RTS
;***************************************************************************
; Increment the tape memory buffer pointer.
;
; Carry will be left set iff A1L == A2L and A1H == A2H
; Blatently stolen from Woz's Apple 1 Monitor
;***************************************************************************
INCADR LDA A1L ; Compare current addr with end addr.
CMP A2L ; Sets carry if A1L >= A2L.
LDA A1H
SBC A2H ; Clears carry if A2H < A1H.
INC A1L
BNE NOCARY ; If A1L is now 00, inc A1H as well.
INC A1H
NOCARY RTS
;***************************************************************************
; Busy loop for Y * 650us
; Modifies A, X, Y
;***************************************************************************
TDELAY LDX #$FF ; for the tape to start playing.
RD1 LDA #$7A ; ($2D times through inner loop,
RD2 SBC #$01 ; $2D * $FF * 650us = 7.5s)
BNE RD2
RD3 DEX
BNE RD1
DEY
BNE TDELAY
RTS
TLOAD_mess
.byte $0D,$0A,"PRESS PLAY ON TAPE",$00
TLOAD_start
.byte $0D,$0A,"LOADING...",$00
TLOAD_err
.byte $0D,$0A,"TAPE CHECKSUM ERROR",$00
TLOAD_succ
.byte $0D,$0A,"LOADED",$0D,$0A,$00
TSAVE_mess
.byte $0D,$0A,"PRESS RECORD AND PLAY ON TAPE",$00
TSAVE_start
.byte $0D,$0A,"SAVING...",$00
TSAVE_succ
.byte $0D,$0A,"SAVED",$0D,$0A,$00
Comments