Introduction
How do you go about developing a replacement for a chip? In this particular case, how do you go about creating a replica Z80? Well, you test and test and test of course.
But, how do you do that? The beginning is fairly easy: you compare against datasheets and other sources of documentation. You run small test programs working your way up to more complex ones, eventually running full end-to-end environments. If you are lucky, you will also have test programs developed to test compatibility.
But the last mile - so to speak - is the hardest. Eventually you will have to test your creation in a real system. That will expose it to all sorts of corner cases and special cases and bugs and limitations that none of the sanitized test environments can give you.
That's more or less where I stand with the UnIC project. The particular issue at hand is a much longer topic, something that's still unfolding. Suffice to say, that my (T80-based) Z80 re-creation can't really reliably handle floppy disk reads in the PCW8256. How do you go about debugging this problem? How do you generate tests, how do you compare results, how do you methodically get to the bottom of an issue like that?
Shadow Tracer
I came up with the following idea: if indeed it is the case that the device under test (T80-based UnIC in this case) is a cycle-accurate replica of the real thing, they should - given the same input stimuli - generate the same responses.
One could therefore give the same inputs to the reference and the replica and compare their reactions. For various reasons the reference would be in the driver seat, that is, its outputs would be connected to the rest of the system, and the replica would be compared against that. The replica would be shadowing the operation of the reference, while comparing the reactions of the two. Tracing out an execution in that environment would give us clues where - and why - their two paths become divergent.

This is the idea behind Shadow Tracer. The implementation comes in two parts:
A small PCB (manufactured by my sponsor PCBWay) would facilitate the connection of the relevant signals.
The core inside UnIC would contain not only the replica implementation but the comparator logic as well.
The HW piece
I've quickly created the schematic and PCB design. Most of the pieces I already had laying around, KiCADs extensive part library made the process rather quick, and - let's be honest - this is a rather trivial piece of fiberglass and metal.

PCBWay fabricated the boards. The usual disclaimer: they are sponsoring me by providing free of charge manufacturing services. This, I highly appreciate not just because it's always nice to get free things but because their work is of excellent quality and their turnaround is fabulous. They provide not only PCB fabrication services but a whole host of others as well, ranging form PCBA assembly, 3D printing, metal fabrication and injection molding. I only have personal experience with the PCB/PCBA services they offer, but so far I have been happy with the results. If you need something to be made, check them out! It's quite likely they can do it for you.
Anyway, back to the main story: the boards showed up quickly.


A few things probably need explaining here: The problem - or a strength depending on your point of view - with UnIC and the project in general is that GND and power pin locations are not well defined. As such, it's not clear how to connect ground planes on the PCB. My solution to that was to remove the solder mask from some sections of the plane: this allows me to add solder-bridges for the GND pins for a particular project. In this case, the Z80 has the ground on pin 29, so that's where the solder bridge would go.
The second is the break-out connector on the left. UnIC has an FPC connector where several otherwise unused FPGA pins are exposed. For the purposes of Shadow Tracer this can be used to surface internal logic that then can be connected as trigger-sources to scopes and logic analyzers. It also gives access to the JTAG configuration pins for reprogramming and to communicate with the embedded logic analyzer (GoWin calls it GOA).
I put the board together or rather, I tried. This is when I learned that the ZIFF sockets I had had have wider pins than the holes that the KiCAD library defines for DIP sockets. What a pain! I don't think there will be another revision of the board, but it's an important lesson for the future: if I ever want to use ZIFF sockets in a design (or even just the option to swap them in) I should have bigger drills.
By applying copious amounts of force, I managed to push the sockets into the PCB and only destroying one of them in the process. That's ... success? Soldering the rest of the stuff together was a breeze; as I've said, this is not a complicated design. This statement needs a caveat: I didn't end up putting together the FPC break-out connector piece. Instead, I've created a different design for that which included the FTDI-based JTAG programmer as well; I'll save that for a different post; the only thing I would mention here is that the FPC connector is a major pain to work with; had I tried to put it on this board, I would not have said that assembly was easy.
When everything is put together, it looks like this in my PCW8256:

The comparator
Modifying the FPGA firmware to include the comparator is relatively straightforward. All the outputs needed to be redefined as inputs, the actual logic instantiated internally and the two sets of signals compared. This is how I compare the control signals for instance:
assign ctrl_match =
(M1_n_i == M1_n) &&
(MREQ_n_i == MREQ_n) &&
(IORQ_n_i == IORQ_n) &&
(RD_n_i == RD_n) &&
(WR_n_i == WR_n) &&
(RFSH_n_i == RFSH_n) &&
(HALT_n_i == HALT_n) &&
(BUSAK_n_i == BUSAK_n);
Comparing the data-bus is slightly more involved, it being a bi-directional affair. The trick here is to know when the bus is supposed to be driven and only compare the two versions in those cases.
assign data_match = (CPU_DO == D) | DO_EN_n | (MREQ_n_i && IORQ_n_i);
Finally, some trickery is needed to register all these signals: simple logical comparison results in a lot of false positives due to propagation delay differences. It is true though that all those differences must settle before the next clock edge. Registering the signals is made a little more complicated by the fact that the Z80 uses both edges. Not that much more complicated though:
always @(posedge CLK_n) begin
rise_match <= ~RESET_n | ctrl_match & data_match;
end;
always @(negedge CLK_n) begin
fall_match <= ~RESET_n | ctrl_match & data_match;
end;
logic match;
assign match = rise_match && fall_match;
Findings
The first thing I found was that the T80 comes out of reset one cycle too early relative to the Z80. This is not a biggie and trivial to fix. On the topic of reset, I've just learned about a rather cool feature of the Z80: it had a special reset feature that could be used for in-circuit debugging purposes. I have no idea if the T80 implements this. But let the be the biggest of the differences.
Actually, the first, first thing was that nothing worked. The Z80 wouldn't boot, let alone the comparator part. Some hear-tearing-out later I realized the issue was the clock. The PCW included a large ferrite bead in series with the 4MHz CPU clock (which they conveniently don't show on any of the published schematics). This is there I'm sure to meet some FCC emission criteria but it makes the rising and falling edges so weak that any extension of the clock lines make the unusable. Shorting the bead improved things dramatically. Thanks, FCC!
Conditional jumps
Conditional jumps in the Z80 take the following form:
JR Z,nn
The 8-bit nn field is a PCB-relative offset and the condition (in this case) is the zero flag being set.
The timing of the instruction depends on whether the jump is taken or not. If it is taken, execution takes an extra 5-cycles; this group of 5 cycles is called an ‘M' or machine cycle. During these 5 cycles no bus access is generated, something that is rather usual for the Z80: it normally generates a bus access of some sort in every ‘M' cycle. So far so good, the T80 was a faithful replica.
The PCW however uses wait-states and generates wait-states rather blindly, using what appears to be a simple counter. The point is: wait states are generated even if no bus access is in progress. This is unusual: normally, logic would not know if wait-states are needed until it completed address-decode, thus would only generate wait-states during active bus-cycles. As I have said, not the PCW.
The problem is that the T80 monitors the WAIT_n line, even when no bus activity is generated in an M-cycle. Thus, in this particular instance, it delays the completion of the conditional jump by a lock cycle. The Z80 – properly – ignores the WAIT_n line and completes the jump in exactly 5 cycles.
The fix is not terribly complicated, though it took me a while to get all the ripple-effects right.
Exchange operations
The Z80 is a CISC processor. It can do some rather crazy things in a single instruction. One of these instances is the following instruction:
EX (SP),HL
This takes the 16-bit register pair, HL and writes it to the memory location pointed to by the stack pointer SP. At the same time, it reads the old value from the same memory location and puts that into the HL register pair. In other words, it exchanges the two values.
The swapping needs two reads and two writes one each to two adjacent memory addresses.
As all swaps, it also needs some temporary storage inside the processor.
The T80 implementation followed the following pseudo-code:
temp = mem(SP)
mem(SP) = H
H = temp
SP = SP + 1
temp = mem(SP)
mem(SP) = L
L = temp
SP = SP - 1
The Z80 does the following:
temp1 = mem(SP)
SP = SP + 1
temp2 = mem(SP)
mem(SP) = L
SP = SP - 1
mem(SP) = H
H = temp
L = temp
I guess the list of steps here underscores just how complex this instruction is.
Of course both accomplish the same results but the bus behavior is different. The fix is not terribly complicated as the Z80 already has two 8-bit temporary registers for purposes like this, something that the T80 replicated as well.
Read-modify-write instructions
Riffing on a similar point: the Z80 has a number of other read-modify-write operations. These on a modern processor normally involve registers only, but the Z80 could do them on memory addresses as well.
I noticed earlier that the T80 generated slightly incorrect bus-timing for these cases, asserting MREQ and presenting the data for the write portion half a cycle later than the datasheet prescribed it. I fixed most but missed a pair:
INC (HL)
DEC (HL)
These increment and decrement the value at memory location (HL) respectively.
The fix, especially because I had done most of the work for other, similar instructions was rather trivial.
On to the hard stuff: interrupts
The Z80 was not a fully specified processor. What I mean by that is that the instruction decode didn't take care of doing something well defined for every possible instruction code combination. Some were specified in the datasheet and those worked (barring documentation or implementation bugs) as it was described, but you could throw something at the processor that wasn't a valid instruction. Modern processors normally throw some sort of exception (undefined instruction or something similar) in those cases. The Z80, resource-constrained as it was, didn't have that luxury. It ended up doing ... something. These became known as undocumented instructions. Most do nothing useful, some do. People went to great lengths to uncover and document this behavior and the T80 developers were rather meticulous in implementing them.
One of the more curious undocumented features of the Z80 was the presence of two undocumented flag bits. These ended up being called X and Y flags and they reside in bit positions 3 and 5 of the F (flags) register respectively.
They - as all other undocumented features - are a simple side-effect of the Z80 implementation and it's micro-code, but the end result is nevertheless the same: two extra flags doing all sorts of weird things. Again, people documented them and the T80 implemented them. Great!
Except...
What happens to these flags during interrupt processing?
The Z80 has an instruction, called LDIR. It's one of those crazy CISC instruction. It performs a block memory copy. In a single instruction. Even more crazy is that this instruction can be interrupted (and resumed) mid-way through execution. It also happens to be one of those instructions with some rather bizarre X and Y flag behavior.
Now, consider the following sequence of events:
1C41 ED B0 LDIR <--- long copy, gets interrupted
0038 C3 A1 FD JP FDA1 <--- interrupt handler
FDA1 ED 73 A3 FE LD (FEA3), SP
FDA5 31 4A FF LD SP, FF4A
FDA8 F5 PUSH AF
...
In other words, we catch an interrupt in the middle of an LDIR operation, where the interrupt handler – sensibly – starts saving the context and pushes the A and F registers onto the stack.
What I have found is that the X and Y bits in the pushed F (flags) register have the wrong values on the Z80. Wrong, as in they are not what they should contain if the LDIR operation was interrupted at the point it was. The T80 faithfully preserves these flags.
My current hypothesis is that the Z80 inverts both of these flags if it interrupts this instruction. Why? Who knows, but – again – this is undocumented behavior, it's unlikely that it was intentional on the designers part.
Replicating this behavior in the T80 is going to be difficult, even if my hunch is correct. It does not appear to be the case that the X and Y flags are inverted on every interrupt.
The impossible: data sampling point
When the Z80 generates a read (in this case I/O read, but that's not all that relevant) bus transaction, it needs to sample the incoming data at some point.

In the datasheet this sampling point is specified as the last falling edge of the clock before the end of the read transaction. The setup time (25 in circle) is defined as 50ns for the lowest speed-grade Z80. Hold time, interestingly is defined relative to the rising edge of RD_n (16 in circle) but it's set at 0. Finally the release of RD_n is also specified relative to the same clock edge (23 in circle) at 85ns, maximum. Since no minimum is defined, it is possible - or valid, rather - for RD_n to get release immediately on the rising edge, so for all intents and purposes the hold time for data is 0ns.
If an external piece of HW can't guarantee these setup and hold times, it can extend the operation by pulling WAIT_n low. All very sensible.
Enter the uPD765 floppy disk controller chip. For it's part, it specifies the following bus transactions:

Here the setup time (T_RD) is 200ns and the hold time (T_DF) is 20ns.
To put these together one needs to know the minimum duration of the low cycle on the floppy controllers' RD_n. This is not generated by the Z80 directly in this machine: it is generated by the ULA. Nevertheless, I've measured it and it's falling edge is delayed by one clock cycle (250ns) from the Z80. The rising edge is not delayed.
The uPD765 has a status register. To read it, the full chain of events is the following:
- Z80 generates a read transaction, lowering RD_n
- 250ns later the ULA lowers RD_n for the uPD765
- 200ns after that the response from the uPD765 should be stable on the bus
- Some time later the sampling clock edge on the Z80 samples the data
- Z80 finishes the transfer by de-asserting RD_n, which is then copied by the ULA as well
As long as that 'some time' in step 4 is at least 50ns, we don't have any timing violations. For that to be the case, the Z80 I/O read cycle should be at least 500ns long. The Z80 I/O cycle is 625ns long. So we're good, right?
Unfortunately no. The uPD765 has one crucial status bit we care a whole lot about: the RQM bit, which is it's 7th bit:

This is a handshake bit, essentially telling the SW whether it's safe to mock around with the state of the floppy controller. If it's zero, it's forbidden to change things, if it's one, it's OK.
So the PCW spends a significant part of it's life monitoring this bit.
Here's the kicker: the RQM bit changes asynchronously during the read operation. It doesn't adhere to the 200ns setup delay:

Here you can see D7 (channel 2) getting driven to 0 for a while during the status register read (RD_n is on channel 4), then changing it's mind mid-read and go high.
You can see the measurement: the change happens 400ns into the read, in clear violation of the stated 200ns of the datasheet. Thanks NEC!
This results in a setup time violation on the Z80. Normally this is not a big deal (provided we are not afraid of meta-stability). It simply means that we don't know for sure if we captured the previous value (0) or the new one (1). Since we are guaranteed to not do anything floppy-controller related if it reads zero (other than trying it again), this is fine. We might just delay things a little.
The problem is that this decision might go in different ways in the reference Z80 and the UnIC FPGA-based T80. It's impossible to fine-tune the sampling point to match that of the Z80 for several reasons, I'll just mention one here: it varies from part to part and with temperature or supply voltage. So, this is a difference that can't be rectified and will always show up as an execution delta between the reference and the replica in Shadow Tracer.
The solution most likely is it to special-case this situation in the comparator and ignore it. The trouble of course is that the execution paths become divergent. There is a cascade of differences afterward. The good news though is that eventually (empirically at least) the paths converge again and further tracing is possible.
Are we there yet?
No, not yet unfortunately. Even after all these fixes, the boot problem on the PCW still remains. Communication with the uPD765 is still shaky resulting in hangs.
The problems with interrupts and the status-check loop I mentioned above also mask other potential issues: they trigger the logic analyzer which takes a very long time to re-activate potentially missing other important discrepancies.
I will have to fix or patch up these problems and make the comparator clean before I can make forward progress.
Still, given short a time I spent with this tool (a few days, on-and-off), I'm very happy with how Shadow Tracer turned out.
It is a very useful tool, something that will pay large dividends in the future: the same process I described here can be repeated for any future project where UnIC is used to replicate the behavior of another chip. Maybe someday I get around replicating a uPD765 and while at it, fix the dastardly RQM status bit.
Leave a comment
Comments are reviewed before they are published, so yours will not show up right away.
You can use
**strong**and*italic*text,`code`, and lists (lines starting with-or1.). Everything else, HTML included, is shown as it was typed.