Z80 blinkenlights
Author: Andre Adrian, DL1ADR
Version: 16.Apr.2026
Introduction
There are some internet pages about Z80 homebrew computers. The
Zilog Z80 and the Intel 8080 are both well known by me. My first own
microcomputer was a Sinclair ZX81 kit. Before I bought the ZX81 kit,
I tried to solder a 8080 system together. But I did not have the
knowledge nor the patience to succeed. That 8080 system of mine used
an 1KByte 2708 EPROM for a little monitor program I found in a
computer book. Before I could build my 8080 system I had to build an
EPROM programmer. The most simple solution I found was to connect
twenty switches to the EPROM data bus and address bus and a push
button switch for the programming voltage. EPROM programming was to
flip the switches and hit the "program" push button. The monitor
program was only 768 bytes long, but I soon gave up. There was too
much human error. At this time I had no EPROM eraser and I did not
know that to erase a EPROM it is sufficient to expose it to direct
sunshine for some hours. Over the years my more then primitive EPROM
programmer and the other parts of the 8080 project got lost.
Today we live in the age of laptop and USB. As another "nostalgia
project" I want to build the Z80 blinkenlights computer. For
the moment the Z80 blinkenlights specification requests only 64KByte
of static RAM, a RS232 port and a tiny monitor program of say
256Bytes. I do not want to spend much time on this project,
therefore there is no fancy stuff like a PS/2 adapter for a PS/2
keyboard or a graphics adapter for a VGA monitor or a Compact Flash
card as hard disk replacement.
The Z80 blinkenlights computer has to find its monitor program.
Again I have to build a programmer. This time it is a SRAM
programmer. Instead of switches and a program push button I use an
ATmega 16A microcontroller. This custom made "DMA controller"
connects the Z80 blinkenlights to my laptop through an USB-UART
bridge like Silicon Labs CP2102, CP2104 or FTDI FT232RL.
Table of contents
Z80 blinkenlights
List of parts
The most important part is the Z80. I have a Z84C0006. This is
the CMOS version for up to 6.17MHz clock speed. Second is the
ATmega 16A-PU. This 8-bit RISC chip operates at maximum 16MHz
clock speed. Within the Z80 blinkenlights the Z80 and ATmega both
operate at 6.144MHz. One gate of a 74HC04 chip implements a Pierce
quartz oscillator. Next to the quartz and the 74HC04 two resistors
and two capacitors are needed. The memory of the Z80 blinkenlights
is 64KByte static RAM. The MAX811 voltage monitor chip, two
Schottky diodes and one Lithium battery realize the battery
buffered RAM. A CP2102, CP2104 or FT232RL USB-UART bridge on a
tiny PCB connects the ATmega 16A RS232 interface to the
"mainframe" computer, my laptop.
Clock generator
The Z80 needs a "single-phase MOS-level clock". The standard clock
generator circuit is the Pierce oscillator. As in every oscillator
there is a frequency dependent network and an amplifier. The
amplifier should be linear, that is the output signal should be a
scaled (enlarged) version of the input signal. There are no "real"
digital devices, just analog devices that are "tuned" in a specific
way. An inverter like the HCMOS 74HC04 has a high voltage
amplification and a low output impedance. There is a small input
voltage range where the output voltage depends on the input voltage.
In the Pierce oscillator circuit below the resistor R2 connects
inverter output to inverter input to give the input a bias to bring
the inverter into the linear region. The resistor R1 reduces the RF
power for the quartz. The quartz can only handle a tiny power.
Further R1 isolates the "analog" frequency dependent network from
the "digital" clock generator output. The quartz Q1 and the two
capacitors C1, C2 form a two port network. The input voltage is
applied at the two pins of C1, the output voltage is available at
the two pins of C2. The dependency from output voltage to input
voltage is linear, but frequency dependent. The quartz behaves for
the resonance frequency as a resistor of some 10Ω. Above and below
the resonance frequency the quartz has a high impedance. If we
switch on the oscillator there is only some thermal noise in the
circuit. Noise is a signal that contains all frequencies. Of all
these frequencies the resonance frequency of the quartz gets the
highest amplification. Sooner or later the supply voltage limits the
amplifier output voltage. The clock generator now produces a square
wave signal. Because the amplifier is not very linear, the duty
cycle is not exactly 50%, but a duty cycle of 45% to 55% is typical.

The following oscilloscope picture shows the output voltage of the
74HC04 clock generator. The load is one HCMOS input gate and the
capacitance load of a 10:1 oscilloscope probe. The oscilloscope
bandwidth is 200MHz. We see damped oscillations every time the
output voltage changes from nearly zero volt to nearly five volt or
back. This "over shoot" is due to inductances and capacitances we
can not avoid. Please remember, a piece of wire is an inductance and
every wire has a capacitive coupling to "earth" or "ground". The
inductance may be only some nano Henry and the capacitance may be
some pico Farad, but at a frequency of 6.144MHz they make an effect.

For the next oscilloscope picture a 4069 device replaces the 74HC04.
The 40xx logic family is older than the 74HCxx family. The 40xx
devices are slower and have a higher output impedance than the
74HCxx devices. The output of the 4069 clock generator is a
distorted sine wave. This picture should show everybody that there
is only analog electronics and digital electronics is just a
simplification. The 4069 has no "amplifier reserve". If we connect a
higher frequency quartz there will be no more oscillation.

The last oscilloscope picture uses a 74AC04 as amplifier. The 74ACxx
logic family is faster than the 74HCxx family. We can see that
faster is not always better. The inverter oscillates at the delay
time frequency of the inverter, not at the resonance frequency of
the quartz. There are five oscillations per grid box in x direction.
One grid box has the "length" of 20 nano seconds. One oscillation
has a length of 4 nano seconds. As f = 1/T, the frequency is 1/4ns
or 250MHz. An analog oscilloscope can display information that is
above the oscilloscope bandwidth, a digital oscilloscope can not. We
have to assume that the voltage swing is larger than the display we
see, this is due to the low pass nature of the oscilloscope
amplifier. The resistor R2 connects amplifier output to amplifier
input- Parallel to this resistor there is a small capacitance of
some Pico Farad. Mostly it is the parasitic capacitance of the
breadboard connectors. The path through the parasitic capacitance
has a lower impedance than the path through the quartz. The circuit
behaves like a ring oscillator where the output of the inverter is
directly connected to the input and the signal travel time of the
inverter defines the output frequency.

Everybody can see that the 74HC04 device is the correct device for a
6.144MHz clock generator. In our case decision is easy. Sometimes
the tolerances of devices are large. Most examples of the device
behave as expected, but some examples behave like a sine wave
oscillator or like a ring oscillator. Because tolerances have a
Gaussian distribution, it is possible that we do not see the "worst
cases" at the lab bench, but only later in mass production.
The first Z80
blinkenlights experiment
What can we do with a clock generator and a Z80? We can build our
first blinkenlights experiment. The most simple CPU command or
opcode is NOP, no operation. The CPU reads the NOP opcode, does
nothing and increases the program counter. The next opcode is
fetched from the next memory location. If the next opcode is again
NOP, the program counter works like a binary counter.

Picture: Z80 blinkenlights NOP test circuit. From left to right is
6.144MHz quartz, 74HC04, Z80 and MAX811 voltage monitor chip on an
adapter board.

Picture: Z80 blinkenlights NOP test circuit schematics.
Hardware fun or
the meaning of data-sheet values
The supply voltage for the CMOS version for the Z80 is 4.5V to 5.5V,
according to the Z80 data sheet. What happens below 4.5V?
Microcontrollers of today have a supply voltage monitor function or
"brown-out detection" and stop program execution below the brown-out
voltage. My Z80 CPU performs the NOP test at a supply voltage of
1.2V, the lowest voltage my power supply can provide. I do not know
if the Z80 can execute every opcode at this low supply voltage
within the full temperature range. I only know that there are
chances that the Z80 will corrupt the battery buffered SRAM after I
switch of the power supply and the voltage drops slowly to zero
volts.
In the 1980s I worked with CMOS Z80 and battery buffered SRAM at my
job. It was quite a challenge to give the Z80 decent brown-out
capabilities. Finally we used a MAX690 chip. This 8 pin voltage
monitor chip did cost more than the Z80 CPU!
Logic Analyzer
measurements
I have a little 8-channel logic analyzer for the USB port. I like
to see the control bus signals to get a better understanding of
the datasheet timing diagrams and to check my glue logic circuits.
The first screen shot shows the Z80 executing the NOP opcode (0):
The datasheet says: NOP opcode has 4 clock cycles (4 T state). If
you look carefully, you see every 13 clock cycles one /M1 cycle.
And you see three /MREQ=low phases within these 13 cycles. The
first and third /MREQ=low is opcode fetch for two NOP opcodes, the
second /MREQ=low is a DRAM refresh. Summary: the measured Z80 does
not behave like the datasheet Z80.
NOP is not a typical opcode. A good NOP tester opcode is to load
an immediate value into a register, like the LD BC,0101h opcode
that is a 3 bytes opcode with coding 01h, 01h, 01h.

The datasheet says: LD BC,immediate opcode has 10 clock cycles (T
states). We can see this with the Logic analyzer. There are three
times /RD=low but four times /MREQ=low, because there is one
/RFSH=low DRAM refresh per opcode. We see that the memory access
time for a /M1 access is shorter then for a non-/M1 access. I
assume, the 150ns ROM 28C64 can handle Z80 8 MHz clock.
The Z80 OUT opcode is used to write to IO devices like the 65C51
ACIA. The opcode is 0D3h and is 2 bytes. If we wire 0D3h to the
data-bus, the opcode OUT (0D3h) is executed.

The datasheet tells the opcode has 11 clock cycles (T states) and
the Logic analyzer confirms this. There are two /RD accesses to read
the two bytes of opcode and there is one /WR access to write to the
IO device. The /IORQ access is longer then a memory access. The
65C51P2 needs at least 240ns for the Phi2=high phase. Again this is
fine for an 8 MHz Z80.
Z80 computer
There is a nice Z80 computer
schematics from Grant Searle. My schematics is a variant. I
use the 65C51P2 ACIA (2 MHz version) instead of 68B50 ACIA.

The ICs pinout 65C51, Z80, 6C1008 and 28C64:
The inverter U5A, the 6.144MHz quartz and the two 33pF capacitors
make a Pierce oscillator. The 10 Mega Ohm resistor R5 and C8 are
necessary for oscillation.
The lower 8 KByte are used for ROM, read-only memory. The upper 56
KByte are RAM, read-write memory. The ACIA uses IO address 7Ch to
7Fh.
The 28C64 ROM has access time of 150ns. This allows 8 MHz clock for
the Z80 CPU. The glue logic for /CE, /OE and /WE signals for ROM and
RAM is simple. The /CE for ROM and CE2 for RAM is logic OR of A13,
A14 and A15, created by U4A and U4B. The OR gate U4D creates /WE out
of /MREQ and /WR. U4C creates /OE out of /MREQ and /RD.
The control bus signals for 65C51 are CS1 (pin 2), /CS2 (pin 3),
Phi2 (pin 27) and R/W (pin 28). A7 is wired to /CS2. The CPU /M1
signal is used as ACIA CS1 to avoid confusion between IO read/write
access (/M1=high) and interrupt acknowledge (/M1=low). Inverter U5F
creates ACIA Phi2 signal out of CPU /IORQ signal. CPU signal /WR is
used as ACIA R/W.
The ACIA input /DSR is connected to push button SW2. The ACIA output
/RTS is connected to LED D1. See "first program" for more
information.
Let's look at the /CEROM signal with the Logic Analyzer. This signal
is low, if A13, A14, A15 are low:

The Logic Analyzer display is a little confusing. For the lowest 8
KBytes of memory everything is as expected, but not for the other
memory addresses. A detailed Logic Analyzer screenshot shows why:

The DRAM refresh address counter runs free and creates /CEROM
signals while the CPU addresses memory above 2000h, the lowest 8
KByte. The "DRAM refresh noise" does not impact ROM or RAM, because
for activation of ROM and RAM there must be an active CE and either
/OE=low or /WE=low.
My Logic Analyzer is a toy, but good enough to show details of the
slow speed CPUs of the 1970s.
The first program
The first program uses the DSR input of the ACIA and the RTS output
of the ACIA. The level of the DSR input is copied to the RTS output.
If push button SW2 is open (not pressed), the LED D1 is dark, If SW2
is pressed, D1 lights. I use a "high brightness" white LED that
needs very low current. The following text is part of FIRST.LST, the assembler list file. The
assembler source is first.asm and the
binary file for the 28C64 ROM programmer is FIRST.BIN.
0000
forever:
; for(;;) {
0000 06
03
ld b,3
; B = RTS_High
0002 DB
7D
in a,(AC_ST) ; A = ACIA_status
0004 E6
40
and 40h
; A &= DSR_mask
0006 20
02
jr nz,endif ; if (A != 0) {
0008 06
0B
ld b,0Bh
; B = RTS_Low
000A
endif:
; }
000A
78
ld a,b
; A = B
000B D3
7E
out (AC_CMD),a ; ACIA_command = A
000D 18
F1
jr forever ; }
The first column is the address for the opcodes in the columns 2 to
5. The /M1 signal is low on fetch of the first byte of an opcode.
The program is discussed with the Logic analyzer scribe:

The ACIA is read with an IN opcode at address 0002h. The ACIA read
happens at Phi2=hi and R/W=hi. The ACIA is written with an OUT
opcode at address 000Bh. The ACIA write happens at Phi2=hi and
R/W=lo.
Z80 blinkenlights
Monitor program
The monitor or BIOS program executes after a CPU reset. The monitor
program initializes the hardware, like configure the ACIA to the
correct speed. Then the monitor program enters the monitor command
loop. Traditionally the monitor has some primitive commands to read
and write the memory. The Z80 blinkenlights monitor is a port of my
6309
monitor.
The monitor prompt is \ (backslash).
To print memory area FF00 to FFFE enter:
FF00:FFFEP
To enter values into memory, starting at address 0x3000, enter:
3000:11,2,AF,
The values are 0x11, 0x02, 0xAF. After EVERY byte a , (comma) is
needed.
To start a program at address 0x00BC enter:
00BCR
Lower case characters are converted to upper case. There is no input
line and therefore no line editing.
About the monitor language: minimum state and postfix. States are
necessary if something from the past is needed to handle the
present. For example, in LET B=A+1 the expression A+1 is part of an
assign, in IF B>A+1 the same expression is part of a compare.
Postfix is NOUN VERB, that is first the object, then the action. The
NOUN in the mon6309 language is a hexadecimal number of 1 to 4
digits, like 0, 19, AFFE or BA9. The VERBS are:
: to store the NOUN as 16-bit address in argA
, to store the NOUN as 8-bit value at address argA and increment
address argA
P to print the contents of the address range from argA to NOUN
R to run the program at address NOUN
CR (carriage return) to print CR, LF and the prompt
The BIOS has the following "operating system" subroutines:
0000h reset, start monz80
0082h keybd, return input character available if register A
!= 0
0087h getc, return input character in register A
009Ch putc, print output character in register A as ASCII
00A7h putbyte, print output character in register A as
hexadecimal
00BCh putcrlf, print carriage return, line feed
The assembler source file is monz80.asm.
The binary file for the programmer is MONZ80.BIN.
I used the MS-DOS program tasm as assembler. The Z80 monitor does
need some RAM space for stack. The two 16-bit monitor variables
accu (NOUN) and argA are registers HL and DE.

This Tera Term terminal emulator screenshot show the monz80 display
its binary code. The Tera Term setting is 19200 baud, 7N2 (7 data
bits, no parity, 2 stop bits):

Intel hexadecimal
object file format
The early Intel development systems Intellec 4 (4004, 4040) and
Intellec 8 (8008, 8080) had no mass storage. The "computer terminal"
of the day was an electromechanical teletype like the ASR 33.
The paper tape read and punch unit of the teletype provided the
first mass storage. The ASR-33 used 7-bit ASCII, but implemented
only upper case letters. The Intelhex format uses two ASCII
characters to encode one byte. At the 20mA current loop interface
the ASR-33 used a parity bit to allow error detection. The paper
tape had no parity bit track. Therefore Intelhex uses a checksum
byte for error detection.
Intelhex record type 00 and 01 were defined in 1973. Other record
types were added for 8086 and later microprocessors.
PL/M-80
Gary Kindall invented the programming language PL/M and the
operating system CP/M. In 1975 one could choose between the
programming languages Pascal, PL/M and C. Only C is in use today.
Pascal was used to teach students structured programming. PL/M was
used to write the single-user/single-task operating system CP/M. C
was used to write the multi-user/multi-task operating system UNIX.
Pascal and C allow recursive functions. PL/M functions are not
recursive by default. This is a good optimization feature for the
Intel 8080 that has no efficient stack-relative addressing modes.
The REENTRANT attribute makes a PL/M function recursive. The
implementation of the strcpy() function shows the power and elegance
of C. Here is the ANSI-C version:
void strcpy(char *d, char *s)
{
while(*d++ = *s++)
;
}
The PL/M version is:
STRCPY: PROCEDURE (D, S);
DECLARE (D, S) ADDRESS,
(DI BASED D, SI BASED S)
BYTE;
LOOP: DI = SI;
S = S + 1;
D = D + 1;
IF DI <> 0 THEN GOTO LOOP;
END STRCPY;
The GOTO statement is needed because there is no do..while loop in
PL/M. Pointer arithmetic in PL/M is strange for a C programmer.
There is no dereference operator in PL/M, one has to create a
variable that is BASED on the pointer variable for indirection. Like
Pascal, PL/M allows nested procedures. The CP/M sources make very
little use of nested procedures. The author wonders: Structured
programming with nested blocks were a great invention in computer
science. Did some people think that nested procedures were even
better?
The 8080 microprocessor can handle three pointers in registers. The
H, L register pair is more flexible than the B, C or D, E register
pair. All "pointer registers" allow to load and store the
accumulator from the pointer address. One implementation of the
strcpy() function in 8080 assembler is:
LOOP: LDAX B
STAX D
INX B
INX D
JNZ LOOP
The pointer s is in registers B, C and the pointer d is in registers
D, E.
PL/M Parameter
passing conventions
The strcpy() function above uses two parameters. One method of
parameter passing is to use the stack. The calling function puts the
values of argument 1 and argument 2 on the stack, the called
function gets the parameter off the stack. This method allows
recursive functions. Another method is to pass arguments in
registers. A third method is to have "hidden" global variables for
parameter passing. The second and third method do not allow
recursive functions. The Intel "A Guide to PL/M Programming"
document from September 1973 tells in section "Subroutine linkage
conventions" how parameter passing is done on a PL/M 8008 system.
The first argument is passed in registers B, C. The second argument
is passed in registers D, E. Additional parameters are passed as
"hidden" global variables. Strange for me, the author, is the
convention to pass low byte in register B or D and high byte in
register C or E. The 8080 microcomputer has 16-bit addition opcodes.
These opcodes require low byte in registers C, E or L and high byte
in registers B, D or H.