diff --git a/src/main/java/com/loomcom/symon/CommandParser.java b/src/main/java/com/loomcom/symon/CommandParser.java deleted file mode 100644 index 5a48efc..0000000 --- a/src/main/java/com/loomcom/symon/CommandParser.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.loomcom.symon; - -import java.io.*; -import com.loomcom.symon.devices.*; -import com.loomcom.symon.exceptions.*; - - -public class CommandParser { - - private BufferedReader in; - private BufferedWriter out; - private Simulator simulator; - - public CommandParser(InputStream i, OutputStream o, Simulator s) { - this.in = new BufferedReader(new InputStreamReader(i)); - this.out = new BufferedWriter(new OutputStreamWriter(o)); - this.simulator = s; - } - - public void run() throws MemoryAccessException { - try { - String command = null; - greeting(); - prompt(); - while (!shouldQuit(command = readLine())) { - dispatch(command); - prompt(); - } - writeLine("\n\nGoodbye!"); - } catch (IOException ex) { - System.err.println("Error: " + ex.toString()); - System.exit(1); - } - } - - public void setSimulator(Simulator sim) { - this.simulator = sim; - } - - public Simulator getSimulator() { - return this.simulator; - } - - /** - * Dispatch the command. - */ - public void dispatch(String command) - throws MemoryAccessException, IOException { - // TODO: Real implementation. This first one is just - // for testing. - if ("test".equals(command)) { - simulator.runTest(); - } else if ("ex".equals(command)) { - writeLine(simulator.getState()); - } else { - writeLine("Huh?"); - } - } - - /******************************************************************* - * Private - *******************************************************************/ - - private void greeting() throws IOException { - writeLine("Welcome to the j6502 Simulator!"); - } - - private void prompt() throws IOException { - out.write("j6502> "); - out.flush(); - } - - private String readLine() throws IOException { - String line = in.readLine(); - if (line == null) { return null; } - return line.trim(); - } - - private void writeLine(String line) throws IOException { - out.write(line); - out.newLine(); - out.flush(); - } - - /** - * Returns true if the line is a quit. - */ - private boolean shouldQuit(String line) { - return (line == null || "q".equals(line.toLowerCase())); - } - -} \ No newline at end of file diff --git a/src/main/java/com/loomcom/symon/Simulator.java b/src/main/java/com/loomcom/symon/Simulator.java index 7bca1db..47a44a8 100644 --- a/src/main/java/com/loomcom/symon/Simulator.java +++ b/src/main/java/com/loomcom/symon/Simulator.java @@ -1,5 +1,15 @@ package com.loomcom.symon; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.List; +import java.util.StringTokenizer; + import com.loomcom.symon.devices.*; import com.loomcom.symon.exceptions.*; @@ -8,11 +18,6 @@ import com.loomcom.symon.exceptions.*; */ public class Simulator { - /** - * Command-line parser used by this simulator. - */ - private CommandParser parser; - /** * The CPU itself. */ @@ -23,35 +28,197 @@ public class Simulator { * correct IO devices. */ private Bus bus; + + private BufferedReader in; + private BufferedWriter out; + + /* If true, trace execution of the CPU */ + private boolean trace = false; public Simulator() throws MemoryRangeException { cpu = new Cpu(); bus = new Bus(0x0000, 0xffff); bus.addCpu(cpu); bus.addDevice(new Memory(0x0000, 0x10000)); - parser = new CommandParser(System.in, System.out, this); - } - - public String getState() throws MemoryAccessException { - return cpu.toString(); + this.in = new BufferedReader(new InputStreamReader(System.in)); + this.out = new BufferedWriter(new OutputStreamWriter(System.out)); } public void run() throws MemoryAccessException { - parser.run(); - } - - public void load(int address, int[] program) - throws MemoryAccessException { - int i = 0; - for (int d : program) { - bus.write(address + i++, d); + try { + greeting(); + prompt(); + String command = null; + while (!shouldQuit(command = readLine())) { + try { + dispatch(command); + } catch (CommandFormatException ex) { + writeLine(ex.getMessage()); + } + prompt(); + } + writeLine("\n\nGoodbye!"); + } catch (IOException ex) { + System.err.println("Error: " + ex.toString()); + System.exit(1); } } - + /** - * A test method. + * Dispatch the command. */ - public void runTest() throws MemoryAccessException { + public void dispatch(String commandLine) + throws MemoryAccessException, IOException, CommandFormatException { + Command c = new Command(commandLine); + String cmd = c.getCommand(); + if (cmd != null) { + if ("test".equals(cmd)) { + doTest(); + } else if (cmd.startsWith("s")) { + doGetState(); + } else if (cmd.startsWith("r")) { + doReset(); + } else if (cmd.startsWith("e")) { + doExamine(c); + } else if (cmd.startsWith("d")) { + doDeposit(c); + } else if (cmd.startsWith("g")) { + doGo(c); + } else if (cmd.startsWith("h")) { + doHelp(c); + } else if (cmd.startsWith("t")) { + doToggleTrace(); + } else if (cmd.startsWith("f")) { + doFill(c); + } else { + writeLine("? Type h for help"); + } + } + } + + public void doHelp(Command c) throws IOException { + writeLine("Symon 6502 Simulator"); + writeLine(""); + writeLine("All addresses must be in hexadecimal. Commands may be short or"); + writeLine("long (e.g. 'e' or 'ex' or 'examine'). Note that 'go' clears the"); + writeLine("Break processor status flag"); + writeLine(""); + writeLine("g [address [steps]] Start running at address."); + writeLine("e [start [end]] Examine memory."); + writeLine("d [address] [data] Deposit data into address."); + writeLine("f [start] [end] [data] Fill memory with data."); + writeLine("r Reset simulator."); + writeLine("s Show CPU state."); + writeLine("t Toggle trace."); + writeLine("q (or Control-D) Quit."); + } + + public void doGetState() throws IOException, MemoryAccessException { + writeLine(cpu.toString()); + writeLine("Trace is " + (trace ? "on" : "off")); + } + + public void doExamine(Command c) throws IOException, MemoryAccessException, CommandFormatException { + try { + if (c.numArgs() == 2) { + int startAddress = stringToWord(c.getArgs()[0]); + int endAddress = stringToWord(c.getArgs()[1]); + while (startAddress < endAddress) { + StringBuffer line = new StringBuffer(); + int numBytes = 0; + line.append(String.format("%04x ", startAddress)); + while (numBytes++ < 8 && startAddress <= endAddress) { + line.append(String.format("%02x ", bus.read(startAddress++))); + } + writeLine(line.toString()); + } + } else if (c.numArgs() == 1) { + int address = stringToWord(c.getArgs()[0]); + writeLine(String.format("%04x %02x", address, bus.read(address))); + } else { + throw new CommandFormatException("e [start [end]]"); + } + } catch (NumberFormatException ex) { + throw new CommandFormatException("Address not understood"); + } + } + + public void doDeposit(Command c) throws MemoryAccessException, CommandFormatException { + if (c.numArgs() != 2) { + throw new CommandFormatException("d [address] [data]"); + } + try { + int address = stringToWord(c.getArg(0)); + int data = stringToByte(c.getArg(1)); + bus.write(address, data); + } catch (NumberFormatException ex) { + throw new CommandFormatException("Address not understood"); + } + } + + public void doFill(Command c) throws MemoryAccessException, CommandFormatException { + if (c.numArgs() != 3) { + throw new CommandFormatException("f [start] [end] [data]"); + } + try { + int start = stringToWord(c.getArg(0)); + int end = stringToWord(c.getArg(1)); + int data = stringToByte(c.getArg(2)); + while (start < end) { + bus.write(start, data); + start++; + } + } catch (NumberFormatException ex) { + throw new CommandFormatException("Address not understood"); + } + } + + public void doGo(Command c) throws IOException, MemoryAccessException, CommandFormatException { + if (c.numArgs() != 1 && c.numArgs() != 2) { + throw new CommandFormatException("g [address [steps]]"); + } + try { + int start = stringToWord(c.getArg(0)); + int steps = -1; + if (c.numArgs() == 2) { + steps = stringToWord(c.getArg(1)); + } + + // Make a gross assumption: Restarting the CPU clears + // the break flag and the IRQ disable flag. + cpu.clearBreakFlag(); + cpu.clearIrqDisableFlag(); + + cpu.setProgramCounter(start); + while (!cpu.getBreakFlag() && (steps == -1 || steps-- > 0)) { + cpu.step(); + if (trace) { + writeLine(cpu.toString()); + } + } + if (!trace) { + writeLine(cpu.toString()); + } + } catch (NumberFormatException ex) { + throw new CommandFormatException("Address not understood"); + } + } + + public void doToggleTrace() throws IOException { + this.trace = !trace; + writeLine("Trace is now " + (trace ? "on" : "off")); + } + + public void doReset() throws MemoryAccessException { + cpu.reset(); + this.trace = false; + } + + /** + * Run a very simple test program that doesn't do + * much other than exercise the accumulator a bit. + */ + public void doTest() throws MemoryAccessException { int[] zpData = { 0x39, // $0000 0x21, // $0001 @@ -100,5 +267,99 @@ public class Simulator { System.err.println("Error: " + ex.toString()); } } + + /******************************************************************* + * Private + *******************************************************************/ + public void load(int address, int[] data) + throws MemoryAccessException { + int i = 0; + for (int d : data) { + bus.write(address + i++, d); + } + } + + private int stringToWord(String addrString) { + return Integer.parseInt(addrString, 16) & 0xffff; + } + + private int stringToByte(String dataString) { + return Integer.parseInt(dataString, 16) & 0xff; + } + + private void greeting() throws IOException { + writeLine("Welcome to the Symon Simulator!"); + } + + private void prompt() throws IOException { + out.write("symon> "); + out.flush(); + } + + private String readLine() throws IOException { + String line = in.readLine(); + if (line == null) { return null; } + return line.trim(); + } + + private void writeLine(String line) throws IOException { + out.write(line); + out.newLine(); + out.flush(); + } + + /** + * Returns true if the line is a quit. + */ + private boolean shouldQuit(String line) { + return (line == null || "q".equals(line.toLowerCase())); + } + + /** + * Command line tokenizer class. Given a command line, tokenize + * it and give easy access to the command and its arguments. + */ + public static final class Command { + private String command; + private String[] args; + + public Command(String commandLine) { + StringTokenizer st = new StringTokenizer(commandLine); + int numTokens = st.countTokens(); + int idx = 0; + args = new String[numTokens > 1 ? numTokens - 1 : 0]; + while (st.hasMoreTokens()) { + if (command == null) { + command = st.nextToken(); + } else { + args[idx++] = st.nextToken(); + } + } + } + + public String getCommand() { + return command; + } + + public String[] getArgs() { + return args; + } + + public String getArg(int argNum) { + if (argNum > args.length - 1) { + return null; + } else { + return args[argNum]; + } + } + + public int numArgs() { + return args.length; + } + + public boolean hasArgs() { + return args.length > 0; + } + } } diff --git a/src/main/java/com/loomcom/symon/devices/Memory.java b/src/main/java/com/loomcom/symon/devices/Memory.java index f31480f..ef377f3 100644 --- a/src/main/java/com/loomcom/symon/devices/Memory.java +++ b/src/main/java/com/loomcom/symon/devices/Memory.java @@ -14,8 +14,8 @@ public class Memory extends Device { super(address, size, "RW Memory"); this.readOnly = readOnly; this.mem = new int[size]; - // Initialize all locations to 0xff - Arrays.fill(this.mem, 0xff); + // Initialize all locations to 0x00 (BRK) + Arrays.fill(this.mem, 0x00); } public Memory(int address, int size) diff --git a/src/main/java/com/loomcom/symon/exceptions/CommandFormatException.java b/src/main/java/com/loomcom/symon/exceptions/CommandFormatException.java new file mode 100644 index 0000000..2208a6b --- /dev/null +++ b/src/main/java/com/loomcom/symon/exceptions/CommandFormatException.java @@ -0,0 +1,10 @@ +package com.loomcom.symon.exceptions; + +public class CommandFormatException extends Exception { + public CommandFormatException(String msg) { + super(msg); + } + public CommandFormatException() { + super(); + } +} diff --git a/src/test/java/com/loomcom/symon/CommandTest.java b/src/test/java/com/loomcom/symon/CommandTest.java new file mode 100644 index 0000000..e7b21b6 --- /dev/null +++ b/src/test/java/com/loomcom/symon/CommandTest.java @@ -0,0 +1,64 @@ +package com.loomcom.symon; + +import org.junit.*; +import static org.junit.Assert.*; + +public class CommandTest { + + @Test + public void testCommandShouldParseCorrectNumberOfArguments() { + Simulator.Command c; + + c = new Simulator.Command("foo"); + assertEquals("foo", c.getCommand()); + assertEquals(0, c.numArgs()); + + c = new Simulator.Command("foo bar"); + assertEquals("foo", c.getCommand()); + assertEquals(1, c.numArgs()); + assertEquals("bar", c.getArgs()[0]); + + c = new Simulator.Command("foo bar baz quux 0 100"); + assertEquals("foo", c.getCommand()); + assertEquals(5, c.numArgs()); + assertEquals("bar", c.getArgs()[0]); + assertEquals("baz", c.getArgs()[1]); + assertEquals("quux", c.getArgs()[2]); + assertEquals("0", c.getArgs()[3]); + assertEquals("100", c.getArgs()[4]); + } + + @Test + public void testCommandShouldIgnoreWhitespaceBetweenTokens() { + Simulator.Command c; + + c = new Simulator.Command("foo bar baz"); + assertEquals("foo", c.getCommand()); + assertEquals(2, c.numArgs()); + assertEquals("bar", c.getArgs()[0]); + assertEquals("baz", c.getArgs()[1]); + } + + @Test + public void testCommandShouldIgnoreWhitespaceBeforeCommand() { + Simulator.Command c; + + c = new Simulator.Command(" foo bar baz"); + assertEquals("foo", c.getCommand()); + assertEquals(2, c.numArgs()); + assertEquals("bar", c.getArgs()[0]); + assertEquals("baz", c.getArgs()[1]); + } + + @Test + public void testCommandShouldIgnoreWhitespaceAfterCommand() { + Simulator.Command c; + + c = new Simulator.Command("foo bar baz "); + assertEquals("foo", c.getCommand()); + assertEquals(2, c.numArgs()); + assertEquals("bar", c.getArgs()[0]); + assertEquals("baz", c.getArgs()[1]); + } + +} diff --git a/src/test/java/com/loomcom/symon/CpuAbsoluteXModeTest.java b/src/test/java/com/loomcom/symon/CpuAbsoluteXModeTest.java index c2b4a02..1546b4a 100644 --- a/src/test/java/com/loomcom/symon/CpuAbsoluteXModeTest.java +++ b/src/test/java/com/loomcom/symon/CpuAbsoluteXModeTest.java @@ -170,7 +170,7 @@ public class CpuAbsoluteXModeTest extends TestCase { 0xa9, 0xff, // LDA #$ff 0x3d, 0x05, 0x1a, // AND $1a05,X 0xa9, 0x01, // LDA #$01 - 0x3d, 0xd2, 0x1a); // AND $1ad2,X + 0x3d, 0xd2, 0x19); // AND $19d2,X cpu.step(); assertEquals(0x00, cpu.getAccumulator()); assertTrue(cpu.getZeroFlag()); diff --git a/src/test/java/com/loomcom/symon/CpuAbsoluteYModeTest.java b/src/test/java/com/loomcom/symon/CpuAbsoluteYModeTest.java index ef2ffdd..da1b9ec 100644 --- a/src/test/java/com/loomcom/symon/CpuAbsoluteYModeTest.java +++ b/src/test/java/com/loomcom/symon/CpuAbsoluteYModeTest.java @@ -115,7 +115,7 @@ public class CpuAbsoluteYModeTest extends TestCase { 0xa9, 0xff, // LDA #$ff 0x39, 0x05, 0x1a, // AND $1a05,Y 0xa9, 0x01, // LDA #$01 - 0x39, 0xd2, 0x1a); // AND $1ad2,Y + 0x39, 0xd2, 0x19); // AND $19d2,Y cpu.step(); assertEquals(0x00, cpu.getAccumulator()); assertTrue(cpu.getZeroFlag()); diff --git a/src/test/java/com/loomcom/symon/CpuImpliedModeTest.java b/src/test/java/com/loomcom/symon/CpuImpliedModeTest.java index 8df6fcd..1489bfa 100644 --- a/src/test/java/com/loomcom/symon/CpuImpliedModeTest.java +++ b/src/test/java/com/loomcom/symon/CpuImpliedModeTest.java @@ -74,7 +74,7 @@ public class CpuImpliedModeTest extends TestCase { cpu.setOverflowFlag(); assertEquals(0x20|Cpu.P_CARRY|Cpu.P_OVERFLOW, cpu.getProcessorStatus()); - assertEquals(0xff, cpu.stackPeek()); + assertEquals(0x00, cpu.stackPeek()); assertFalse(cpu.getBreakFlag()); assertEquals(0x0200, cpu.getProgramCounter()); assertEquals(0xff, cpu.getStackPointer()); diff --git a/src/test/java/com/loomcom/symon/CpuIndexedIndirectModeTest.java b/src/test/java/com/loomcom/symon/CpuIndexedIndirectModeTest.java index 72eaa76..5885ec0 100644 --- a/src/test/java/com/loomcom/symon/CpuIndexedIndirectModeTest.java +++ b/src/test/java/com/loomcom/symon/CpuIndexedIndirectModeTest.java @@ -1,7 +1,6 @@ package com.loomcom.symon; import com.loomcom.symon.devices.Memory; -import com.loomcom.symon.exceptions.MemoryAccessException; import org.junit.*; import static org.junit.Assert.*; diff --git a/src/test/java/com/loomcom/symon/CpuIndirectXModeTest.java b/src/test/java/com/loomcom/symon/CpuIndirectXModeTest.java index 45d88cf..d9bf3c9 100644 --- a/src/test/java/com/loomcom/symon/CpuIndirectXModeTest.java +++ b/src/test/java/com/loomcom/symon/CpuIndirectXModeTest.java @@ -115,7 +115,7 @@ public class CpuIndirectXModeTest extends TestCase { 0xa9, 0xff, // LDA #$ff 0x3d, 0x05, 0x1a, // AND $1a05,X 0xa9, 0x01, // LDA #$01 - 0x3d, 0xd2, 0x1a); // AND $1ad2,X + 0x3d, 0xd2, 0x19); // AND $19d2,X cpu.step(); assertEquals(0x00, cpu.getAccumulator()); assertTrue(cpu.getZeroFlag()); diff --git a/src/test/java/com/loomcom/symon/CpuTest.java b/src/test/java/com/loomcom/symon/CpuTest.java index fe41322..ea86bf8 100644 --- a/src/test/java/com/loomcom/symon/CpuTest.java +++ b/src/test/java/com/loomcom/symon/CpuTest.java @@ -70,7 +70,7 @@ public class CpuTest extends TestCase { public void testStackPush() throws MemoryAccessException { assertEquals(0xff, cpu.getStackPointer()); - assertEquals(0xff, bus.read(0x1ff)); + assertEquals(0x00, bus.read(0x1ff)); cpu.stackPush(0x06); assertEquals(0xfe, cpu.getStackPointer()); @@ -179,7 +179,7 @@ public class CpuTest extends TestCase { } public void testStackPeekDoesNotAlterStackPointer() throws MemoryAccessException { - assertEquals(0xff, cpu.stackPeek()); + assertEquals(0x00, cpu.stackPeek()); assertEquals(0xff, cpu.getStackPointer()); cpu.stackPush(0x01);