From 7c1dbe10597e6d3cdbf934755a1cfaa4ab906c5e Mon Sep 17 00:00:00 2001 From: Viacheslav Kaloshin Date: Thu, 25 Sep 2025 15:46:50 +0300 Subject: [PATCH] first public release: --- README.md | 33 ++++++++- go.mod | 5 ++ go.sum | 2 + main.go | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++ zexall.com | Bin 0 -> 8704 bytes zexdoc.com | Bin 0 -> 8704 bytes 6 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 zexall.com create mode 100644 zexdoc.com 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 0000000000000000000000000000000000000000..fa0e6ff0854197656e31de9af06c76a7877978b8 GIT binary patch literal 8704 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 literal 0 HcmV?d00001 diff --git a/zexdoc.com b/zexdoc.com new file mode 100644 index 0000000000000000000000000000000000000000..5cfd53161cd70d5a907b4e6fae3ed870dcfd103f GIT binary patch literal 8704 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 literal 0 HcmV?d00001