UP | HOME

Z80 notes

1. Z80 Notes

1.1. Project log

1.1.1. <2019-08-22 Thu>

The 20MHz Z80s showed up yesterday \o/, not that I have time to do anything with them yet. On the bus this morning, I laid out a clock module and CPU carrier board; the routing is pretty gnarly but I didn't want to spend the time hand routing everything for a dev board. I'm working on the memory board now, and hoping to get everything done and ordered by the end of the day.

Debating using an m328 as the I/O controller. That would give me a serial port, and I could use I2C for most of the peripherals.

Another realisation I had while reading the Teensy Z80 post is that I can simulate a clock using an Arduino, which would also let me manipulerate the other pins, too.

On the subject of the SREC parser and serialiser, I kind of had this pet dream of building one that works a character at a time (e.g. fed from the USART). That requires building out the state machine (which I should probably draw out) and probably adding some callback functions once a record is ready.

1.1.2. <2019-08-21 Wed>

I started writing an SREC implementation in C++ that would work on an Arduino. I'm calling it the AEPRO - Arduino EEPROM programmer. I had been thinking I would just do write-only, but it would be almost criminal to not be able to verify that the EEPROM was written correctly. That led to poking around for a PISO equivalent of the 74595, which led to finding the MCP23008. That's an I2C I/O expander; three of them still only requires two pins on the Arduino and there's fewer chips on the board than with a SIPO/PISO combination and I get full I/O on the data pins. So I got that laid out right quick and ordered.

1.1.3. <2019-08-20 Tue>

I think the next steps for the Z80 project are twofold:

First, I need an EEPROM programmer. Right now I don't have a way to upload programs to the EEPROM, and that should not be so. I have a handful of SN74HCT595s, so I'll probably slap those on an Arduino and call that particular cat skinned, at least for a first pass at programmer. Following that, I'd like to make a ZIF-socket shield and figure out how to read data from the EEPROM. I envision that this is a serial interface for maximum justice or whatever.

The second thing is I need an EEPROM to program; to wit, I need a way to connect the two boards together. Now this turns out to be more of a challenge than I thought previously for a number of reasons:

  • My EDA software (Eagle) is limited in the number of layers (maximum of two), sheets (2), and board size (eurocard).
  • I am unwilling to pay $100/year for a subscription-only upgrade when I paid $160 for a fully unlocked version several years ago.
  • The alternative EDA software, Kicad [1], is pretty bad. I can symapthise with the plight of open source software, but I can't use it. So far, every attempt at learning it has lasted about 15 minutes before I have to go to Eagle to get done what I set out to do. I'll keep trying, but this is kind of like training yourself to enjoy surströmming.
  • It turns out to be hard to layout everything in one schematic, and a Eurocard-sized board isn't really big enough.

One idea I've been floating around is using IDC40 cables (e.g. the same connector used on ATA drives and the Raspberry Pi's GPIO header) which coincidentally have as many connections as the Z80 has pins. This would let me build small boards, like a CPU board and separate clock boards, without having to commit to an overall board design early on. It also lets me vet components in stages - first I can make test the clock, power, and basic Z80 functionality; then, I can check the memory subsystem, etc.

To that end, I think it'd be useful to have a jumper on the clock line so that I can also inject an external clock in order to use a manual timing cycle.

Another idea was stacking boards using the same sort of connector, similar to how the raspberry pi does its add-on boards.

1.1.4. <2019-08-19 Mon>

Lots of designing today. Some thoughts:

It would be cool to have an ESP8266-based modem; send a connect command (SSID and password), then another connect command to set host and port. The terminology for this needs to be sorted out. I was thinking of a gopher browser and an IRC client.

Graphics remains an open question, and there's two main questions. The first is what chipset to use, the second is how to deal with the VRAM requirements.

For graphics chipsets, there are a number of options that I see with more to come later as I do more research. The SSD1306 is a venerable chipset, but I don't really like the dimensions - all of mine are extremely small displays with 128x64 or 64x64 pixels. I'd like at least 5.7", with 7" closer to my ideal. Another option that I've seen is the KS018 chipset - I've never used it, but keeping it in mind.

There's also the option of using an AVR as a GPU, which might help with the VRAM issue, which is essentially that, at 320x240 pixels, requires tracking 75k of pixels. If I do this as a bitmap (each byte representing 1 pixel), then that's still roughly 9.5k of memory. I could put another RAM chip on the I/O bus and hook it up as VRAM, maybe? Not really sure.

I wonder if it might be easier to implement an interface that's text-only; rather than drawing pixels, the Z80 sends the characters to print. I'm really just not sure yet.

I also did a writeup on the memory layout, which is in the design section.

1.1.5. <2019-08-18 Sun>

Watching HCF made me want to build my own computer, and of course, it should be a laptop. My first inclination was a 6502-based system, but I just got the RC2014 which is made me think I should do a Z80. Then, I found Steve Ciarcia's "Build Your Own Z80 Computer" and I guess that's that. I have no idea what I'm doing here, so I expect to learn a lot.

I'm thinking a 7" display, some kind of mechanical keyboard. The first iteration probably won't do sound for simplicity's sake. For the power supply, I was thinking micro USB or USB-C to an internal LiPo pack, but I think I might just do a pair of 18650s. The first revision probably won't have a charging circuit inside; I might do a barrel jack connector too, but will need make sure to disable the battery if it's powered that way.

1.2. Design

1.2.1. Memory layout

While trying to figure out how to add memory and EEPROM to the machine, I had a hard time finding good information on how to do this; even harder was finding a writeup of the thought process behind this. So, as best as I understand it, here's how you add memory to the Z80.

While the Z80 is an 8-bit processor, it has a 16-bit address space (address pins A0…A15), which means it has a maximum of 65,535 bytes (64K) of accessible memory. This address space has to be split between ROM and RAM (fortunately I/O devices have another access mechanism). The task is to figure out what the split should be and how to accomplish it.

In the case of my computer, I'm using an AT28C256 32KB parallel EEPROM with a CY62256 parallel SRAM chip; because the CY62256 is 32KB, we need two of them to get the full address space. All three of these pins have a chip enable pin /CE; the leading slash in front of the pin name indicates active-low logic. With active high signalling, a high signal (i.e. a digital 1 or +5V) means active, but with active low signalling, a LOW signal means active. Basically, when /CE is pulled to ground, the chip is enabled. Note that the chips have slightly different names for these pins but they have the same function. For consistency, I'll just use one set of names.

It's important to know that the Z80 starts execution at address 0000H, which implies that the ROM should occupy the bottom of the address space. In that case, if we look at the binary representation of addresses, we can make some observations that will govern how we lay out memory:

MSB            LSB
  111111
  5432109876543210
+------------------+-------+
| 0010000000000000 | 8192  |
| 0100000000000000 | 16384 |
| 1000000000000000 | 32768 |
+------------------+-------+

If we give 8K of space to the ROM, we get 56K of RAM and an easy way to tell which chip should be selected: address pins A13, A14, and A15 decide which of our three chips should be chosen. The selection table looks like this:

A13 A14 A15 chip
L L L ROM
x x H RAM1
L x L RAM0
x L L RAM0

Basically, the three conditions are:

  • the ROM should be chosen if NAND(A13, A14, A15).
  • RAM1 should be chosen if A15
  • RAM2 should be chosen if AND(NOT(A15), OR(A13, A14)).

This could be done with logic gates, though it might get a little messy on the board. Fortunately, there's a 7400 series chip that does a lot of heavy lifting for us: the 74139 (I'm using the TI SN74HCT family, e.g. the SN74HCT139) 2-of-4 decoder and demux. It has three input pins (/E, A0, and A1) and four output pins (O0, O1, O2, O3). The enable pin is active low while the address pins are active high, so this can be a little confusing, but let's figure this out. Here's the 74139's logic table where H means HIGH and L means LOW (as in signal levels); I've added a final column that expresses the output pins as a binary value with O0 as the LSB. Note that if /E is low, it doesn't matter what A0 and A1 are - the output will be all high values.

/E A0 A1 O0 O1 O2 O3 O
H x x H H H H 1111
L L L L H H H 1110
L H L H L H H 1101
L L H H H L H 1011
L H H H H H L 0111

There's an important property of this chip: at most one output pin is low. If it's not enabled (e.g. /E is high), then none of the output pins are low. This is what we want when using active low logic to select a memory chip. At most one device is active, and if disabled, no devices are active.

The Z80 indicates that the address bus has a memory address by pulling its /MREQ pin low; we'll tie that to the /E pin. We can put pins A13 and A14 into a 7432 OR gate, and connect that to A0. Note that you should connect the unused gate pins to ground. A15 gets connected to A1, which means we can revise our table (condensing the output pins to a single binary value for clarity).

/MREQ A13 A14 A0 A15 O Notes
H x x x x 1111 no memory operation
L L L L L 1110 address is < 8192
L H L H L 1101  
L L H H L 1101  
L H H H L 1101  
L x x L H 1011 if A15 is high, it doesn't really matter what A0 is.
L x x H H 0111  

So far, we know we can tie O0 directly to the ROM's /CE pin, and O1 directly to RAM1's /CE pin. But how do we combine O2 and O3? Let's see what we want, behaviour wise.

O2 O3 /CE Notes
H H H if both pins are high, /CE should be high.
L H L if either O2 or O3 are low, /CE should
H L L be low.

This looks almost like an AND gate, which would also pull /CE low if both O2 and O3 are selected. There isn't a case where this happens, so we don't need to consider it. That means we should tie O2 and O3 together behind a 7408 AND gate. Lets simplify the table above, with A0=OR(A13,A14) and O=(AND(O2,O3),O1,O0).

/MREQ A0 A15 O Memory device
1 x x 111 None
0 0 0 110 ROM
0 1 0 101 RAM0
0 x 1 011 RAM1

But does this actually work? Let's verify the basic idea in Python. First, let's write a function that returns the memory chip for a given three-bit value:

def chip_selected(pins):
    if pins == 0b111:
        return None
    elif pins == 0b110:
        return "ROM"
    elif pins == 0b101:
        return "RAM0"
    elif pins == 0b011:
        return "RAM1"

    raise Exception("multiple devices active ({})".format(bin(pins)))

Now, let's emulate the 74139:

def demux(e, a0, a1):
    if e:
        return (1, 1, 1, 1)
    if a1:
        if a0:
            return (1, 1, 1, 0)
        return (1, 1, 0, 1)
    if a0:
        return (1, 0, 1, 1)
    return (0, 1, 1, 1)

This is really just hardcoding the logic table above. Okay, now the meat of the problem: actually checking our addresses:

def memselect(mreq, addr):
        # ex. memselect(0, 0x8000)
    # mreq is active high, so it should be 0 or False to enable
    # memory devices.

    # the address bus is 16 bits
    addr = addr & 0xFFFF

        # get our three chip selection bits from the address.
    a13 = addr & (1 << 13)
    a14 = addr & (1 << 14)
    a15 = addr & (1 << 15)

    # select an address.
    a0 = a13 | a14
    a1 = a15
    (o0, o1, o2, o3) = demux(mreq, a0, a1)
    ando = o2 & o3
    muxval = (ando << 2) | (o1 << 1) | o0

    return chip_selected(muxval)

Now we should be able to write a self test for this; there's few enough memory addresses that we can test all of them.

def self_test():
    """test all 16-bit addresses against their expected memory chip."""

    for addr in range(0, 8192):
        assert memselect(0, addr) == "ROM"
        assert memselect(1, addr) == None
    print("ROM: OK")

    for addr in range(8192, 32768):
        assert memselect(0, addr) == "RAM0"
        assert memselect(1, addr) == None
    print("RAM0: OK")

    for addr in range(32768, 65536):
        assert memselect(0, addr) == "RAM1"
        assert memselect(1, addr) == None
    print("RAM1: OK")

Hey, look at that - it works!

The memory chips also have a write enable pin, /WE, and an 'output enable' pin, /OE. These tell the chip whether a read or write is occurring. For the ROM, it might make sense not to wire in the write enable line, tying it to ground instead to hardwire a write protect. Either way, you can attach the Z80's /WR pin to all three pins /WE and the /RD pin to all three's /OE pin. Throw in some power and you got yourself a memory bus. Total parts list (apart from the Z80), with digikey part numbers:

Qty Part Digikey Part
1 AT28C256 AT28C256-15PU-ND
2 CY62256 1450-1480-ND
1 SN74HC139 296-8230-5-ND
1 SN74HC08N 296-1570-5-ND
1 SN74HC32N 296-1589-5-ND

Here's a schematic of this, with just enough wiring to show the layout:

memory_layout.jpg
Figure 1: A schematic connecting an EEPROM and two 32K SRAM chips to a Z80.

Finally, there are some things to note: the Z80's pins should still be buffered, which is something for a later post. This also isn't the most efficient use of the space available, as there's an 8K hole that's never used in RAM0's address space and we're also only using a quarter of our ROM space. If we wanted to support multiple 8K ROMs, we could add a DIP-2 switch to address pins A13 and A14 on the EEPROM. Basically:

A13 A14 EEPROM address ROM
0 0 0-1FFFH 0
1 0 2000H-3FFFH 1
0 1 4000H-5FFFH 2
1 1 6000H-7FFFH 3

Just a thought.

1.3. Related links

1.3.1. Z80 computers

1.3.2. Memory management

1.3.3. I/O