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.

Continue reading “Retrochallenge WW Wrapup”


Good news! I tracked down my checksum bug last night, and just in the nick of time, everything is working. I can both LOAD and SAVE from EhBASIC, and my little SBC is happy. Finally, a Retrochallenge that I’ve completed successfully!

For the technically minded who are interested in the gory details, I’ll explain exactly what was going wrong. BASIC programs are stored in tape files that contain two records.

BASIC tape record

Each record starts with a 770Hz tone as a header, followed by a short sync bit, then the record data, and finally a checksum. The checksum is just a running-XOR of the data.

The first record is just two bytes, it specifies the length of the data in the second record (little endian, which is, as we all know, the only true endianness).

The second record is the BASIC program data. The bug I was seeing on Sunday was that the checksum failed, but only on the DATA record, never on the LENGTH record. I could not for the life of me figure out what was happening. I went so far as to capture the tape data on my oscilloscope and walk over it bit by bit, by hand, and calculate a checksum. The checksum on the tape was correct, it should have read correctly. And of course the checksum of the LENGTH record was matching correctly.

Then I had a breakthrough, one of those things that should have been painfully obvious but was not. I discovered it by walking through the data that was loaded into memory off the tape. EhBASIC puts the program at $0301, so I dumped page 3 to see what was in it. It all looked correct, until I noticed that the checksum itself was being loaded as data. I had a simple, very stupid off-by-one error in my arithmetic, so I was slurping in the checksum as part of the program data by mistake. It was then reading garbage (random tape hiss noise) from just after the record and treating that as the checksum, which of course never matched. Aha!

I fixed my arithmetic, burned a new EPROM, and now, at last, I can SAVE and LOAD just like it’s 1977 all over again.

Tomorrow I’ll post a quick video, and a final Retrochallenge wrap-up.

Still Flogging Away

OK, it turns out this is harder than I expected. I’m fighting a very strange bug in my LOAD code. It’s complaining of a checksum mismatch, but there doesn’t actually seem to be one. And, weirder, my program loads anyway, despite the fact that the checksum failure branches away from the code that is supposed to reset the BASIC program and variable space.

In short, it’s failing where it’s supposed to succeed, and it’s succeeding where it’s supposed to fail. Wait, what?!

Whatever the bug is, I still intend to squash it before the Retrochallenge deadline on Thursday. Plenty of time! (he said, over-confidently)

Down to the Wire

Golly, I do like waiting until the last minute, don’t I?

No, I haven’t given up. I’m just finishing up the last few bits. I meant to get everything done yesterday, but got distracted by Real Life Matters. Oh well! But I have plenty of time today.

The very last thing I need to do is patch together my READ and WRITE routines into LOAD and SAVE BASIC commands. Since my little computer uses Enhanced 6502 BASIC, this will not be difficult. EhBASIC provides empty hooks for your own LOAD and SAVE code, so it’s very easy to patch into.

By this time tomorrow, I hope to have a finished project ready to demo.

Getting Closer

Good progress tonight! I think the code to WRITE data to tape is fully complete. I’m not particularly impressed with my own 6502 assembly, but by golly it works.

Note that there is no checksum yet. I’m calling this “Optional” for now. I may regret that later.

The next step is the opposite process: READING THE DATA! I’ll have more on that when I’m a little less tired.

.alias TAPEST   $40
.alias A1L      $41             ; Start address low byte
.alias A1H      $42             ; Start address high byte
.alias A2L      $43             ; End address low byte
.alias A2H      $44             ; End address high byte

.alias IOB      $8000
.alias IOA      $8001
.alias DDRB     $8002
.alias DDRA     $8003

.org $0300

INIT:   LDA     #$FF            ; Make all Port-A IO lines outputs
        STA     DDRA
        LDA     #$00            ; Initialize tape state scratch space
        STA     TAPEST
        STA     IOA

; Write Memory to Tape
;   Start Address: A1L,A1H
;   End Address: A2L,A2H
        JSR     WRSYNC
        JSR     WRDATA

        ; END
        ; END

; Write the Tape Header.
; This outputs 10 seconds of 750 Hz square wave.
; NB: Measured values for cycle widths at 1MHz PHI2:
;  $79 = 776.4Hz  1.288ms
;  $7A = 766.9Hz  1.304ms
;  $7B = 757.6Hz  1.320ms
HEADR:  LDX     #$3C            ; 60 times thru inner loop
HD0:    LDY     #$FF            ;  (60 * 255 = 15,300 half cycles)
HD1:    JSR     TOGTAP
        LDA     #$7A            ; (122 * 5 uS) + overhead = 650 uS width
HD3:    SBC     #$01            ; Delay (using A, because X and Y are busy)
        BNE     HD3
        BNE     HD1             ; Inner Loop
        BNE     HD0             ; Outer Loop

; Write a single Sync Bit.

WRSYNC: LDX     #$24
        JSR     WS0             ; Recursive call to toggle bit twice
WS0:    JSR     TOGTAP
WS1:    DEX
        BNE     WS1
        LDX     #$28

; 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: LDX     #$00
        LDA     (A1L,X)         ; Get first byte to send to tape.
        LDY     #$08            ; 8 bits to do
WRBIT:  ROL                     ; Shift leftmost bit into C
        PHA                     ; Save the accumulator.
WD0:    JSR     TOGTAP          ; 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     WD0             ; If not, write second half.
        PLA                     ; If so, restore the input byte
        DEY                     ; If not done with bits, do
        BNE     WRBIT           ;   next bit...
        JSR     INCADR          ; Increment read address,
        BCC     WRDATA          ;   and get the next byte
        ; Done. At this point, the entire block of memory to
        ; dump to tape has been sent. A1L,A1H have been
        ; incremented to be > A2L,A2H

; Toggle the current tape output state. Modifies Accumulator.
TOGTAP: LDA     TAPEST          ; Load current tape state
        EOR     #$01            ; Toggle it
        STA     TAPEST          ; Store back into tape state,
        STA     IOA             ;    and out to tape.

; 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

Retrochallenge: The First Piece of Code

Alright, it’s not much to look at, but it’s a start.

This code generates ten seconds of 770Hz square wave, which is the tape header. It is both less compact and less elegant than the code that Woz wrote for the Apple 1 and Apple II, but for that I blame my inexperience (and the fact that Woz was a kind of 6502 savant who had no need for such niceties as assemblers). I hope that it is at least fairly easy to read and understand.

Next up is writing a sync bit, followed by shifting in data from memory and writing it to tape one bit at a time.

.alias IOA    $8001             ; 6522 ORA register
.alias DDRA   $8003             ; 6522 DDRA register

.alias TAPEST $40               ; Current tape state scratch area

.org $0300

INIT:   LDA     #$FF            ; Make all Port-A IO lines outputs
        STA     DDRA
        LDA     #$00            ; Initialize tape state scratch space
        STA     TAPEST

; Write 10 seconds of 770 Hz square wave to tape.
HEADR:  LDX     #$3C            ; 60 times thru inner loop
HEADR0: LDY     #$FF            ;   (60 * 255 = 15,300 half cycles)
HEADR1: LDA     #$7A            ; (122 * 5 uS) + overhead = 650 uS width
        JSR     DELAY
        JSR     WRTAPE
        BNE     HEADR1          ; Inner Loop
        BNE     HEADR0          ; Outer Loop

        ;; ----- END -------
        ;;  TODO: Write sync bit, then shift in data and write bit-by-bit.
        ;; ----- END -------

; Delay for 'A' cycles ('A' * 5 clocks). Modifies the Accumulator.
DELAY:  SBC     #$01
        BNE     DELAY

; Toggle the current tape output state. Modifies the Accumulator.
WRTAPE: LDA     TAPEST          ; Load current tape state
        EOR     #$01            ; Toggle it
        STA     TAPEST          ; Store back into tape state,
        STA     IOA             ;    and out to tape.

Whither Java on the Desktop?

[Symon running on the Desktop]

A little while ago I wrote the 6502 simulator pictured above, Symon, and released it as open source software. I wrote it because I was developing a small 6502 computer, and wanted a simulator that matched the hardware’s memory map. I’ve always been interested in learning more about simulation, so it was a natural project for me to gravitate to. I’ve enjoyed working on it tremendously, but there is only one problem: I wrote it in Java.

Java is undergoing what seems to me to be a crisis of public opinion. The news is full of stories about another critical vulnerability, and vendors are rushing to disable Java in browsers by default. Users are being told to turn off Java wherever unless they really need it.

So why did I choose Java in the first place? For several reasons. First, I started this project a couple of years ago. When I first set out to write the emulator in 2008, Java was still owned by Sun Microsystems, still enjoyed widespread popularity, and wasn’t yet considered as much of a security risk. Second, Java was a language that I knew extremely well, having worked on many Java projects in the past. I started writing Java in 1997, and it was the primary language used in my day job straight through 2007, so it was quite natural for me to fall back on it. Third, and maybe most importantly, I very much wanted Symon to be fully cross-platform. Although I use a Mac with OS X as my main system, I also have a Linux laptop and a Windows 7 PC that I spend quite a bit of time on. It was important to me that I’d be able to use Symon on all three platforms. Java gave me that feature for free.

But with the latest goings-on in the world of Java on the desktop, I have to wonder, will Java even have a viable consumer desktop runtime in the near future? Sure, it has tremendous support on mobile thanks to Android, and its position in the server market is assured after a decade and a half of Java as a web platform, but it never gained the kind of wide-spread support on the desktop that it has on the server. With more bad news coming out and more end users disabling Java how will people run Symon?

I feel like I’m left with a couple of options.

  1. Ignore the problems. Just keep updating the Java Symon code, and let things fall where they may.
  2. Give up multi-platform. Port Symon to a native OS X Cocoa version, and accept the fact that I won’t be able to run the code on Windows or Linux.
  3. Find a new multi-platform runtime. But which one? Mono, an ecosystem that I know nothing about?

None of these is ideal. The easiest for me to do, of course, is the first, and this is likely what I will do for a while at least. But if my instincts are right, and Java has a very limited lifetime on the desktop, I’ll have to pick one of the other two not too long from now.

How It Works

I’ve been pretty quiet over the past week. This is primarily because my day job has kept me busier than I’d like, compounded by the fact that I’ve come down with a cold. But I shall persevere and forge on ahead. The hardware is done, and I’m currently working on the software. But, how exactly does it work? That’s what I hope to show in this post.

Recording data on a cassette tape is actually very simple, at least the way the Apple II does it. The output signal from the computer to the cassette recorder is just a square wave, varying between TTL low and high logic values (about 0V and +5V). The Apple II drives the signal from a 74LS74, and the SYM-I drives it from a 74LS145. Just about anything could drive it, the input impedance to the cassette is about 4kΩ, it requires very little current.

The width of the cycles in the square wave determine the values being stored. To write a “0”, one full cycle of a 2,000 Hz signal is recorded (low for 250 µs, then high for 250 µs). To write a “1”, one full cycle of a 1,000 Hz signal is recorded (low for 500 µs, high for 500 µs). Here’s a diagram to show what I mean.

Apple II Cassette IO

Of course there’s more than just bits on the tape. A full file on tape starts with a header (10 seconds of a 770 Hz square wave), a “Sync” bit indicating the start of data (a single 2500 Hz cycle, 400 µs wide), then the data itself. The last byte of the data is a checksum byte. See, not very complex!

So, now that we know what’s on the tape, how do we write code to read it and write it? Here’s the algorithm, in a slightly condensed form.

To write a single bit of data to the tape, we need to flip the output state from whatever it currently is, to the opposite (say, from high to low). Then, using a calibrated delay loop, wait until either 250 µs has passed if writing a “0”, or 500 µs if writing a “1”. We do this one more time, this time flipping from low back to high. At the end of the second trip through the delay loop, the bit has been fully written.

Cassette Output

Reading data that’s already been written is similar. The state of an input line coming in from the cassette is monitored in a polling loop (remember, it’s going through a comparator that turns the messy, degraded cassette signal into a nice clean square wave). Each time through the polling loop, a counter is incremented, and we keep looping until we see that the input has changed twice. When that happens, we compare our counter with a known value to see whether we just read a “0” or a “1”

Cassette Input

It’s not a difficult problem, but getting the delay loops just right is important. I hope I’ll have some more concrete results to show by next week. We’re closing in on the 15th, so I only have half a month to get everything working. But I’m not worried… yet!