Skip to content

CPU emulator and disassembler for 8-bit processors. Currently supports Z80.

License

Notifications You must be signed in to change notification settings

Andy4495/emulator-8-bit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

8-bit Emulator

Build Check Markdown Links Test Disassembler Test Opcodes

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.

Usage

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.

Building the Emulator

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

Implementation Details

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.

General Program Flow

  1. Parse the command line.

  2. Read the input file into an array representing the processor's memory

  3. 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
  4. Return to step 3

Defining the CPU

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.)

Z80 Assemblers

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

Automated Test

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

Test Types

Round Trip

  • 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.

Known Good

  • 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.

Future Functionality

  • 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

References

License

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.