diff --git a/README.md b/README.md index 785fe0d..62345c9 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,12 @@ Properties of this simulator: - passes several extensive unit test suites that verify instruction and cpu flags behavior - maximum simulated performance is a 6502 running at ~100 Mhz (on my machine) +## Virtual machine examples + +Two virtual example machines are included. +The default one starts with ``gradle run`` or run the ``ksim64vm`` command. +There's another one ``ehBasicMain`` that is configured to run the "enhanced 6502 basic" ROM. + ## Documentation Still to be written. For now, use the source ;-) diff --git a/build.gradle.kts b/build.gradle.kts index 2e26001..efd46ea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,7 +42,7 @@ dependencies { application { applicationName = "ksim65vm" - mainClassName = "razorvine.examplemachine.SystemMainKt" + mainClassName = "razorvine.examplemachine.MachineMainKt" } tasks.named("test") { diff --git a/src/main/kotlin/razorvine/examplemachine/GUI.kt b/src/main/kotlin/razorvine/examplemachine/GUI.kt index bab3701..e4a070a 100644 --- a/src/main/kotlin/razorvine/examplemachine/GUI.kt +++ b/src/main/kotlin/razorvine/examplemachine/GUI.kt @@ -1,12 +1,12 @@ package razorvine.examplemachine +import razorvine.ksim65.Bus import razorvine.ksim65.Cpu6502 import java.awt.* import java.awt.image.BufferedImage import javax.imageio.ImageIO import javax.swing.event.MouseInputListener import razorvine.ksim65.IHostInterface -import razorvine.ksim65.components.MemoryComponent import java.awt.event.* import java.util.* import javax.swing.* @@ -22,7 +22,7 @@ object ScreenDefs { const val SCREEN_HEIGHT_CHARS = 30 const val SCREEN_WIDTH = SCREEN_WIDTH_CHARS * 8 const val SCREEN_HEIGHT = SCREEN_HEIGHT_CHARS * 16 - const val DISPLAY_PIXEL_SCALING: Double = 1.5 + const val DISPLAY_PIXEL_SCALING: Double = 1.25 val BG_COLOR = Color(0, 10, 20) val FG_COLOR = Color(200, 255, 230) val BORDER_COLOR = Color(20, 30, 40) @@ -159,7 +159,6 @@ class DebugWindow(val vm: VirtualMachine) : JFrame("debugger"), ActionListener { private val pauseBt = JButton("Pause").also { it.actionCommand = "pause" } init { - isFocusable = true defaultCloseOperation = EXIT_ON_CLOSE preferredSize = Dimension(350, 600) val cpuPanel = JPanel(GridBagLayout()) @@ -222,11 +221,11 @@ class DebugWindow(val vm: VirtualMachine) : JFrame("debugger"), ActionListener { when(e.actionCommand) { "reset" -> { vm.bus.reset() - updateCpu(vm.cpu, vm.ram) + updateCpu(vm.cpu, vm.bus) } "step" -> { vm.stepInstruction() - updateCpu(vm.cpu, vm.ram) + updateCpu(vm.cpu, vm.bus) } "pause" -> { vm.paused = true @@ -246,7 +245,7 @@ class DebugWindow(val vm: VirtualMachine) : JFrame("debugger"), ActionListener { } } - fun updateCpu(cpu: Cpu6502, mem: MemoryComponent) { + fun updateCpu(cpu: Cpu6502, bus: Bus) { cyclesTf.text = cpu.totalCycles.toString() regAtf.text = cpu.hexB(cpu.regA) regXtf.text = cpu.hexB(cpu.regX) @@ -254,7 +253,8 @@ class DebugWindow(val vm: VirtualMachine) : JFrame("debugger"), ActionListener { regPtf.text = "NV-BDIZC\n" + cpu.regP.asByte().toString(2).padStart(8, '0') regPCtf.text = cpu.hexW(cpu.regPC) regSPtf.text = cpu.hexB(cpu.regSP) - disassemTf.text = cpu.disassembleOneInstruction(mem.data, cpu.regPC, mem.startAddress).first.substringAfter(' ').trim() + val memory = bus.memoryComponentFor(cpu.regPC) + disassemTf.text = cpu.disassembleOneInstruction(memory.data, cpu.regPC, memory.startAddress).first.substringAfter(' ').trim() } } @@ -327,6 +327,8 @@ class MainWindow(title: String) : JFrame(title), KeyListener, MouseInputListener setLocationRelativeTo(null) setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, mutableSetOf()) isVisible = true + toFront() + requestFocus() } fun start() { diff --git a/src/main/kotlin/razorvine/examplemachine/ehBasicMain.kt b/src/main/kotlin/razorvine/examplemachine/ehBasicMain.kt new file mode 100644 index 0000000..67c58a2 --- /dev/null +++ b/src/main/kotlin/razorvine/examplemachine/ehBasicMain.kt @@ -0,0 +1,63 @@ +package razorvine.examplemachine + +import kotlin.concurrent.scheduleAtFixedRate +import razorvine.ksim65.Bus +import razorvine.ksim65.Cpu6502 +import razorvine.ksim65.Version +import razorvine.ksim65.components.* +import javax.swing.ImageIcon + +/** + * A virtual computer constructed from the various virtual components, + * running the 6502 Enhanced Basic ROM. + */ +class EhBasicMachine(title: String) { + val bus = Bus() + val cpu = Cpu6502(false) + val ram = Ram(0x0000, 0xbfff) + val rom = Rom(0xc000, 0xffff).also { it.load(javaClass.getResourceAsStream("/ehbasic_C000.bin").readAllBytes()) } + + private val hostDisplay = MainWindow(title) + private val display = Display(0xd000, 0xd00a, hostDisplay, + ScreenDefs.SCREEN_WIDTH_CHARS, ScreenDefs.SCREEN_HEIGHT_CHARS, + ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT) + private val keyboard = Keyboard(0xd400, 0xd400, hostDisplay) + + init { + hostDisplay.iconImage = ImageIcon(javaClass.getResource("/icon.png")).image + + bus += display + bus += keyboard + bus += rom + bus += ram + bus += cpu + bus.reset() + + hostDisplay.start() + hostDisplay.requestFocus() + } + + var paused = false + + fun stepInstruction() { + while (cpu.instrCycles > 0) bus.clock() + bus.clock() + while (cpu.instrCycles > 0) bus.clock() + } + + fun start() { + val timer = java.util.Timer("clock", true) + timer.scheduleAtFixedRate(1, 1) { + if(!paused) { + repeat(500) { + stepInstruction() + } + } + } + } +} + +fun main(args: Array) { + val machine = EhBasicMachine("KSim65 demo virtual machine - using ksim65 v${Version.version}") + machine.start() +} diff --git a/src/main/kotlin/razorvine/examplemachine/systemMain.kt b/src/main/kotlin/razorvine/examplemachine/machineMain.kt similarity index 95% rename from src/main/kotlin/razorvine/examplemachine/systemMain.kt rename to src/main/kotlin/razorvine/examplemachine/machineMain.kt index 4848196..2aa7ca8 100644 --- a/src/main/kotlin/razorvine/examplemachine/systemMain.kt +++ b/src/main/kotlin/razorvine/examplemachine/machineMain.kt @@ -18,8 +18,8 @@ class VirtualMachine(title: String) { private val rtc = RealTimeClock(0xd100, 0xd108) private val timer = Timer(0xd200, 0xd203, cpu) - private val hostDisplay = MainWindow(title) private val debugWindow = DebugWindow(this) + private val hostDisplay = MainWindow(title) private val display = Display(0xd000, 0xd00a, hostDisplay, ScreenDefs.SCREEN_WIDTH_CHARS, ScreenDefs.SCREEN_HEIGHT_CHARS, ScreenDefs.SCREEN_WIDTH, ScreenDefs.SCREEN_HEIGHT) @@ -47,6 +47,7 @@ class VirtualMachine(title: String) { debugWindow.setLocation(hostDisplay.location.x+hostDisplay.width, hostDisplay.location.y) debugWindow.isVisible = true + hostDisplay.requestFocus() } var paused = false @@ -62,10 +63,10 @@ class VirtualMachine(title: String) { val startTime = System.currentTimeMillis() timer.scheduleAtFixedRate(1, 1) { if(!paused) { - repeat(20) { + repeat(50) { stepInstruction() } - debugWindow.updateCpu(cpu, ram) + debugWindow.updateCpu(cpu, bus) val duration = System.currentTimeMillis() - startTime val speedKhz = cpu.totalCycles.toDouble() / duration debugWindow.speedKhzTf.text = "%.1f".format(speedKhz) diff --git a/src/main/kotlin/razorvine/ksim65/Bus.kt b/src/main/kotlin/razorvine/ksim65/Bus.kt index a6d9d9f..5f2a994 100644 --- a/src/main/kotlin/razorvine/ksim65/Bus.kt +++ b/src/main/kotlin/razorvine/ksim65/Bus.kt @@ -1,9 +1,6 @@ package razorvine.ksim65 -import razorvine.ksim65.components.Address -import razorvine.ksim65.components.BusComponent -import razorvine.ksim65.components.MemMappedComponent -import razorvine.ksim65.components.UByte +import razorvine.ksim65.components.* /** * The system bus that connects all other components together. @@ -73,4 +70,13 @@ class Bus { it[address] = data } } + + fun memoryComponentFor(address: Address): MemoryComponent { + memComponents.forEach { + if (it is MemoryComponent && address >= it.startAddress && address <= it.endAddress) { + return it + } + } + throw NoSuchElementException() + } } diff --git a/src/main/kotlin/razorvine/ksim65/Cpu6502.kt b/src/main/kotlin/razorvine/ksim65/Cpu6502.kt index 643f807..2e007e2 100644 --- a/src/main/kotlin/razorvine/ksim65/Cpu6502.kt +++ b/src/main/kotlin/razorvine/ksim65/Cpu6502.kt @@ -151,116 +151,130 @@ open class Cpu6502(private val stopOnBrk: Boolean = false) : BusComponent() { disassemble(memory.data, memory.startAddress, from, to) fun disassemble(memory: Array, baseAddress: Address, from: Address, to: Address): List { - var location = from - baseAddress + var location = from val result = mutableListOf() - while (location <= (to - baseAddress)) { + while (location <= to) { val dis = disassembleOneInstruction(memory, location, baseAddress) result.add(dis.first) - location = dis.second + location += dis.second } return result } - fun disassembleOneInstruction(memory: Array, address: Address, baseAddress: Address): Pair { + fun disassembleOneInstruction(memory: Array, address: Address, baseAddress: Address): Pair { val spacing1 = " " val spacing2 = " " val spacing3 = " " - var location = address + val location = address-baseAddress val byte = memory[location] var line = "\$${hexW(location+baseAddress)} ${hexB(byte)} " - location++ val opcode = instructions[byte.toInt()] - when (opcode.mode) { + return when (opcode.mode) { AddrMode.Acc -> { line += "$spacing1 ${opcode.mnemonic} a" + Pair(line, 1) } AddrMode.Imp -> { line += "$spacing1 ${opcode.mnemonic}" + Pair(line, 1) } AddrMode.Imm -> { - val value = memory[location++] + val value = memory[location+1] line += "${hexB(value)} $spacing2 ${opcode.mnemonic} #\$${hexB(value)}" + Pair(line, 2) } AddrMode.Zp -> { - val zpAddr = memory[location++] + val zpAddr = memory[location+1] line += "${hexB(zpAddr)} $spacing2 ${opcode.mnemonic} \$${hexB(zpAddr)}" + Pair(line, 2) } AddrMode.Zpr -> { // addressing mode used by the 65C02, put here for convenience - val zpAddr = memory[location++] - val rel = memory[location++] + val zpAddr = memory[location+1] + val rel = memory[location+2] val target = if (rel <= 0x7f) - location + rel + baseAddress + location + 3 + rel + baseAddress else - location - (256 - rel) + baseAddress + location + 3 - (256 - rel) + baseAddress line += "${hexB(zpAddr)} ${hexB(rel)} $spacing3 ${opcode.mnemonic} \$${hexB(zpAddr)}, \$${hexW(target, true)}" + Pair(line, 3) } AddrMode.Izp -> { // addressing mode used by the 65C02, put here for convenience - val zpAddr = memory[location++] + val zpAddr = memory[location+1] line += "${hexB(zpAddr)} $spacing2 ${opcode.mnemonic} \$(${hexB(zpAddr)})" + Pair(line, 2) } AddrMode.IaX -> { // addressing mode used by the 65C02, put here for convenience - val lo = memory[location++] - val hi = memory[location++] + val lo = memory[location+1] + val hi = memory[location+2] val absAddr = lo.toInt() or (hi.toInt() shl 8) line += "${hexB(lo)} ${hexB(hi)} $spacing3 ${opcode.mnemonic} \$(${hexW(absAddr)},x)" + Pair(line, 3) } AddrMode.ZpX -> { - val zpAddr = memory[location++] + val zpAddr = memory[location+1] line += "${hexB(zpAddr)} $spacing2 ${opcode.mnemonic} \$${hexB(zpAddr)},x" + Pair(line, 2) } AddrMode.ZpY -> { - val zpAddr = memory[location++] + val zpAddr = memory[location+1] line += "${hexB(zpAddr)} $spacing2 ${opcode.mnemonic} \$${hexB(zpAddr)},y" + Pair(line, 2) } AddrMode.Rel -> { - val rel = memory[location++] + val rel = memory[location+1] val target = if (rel <= 0x7f) - location + rel + baseAddress + location + 2 + rel + baseAddress else - location - (256 - rel) + baseAddress + location + 2 - (256 - rel) + baseAddress line += "${hexB(rel)} $spacing2 ${opcode.mnemonic} \$${hexW(target, true)}" + Pair(line, 2) } AddrMode.Abs -> { - val lo = memory[location++] - val hi = memory[location++] + val lo = memory[location+1] + val hi = memory[location+2] val absAddr = lo.toInt() or (hi.toInt() shl 8) line += "${hexB(lo)} ${hexB(hi)} $spacing3 ${opcode.mnemonic} \$${hexW(absAddr)}" + Pair(line, 3) } AddrMode.AbsX -> { - val lo = memory[location++] - val hi = memory[location++] + val lo = memory[location+1] + val hi = memory[location+2] val absAddr = lo.toInt() or (hi.toInt() shl 8) line += "${hexB(lo)} ${hexB(hi)} $spacing3 ${opcode.mnemonic} \$${hexW(absAddr)},x" + Pair(line, 3) } AddrMode.AbsY -> { - val lo = memory[location++] - val hi = memory[location++] + val lo = memory[location+1] + val hi = memory[location+2] val absAddr = lo.toInt() or (hi.toInt() shl 8) line += "${hexB(lo)} ${hexB(hi)} $spacing3 ${opcode.mnemonic} \$${hexW(absAddr)},y" + Pair(line, 3) } AddrMode.Ind -> { - val lo = memory[location++] - val hi = memory[location++] + val lo = memory[location+1] + val hi = memory[location+2] val indirectAddr = lo.toInt() or (hi.toInt() shl 8) line += "${hexB(lo)} ${hexB(hi)} $spacing3 ${opcode.mnemonic} (\$${hexW(indirectAddr)})" + Pair(line, 3) } AddrMode.IzX -> { - val zpAddr = memory[location++] + val zpAddr = memory[location+1] line += "${hexB(zpAddr)} $spacing2 ${opcode.mnemonic} (\$${hexB(zpAddr)},x)" + Pair(line, 2) } AddrMode.IzY -> { - val zpAddr = memory[location++] + val zpAddr = memory[location+1] line += "${hexB(zpAddr)} $spacing2 ${opcode.mnemonic} (\$${hexB(zpAddr)}),y" + Pair(line, 2) } } - return Pair(line, location) } /** diff --git a/src/main/kotlin/razorvine/ksim65/components/Ram.kt b/src/main/kotlin/razorvine/ksim65/components/Ram.kt index 5673cba..d8cf808 100644 --- a/src/main/kotlin/razorvine/ksim65/components/Ram.kt +++ b/src/main/kotlin/razorvine/ksim65/components/Ram.kt @@ -53,11 +53,6 @@ class Ram(startAddress: Address, endAddress: Address) : MemoryComponent(startAdd load(bytes, address) } - fun load(source: URL, address: Address) { - val bytes = source.readBytes() - load(bytes, address) - } - fun load(data: Array, address: Address) = data.forEachIndexed { index, byte -> val baseAddress = address - startAddress diff --git a/src/main/kotlin/razorvine/ksim65/components/Rom.kt b/src/main/kotlin/razorvine/ksim65/components/Rom.kt index 3e15d90..1afb7d1 100644 --- a/src/main/kotlin/razorvine/ksim65/components/Rom.kt +++ b/src/main/kotlin/razorvine/ksim65/components/Rom.kt @@ -1,11 +1,13 @@ package razorvine.ksim65.components +import java.io.File + /** * A ROM chip (read-only memory). */ class Rom(startAddress: Address, endAddress: Address, initialData: Array? = null) : MemoryComponent(startAddress, endAddress) { override val data: Array = - initialData?.copyOf() ?: Array(endAddress - startAddress - 1) { 0 } + initialData?.copyOf() ?: Array(endAddress - startAddress + 1) { 0 } init { require(endAddress - startAddress + 1 == data.size) { "rom address range doesn't match size of data bytes" } @@ -15,4 +17,26 @@ class Rom(startAddress: Address, endAddress: Address, initialData: Array? override operator fun set(address: Address, data: UByte) { /* read-only */ } override fun clock() {} override fun reset() {} + + /** + * load a binary program at the given address + */ + fun load(filename: String) { + val bytes = File(filename).readBytes() + load(bytes) + } + + fun load(data: Array) = + data.forEachIndexed { index, byte -> + this.data[index] = byte + } + + fun load(data: ByteArray) = + data.forEachIndexed { index, byte -> + this.data[index] = + if (byte >= 0) + byte.toShort() + else + (256 + byte).toShort() + } } diff --git a/src/main/resources/ehbasic_C000.bin b/src/main/resources/ehbasic_C000.bin new file mode 100644 index 0000000..b6b01b0 Binary files /dev/null and b/src/main/resources/ehbasic_C000.bin differ diff --git a/src/test/kotlin/Test6502CpuBasics.kt b/src/test/kotlin/Test6502CpuBasics.kt index 53cacc7..cffb3f5 100644 --- a/src/test/kotlin/Test6502CpuBasics.kt +++ b/src/test/kotlin/Test6502CpuBasics.kt @@ -116,7 +116,7 @@ class Test6502CpuBasics { val ram = Ram(0, 0xffff) ram[Cpu6502.RESET_vector] = 0x00 ram[Cpu6502.RESET_vector +1] = 0x10 - val bytes = javaClass.getResource("bcdtest6502.bin")!! // only works on 6502, not on the 65c02 + val bytes = javaClass.getResource("bcdtest6502.bin").readBytes() // only works on 6502, not on the 65c02 ram.load(bytes, 0x1000) bus.add(ram) bus.reset() diff --git a/src/test/kotlin/TestDisassembler.kt b/src/test/kotlin/TestDisassembler.kt index f97bb9b..7db0d7e 100644 --- a/src/test/kotlin/TestDisassembler.kt +++ b/src/test/kotlin/TestDisassembler.kt @@ -29,7 +29,7 @@ class TestDisassembler { fun testDisassembleRockwell65C02() { val cpu = Cpu65C02() val memory = Ram(0, 0x0fff) - val source = javaClass.classLoader.getResource("disassem_r65c02.bin")!! + val source = javaClass.classLoader.getResource("disassem_r65c02.bin").readBytes() memory.load(source, 0x0200) val resultLines = cpu.disassemble(memory, 0x0200, 0x0250) val result = resultLines.joinToString("\n") @@ -74,7 +74,7 @@ ${'$'}0250 00 brk""", result) fun testDisassembleWDC65C02() { val cpu = Cpu65C02() val memory = Ram(0, 0x0fff) - val source = javaClass.classLoader.getResource("disassem_wdc65c02.bin")!! + val source = javaClass.classLoader.getResource("disassem_wdc65c02.bin").readBytes() memory.load(source, 0x200) val resultLines = cpu.disassemble(memory, 0x0200, 0x0215) val result = resultLines.joinToString("\n")