Z80 blinkenlights
Author: Andre Adrian, DL1ADR
Version: 29.Apr.2015
Introduction
There are some web 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 an 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 work 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!
The SRAM
programmer
The Z80 blinkenlights needs a monitor program to get things started.
The traditional solution in the 1970s and 1980s was to have ROM
(read only memory). The Z80 blinkenlights has no ROM, PROM, EPROM or
EEPROM, it has only 64KByte of battery buffered SRAM. The SRAM can
hold the contents between "sessions" due to a Lithium battery. But
at some point in time the monitor program must be loaded into the
SRAM. A SRAM programmer does this cold start job. The "Ecstatic Lyrics" web
page explains how to build a Z80
EEPROM programmer with a FT245RL chip and (many) glue logic
chips. This is a hardware only solution. The author uses an Atmel
ATmega 16A microcontroller. The ATmega has a DIL40 package and
offers 32 digital input/output pins. The ATmega behaves like a DMA
(direct memory access) controller. A DMA controller uses the /BUSRQ
input of the Z80 to get control over the address bus, the data bus
and the control lines. The SRAM control lines /CE, /OE and /WE
connect to the Z80 control lines /MREQ, /RD and /WR and to the
ATmega control lines. The ATmega can read and write the SRAM like
the Z80 does.
Picture: SRAM programmer. Top board has Atmel ATmega 16A and
128KByte SRAM. Buttom board has programming adapter, USB-UART
adapter and four LEDs for /WR, /RD, /MREQ and /BUSRQ.
Default behavior
fun or read the f***ing manual
At first, the SRAM programmer showed some strange behaviour. The Z80
data bus connects to Atmel port C. It was possible to write all
zeros in a memory cell, but it was not possible to write all ones.
The Atmel program did not control port C bits 2, 3, 4 and 5. These
four bits have an alternate use as the JTAG interface. The Atmel
manual tells us in chapter 26: "To be able to use the JTAG
interface, the JTAGEN Fuse must be programmed. The device is default
shipped with the fuse programmed". The author did carefully read
chapter 12.3.3 "Alternate Functions of Port C". It would have helped
a lot if the document writers at Atmel told me in chapter 12 that by
default the JTAG interface is enabled.
Z80 blinkenlights Monitor program
The monitor or BIOS program executes after a CPU reset. The monitor
program initializes the hardware, like configure the UART 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 can read
Intelhex record types 00 and 01. The monitor program is written in
PL/M-80 and assembler.
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.