Pixelis Arcanum
contact rss


⏮ Testing the Z80, Part 3 *

May 22, 2026

Testing the Z80, Part 4

Well, well, well! Look who decided to show up! If it isn't the new Z80 we've been waiting for!

Z80 CMOS

First things first, to the test board! We know the drill: plug it onto the test board which is wired to the Arduino Mega that will run the test. The same test we've used before on the older NMOS Z80, running an infinite repetition of NOP instructions to verify that our machine cycles are sound. And, as expected, all is well.

Let's take things a bit further. Instead of executing NOP instructions ad nauseam, let's try one of the instruction examples from the Z80 user manual. The first example is a string transfer. It is simple and will force us to learn how to read and write back bytes in memory. We've already figured out the machine cycles, now is the time to figure out the read and the write cycles.

Here is the listing, it copies a 737-byte string from the address DATA to the address BUFFER.

LD HL, DATA     ;START ADDRESS OF DATA STRING
LD DE, BUFFER   ;START ADDRESS OF TARGET BUFFER
LD BC, 737      ;LENGTH OF DATA STRING
LDIR            ;MOVE STRING–TRANSFER MEMORY POINTED
                ;TO BY HL INTO MEMORY LOCATION POINTED
                ;TO BY DE INCREMENT HL AND DE,
                ;DECREMENT BC PROCESS UNTIL BC = 0

We have to convert this code into byte code and store it on the Arduino. The Arduino will send it to the Z80, one byte at a time. It will also fake the memory to hold the string data and the destination buffer. We are going to modify this example to use a shorter string, Frankenstrad 6128, which is 18 byte long. We will also add an extra instruction at the end to halt to execution.

To store the code and the data, the Arduino is going to fake the memory. This is a simple C array big enough to host a code segment and a data segment. The code will need 12 bytes, and the data 36 bytes to store both buffers. Let's round up these numbers for good measyre to 16 bytes for code and 48 bytes for data. 64 bytes total.

uint8_t memory[64];
uint16_t code_segment = 0;
uint16_t data_segment = 16;
uint16_t source_address = data_segment;
uint16_t destination_address = source_address + 24;

void setup()
{
    strcpy(&memory[source_address], "Frankenstrad 6128");
    strcpy(&memory[destination_address], ".................");
}

With all that in mind, let's modify the example:

LD HL, 10h      ;Start address of source
LD DE, 28h      ;Start address of destination
LD BC, 12h      ;Length of data string
LDIR            ;Move String
HALT            ;Stop here (so that we don't try execute what is in the data segment)

Let's translate this into byte code.

The LD instructions load 16-bit values (the second operands) into a pair of 8-bit registers (the first operands). The first operand is embedded in the first byte of the op code.

Destination Op Code
BC 01h
DE 11h
HL 21h

The second operand, the 16-bit value, is split into the two next bytes of the op code, low-order byte first.

Instruction Op Code
LD HL, 0010h 21 10 00
LD DE, 0028h 11 28 00
LD BC, 0012h 01 12 00

Next is the LDIR instruction. It has no operand and its op code is EDB0h.

And finally we have the HALT instruction, whose op code is a single byte 76h.

We have all our 12 bytes of code to populate our code segment. Since our code segment is conveniently mapped at the address 0000, we can just initialize our memory with it. The data will be mapped after that at address 16.

uint8_t memory[64] =
{
    0x21, 0x10, 0x00,   // LD HL, 0010h
    0x11, 0x28, 0x00,   // LD DE, 0028h
    0x01, 0x12, 0x00,   // LD BC, 0012h
    0xed, 0xb0,         // LDIR
    0x76                // HALT
};

We byte code is ready to be sent to the Z80, now we need to determine when to send it. In part 2, we were sampling the address lines on the falling edge of the clock while the read signal was active. We knew that time to be safe to read the program counter only once per machine cycle. But now things are different. Whatever we do must be valid not only for the machine cycle, but also for the read and write cycles. What really matters to us now is to know when we should access the data bus to meet the CPU requests.

Machine Cycle

Read and Write Cycles

Looking at these two diagrams, the falling edge of the clock, in blue, looks like a good moment to sample the address bus. Once it's been sampled by the Arduino, we can use it to read or write the data bus on the rising edge of the clock. If the read signal is active, on the green lines, we can write the data we have in memory onto the data bus to serve it to CPU. Likewise, if the write signal is active, on the orange line, we can sample the data bus, filled by the CPU, and store it in our memory.

So really, all we need to do is sample the address bus on the falling edge of the clock and access the data bus as needed on the rising edge of the clock. I like how simple this is on the Arduino, almost mechanical.

void loop()
{
    // Keep track of the time
    uint32_t half_clock_period_end = micros() + clock_period_in_us/2;

    // Rising edge of the clock
    digitalWrite(clock_pin, HIGH);

    // Check if the CPU wants to read data from memory
    if (digitalRead(read_pin) == LOW)
    {
        write_data_lines(memory[address_bus]);

        print_format("Reading [%04x] %02x", address_bus, memory[address_bus]);
    }

    // Check if the CPU wants to write data to memory
    if (digitalRead(write_pin) == LOW)
    {
        memory[address_bus] = read_data_lines();

        print_format("Writing [%04x] %02x", address_bus, memory[address_bus]);
        print_format("%16s -> %16s", (char*)&memory[source_address], (char*)&memory[destination_address]);
    }

    // Wait until the end of the half clock period
    while (micros() < half_clock_period_end)
    {
        yield();
    }
    half_clock_period_end = micros() + clock_period_in_us/2;

    // Falling edge of the clock
    digitalWrite(clock_pin, LOW);

    // Sample the address bus
    address_bus = read_address_lines();

    // Wait until the end of the clock period
    while (micros() < half_clock_period_end)
    {
        yield();
    }
}

And here we go, the string is getting copied!

Initializing pins
Reset Sequence
Ready
SRC 'Frankenstrad 6128'
DST '.................'
Reading [0000] 21
Reading [0001] 10
Reading [0002] 00
Reading [0003] 11
Reading [0004] 28
Reading [0005] 00
Reading [0006] 01
Reading [0007] 12
Reading [0008] 00
Reading [0009] ed
Reading [000a] b0
Reading [0010] 46
Writing [0028] 46
Frankenstrad 6128 -> F................
Reading [0009] ed
Reading [000a] b0
Reading [0011] 72
Writing [0029] 72
Frankenstrad 6128 -> Fr...............
Reading [0009] ed
Reading [000a] b0
Reading [0012] 61
Writing [002a] 61
Frankenstrad 6128 -> Fra..............
Reading [0009] ed
Reading [000a] b0
Reading [0013] 6e
Writing [002b] 6e
Frankenstrad 6128 -> Fran.............
Reading [0009] ed
Reading [000a] b0
Reading [0014] 6b
Writing [002c] 6b
Frankenstrad 6128 -> Frank............
Reading [0009] ed
Reading [000a] b0
Reading [0015] 65
Writing [002d] 65
Frankenstrad 6128 -> Franke...........
Reading [0009] ed
Reading [000a] b0
Reading [0016] 6e
Writing [002e] 6e
...
Frankenstrad 6128 -> Frankenstrad 61..
Reading [0009] ed
Reading [000a] b0
Reading [001f] 32
Writing [0037] 32
Frankenstrad 6128 -> Frankenstrad 612.
Reading [0009] ed
Reading [000a] b0
Reading [0020] 38
Writing [0038] 38
Frankenstrad 6128 -> Frankenstrad 6128
Reading [0009] ed
Reading [000a] b0
Reading [0021] 00
Writing [0039] 00
Frankenstrad 6128 -> Frankenstrad 6128
Reading [000b] 76
Reading [000c] 00

It is... beautiful! Time to pop the Z80 out of the testing board and take it to the "real" board. We are also going to carry over these new timings to the ESP32 code to sample the address and data bus.

And this concludes our testings of the Z80. Actually, we think we've been testing the Z80 but, really, the Z80 has been testing us. The CPU had no problem doing its thing, we did! We were the test subjects all along, and I'm glad to see that we've passed the test. We are worthy to continue!

What's next? We grab the ROM from the CPC 6128, dump it onto our EEPROM, run it, and get the Ready screen.


⏮ Testing the Z80, Part 3 *