A standalone Z80 CPU emulator with built-in debugger, forked from the RunCPM Project (implemented by MockbaTheBorg). This library provides a complete Z80 processor implementation that can be used independently or integrated into computer simulation projects.
- Complete Z80 instruction set - Implements all documented Z80 instructions
- Built-in debugger - Interactive debugger with step-by-step execution, register inspection, and memory viewing
- Multiple CPU instances - Support for creating multiple Z80 instances in the same process
- Clean API - Simple C interface for easy integration
- Platform independent - Written in standard C with minimal dependencies
- Intercept functionality - Custom callback for instruction-level monitoring and control
z80.h- Public API and type definitionsz80.c- Z80 CPU implementation (4,600+ lines) - modify this file to implement I/Oconsole.h/console.c- Terminal I/O and debugger interfaceglobals.h- Global definitions and macrosmain.c- Example usageMakefile- Build configuration
Note: This project is structured as a standalone emulator rather than a library. To customize I/O behavior, you need to modify the cpu_out() and cpu_in() functions in z80.c directly.
make allThis will compile the example program main which demonstrates basic usage.
./mainThe example creates a Z80 CPU instance, enables debug mode, sets an intercept function, and runs the CPU until the program counter reaches address 5.
Creates and initializes a new Z80 CPU instance. Returns a pointer to the CPU structure.
Destroys a Z80 CPU instance and frees associated resources.
Starts execution of the Z80 CPU. The CPU will run until its status changes (e.g., via an intercept function or debugger command).
Sets an intercept function that is called before each instruction execution. The intercept function can examine or modify CPU state.
The z80 structure contains all CPU registers and state:
typedef struct {
int32_t pcx; /* external view of PC */
int32_t af; /* AF register */
int32_t bc; /* BC register */
int32_t de; /* DE register */
int32_t hl; /* HL register */
int32_t ix; /* IX register */
int32_t iy; /* IY register */
int32_t pc; /* program counter */
int32_t sp; /* SP register */
int32_t af1; /* alternate AF register */
int32_t bc1; /* alternate BC register */
int32_t de1; /* alternate DE register */
int32_t hl1; /* alternate HL register */
int32_t iff; /* Interrupt Flip Flop */
int32_t ir; /* Interrupt (upper) / Refresh (lower) register */
int32_t status; /* Status: 0=running, 1=end request, 2=back to CCP */
int32_t debug; /* Debug mode flag */
// ... internal state
} z80;The Z80 emulator includes a 64KB RAM array as part of the CPU structure (cpu->RAM[65536]). Memory access is handled internally through macros:
_RamRead(address)- Read byte from memory_RamWrite(address, value)- Write byte to memory_RamRead16(address)- Read 16-bit word from memory_RamWrite16(address, value)- Write 16-bit word to memory
For I/O operations, you need to implement these functions:
void cpu_out(z80 *cpu, uint32_t port, uint32_t value);
uint32_t cpu_in(z80 *cpu, uint32_t port);The default implementations in z80.c are stubs that return default values. You should replace them with your actual I/O implementation.
#include "z80.h"
int main() {
z80* cpu = z80_new();
cpu->debug = 1; // Enable debug mode
z80_run(cpu);
z80_destroy(cpu);
return 0;
}#include <stdio.h>
#include "z80.h"
/* Intercept function to stop execution at address 5 */
static void intercept(void *ctx) {
z80 *cpu = ctx;
if (cpu->pc == 5) {
printf("\nReached address 5, stopping execution.\n");
cpu->status = 1;
}
}
int main() {
z80 *cpu = z80_new();
cpu->debug = 1; // Enable debug mode
z80_set_intercept(cpu, cpu, intercept);
z80_run(cpu);
z80_destroy(cpu);
return 0;
}Compile and run:
gcc -o example z80.c console.c example.c
printf "c\n" | ./example # Press 'c' to continue execution#include "z80.h"
static void my_intercept(void* ctx) {
z80* cpu = ctx;
if (cpu->pc == 0x100) {
printf("Reached address 0x100\n");
cpu->status = 1; // Stop execution
}
}
int main() {
z80* cpu = z80_new();
z80_set_intercept(cpu, cpu, my_intercept);
z80_run(cpu);
z80_destroy(cpu);
return 0;
}To implement I/O, you need to modify the cpu_out() and cpu_in() functions in z80.c:
// In z80.c, replace the existing cpu_out function:
void cpu_out(z80 *cpu, const uint32_t Port, const uint32_t Value) {
// Handle output to port
switch (Port) {
case 0x00:
// Write to console
putchar(Value & 0xFF);
break;
// Add more port handlers as needed
}
}
// In z80.c, replace the existing cpu_in function:
uint32_t cpu_in(z80 *cpu, const uint32_t Port) {
// Handle input from port
switch (Port) {
case 0x00:
// Read from keyboard
int ch = getchar();
return (ch == EOF) ? 0xFF : ch;
// Add more port handlers as needed
}
return 0xFF; // Default value
}Since memory is internal to the CPU structure, you can access it directly:
// Write a program to memory
uint8_t program[] = {0x3E, 0x41, 0xD3, 0x00, 0x76}; // LD A,0x41; OUT (0),A; HALT
for (int i = 0; i < sizeof(program); i++) {
cpu->RAM[i] = program[i];
}
// Read from memory
uint8_t value = cpu->RAM[0x1000];
// Write to memory
cpu->RAM[0x2000] = 0x55;When debug mode is enabled (cpu->debug = 1), the emulator enters an interactive debugger after each instruction. The debugger displays:
BC:0000 DE:0000 HL:0000 AF:0000 : [........]
IX:0000 IY:0000 SP:0000 PC:0000 : NOP
Command|? :
Press ? in the debugger to see all available commands:
Lowercase commands:
t- Trace to the next instruction (single step)c- Continue execution (exit debug mode)b- Dump memory pointed to by BC registerd- Dump memory pointed to by DE registerh- Dump memory pointed to by HL registerp- Dump the memory page PC points to (PC & 0xFF00)s- Dump the memory page SP points to (SP & 0xFF00)x- Dump the memory page IX points to (IX & 0xFF00)y- Dump the memory page IY points to (IY & 0xFF00)l- Disassemble 16 instructions from current PCq- Quit simulation (set CPU status to 1)
Uppercase commands:
B- Set breakpoint at specified addressC- Clear breakpointD- Dump memory at specified addressL- Disassemble at specified addressT- Step over a CALL instructionW- Set a byte/word watch at specified address
Note: Pressing Enter (no command) will single step to the next instruction.
BC:0000 DE:0000 HL:0000 AF:0000 : [........]
IX:0000 IY:0000 SP:0000 PC:0000 : NOP
Command|? : l
0000 : NOP
0001 : NOP
0002 : NOP
0003 : NOP
0004 : NOP
0005 : NOP
0006 : NOP
0007 : NOP
0008 : NOP
0009 : NOP
000A : NOP
000B : NOP
000C : NOP
000D : NOP
000E : NOP
000F : NOP
BC:0000 DE:0000 HL:0000 AF:0000 : [........]
IX:0000 IY:0000 SP:0000 PC:0000 : NOP
Command|? : B
Addr: 0100
Breakpoint set to 0100
BC:0000 DE:0000 HL:0000 AF:0000 : [........]
IX:0000 IY:0000 SP:0000 PC:0000 : NOP
Command|? : c
Copy z80.h, z80.c, console.h, console.c, and globals.h to your project.
Provide implementations for cpu_in() and cpu_out() functions. Memory is already built into the CPU structure.
z80* cpu = z80_new();
// Configure memory/I/O callbacks
// Load program into memory
z80_run(cpu);
z80_destroy(cpu);Set cpu->debug = 1 to enable the built-in debugger for development.
gcc -o my_emulator z80.c console.c my_code.cgcc -o my_emulator.exe z80.c console.c my_code.cThe Makefile can be extended with additional compiler flags:
CFLAGS = -O2 -Wall -WextraThe project includes a basic test in main.c that demonstrates CPU instantiation and debug mode. For comprehensive testing:
- Create test programs in Z80 machine code
- Load them into memory via direct access to
cpu->RAM[] - Set the program counter and run
- Verify register states and memory contents
- I/O stubs: The default I/O implementations are empty stubs
- Fixed 64KB memory: Memory is fixed at 64KB within the CPU structure
- Instruction timing: Cycle-accurate timing is not implemented
- Interrupt handling: Basic support exists but may need enhancement for specific use cases
- Platform-specific console: The console code uses termios and may need adaptation for Windows
Contributions are welcome! Please ensure:
- Code follows the existing style (spaces, not tabs)
- New features include appropriate documentation
- Changes don't break existing functionality
MIT License
Copyright (c) 2017 Mockba the Borg
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- MockbaTheBorg for the original RunCPM implementation
- Zilog for the Z80 microprocessor architecture
- Contributors to the RunCPM project