Skip to content

suzukiplan/z80

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SUZUKI PLAN - Z80 Emulator

suzukiplan

The Z80 is an 8-bit CPU developed by Zilog corporation, released in 1976, and widely used in computers and game consoles in the 1980s. It is not just a relic of the past, but continues to be used in embedded systems that require accuracy in processing execution time, such as real-time systems.

SUZUKI PLAN - Z80 Emulator is an emulator under development based on the following design guidelines to support the development of programs and/or emulators using Z80, 8080:

(FOUR EASY GUIDELINES FOR EASILY)

  1. Make emulator implementation EASY & simple (Realized by providing single header: z80.hpp)
  2. EASILY debugging the Z80 programs (Realized by having dynamic disassemble feature)
  3. Highly readable and EASILY customizable (priority for readability over performance)
  4. Provide under the license that EASY to adopt in various programs (MIT)

Since I do not have deep knowledge about Z80 myself, I'm implementing it with reference to the information on the following web sites:

z80.hpp passes all zexdoc and zexall tests. (See the detail)

How to test

Prerequests

You can test on the any 32bit/64bit platform/OS (UNIX, Linux, macOS...etc) but needs following middlewares:

  • Git
  • GNU Make
  • clang
  • clang-format

Build (UNIX, Linux, macOS)

git clone https://github.com/suzukiplan/z80.git
cd z80
make

Minimum usage

1. Include

#include "z80.hpp"

2. Implement MMU & access callback

class MMU
{
  public:
    unsigned char RAM[0x10000]; // 64KB memory (minimum)
    unsigned char IO[0x100]; // 256bytes port

    MMU()
    {
        memset(&RAM, 0, sizeof(RAM));
        memset(&IO, 0, sizeof(IO));
    }
};

// memory read request per 1 byte from CPU
unsigned char readByte(void* arg, unsigned short addr)
{
    // NOTE: implement switching procedure here if your MMU has bank switch feature
    return ((MMU*)arg)->RAM[addr];
}

// memory write request per 1 byte from CPU
void writeByte(void* arg, unsigned short addr, unsigned char value)
{
    // NOTE: implement switching procedure here if your MMU has bank switch feature
    ((MMU*)arg)->RAM[addr] = value;
}

// IN operand request from CPU
unsigned char inPort(void* arg, unsigned short port)
{
    return ((MMU*)arg)->IO[port];
}

// OUT operand request from CPU
void outPort(void* arg, unsigned short port, unsigned char value)
{
    ((MMU*)arg)->IO[port] = value;
}

3. Make Z80 instance

    MMU mmu;
    /**
     * readByte: callback of memory read request
     * writeByte: callback of memory write request
     * inPort: callback of input request
     * outPort: callback of output request
     * &mmu: 1st argument of the callbacks
     */
    Z80 z80(readByte, writeByte, inPort, outPort, &mmu);

3-1. Cases when want to use 16bit port

Note that by default, only the lower 8 bits of the port number can be obtained in the callback argument, and the upper 8 bits must be referenced from register B.

If you want to get it in 16 bits from the beginning, please initialize with returnPortAs16Bits (6th argument) to true as follows:

    Z80 z80(readByte, writeByte, inPort, outPort, &mmu, true);

3-2. Cases when performance-sensitive

Normally, std::function is used for callbacks, but in more performance-sensitive cases or std::function is not exist environment (ex: RaspberryPi Baremetal), all can be replaced with function pointers by specifying the compile option -DZ80_NO_FUNCTIONAL.

The following article (in Japanese) provides a performance comparison between function pointers and std::function:

https://qiita.com/suzukiplan/items/e459bf47f6c659acc74d

The above article gives an example of the time it took to execute 100 million times call of function-pointer and three patterns of std::function calls (bind, direct, lambda) as following:

Optimization Option function pointer bind direct lambda
none 195,589μs 5,574,856μs 2,570,016μs 2,417,802μs
-O 184,692μs 2,293,151μs 827,113μs 580,442μs
-O2 154,206μs 197,683μs 209,626μs 167,703μs
-Ofast 154,332μs 250,490μs 255,401μs 164,773μs

4. Execute

    // when executing about 1234Hz
    int actualExecutedClocks = z80.execute(1234);

4-1. Actual executed clocks

  • The execute method repeats the execution of an instruction until the total number of clocks from the time of the call is greater than or equal to the value specified in the "clocks" argument.
  • If a value less than or equal to 0 is specified, no instruction is executed at all.
  • If you want single operand execution, you can specify 1.

4-2. Interruption of execution

Execution of the requestBreak method can abort the execute at an arbitrary time.

A typical 8-bit game console emulator implementation that I envision:

  • Implement synchronization with Video Display Processor (VDP) and other devices (sound modules, etc.) in consumeClock callback.
  • Call requestBreak when V-SYNC signal is received from VDP.
  • Call execute with a large value such as INT_MAX.

4-3. Example

Code: test/test-execute.cpp

#include "z80.hpp"

int main()
{
    unsigned char rom[256] = {
        0x01, 0x34, 0x12, // LD BC, $1234
        0x3E, 0x01,       // LD A, $01
        0xED, 0x79,       // OUT (C), A
        0xED, 0x78,       // IN A, (C)
        0xc3, 0x09, 0x00, // JMP $0009
    };
    Z80 z80([=](void* arg, unsigned short addr) { return rom[addr & 0xFF]; },
            [](void* arg, unsigned short addr, unsigned char value) {},
            [](void* arg, unsigned short port) { return 0x00; },
            [](void* arg, unsigned short port, unsigned char value) {
                // request break the execute function after output port operand has executed.
                ((Z80*)arg)->requestBreak();
            }, &z80);
    z80.setDebugMessage([](void* arg, const char* msg) { puts(msg); });
    z80.setConsumeClockCallback([](void* arg, int clocks) { printf("consume %dHz\n", clocks); });
    puts("===== execute(0) =====");
    printf("actualExecuteClocks = %dHz\n", z80.execute(0));
    puts("===== execute(1) =====");
    printf("actualExecuteClocks = %dHz\n", z80.execute(1));
    puts("===== execute(0x7FFFFFFF) =====");
    printf("actualExecuteClocks = %dHz\n", z80.execute(0x7FFFFFFF));
    return 0;
}

Result is following:

===== execute(0) =====
actualExecuteClocks = 0Hz ... 0 is specified, no instruction is executed at all
===== execute(1) =====
consume 2Hz
consume 2Hz
consume 3Hz
consume 3Hz
[0000] LD BC<$0000>, $1234
actualExecuteClocks = 10Hz ... specify 1 to single step execution
===== execute(0x7FFFFFFF) =====
consume 2Hz
consume 2Hz
consume 3Hz
[0003] LD A<$FF>, $01
consume 2Hz
consume 2Hz
consume 4Hz
[0005] OUT (C<$34>), A<$01>
consume 4Hz
actualExecuteClocks = 19Hz ... 2147483647Hz+ is not executed but interrupted after completion of OUT due to requestBreak during OUT execution

5. Generate interrupt

IRQ; Interrupt Request

    z80.generateIRQ(vector);

NMI; Non Maskable Interrupt

    z80.generateNMI(address);

Optional features

Dynamic disassemble (for debug)

You can acquire the debug messages with setDebugMessage. Debug message contains dynamic disassembly results step by step.

    z80.setDebugMessage([](void* arg, const char* message) -> void {
        time_t t1 = time(NULL);
        struct tm* t2 = localtime(&t1);
        printf("%04d.%02d.%02d %02d:%02d:%02d %s\n",
               t2->tm_year + 1900, t2->tm_mon, t2->tm_mday, t2->tm_hour,
               t2->tm_min, t2->tm_sec, message);
    });
  • call resetDebugMessage if you want to remove the detector.
  • call setDebugMessageFP if you want to use the function pointer.

Use break point

If you want to execute processing just before executing an instruction of specific program counter value (in this ex: $008E), you can set a breakpoint as follows:

    z80.addBreakPoint(0x008E, [](void* arg) -> void {
        printf("Detect break point! (PUSH ENTER TO CONTINUE)");
        char buf[80];
        fgets(buf, sizeof(buf), stdin);
    });
  • addBreakPoint can set multiple breakpoints for the same address.
  • call removeBreakPoint or removeAllBreakPoints if you want to remove the break point(s).
  • call addBreakPointFP if you want to use the function pointer.

Use break operand

If you want to execute processing just before executing an instruction of specific operand number, you can set a breakpoint as follows:

    // break when NOP ... $00
    z80.addBreakOperand(0x00, [](void* arg, unsigned char* opcode, int opcodeLength) -> void {
        printf("Detect break operand! (PUSH ENTER TO CONTINUE)");
        char buf[80];
        fgets(buf, sizeof(buf), stdin);
    });

    // break when RLC B ... $CB $00
    z80.addBreakOperand(0xCB, 0x00, [](void* arg, unsigned char* opcode, int opcodeLength) -> void {
        printf("Detect break operand! (PUSH ENTER TO CONTINUE)");
        char buf[80];
        fgets(buf, sizeof(buf), stdin);
    });

    // break when RLC (IX+d) ... $DD $CB, $06
    z80.addBreakOperand(0xDD, 0xCB, 0x06, [](void* arg, unsigned char* opcode, int opcodeLength) -> void {
        printf("Detect break operand! (PUSH ENTER TO CONTINUE)");
        char buf[80];
        fgets(buf, sizeof(buf), stdin);
    });
  • the opcode and length at break are stored in opcode and opcodeLength when the callback is made.
  • addBreakOperand can set multiple breakpoints for the same operand.
  • call removeBreakOperand or removeAllBreakOperands if you want to remove the break operand(s).
  • call addBreakOperandFP if you want to use the function pointer.

Detect clock consuming

If you want to implement stricter synchronization, you can capture the CPU clock consumption timing as follows:

    z80.setConsumeClockCallback([](void* arg, int clock) -> void {
        printf("consumed: %dHz\n", clock);
    });
  • call resetConsumeClockCallback if you want to remove the detector.
  • call setConsumeClockCallbackFP if you want to use the function pointer.

With this callback, the CPU cycle (clock) can be synchronized in units of 3 to 4 Hz, and while the execution of a single Z80 instruction requires approximately 10 to 20 Hz of CPU cycle (time), the SUZUKI PLAN - Z80 Emulator can synchronize the CPU cycle (time) for fetch, execution, write back, etc. However, the SUZUKI PLAN - Z80 Emulator can synchronize fetches, executions, writes, backs, etc. in smaller units. This makes it easy to implement severe timing emulation.

If implement quick save/load

Save the member variable reg when quick saving:

    fwrite(&z80.reg, sizeof(z80.reg), 1, fp);

Extract to the member variable reg when quick loading:

    fread(&z80.reg, sizeof(z80.reg), 1, fp);

Handling of CALL instructions

The occurrence of the branches by the CALL instructions can be captured by the CallHandler. CallHandler will be called back immediately after a branch by a CALL instruction occurs.

CallHandle will called back after the return address is stacked in the RAM.

    z80.addCallHandler([](void* arg) -> void {
        printf("Executed a CALL instruction:\n");
        printf("- Branched to: $%04X\n", ((Z80*)arg)->reg.PC);
        unsigned short sp = ((Z80*)arg)->reg.SP;
        unsigned short returnAddr = ((Z80*)arg)->readByte(sp + 1);
        returnAddr <<= 8;
        returnAddr |= ((Z80*)arg)->readByte(sp);
        printf("- Return to: $%04X\n", returnAddr);
    });
  • addCallHandler can set multiple CallHandlers.
  • call removeAllCallHandlers if you want to remove the CallHandler(s).
  • call addCallHandlerFP if you want to use the function pointer.
  • CallHandler also catches branches caused by interrupts.
  • In the case of a condition-specified branch instruction, only the case where the branch is executed is callbacked.

Handling of RET instructions

The occurrence of the branches by the RET instructions can be captured by the ReturnHandler. ReturnHandler will be called back immediately before a branch by a RET instruction occurs.

ReturnHandle will called back while the return address is stacked in the RAM.

    z80.addReturnHandler([](void* arg) -> void {
        printf("Detected a RET instruction:\n");
        printf("- Branch from: $%04X\n", ((Z80*)arg)->reg.PC);
        unsigned short sp = ((Z80*)arg)->reg.SP;
        unsigned short returnAddr = ((Z80*)arg)->readByte(sp + 1);
        returnAddr <<= 8;
        returnAddr |= ((Z80*)arg)->readByte(sp);
        printf("- Return to: $%04X\n", returnAddr);
    });
  • addReturnHandler can set multiple ReturnHandlers.
  • call removeAllReturnHandlers if you want to remove the ReturnHandler(s).
  • call addReturnHandlerFP if you want to use the function pointer.
  • In the case of a condition-specified branch instruction, only the case where the branch is executed is callbacked.

Advanced Compile Flags

There is a compile flag that disables certain features in order to adapt to environments with poor performance environments, i.e: Arduino or ESP32:

Compile Flag Feature
-DZ80_DISABLE_DEBUG disable setDebugMessage method
-DZ80_DISABLE_BREAKPOINT disable addBreakPoint and addBreakOperand methods
-DZ80_DISABLE_NESTCHECK disable addCallHandler and addReturnHandler methods
-DZ80_CALLBACK_WITHOUT_CHECK Omit the check process when calling consumeClock callback (NOTE: Crashes if setConsumeClock is not done)
-DZ80_CALLBACK_PER_INSTRUCTION Calls consumeClock callback on an instruction-by-instruction basis (NOTE: two or more instructions when interrupting)
-DZ80_UNSUPPORT_16BIT_PORT Reduces extra branches by always assuming the port number to be 8 bits
-DZ80_NO_FUNCTIONAL Do not use std::function in the callbacks (use function pointer)
-DZ80_NO_EXCEPTION Do not throw exceptions

License

MIT