Introduction
If you've been following this project, you know that the goal is to create a universal IC replacement, I call, UnIC. If successful, this piece of technology would slot into any DIP-40 socket and mimic the functionality of whatever originally was supposed to go in there. You could use it to replace hard-to-find old parts (ZX81 ULA), add extra functionality to old devices (HDMI output on a VIC-II), or dream up new uses that never existed. Some limitations apply of course. Whatever the thing needs to do would need to be possible with an FPGA, so no fancy analog functions for instance. Still it would be as universal as possible. Crucially: it would work no matter what the desired pinout is, including the location of power and ground pins and even to a certain extent the voltages on said power pins.
The board itself is in fairly mature state, but I needed a test and demo project to not only demonstrate the capabilities but to prove out the concept. The recent cancellation of the venerable Z80 line gave me the idea: create a Z80 replacement.
In the previous installment I detailed my journey to the point of booting CP/M on a PCW512. In this, hopefully last write-up on the Z80 saga, I'll take you the rest of the way.
The problem of sampling
My approach to tracking down differences between the Z80 proper and my (T80-based) implementation was to put them in the system side-by-side, making the T80 core shadow the Z80 lock-step, recording any differences in observable behavior. These differences can then be used to trigger a logic analyzer to uncover the reasons for the mismatches and find some fixes for them.
This idea worked great, until it didn't: the external, physical world can throw unexpected problems at you. One of these was that certain signals change their state asynchronously relative to the CPU clock. This means (even if we disregard the potential for meta-stability) that logic inside the processor can detect the change in signal level one clock earlier or later, depending on setup and hold-times. These are obviously rather different between the FPGA I'm using and the Z80 reference, resulting in natural mismatches in behavior. Such problematic signals include the interrupt and NMI lines but - as I found out in the hard way - also the data lines.
To solve this issue inside the FPGA is next to impossible, but it can be handled externally: what if you re-sample all problematic signals with the CPU clock, before feeding them to either the reference Z80 or the FPGA? That way we cleared setup and hold violations before entering these chips and so their behavior now should match.
I created a small circuit that would be able to do this:

The logic isn't all that complicated, or interesting, really. It's just some flip-flops doing the registration. One notable problem was that the Z80 samples certain signals on both the rising and falling edges. To properly deal with that, I needed a clock-doubler, to generate sampling edges for the the registers. I've done that with a set of XOR gates and an R/C delay circuit:

Ideally, you would want schmidt-trigger inputs for this kind of stuff, but I figured the small amount of delay I intend to introduce here should not slow the edges down too much.
The other interesting detail is the handling of the data-bus: here I needed to register only reads and make sure that the registered value is only presented when the CPU is not driving the bus, necessitating two 8-bit bus-drivers and some decoder logic to identify which should be enabled when.
Sponsored content
I've designed the PCB and sent out the files to PCBWay for manufacturing, the sponsor of this article. They are a fast turn-around PCB prototyping house that I've been using for a while to my satisfaction. My boards are usually not that complex, but their capabilities are quite impressive. Even more interesting is that they are offering additional services, such as flex and rigid-flex manufacturing, PCBA assembly, 3D printing. They offer even CNC machining and injection molding services. If these are things that interest you - and if you are here, some of them probably are - check them out!
PCBWay is based in China, but they use DHL for shipping, so the finished boards show up extremely fast; sometimes faster then the components from DigiKey. After submitting your design, you can expect a package at your doorstep in a week or so. In it, there would be a paper box containing your PCBs, nicely shrink-wrapped:

From here on, you are on your own, unless you used their assembly services of course. Putting the board together was a breeze though, mostly because I was mindeful enough to pick age-appropriate components and placement (large pins, plenty of room):

First Findings
Unlike some previous horror stories, this time the bring-up was rather uneventful. Everything worked as expected, the only slight issue I encountered was that my clock-doubler distorted the duty-cycle quite a bit. This is due to the propagation delay of the XOR gates. It isn't a big deal, just that I expected it to be better. Still, I got a usable 8MHz clock out of it and the circuit is happy.
With this synchronizer in the 'train' of boards I'm using, my setup started looking really ridiculous:

Ridiculousness aside, the bigger problem is that the long traces started to impact reliability. While I could still boot into CP/M most of the time, occasionally I started experiencing hangs and other strange behavior. I don't believe this has anything to do with the registering of the signals themselves as the problems persisted if I put the logic into inert, pass-through mode.
More importantly though, did it get rid of all the discrepancies between the reference and my implementation?
No, unfortunately. The first problem was with this piece of code:
; Delay loop. B is length of delay.
; Clobbers: A. (Preserves carry.)
delay:
00B1 3E LD A,0B3h
00B2 B3
delaylp: ; inner loop controlled by A
00B3 E3 EX (SP),HL ; beefy NOPs?
00B4 E3 EX (SP),HL
00B5 E3 EX (SP),HL
00B6 E3 EX (SP),HL
00B7 3D DEC A
00B8 20 JR NZ,delaylp
00B9 F9
00BA 10 DJNZ delay
00BB F5
00BC C9 RET
This is very early in the boot process and the logic triggered a data mismatch in the value of the HL register. The problem turned out to be the following: after reset, the content of the HL register is not deterministic. It can easily be different between the Z80 and my T80. Though I'm not showing this, no instruction between reset and this piece of code touches the content of HL, so this nondeterministic value is retained, and when written into memory (by the EX instruction) it shows up as a difference.
Unfortunately this is not anything I can fix, not even with the extra circuit I've added. In fact, I can't think of any way of getting rid of it and this represents another source of unavoidable mismatches between the two implementations.
Luckily the problem quickly goes away: the following EX instruction swaps back the value from memory into the HL register and - since the T80 was prevented from actually performing the write (remember, it is shadowing the Z80 but not actually driving anything) - from there on, the two register values match.
Later, problem different popped up when the second register-set of the Z80 was first used:
exx ;2044 d9 .
push hl ;2045 e5 .
push de ;2046 d5 .
push bc ;2047 c5 .
exx ;2048 d9 .
ex af,af' ;2049 08 .
push af ;204a f5 .
Here the 'PUSH AF' instruction showed differences in values. This is also very early in the boot, the first time the 'apostrophe' register set is accessed. So it is a similar problem with a similar effect: the undetermined reset value of "AF'" in this case causes a mismatch.
To deal with these, I decided to simply ignore the first two mismatches before triggering. Not fool-proof (as they occasionally may not happen if the reset values happens to agree), but I had no better idea.
Real Problems
This was the point where I started to see real problems. True mismatches in behavior around interrupts. If you've read my previous story on NMI misgivings, you know that that's the area I apparently always run into issues in.
This time both cores handled the interrupts but with slightly different timing. First off, the Z80 seemed to simply register the interrupt request a cycle later than the T80 did. This is of course easy to fix: just delay the interrupt signal by one clock cycle.
The second problem was trickier to track down: the symptom was that, after an NMI routine finished processing (returning with a RETN) instruction, interrupt was still handled too early by the T80 relative to the reference Z80. After some head-scratching, I noticed that the problem only occurred if there was a pending interrupt at the end of NMI processing.
Here, I need to take a slight detour: the Z80 allows for a few ways of enabling interrupts. One is the RETN instruction, which - if interrupts were enabled prior to entering the NMI handler - restores that state. The second is the 'EI' instruction, which does what it says on the tin: enables interrupts. The quirk is that the EI instruction doesn't enable interrupts immediately. It still allows for the execution of the following instruction, before enabling interrupts. This is very important and necessary for interrupt handlers. Normally, an interrupt handler terminates in the following fashion:
EI
RETI
The first instruction enables interrupts, the second returns from the interrupt handler. Now, imagine an interrupt hitting the CPU in between those two instruction! What would happen is that we re-enter the interrupt handler before popping the old return address of the stack. At that point we end up with two nested interrupt handlers on the stack. If this happens again and again, we have the possibility to overflow the stack. Crucially we have no way of avoiding this as interrupts by nature are asynchronous and can happen at any time. This is the reason the designers of the Z80 decided to delay the action of the EI instruction such that interrupts are still disabled during the execution of the RETI instruction.
How about RETN though? It can re-enable interrupts as well. There's no similar need to delay the enabling of the interrupts, but - since that logic is still there - it is not extra complexity to do so. In fact, it's probably (depending in the implementation) more complex not to delay them. This is not documented anywhere, but the behavior of the Z80 I'm seeing is consistent with this hypothesis: interrupts remain disabled for the instruction following the RETN; that is the instruction that was interrupted by the NMI to begin with.
Clean plate!
I've implemented this change, and finally, for the first time, I managed to boot into CP/M without any mismatch triggers. This was of course still in the shadow-tracer configuration. So, can my core boot into CP/M on it's own? Is it a functional Z80 replacement without the hand-holding of the real-deal? There's only one way to know: program the core into UnIC in its functional configuration, rip out the Z80 and try:

So, good news everyone! The beast is alive. As far as I can tell, this processor is now a faithful, cycle-accurate replica of a Z80. It can be used in place of the original chip wherever it is needed.
Conclusion
This has been a long process. Much longer than I thought it would be at the outset. I started off with a well known, well-tested, highly accurate core for the Z80. Something that has been successfully used in other FPGA projects such as the MiSTer emulator platform. Yet, I still had to fight my way through several discrepancies. There were differences in bus timing, there were implementation differences in certain instructions resulting in different ordering of memory accesses. There was the nasty priority inversion bug I needed to fix. There were several subtle differences in timing. There still is a strange mismatch in undocumented flags behavior during interrupt handing, something I couldn't make sense of.
I guess it just goes to show how much work is between getting things "working" and getting things really working. I'm not bitter, though. I'm still thankful to all the work that went into this core before I started working on it: overall it would have been significantly more work to start from scratch. I just need to calibrate myself better for these endeavors in the future.
Yet, after all that, the project is a success: not only I have a Z80 replacement, but I have proven that UnIC is indeed capable of impersonating an old design, logic, function, power and all.
My T80 variant is checked in here: https://github.com/andrastantos/unic\_t80 and I will see how to push my changes back into the parent fork.
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.