Skip navigation
Published Wednesday, January 30, 2013 at 11:43 PM

Retrochallenge WW Wrapup

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