ZX81: How Programs Go to Tape
The ZX81 has no disk drive and no memory that survives switching off. When you pull the plug, your
program is gone. To keep it, you connect the computer to an ordinary audio cassette recorder,
the same kind you would play music on, type SAVE "NAME", and press Record. Minutes later the
program is on the tape as sound. To get it back, you type LOAD "NAME" and press Play.
This article explains how that works, from the magnetic tape up to the last instruction of the ROM. As in How the Screen Is Drawn, you need to know the Z80 and have some engineering basics. You do not need to know anything about tape recorders: we start there.
Like the picture, the tape interface is a lesson in doing a lot with very little. There is no
tape chip: the ULA gives the program one input bit and one output level, and the rest is
software, a few hundred bytes of ROM. The CPU makes every pulse of a recording with its own IN
and OUT instructions and counts their length by executing loops of known duration. Reading is the
same in reverse: the CPU samples one bit tens of thousands of times a second, and decides what it
heard by counting. The format it uses is slow, but it is remarkably hard to get wrong, and you will
see why.
How this article is organised
- The foundations: how a cassette recorder stores sound, and why that matters to a computer (Part 1), and the hardware the ZX81 uses for its tape (Part 2).
- The format: how bits become sound (Part 3), and what a
recording holds and what a
.Pfile is (Part 4). - The ROM: the
SAVEroutine (Part 5) and theLOADroutine (Part 6), with every instruction explained. Part 7 collects the practical consequences of the design. - The emulator (Part 8): how Klive plays a tape,
how it records a
SAVEas a.Pfile, and how it can skip the waiting. A table of all the tape routines and a Summary close the article.
Every complete ROM listing here follows the real ROM byte for byte; the names in it are this article’s own. The timings are counted from the listings, in T-states of the 3.25 MHz Z80 (one T-state is about 0.31 µs).
Everything below is how the real machine works and how Klive emulates it. Klive runs the ROM’s
own SAVE and LOAD code on its Z80; for loading, it feeds the ULA’s tape input a signal built
from the program file, pulse by pulse, and for saving, it records the bytes the ROM sends as a .P
file. The last part shows where each piece lives in the source.
Part 1: How a cassette recorder works
Magnetism on a ribbon
A cassette holds a long, thin ribbon of plastic, the tape, coated on one side with a layer of tiny iron-oxide particles. Each particle behaves like a minute bar magnet. On a blank or erased tape they point every which way, and their fields cancel out. Recording lines them up: wherever many particles point the same way, that stretch of tape becomes a weak magnet, and it stays one for years.
The thing that lines them up is the head: a ring of soft iron with a coil of wire wound round it and a very narrow gap cut into the ring where it touches the tape. A current through the coil makes a magnetic field in the ring. At the gap the field has nowhere to go but out, so it bulges into the tape running past and magnetizes whatever is under the gap at that instant, more strongly the stronger the current, one way or the other depending on its direction.
So recording turns time into position, just as a television turns time into position across the screen. Whatever current flows through the head now is written onto the bit of tape under the gap now; a moment later that bit has moved on, and the next one gets the next moment.
Reading it back: the head sees changes, not levels
To play the tape, the same kind of head is used the other way round. The magnetized tape moving past the gap makes a magnetic field flow round the ring and through the coil. But a coil does not produce a voltage from a field; it produces one from a changing field. That is Faraday’s law of induction: the voltage is proportional to how fast the field through the coil changes.
This has a consequence that every tape format has to live with. Suppose a computer records a clean square wave: the current jumps up, stays, jumps down, stays. On the tape, that is long stretches of “magnetized this way” and “magnetized that way”. On playback, a long stretch of constant magnetization produces no voltage at all. Only the boundaries, where the magnetization flips, produce a pulse: a spike up for one direction of flip, a spike down for the other.
The spikes are also blurred. The gap has a width, so it reads a small stretch of tape at once and smears fast changes together; the tape’s particles are not perfect; and the recorder’s amplifiers are built for music, not for square waves. A cheap recorder of the time handled frequencies up to roughly 10 kHz and filtered out the rest. What reaches the earphone socket is a wobbly analogue wave that only resembles what was recorded.
A real recorder does more to make music sound good. It erases the tape with a separate head just before the recording head, so old sound does not mix with new; and it adds a strong, inaudible high-frequency bias to the recording current, which makes the particles respond more evenly. None of that changes the picture above: a computer gets back the changes in what it recorded, blurred.
Moving the tape at a steady speed
Since time is position, the tape must move past the head at a constant speed, or the playback runs fast or slow. A compact cassette runs at 4.76 cm/s (1⅞ inches a second). The reels do not set that speed: as the tape winds from one reel to the other their diameters keep changing. A spinning steel shaft, the capstan, does. A rubber pinch roller presses the tape against it, and the capstan pulls the tape past the heads at its own steady speed. The take-up reel only winds up what the capstan delivers.
No mechanism is perfect. The capstan is never exactly round, the motor’s speed drifts with the battery and the temperature, and two recorders never run at quite the same speed. Slow speed wobble is called wow, fast wobble flutter, and a tape recorded on one machine and played on another can easily run a few per cent fast or slow. There is one more classic trap: the head’s gap must stand exactly at right angles to the tape (its azimuth). If the heads of two recorders are tilted differently, the second one reads every edge smeared across the gap, and the high frequencies fade away. “It loads on my recorder but not on yours” was very often azimuth.
What this means for a computer
So a cassette recorder gives a computer a channel that:
- returns changes, not levels: spikes at the edges rather than the square wave that was sent;
- blurs fast detail: anything much above 10 kHz is lost;
- does not keep time: the playback speed is a few per cent off, and wobbles;
- has an unknown volume and polarity: the user turns the volume knob, and some recorders invert the signal.
A tape format that works on cheap recorders has to shrug off all four. Hold on to that list: the ZX81’s format answers every item on it.
Part 2: The players
| Player | Its job on tape |
|---|---|
| The cassette recorder | Any audio recorder. Its MIC (microphone) input records what the computer sends; its EAR (earphone) output plays it back. The ZX81 has no motor control, so you press Play and Record yourself. |
| The ZX81’s MIC socket | The computer’s output to the recorder. It is driven by the same ULA pin that makes the video signal, so whatever the CPU sends to tape also reaches the television: that is why the screen fills with stripes during SAVE. |
| The ZX81’s EAR socket | The computer’s input from the recorder. The ULA turns the incoming audio into one bit: bit 7 of port $FE, high while there is signal. |
| The Z80 CPU | Does all the work. It makes each pulse with OUT and IN instructions and times it with delay loops; it reads by sampling bit 7 of port $FE in loops of known length, and counts. |
| The 8K ROM | Holds the SAVE and LOAD routines: about 240 bytes at $02F6–$03E4, plus a few helpers. |
One output level, one input bit
The ZX81’s output to tape is not a separate signal. The display article explained that, while the
NMI generator is off, an IN from an even port starts VSYNC (the ULA’s output drops to the sync
level) and any OUT ends it. During SAVE the NMI generator is off, so the CPU can switch the
ULA’s output between two levels with nothing but IN and OUT, and that output is what reaches the
MIC socket.
The input is just as plain. Reading port $FE returns the keyboard in bits 0–4, and in bit 7 the
tape signal from the EAR socket: 1 while the audio is above the ULA’s threshold, 0 while it is not.
Reading it is the same IN instruction that reads the keys.
So all the ZX81 offers its software is one level to switch and one bit to look at. Everything else, the speed, the format, the error handling, is in the ROM.
Why the picture breaks up. Both SAVE and LOAD need every T-state of the CPU for timing, so
the ROM switches the display off first, exactly as FAST does: the routine that does it ($02E7)
is the one the display article explains in
Starting and stopping the loop. With the display stopped, the ULA’s video
pin carries only what the tape routines do with IN and OUT. During SAVE that is the tape
signal itself; during LOAD it is the rhythm of the sampling loop, which changes when a signal
arrives. Either way, the television shows moving stripes: the sound made visible.
Part 3: Bits as bursts of sound
Bursts and silences
The ZX81 does not encode its bits in the shape or the timing of individual edges. It sends bursts of a tone, separated by silences:
- A wave is one high stretch and one low stretch of the output, about 300 µs in all: a tone of about 3.3 kHz, in the middle of what any recorder can handle.
- A 0 bit is a burst of 4 waves; a 1 bit is a burst of 9 waves.
- After every bit’s burst comes a silence of about 1.5 ms, the output held low.
A byte is its 8 bits, the most significant bit first. There is nothing between bytes but the silence that follows every bit.
Why bursts?
Go back to the four problems of a cassette recorder in Part 1. The ZX81’s loader never looks at an edge, a wave or a level. It asks one question, over and over: “is there sound right now?”, and it measures how long the sound lasts. Then:
- Changes, not levels. The spikes that come back at every edge are sound. A burst of spikes is a burst of sound, just as long as the burst that was recorded.
- Blurring. A 3.3 kHz tone survives any recorder. Even a smeared, rounded burst is still a burst.
- Speed. A 1 bit’s burst is more than twice as long as a 0 bit’s, and the loader’s dividing line sits between them with a wide margin (Part 6 counts it): the tape can run very fast or very slow and still load.
- Volume and polarity. An inverted signal is still sound, and so is a quiet or a loud one, as long as it gets above the input’s threshold.
The price is speed. A 0 bit takes about 2.55 ms and a 1 bit about 4.05 ms, so the ZX81 manages about 390 bits a second for zeros and 250 for ones, roughly 40 bytes a second for mixed data. These are the figures usually quoted for the machine, for example in Wikipedia’s ZX81 article (400 and 250 bits a second). A 16K program takes several minutes. Sinclair chose robustness over speed, and for a machine sold with whatever recorder the customer happened to own, that was the right choice.
Part 4: What is on the tape
A recording
A SAVE writes four things, one after the other:
- About 5 seconds of silence. It gives the recorder time to get up to speed and lets you find the start of a program by ear. More importantly, a long silence is how the loader recognises that a program starts (Part 6).
- The name, one byte per character in the ZX81’s own character codes (not ASCII). Its last character has bit 7 set, which marks the end of the name. That is the only length information it has.
- The memory from
$4009up to the byte beforeE_LINE: most of the system variables, the BASIC program, the display file and the variables, exactly as they are in RAM. - Nothing else. There is no header, no length, no checksum. The end of the data is found the
way it is found in RAM: by the value of
E_LINEinside the data.
A snapshot, not a file
That third item deserves a closer look, because it makes the ZX81’s recording quite unlike a file on a modern computer. The ROM does not pick out “the program”. It saves a contiguous slice of memory, and the ZX81 keeps everything a BASIC session needs in that slice, in this order:
| Address | What it is |
|---|---|
$4009–$407C | The system variables from VERSN on: 116 bytes, among them the pointers D_FILE, VARS and E_LINE, the line to run next (NXTLIN), and the display mode (CDFLAG). |
$407D… | The BASIC program, line by line. |
D_FILE… | The display file: the screen. |
VARS… | The variables, ending with an $80 byte. |
E_LINE | The line being edited. Not saved: it marks the end. |
Loading puts every one of those bytes back at the same address. So LOAD restores the whole
state of the session: the program, every variable with its value, the screen as it was, and the
system variables that point to all of them. Three of those system variables produce well-known
effects, which Part 7 explains: NXTLIN makes self-starting
programs, CDFLAG brings back FAST or SLOW mode, and the display file makes the old screen reappear.
The nine bytes below $4009 are not saved, and for a good reason. They describe this machine and
this moment: the error code (ERR_NR), the interpreter’s flags, the machine stack pointer
(ERR_SP), the top of the memory BASIC may use (RAMTOP), and the line being run (PPC). A program
saved on a 16K machine must not tell a 1K machine that it has 16K of RAM.
The .P file
Emulators store ZX81 programs in .P files (also named .81), and the format is as simple as it
gets: a .P file is the bytes the ROM saves after the name, from $4009 to E_LINE − 1, with
nothing added. Its first byte is VERSN (always 0 on a ZX81), and the word at offset 11 (address
$4014) is E_LINE, which says where the file ends: a valid file is E_LINE − $4009 bytes long.
A .P file has no name. Klive puts a one-byte name in front of it when it plays one, $80, which is
a single inverted space with its “last character” bit set: LOAD "" accepts any name, so it accepts
that one.
The ZX80 has its own format, the .O (or .80) file. The ZX80’s 4K ROM saves memory from
$4000, with no name at all, and its routines are different. This article stays with the ZX81.
Part 5: SAVE
Saving is the simpler half, because the CPU is in charge of the timing: it just has to make the right pulses at the right moments.
The name
SAVE and LOAD start with the same helper at $03A8, which we call Name. It evaluates the
string after the command and gets it ready to go to tape:
; $03A8: evaluate and prepare the name of SAVE and LOAD
; Out: DE = the name's first character, HL = its last
; carry set if the name is empty
Name:
call $0F55 ; evaluate the expression after the command
ld a,($4001) ; A = FLAGS
add a,a ; bit 6 (string or number) to the sign,
; bit 7 (checking or running) to carry
jp m,$0D9A ; a number, not a string: report C
pop hl ; HL = the caller's return address
ret nc ; only checking the syntax: return past the caller
push hl ; running: put it back
call SetFast ; $02E7: stop the display (as FAST does)
call $13F8 ; DE = the string's start, BC = its length
ld h,d
ld l,e ; HL = the string's start
dec c ; C = length - 1 ...
ret m ; ... negative: empty, return with carry set
add hl,bc ; HL = the last character (clears carry)
set 7,(hl) ; mark it as the last one
retA few things in this short routine are worth a second look:
- The same code checks syntax and runs. The ZX81 checks each BASIC line when you enter it, by
running the same routines with a flag saying “only check”. In that case
RET NCreturns not toSAVEbut to whoever calledSAVE, because thePOP HLhas removedSAVE’s return address: the name is checked, and nothing else happens. - The display stops here. The call to
SetFast($02E7, see Starting and stopping the loop) waits for the next border NMI and switches the NMI generator off, so from here on the CPU belongs to the tape routines, and theIN/OUTVSYNC switch of Part 2 works. - The end marker goes into the string itself.
SET 7,(HL)changes the last character of the name in the calculator’s workspace, so thatSAVEcan send the name exactly as it is in memory. - Carry survives to say “empty”.
INC/DECon 8-bit registers do not touch carry, and neither does anything after theADD A,A: when the name is empty, the carry set by “running” comes back to the caller unchanged. A non-empty name leaves through theADD HL,BC, which clears it.
DEC C / RET M looks only at the low byte of the length and calls it “negative” when bit 7 is
set. So a name of 129 to 256 characters is treated as an empty one, a corner the ROM’s authors could
safely leave alone.
The leader and the bytes
Here is SAVE itself:
; $02F6: SAVE "name"
Save:
call Name ; DE = the name, HL = its last character
jr c,$02F4 ; empty name: report F
ex de,hl ; HL = the name's first character
ld de,$12CB ; 4811 rounds of the leader loop: about 5 s
Leader:
call Break1 ; read the SPACE key (and start VSYNC)
jr nc,$0332 ; BREAK pressed: report D
Delay:
djnz Delay ; 256 x 13 T-states
dec de
ld a,d
or e
jr nz,Leader
SaveName:
call OutByte ; send one character of the name
bit 7,(hl) ; was it the last one?
inc hl
jr z,SaveName
ld hl,$4009 ; from VERSN on
SaveData:
call OutByte ; send one byte of memory
call NextByte ; $01FC: next byte; leave when HL reaches E_LINE
jr SaveDataThe report codes are a byte after a RST $08: at $02F4 the ROM holds RST $08 / DEFB $0E,
which prints report F (“no program name”).
The leader is silence, made by an IN. Break1 ($0F46) is the ROM’s BREAK test, and it reads
the keyboard row that holds SPACE with IN A,($FE). With the NMI generator off, that IN starts
VSYNC, and no OUT follows to end it, so the output to tape sits at the sync level for the whole
leader: about 5 seconds of silence. The loop takes 3,448 T-states a round, 4,811 rounds: 16.6
million T-states, 5.1 seconds. Checking BREAK in every round means you can stop a SAVE at any time.
The name and the data are sent the same way, one byte at a time by OutByte. The name loop stops
after the character with bit 7 set. The data loop starts at $4009 and leaves through $01FC:
; $01FC: step to the next byte of a SAVE or LOAD; finish at E_LINE
NextByte:
inc hl ; the next address
ex de,hl
ld hl,($4014) ; HL = E_LINE
scf
sbc hl,de ; E_LINE - address - 1
ex de,hl ; HL = the address again
ret nc ; address < E_LINE: carry on
pop hl ; done: drop the return into the loop ...
; ... and fall into $0207, which restarts
; the display in SLOW mode, then returns
; from SAVE or LOADWhen the address reaches E_LINE, POP HL throws away the return into the byte loop, and the code
runs straight on into the routine at $0207, the one that starts SLOW mode (the display article
explains it in Starting and stopping the loop). That routine restarts the picture if the machine is in SLOW
mode and returns, and since the loop’s return address is gone, it returns from the SAVE command
itself. The same routine ends LOAD.
One byte, eight bursts
OutByte is the heart of SAVE. It sends the byte at (HL), most significant bit first:
; $031E: send the byte at (HL)
OutByte:
ld e,(hl) ; E = the byte
scf ; a 1 that follows the bits as a marker
OutBit:
rl e ; next bit to carry; the marker moves in
ret z ; E = 0: only the marker was left, done
sbc a,a ; A = $FF for a 1 bit, $00 for a 0 bit
and $05 ; 5 or 0
add a,$04 ; 9 or 4
ld c,a ; C = the number of waves
OutWave:
out ($FF),a ; end VSYNC: the output goes high
ld b,$23
High:
djnz High ; wait
call Break1 ; IN A,($FE): VSYNC on, the output goes low
jr nc,$03A6 ; BREAK pressed: report D
ld b,$1E
Low:
djnz Low ; wait
dec c
jr nz,OutWave ; the next wave of this burst
Silence:
and a ; clear carry for the next RL E
djnz Silence ; B = 0: 256 rounds, about 1.3 ms
jr OutBitThe bits come out through carry. SCF followed by RL E moves the byte’s top bit into carry and
shifts a 1 into the bottom of E. Each further RL E (with carry cleared by AND A) moves the next
bit out and the 1 further up. After eight bits, the next RL E pushes the marker out too, leaves E
at 0, and RET Z ends the byte. A marker bit instead of a loop counter saves a register.
A bit becomes a count without a jump. SBC A,A turns carry into $FF or $00; AND $05 and
ADD A,$04 make that 9 or 4. No branch means both kinds of bit go through exactly the same
instructions, so they differ only in the number of waves, never in the timing of each wave.
OUT and IN make the waves. OUT ($FF),A ends VSYNC, and the output to tape goes high. Then
Break1’s IN A,($FE) starts VSYNC again, and the output goes low. The BREAK test is not an extra:
it is the falling edge. Counted from the listing:
| Part | T-states | Time |
|---|---|---|
High: OUT, LD B,$23, 35 rounds of DJNZ, into Break1 up to its IN | 503 | 155 µs |
Low: the rest of Break1, JR NC, LD B,$1E, 30 rounds of DJNZ, DEC C, JR NZ | 472 | 145 µs |
| One wave | 975 | 300 µs |
Silence after the last wave: its low part, then 256 rounds of AND A and DJNZ, and the next bit’s setup | 4,861 | 1.50 ms |
That gives 8,289 T-states, 2.55 ms, for a 0 bit (4 waves and the silence) and 13,164 T-states,
4.05 ms, for a 1 bit (9 waves and the silence): the figures of Part 3. Notice that the silence loop
runs AND A and DJNZ with B = 0, 256 times: B is always 0 there, because the DJNZ before it has
just counted it down. Repeating AND A costs nothing but time, and time is what the loop is for.
Every one of these T-states is spent by the CPU, which is why the display has to be off. An NMI in the
middle of a wave would stretch it, and the ULA’s WAIT would stretch it more.
Part 6: LOAD
Loading is the harder half. The CPU no longer sets the pace; the tape does, and the CPU has to listen
for it. It does so by sampling: reading bit 7 of port $FE in a loop, again and again, and
counting.
The plan
LOAD has to find the start of a program, read its name and compare it, skip the program if the name
is wrong, and otherwise read bytes into memory until E_LINE. The ROM does all of that with one byte
reader, InByte, and three loops around it:
The entry
; $0340: LOAD "name"
Load:
call Name ; DE = the name, carry set if it is empty
rl d ; carry into bit 0 of D, D's bit 7 into carry ...
rrc d ; ... and bit 0 back to bit 7:
; D's bit 7 = 1 means "any name will do"
NextProg:
call InByte ; read a byte and forget it ...
jr NextProg ; ... until a silence (see InByte)A flag in an address. The name’s address in DE is somewhere in RAM above $4000, so its top bit,
bit 7 of D, is always 0, and the ROM borrows it. RL D / RRC D puts Name’s “empty name” carry
into it without disturbing the address. LOAD "" sets it, and then every name matches.
An endless loop, on purpose. NextProg calls InByte and throws the byte away, forever. The tape
may be in the middle of some other program, or playing noise; none of it matters until the start of a
program, and a program starts after a long silence. The way out of the loop is therefore not here
but inside InByte, which notices the silence. InByte follows in the ROM, at $034C.
Reading one byte
InByte waits for a burst, measures it, turns it into a bit, and repeats until it has eight:
; $034C: read one byte from tape into C
InByte:
ld c,$01 ; a marker bit: when it falls out, C is full
WaitBit:
ld b,$00 ; up to 256 samples of waiting
Wait:
ld a,$7F
in a,($FE) ; read the SPACE row and the tape (VSYNC on)
out ($FF),a ; VSYNC off again
rra ; bit 0, SPACE, into carry
jr nc,BreakLoad ; BREAK pressed
rla ; undo the RRA ...
rla ; ... and move bit 7, the tape, into carry
jr c,GetBit ; sound: a burst begins
djnz Wait ; still silent: wait some more
; 256 samples without sound: fall into SilenceFound ($035F)While there is sound, InByte jumps to GetBit, which measures the burst and turns it into a bit
(Measuring a burst shows how). Here, we follow the other way out: no sound at
all.
Each round of Wait takes 68 T-states, so 256 rounds without sound are 17,408 T-states, 5.4 ms.
That is far longer than the 1.5 ms silence between bits, and far shorter than the 5 s leader. A
silence that long can mean only one thing: the tape is between programs.
Then InByte does not return. When DJNZ runs out, the code simply carries on with the next
instruction in the ROM, at $035F.
What a silence means
; $035F: InByte has heard a long silence
SilenceFound:
pop af ; take InByte's return address off the stack
cp d ; A = $03, the return address's high byte;
; D = the name's address ($40 or more), or 0 in the data
jp nc,$03E5 ; D = 0: the data was cut off, start afresh
ld h,d
ld l,e ; HL = the name we want: read the nameInByte was called, so its return address is still on the stack, and POP AF takes it off: the
call will never return. That address was $034A (when NextProg called InByte), $0369 (from InName,
below) or $037F (from InProg, below). Its high byte, $03, is now in A, and CP D compares it
with D:
Where InByte was called from | D | CP D | What happens |
|---|---|---|---|
NextProg, waiting for a program | the name’s address, $40–$7F (bit 7 maybe set) | $03 < D: carry | Go on to read the name: a program starts. |
InName, in the middle of the name | the same | carry | Start reading the name again. |
InProg, in the data | 0 (InProg sets it, as you will see) | $03 ≥ 0: no carry | JP NC,$03E5: the machine starts afresh. |
So one test, borrowing the high byte of a return address, gives three different behaviours. The
last one is drastic, and deliberate: when the sound stops in the middle of the data, memory from
$4009 up holds half of the new program over half of the old one, and the system variables may point
anywhere. There is nothing safe to go back to. $03E5 is the end of the ROM’s start-up code: it keeps
RAMTOP and initializes everything else, as NEW does.
BREAK takes the same decision. Wait jumps here when SPACE is pressed:
; $03A2: SPACE pressed during LOAD
BreakLoad:
ld a,d
and a ; D = 0: we are in the data ...
jr z,$0361 ; ... JP NC,$03E5 (AND cleared carry): start afresh
rst $08 ; otherwise report D: BREAK
defb $0C$0361 is the JP NC,$03E5 in SilenceFound. Before the data starts, BREAK stops the LOAD with
report D and your old program intact; once the data has started, there is no old program left, and
BREAK resets the machine.
The name and the data
After a silence, with HL pointing at the name it wants, LOAD reads the name from tape, and then the
data:
; $0366: read and compare the name, then load the data
InName:
call InByte ; C = the next name character on tape
bit 7,d ; any name will do?
ld a,c
jr nz,AnyName
cp (hl) ; the character we want?
jr nz,NextProg ; no: this is not our program
AnyName:
inc hl
rla ; bit 7: the last character of the name?
jr nc,InName
inc (iy+$15) ; E_LINE + 256: see below
ld hl,$4009 ; load from VERSN on
InProg:
ld d,b ; D = 0 (B is 0 after InByte): we are in the data
call InByte ; C = the next byte
ld (hl),c ; into memory
call NextByte ; $01FC: next address; leave at E_LINE
jr InProgThe name is compared as it arrives. The loader keeps no buffer. It compares each character from
tape with the next character of the name it wants, and on the first difference it goes back to
NextProg, which reads and throws away bytes until the next silence: the end of that program, the
silence before the next one. So LOAD "GAME" skips every program on the tape until it finds GAME.
The ZX81 shows you nothing while it skips: you only see the stripes.
E_LINE is moved out of the way. The data loop stops when the address reaches E_LINE, but
E_LINE itself is one of the bytes being loaded, at $4014–$4015. INC (IY+$15) adds 256 to the
current value before the data starts, so that the end test cannot trigger while the new value
arrives over it one byte at a time. From the moment both bytes have been loaded, the test uses the
new program’s E_LINE, and loading stops exactly at the end of the program’s data.
D changes its meaning. LD D,B at the start of every data byte sets D to 0 (B is always 0 when
InByte returns). From now on, D says “we are in the data”, and that is the 0 that
What a silence means tests for: a silence or BREAK now resets the machine.
Measuring a burst
When Wait hears sound, GetBit measures how long it lasts:
; $0385: measure one burst; shift its bit into C
GetBit:
push de
ld e,$94 ; E counts all samples, down from 148
Restart:
ld b,$1A ; B counts silent samples: 26 in a row end the burst
Sample:
dec e
in a,($FE) ; read the tape (the key row A selects does not matter)
rla ; bit 7, the tape, into carry
bit 7,e ; Z = 0 if E has gone below zero
ld a,e
jr c,Restart ; sound: start counting the silence again
djnz Sample ; silence: one more silent sample
pop de
jr nz,LongBurst ; E below zero: 149 samples or more
cp $56
jr nc,WaitBit ; E >= $56, 62 samples or fewer: noise, ignore it
LongBurst:
ccf ; the bit: 1 for a long burst, 0 for a short one
rl c ; into C; the marker falls out after 8 bits
jr nc,WaitBit ; more bits to come
ret ; C is a whole byteTwo counters, one test. B counts silent samples in a row: every sample with sound sets it back to 26, so the loop ends only after 26 silent samples in a row, about 400 µs. That is longer than the low half of a wave (145 µs) and much shorter than the 1.5 ms silence after a bit, so the loop runs through the whole burst and stops early in the silence. E counts all samples, down from 148.
The decision, with n the number of samples taken (one round of Sample is 51 T-states,
15.7 µs, and n includes the 26 silent ones at the end):
| Samples n | Sound before the silence | Meaning | How the code tells |
|---|---|---|---|
| up to 62 | under about 0.6 ms | noise: ignored, wait for the next burst | E ≥ $56: JR NC,WaitBit, with C unchanged |
| 63 to 148 | about 0.6 to 1.9 ms | a 0 bit | CP $56 sets carry; CCF makes it 0 |
| 149 or more | over about 1.9 ms | a 1 bit | E has passed zero, bit 7 is set; carry is clear from RLA; CCF makes it 1 |
A 0 bit’s burst lasts about 1.05 ms from its first edge to its last (4 waves) and a 1 bit’s about 2.55 ms (9 waves), so they land near the middle of their windows. That is the margin Part 3 promised: the 0 burst could shrink by almost half or grow by 80 per cent and still read as 0. A recorder running a few per cent off speed does not come close.
Three small tricks:
-
BIT 7,Eis a “below zero” test. E starts at$94, which already has bit 7 set; decrementing it 26 times or more (the minimum, since B starts at 26) takes it below$80, and bit 7 comes back only when E passes zero and wraps to$FF. TheLD A,Ekeeps a copy for theCP $56after the loop, so the loop does not have to test anything else.The test is made inside the loop, but its result is used only after it, by the
JR NZthat followsPOP DE. That works because nothing in between touches the Z flag.LD A,EandPOP DEchange no flags (onlyPOP AFwould), and neither doesDJNZ, a fact many Z80 programmers do not know.DJNZdecrements B and jumps on its own test of B, not on a flag, and it leaves every flag as it was: Zilog’s Z80 CPU User Manual lists “Condition Bits Affected: None” for it. So when the loop ends, Z still holds the result of the lastBIT 7,E.The same care keeps the carry flag alive.
RLAputs the tape bit into carry, andBITleaves carry alone (Zilog lists it as “C: Not affected”), so theJR CafterBIT 7,EandLD A,Estill jumps on the tape bit. At the end of the loop, that carry is 0, from the last silent sample. For a long burst it survivesDJNZ,POP DEand the takenJR NZto reachCCFinLongBurst, which makes it the 1 bit; for a shorter one,CP $56sets carry afresh. One flag register carries two results, Z fromBITand C fromRLA, out of a loop whose own counter needs neither. -
The bit is shifted in with its marker, just as
OutByteshifted it out: C starts as 1, andRL Cpushes it out into carry after eight bits. -
Noise costs nothing. A short click on the tape is measured, found too short, and forgotten: C is left unchanged, and
WaitBitwaits for the next burst.
Note what is not here: no checksum, no parity, no retry. If dust on the tape turns one 1 into a 0,
LOAD puts the wrong byte into memory and carries on. The burst format makes errors rare, but when
one happens, nothing notices.
Part 7: Living with the format
The design has some consequences every ZX81 owner learned, sooner or later.
The old screen comes back. The display file is in the saved slice of memory, so loading a
program also loads the screen as it was at the moment of SAVE: it appears when the display restarts
at the end of the LOAD.
Programs can start themselves. NXTLIN ($4029), the number of the line to run next, is one of
the saved system variables. When you type SAVE "X" as a command, there is no next line. But if a
program line does the saving, NXTLIN holds the line after it, and the tape keeps that. After LOAD,
the interpreter takes its next line from NXTLIN as it does after every statement, now the loaded
value, and the program carries on running from the line after its SAVE. That is how commercial
software started without RUN:
9000 SAVE "GAME"
9010 GOTO 1FAST and SLOW come back too. CDFLAG ($403B) holds the display mode, and it is saved. At the
end of LOAD, the routine at $0207 restarts the display only if CDFLAG says SLOW, and by then it
is the loaded CDFLAG. A program saved in FAST mode loads in FAST mode.
The size depends on the machine it was saved on. A 1K ZX81 collapses its display file to as little as 25 bytes; a 16K one keeps all 793. So the same BASIC program saves longer from a 16K machine.
Zeros are cheap. A 0 bit takes 2.55 ms and a 1 bit 4.05 ms, and the ZX81 space character is code
$00, so a screen full of spaces saves fast: a full display file of spaces is 768 bytes of eight 0
bits each.
The tape is not checked. Without a checksum, a bad load shows up only when the program does something odd, or when the machine crashes. Owners learned to try again with the volume turned up or down.
LOAD "" takes the first program. With an empty name every name matches, so LOAD "" loads
whatever program comes next on the tape.
Part 8: How Klive plays and records a tape
Klive’s ZX81 runs the real ROM, so LOAD is the ROM’s own LOAD, sampling bit 7 of port $FE. All
the emulator has to provide is the signal on that bit, at the right T-state. It does so in one of two
ways.
Real-time loading: a synthesized signal
When you load a .P file with Fast Load off, Klive puts its bytes (with the $80 name in front,
Part 4) into the core, and the core generates the tape signal from them as the emulated time
passes:
| Part of the signal | Klive’s timing |
|---|---|
| The leader | 1 second of silence |
| Before every bit | a 1,300 µs silence |
| Each wave | 150 µs of sound, then 150 µs of silence |
| A 0 bit / a 1 bit | 4 waves / 9 waves |
| After the last byte | silence |
These figures come from Thomas Harte’s Clock Signal emulator, whose tape format Klive follows. They are rounder than the ROM’s own (1.5 ms of silence after each bit, a 155/145 µs wave), yet a whole bit comes out the same length: 2.5 ms for a 0 and 4.0 ms for a 1. And as Part 6 showed, the loader does not mind: a 0 burst still measures 1.05 ms and a 1 burst 2.55 ms, in the middle of their windows. One second of leader is enough, because the loader needs only 5.4 ms of silence to recognise the start of a program.
The core does not store a list of pulses: a 16K program would be millions of them. It keeps only its
place in the file (the byte, the bit, the wave, and which half of it), and works out the length of
the next pulse when the current one ends. Each instruction’s T-states count down the current pulse,
and the level of the pulse is what an IN from port $FE returns in bit 7.
The motor. A real ZX81 has no motor control: the tape plays as long as you hold Play. Klive
plays it only while the CPU is inside the ROM’s LOAD code, $0340–$03C2. Between commands, or
while you type LOAD "", the tape waits. This “automatic motor” is a convenience a real ZX81 does
not have, and it means no program is lost because the tape started too early.
While a real-time load runs, Klive draws the screen exactly as the ROM’s INs and OUTs make it, so
you see the same moving stripes as on a television.
Fast loading: skipping the sound
With Fast Load on (the default), Klive does not wait for the sound at all. It traps the three
places in the ROM where LOAD calls InByte, and answers each call at once:
| ROM address | The call | What the trap does |
|---|---|---|
$0347 | NextProg: wait for a silence | Pretend the silence has come: A = 0, continue at $0360 (CP D), which goes on to the name. |
$0366 | InName: read a name character | Put the next byte from the file into C, continue at $0369. |
$037C | InProg: read a data byte | Put the next byte into C, continue at $037F (LD (HL),C). |
Everything else is the ROM’s own code: the name comparison, the E_LINE trick, the end test in
$01FC, and the restart of the display. A 16K program loads in a fraction of a second, and the
machine ends up exactly as if the tape had played. The trap at $0347 continues at $0360 rather
than at $035F, because no CALL was made, so there is no return address for the POP AF to drop.
When you choose Load and Run, Klive resets the machine, types LOAD "" for you, and waits for the
ROM to finish the command. Then it types RUN, unless the program started itself (Part 7).
Saving: listening to OUT-BYTE
A SAVE in Klive writes a .P file. The Spectrum cores decode their SAVE from the pulses on the
MIC bit; the ZX81’s tape signal is VSYNC, which the same ROM also toggles to draw the picture, so
Klive listens one level up, at the byte. The core follows the ROM’s SAVE (Part 5) through four
addresses:
| ROM address | What the core does |
|---|---|
$02F6 | SAVE: starts a new file; one stopped by BREAK earlier is forgotten. |
$0313 | LD HL,$4009: the name is out, the program follows. |
$031E | OutByte: records the byte at (HL), the name’s or the program’s. |
$02FF | The lead-in loop (fast only, below). |
The program’s bytes, from $4009 up to E_LINE − 1, are exactly a .P file (Part 4). When the last
one is out, the core hands the file to the IDE, which names it after the tape name and writes it to
the saved-files folder. A SAVE stopped by BREAK never reaches its last byte, and writes nothing.
With Fast Load off, the ROM’s own code runs: five seconds of lead-in, every burst on the screen,
the bytes recorded as OutByte sends them. With it on, the core skips the lead-in loop and returns
from OutByte at once, so the same bytes are written in a fraction of a second. Only the ROM’s
SAVE is heard: a program with its own tape routine writes nothing.
Where to read the code
The ZX81 core lives in src/emu/machines/zx8081/wasm/ of the Klive repository. Its README.md has a
section on the tape, and:
zx8081/zx8081-tape.cholds the pulse synthesizer, the automatic motor, the fast-load traps and theSAVEcapture;zx8081/zx8081-ula.cputs the tape level into bit 7 of port$FE, and starts and ends VSYNC onINandOUT;src/emu/machines/zx8081/ZxPFile.tsrecognises.Pand.Ofiles and adds the$80name.
The tape logic is ported from Thomas Harte’s Clock Signal emulator (MIT licence).
The tape routines at a glance
| Routine | Address | Its job | Listed in |
|---|---|---|---|
Save | $02F6 | The SAVE command: 5 s of silence, the name, then memory from $4009 to E_LINE. | The leader and the bytes |
OutByte | $031E | Sends one byte, MSB first: 4 or 9 waves per bit, then 1.5 ms of silence. | One byte, eight bursts |
Load | $0340 | The LOAD command: waits for a program, compares its name, loads its data. | The entry, The name and the data |
InByte | $034C | Reads one byte; on a 5.4 ms silence, leaves to SilenceFound instead. | Reading one byte |
SilenceFound | $035F | Decides what a silence means: a program starts, a name restarts, or the data was cut off. | What a silence means |
GetBit | $0385 | Measures one burst and turns it into a bit, or ignores it as noise. | Measuring a burst |
BreakLoad | $03A2 | BREAK during LOAD: report D, or a fresh start once the data has begun. | What a silence means |
Name | $03A8 | Evaluates the name, stops the display, and sets bit 7 of the last character. | The name |
NextByte | $01FC | Steps to the next address; at E_LINE, finishes SAVE or LOAD through $0207. | The leader and the bytes |
Break1 | $0F46 | The BREAK test. Its IN also starts VSYNC: in SAVE, the falling edge of every wave. | One byte, eight bursts |
Summary
| Question | Answer |
|---|---|
| What hardware does the tape use? | One output level and one input bit. IN from port $FE drives the output low (it starts VSYNC), any OUT drives it high; bit 7 of port $FE is the input. |
| Why does the screen show stripes? | The display is off, and the ULA’s video pin is also the tape output. |
| How is a bit recorded? | A burst of 4 waves (0) or 9 waves (1) of about 300 µs each, followed by about 1.5 ms of silence. |
| Why bursts? | The loader only asks “is there sound?” and “for how long?”, which survive the head’s spikes, blurring, speed errors, volume and polarity. |
| What does a recording hold? | About 5 s of silence, the name with bit 7 set on its last character, then memory from $4009 to E_LINE − 1. |
What is a .P file? | Exactly that memory, without the name. |
How does LOAD find a program? | A silence of 5.4 ms means “between programs”; it then compares the name as it arrives, and skips programs that do not match. |
| How does it tell a 0 from a 1? | It counts 15.7 µs samples until 26 silent ones in a row: up to 62 is noise, up to 148 is a 0, more is a 1. |
| How does it know where the data ends? | At E_LINE, which is itself loaded, as the bytes at $4014–$4015. |
| What if the tape stops mid-program? | The machine starts afresh, as after NEW: half a program cannot be kept. |
| Is a load checked for errors? | No. There is no checksum. |
| How does Klive load a tape? | In real time, by synthesizing the signal from the file into bit 7 of port $FE; or at once, by trapping the ROM’s three calls of InByte. |
How does Klive record a SAVE? | It follows the ROM’s SAVE and records each byte as OutByte starts to send it; the bytes from $4009 are written out as a .P file. With Fast Load on, it skips the leader and the bursts. |
For the other half of the ZX81’s hardware story, see How the Screen Is Drawn. To load programs in Klive, see Using the ZX81 and ZX80.