Skip to content

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 and my Homemade CPU. 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
  • Emulates at the instruction level (it is not "clock cycle accurate")
  • Does not emulate external hardware
  • The HALT statement is supported as if it were a breakpoint. Execution is stopped.

For the Z80:

  • All official Zilog opcodes and all undocumented Z80 opcodes listed in this table are supported
  • Undocumented flag bits 5 and 3 (sometimes referred to as XF and YF) are not updated
  • The R register is not updated while the processor is halted
  • The R register may not be updated correctly in cases where there are strings of $FD/$DD opcode prefixes that do not represent a valid opcode
  • The undocumented internal MEMPTR and Q registers are not emulated

For the Homemade CPU:

  • The emulator always executes the instruction after a jump instruction (a branch delay slot), since that instruction is already in the pipeline in the actual CPU hardware. The emulator may not produce the correct results if the instruction in the delay slot is another jump (but this will probably be disallowed either by the hardware or the assembler anyway).
  • A store-to-memory (STOR (mm) and PUSH instructions) delay slot is probably also needed, but is not implemented. This may be better described as a delayed fetch, because the fetch that would occur during the execution phase of a store-to-memory instruction would need to be delayed until the existing write to memory has completed.

The Future Functionality items listed below may be included in later releases.

Usage

emulator [-az | -ah | -ahv1] [-h] [input-file]

Optional parameters -az, -ah, or -ahv1 specify the architecture to emulate: -az for Z80, -ah for the Homemade CPU (current design), or ahv1 for the Homemade CPU v1 design. If no architecture parameter is specified, then Z80 is emulated.

-h displays a brief help summary, then exits the program.

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 following bytes are assumed to be processor state information (registers and similar data): 29 bytes for the Z80, 12 bytes for the Homemade CPU (current version) or 11 bytes for the Homemade CPU version 1.

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

Supported OSes

The emulator should compile and run on any modern unix or Linux OS.

It was originally developed and tested using Ubuntu 20.04 with gcc version 9.4.0 (by way of WSL 2) and MacOS Ventura with clang version 12.0.0.

Further development and testing was done with MacOS Sequoia and clang version 17.0.

Within the GitHub Actions environment, it has been tested with Ubuntu 22.04/gcc 11.3 and Ubuntu 24.04/gcc 13.2.

Implementation Details

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 CPU-specific code is encapsulated in classes named Z80 and HomemadeCPU which inherit 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, HomemadeCPU_opcodes.h). Each array entry contains the opcode/data layout, the instruction mnemonic, and, if necessary, the length of the instruction. The opcode value is represented by the array index.

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

The HomemadeCPU class structure is similar to the Z80 class, but simplified in many areas due to the RISC vs. CISC differences in the processor architectures. One notable difference is that the Homemade CPU has fixed-length 1-byte instructions, and therefore does not need to specify the instruction length in the opcodes array.

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.

Z80 Emulator Tests

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

Homemade CPU Tests

Workflow actions have not yet been created for the Homemade CPU.

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

  • Homemade CPU:
    • Update as needed as the hardware is implemented
    • Add automated tests
  • 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.

About

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

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •