This is a simple 8-bit CPU emulator and disassembler. It currently supports the Z80; other CPUs may be added in the future.
I created it as a learning exercise to refresh my C++ programming skills and to spend some time diving into the Z80 CPU architecture.
The emulator:
- Is text-based
- Supports all official Zilog opcodes and all undocumented Z80 opcodes listed in this table
- Emulates at the instruction level (it is not "clock accurate")
- Does not emulate external hardware
- Does not update the undocumented flag bits 5 and 3 (sometimes referred to as XF and YF)
- Does not emulate the undocumented internal MEMPTR and Q registers
- Supports the HALT statement as if it were a breakpoint. Execution is stopped and the R register is not updated while the processor is halted
- May not update the R register correctly in cases where there are strings of $FD/$DD opcode prefixes that do not represent a valid opcode
The Future Functionality items listed below may be included in later releases.
emulator [input-file]
input-file
is an optional parameter which is the path of a binary file containing the program code and data. The first byte of the file represents location $0000 in memory, and each successive byte represents the next memory location.
If input-file
is not specified, then the default name data.bin
is used.
No error checking is performed on the input file, except that a maximum of 65536 bytes are read into memory. If the file is larger than 65536 bytes, then the next 29 bytes are assumed to be the processor registers and interrupt settings. Any data beyond that is ignored.
The repository contains a Makefile to automate the build process. To build the emulator
executable, simply run make
at the command line from the top-level directory of the repo:
make
The Makefile also has the following targets defined:
make debug # Adds -g to the compiler options to create debugging information
make verbose # Adds --verbose to the compiler and linker options
make clean # Removes the executable, object, and linker files
The emulator was developed using Ubuntu 20.04 with gcc version 9.4.0 (by way of WSL 2) and MacOS Ventura with clang version 12.0.0.
It is also known to be compatible with Ubuntu 22.04 and gcc version 11.3.0 within the GitHub Actions environment.
I have not tried it on other platforms, but there is no machine dependent code. It should work as-is (or with minimal changes) on other unix-like platforms and compiler versions.
-
Parse the command line.
-
Read the input file into an array representing the processor's memory
-
Display menu and choose operating mode
A. Execute mode:
- Loop:
- Fetch and decode instruction
- Execute instruction
- Print instruction
- Continue loop until HALT reached
- Display machine state
B. Disassemble mode:
- Loop:
- Fetch and decode instruction
- Print instruction
- Continue loop until ending address reached
- Loop:
-
Return to step 3
The Z80-specific code is encapsulated in a class named Z80
which inherits from the abstract base class abstract_CPU
. Additional CPUs can be emulated by creating classes specific to those CPUs.
The CPU opcodes are defined in several tables implemented with arrays of structs for the main and extended opcodes (Z80_opcodes.h
). Each array entry contains the size of the instruction, the opcode/data layout, and the instruction mnemonic. The opcode value is represented by the array index.
Zilog-documented and undocumented opcodes are defined and supported by the emulator.
The Z80
class contains:
- An array representing the memory (RAM and ROM) available to the processor
- This is currently defined as a single structure of 65536 bytes of RAM (16-bit address space)
- Arrays representing the input and output address space (256 bytes each)
- All programmer-accessible processor registers
- Other internal registers and flip-flops that aren't directly avaiable to the programmer, but represent the internal state of the processor.
- Methods representing various CPU operations, including:
- Load memory from file
- Loads ROM and RAM with program and data as defined in the input file
- Cold restart
- Power-on restart where registers and other state information is initialized
- Warm restart
- PC is set to zero, other registers and state left as-is
- For the Z80, this is also referred to as "Special Reset"
- Jump to a specific address
- All internal state information is left as-is except for the Program Counter
- Fetch and decode instruction and data
- Load byte from memory into Instruction Register and update Program Counter
- Load additional bytes from memory depending on the fetched opcode
- Generate a string containing the disassembled instruction and data
- Execute the actual instruction (load, store, jump, etc.)
- Load memory from file
I have tested the emulator with the following Z80 assemblers:
I have included x86 linux executables for these assemblers in this repo as a convenience in automated testing.
When testing with zasm
, the --ixcbr2
command line option should be used so that the undocumented opcodes are interpreted correctly:
zasm --ixcbr2 filename.asm
Various workflow actions are defined to test the emulator:
Disassemble Mode - TestDisassembler.yml
Input File | Test Type | Notes |
---|---|---|
test_disassembler_1.asm | Round Trip | Opcode list taken from zasm documentation. |
test_disassembler_2.asm | Round Trip | All opcodes in order by opcode value. |
test_disassembler_3.asm | Round Trip | All undefined opcodes. |
test_disassembler_4.asm | Known Good | Opcodes that duplicate other mnemonics. |
Execute Mode - TestOpcodes.yml
Input File | Test Type | Notes |
---|---|---|
test_execution_no_flag_updates.asm | Known Good | Opcodes that don't update flags |
test_execution_call_jump_loop_return.asm | Known Good | Call, jump, return, rst opcodes |
test_execution_with_flag_updates.asm | Known Good | Opcodes that affect flags |
test_execution_daa.asm | Known Good | DAA opcode and flags |
test_execution_duplicate_mnemonics.asm | Known Good | Opcodes where mnemonics are same as other opcodes |
- Tests the disassembler functionality of the emulator. The input file is assembled. The assembled output
.rom
file is run through the emulator in disassemble mode. The disassembled output is assembled again to create a second.rom
file. This second.rom
file is compared against the initial.rom
file created with the input file.
- For disassemble mode: The input file is assembled. The assembled output
.rom
file is run through the emulator in disassemble mode. The disassembled output is then compared against a "known good" disassemble file. This type of test is needed in cases where re-assembling the disassembled mnemonic will not produce the same opcode values (e.g., in the case of undocumented opcodes that perform the same function as documented opcodes). - For execute mode: The input file is assembled. The assembled output
.rom
file is input to the emulator and executed. The memory and registers are dumped to a file which is then compared to a known good memory/register file.
- Additional breakpoint functionality
- Break when a register contains a certain value
- Break when a memory location contains a certain value
- Break when a certain location/loop is accessed N times
- Define Multiple breakpoints
- Support additional configuration options, possibly with a configuration file and/or command line arguments
- Allow the configuration of segments of read-only ROM, read/write RAM, overlay areas, and undefined areas
- Support RAM and/or ROM banking
- Support additional input file formats such as Intel Hex or Motorola S-Records which would allow specific memory locations to be defined by the file.
- Interrupts (maskable and non-maskable)
- Support additonal processor types
- HALT state handler improvements
- Since interrupts are not implemented, HALT just stops the emulator
- HALT should act like a breakpoint, in that execution can be continued after performing available debugging operations
- HALT does not execute NOPs, so R register is not updated
- Z80 User Manual
- Note: The Z80 User Manual has many errors, ambiguities, and inconsistencies. It is sometimes necessary to consult other references (or experiment on an actual chip) to determine the correct behavior for certain opcodes.
- Z80 Info: Comprehensive source of Z80 information: hardware, compilers, assemblers, documentation
- Z80 opcode table (GitHub repo)
- The Undocumented Z80 Documented white paper
- Z80 assemblers:
zasm
- online version or download- GitHub repo
z88dk-z80asm
- Z88DK - The Development Kit for Z80 Computers- GitHub repo
- Z80 emulator project which includes test cases and a substantial reference list
- hex2bin - Tool for converting Intex Hex or Motorola S-Record files to binary
- Make reference
- Online Makefile generator
- Installing WSL 2 reference and devblog post
The zasm
assembler is distributed under the BSD 2-Clause license. See zasm_LICENSE.txt in the tools/zasm
directory.
The z88dk-z80asm
assembler is distributed under the Clarified Artistic License. See LICENSE.txt in the tools/z88dk
directory. z88dk-z80asm
was built on 20-Dec-2023 from the source files available at https://github.com/z88dk/z88dk.
The other software and files in this repository are released under what is commonly called the MIT License. See the file LICENSE.txt
in this repository.