Revisiting the Z80 Computer
by tridecagon in Circuits > Computers
1292 Views, 8 Favorites, 0 Comments
Revisiting the Z80 Computer
In the past, I have written up a guide on how to build a Z80-based computer, and I designed the circuit to be as simplistic as possible so that it could be built as easily as possible. I wrote a small program too using the same idea of simplicity. This design worked rather well, but I wasn’t totally happy with it. I started with rewriting a program for it that allowed it to be programmed during runtime. This was to let me test pieces of code without having to dedicate it to EEPROM, which would in turn, require me to reprogram the EEPROM. This didn’t sound like a fun idea to me. Then I started thinking about memory spaces. If I wanted to interface a piece of hardware (IO mainly), a piece of code could potentially exceed the amount of memory space available to the system. Remember, the design only used the lower byte of the address bus and then the lower bit of the high byte was used to select between ROM and RAM spaces. This meant I only had 253 bytes of space to use. You may be asking why 253 instead of 256. That’s because my new code injects three bytes of data at the end of a written program (this will be covered later, as I modified it to work on the new design).
I went back over my old schematics to see what else was going on. I found a small flaw with the memory selection circuit, which I will cover when I get there. The simplified version: all write requests would actually go through, though it always got put in RAM. This probably wasn’t anything worth worrying about, but I wanted to do it properly this time. And with that, I started to draw a new schematic. The two pics attached to this page are before and after of the actual circuit. I cleaned up so much of the spaghetti wiring, it ain’t funny.
If you followed along with my original submission and plan to follow along with this one, you are going to hate me. If you’re starting fresh, then you’re in luck. Just grab the parts in the list (or their equivalent) and follow along.
Supplies
LM7805 - 5 Volt regulator
Z80 - the CPU; the brains of the system
AT28C64B - EEPROM. “Permanent” data storage used for the computer’s firmware
IDT6116SA - SRAM; used for storing user code and/or general data storage
NE555 - System clock
74HC374 - Octal D-Latch with /OE; used as the input chip
74LS273 - Octal D-Latch with /MR; output chip
TLC59211 - LED driver chip (used so the 74LS273 can drive LEDs, as it alone is not capable of the current output)
MC14572 - This is a “Line Driver” chip, but I found it to be perfect for the Memory Control logic. It has 4 inverters, and a NAND and NOR gate built in
74LS32 - Quad OR gate
CD4001 - Quad NOR gate
CD4040 - 12 Stage Ripple Counter; Drawn, but not implemented clock divider (for running the system at slower clock speeds)
2 10K Ohm Resistors - One is used in the 555 timer circuit, so use whatever value you’d like for it
4 1K Ohm Resistors - One is used for the 555 timer circuit, so use whatever you wish for it. Another is used for driving LEDs, so vary it too if you’d like
8x330 Ohm Resistor Bus
8x10K Ohm Resistor Bus
11 LEDs - Three are used for system status and the other eight are outputs. For the 8, I used a bar graph display (HDSP-4836)
4 Capacitors - Two are used the the LM7805; 0.22uF and 0.1uF. One is for the 555 timer, so use what you feel is right. The last is for power-on reset; 100uF
2 N.O. Push Buttons - One is used for input, the other for reset
8 SPST DIP Switches - Data input; I used Piano Key style
Wire. Lots and lots of wire
NOTE: the MC14572 through hole version is obsolete, but the SMD version is still active (not even “not for new design” status), so you may need to purchase a circuit board to allow you to use it. A second 74LS32 can be used in place of the MC14572 (refer to “memory selection circuit” schematic of previous ible)
Quick Overview of Changes + Schematics
How to read the schematics:
An arrow pointed into a chip is an input:
Input >—
An arrow pointed away from a chip is an output:
Output <—
Busses use a line instead of an arrow:
Bus |—
Most of the chips have been drawn with their exact pinouts. The little dip has been drawn on these chips. Most chips also have pin numbers and labels on them. They may be a tad hard to read. My pencil was getting dull.
In terms of circuitry connections, the layout of the new design is mostly unchanged from the original. I connected the lower nibble of the address high byte to the memories and then used the low bit of the upper nibble (A12) for RAM/ROM selection. This meant that ROM space went from 0000-00FF up to 0000-0FFF. Ram space went from 0100-01FF to 1000-1FFF. I also swapped the Memory Control logic for a better design and added two new status LEDs (and some glue logic). I’ve also drawn (but didn’t wire) a clock divider circuit. It was to perform two functions. The obvious function is to divide the clock frequency down. The other function is for PWM (Pulse Width Modulation) purposes, as the 555 does not generate waves with 50% duty cycles. That doesn’t really matter in this circuit, but if you were to want to use the clock to drive some LEDs, you’ll definitely notice the effects (one (set of) LED(s) will be dimmer than the other). The entire rest of the circuitry is essentially unchanged.
CPU, Memory and Memory Control
This is the part where readers of my previous version hate me. In the original build, I just kinda threw parts on the board in a place they looked like they’d impose little issue with getting wired up. The result looked like someone dumped a plate of spaghetti on it and was like “wires!” I wanted to clean it up a little bit, so I started by ripping up everything except the CPU, RAM and ROM. I pulled up nearly the entire input circuit, output circuit, and the glue logic. It almost hurt me to do, but it was necessary. I left all of the data connections intact and the lower byte of the address bus. I then connected the next four bits of the address bus (A8-A11) over to the ROM chip. I took care to go around the chip this time to make it easier to pull up for reprogramming. I also jumped the address connections down to the RAM chip.
With that out of the way, I had to now get the memory control logic wired up. In the original schematic, I had connected the processor’s /MREQ line directly to /CE to both memory chips, then I wired /WR to the RAM’s /WE. Then I had the CPU’s /RD and /MREQ logically OR’d together as well as A9. Essentially, it was set up so that all memory requests activated both RAM and ROM, but A9 was used to select which of the chips’ /OE got selected. This was fine and all because the chips would remain inactive until a memory request was made and then only one /OE would be active during a read request. This prevented crosstalk, but introduced an awkward nuance. Because A9 was only used to determine which chip was outputting data and because the CPU had direct access to the RAM’s /WE pin, any and all write requests would go through. This was okay for the ROM because its write mode is inhibited by tying /WE directly to the 5V supply. The RAM, however, would be written to regardless of A9. This meant that an attempted write to a ROM space location would write to the same location in RAM space.
One solution for this would be to rewire the control logic so that the CPU has direct access to the chips’ /OE and /WE pins and then using MREQ and A12 to select which chips /CE was driven. I went with this idea, but instead of using four NOR gates and an inverter like the original design, I found an awkward little chip that was perfect for the task. I had to create a circuit that used only the logic gates available in the chip, but that was easy enough. A12 feeds directly into a NAND gate and a NOR gate. /MREQ is fed into the NOR gate and its compliment is fed into the NAND gate. The NAND gate is used to drive /CE for the RAM and the NOR output gets inverted and used to drive the ROM /CE. This makes it so that /MREQ has to be low before either chip is selected and then A12 chooses which one gets selected. With this setup, now any write requests to ROM will do nothing. It also saves power because only one chip is active instead of both. As for the logic chip itself, we still have two unused inverters inside. One will get used later, but we’ll get there when we get there.
System Status LEDs
Before I began this project, I was trying to interface with a certain IC, but I was having trouble with it. Unsure of what was going on, I used a panel mount LED to probe around (one of those assemblies that has a resistor built in). Doing this gave me a nostalgia idea that’s still used today: status LEDs used to indicate if memory was being read from or written to. It was to be used in conjunction with the input LED I already had. The input LED was connected to the /WAIT signal generator to indicate to us that the system is, well, waiting for input (I’ll get there, don’t worry). I considered adding an LED for indicating an IO write, but I figured that the output LEDs getting changed would already be a great indicator of that. Thinking on it, I may still add it yet. Nonetheless, I find it useful to know if memory is being read or written. Well, it’s useful for program debugging, anyway. I actually made heavy use of it as such when trying to get my program working: “why is it writing to memory? It’s not supposed to be doing that yet!”
To control these LEDs, I used the quad NOR gate. I used all of the gates. Only two were used to generate the status signals, but the chip doesn’t have the power capabilities to actually drive the LEDs. They are capable of sinking that much power, so I used the other two NOR gates as inverters and connected the LEDs as such. Because one LED is used to indicate reads and the other for writes, and a read and write request won’t occur at the same time, I was able to get away with using only one resistor for both LEDs. As for the signals I needed to decode, that was also easy enough. I wanted all memory read requests to get indicated, so the first NOR gate had /MREQ and /RD on its inputs. The write status was a little trickier, but just as easy. I still used /MREQ as one input, but using /WR as the other would cause a minor nuance I wanted to avoid. It would have indicated ALL write requests. I only wanted the ones that actually went through. So how would I do that? Well, remember how I have the system set up so only the RAM can be written? I used the RAMs /CE as the other input to the NOR gate. This means that the LED will only light up when RAM is selected and a write request is being made. In terms of LED color, I chose orange as the read indicator (but I only found yellow ones) and red as the write indicator.
Input and Output
In the previous step, you may have noticed I added some of the rest of the components to the board already. I was reserving the space so I wouldn’t accidentally place wires where I wanted a component (thus I would have to find a new location for said component). You may have also noticed I left the input switches in place and wired up to the power rail. I decided that the original location was the perfect spot and decided to place the output LEDs nearby (above). To the right of the bar display is the input latch. Above that is the output latch, and to the left of it is the LED driver. I started by connecting the display to the driver since that was the easiest to do. Then I connected the switches to the input side of the input latch. Next I connected the output side of the output latch to the LED driver. This may seem like an awkward order to get these wired, but it was for a reason. The input of the output latch was to be connected to the data bus as well as the output of the input latch. The idea was to connect the outputs of the input latch to the inputs of the output latch, which I did. Then all I had to do was get that mess connected to the data bus. It didn’t matter where these connections went physically because they would all be electrically connected. The computer is now almost done.
Reset and Finishing Input and Output
Sorry, no pics for this step. Refer to the previous step for the pics.
You may have noticed in the last pic of the previous step, I had a green button and another logic chip installed. The chip is the OR gate. Two gates are used to generate the /WAIT signal. Well, one generates the signal by OR-ing /IORQ and /RD from the processor. The output is fed into the second gate, where it gets OR’d again to a push button. The button brings the input of the gate high, thus bringing the output high. This output is fed to the processors /WAIT pin. While not pressed, a resistor holds the input low. I initially used a 10K resistor, but the LS32 was actually putting voltage out on the input. The resistor did not drop it low enough and I had to replace it with a 1K. Anyway, the idea is that when an IO read request is made, the first and second OR gates tells the processor to wait. Once you set the input switches to whatever you want, you press the button and it brings the CPU out of the wait condition. The green “input” LED, as I called it in an earlier step, is wired so that when the /WAIT pin goes low, it lights up.
But we’re not done just yet. The input flip flop needs a signal to let it know when the data input is valid and should be put out to the CPU. This clock pin is active high. Before, we just connected it to the button. This is still a valid option, but this time I chose to put it on the same output as the second OR gate. This IC also has an /OE pin that needs to be driven. If it were to be held high, it would never insert data to the bus. If held low, it would always be driving the bus. To fix this, I simply used a third OR gate. The inputs are /IORQ and /RD and the output goes directly to the latch’s /OE.
The output latch also needs the clock pin to be driven. Again, it is active high. In my schematic, I drew the fourth OR gate directly driving the pin using /IORQ and /WR. This meant that the clock pin would be held high until a write request was made, then it would go low then high again. This probably would have been fine because the data bus would still have had valid data on it immediately after the attempted write, but from an engineering standpoint, was a garbage design. I didn’t notice this error until after I had taken the final pics, but I did rip up that connection and then fed the OR gate output into one of the unused inverters from the memory control logic, then connected its output to the clock pin. I also fixed the schematic and found another error I had made. I corrected it too.
With all of that finally done, I had a very small amount of work to do: the reset circuit. I added a button to the board and used a 10K resistor to hold one side high. The other side goes directly to ground. The side held high is the /RESET output, which went to every chip with a /RESET pin (the CPU and output latch). To accomplish power-on reset, I added a capacitor to the /RESET output. The idea is that the large value resistor would cause the relatively large capacitor to charge slowly and hold the /RESET pins low for some amount of clock cycles (the CPU needs four clock cycles). You can probably already guess what the negative side of this circuit is. It’s the same negative as the previous version because it’s the same circuit. When the button is pressed, the capacitor is essentially shorted through the button. This is bad for both the cap and button, so if you’re wanting to make your build a little more permanent, you may wish to redesign it. I was thinking of another 555 timer set up in monostable mode. But with that, the computer circuit is now finished. Yay. Now it needs programmed.
Programming
Programming this thing was a nightmare. I built an Arduino EEPROM programmer. It didn’t work. I built another one based on someone else’s design and coding. Still didn’t work. I went back to the tried-and-true method of manually setting the addresses and data bytes by hand. Somehow, I messed that up. I tried again and still got it wrong. I went back yet again and discovered it was off by a single byte, so I corrected it and it finally worked, thank God.
As for the actual program, it looks like it’s super complex and hard to follow, but it’s not. It’s quite simple, actually. Half of it is copying numbers around. The other half is shared between 16-bit math, conditional jumps, and yet even more copying numbers around. So let me go through it and tell you how it works.
Initialization just sets some register values for use by the program. The program loop is a bit more complex, but not a whole lot. First, it accepts input to the A register on port 00. Then the E register gets written to memory. On the first two loops, the E register contains junk data, so we attempt to write it to the last two bytes of ROM space because it won’t actually be written; the address pointer (IY) is then incremented. The value stored in D is then moved into E to be written next. A is then loaded into D and L and E is copied into H. HL is where the value comparison takes place via subtraction and checking ZF (zero flag). The first value compared against is stored in registers B and C. B and C are treated as a single 16-bit register, BC. If the values are the same, then the program jumps straight into RAM space, where user code is assumed to reside. If the code in BC isn’t a match, then HL is reloaded with the initial values from D and E and get compared again to the value in SP in the same way it was compared to BC. If it’s a match, it has the same result, but three extra bytes are written to memory. The bytes are a code that causes the CPU to jump back to the very beginning of its program (a software reset). If the second comparison wasn’t a match, however, the program loops to where it grabs a value from the user.
LD SP, EDBFH ; exe code (adds jump)
LD IY, FFEH ; initial memory pointer for code storage
LD BC, EDC3H ; exe code (no loop)
loop ; assembler directive so we don't have to know where in memory this portion resides
IN A, (00H) ; get program data
LD (IY+00H), E ; E contains code to be stored
INC IY ; move to next memory location
LD E, D ; ld D into E
LD D, A ; ld A into D
LD H, E ; ld E into H
LD L, D ; ld D into L
OR A ; reset carry flag
SBC HL, BC ; returns 0 if exe code 2 was entered
JP Z, 1000H ; if so, jump to and execute program
LD H, E ; otherwise, refresh these to proper values
LD L, D
OR A ; first subtract may have set carry flag. Clear it
SBC HL, SP ; returns 0 if exe code 1 was entered
JP NZ, loop ; if not, repeat process (starting with getting a value)
LD (IY+00H), C3H ; otherwise, inject a jump code at the end of user program
LD (IY+01H), 00H ; jump basically acts as a software reset
LD (IY+02H), 00H ; it's a full reset in case registers got modified
JP 1000H ; jump to and execute user program