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

3 thoughts on “Retrochallenge WW Wrapup”

  1. An excellent explaination of how to do cassette tape loading and saving, of which I some of the ideas I will use for my own single board computer.
    You can use timer one to toggle pb7 every time it counts down to zero in free running mode, so by loading a particular value into the timer one latch, you can generate the tones without the effort of bit-banging. This can be done in combination with timer two to generate exact number of cycles for each mark and space tone (two cycles at 1 kHz, four cycles at 2 kHz). All the CPU has to do is examine the fifth bit of the 6522’s interrupt flag register to see if timer two has counted down to zero. Then you can write the next bit to tape.
    pb6 can be used with timer two to count pulses so frequencies can be measured. I am working on this part for loading data back to memory.
    I’m no master at 6502 assembly either, so there is probably an even better way of doing things.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.