Couch to 64k Part 2: Adding ROM to Our Breadboard Z80 Computer

In part one of this series we explored the Z80 processor and found out what various pins do. And we explored a few of the Z80s opcodes by manually feeding them into the data pins.

The circuit we built in part one of this series.
The circuit we built in part one of this series.

But feeding instructions and data in manually is hard work. We need to add some memory so we can feed more complex instructions and even whole programs into the processor.

There are two basic types of memory we can use: ROM and RAM. ROM stands for Read Only Memory. There is a clue in the name, that this is memory which can only be read from, and not written to1. ROM is used to store things such as boot code, which runs when you turn on the computer, and code which allows you to access devices attached to the computer such as the display and keyboard. Many old home computers would have a language, such as BASIC, stored in ROM because loading programs from cassette tape was slow. Storing software in ROM also left more RAM available for user programs.

The other type of memory is RAM, or Random Access Memory. RAM can be both read from and written to but it’s contents are lost whenever the machine is switched off.

At the moment I want to be able to store a program to run on our Z80 and have it persist between sessions, so in this post I’ll be showing how to add ROM to the Z80.

Extra Parts Needed

We’ll need two additional items for this session. See the end of the article for buying links.

  • A ROM chip. I’m using a 28C256 which stores 32k bytes. If you have other chips available, such as the 28C16 used in Ben Eater’s ‘Bentium’ breadboard computer than that will work perfectly well for now. If we get as far as using software which won’t fit in the 28C16’s 2k bytes then it’s easy to upgrade – just leave a bit of space on the board for a couple of extra rows of pins. You can also use chips from other series such as the 27C chips which will work perfectly well but have slightly different pinouts.
  • An EPROM programmer. I recommend the TL866II Plus. If you’re on a tight budget you can use Ben Eaters breadboard EPROM programmer but note that this will need some rewiring to write to anything other than the 28C16, and software will need modifying to make downloading code easier. It also won’t be able to write to anything other than 28C series chips.

Installing the Chip

The finished board after part 2
The finished board after part 2

First, a couple of notes on board layout. I’ve moved the Z80 slightly from where it was positioned in part 1 to make more room for the ROM. (See the photo above). I’ve also installed it with three empty rows of pins below and two above. This positioning will make wiring easier in future parts. There should now be space for the ROM to slip in beside the Z80. We’ll need to be able to pull out the ROM chip for programming, so check that there’s enough space to get your hands or a tool in to do this. Also bear in mind that we’ll be adding a lot of wires which we don’t want to disturb while removing or inserting the ROM. I’ve positioned the ROM where I have so it’s fairly easy to avoid having wires running over the top of it.

I’ve also added a reset button, as per the schematic. The button connects the /RESET line to ground when pushed and has a 1k pull-up resistor to keep it high at other times.

In regard to wiring style, I’m ‘jumping’ wires over the board in an arc (as opposed to laying them flat over the board. To me this makes it easier to run a large number of wires fairly neatly and still be able to trace wiring errors and re-route if needed. It also saves having to cut and bend wires precisely to length and shape. (While I tend to enjoy doing the precision stuff it does take an eternity to do). The downside to the style of wiring I’m using here is that the wires can find it easier to worm their way up out of the breadboard, especially when reaching in to re-route other wires.

Z80 pinout diagram
Z80 pinout diagram
28C256 pinout diagram
28C256 pinout diagram

Start by adding power and ground for the ROM, and remove the resistors from the data pins of the Z80 (but, for now, keep the resistors running from the address pins to the LEDs.

Wire the Address pins of the ROM to the address pins of the Z80, connecting A0 to A0, A1 to A1 and so on. Note that there are less address lines on the ROM than there are on the Z80 so the A15 line of the Z80 will be left unconnected. This is fine. Also notice that the address lines on the ROM are not in sequence. In future parts of this series we’ll have more connections to make on the Z80’s address lines so for address lines connecting above the Z80 I recommend using the row of holes closest to the Z80, for those below I recommend using the central row of the holes left. On the ROM I recommend leaving an empty row of breadboard holes next to the chip which will make removal and re-insertion of the chip easier.

Wire the data pins between the Z80 and the ROM chip to each other (labelled I/O0 to I/O7 on the above pinout for the ROM), again connecting D0 to I/O0, D1 to I/O1 and so on. This time it’s data pins of the Z80 which aren’t in sequence. Again we’ll be adding extra connections to the Z80 in future parts so I recommend using the same breadboard rows as discussed for the address lines.

Who Gets to Drive the Bus?

This brings us to the control lines of the ROM chip. Computers use a bus to exchange data between various devices. We need to make sure that no more than one device is writing to the bus at any one time, and we need to co-ordinate which device should be reading from the bus2. This is what many of the Z80’s control signals are used for.

Most devices will have a pin which tells them to listen to or talk to the bus. This is the enable pin, sometimes called chip enable or chip select, all of which may be abbreviated to E, CE or CS (and other terms may be used, depending on the device).

Our ROM chip uses the /CE line to do that. Since, for the moment, the ROM will be the only memory device attached to the processor we can simply connect the /CE pin directly to the Z80s /MREQ pin. (Note that logic signals are often inverted – they are active when low and inactive when high. This is indicated in documentation by the bar above the name (as in the pinout diagrams above). In print you will often see inverted signals preceded by a / or a ! or a ~ and occasionally by a capital N at the start of the name.)

/WE (write enable) is asserted when writing to the chip. We aren’t going to be writing to the ROM, so the can connect it directly the 5 volt rail.

The final pin to connect is the /OE (output enable) pin of the ROM. This needs to be active whenever we want the ROM to output data to the bus. We want the ROM to output when both the Z80s /RD and /MREQ signals are active. So do we need some logic to ensure they’re both active? Well actually, no. We’ve already connected /MREQ to /CE so the ROM will only be enabled when /MREQ is asserted, and the chip won’t output anything unless it is enabled, all of which means we can connect /OE directly to the Z80s /RD signal3.

The schematic for this is as shown here,

Schematic for Part 2

And here’s another photo of the finished board:

Another angle of the finished board
Another angle of the finished board

Finding an Assembler

In part one of this series we sent instructions to Z80 by passing binary numbers directly into the processor’s data bus. While we could program our Z80 directly using such machine code doing so for anything more than a trivial program would be time consuming and error prone. Instead we can use assembly code or assembler.

Assembler uses human readable mnemonics for the processors instructions and for each instruction’s parameters (such as register names and inline data). The software we use to convert assembly language to machine code is called an assembler.

Most assemblers also have a number of directives to either control their behaviour or add convenient functionality. Directives may, for instance, allow us to add static data to out code, or to turn assembly on or off for different versions of our code.

There is a range of assemblers available for the Z80, each of which has it’s own advantages and disadvantages. Sadly, each one also uses a slightly different syntax, meaning that code written to compile on one may not compile on others. While the actual assembly language used by each is the same things such as the format of hexadecimal constants often varies between them and each has it’s own selection directives and other advanced features.

RASM

For this series I’m going to use an assembler called RASM. This is a fast, modern assembler which I find easy to use and is available for both Windows and Linux. I’m going to avoid using advanced features such as macros so the code will be fairly easy to convert to a different assembler if that is your preference.

To download RASM go to https://github.com/EdouardBERGE/rasm/releases/tag/v1.1 and download the appropriate version for your operating system. I’d suggest you rename the executable to just rasm.exe to make it easier to run. If you’re on Windows I’d also recommend adding the name of the folder it is in to your path environment variable. This will mean you can run it from within any folder. To do this view the guide at https://www.computerhope.com/issues/ch000549.htm (if you have a command window open, you’ll need to close and re-open it for the new path variable to take effect).

Writing Your First Assembler Program

Let’s write and assemble our first Z80 program. Open your text editor of choice, enter the code below and save it as ‘P2.1-JP0.z80’:

;Pt2.1-JP0.z80

org 0           ;Compile code to run at address $0000

loop:           ;Label - an address referenced elsewhere in the code
    jp loop     ;Actual Z80 code - jump to 'loop'

What is this code doing?

  • ‘org 0’ is a directive which tells the assembler to generate code to run starting at address 0. (Some assemblers require directives such as this to start with a period, i.e. ‘.org 0’).
  • ‘loop:’ creates a label which we can reference elsewhere in the code. A label is similar to a constant in high level languages. When created, as here, at the start of a line and followed by a colon it creates a label who’s value is the program address at that point in the program.
  • ‘jp loop’ is a Z80 instruction which jumps execution to the address given by the ‘loop’ label. In other words the Z80’s program counter will be assigned the address ‘loop’ and execution will continue from that address. Thus, in this program the processor will keep executing the same ‘jp loop’ instruction for ever.
  • And comments begin with semi-colons and continue until the end of the line.

Of these lines, ‘jp loop’ is the only actual Z80 instruction, the other lines are instructions to the assembler (or comments or white space).

Assembling the Code

Open a command prompt. On Windows press the Windows key and type ‘Command Prompt’ and run it. Then navigate to the folder where you saved the code. If you updated the Path variable you can now type:

rasm P2.1-JP0.z80

And should see that RASM has created an output file called rasmoutput.bin which is three bytes long:

Successful RASM assembly
Successful RASM assembly

EPROM Programmer Setup

If you have the TL866II Plus EPROM programmer you should have instructions to install the software. If not, go to http://www.xgecu.com/en/download.html and click the link for the “T56/TL866II Plus Programmer Application Software (Chinese/English)”, and then the Download button. This file can be uncompressed with WinRAR. Once uncompressed you can run the installer within. (If you’re not using an administrator account you’ll need to right-click on the executable and select “Run As Administrator”.)

The main XGPro EPROM programmer screen

Run the software (Xgpro). You now need to select the type of IC you will be programming. Click the Select IC button and enter 28C256 (or whatever you are using) in the search box, then select the manufacturer and the exact device you are using. For this project we’re using the DIP version of the chip.

XGPro Select IC Window
XGPro Select IC Window

Before inserting the chip into the programmer ensure the lever is in the raised position. Orientate the chip with it’s ‘notch’ toward the lever and push the lever fully down.

To check we have everything set up correctly we’ll start by reading the contents of the chip. Click the Chip Read button in Xgpro, then click Read in the window that shows.

Hopefully everything will go okay and the message “Read successful!” will appear and the data read back will show in the main screen. If the chip was new the data will be all $FFs. If not you may want to erase the chip with the Chip Erase operation.

XGPro successful chip read
XGPro successful chip read

Now let’s test writing data to the chip. Click the Chip Program button, then the Program button on the window that appears. After a few moments you should see the message “Programming Successful!” displayed.

XGPro successful chip program
XGPro successful chip program

Now that we’ve verified we can read and write to the EPROM we can write our actual code. Click File/Open and select the rasmoutput.bin file that we assembled earlier. You’ll then see the File Load Options window. The defaults should be fine here and click OK.

XGPro load file
XGPro load file

And you should now see the contents of the file in the data display area. Remember that our assembled file was only three bytes long and the screen shows the first three bytes as $C3 $00 $00 with every other byte being $FF.

Our file loaded into XGPro

Send this program to the chip as we did before with the Chip Program operation.

Running your First Program

The chip is now ready to be removed from the programmer and inserted into our computer’s breadboard.

Power up and reset and the code should start running. Inserting the LED’s ground wire (as we did last time) into the /RD line and you should see the address LEDs repeatedly counting from zero to two.

You may remember the $C3 instruction from part 1 of this series. It’s a three byte instruction which jumps the program counter to the address in the second and third bytes. Since we use the address $0000 here both of these bytes are zeroes and the Z80’s program counter jumps to address zero after reading the instruction. Because the instruction is three bytes long the Z80 has to increment the program counter by one between reading each byte.

Running Pt 2.1: program counter while running JP 0 (/RD signal)

Below are some more pieces of code you can play with, which explore more of the Z80s pins and also introduces a few more of the z80’s instructions.

;Pt2.2-Write.z80

org 0

    ld hl,42        ;Load 42 into the HL register pair
loop:
    ld (hl),a       ;Load contents of the A register into the memory 
                    ;address pointed to by HL
    jr loop

This code repeatedly writes data to memory address 42 (%101010 binary) which you can verify with the /WR signal. It also uses a ‘jump relative’ instruction (JR) to do the jump. JR is a two byte instruction (as opposed to the three bytes required for JP) with the second byte being a twos-complement offset from the current program counter address. Thus it can only jump short distances but saves a byte of memory. However, it is also slightly slower than a JP instruction.

Also new here is the LD instruction. This LoaDs the second parameter into the register or memory location given in the first parameter. The Z80 has a wide range of LD instructions but not every combination of registers and locations is available. The brackets around HL indicate that it is specifying a memory address.

Running Pt2.2: address pins while writing to address 42 (/WR signal)
;Pt2.3-WriteRotate.z80

org 0

    ld hl,1
loop:
    ld (hl),a
    rlc l           ;Rotate the L register left, bit 7 goes to both bit 0
                    ;and the carry flag.
    jr loop

This code will also write to memory, but the addresses will start at 1 and double with each write (or in other words, you’ll see a single address bit which is high, and which rotates left with every write). Since we’re only rotating the L register you’ll only see address lines A0 to A7 being used.

Running Pt2.3: Writing to more memory addresses (/WR signal)
;Pt2.4-Input.z80

org 0

loop:
    in a,(255)     ;Read data from I/O port 42 into the A register.
    jr loop

This code inputs from I/O port 255. You can see this by probing the /IORQ signal. (The /RD signal will also be active but the you’ll mostly see memory read requests from the ROM if you probe it.) (Even though we’re only writing to port 255 you’ll also see address pins A8 and A9 here. Technically the Z80 only has 255 I/O ports (lines A0 to A7) it also outputs data on lines A8 to A15 while doing I/O operations but what it’s actually doing is outside the scope of this article).

Running Pt2.4: Address pins while inputting from IO port 255 (/IORQ signal)
;Pt2.5-Output.z80

org 0

loop:
    out($96),a     ;Output contents of the A register to I/O port 42
    jr loop

And this is the same code but writing to I/O port $96. Test with /IORQ and /WR.

Running Pt2.5: address pins while outputting to port $96 (/IORQ signal shown here. /WR will give the same output)
;Pt2.6-ConditionalOutput.z80

org 0

    ld a,0       ;Zero the A register
loop:
    xor 1        ;Toggle bit 0 of A
    jr z,zero    ;Just to 'zero' if the z (zero) flag is set

    out (42),a
    jr loop

zero:
    out (84),a
    jr loop

And to finish some code which outputs to either port 42 or port 84 depending on the value in the A register. XOR is the exclusive-or instruction which is being used to toggle bit 0 of A with every iteration of the loop.

The ‘z’ in the following JR instruction tells the Z80 to test the zero flag and only jump if it is set (i.e if the result of the previous operation was zero).

Running Pt2.6: address pins while to port 42 or port 84 (/IORQ shown here, /WR will give the same output).

Visit my Github for source code, binaries and schematics.

Wrapping Up

At this point we’ve explored a few of the Z80’s instructions but our little Z80 is very restricted in what it can do with only a ROM chip connected. Next time we’ll see how to attach an LCD display so out little processor can talk to the humans around it.

Until next time, have fun.

Buying Links and Advice

Link marked (Aff) are affiliate links

Footnotes

  1. While some ROM is programmed at the factory and can never be changed other types can be reprogrammed but such ROMs may be difficult to reprogram, or may only have a limited number or write cycles before the chip turns bad.
  2. Of course if the processor is reading or writing then signals are not needed for the processor itself, but there may be situations when two devices are talking to each other without involving the processor, such as DMA. The processor has additional pins (BUSRQ and BUSAK) which are used to stop the processor trying to use the bus at the same time
  3. We’ll need to redo this when we add some RAM and need to discriminate between accesses to ROM and RAM.

One Reply on “Couch to 64k Part 2: Adding ROM to Our Breadboard Z80 Computer”

Leave a Reply

Your email address will not be published. Required fields are marked *