Z80 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 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 a 32KB parallel EEPROM [1] with a parallel SRAM chip [2]. This RAM chip is also 32KB; in order to get the full address space, we need two of them. 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
| 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
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 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 an OR gate [3], and connect that to A0. Note that you should connect the unused gate pins to ground [4]. 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 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 an AND gate [5]. 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:

Schematic of the circuit in this post.

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]An AT28C256 EEPROM.
[2]A CY62256 SRAM chip.
[3]Otherwise the electric pixies might get a little confusticated and agitate the signals what are coming from the Z80 - you have to give them somewheres to lie down and take a nap.
[4]For example, the SN74HC32N quad 2-input OR gate.
[5]For example, the SN74HC08N.