diff --git a/README.md b/README.md index 11e30be..54faacf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,33 @@ -# emuz80gozex +# Z80 ZEX Test Suite +This directory contains a boilerplate for running Z80 processor tests, specifically designed for the ZEXALL.COM test suite. + +## Features + +- 64KB memory implementation +- Z80 CPU emulation using the z80 package +- BDOS call handling for CP/M functions: + - Print character (function 2) + - Print string (function 9) +- Proper CALL/RET simulation for BDOS calls +- Program termination detection (when PC reaches 0x0000) + +## Setup + +1. Place the zexall.com file in this directory +2. Run with: `go run .` + +## Behavior + +- If zexall.com is not found, the program will exit with an error message +- When the file is loaded successfully, the Z80 emulator will execute the test suite +- BDOS calls for character and string output are properly handled +- Program termination is detected when PC reaches 0x0000 + +## Implementation Details + +The boilerplate includes: +- Memory management for 64KB address space +- I/O handling for CP/M system calls +- CPU initialization for CP/M programs +- Execution loop with termination condition diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..51f91ed --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module z80zex + +go 1.25.1 + +require git.tygh.ru/kiltum/emuz80go v1.0.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b58fac7 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +git.tygh.ru/kiltum/emuz80go v1.0.0 h1:xoaUVxobRcM/FkP42hc92gpNiR+u1CSpzEUSeWBOsYM= +git.tygh.ru/kiltum/emuz80go v1.0.0/go.mod h1:GOLmpQB+SGU7e65MbvpAp57lDnu3LK/SNdVi3SD6Oz4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..7833b70 --- /dev/null +++ b/main.go @@ -0,0 +1,201 @@ +package main + +import ( + "fmt" + "os" + + z80 "git.tygh.ru/kiltum/emuz80go" +) + +// Memory64K represents 64KB of memory +type Memory64K struct { + data [0x10000]byte // 64KB = 65536 bytes +} + +// ReadByte reads a byte from memory +func (m *Memory64K) ReadByte(address uint16) byte { + return m.data[address] +} + +// WriteByte writes a byte to memory +func (m *Memory64K) WriteByte(address uint16, value byte) { + m.data[address] = value +} + +// ReadWord reads a word from memory +func (m *Memory64K) ReadWord(address uint16) uint16 { + return uint16(m.data[address]) | (uint16(m.data[address+1]) << 8) +} + +// WriteWord writes a word to memory +func (m *Memory64K) WriteWord(address uint16, value uint16) { + m.data[address] = byte(value) + m.data[address+1] = byte(value >> 8) +} + +// IOHandler handles I/O operations +type IOHandler struct{} + +// ReadPort reads from an I/O port +func (io *IOHandler) ReadPort(port uint16) byte { + return 0xFF // Default value +} + +// WritePort writes to an I/O port +func (io *IOHandler) WritePort(port uint16, value byte) { + // No I/O port handling needed for BDOS calls +} + +// CheckInterrupt checks for interrupts +func (io *IOHandler) CheckInterrupt() bool { + return false +} + +// handleBDOSCall handles CP/M BDOS calls +func handleBDOSCall(cpu *z80.CPU, memory *Memory64K) { + // BDOS is called by jumping to 0x0005 + // Function number is in register C + // Parameters are in other registers depending on the function + + function := cpu.C + + switch function { + case 2: // Print character (character in E) + fmt.Print(string(cpu.E)) + case 9: // Print string (string address in DE) + addr := cpu.GetDE() + // Read characters from memory until we encounter '$' + for { + char := memory.ReadByte(addr) + if char == '$' { + break + } + fmt.Print(string(char)) + addr++ + } + } + + // In a real CP/M system, after handling the BDOS call, + // execution would return to the caller via a RET instruction. + // We simulate this by popping the return address from the stack + // and setting the PC to that address. + + // Pop return address from stack + lowByte := memory.ReadByte(cpu.SP) + cpu.SP++ + highByte := memory.ReadByte(cpu.SP) + cpu.SP++ + returnAddress := (uint16(highByte) << 8) | uint16(lowByte) + + // Set PC to return address + cpu.PC = returnAddress +} + +// loadZEXALL loads the zexall.com file into memory at address 0x100 +func loadZEXALL(memory *Memory64K, filename string) error { + // Load file data + data, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read file %s: %v", filename, err) + } + + // Load into memory at address 0x100 + loadAddress := uint16(0x100) + for i, b := range data { + addr := uint32(loadAddress) + uint32(i) + if addr < 0x10000 { + memory.data[uint16(addr)] = b + } else { + break // Memory overflow protection + } + } + + return nil +} + +func main() { + // Create memory and IO instances + memory := &Memory64K{} + io := &IOHandler{} + + // Create CPU instance + cpu := z80.New(memory, io) + + // Set up initial state for CP/M program + // Stack pointer typically starts at 0xFFFF in CP/M programs + cpu.SP = 0xFFFF + + // Program counter starts at 0x100 for .COM files + cpu.PC = 0x100 + + fmt.Printf("ZEXDOC\n") + // Load zexall.com file + err := loadZEXALL(memory, "zexdoc.com") + if err != nil { + fmt.Printf("Error: Could not load zexdoc.com: %v\n", err) + fmt.Println("Exiting program.") + return + } + + // Execute instructions + for { + // Check if program has ended (PC = 0x0000) + if cpu.PC == 0x0000 { + fmt.Println("Program ended (PC reached 0x0000)") + break + } + + // Check if this is a BDOS call (PC = 0x0005) + if cpu.PC == 0x0005 { + handleBDOSCall(cpu, memory) + // After handling BDOS call, continue execution + continue + } + + // Execute one instruction + ticks := cpu.ExecuteOneInstruction() + _ = ticks // Ignore ticks for now + + // Optional: Add a safety counter to prevent infinite loops during development + // You can remove this once everything is working properly + } + + fmt.Printf("ZEXALL\n") + + cpu.SP = 0xFFFF + + // Program counter starts at 0x100 for .COM files + cpu.PC = 0x100 + + // Load zexall.com file + err = loadZEXALL(memory, "zexall.com") + if err != nil { + fmt.Printf("Error: Could not load zexall.com: %v\n", err) + fmt.Println("Exiting program.") + return + } + + // Execute instructions + for { + // Check if program has ended (PC = 0x0000) + if cpu.PC == 0x0000 { + fmt.Println("Program ended (PC reached 0x0000)") + break + } + + // Check if this is a BDOS call (PC = 0x0005) + if cpu.PC == 0x0005 { + handleBDOSCall(cpu, memory) + // After handling BDOS call, continue execution + continue + } + + // Execute one instruction + ticks := cpu.ExecuteOneInstruction() + _ = ticks // Ignore ticks for now + + // Optional: Add a safety counter to prevent infinite loops during development + // You can remove this once everything is working properly + } + +} diff --git a/zexall.com b/zexall.com new file mode 100644 index 0000000..fa0e6ff Binary files /dev/null and b/zexall.com differ diff --git a/zexdoc.com b/zexdoc.com new file mode 100644 index 0000000..5cfd531 Binary files /dev/null and b/zexdoc.com differ