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.