300 Baud · engineering notebook

The engineering notebook

What is actually going on inside the machine, why it is built the way it is, and the measurements that settled the arguments. Everything here is checkable: the numbers came out of the test suite or out of experiments you can re-run.

The detector

What the detector actually computes

For each of the two tones, the incoming signal is multiplied by a sine and a cosine at that frequency, and both products are summed over exactly one bit period. That gives a complex number per tone. Its squared magnitude is how much energy the signal holds at that frequency over that window, and the difference between the two is the decision: positive means mark, negative means space.

Multiplying by both a sine and a cosine is what makes it work regardless of where in the tone's cycle the window happens to start. Use only one and the answer would swing with the phase, and the detector would read a perfectly good signal as garbage half the time. That pair is called quadrature, and it is the reason no phase recovery is needed anywhere in this code.

Mixing and then taking a moving sum is the same arithmetic as sliding a correlator along the signal, but it costs a fixed amount of work per sample instead of one multiply per sample per window position. At 48 kHz that is the difference between comfortable and unusable.

Why a flat window beats a tapered one, measured

A panel of four language models, asked what was wrong with this detector, said it suffered from spectral leakage and wanted a tapered window. That is a testable claim, so it was tested: character error rate against signal-to-noise ratio, rectangular window against Hann, the same seeded noise for both, 54 characters, five seeds each.

SNRRectangularHann
0 dB0 errors0 errors
-2 dB00
-3 dB03
-4 dB07
-6 dB233

The flat window is not merely adequate, it is better by a wide margin, and it should be. A flat sum over exactly one bit period is the matched filter for a constant-amplitude tone burst, and a matched filter maximises signal-to-noise ratio by construction. Tapering throws away signal energy at the edges of the window to buy rejection of distant frequencies, and the only competing tone here is 200 Hz away and already outside the main lobe. There is nothing to buy.

The general point is worth more than the specific result. An expert opinion about code is a hypothesis. This one took ten minutes to falsify.

How far down it still reads

The same experiment gives the operating range. Over a 54 character message the detector recovers every character cleanly at 0 dB, where the noise is exactly as loud as the signal, and only begins dropping characters below about minus three. The line quality control on the front page lets you hear that happen: it adds Gaussian noise to the audio before it is played and before it is demodulated, so what reaches your ears is what the detector is up against.

Finding a character without a clock

Why the half-window lag cancels

The detector's output at sample n describes the window ending at n, so it lags the signal by half a window. That looks like it should need correcting, and it does not, because the lag applies twice and cancels.

The start bit is found by watching for the decision to cross from positive to negative. That crossing happens when the window is half full of space, which is half a window after the transition really occurred. So the edge is late by half a window. The instants where data bits should be sampled are also read from the same lagged signal, so they need to be late by half a window too. Both offsets are the same size and the same sign, so reading the decision at the edge plus one and a half bit periods lands exactly on the centre of the first data bit. No correction term appears anywhere in the code, and that is why.

Two bugs about starting from cold

The first character of every transmission was being decoded out of a window that was only half full of signal, because the detector began clocking before it had a full bit period of audio behind it. The fix is to wait, which is what a real receiver does.

Carrier returning after a silence has exactly the same problem again: the window straddles the silence that came before. That is why there is a carrier detect gate that waits for a full window of continuous energy before the detector is believed. It measures total energy rather than the mark-minus-space difference, because that difference swings through zero every time the data changes, and a dropout detector built on it would fire on ordinary text.

The bug only an end-to-end test could find

Every unit test passed while the page was broken. Audio is modulated ten bytes at a time so a long screen starts arriving immediately rather than after the page has built two minutes of sound, and each chunk was being modulated from phase zero. That puts a step in the waveform at every buffer boundary: precisely the discontinuity the continuous-phase rule exists to prevent, moved from bit boundaries to buffer boundaries. The detector's window straddles the step and the characters around each join come out wrong.

No unit test could see it, because each of them modulated one buffer. It took a test that carries a whole session through real audio in deliberately awkward block sizes. The lesson generalises: a rule enforced inside a function is not enforced across calls to it.

Why there is no 1200 in the menu

The arithmetic that rules it out

Mark and space are 200 Hz apart. Telling two frequencies that close apart requires observing them for long enough to resolve 200 Hz, which means a window of very roughly one over 200 of a second, about five milliseconds. At 300 bits per second a bit lasts 3.3 milliseconds and the detector gets most of what it needs. At 1200 bits per second a bit lasts 0.83 milliseconds, nowhere near enough, and the two tones become indistinguishable.

This is not a shortcoming of this implementation. It is the reason the industry abandoned frequency-shift keying to go faster. A Bell 212A at 1200 bps does not shift frequency at all: it holds one carrier and shifts its phase, four phases carrying two bits at a time, so it moves 1200 bits per second while only changing symbol 600 times a second. The page offers 300 and 110, which are speeds a Bell 103 line actually ran, and a test asserts that 1200 stays broken so nobody restores it to the menu by mistake.

The handshake, in order

What happens between dialling and CONNECT

The answering modem raises its own mark tone, 2225 Hz, and sits on it. That tone does three jobs at once: it disables the echo suppressors the telephone network used on long-distance circuits, it tells the calling modem that something speaking Bell 103 picked up, and it is already the carrier that will hold for the rest of the call. It is not a separate signal, which is why this page generates it from the same transmitter that produces the carrier afterwards, with no seam where they meet.

The calling modem answers with its own mark tone at 1270 Hz, in the other half of the band, and from then on both tones are present continuously until somebody hangs up. Data is those tones moving. An idle line is them standing still.

S9, and why the speaker cuts when it does

A modem does not trust a carrier the instant it appears. Register S9, Carrier Detect Response Time, is the number of tenths of a second the carrier must be present before the modem accepts it and raises carrier detect, and it left the factory set to 6. Six tenths of a second.

That single number is why a real modem's speaker cut out part-way through the answer tone rather than at the end of it. The default speaker setting, ATM1, means audible until carrier detect, so the moment S9 elapsed the monitor went quiet and the rest of the tone played to nobody. This page had that wrong at first: it reported CONNECT at the end of the tone, so you sat listening to a second of handshake after the call was already up. It now waits S9 and no longer.

Its mirror image is S10, Lost Carrier to Hang-up Delay, which shipped at 14, or 1.4 seconds. The manuals warn that S10 must always exceed S9, because otherwise a momentary dropout satisfies the hang-up condition before it can satisfy the detect condition and the modem drops calls it should keep. Both registers are readable and settable here: try ATS9?, or ATS9=25 to make the modem hang on for two and a half seconds before it believes anything.

Dialling, and why zero takes longest

Touch tones are two frequencies at once, one from a row and one from a column of a four by four grid. Two simultaneous tones is what makes them impossible to trigger by accident: no single voice or whistle lands on two grid frequencies at the same moment, which was the whole design problem.

Pulse dialling contains no tone whatsoever. The line is interrupted, once per unit, ten times a second, and the exchange counts the interruptions. A rotary dial is a clockwork timer for doing exactly that, and zero is encoded as ten pulses rather than none, which is why it takes a full second to dial and why phone numbers full of zeros were unpopular. Both are generated here from that description rather than from recordings, and the tests measure the audio to confirm the frequencies are where they should be.

The board, and getting files off it

Why the screen is code page 437

Everything above character 127 in a BBS screen is box drawing, shading and block characters from the IBM PC's character set. That set is the entire visual vocabulary of the form. A terminal that treats those bytes as Latin-1 renders every screen as accented gibberish, so the terminal here implements the real mapping. The corner that starts most BBS frames is byte 201, which is a double-line top-left corner in code page 437 and a capital E with an acute accent in Latin-1.

Why CRC replaced the checksum in XMODEM

XMODEM shipped with a one-byte checksum: add up the 128 data bytes and keep the low byte. It catches a great deal, and it misses anything that leaves the sum unchanged. The clearest case is two bytes swapping places, which alters the data and not the total at all.

The test suite demonstrates exactly that: two byte arrays that differ only in order produce an identical checksum and different CRC-16 values. Both schemes are implemented here, because a receiver that only spoke checksum was still a thing you met in 1987, and the receiver chooses which by sending either NAK or the letter C to open the transfer.

Sources

Back to the modem