The Hacker School space has an old Apple //e sitting around.
Due to the fact that we are hackers, my friend Martin Törnwall and I decided to turn it into a lisp machine. (Full source available here.)
The main obstacle was not developing the lisp itself. It was that developing software on the Apple //e was astonishingly painful:
- The Apple //e has less available RAM than my computer has L1 cache, which means that the overhead of BASIC was too much for an implementation of lisp.
- There is no assembler and no text editor, so the only development possibility other than BASIC is to implement directly in machine code.
- We had no extra disks, meaning we could not simply store a program. We had to type in the machine code manually, as hex digits.
But, while we didn’t have extra floppy disks, we did have our laptops.
So, we ended up writing some C code that allows us to encode arbitrary binary data as an audio signal.
Using this, we were able to write the lisp prototype on our laptops, and simply transmit the program via the audio jack to the Apple //e.
(UPDATE: This C code is located here, and as lscharen points out, it should be noted that it is very much derivative of David Schmidt’s excellent ADTPro library.)
Code for the prototype is here—the README contains a reasonably complete explanation of what is possible, and the code is vaguely readable. A demonstration of the system in action is below. Following the video is a discussion of how the deployment code works.
The Apple //e deployment pipeline
The Apple //e has a built-in mechanism to take audio data in, decode it as binary data, and drop it into a contiguous chunk of memory.
So, if we are given this program as a fully-compiled 6502 binary, we can push it to the Apple //e via the audio jack, and it will be deposited in memory for us.
From here, we can jump to the first instruction and execute it as we would any binary.
This leaves two questions:
- How do we encode a binary as audio data?
- How does the Apple interpret it?
Encoding the binary as audio data
Our C code that actually encodes the binary is here.
The official description of the protocol is here: part 1, part 2. The gist is this.
A machine language program is transmitted using a record. A record is a block of binary data which, in this case, is formatted as follows.
+--------+-+-----------------------+-+
| HEADER |S| DATA |C|
+--------+-+-----------------------+-+
Here S
is the so-called synchronous bit, which is used to tell the Apple that the header is stopping and the data is beginning. C
is the checksum byte.
The header is one long steady tone, and is used to tell the Apple that data is about to be sent over the wire. It is designed to give cassette players enough time to get up to speed and let the tape leader go by.
The checksum byte is used to check that there weren’t errors in transmission of the packets. It is generated by XOR'ing all the bytes in the record together. If the Apple receives the record, and the XOR of those bytes doesn’t match what was transmitted, it outputs a very helpful error message:
ERR
When encoding the data, it is important to generate the right tones at the right frequencies for the right periods of time. The header, for example, is encoded as one long 770 Hz tone (be sure to make this long enough—at least 15 seconds in our experience). The sync bit, and the 1’s and 0’s in the data are all encoded similarly and predictably so that the Apple //e can read them back.
The code above basically does all of this and emits the result over the audio jack to the Apple.
How the Apple //e decodes the audio
This part can be clearly seen in the video above. But, as a recap, the Apple //e can be put into “listening” mode by entering the monitor program and specifying the address range in which to load the data. So to read a few bytes, the syntax would be:
* 2000..2008R
The R
at the end stands for “read”.
Under the hood, the reading operation is actually implemented using a “zero-crossing” detector. This refers to the fact that you can think of audio as a wave that vascillates between being positive and negative, and frequency is (roughly) defined as the number of times we change between the two in a certain period of time.
In the Apple //e, this vascillation is represented as a voltage. When the voltage becomes negative, the detector switches—if it’s a 0, it becomes a 1, and if it’s a 1 it becomes a 0. To detect the change, we continually XOR the current status with the previous status, so that when it flips, it becomes 1, and in every case where it’s the same, it’s a 0.
The detector can be accessed at memory address $C060
.
After reading the beginning of the header, the Apple will wait for 3.5 seconds, and then wait for the sync bit to be transmitted. After that, it will read as many bytes as you have specified, plus 1 for the checksum byte, XOR the data bytes together, and check against the checksum byte.
If it passes, you win, and the data is now accessible in memory. You can run the program by jumping to the instruction you want to execute. Jumping to memory address 2000, for example, looks like 2000G
, where G
presumably stands for “go” or something.
If it fails, you get ERR
.
What’s next
Part 2 of this series will be about actually implementing lisp on the Apple //e. This includes talking about things like how to get characters (remember that there’s no OS, so this is different than normal), printing things to the display, and a discussion of the Apple’s odd memory model.
If you enjoyed this post, you should consider applying to Hacker School, which is where this work was done. See some other stuff I did at Hacker School for more reference points. The space is full of some of the nicest, smartest hackers I have ever met. Working around them has been a joy and an inspiration.