Skip to Content
Emulator InternalsZX81: How Programs Go to Tape

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 .P file is (Part 4).
  • The ROM: the SAVE routine (Part 5) and the LOAD routine (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 SAVE as a .P file, 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.

Recording: the current through the headOn the tape←→←→←→←Playback: the voltage from the heada stretch of constant magnetization gives no voltage; only its edges do
The head reacts to change. A square wave goes onto the tape, but what comes back is a spike at each edge, rounded by the head and the electronics.

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.

supply reeltake-up reeleraseheadrecord/playhead (its gap)capstan andpinch roller4.76 cm/sthe cassette
The tape path, simplified. The capstan sets the speed; the reels only feed and wind the tape.

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

PlayerIts job on tape
The cassette recorderAny 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 socketThe 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 socketThe 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 CPUDoes 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 ROMHolds 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.

0 bit4 wavessilence ≈ 1.5 ms≈ 2.55 ms1 bit9 wavessilence ≈ 1.5 ms≈ 4.05 ms1 ms
The two bits, drawn to scale from the ROM’s timing. One wave is 300 µs; the silence after every bit lets the loader see where a burst ends.

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:

  1. 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).
  2. 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.
  3. The memory from $4009 up to the byte before E_LINE: most of the system variables, the BASIC program, the display file and the variables, exactly as they are in RAM.
  4. 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_LINE inside the data.
silence≈ 5 snamelast char + $80system variables116 bytesBASIC programdisplay filevariables… $80$4009$407DD_FILEVARSE_LINEthe bytes of a .P filememory from $4009 to E_LINE − 1, exactly as it was in RAM
One recording, not to scale. Everything after the name is a straight copy of memory; the blue part is what a .P file holds.

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:

AddressWhat it is
$4009–$407CThe 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_LINEThe 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 ret

A 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 NC returns not to SAVE but to whoever called SAVE, because the POP HL has removed SAVE’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 the IN/OUT VSYNC 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 that SAVE can send the name exactly as it is in memory.
  • Carry survives to say “empty”. INC/DEC on 8-bit registers do not touch carry, and neither does anything after the ADD A,A: when the name is empty, the carry set by “running” comes back to the caller unchanged. A non-empty name leaves through the ADD 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 SaveData

The 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 LOAD

When 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 OutBit

The 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:

PartT-statesTime
High: OUT, LD B,$23, 35 rounds of DJNZ, into Break1 up to its IN503155 µs
Low: the rest of Break1, JR NC, LD B,$1E, 30 rounds of DJNZ, DEC C, JR NZ472145 µs
One wave975300 µs
Silence after the last wave: its low part, then 256 rounds of AND A and DJNZ, and the next bit’s setup4,8611.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:

NextProgread bytes, ignore them,until a silenceInNameread and compare the nameto its bit-7 characterInProgread bytes from $4009until E_LINEsilencematchwrong name: skip this programsilence: start the name againdone$0207: restart the displaysilence or BREAK mid-data:the machine starts afresh
The three stages of LOAD. A silence moves it forward while it waits for a program, starts the name again while it reads one, and is fatal once the data has begun.

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 name

InByte 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 fromDCP DWhat happens
NextProg, waiting for a programthe name’s address, $40–$7F (bit 7 maybe set)$03 < D: carryGo on to read the name: a program starts.
InName, in the middle of the namethe samecarryStart reading the name again.
InProg, in the data0 (InProg sets it, as you will see)$03 ≥ 0: no carryJP 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 InProg

The 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 byte

Two 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 nSound before the silenceMeaningHow the code tells
up to 62under about 0.6 msnoise: ignored, wait for the next burstE ≥ $56: JR NC,WaitBit, with C unchanged
63 to 148about 0.6 to 1.9 msa 0 bitCP $56 sets carry; CCF makes it 0
149 or moreover about 1.9 msa 1 bitE 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,E is 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. The LD A,E keeps a copy for the CP $56 after 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 NZ that follows POP DE. That works because nothing in between touches the Z flag. LD A,E and POP DE change no flags (only POP AF would), and neither does DJNZ, a fact many Z80 programmers do not know. DJNZ decrements 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 last BIT 7,E.

    The same care keeps the carry flag alive. RLA puts the tape bit into carry, and BIT leaves carry alone (Zilog lists it as “C: Not affected”), so the JR C after BIT 7,E and LD A,E still 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 survives DJNZ, POP DE and the taken JR NZ to reach CCF in LongBurst, which makes it the 1 bit; for a shorter one, CP $56 sets carry afresh. One flag register carries two results, Z from BIT and C from RLA, out of a loop whose own counter needs neither.

  • The bit is shifted in with its marker, just as OutByte shifted it out: C starts as 1, and RL C pushes 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 WaitBit waits 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 1

FAST 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 signalKlive’s timing
The leader1 second of silence
Before every bita 1,300 µs silence
Each wave150 µs of sound, then 150 µs of silence
A 0 bit / a 1 bit4 waves / 9 waves
After the last bytesilence

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 addressThe callWhat the trap does
$0347NextProg: wait for a silencePretend the silence has come: A = 0, continue at $0360 (CP D), which goes on to the name.
$0366InName: read a name characterPut the next byte from the file into C, continue at $0369.
$037CInProg: read a data bytePut 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 addressWhat the core does
$02F6SAVE: starts a new file; one stopped by BREAK earlier is forgotten.
$0313LD HL,$4009: the name is out, the program follows.
$031EOutByte: records the byte at (HL), the name’s or the program’s.
$02FFThe 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.c holds the pulse synthesizer, the automatic motor, the fast-load traps and the SAVE capture;
  • zx8081/zx8081-ula.c puts the tape level into bit 7 of port $FE, and starts and ends VSYNC on IN and OUT;
  • src/emu/machines/zx8081/ZxPFile.ts recognises .P and .O files and adds the $80 name.

The tape logic is ported from Thomas Harte’s Clock Signal emulator (MIT licence).

The tape routines at a glance

RoutineAddressIts jobListed in
Save$02F6The SAVE command: 5 s of silence, the name, then memory from $4009 to E_LINE.The leader and the bytes
OutByte$031ESends one byte, MSB first: 4 or 9 waves per bit, then 1.5 ms of silence.One byte, eight bursts
Load$0340The LOAD command: waits for a program, compares its name, loads its data.The entry, The name and the data
InByte$034CReads one byte; on a 5.4 ms silence, leaves to SilenceFound instead.Reading one byte
SilenceFound$035FDecides what a silence means: a program starts, a name restarts, or the data was cut off.What a silence means
GetBit$0385Measures one burst and turns it into a bit, or ignores it as noise.Measuring a burst
BreakLoad$03A2BREAK during LOAD: report D, or a fresh start once the data has begun.What a silence means
Name$03A8Evaluates the name, stops the display, and sets bit 7 of the last character.The name
NextByte$01FCSteps to the next address; at E_LINE, finishes SAVE or LOAD through $0207.The leader and the bytes
Break1$0F46The BREAK test. Its IN also starts VSYNC: in SAVE, the falling edge of every wave.One byte, eight bursts

Summary

QuestionAnswer
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.

Last updated on