Saturday, January 18, 2025

The "35-cent" Commodore 64 softmodem

Rockwell famously used 6502-based cores in modems for many years, but that doesn't mean other 6502s couldn't be used. If only there were a way to connect a Commodore 64's audio output directly to an RJ-11 plug ...
Of the many interesting posts from Usenet's more golden days, one of my favourites was John Iannetta's "35-cent modem," where the SID chip provides one-way data modulation to a receiving modem connected via the C64's sound output. While I remember him posting it back in 1998, I never actually tried it at the time.

Wouldn't you know it, but it came to mind the other day when I was looking at a recent haul of Convergent WorkSlate stuff I've got to catalogue. Officially the WorkSlate's only means of telecommunications is its 300 baud internal modem. While we have a 9600bps way of wiring up a Workslate to a modern computer, it's always nice to have a simpler alternative, and I figured this would be a great challenge to see if John's old program could let my Commodore SX-64 talk to my WorkSlate. Spoiler alert: it works!

I don't know precisely what happened to John; regrettably I know little of his personal history. For a period of time he was a very prolific poster on comp.sys.cbm, but his last post there was July 19, 2000, in which he replied to someone's question about the relationship between sound frequencies and SID register values. (We'll actually talk about this in a bit.) His last post I can find in any Commodore newsgroup of the era is dated the next day, July 20, though he posted through CompuServe and it's possible may have made later posts there. Among his many contributions, including this one, are the Spyne self-extracting file archive utility, a .d64 downloader patch for Common Sense, an in-place PETSCII to ASCII text file converter, a user port-based audio A/D converter and player, and a custom track-by-track floppy disk formatting tool. He issued them all freely for anyone to use for any purpose. While he and I briefly corresponded over snail mail, I can't find the letter he sent me and I don't remember his exact location, and I never heard from him again. Sadly, although I hope I'm wrong, from his handwriting I knew he wasn't a young man and I'm all but certain he has since passed away. Rootsweb lists a John J. Iannetta who died in April 2001 at the age of 82. (If you know for sure, post in the comments, or E-mail me privately at ckaiser at floodgap dawt com.)

The "35-cent modem" was first posted on October 27, 1998. John estimated the 35 cent cost based on the then-purchase price of an RCA jack ("listed in my Jameco catalog at 35 cents each"), though this didn't include the phone cable, or about 68 cents in 2025 dollars. Looking in their online catalogue now, you could even go cheaper, since Jameco (not affiliated, not sponsored, just using for comparison) now sells a through-hole right-angle RCA jack for $0.29 — in 1998 dollars, that would have been a mere 15 cents. If you don't have a landline phone cable anymore, the lowest Jameco price for a compatible connector I could find was a 6P6C modular cable for $1.49. Such a cable is technically a RJ-25, but it or an RJ-14 (6P4C) will do just fine. This project is a very easy build job, so let's do it.

The key notion is that we're replacing the telephone tip and ring connection (carried on the innermost centre conductors of the RJ-11 cable) with an RCA phono jack that we can directly connect to the 64's audio line output. We last talked about tip-and-ring when we discussed how T1 lines work. On old exchanges like this one (used in rural New South Wales, Australia), phone lines were carried by literal tip-and-ring connectors plugged into the switchboard to connect calls, which is where the name comes from. Telco guys call a combination of tip and ring a "pair." Each phone line uses one pair.

Although it likely makes little difference for this application, there is a polarity to the connection which should be observed, i.e., tip is positive and ring is negative. The cable I used is a real USOC RJ-11C with a 6P2C connector and thus has only two wires for a single pair. The tip wire for this first pair on typical North American RJ-11 installations can be green, or white with a blue stripe (in other countries it may be any number of other colours); the ring wire can be red, blue, or blue with a white stripe. Thus, after stripping back the wires — sometimes easier said than done on a sticky old cable — connect ring to the phono jack's ground/sheath and tip to the phono jack's centre. Make it pretty and you're done.

An important warning before we continue: from the telephone company side the line pair carries voltage used to power the phone and ringer, so never plug this cable into a wall jack — doing so could potentially send up to 48 volts to the computer, with likely undesirable and even fiery results. A cable like this should only ever be directly connected to another modem.

The software part has to do with how data from the Commodore 64 is modulated to send to the other system's modem. For that, we turn to John's program, as he posted it (in separate versions for NTSC and PAL Commodores for reasons I'll explain as we analyse the disassembly). It was presented as a type-in program in BASIC, short enough to type in by hand, with an embedded machine language section loaded from DATA statements. Here's a couple videos showing what it looked like in practice. The modulated audio is played through the speaker, so don't have it up too high.

After the machine code is transferred to memory, a continuous tone starts playing and the program enters a basic terminal mode. As we type on the keyboard, we hear the tone "warble" as each bit of the character's ASCII value is modulated and played to the other end.

If you press F1, it will then give you the opportunity to send a file. This file is sent using Xmodem-CRC so the remote side can reliably detect errors, but since the transmission is one-direction the 64 side simply pretends everything succeeds, pauses briefly to let the other end send its acknowledge byte, and then goes onto the next block. You can hear the file itself being modulated in the audio also; around 1'10" the last block is sent, which is padded with ^Z and thus has a characteristic repeated sound (plus a single "blip" after it for the end-of-text character ^D). When the transmission is complete, the program ends.

The modulation scheme itself is very simple, and the phone dweebs reading this have already guessed what it is just from the audio.

Recall from earlier discussions, such as when we wrote a bitbanged serial transmission routine so the KIM-1 could speak through a DECtalk, that the transmission of a byte or character of data is divided into more or less distinct phases. The default state is mark (one). At the beginning of transmission comes a start bit (always space, or zero), followed by the data (seven or eight bits). After the data comes an optional parity bit, then back to the stop bit for at least one and sometimes two or more bit times. The most common transmission type is 8N1, which is eight data bits, no parity bit, and one stop bit (i.e., characters must be separated by no less than one stop bit, though it can be more).
If we load the audio up into an acoustic analysis program like Praat, we see a sine wave of varying wavelength. In the spectrogram at the bottom we can pick out two distinct frequencies being used to encode a character, as shown on the dark black band. This is the hallmark of audio frequency-shift keying (AFSK), or often just called FSK.

For this spectrogram I've typed the letter "U" which in binary is 01010101. That's the "wiggle" in the middle. John's program sends 8-N-1, so since we know the byte is framed by stop bits, which are marks/ones, we can deduce the initial frequency is used to transmit a one. Serial communications send the bits in little endian order, i.e., from least significant to most significant, meaning the wiggle is actually the start bit (space/zero), followed by 10, 10, 10, 10, then a stop bit (mark/one) and finally the normal mark state between bytes, which in this plot is indistinguishable from the stop bit.

By zooming in on the spectrogram we can quantify the frequencies more precisely. Eyeballing the centre of the black band during the stop bit, we get ~2226Hz.
Similarly, the centre of the black band during the start bit looks around 2054Hz. In fact, the frequencies are officially 2225Hz and 2025Hz, exactly 200 cycles apart, allowing easy distinguishability on an acoustic coupler or analogue phone lines of even poor quality. (Note that the band's thickness in these images can vary based on the analysis time slice used to compute the spectrogram.) These two frequencies represent the answer side of a Bell 101 or 103-compatible modulation scheme.

The Bell 101 was the first commercial modem (then called a "data set"), developed by AT&T in 1958 for the U.S. military's Semi-Automatic Ground Environment (SAGE) environment. SAGE integrated data from multiple radar sites into a common wide-area view used as part of NORAD's nuclear response capability and was at least in part the inspiration for Dr. Strangelove. It remained in operation well into the 1980s; even as just a giant coincidence there are many suspicious similarities between the concept and WarGames' WOPR. The 101 ran at 110 baud over regular telephone lines and became available for commercial sale in 1959. On the wire it uses separate sets of frequencies for each side of the conversation: 1070Hz and 1270Hz (space, mark) for the modem originating the call, and 2025Hz and 2225Hz (space, mark) for the modem answering the call.

In 1962 AT&T introduced the Bell 103, which used the same frequencies but ran over twice as fast at 300 baud. It quickly became very popular and almost completely replaced the 101 in commercial use. Even after the 1976 Bell 202 introduced 1200 baud operation (with different frequencies and duplex modes), it remained compatible with the 103 in 300 baud mode, and virtually every third-party 300 baud modem was compatible as well (many were also compatible with ITU-T V.21, which uses the same basic communication scheme but different frequency sets). The Originate-Answer switch on 300 baud modems like the Commodore 1600 VICMODEM and Commodore 1660 "Modem/300" selects which two frequencies the modem will send bits with, using the other two frequencies for receiving. Since any 300bps modem can speak Bell 103, that's why John chose it, and since we're "responding" to the other side that "initiated" the "call" this code uses answerer frequencies.

The software came in both NTSC and PAL versions because obviously something like this is highly timing-dependent, and PAL Commodore 64s run slightly slower (0.985250MHz) than NTSC systems (1.022730MHz). The variance is because each video standard uses a different master crystal from which all other clocks are obtained by dividing down, including the colourburst frequency needed for correct display, and also the clock speed of the CPU. This speed additionally affects the 6581 SID sound chip, since each of its three oscillators are incremented by the given audio value (0-65535) every clock cycle, so we need different values for the mark and space frequencies on PAL and NTSC systems. John's last known post in comp.sys.cbm, using slightly different processor speeds, explains the math (shown as written):

It might be a good idea if you ask specific questions, since we don't
know what info you already have. Regarding the low/high values for frequency
(for registers 0/1, 7/8, and 14/15), together they defines a 16-bit word:

Word = low_value + 256 * high_value

The frequency of the tone is proportional to that value:

Frequency = Word * F / 2^24

where F = system clock frequwency (1.022727 MHz for NTSC and 0.985249 MHz for
PAL). So we get:

Frequency = Word / A

where A = 16.4044 for NTSC and 17.0284 for PAL.

Using this relationship, we can then solve for "word" to get the proper value for the SID frequency register based on the detected video standard. SID's three voices are thus able to generate tones up to ~3848Hz on PAL machines and ~3995Hz on NTSC machines, well in excess of the necessary range. Because SID generates audio asynchronously, we can just tell it to use infinite sustain (to infinitely prolong the note until we gate it off), play the mark frequency, and then leave the note playing while we go do something else, keeping the line open. Since the specification requires a sinusoidal wave, the code uses the SID's triangle waveform which is the closest approximation. The result is, in fact, the very tone you hear at the beginning of the videos.

However, there's one other reason we need separate NTSC and PAL versions, and that's because of how John set up the baudrate. Here's how the BASIC loader starts (from the NTSC version):

10 poke55,.:poke56,160:clr:c=49152:b=1
20 ready$:print"reading line";b;"of 20"
30 fork=1to2:a$=mid$(y$,j*2+k)
40 a=asc(a$)-48:ifa>9thena=a-7
50 e=a+16*e:next:ife=256then90
60 pokej+i+c,e:n=n+e:j=j+1:e=0
70 ifj=15thenj=0:i=i+15:b=b+1:goto20
80 goto30
90 s=54272:ifn=38107then110
100 print"data statement error":end
110 d=56320:fori=stos+24:pokei,.:next
120 e=56576:pokee+4,80:z=d+13:pokez,127
130 pokee+5,13:pokee+13,127:pokee+15,8
140 pokes+6,240:pokes,148:pokes+1,142
150 pokes+24,6:pokee+11,.:pokee+14,1
160 poked+15,8:poked+11,.:pokes+4,17
170 sys49152:pokez,129:ifpeek(2)then190
180 end

After reading the hex-encoded DATA statements into memory, at line 110 John's code starts initializing both the SID and the two CIA chips, though he primarily uses CIA #2. To determine bit times, rather than having the CPU manually count off a specific number of clock cycles, this code has the CIA do it. A critical point is that while the CIA chips can be set to issue IRQs or not, their interrupt control registers will still indicate when they would have fired one, even if that individual interrupt condition is technically disabled. John turns off all interrupts on both CIAs so they won't fire and upset system timing, including the usual Timer A IRQ on CIA #1 used for keyscan, then sets Timer A on CIA #2 to repeatedly count down $0d50 (3408) clock cycles. If we divide 1022730 by 3408, we get ... 300.09, almost exactly our baud rate. (It's okay to be a bit faster as long as you're never slower.) A smaller value is used for PAL systems.

With the CIAs (and audio) set up, we then go into the mini-terminal, which is loaded into memory and started from the usual location for such routines at 49152 ($c000). We disassemble that next.

lc000   jsr $e518     ; Kernal cint without PAL/NTSC detect
lc003   lda $dc0d     ; wait until Timer A interrupt time
        beq lc003
lc008   jsr $ea87     ; Kernal scnkey
        lda $c6
        beq lc003     ; no key, go back to waiting
lc00f   jsr $f142     ; Kernal getin but always keyboard
        cmp #$85      ; F1
        beq lc024
lc016   cmp #$88      ; F7
        beq lc022
lc01a   jsr $e716     ; Kernal chrout to screen
        jsr lc027     ; modulate the character
        bcc lc003     ; go again
lc022   lda #$00
lc024   sta $02
        rts

John chose to call direct into the Kernal for these routines to short-circuit code he didn't need. With thousands of cycles available to send each single bit, the full-fat routines would have been fine, but why bother with work you don't need to do? After initializing the screen editor, the code waits for the next Timer A interval to fire and scans the keyboard manually (since the IRQ isn't running anymore), then fetches the next key, if there is one. Assuming it's not F1 (send a file) or F7 (quit the terminal), the code then goes on to send a character using this subroutine at $c027:

lc027   sta $b0       ; stash registers
        stx $b1
        sty $b2
        ldx $dd0d     ; clear ICR on CIA #2
lc030   ldx $dd0d     ; wait for Timer A CIA #2
        beq lc030

Each access on the interrupt control register clears any conditions that were set. This apparent "double-wait" on entering the routine isn't an error: it ensures not only that everything's in a known state, but that also at least one stop bit's interval has elapsed between the prior character and this one. Once that has occurred, we clock out the start bit, then eight data bits least significant first, and finally leave back at the stop bit frequency. Each time, except for the very end, we wait for another trigger on CIA #2's ICR before we proceed.

lc035   ldx #$81
        stx $d401
        ldx #$c3
        stx $d400     ; start bit = 0 = 2025 Hz
        ldy #$08
lc041   ldx $dd0d     ; wait for next interval
        beq lc041
        ; shift bits out
lc046   lsr
        bcs lc058
lc049   ldx #$81
        stx $d401
        ldx #$c3
        stx $d400     ; 0 bit = 2025 Hz
        dey
        bne lc041
lc056   beq lc065
lc058   ldx #$8e
        stx $d401
        ldx #$94
        stx $d400     ; 1 bit = 2225 Hz
        dey
        bne lc041
lc065   ldx $dd0d     ; wait one bit time on last bit
        beq lc065
lc06a   ldx #$8e
        stx $d401
        ldx #$94
        stx $d400     ; stop bit = 1 = 2225 Hz
        lda $b0
        ldx $b1
        ldy $b2       ; restore registers
        clc           ; jorb well done
        rts

When F1 or F7 is pressed, the mini-terminal sets location $2 to non-zero or zero respectively (above, after the call at $c00f) and returns to BASIC. BASIC then turns back on the Timer A IRQ on CIA #1, and if $2 is non-zero, it proceeds to ask for a device number and filename. This is a fun routine on its own, but you've seen enough of the code to understand the basics of how it works, so let's get out the WorkSlate now and try it with a real device.

For the Commodore side, we're going to use one of my portable SX-64 systems. A warning about the SX-64 specifically: never plug in a video cable — more specifically, never connect the audio output — with the computer's power on. Doing so runs you a decent chance of frying the SID, something I actually did many years ago and is a well-known problem. This goes likewise for connecting our mutant phone cable to the SX-64, since we necessarily have to use the computer's video port for the audio signal.

The WorkSlate has a built-in terminal desk accessory which can be activated from the Phone menu and selecting Terminal. However, simply selecting the Terminal is not enough. The trick with the WorkSlate is to have the speakerphone line open (Phone, SpkPhone) first, and then try to answer with the Terminal. This is supported by the device; it assumes in this case that you've manually dialed a number somehow (say, from an attached phone handset) and the computer on the other end has answered. We already have the answer stop bit tone playing, so the WorkSlate's modem immediately hears it and tries to go on-line.

And here we are! John's code indeed works as written and what we type on the SX-64 appears on the WorkSlate. The initial call to Kernal CINT resets the Commodore to uppercase/graphics mode, so we don't need a PETSCII-to-ASCII conversion step because unshifted letters we type on the Commodore come out as uppercase on both sides. If you were doing this with a Hayes-compatible modem connected to the C64, a command like ATX1D sets up a "blind dial" so that the 64's answer signal is also immediately recognized.

The interesting part is comparing how the speakerphone operated between the three Workslates I now have. On the most recent one I acquired and on my "tester" unit that I soldered jumper probes to (both with serial numbers starting with CCA8415), I could hear the "call" and what the SX-64 was sending when the Workslate was in speakerphone mode, as expected. However, on my regular unit (a later machine with a CCA8417 serial number), I could hear both ends of the conversation through the SX-64's speaker, including the Workslate's dial tones and originate frequencies — and nothing on the Workslate's speaker. I'm not sure if this is due to different internal wiring, changes in the tape gate array or both. Again, this is a good reminder that the SID in the SX-64 is unusually vulnerable to stray voltages: if there were proper isolation I shouldn't have been able to hear incoming audio through the speaker output. In fairness, Commodore probably didn't think people would be wiring phone lines to SID audio either.

But let's embroider the situation a little more. Some modems may listen for a dial tone first before they attempt to do anything, especially if you need to actually dial a phony (narf narf narf) telephone number, since they reasonably expect there's a real POTS "plain old telephone service" line on the other side.

Indeed, if you try to manually dial a number on the WorkSlate ...
... there's no dial tone, so it fails, and the connection is dropped.

Fortunately, the SID is perfectly capable of synthesizing other in-band telephony signals. For example, the Modem/300 came with cables that hooked up to the SID's audio output, and instead of the modem sending out DTMF touch tones to dial numbers, the computer did. Programs like Common Sense could be provided a phone number and dial it by playing tones like music. (Interestingly, the VIC-20 does not seem to be capable of precise enough frequency control to generate DTMF; Commodore even warns against it in the Modem/300 manual.)

Dialtones and other call-progress tones are often multi-frequency tones similar to DTMF, but they're specified separately by each region's telephone system. In the North American Bell System's Precise Tone Plan, dialtone is a combination of 350Hz and 440Hz at -13dBm, also played using a sine wave. If we use the formulas above and solve for the SID register values using those frequencies, these statements in BASIC will make a sufficient approximation of a dialtone on SID voices 1 and 2 (NTSC):

fori=54272to54296:pokei,0:next
poke54272,49:poke54273,28
poke54279,109:poke54280,22
poke54278,240:poke54285,240
poke54276,17:poke54283,17
poke54296,6
With the dial tone playing, the dial function will treat the line as a true phone line and try to dial out through it. On the WorkSlate this drops us cleanly into the speakerphone and should offer a working alternative for virtually any modem.

The last thing to do is tie this all together. While John's original program was written as a quick, type-in-able means to get files transferred one way from a Commodore 64 to a larger system, here we're using it more as an experimental way to allow the Commmodore to talk to a smaller system, so let's make it a little friendlier. I removed the BASIC portion and wrote up a new menu system in pure assembly, incorporating and converting John's original code, and merging the PAL and NTSC versions together. It LOADs and RUNs like a BASIC program but is fully machine language.

This version can synthesize both a dial tone and the answer frequency, and I also added an ASCII translation table so that you send true ASCII in the mini-terminal.
I also kept John's Xmodem-CRC code and added a simple transmit routine that sends a file straight up with no protocol, since the WorkSlate doesn't speak any file transfer protocols internally. Vanilla Xmodem, written by Ward Christensen, uses a fixed 128 data bytes per block and a simple checksum with known deficiencies, so John opted for the more complex version with a cyclic redundancy check to ensure errors could be promptly detected. Most terminal programs support this mode. We previously encountered a variant of Xmodem-CRC when we were figuring out how The Newsroom's Wire Service operated. From that article, the CRC-16-CCITT used in Xmodem-CRC is transmitted using this algorithm, rendered in K&R C:

/*
 * This	function calculates the	CRC used by the	XMODEM/CRC Protocol
 * The first argument is a pointer to the message block.
 * The second argument is the number of	bytes in the message block.
 * The function	returns	an integer which contains the CRC.
 * The low order 16 bits are the coefficients of the CRC.
 */
int calcrc(ptr,	count)
char *ptr;
int count;
{
    int	crc, i;

    crc	= 0;
    while (--count >= 0) {
	   crc = crc ^ (int)*ptr++ << 8;
	   for (i = 0; i < 8; ++i)
           if (crc & 0x8000)
               crc = crc << 1 ^ 0x1021;
           else
               crc = crc << 1;
	}
    return (crc & 0xFFFF);
}

Many implementations of this algorithm use a lookup table for computation, but not John, who instead was optimizing for code size. Here's how John's routine sends an Xmodem-CRC packet (we'll assume that a file has already been opened by the Kernal and set up for reading).

        lda #$01
        sta $96       ; number of current packet
        ; start sending the current Xmodem packet
lc08a   lda #$00
        sta $02
        lda #$01
        jsr lc027     ; send Xmodem SOH $01
        lda $96
        jsr lc027     ; send packet number
        eor #$ff
        jsr lc027     ; send inverse of packet number
        ldy #$03
        jsr $ffa5     ; Kernal acptr, read next byte from file
        sta $8b       ; store in high byte of CRC
        jsr lc027     ; transmit it
        inc $02
        ldx $90       ; EOF?
        beq lc0b3
lc0ad   lda #$1a      ; yes, handle final packet, store a ^Z
        sta $8c
        bne lc115
lc0b3   jsr $ffa5     ; read again
        sta $8c       ; store in low byte of CRC
        jsr lc027     ; transmit again
        ldx $90
        bne lc115     ; check EOF again
lc0bf   inc $02

The routine keeps a running tally of the number of data bytes in the current Xmodem block in location $2, the current packet number (0-255) in $96, and the running CRC-16 in $8b/$8c. Notably, John maintains this value big-endian, unlike the usual 6502 little-endian convention, and exploits the fact that most transmitted blocks will have at least two data bytes. The routine starts each block by clearing the count, then with the modulation routine at $c027 above it sends the standard Xmodem start-of-header (^A) character, the packet number and inverse of packet number, then (checking for EOF each time) reads two characters into the high byte and low byte of the running CRC-16 and transmits them. If the status word at $90 shows an EOF, this condition remains until the file is closed.

For each byte after that to complete the block, another one is read and stored into $8d, then shifted into $8b and $8c:

lc0c1   jsr $ffa5     ; read again
lc0c4   jsr lc027     ; transmit again
lc0c7   sta $8d       ; hold this byte
        ; for eight bits, compute crc, rolling newest byte in
        ldx #$08
lc0cb   asl $8d
        rol $8c
        rol $8b       ; << 1
        bcc lc0df     ; if (crc & 0x8000)
lc0d3   lda $8b       ; crc ^= 0x1021
        eor #$10
        sta $8b
        lda $8c
        eor #$21
        sta $8c
lc0df   dex
        bne lc0cb

When a high bit is rolled out of the rolling CRC-16, this is detected as carry being set (no need for a bitmask) and the rolling CRC-16 bytes are exclusive-ORed with the required polynomial value ($1021, 4129). This is a very efficient translation of the algorithm.

The code then continues to run to complete the block of 128 bytes.

lc0e2   lda $90
        bne lc115     ; check EOF again
lc0e6   inc $02
        bpl lc0c1     ; if not 128 bytes, get some more
        ; packet's done
lc0ea   txa           ; x is zero, so zero accumulator
        dey           ; flush the header as zeroes into CRC
        bne lc0c7
lc0ee   inc $96       ; increment packet number
        lda $8b       ; send CRC high byte first
        jsr lc027
        lda $8c
        jsr lc027
        ; delay briefly to wait for the ACK $06 to go by
lc0fa   inx
        txa
        pha
        pla
        tax
        bne lc0fa
lc101   iny
        bne lc0fa
        ; pretend it worked
lc104   lda $90
        beq lc08a     ; if not EOF, go back and do another packet

After the last byte is read from the file and shifted in, we need to incorporate three zero bytes into the CRC-16 to represent the header we sent. We then send that value and "wait" to pretend it worked, then go back for the next block. The weird delay routine allowed John to fit the entire loop into the maximum 7-bit displacement of a relative branch instruction. In fact, when I added code to flash the border on each block, I had to insert an absolute jump instead since those three extra bytes upset the apple cart.

At the very end of the file, any block in progress is padded with EOT (^Z), and then Xmodem EOF (^D) is sent to terminate the transmission:

        ; EOF, send EOT
lc108   lda #$04
        jsr lc027       ; send Xmodem EOT $04
        lda #$81
        sta $dc0d       ; turn CIA #1 interrupts back on
        jmp $ffcc       ; Kernal clrchn and exit back to BASIC
        ; handle end of file
        ; basically fill the last packet up with ^Z
lc115   inc $02
        bmi lc0ea       ; it's full
lc119   lda #$1a
        bne lc0c4       ; it's not, send and stuff ^Z

Note that the branch at $c11b will always be taken since we just loaded a non-zero immediate into the accumulator. Again, another nice way of increasing code density and reducing type-in size. Should the file have ended in the first two bytes used to prime the CRC-16, John's code just stuffs ^Zs into it manually.

This video shows the unified, enhanced version in action. When it starts up, it detects the correct video standard (or you can force it), and allows you to send a dialtone or go directly to an answer tone. Once the "softmodem" has answered, you can then enter the terminal or upload files. You can enter and exit the terminal or file transfer as desired, or simply "hang up" when you're done, which turns off the SID's output and causes any connected modem to perceive loss of carrier and terminate the link.

I've put John's original post containing the type-in versions (you can cut and paste these into VICE, if you like), plus the assembly source for this unified version and a pre-built binary, on Github. As John never asserted copyright to his programs and explicitly intended them to be freely distributable so that others could use and learn from them, I've placed this version into the public domain (to the extent available in your jurisdiction). You can assemble it using xa65.

John was a good guy with a clever programming style and it was nice to see his code running again (and working, though that was a given). Plus, this is a great use for a Commodore to support your other systems, and a roadmap for doing something similar on other machines with sufficiently capable sound hardware. In future articles I think we'll explore a few other things he wrote, including that audio digitizer. I think he would have enjoyed it.

No comments:

Post a Comment

Comments are subject to moderation. Be nice.