A Z80 emulator
Introduction⌗
The Zilog Z80 is an 8-bit microprocessor developed by Federico Faggin and his 11 employees in early 1975. The first working samples were delivered in March 1976, and Z80 was officially introduced on the market in July 1976.
The Z80 is a backwards-compatible enhancement of the Intel 8080. Z80 quickly became one of the most widely used CPUs in desktop computers and home computers from the 1970s to the 1980s. It was also common in military applications, musical equipment such as synthesizers, calculators such as the classy TI83+, and arcade games such as Pac-Man.
Preparations⌗
I like to define a few convenience macros for my small C projects to make the code more succinct:
Next up, we define the processor state. Generally speaking, there are a few things we need to take care of:
- The processor’s memory.
- The instruction pointer, stack pointer and index registers (
ix
,iy
). - The WZ register1. We introduce it for compatibility with some of the Z80’s undocumented features. For example, the
BIT n,(HL)
instruction is known to notoriously leak bits 11 and 13 ofwz
into bits 3 and 5 of the WZ register. - The exchange registers. The z80 actually has two sets of general purpose registers and we can switch between those freely. They were initially designed to make handling interrupts easier, but programmers found a way to use and abuse them for sizecoding or performance tricks.
- Interrupt vectors and the refresh register. While the refresh register is not very useful, the program can load it to
A
and depend on its behaviour, so we must implement it correctly. - The flags: sign, zero, halfcarry, parity, negative, carry. My emulator also adds flags
yf
andxf
(3rd and 5th bits of the result). They are undocumented, but we must implement them, since the program can perceive their values.
The Z80 microprocessor has two interrupt lines, a software-masked INT
line and an unmasked NMI
line. The unmasked interrupt cannot be disabled by the programmer and is accepted whenever a peripheral device requests it. The second type of interrupt, masked INT
, is usually reserved for very important functions that the programmer can selectively block or unblock. This allows the programmer to block this interrupt at times when their program has time constraints that do not allow it to handle interrupts. The Z80 microprocessor has an Interrupt Flip-Flop (IFF
) interrupt handler activation flip-flop, which is set or reset by the programmer using the EI
(Enable Interrupt) and DI
(Disable Interrupt) instructions. When the IFF
interrupt is reset, the microprocessor does not accept INT
interrupts. The state of the IFF1
metastable is used to block interrupt handling, while the IFF2
metastable is used as a temporary place to remember the state of the IFF1
metastable. A reset of the microprocessor forces both the IFF1
and IFF2
metastables to reset to zero, which blocks interrupt handling. This support can obviously be enabled programmatically at any time with the EI
instruction. When an EI
instruction is in the process of execution, no active interrupt is accepted until the next instruction after EI
is executed. This delay of one instruction is necessary when the next instruction is a return instruction. Interrupts will not be handled until the execution of the return instruction is complete. The EI
instruction sets both the IFF1
and IFF2
interrupts to the on state. When the microprocessor accepts a masked interrupt, both IFF1
and IFF2
are automatically reset to zero, which blocks handling of further interrupts until the programmer issues a new EI
instruction. Note that in all previous cases IFF1
and IFF2
are always equal.
The purpose of IFF2
is to store the state of IFF1
when an unmasked interrupt occurs. When it is accepted, IFF1
resets to prevent the acceptance of further interrupts until the programmer re-enables their handling. In this way, when an unmasked interrupt is accepted, the masked interrupts are disabled, but the previous state of IFF1
is remembered, so the complete state of the microprocessor before the unmasked interrupt can be restored at any time. With the instructions LD A,I
and LD A,R
, the state of the IFF2
interrupt is copied to the parity marker, where it can be tested or stored. The second method of reproducing the state of IFF1
is to execute the RETN
(return from a NMI
) instruction. This instruction signifies that the procedure for handling the non-maskable interrupt has been completed and the contents of IFF2
are now copied back to IFF1
, so that the state of this interrupt is recreated automatically from before the non-maskable interrupt was accepted.
Let’s define the API of our emulator now. We want to be supplied port I/O handlers by the client application and expose a few functions to reset the CPU, perform an emulation step, or generate a NMI or an interrupt.
It is important to define a few convenience functions when delaing with emulator code, starting with memory accesses, extracting a higher/lower byte of a word and extracting a given bit from a word. All of these can be implemented as straightforward macros.
Going further, we need to define functions that let us access data with word granularity:
The Z80 also has a 16-bit stack, so we must implement the approperiate operations for it.
The Z80 is known for uniquely dealing with instruction “pairs” to form together 16-bit numbers that use one register as the lower byte, and another register as the higher byte.
To facilitate working with the rest of the processor state, the following straightforward functions simplify working with the flags register and obtaining instruction data from the program:
Instruction implementation⌗
We can start implementing z80 instructions now, starting with a few utility functions that will be useful later:
The implementation of jumps, calls, conditional jumps, conditional calls and returns is easy. We need to take care of the WZ
register, the nuissances of which are described in the first footnote1.
Z80 also employs relative jumps, which are cheaper to encode (8-bit displacement instead of 16-bit absolute address):
It is common for z80 instructions to update the flags register in a very monotone way (in particular, setting the sign and zero flags) and seemingly randomly tweak the “extra” xf
and yf
flags. Let’s implement these:
Now we can implement some arithmetics, starting with addition and subtraction, which is implemented as addition of the negated value:
16-bit variants of these instructions & the increment and decrement instructions trivially follow:
A recurring theme in the z80 instruction set is adding something to the 16-bit register pairs, so we implement two convenience functions for it. One is specialised in adding a word to the HL
register, and the other is more general and can add a word to any 16-bit register we pass to it via a pointer. Additionally, we add two convenience functions to add/subtract to/from the HL
register with carry.
The z80 has a “comparison instruction” to compare with A
(which is actually defined using subtraction), so we define it too:
All bit operations on z80 impact the flags register in the same way, so we can implement a helper macro for putting these together:
The 0xCB
opcodes (shifts) similarly share the flag update pattern characteristics, so we implement a helper macro for them too:
For the last bit operations, we want to test individual bits in a byte:
LDI
and LDD
will make heavy use of 16-bit registers, so we implement a few helper macros to offset them by some constant value:
The implementations are notoriously simple, though (beside the flag update black magic):
Moving on, we implement port I/O functions that are merely just wrappers over the user-supplied port_in
and port_out
functions:
The decimal adjust is probably one of the most complicated devices that the z80 consists of. Let’s define it in pseudocode and then transcribe it into C:
- Take
A
as our binary-coded decimal number. - If the second digit is “greater” than 9 or the half-carry flag is set, add 6 to the second digit.
- If the binary-coded decimal number exceeds 99 or the carry flag is set, add 6 to the first digit.
- If the negative flag is set, subtract the result from 0 and set the half-carry flag to
1
only if it was already set, and the second digit was less than 6. Otherwise, set the half-carry flag to1
if the last digit is greater than 9. - Add the result to the
A
register. - Set the parity flag to the parity of the
A
register. - Perform standard flag adjustments with respect to
A
.
As a final piece of the puzzle, we simplify updating the WZ
register to the sum of the B
and D
registers:
Implementing the interface⌗
Let’s implement the interface we have defined earlier, starting with some helper functions to execute particular opcodes/prefixed opcodes:
The init
function simply resets the emulator state:
The step
function executes a single instruction, taking care of the interrupt flip-flops and correct dispatching of the interrupts:
Finally, we implement the emulator API for scheduling interrupts:
The exec
function is supposed to execute a regular opcode, or if the opcode is not regular, call the function that handles the approperiate prefixed opcode. The exec_ind
function performs the same operation, but on a restricted set of opcodes further modified by the values of the index registers. Because of the length of both of the functions and their dubious value to the blog post as a whole, I have put it in a collapsed code block (TL;DR: Just a massive switch..case
).
One way more interesting part is the implementation of the 0xCB
prefix opcode executors: they have a very unique encoding implemented below.
Since the version of this function that depends on the IX
and IY
is almost analogically the same, I decided to also collapse it in a code block.
The final part of our emulator is the 0xED
opcode handling (primarily I/O, memory, some obscure fluff), implemented below:
Emulator testing⌗
I have been using the zexall and zexdoc tests to verify the emulator and its implementation of the undocumented capabilities. Writing a client application for this purpose turned out to be very simple:
All tests pass, meaning that we are ready for something bigger!
The MS BASIC⌗
To run MS BASIC we slighly need to modify our code. First, MS BASIC will expect the input to be provided in raw format, so we need to tweak the terminal via termios
accordingly:
Next up, we copy over our standard load_file
function from the zexall/zexdoc testing suite client and stub out port I/O (unused):
At this point, we are expected to implement a few BDOS routines to handle the input and output. The code below accomplishes this and handles restarts.
Finally, we load the BASIC interpreter into memory and run it, accordingly supplying the BDOS service support:
Finally, I ran the emulator and played a little with the classy MS BASIC:
Conclusion⌗
In this post, we have implemented a Z80 emulator in C. We have also used it to run the zexall/zexdoc tests (all of which pass) and the MS BASIC interpreter. The code is available on GitHub: https://github.com/kspalaiologos/tinyz80