From d2c897f7ea6db3982103b409a2fc73048e8bd645 Mon Sep 17 00:00:00 2001 From: Ivan Izaguirre Date: Sun, 24 Feb 2019 00:41:32 +0100 Subject: [PATCH] Improved memory manager to support Apple2e shadow ROM --- apple2/addressSpace.go | 110 ------------------- apple2/apple2.go | 12 ++- apple2/ioC0Page.go | 151 ++++++++++++++------------- apple2/memoryManager.go | 144 +++++++++++++++++++++++++ apple2/pagedMemory.go | 8 +- apple2/romdumps/Apple2.rom | Bin 0 -> 12288 bytes apple2/romdumps/Apple2_Plus.rom | Bin 0 -> 12288 bytes apple2/romdumps/Apple2e.rom | Bin 0 -> 16384 bytes apple2/romdumps/Apple2e_Enhanced.rom | Bin 0 -> 16384 bytes apple2/softSwitches2e.go | 74 +++++++++++++ core6502/execute.go | 60 +++++++---- core6502/registers.go | 44 ++++---- core6502/registers_test.go | 8 +- main.go | 8 +- 14 files changed, 380 insertions(+), 239 deletions(-) delete mode 100644 apple2/addressSpace.go create mode 100644 apple2/memoryManager.go create mode 100644 apple2/romdumps/Apple2.rom create mode 100644 apple2/romdumps/Apple2_Plus.rom create mode 100644 apple2/romdumps/Apple2e.rom create mode 100644 apple2/romdumps/Apple2e_Enhanced.rom create mode 100644 apple2/softSwitches2e.go diff --git a/apple2/addressSpace.go b/apple2/addressSpace.go deleted file mode 100644 index 7bfe6d5..0000000 --- a/apple2/addressSpace.go +++ /dev/null @@ -1,110 +0,0 @@ -package apple2 - -import ( - "bufio" - "os" -) - -// See https://i.stack.imgur.com/yn21s.gif - -type addressSpace struct { - activeMemory *pagedMemory - physicalRAM [256]ramPage // up to 64 Kb - physicalROM [48]romPage // up to 12 Kb - ioPage ioC0Page - textPages1 *textPages - activeSlow int // Slot that has the addressing 0xc800 to 0ccfff -} - -const ( - ioAreaMask uint16 = 0xFF80 - ioAreaValue uint16 = 0xC000 - ioC8Off uint16 = 0xCFFF -) - -// Peek returns the data on the given address -func (a *addressSpace) Peek(address uint16) uint8 { - if address == ioC8Off { - a.resetSlotRoms() - } - if (address & ioAreaMask) == ioAreaValue { - return a.ioPage.Peek(uint8(address)) - } - return a.activeMemory.Peek(address) -} - -// Poke sets the data at the given address -func (a *addressSpace) Poke(address uint16, value uint8) { - if address == ioC8Off { - a.resetSlotRoms() - } - if (address & ioAreaMask) == ioAreaValue { - a.ioPage.Poke(uint8(address), value) - } - a.activeMemory.Poke(address, value) -} - -func (a *addressSpace) resetSlotRoms() { - // TODO -} - -func newAddressSpace() *addressSpace { - var a addressSpace - - var m pagedMemory - a.activeMemory = &m - - // Assign RAM from 0x0000 to 0xbfff, 48kb - for i := 0; i <= 0xbf; i++ { - m.SetPage(uint8(i), &(a.physicalRAM[i])) - } - - // Assign ROM from 0xd000 to 0xfff, 12 kb. The ROM is empty - for i := 0xd0; i <= 0xff; i++ { - m.SetPage(uint8(i), &(a.physicalROM[i-0xd0])) - } - - // Set the 0xc000 to 0xcfff as unasigned, 4kb. It wil be taken by slot cards. - for i := uint8(0xc0); i <= 0xcf; i++ { - var p unassignedPage - p.page = i - m.SetPage(i, &p) - } - - // Replace RAM in the TEXT1 area. - // TODO: treat as normal ram. Add is dirty in all RAM pages - var t textPages - a.textPages1 = &t - for i := 0; i < 4; i++ { - m.SetPage(uint8(4+i), &(t.pages[i])) - } - - return &a -} - -// LoadRom loads a binary file to the top of the memory. -func (a *addressSpace) loadRom(filename string) { - f, err := os.Open(filename) - if err != nil { - panic(err) - } - defer f.Close() - - stats, statsErr := f.Stat() - if statsErr != nil { - panic(err) - } - - size := stats.Size() - if size != 12288 { - panic("Rom size not supported") - } - bytes := make([]byte, size) - - buf := bufio.NewReader(f) - buf.Read(bytes) - - for i, v := range bytes { - a.physicalROM[i>>8].burn(uint8(i), uint8(v)) - } -} diff --git a/apple2/apple2.go b/apple2/apple2.go index c93e683..cbda391 100644 --- a/apple2/apple2.go +++ b/apple2/apple2.go @@ -4,15 +4,17 @@ import "go6502/core6502" // Run instantiates an apple2 and start emulation func Run(romFile string, log bool) { - a := newAddressSpace() - a.loadRom(romFile) + mmu := newAddressSpace(romFile) + if mmu.isApple2e { + addApple2ESoftSwitches(mmu) + } var s core6502.State - s.Mem = a + s.Mem = mmu var fe ansiConsoleFrontend - a.ioPage.setKeyboardProvider(&fe) - go fe.textModeGoRoutine(a.textPages1) + mmu.ioPage.setKeyboardProvider(&fe) + go fe.textModeGoRoutine(mmu.textPages1) // Start the processor core6502.Reset(&s) diff --git a/apple2/ioC0Page.go b/apple2/ioC0Page.go index 3eafce0..6e2587c 100644 --- a/apple2/ioC0Page.go +++ b/apple2/ioC0Page.go @@ -5,12 +5,15 @@ import ( ) type ioC0Page struct { - ioFlags uint64 - data [1]uint8 - keyboard keyboardProvider - addressSpace *addressSpace + softSwitches [128]softSwitch + softSwitchesData [128]uint8 + keyboard keyboardProvider + mmu *memoryManager } +type softSwitch func(io *ioC0Page, isWrite bool, value uint8) uint8 + +// TODO: change interface to func type keyboardProvider interface { getKey() (key uint8, ok bool) } @@ -19,44 +22,52 @@ type keyboardProvider interface { // See https://stason.org/TULARC/pc/apple2/programmer/004-I-d-like-to-do-some-serious-Apple-II-programming-Whe.html const ( - ioFlagNone uint8 = 0 - ioFlagGraphics uint8 = 3 - ioFlagMixed uint8 = 8 - ioFlagSecondPage uint8 = 1 - ioFlagHiRes uint8 = 2 - ioFlagAnnunciator0 uint8 = 4 - ioFlagAnnunciator1 uint8 = 5 - ioFlagAnnunciator2 uint8 = 6 - ioFlagAnnunciator3 uint8 = 7 + ioDataKeyboard uint8 = 0x10 + ioFlagGraphics uint8 = 0x50 + ioFlagMixed uint8 = 0x52 + ioFlagSecondPage uint8 = 0x54 + ioFlagHiRes uint8 = 0x56 + ioFlagAnnunciator0 uint8 = 0x58 + ioFlagAnnunciator1 uint8 = 0x5a + ioFlagAnnunciator2 uint8 = 0x5c + ioFlagAnnunciator3 uint8 = 0x5e ) -const ( - ioDataKeyboard uint8 = 0 -) - -type softSwitch struct { - ioFlag uint8 - value bool - onWriteOnly bool +func (p *ioC0Page) isSoftSwitchExtActive(ioFlag uint8) bool { + return (p.softSwitchesData[ioFlag] & 0x08) == 0x80 } -var softSwitches = [256]softSwitch{ - 0x50: softSwitch{ioFlagGraphics, false, false}, - 0x51: softSwitch{ioFlagGraphics, true, false}, - 0x52: softSwitch{ioFlagMixed, false, false}, - 0x53: softSwitch{ioFlagMixed, true, false}, - 0x54: softSwitch{ioFlagSecondPage, false, false}, - 0x55: softSwitch{ioFlagSecondPage, true, false}, - 0x56: softSwitch{ioFlagHiRes, false, false}, - 0x57: softSwitch{ioFlagHiRes, true, false}, - 0x58: softSwitch{ioFlagAnnunciator0, false, false}, - 0x59: softSwitch{ioFlagAnnunciator0, true, false}, - 0x5a: softSwitch{ioFlagAnnunciator1, false, false}, - 0x5b: softSwitch{ioFlagAnnunciator1, true, false}, - 0x5c: softSwitch{ioFlagAnnunciator2, false, false}, - 0x5d: softSwitch{ioFlagAnnunciator2, true, false}, - 0x5e: softSwitch{ioFlagAnnunciator3, false, false}, - 0x5f: softSwitch{ioFlagAnnunciator3, true, false}, +func newIoC0Page(mmu *memoryManager) *ioC0Page { + var p ioC0Page + p.mmu = mmu + ss := &p.softSwitches + + ss[0x00] = getKeySoftSwitch // Keyboard + ss[0x10] = strobeKeyboardSoftSwitch // Keyboard Strobe + ss[0x30] = notImplementedSoftSwitch // Speaker + + ss[0x50] = getSoftSwitch(ioFlagGraphics, false) + ss[0x51] = getSoftSwitch(ioFlagGraphics, true) + ss[0x52] = getSoftSwitch(ioFlagMixed, false) + ss[0x53] = getSoftSwitch(ioFlagMixed, true) + ss[0x54] = getSoftSwitch(ioFlagSecondPage, false) + ss[0x55] = getSoftSwitch(ioFlagSecondPage, true) + ss[0x56] = getSoftSwitch(ioFlagHiRes, false) + ss[0x57] = getSoftSwitch(ioFlagHiRes, true) + ss[0x58] = getSoftSwitch(ioFlagAnnunciator0, false) + ss[0x59] = getSoftSwitch(ioFlagAnnunciator0, true) + ss[0x5a] = getSoftSwitch(ioFlagAnnunciator1, false) + ss[0x5b] = getSoftSwitch(ioFlagAnnunciator1, true) + ss[0x5c] = getSoftSwitch(ioFlagAnnunciator2, false) + ss[0x5d] = getSoftSwitch(ioFlagAnnunciator2, true) + ss[0x5e] = getSoftSwitch(ioFlagAnnunciator3, false) + ss[0x5f] = getSoftSwitch(ioFlagAnnunciator3, true) + + return &p +} + +func (p *ioC0Page) setKeyboardProvider(kb keyboardProvider) { + p.keyboard = kb } func (p *ioC0Page) Peek(address uint8) uint8 { @@ -70,47 +81,47 @@ func (p *ioC0Page) Poke(address uint8, value uint8) { } func (p *ioC0Page) access(address uint8, isWrite bool, value uint8) uint8 { - - ss := softSwitches[address] - if ss.ioFlag != ioFlagNone { - if !isWrite || !!ss.onWriteOnly { - if ss.value { - p.ioFlags |= 1 << ss.ioFlag - } else { - p.ioFlags &^= 1 << ss.ioFlag - } - } - } else { - switch address { - case 0x00: // keyboard (Is this the full range 0x0?) - return p.getKey() - case 0x10: // strobe (Is this the full range 0x1?) - return p.strobeKeyboard() - case 0x30: // spkr - // TODO: Support sound - default: - panic(fmt.Sprintf("Unknown softswitch 0xC0%02x", address)) - } + // The second hals of the pages is reserved for slots + if address >= 0x80 { + // TODO reserved slots data + return 0 } + + ss := p.softSwitches[address] + if ss == nil { + panic(fmt.Sprintf("Unknown softswitch 0xC0%02x", address)) + } + + return ss(p, isWrite, value) +} + +func getSoftSwitch(ioFlag uint8, isSet bool) softSwitch { + return func(io *ioC0Page, isWrite bool, value uint8) uint8 { + if isSet { + io.softSwitchesData[ioFlag] = 0x80 + } else { + io.softSwitchesData[ioFlag] = 0 + } + return 0 + } +} + +func notImplementedSoftSwitch(*ioC0Page, bool, uint8) uint8 { return 0 } -func (p *ioC0Page) setKeyboardProvider(kb keyboardProvider) { - p.keyboard = kb -} - -func (p *ioC0Page) getKey() uint8 { - strobed := (p.data[ioDataKeyboard] & (1 << 7)) == 0 +func getKeySoftSwitch(p *ioC0Page, _ bool, _ uint8) uint8 { + strobed := (p.softSwitchesData[ioDataKeyboard] & (1 << 7)) == 0 if strobed && p.keyboard != nil { if key, ok := p.keyboard.getKey(); ok { - p.data[ioDataKeyboard] = key + (1 << 7) + p.softSwitchesData[ioDataKeyboard] = key + (1 << 7) } } - return p.data[ioDataKeyboard] + return p.softSwitchesData[ioDataKeyboard] } -func (p *ioC0Page) strobeKeyboard() uint8 { - result := p.data[ioDataKeyboard] - p.data[ioDataKeyboard] &^= 1 << 7 +func strobeKeyboardSoftSwitch(p *ioC0Page, _ bool, _ uint8) uint8 { + result := p.softSwitchesData[ioDataKeyboard] + p.softSwitchesData[ioDataKeyboard] &^= 1 << 7 return result } diff --git a/apple2/memoryManager.go b/apple2/memoryManager.go new file mode 100644 index 0000000..92eb18b --- /dev/null +++ b/apple2/memoryManager.go @@ -0,0 +1,144 @@ +package apple2 + +import ( + "bufio" + "os" +) + +// See https://fabiensanglard.net/fd_proxy/prince_of_persia/Inside%20the%20Apple%20IIe.pdf +// See https://i.stack.imgur.com/yn21s.gif + +type memoryManager struct { + // Map of assigned pages + activeMemory *pagedMemory + // Pages prepared to be paged in and out + physicalMainRAM []ramPage // 0x0000 to 0xbfff, Up to 48 Kb + physicalROM []romPage // 0xd000 to 0xffff, 12 Kb + physicalROMe []romPage // 0xc000 to 0xcfff, Zero or 4bk in the Apple2e + unassignedExpansionROM []unassignedPage // 0xc000 to 0xcfff + ioPage *ioC0Page // 0xc000 to 0xc080 + isApple2e bool + textPages1 *textPages // 0x0400 to 0x07ff + activeSlow int // Slot that has the addressing 0xc800 to 0ccfff +} + +const ( + ioAreaMask uint16 = 0xFF80 + ioAreaValue uint16 = 0xC000 + ioC8Off uint16 = 0xCFFF +) + +// Peek returns the data on the given address +func (mmu *memoryManager) Peek(address uint16) uint8 { + if address == ioC8Off { + mmu.resetSlotExpansionRoms() + } + return mmu.activeMemory.Peek(address) +} + +// Poke sets the data at the given address +func (mmu *memoryManager) Poke(address uint16, value uint8) { + if address == ioC8Off { + mmu.resetSlotExpansionRoms() + } + mmu.activeMemory.Poke(address, value) +} + +// When 0xcfff is accessed the card expansion rom is unassigned +func (mmu *memoryManager) resetSlotExpansionRoms() { + if mmu.ioPage.isSoftSwitchExtActive(ioFlagIntCxRom) { + // Ignore if the Apple2 shadow ROM is active + return + } + for i := 8; i < 16; i++ { + p := mmu.unassignedExpansionROM[i] + mmu.activeMemory.SetPage(uint8(i+0xc0), &p) + } +} + +func newAddressSpace(romImage string) *memoryManager { + var mmu memoryManager + + var m pagedMemory + mmu.activeMemory = &m + + // Assign RAM from 0x0000 to 0xbfff, 48kb + mmu.physicalMainRAM = make([]ramPage, 0xc0) + for i := 0; i <= 0xbf; i++ { + m.SetPage(uint8(i), &(mmu.physicalMainRAM[i])) + } + + mmu.loadRom(romImage) + // Assign the first 12kb of ROM from 0xd000 to 0xfff + for i := 0xd0; i <= 0xff; i++ { + m.SetPage(uint8(i), &(mmu.physicalROM[i-0xd0])) + } + + // Set the io in 0xc000 + mmu.ioPage = newIoC0Page(&mmu) + m.SetPage(0xc0, mmu.ioPage) + + // Set the 0xc100 to 0xcfff as unasigned, 4kb. It wil be taken by slot cards. + mmu.unassignedExpansionROM = make([]unassignedPage, 0x10) + for i := 1; i < 0x10; i++ { + page := uint8(i + 0xc0) + p := &mmu.unassignedExpansionROM[i] + p.page = page + m.SetPage(page, p) + } + + // Replace RAM in the TEXT1 area. + // TODO: treat as normal ram. Add is dirty in all RAM pages + var t textPages + mmu.textPages1 = &t + for i := 0; i < 4; i++ { + m.SetPage(uint8(4+i), &(t.pages[i])) + } + + return &mmu +} + +// LoadRom loads a binary file to the top of the memory. +const ( + apple2RomSize = 12 * 1024 + apple2eRomSize = 16 * 1024 +) + +func (mmu *memoryManager) loadRom(filename string) { + f, err := os.Open(filename) + if err != nil { + panic(err) + } + defer f.Close() + + stats, statsErr := f.Stat() + if statsErr != nil { + panic(err) + } + + size := stats.Size() + if size != apple2RomSize && size != apple2eRomSize { + panic("Rom size not supported") + } + bytes := make([]byte, size) + buf := bufio.NewReader(f) + buf.Read(bytes) + + romStart := 0 + if size == apple2eRomSize { + // The extra 4kb ROM is first in the rom file. + // It starts with 256 unused bytes not mapped to 0xc000. + mmu.isApple2e = true + extraRomSize := apple2eRomSize - apple2RomSize + mmu.physicalROMe = make([]romPage, extraRomSize>>8) + for i := 0; i < extraRomSize; i++ { + mmu.physicalROMe[i>>8].burn(uint8(i), bytes[i]) + } + romStart = extraRomSize + } + + mmu.physicalROM = make([]romPage, apple2RomSize>>8) + for i := 0; i < apple2RomSize; i++ { + mmu.physicalROM[i>>8].burn(uint8(i), bytes[i+romStart]) + } +} diff --git a/apple2/pagedMemory.go b/apple2/pagedMemory.go index c977b3d..e3f6903 100644 --- a/apple2/pagedMemory.go +++ b/apple2/pagedMemory.go @@ -1,6 +1,9 @@ package apple2 -import "fmt" +import ( + "fmt" + "reflect" +) // memoryPage is a data page of 256 bytes type memoryPage interface { @@ -24,12 +27,11 @@ func (m *pagedMemory) Peek(address uint16) uint8 { func (m *pagedMemory) Poke(address uint16, value uint8) { hi := uint8(address >> 8) lo := uint8(address) - //fmt.Println(hi) m.data[hi].Poke(lo, value) } // SetPage assigns a MemoryPage implementation on the page given func (m *pagedMemory) SetPage(index uint8, page memoryPage) { - fmt.Printf("Seeting page 0x%02x\n", index) + fmt.Printf("Assigning page 0x%02x type %s\n", index, reflect.TypeOf(page)) m.data[index] = page } diff --git a/apple2/romdumps/Apple2.rom b/apple2/romdumps/Apple2.rom new file mode 100644 index 0000000000000000000000000000000000000000..e53fa17ee777b0aa6c498ff7cb61369d83d604cc GIT binary patch literal 12288 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 E7hH{c!~g&Q literal 0 HcmV?d00001 diff --git a/apple2/romdumps/Apple2_Plus.rom b/apple2/romdumps/Apple2_Plus.rom new file mode 100644 index 0000000000000000000000000000000000000000..b0641c38987bbfb407afb1e5cda45b5efcf0e1d3 GIT binary patch literal 12288 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 F{|8ULi|GIW literal 0 HcmV?d00001 diff --git a/apple2/romdumps/Apple2e_Enhanced.rom b/apple2/romdumps/Apple2e_Enhanced.rom new file mode 100644 index 0000000000000000000000000000000000000000..65bb648567f66b37874b1a417f564b296280c5f7 GIT binary patch literal 16384 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 I|Nid(1v1KeNdN!< literal 0 HcmV?d00001 diff --git a/apple2/softSwitches2e.go b/apple2/softSwitches2e.go new file mode 100644 index 0000000..9805fbe --- /dev/null +++ b/apple2/softSwitches2e.go @@ -0,0 +1,74 @@ +package apple2 + +import "fmt" + +const ( + ioFlagIntCxRom uint8 = 0x15 + ioFlagSlotC3Rom uint8 = 0x17 +) + +func addApple2ESoftSwitches(mmu *memoryManager) { + ss := &mmu.ioPage.softSwitches + ss[0x06] = getSoftSwitchExt(ioFlagIntCxRom, 0x00, softSwitchIntCxRomOff) + ss[0x07] = getSoftSwitchExt(ioFlagIntCxRom, 0x80, softSwitchIntCxRomOn) + ss[0x15] = getStatusSoftSwitchExt(ioFlagIntCxRom) + + ss[0x0A] = getSoftSwitchExt(ioFlagSlotC3Rom, 0x00, softSwitchSlotC3RomOff) + ss[0x0B] = getSoftSwitchExt(ioFlagSlotC3Rom, 0x80, softSwitchSlotC3RomOn) + ss[0x17] = getStatusSoftSwitchExt(ioFlagSlotC3Rom) + + ss[0x1c] = getStatusSoftSwitchExt(ioFlagSecondPage) + +} + +type softSwitchExtAction func(io *ioC0Page) + +func getStatusSoftSwitchExt(ioFlag uint8) softSwitch { + return func(io *ioC0Page, isWrite bool, value uint8) uint8 { + return io.softSwitchesData[ioFlag] + } +} + +func getSoftSwitchExt(ioFlag uint8, dstValue uint8, action softSwitchExtAction) softSwitch { + return func(io *ioC0Page, isWrite bool, value uint8) uint8 { + fmt.Printf("Softswitch 0x%02x %v %v\n", ioFlag, isWrite, dstValue) + if !isWrite { + return 0 // Ignore reads + } + currentValue := io.softSwitchesData[ioFlag] + if currentValue == dstValue { + return 0 // Already switched, ignore + } + action(io) + io.softSwitchesData[ioFlag] = value + return 0 + } +} + +func softSwitchIntCxRomOn(io *ioC0Page) { + for i := uint8(1); i < 16; i++ { + io.mmu.activeMemory.SetPage(uint8(0xc0+i), &io.mmu.physicalROMe[i]) + } +} + +func softSwitchIntCxRomOff(io *ioC0Page) { + // TODO restore all the ROM from the slot for 0xc1 to 0xc7 + for i := 1; i <= 16; i++ { + io.mmu.activeMemory.SetPage(uint8(0xc0+i), &io.mmu.unassignedExpansionROM[i]) + } +} + +func softSwitchSlotC3RomOn(io *ioC0Page) { + if io.isSoftSwitchExtActive(ioFlagIntCxRom) { + return // Ignore if allt the Apple2 shadow ROM is active + } + // TODO restore the slot 3 ROM + io.mmu.activeMemory.SetPage(0xC3, &io.mmu.unassignedExpansionROM[3]) +} + +func softSwitchSlotC3RomOff(io *ioC0Page) { + if io.isSoftSwitchExtActive(ioFlagIntCxRom) { + return // Ignore if allt the Apple2 shadow ROM is active + } + io.mmu.activeMemory.SetPage(0xC3, &io.mmu.physicalROMe[3]) +} diff --git a/core6502/execute.go b/core6502/execute.go index 6d736e0..84b66e1 100644 --- a/core6502/execute.go +++ b/core6502/execute.go @@ -4,7 +4,7 @@ import "fmt" // State represents the state of the simulated device type State struct { - Reg Registers + Reg registers Mem Memory } @@ -41,25 +41,46 @@ func getWordInLine(line []uint8) uint16 { return uint16(line[1]) + 0x100*uint16(line[2]) } -func resolve(s *State, line []uint8, opcode opcode) (value uint8, address uint16, setValue func(uint8)) { +func resolveValue(s *State, line []uint8, opcode opcode) uint8 { + getValue, _, _ := resolve(s, line, opcode) + return getValue() +} + +func resolveGetSetValue(s *State, line []uint8, opcode opcode) (value uint8, setValue func(uint8)) { + getValue, setValue, _ := resolve(s, line, opcode) + value = getValue() + return +} + +func resolveSetValue(s *State, line []uint8, opcode opcode) func(uint8) { + _, setValue, _ := resolve(s, line, opcode) + return setValue +} + +func resolveAddress(s *State, line []uint8, opcode opcode) uint16 { + _, _, address := resolve(s, line, opcode) + return address +} + +func resolve(s *State, line []uint8, opcode opcode) (getValue func() uint8, setValue func(uint8), address uint16) { hasAddress := true register := regNone switch opcode.addressMode { case modeAccumulator: - value = s.Reg.getA() + getValue = func() uint8 { return s.Reg.getA() } hasAddress = false register = regA case modeImplicitX: - value = s.Reg.getX() + getValue = func() uint8 { return s.Reg.getX() } hasAddress = false register = regX case modeImplicitY: - value = s.Reg.getY() + getValue = func() uint8 { return s.Reg.getY() } hasAddress = false register = regY case modeImmediate: - value = line[1] + getValue = func() uint8 { return line[1] } hasAddress = false case modeZeroPage: address = uint16(line[1]) @@ -85,7 +106,7 @@ func resolve(s *State, line []uint8, opcode opcode) (value uint8, address uint16 } if hasAddress { - value = s.Mem.Peek(address) + getValue = func() uint8 { return s.Mem.Peek(address) } } setValue = func(value uint8) { @@ -122,7 +143,7 @@ func buildOpTransfer(regSrc int, regDst int) opFunc { func buildOpIncDec(inc bool) opFunc { return func(s *State, line []uint8, opcode opcode) { - value, _, setValue := resolve(s, line, opcode) + value, setValue := resolveGetSetValue(s, line, opcode) if inc { value++ } else { @@ -135,7 +156,7 @@ func buildOpIncDec(inc bool) opFunc { func buildOpShift(isLeft bool, isRotate bool) opFunc { return func(s *State, line []uint8, opcode opcode) { - value, _, setValue := resolve(s, line, opcode) + value, setValue := resolveGetSetValue(s, line, opcode) oldCarry := s.Reg.getFlagBit(flagC) var carry bool @@ -160,7 +181,7 @@ func buildOpShift(isLeft bool, isRotate bool) opFunc { func buildOpLoad(regDst int) opFunc { return func(s *State, line []uint8, opcode opcode) { - value, _, _ := resolve(s, line, opcode) + value := resolveValue(s, line, opcode) s.Reg.setRegister(regDst, value) s.Reg.updateFlagZN(value) } @@ -168,7 +189,7 @@ func buildOpLoad(regDst int) opFunc { func buildOpStore(regSrc int) opFunc { return func(s *State, line []uint8, opcode opcode) { - _, _, setValue := resolve(s, line, opcode) + setValue := resolveSetValue(s, line, opcode) value := s.Reg.getRegister(regSrc) setValue(value) } @@ -192,7 +213,7 @@ func buildOpBranch(flag uint8, value bool) opFunc { } func opBIT(s *State, line []uint8, opcode opcode) { - value, _, _ := resolve(s, line, opcode) + value := resolveValue(s, line, opcode) acc := s.Reg.getA() // Future note: The immediate addressing mode (65C02 or 65816 only) does not affect V. s.Reg.updateFlag(flagZ, value&acc == 0) @@ -202,7 +223,7 @@ func opBIT(s *State, line []uint8, opcode opcode) { func buildOpCompare(reg int) opFunc { return func(s *State, line []uint8, opcode opcode) { - value, _, _ := resolve(s, line, opcode) + value := resolveValue(s, line, opcode) reference := s.Reg.getRegister(reg) s.Reg.updateFlagZN(reference - value) s.Reg.updateFlag(flagC, reference >= value) @@ -215,7 +236,7 @@ func operationXor(a uint8, b uint8) uint8 { return a ^ b } func buildOpLogic(operation func(uint8, uint8) uint8) opFunc { return func(s *State, line []uint8, opcode opcode) { - value, _, _ := resolve(s, line, opcode) + value := resolveValue(s, line, opcode) result := operation(value, s.Reg.getA()) s.Reg.setA(result) s.Reg.updateFlagZN(result) @@ -223,7 +244,7 @@ func buildOpLogic(operation func(uint8, uint8) uint8) opFunc { } func opADC(s *State, line []uint8, opcode opcode) { - value, _, _ := resolve(s, line, opcode) + value := resolveValue(s, line, opcode) aValue := s.Reg.getA() carry := s.Reg.getFlagBit(flagC) @@ -251,7 +272,7 @@ func opADC(s *State, line []uint8, opcode opcode) { } func opSBC(s *State, line []uint8, opcode opcode) { - value, _, _ := resolve(s, line, opcode) + value := resolveValue(s, line, opcode) aValue := s.Reg.getA() carry := s.Reg.getFlagBit(flagC) @@ -323,7 +344,7 @@ func opPHP(s *State, line []uint8, opcode opcode) { } func opJMP(s *State, line []uint8, opcode opcode) { - _, address, _ := resolve(s, line, opcode) + address := resolveAddress(s, line, opcode) s.Reg.setPC(address) } @@ -331,7 +352,7 @@ func opNOP(s *State, line []uint8, opcode opcode) {} func opJSR(s *State, line []uint8, opcode opcode) { pushWord(s, s.Reg.getPC()-1) - _, address, _ := resolve(s, line, opcode) + address := resolveAddress(s, line, opcode) s.Reg.setPC(address) } @@ -556,9 +577,6 @@ func ExecuteInstruction(s *State, log bool) { } opcode.action(s, line, opcode) if log { - // Warning: this create double accesses and can interfere on memory mapped I/O - //value, address, _ := resolve(s, line, opcode) - //fmt.Printf("%v, [%04x:%02x], [%02x]\n", s.Reg, address, value, line) fmt.Printf("%v, [%02x]\n", s.Reg, line) } } diff --git a/core6502/registers.go b/core6502/registers.go index 494a698..b207578 100644 --- a/core6502/registers.go +++ b/core6502/registers.go @@ -23,56 +23,56 @@ const ( flagC uint8 = 1 << 0 ) -type Registers struct { +type registers struct { data [8]uint8 } -func (r *Registers) getRegister(i int) uint8 { return r.data[i] } +func (r *registers) getRegister(i int) uint8 { return r.data[i] } -func (r *Registers) getA() uint8 { return r.data[regA] } -func (r *Registers) getX() uint8 { return r.data[regX] } -func (r *Registers) getY() uint8 { return r.data[regY] } -func (r *Registers) getP() uint8 { return r.data[regP] } -func (r *Registers) getSP() uint8 { return r.data[regSP] } +func (r *registers) getA() uint8 { return r.data[regA] } +func (r *registers) getX() uint8 { return r.data[regX] } +func (r *registers) getY() uint8 { return r.data[regY] } +func (r *registers) getP() uint8 { return r.data[regP] } +func (r *registers) getSP() uint8 { return r.data[regSP] } -func (r *Registers) setRegister(i int, v uint8) { +func (r *registers) setRegister(i int, v uint8) { r.data[i] = v } -func (r *Registers) setA(v uint8) { r.setRegister(regA, v) } -func (r *Registers) setX(v uint8) { r.setRegister(regX, v) } -func (r *Registers) setY(v uint8) { r.setRegister(regY, v) } -func (r *Registers) setP(v uint8) { r.setRegister(regP, v) } -func (r *Registers) setSP(v uint8) { r.setRegister(regSP, v) } +func (r *registers) setA(v uint8) { r.setRegister(regA, v) } +func (r *registers) setX(v uint8) { r.setRegister(regX, v) } +func (r *registers) setY(v uint8) { r.setRegister(regY, v) } +func (r *registers) setP(v uint8) { r.setRegister(regP, v) } +func (r *registers) setSP(v uint8) { r.setRegister(regSP, v) } -func (r *Registers) getPC() uint16 { +func (r *registers) getPC() uint16 { return uint16(r.data[regPC])*256 + uint16(r.data[regPC+1]) } -func (r *Registers) setPC(v uint16) { +func (r *registers) setPC(v uint16) { r.data[regPC] = uint8(v >> 8) r.data[regPC+1] = uint8(v) } -func (r *Registers) getFlagBit(i uint8) uint8 { +func (r *registers) getFlagBit(i uint8) uint8 { if r.getFlag(i) { return 1 } return 0 } -func (r *Registers) getFlag(i uint8) bool { +func (r *registers) getFlag(i uint8) bool { return (r.data[regP] & i) != 0 } -func (r *Registers) setFlag(i uint8) { +func (r *registers) setFlag(i uint8) { r.data[regP] |= i } -func (r *Registers) clearFlag(i uint8) { +func (r *registers) clearFlag(i uint8) { r.data[regP] &^= i } -func (r *Registers) updateFlag(i uint8, v bool) { +func (r *registers) updateFlag(i uint8, v bool) { if v { r.setFlag(i) } else { @@ -80,12 +80,12 @@ func (r *Registers) updateFlag(i uint8, v bool) { } } -func (r *Registers) updateFlagZN(t uint8) { +func (r *registers) updateFlagZN(t uint8) { r.updateFlag(flagZ, t == 0) r.updateFlag(flagN, t >= (1<<7)) } -func (r Registers) String() string { +func (r registers) String() string { return fmt.Sprintf("A: %#02x, X: %#02x, Y: %#02x, SP: %#02x, PC: %#04x, P: %#02x, (NV-BDIZC): %08b", r.getA(), r.getX(), r.getY(), r.getSP(), r.getPC(), r.getP(), r.getP()) } diff --git a/core6502/registers_test.go b/core6502/registers_test.go index 47e5079..403c1d7 100644 --- a/core6502/registers_test.go +++ b/core6502/registers_test.go @@ -3,7 +3,7 @@ package core6502 import "testing" func TestRegA(t *testing.T) { - var r Registers + var r registers var data uint8 data = 200 r.setA(data) @@ -12,7 +12,7 @@ func TestRegA(t *testing.T) { } } func TestRegPC(t *testing.T) { - var r Registers + var r registers var data uint16 data = 0xc600 r.setPC(data) @@ -22,7 +22,7 @@ func TestRegPC(t *testing.T) { } func TestFlags(t *testing.T) { - var r Registers + var r registers r.setP(0x23) if r.getP() != 0x23 { t.Error("Error storing and loading P") @@ -51,7 +51,7 @@ func TestFlags(t *testing.T) { } func TestUpdateFlagZN(t *testing.T) { - var r Registers + var r registers r.updateFlagZN(0) if r.getP() != flagZ { t.Error("Error update flags ZN with 0") diff --git a/main.go b/main.go index ed3d729..9bc1d61 100644 --- a/main.go +++ b/main.go @@ -3,10 +3,10 @@ package main import "go6502/apple2" func main() { - romFile := "../roms/apple.rom" - //romFile := "../roms/APPLE2.ROM" - - log := true + //romFile := "apple2/romdumps/Apple2.rom" + romFile := "apple2/romdumps/Apple2_Plus.rom" + //romFile := "apple2/romdumps/Apple2e.rom" + log := false apple2.Run(romFile, log) }