MZ80K Retro Cassette Project
Last updated : May 10, 2024 @ 10:55 am

Project Updates

  • 11 April 2024 – Added tape data format section
  • 10 April 2024 – Added initial page 

This is an ongoing project for the Sharpe MZ80K computer.
I like Retro and prefer not to use fast loaders/ SD/Compact flash, rather keeping with the original processes.
To that end, I have been coding up a project to decode and encode MZ80K programs to and from tape using a Commodore 64 cassette tape deck.

There is an available program to encode the MZF files onto tape using the Commodore 64 tape deck via the PC parallel port.
But…. I wanted the fun of coding this myself, decoding the file structures and building up a device to interface into the tape deck.
Of course, in future, it would be nice to add encoding/deciding for ZXC spectrum, C64 and others, but I though I would start with the MZ80K.

Luckly for me, there is a site that has a lot of info on the MZ80K tape format -Credit for much of the tech details goes to that site.

Im using a NUCLEO-F411RE dev board with STM32 processor and STM32CubeIDE.

I had some PCB’s made up for the Commodore 64 Tape plug so that it was easy to connect it up. This is single sided, and the plug is double sided, however, its just a mirror so only one side needed.

C64 Pinout

Pin Name Description
1-A GND Ground connection
2-B +5V 5V dc
3-C Motor 6V dc to power the motor. This can be used to enable or disable the motor, otherwise it will run continuously.
4-D Read Data Read – 5V digital
5-E Write Data Write 5V digital pulse
6-F Sense Logical Low to indicate if any of the cassette button have been pressed

The Cassette port is connected to the NUCLEO-F411 as follows. The selected IO pins are 5V tolerant, though in the final design, I might add a high speed level shifter.

Nucleo Pin STM32 Pin C64 Port Pin
3-C Connected to separate 6V Power
D10 PB-6 4-D Read input to STM32
CN7 – 21 PB-7 5-E Write output from STM32
6-F Not connected Yet

Pulse Read Timer Setup

 I initially thought I would use the combined PWM input of the timer and use the duty cycle to calculate the pulse width and hence the pule time. However the PWN will wait until the next pulse rise to trigger so that it gets the full pulse length. This does not work for reading just the pulse length. The PWM is really meant for a continuous wave form. I could probably have used timeout more, but in the end I opted for “Input Capture direct mode” using Timer 4.

The system clock is running at 84Mhz so the timer set up is as follow

Prescaler – 84 to bring the timer to 1Mhz
Counter Period 1000 – to allow for timeout detection
Input capture Polarity Selection – Rising Edge
Prescaler division ratio – None

 NVIC TIM4 global interrupt – Enabled

Pulse measurement

The pulse measurement for the MZ80K is defined by a read time of around 400us after the rising edge of the pulse, the a long pulse being defined at around 380 us and a short pulse being around 200us.

Using my Saleae Logic Analyzer confirms the pulse lengths which matches much of the documentation I have found online. Of course in the real world, the pulse durations are not perfect and especially with a tape deck. My C64 desk at time of writing this code is old and probably needs to have the belts replaces and a clean up which is on my todo list.

I have set a Long Pulse duration in config for 380us – anything equal or longer is a Long pulse and anything less tahn 380 is decoded as a short pulse. I probably should be a bit more specific about that in the pulse measurement, but it seems to work.

The interrupt is configured to initially trigger on the rising edge and once triggered, reset to trigger on the falling edge. This allows me to measure the pulse length and not worry about the timing on the next incoming pulse.

if (htim->Instance == TIMER_DECODER)

		overflow = 0;

		if (isFirstCaptured == 0)
			ICValueRising = HAL_TIM_ReadCapturedValue(htim, TIMER_DECODER_CHANNEL); // We can discard this as we are resetting the counter
			isFirstCaptured =1;
		else if (isFirstCaptured)
			isFirstCaptured = 0;
			pulseWidth = HAL_TIM_ReadCapturedValue(htim, TIMER_DECODER_CHANNEL);

			if (pulseWidth > (PULSE_LONG_TIME * 2 ) && currentExpectation->expectationType != TYPE_EXPECTATION_SYNC)

			if (pulseWidth > PULSE_LONG_TIME)
				pulseType = PULSE_LONG;
				pulseType = PULSE_SHORT;

Data Format (Tape)

The data is stored on the tape in pulses with a long pulse being a logical 1 and a short pulse as a logical 0. The tape also has a number of synchronising pulses and markers to surround the data blocks.
The tape starts with a synchronising block of pulses and then a sequence of markers, headers and data. 

 The documentation I found online says that this block is 22000 shorty pulses, but when I analysed the actual pulses from a few MZ80K tapes, the number of pulses was vastly different and in one tape (NumberTron), the initial pulse markers were fragmented. There were 6 short pulses, followed by a long pulse of 9ms! then another block of 198 short pulses and finally a block of 13202 short pulses.

Header File format

Data file format

Sync Block (Long GAP)

The documentation I found online says that this block is 22000 shorty pulses, but when I analysed the actual pulses from a few MZ80K tapes, the number of pulses was vastly different and in one tape (NumberTron), the initial pulse markers were fragmented. There were 6 short pulses, followed by a long pulse of 9ms! then another block of 198 short pulses and finally a block of 13202 short pulses.

Numbertron tape sync pulses

Astrododge, had a clean 7 pulses followed by a 13ms gap and finally the main sync block of only 4424 short pulses.
Princes and Monsters had a clean single block of 4365 pulses.

 This posed a bit of a problem as from these three tapes it is evident that the initial sync pulses are anything but uniform. In the end I count 1000 pulses regardless of gaps and ignore the rest of the pulses until the next section (Tape markers). As long as there are 1000 or more short pulses, I can safely assume this is the sync block. 

Tape Marks

The next section is the Long Tape Mark which marks the start of the header. For the header this is 40 Long Pulses and then 40 Short Pulses. This is, in my reckoning, where the stable data starts. I’m not sure why the initial sync block is so long.

The change in pulse lengths in the section (ie the single Long  Pulse after the 40 Short pulse Tape marker is useful to define the end and start of another block. This enables the decoding code to ensure that the pulse counts are what is expected.

Tape header

The header describes the the fie name, size, load and execute addresses. Its 128 bytes long. There is also a ‘comment’ section of 104 bytes which is just using up the balance of the 128-byte block. I’ve not seen much data in here, though there do seem to be a few bytes of non-text data at the end. I’m not sure if this is noise or used in some way.
The program Type is a number that defines the type of data being loaded.

The tape header is duplicated after 256 short pulse marker. This, I assume is a redundancy. Tapes have issues!

Length Description

File Type

01 : Machine code file
02 : MZ-80 BASIC file
03: MZ-80 data file
04: MZ7-00 data file
05: MZ-700 BASIC Program

17 File Name terminated with 0x0d
2 Size of file
2 Load address
2 Execution address
104 Free text – Comments


Header Checksum

The checksum for both the header and file section is a simple bit counter, counting up the number of set bits in each byte. Its not the best checksum and is open to false results. The checksum is a 2 byte unsigned value and will roll over if the value is over 0xFFFF.


The data block is essentially the same sequence as the header, just that the data section is whatever length is specified in the header. It is also repeated along with a checksum. 
The main difference is that the initially sync block is 11000 short pulses and the Tape markers are 20 pulses long each instead of 40.