diff --git a/a2audit_test.go b/a2audit_test.go index 67d9070..7229ad4 100644 --- a/a2audit_test.go +++ b/a2audit_test.go @@ -22,9 +22,7 @@ func testA2AuditInternal(t *testing.T, model string, removeLangCard bool, cycles if err != nil { t.Fatal(err) } - at.terminateCondition = func(a *Apple2) bool { - return a.cpu.GetCycles() > cycles - } + at.terminateCondition = buildTerminateConditionTexts(at, messages, false, cycles) at.run() text := at.getText() diff --git a/apple2Run.go b/apple2Run.go index b36d641..e3399db 100644 --- a/apple2Run.go +++ b/apple2Run.go @@ -37,7 +37,7 @@ func (a *Apple2) Start(paused bool) { for i := 0; i < cpuSpinLoops; i++ { // Conditional tracing //pc, _ := a.cpu.GetPCAndSP() - //a.cpu.SetTrace((pc >= 0xc500 && pc < 0xc600) || (pc >= 0xc700 && pc < 0xc800)) + //a.cpu.SetTrace(pc >= 0x7f0 && pc < 0x0820) // Execution a.cpu.ExecuteInstruction() diff --git a/apple2Tester.go b/apple2Tester.go index f8b4251..ecb0c8a 100644 --- a/apple2Tester.go +++ b/apple2Tester.go @@ -1,12 +1,16 @@ package izapple2 import ( + "strings" + "github.com/ivanizag/izapple2/screen" ) +type terminateConditionFunc func(a *Apple2) bool + type apple2Tester struct { a *Apple2 - terminateCondition func(a *Apple2) bool + terminateCondition terminateConditionFunc } func makeApple2Tester(model string, overrides *configuration) (*apple2Tester, error) { @@ -46,3 +50,43 @@ func (at *apple2Tester) getText() string { func (at *apple2Tester) getText80() string { return screen.RenderTextModeString(at.a, true, false, false, at.a.isApple2e) } + +func buildTerminateConditionCycles(cycles uint64) terminateConditionFunc { + return func(a *Apple2) bool { + return a.cpu.GetCycles() > cycles + } +} + +const textCheckInterval = uint64(100_000) + +func buildTerminateConditionText(at *apple2Tester, needle string, col80 bool, timeoutCycles uint64) terminateConditionFunc { + needles := []string{needle} + return buildTerminateConditionTexts(at, needles, col80, timeoutCycles) +} + +func buildTerminateConditionTexts(at *apple2Tester, needles []string, col80 bool, timeoutCycles uint64) terminateConditionFunc { + lastCheck := uint64(0) + found := false + return func(a *Apple2) bool { + cycles := a.cpu.GetCycles() + if cycles > timeoutCycles { + return true + } + if cycles-lastCheck > textCheckInterval { + lastCheck = cycles + var text string + if col80 { + text = at.getText80() + } else { + text = at.getText() + } + for _, needle := range needles { + if !strings.Contains(text, needle) { + return false + } + } + found = true + } + return found + } +} diff --git a/cardCat_test.go b/cardCat_test.go index 84bd3e4..44add60 100644 --- a/cardCat_test.go +++ b/cardCat_test.go @@ -19,9 +19,7 @@ func testCardDetectedInternal(t *testing.T, model string, card string, cycles ui if err != nil { t.Fatal(err) } - at.terminateCondition = func(a *Apple2) bool { - return a.cpu.GetCycles() > cycles - } + at.terminateCondition = buildTerminateConditionText(at, banner, true, cycles) at.run() text := at.getText80() diff --git a/cardSwyft_test.go b/cardSwyft_test.go index e8a37e0..d7898c7 100644 --- a/cardSwyft_test.go +++ b/cardSwyft_test.go @@ -11,9 +11,8 @@ func TestSwyftTutorial(t *testing.T) { t.Fatal(err) } - at.terminateCondition = func(a *Apple2) bool { - return a.cpu.GetCycles() > 10_000_000 - } + at.terminateCondition = buildTerminateConditionText(at, "HOW TO USE SWYFTCARD", true, 10_000_000) + at.run() text := at.getText80() diff --git a/e2e_boot_test.go b/e2e_boot_test.go index aa536bc..eec1c79 100644 --- a/e2e_boot_test.go +++ b/e2e_boot_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func testBoots(t *testing.T, model string, disk string, cycles uint64, banner string, prompt string) { +func testBoots(t *testing.T, model string, disk string, cycles uint64, banner string, prompt string, col80 bool) { overrides := newConfiguration() if disk != "" { overrides.set(confS6, "diskii,disk1=\""+disk+"\"") @@ -17,12 +17,15 @@ func testBoots(t *testing.T, model string, disk string, cycles uint64, banner st if err != nil { t.Fatal(err) } - at.terminateCondition = func(a *Apple2) bool { - return a.cpu.GetCycles() > cycles - } + at.terminateCondition = buildTerminateConditionTexts(at, []string{banner, prompt}, col80, cycles) at.run() - text := at.getText() + var text string + if col80 { + text = at.getText80() + } else { + text = at.getText() + } if !strings.Contains(text, banner) { t.Errorf("Expected '%s', got '%s'", banner, text) } @@ -33,21 +36,25 @@ func testBoots(t *testing.T, model string, disk string, cycles uint64, banner st } func TestPlusBoots(t *testing.T) { - testBoots(t, "2plus", "", 200_000, "APPLE ][", "\n]") + testBoots(t, "2plus", "", 200_000, "APPLE ][", "\n]", false) } func Test2EBoots(t *testing.T) { - testBoots(t, "2e", "", 200_000, "Apple ][", "\n]") + testBoots(t, "2e", "", 200_000, "Apple ][", "\n]", false) } func Test2EnhancedBoots(t *testing.T) { - testBoots(t, "2enh", "", 200_000, "Apple //e", "\n]") + testBoots(t, "2enh", "", 200_000, "Apple //e", "\n]", false) } func TestBase64Boots(t *testing.T) { - testBoots(t, "base64a", "", 1_000_000, "BASE 64A", "\n]") + testBoots(t, "base64a", "", 1_000_000, "BASE 64A", "\n]", false) } func TestPlusDOS33Boots(t *testing.T) { - testBoots(t, "2plus", "/dos33.dsk", 100_000_000, "DOS VERSION 3.3", "\n]") + testBoots(t, "2plus", "/dos33.dsk", 100_000_000, "DOS VERSION 3.3", "\n]", false) +} + +func TestCPM65Boots(t *testing.T) { + testBoots(t, "2enh", "/cpm65.po", 5_000_000, "CP/M-65 for the Apple II", "\nA>", true) } diff --git a/memoryRange.go b/memoryRange.go index 3467747..af5a721 100644 --- a/memoryRange.go +++ b/memoryRange.go @@ -72,10 +72,6 @@ func (m *memoryRangeROM) poke(address uint16, value uint8) { // Ignore } -func (m *memoryRangeROM) subRange(a, b uint16) []uint8 { - return m.data[a-m.base+m.pageOffset : b-m.base+m.pageOffset] -} - //lint:ignore U1000 this is used to write debug code func identifyMemory(m memoryHandler) string { ram, ok := m.(*memoryRange) diff --git a/resources/cpm65.po b/resources/cpm65.po new file mode 100644 index 0000000..b6913d2 Binary files /dev/null and b/resources/cpm65.po differ diff --git a/traceBuilder.go b/traceBuilder.go index 7996b8b..8ec16b9 100644 --- a/traceBuilder.go +++ b/traceBuilder.go @@ -62,6 +62,11 @@ func buildTracerFactory() map[string]*traceBuilder { description: "Panic on unimplemented softswitches", connectFunc: func(a *Apple2) { a.io.setPanicNotImplemented(true) }, } + tracerFactory["cpm65"] = &traceBuilder{ + name: "cpm65", + description: "Trace CPM65 BDOS calls", + executionTracer: newTraceCpm65(false), + } return tracerFactory } diff --git a/traceCpm65.go b/traceCpm65.go new file mode 100644 index 0000000..597aebe --- /dev/null +++ b/traceCpm65.go @@ -0,0 +1,116 @@ +package izapple2 + +import ( + "fmt" +) + +/* + See: + https://github.com/davidgiven/cpm65 +*/ + +type traceCpm65 struct { + a *Apple2 + skipConsole bool +} + +const ( + cpm65BdosEntrypoint uint16 = 0x0804 // start-3, not really sure about this +) + +func newTraceCpm65(skipConsole bool) *traceCpm65 { + var t traceCpm65 + t.skipConsole = skipConsole + return &t +} + +func (t *traceCpm65) connect(a *Apple2) { + t.a = a +} + +func (t *traceCpm65) inspect() { + pc, _ := t.a.cpu.GetPCAndSP() + if pc == cpm65BdosEntrypoint { + regA, regX, regY, _ := t.a.cpu.GetAXYP() + param := uint16(regX)<<8 | uint16(regA) + switch regY { + case 2: // CONSOLE_OUTPUT + if !t.skipConsole { + fmt.Printf("CPM65 BDOS call $%02x:%s from $%04x with \"%c\"\n", regY, bdosCodeToName(regY), pc, regA) + } + case 9: // WRITE_STRING + if !t.skipConsole { + text := t.getCpmString(param) + fmt.Printf("CPM65 BDOS call $%02x:%s from $%04x with \"%s\"\n", regY, bdosCodeToName(regY), pc, text) + } + default: + fmt.Printf("CPM65 BDOS call $%02x:%s from $%04x\n", regY, bdosCodeToName(regY), pc) + } + } +} + +var cpm65BdosNames = []string{ + "EXIT_PROGRAM", // 0 + "CONSOLE_INPUT", // 1 + "CONSOLE_OUTPUT", // 2 + "AUX_INPUT", // 3 + "AUX_OUTPUT", // 4 + "PRINTER_OUTPUT", // 5 + "DIRECT_IO", // 6 + "GET_IO_BYTE", // 7 + "SET_IO_BYTE", // 8 + "WRITE_STRING", // 9 + "READ_LINE", // 10 + "CONSOLE_STATUS", // 11 + "GET_VERSION", // 12 + "RESET_DISKS", // 13 + "SELECT_DISK", // 14 + "OPEN_FILE", // 15 + "CLOSE_FILE", // 16 + "FIND_FIRST", // 17 + "FIND_NEXT", // 18 + "DELETE_FILE", // 19 + "READ_SEQUENTIAL", // 20 + "WRITE_SEQUENTIAL", // 21 + "CREATE_FILE", // 22 + "RENAME_FILE", // 23 + "GET_LOGIN_BITMAP", // 24 + "GET_CURRENT_DRIVE", // 25 + "SET_DMA_ADDRESS", // 26 + "GET_ALLOCATION_BITMAP", // 27 + "SET_DRIVE_READONLY", // 28 + "GET_READONLY_BITMAP", // 29 + "SET_FILE_ATTRIBUTES", // 30 + "GET_DPB", // 31 + "GET_SET_USER_NUMBER", // 32 + "READ_RANDOM", // 33 + "WRITE_RANDOM", // 34 + "COMPUTE_FILE_SIZE", // 35 + "COMPUTE_RANDOM_POINTER", // 36 + "RESET_DISK", // 37 + "GET_BIOS", // 38 + "", // 39 + "WRITE_RANDOM_FILLED", // 40 + "GETZP", // 41 + "GETTPA", // 42 +} + +func bdosCodeToName(code uint8) string { + if code < uint8(len(cpm65BdosNames)) { + return cpm65BdosNames[code] + } + return fmt.Sprintf("BDOS_%d", code) +} + +func (t *traceCpm65) getCpmString(address uint16) string { + s := "" + for { + ch := t.a.mmu.Peek(address) + if ch == '$' { + break + } + s += string(ch) + address++ + } + return s +}