FPGABee is a hardware emulation of a Microbee - the classic Aussie home computer of the 80's!

Building FPGABee - CPU Environment

Wednesday, July 18th, 2012     #version1 #everything

Getting Started

FPGABee is the first project I've developed for an FPGA so to get started I first knocked together a few simple projects to respond to switches and buttons, light up leds, learned about clocks and counters, basic VHDL syntax and made myself familiar with the development environment and tools. Once I had a grasp on the basics the first step was getting the Z-80 CPU core up and running.

(VHDL is a language for designing digital circuits and what FPGABee is written in.)

Clock and Reset

Once you get into digital electronics a little the first thing you'll notice is that clock and reset lines are everywhere. Clock signals are used for synchronization and driving anything sequential, while reset is for initializing everything to a known startup state.

The Microbee ran at a clock speed of 3.375Mhz. The Nexys3 board has a 100Mhz clock but includes "Digital Clock Management" (DCM) tiles that can multiply/divide the clock frequency to other frequencies.

To set this up the Xilinx development tools include a "Core Generator" - a tool that can generate components that utilize various features of the FPGA chip. So to get a 3.375Mhz clock was simply a matter of running the Core Core Generator, entering the required frequency and clicking the generate button. It got close - 3.373Mhz.

A few lines of code in the main FPGABee module and I had a suitable clock for driving the CPU:

-- Clock Generation
clock_core : entity work.ClockCore
PORT MAP 
(
    clock => clock,                  -- input clock = 100Mhz
    clock_3375 => z80_clock,     -- 3.375Mhz for main z80 clock (DCM actually delivers 3.373)
    RESET  => reset
);

(note that the code examples in these articles are extracts only with non-relevant parts omitted for clarity)

For reset I've simply wired it to the center push-button on the Nexys3 board. (and by "wired" I mean written a line of code to do that - no soldering irons involved).

Hosting the Z-80

T80 is an open source implementation of the Z-80 written in VHDL. Using the Z-80 User Reference Manual, it was pretty easy to figure out how to interface it.

Unlike a real Z-80 that uses a bi-directional data bus, the T80 has a separate bus for data-in and data-out which makes it a bit easier to use. The data-out bus is simply wired to each of the memory and I/O components. The data-in bus is multiplexed from the same set of components using the Z-80's control and address lines to work out which component to read the data from.

The main signals from the Z-80 are:

  • Clock and Reset - as above.
  • Address - 16 bit address.
  • Data In - 8 bit data read bus.
  • Data Out - 8 bit data write bus.
  • Memory Request Signal - Indicates the CPU wants to read or write from memory.
  • I/O Request Signal - Indicates the CPU wants to input or output from an I/O port.
  • Read Signal - Memory or I/O request is a read.
  • Write Signal - Memory or I/O request is a write.
  • All the other signals were simply left open (for outputs) or tied to 1 (for inputs).

Note that most of the control lines are active low where zero means true. Active low is indicated by a _n suffix on the signal.

-- Z80 CPU Core
z80_core: entity work.T80se 
PORT MAP
(
    RESET_n => reset_n,
    CLK_n =>  z80_clock,
    A => z80_addr,
    DI => z80_din,
    DO => z80_dout,
    MREQ_n => z80_mreq_n,
    IORQ_n => z80_iorq_n,
    RD_n => z80_rd_n,
    WR_n => z80_wr_n,
);

So for example if the Memory Request and Read Flags are active (zero), the CPU wants to read 1 byte from whatever address is on the the address bus and it expects the value to be available on the data-in bus on the next clock cycle. Memory writes and I/O operations are all very similar.

Hooking up RAM

I decided to set up FPGABee with 32K of RAM. The Nexys3 board has a number of options for memory:

  1. FPGA Block Memory - about 70K of memory blocks in the FPGA chip designed for local fast storage.
  2. FPGA Distributed Memory - essentially flip-flops distributed around the FPGA used primarily for registers. About 17K available.
  3. On Board Cellular RAM - off-chip memory, 16Mb, harder to interface.

For simplicity I decided to use the block memory and ran up the Core Generator again, specified the about of memory (32K) and the data width (8 bits). A few more lines of VHDL to instantiate it and hook it up to the Z-80:

ram_core : entity work.Ram
    PORT MAP 
    (
        clka => z80_clock,
        addra => z80_addr(14 downto 0),
        dina => z80_dout,
        wea => ram_wea,
        douta => ram_dout
    );

Note that the memory matches perfectly with what the CPU is expecting... on the next clock cycle it will return the byte stored in the address indicated by the address lines. If the write enable (wea) line is active, it writes instead of reads.

Next was the control signal and address decoding. To start with I resolved the CPU's control lines into memory read and write signals: (don't forget the _n suffixes mean active low)

mem_rd <= '1' when (z80_mreq_n = '0' and z80_iorq_n = '1' and z80_rd_n = '0') else '0';
mem_wr <= '1' when (z80_mreq_n = '0' and z80_iorq_n = '1' and z80_wr_n = '0') else '0';

Next it checks if the address is within the first 32K by simply looking at the top bit:

ram_rd <= '1' when (mem_rd = '1' and z80_addr(15)='0') else '0';

And finally the data-in bus is populated by multiplexing from the different memory and I/O components that the CPU is connected to (for the moment, return 0 if out of address range):

z80_din <=      ram_dout when (ram_rd = '1') else
            -- other data sources ommitted
                            x"00";

You'll see the above line of code get more and more complex as I add other memory and I/O components.

(In retrospect, while writing this article I've realized that multiplexing is probably not the best way to implement this. It should probably be using Chip Select or Chip Enable signals, but that's for another day).

When writing to memory it's simply a matter of setting the RAM core's write enable line:

ram_wea(0) <= '1' when (z80_addr(15)='0' and mem_wr = '1') else '0';

First Run

It's a long way from a working Microbee, but the next step was to test whether the Z-80 was actually running. To do this I hooked up I/O port zero to the 8 on-board LEDs to see if I could get them to light up.

In order to implement a Z-80 output port we need to check the control lines when the clock ticks. In VHDL a process is something that "runs" when one of the declared signals changes, so the following process runs whenever the z80_clock or reset signal changes.

process(z80_clock, reset)
begin
    if reset='1' then

        led_reg <= "00000000";

    elsif (z80_clock'event and z80_clock='0') then

        -- Write port 0 -> LEDs
        if z80_addr(7 downto 0)=x"00" and port_wr = '1' then
            led_reg <= z80_dout;
        end if;

    end if; 
end process;

Next a simple Z-80 program. I found a Windows based assembler that I use for these test scripts:

ld  a,0AAh  ; every second led
out (0),a
halt

As part of generating a memory core, you can specify the initial contents of the RAM, so I used this to load up the above program and got it working pretty much straight away.

Implementing ROM

The Microbee's 16K of BASIC ROM starts at address 0x8000. Additional ROMs for EDASM, or WordBee were often also installed and these occupied the space from 0xC000 up to 0xF000.

For my first attempt at setting up the ROM I used the block memory core again, but because I was trying to load various programs into to test things, this soon became tiresome - the Xilinx build tools are very slow (several minutes typically).

So I started to look into how to use the on-board flash memory which can be directly written to using utility programs that come with the board. By avoiding the build step I could make the whole process a lot quicker. The other reason for doing this is that I wanted to keep some of the FPGA block memory available for video memory.

The Nexys has 2 kinds of flash memory

  1. 16Mb Parallel PCM Non-volatile Memory
  2. 16Mb SPI PCM Non-volatile Memory

Connecting the Parallel PCM memory is similar to using the core generated memory modules, but instead of instantiating a VHDL component, the control, data and address lines are hooked to the I/O pins on the FPGA which are in-turn wired to the flash memory:

-- 28K ROM at 0x8000 - 0xEFFF is read from external flash
flash_rd <= '1' when (mem_rd = '1' and z80_addr >= x"8000" and z80_addr<=x"efff") else '0';
MemOE <= NOT flash_rd;
MemWR <= '1';
FlashRP <= reset_n;
FlashCS <= NOT flash_rd;
MemAdr(14 downto 1) <= z80_addr(14 downto 1);       -- (addr & 0x7FFF)>>1
MemAdr(26 downto 15) <= "0" & RomFlashAddress(26 downto 16);
MemDB <= "ZZZZZZZZZZZZZZZZ";

The MemAdr is manipulated to offset it to 100000h. This allows room for an FPGA bit image to also be stored in the flash memory if needed. (A bit image is the compiled VHDL circuit of what should be programmed into the FPGA on power up.)

The external memory uses a bi-directional data bus, so in order to be able to read the data bus the FPGA needs to set the data lines to "high impedance" which is what the string of "Z"'s means. I tend to think of high impedance as simply meaning "disconnected", so here we're telling the FPGA to disconnect any drivers from the MemDB bus, making the bus available to external components to send signals.

Also note that the flash memory has a 16-bit wide data bus so the address needs to be halved - achieved by using z80_addr(14 downto 1) instead of (14 downto 0) and the correct half of that word needs to multiplexed back to the CPU:

z80_din <= 
        -- other inputs ommitted
        MemDB(7 downto 0) when (flash_rd = '1' and z80_addr(0)='0') else
        MemDB(15 downto 8) when (flash_rd = '1' and z80_addr(0)='1') else
        x"00";

As a side note, it took me ages to get this to work but it came down to a simple error where I neglected to connect the MemOE signal. MemOE is "Output Enable" - so of course reading wasn't working. Lesson learned.

I proved this was working by loading a Z-80 program into the flash memory that output a different LED pattern and configured the RAM to do a jump to 0x8000.

Once working I spent a little time just refreshing my memory on Z-80 assembler and generating flashing LED patterns.

Microbee Boot Scan

On power up or reset, the Z-80 starts executing from address 0x0000, but the Basic ROM starts at 0x8000. In order to boot correctly and not execute any garbage that might be in RAM, the Microbee watches for the first memory access at 0x8000 or higher. Until it sees that it returns zero for all memory read requests. Zero is the Z-80's instruction code for nop or "do nothing" so on boot the CPU "screams" through the lower 32K of address space doing nothing until it hits 0x8000 - at which point it starts executing the ROM code.

(Apparently the reason for this is to keep the lower address space free for RAM which is needed to boot the CP/M operating system - which the later Microbees did in fact run)

Implementing this in FPGABee was fairly trivial. Just watch for a memory read at 0x8000 or higher:

-- On first read of a memory address >= 0x8000, clear the boot scan flag
if z80_addr(15)='1' and mem_rd = '1' then
    boot_scan <= '0';
end if;

and until then, return zero to the CPU:

z80_din <=  x"00" when (boot_scan = '1') else
                ram_dout when (ram_rd = '1') else
                -- etc...

I tested this by loading a program to output one LED pattern into low RAM, and another into ROM and checking only the ROM one ran.

CPU Environment Finished

By now I had the basic CPU environment up and running and was getting bored with flashing LEDs. Next was to connect a monitor, which will be the topic for Building FPGABee - Part 2 - Video Controller


« Older - Building FPGABee - Video Controller Newer - Building FPGABee - CPU Environment »