From 00ab8cd9ffa1c982036720cbbe61730e1053588d Mon Sep 17 00:00:00 2001 From: "Seth J. Morabito" Date: Wed, 20 Jan 2010 18:19:39 -0800 Subject: [PATCH] Simulator is just about ready for real-world testing now. Added a simulated MOS6551 ACIA at address $C000 which does buffered input and output via the console. Updated the README with a bit more documentation, and bumped the version number to 0.1 because I'm impatient. --- README | 221 +++++++++++++++++- pom.xml | 2 +- src/main/java/com/loomcom/symon/Cpu.java | 113 +++++---- .../java/com/loomcom/symon/Simulator.java | 192 ++++++++++----- .../java/com/loomcom/symon/devices/Acia.java | 124 ++++++++++ .../com/loomcom/symon/devices/Memory.java | 13 +- .../exceptions/FifoUnderrunException.java | 7 + .../loomcom/symon/util/FifoRingBuffer.java | 78 +++++++ src/test/java/com/loomcom/symon/AciaTest.java | 122 ++++++++++ .../symon/CpuIndirectIndexedModeTest.java | 1 - 10 files changed, 749 insertions(+), 124 deletions(-) create mode 100644 src/main/java/com/loomcom/symon/devices/Acia.java create mode 100644 src/main/java/com/loomcom/symon/exceptions/FifoUnderrunException.java create mode 100644 src/main/java/com/loomcom/symon/util/FifoRingBuffer.java create mode 100644 src/test/java/com/loomcom/symon/AciaTest.java diff --git a/README b/README index 3b0f058..6d7348c 100644 --- a/README +++ b/README @@ -10,8 +10,8 @@ deemed ready for testing, it will be given a version number of "0.1". ==================================================================== -Version: PRERELEASE -Last Updated: 10 January, 2010 +Version: 0.1 +Last Updated: 20 January, 2010 Copyright (c) 2008-2010 Seth J. Morabito See the file COPYING for license. @@ -29,26 +29,223 @@ The initial goal is to simulate a system with an NMOS 6502 or CMOS 65C02 central processor; one or more 6522 VIAs; and one or more 6551 ACIAs. More functionality may be considered as time goes on. -2.0 Usage + +2.0 Requirements +---------------- + + - Java 1.5 or higher + - Maven 2.0.x or higher (for building from source) + - JUnit 4 or higher (for testing) + + +3.0 Usage --------- - 2.1 Requirements +To build Symon with Apache Maven, just type: - - Java 1.5 or higher - - Maven 2.0.x or higher (for building from source) - - JUnit 4 or higher (for testing) + $ mvn package - (More to come!) +Maven will build Symon, run unit tests, and produce a jar file in the +'target' directory containing the compiled simulator. -3.0 To Do +Symon is meant to be invoked directly from the jar file. To run with +Java 1.5 or greater, just type: + + $ jar -jar symon-0.1.jar + +When Symon is running , you should be greeted by the following message + + Welcome to the Symon 6502 Simulator. Type 'h' for help. + symon> + +Commands are entered at the monitor 'symon>' prompt. They can be the +full name (e.g. 'examine') or abbrevations (e.g. 'ex', or even just +'e' if there is no ambiguity). The basic commands are documented below + +3.1 Help +-------- + +Typing 'h' or 'help' will show the help screen. + + symon> h + Symon 6502 Simulator + + All addresses must be in hexadecimal. + Commands may be short or long (e.g. 'e' or 'ex' or 'examine'). + Note that 'go' clears the BREAK processor status flag. + + h Show this help file. + e [start] [end] Examine memory at PC, start, or start-end. + d
Deposit data into address. + f Fill memory with data. + set {pc,a,x,y} [data] Set register to data value. + load
Load binary file at address. + g [address] [steps] Start running at address, or at PC. + step [address] Step once, optionally starting at address. + stat Show CPU state. + reset Reset simulator. + trace Toggle trace. + q (or Control-D) Quit. + +3.2 Examine +----------- + +Memory locations can be examined with the 'examine' (abbreviated 'ex' +or 'e') command. + +If given no arguments, it will display the contents of memory at the +program counter, as well as the address it is showing. + +Example: Examine memory location at the program counter. + + symon> ex + 0000 35 + +Subsequent invocations without arguments will move forward in memory +from the program counter, showing one byte per invocation + +Example: Continue examining memory from previous location, one byte at +a time. + + symon> ex + 0001 ff + symon> ex + 0002 1d + symon> ex + 0003 a9 + +An address to examine can be specified by a single argument. + +Example: Examine memory location $200 + + symon> ex 0200 + 0200 a9 + +To examine a range of memory, both a start and end address can be +specified. + +Example: Examine all memory from memory location $300 to $325 + + symon> e 0300 0325 + 0300 a2 00 bd 11 03 f0 07 8d 00 c0 e8 4c 02 03 4c 0e + 0310 03 48 65 6c 6c 6f 2c 20 36 35 30 32 20 77 6f 72 + 0320 6c 64 21 0d 0a 00 + + +3.3 Deposit +----------- + +Memory contents can be updated using the 'deposit' command +(abbreviated 'dep' or 'd'). It takes a single argument, a +16-bit address. + +Example: Deposit the byte value $A9 into memory location $400 + + symon> d 0400 a9 + + +3.4 Fill +-------- + +A region of memory can be filled using the 'fill' command (abbreviated +'fi', 'fl', or 'f'). It takes three arguments. The first is the start +address, the second is the end address (inclusive), and the third is +the byte value to fill the memory with. + +Example: Fill the memory range $400 to $4FF with the byte value $EA + + symon> fill 0400 04ff ea + + +3.5 Set Registers +----------------- + +Individual registers can be modified directly using the 'set' command. +The program counter (PC), accumulator (A), X index register and Y +index register can be set. + +The 'set' command takes two arguments. The first is the register to be +modified, and the second is the one or two byte value to set. + +Example: Set the program counter to $300 + + symon> set pc 0300 + +Example: Set the X register to 00 + + symon> set x 00 + + +3.6 Load A Binary File +---------------------- + +Programs in the form of raw binary object files can be loaded directly +into memory with the 'load' command (abbreviated 'lo' or 'l'). It +takes two arguments: A file name (with full path if not in the current +working directory where the simulator was started), and a starting +address. + +Example: Load the file 'test.bin' at address $300 + + symon> load test2.bin 0300 + Loading file 'test2.bin' at address 0300... + Loaded 38 ($0026) bytes + +3.7 Start Running +----------------- + +The simulator's CPU can be started running with the 'go' command. If +invoked with no argument, the simulator will start running from the +current program counter. + +Example: Start running from the current program counter. + + symon> go + +If invoked with one argument, the PC will first be set to the +specified address before the simulated CPU starts running. + +Example: Start running from memory location $300 + + symon> go 0300 + +If invoked with two arguments, the second is interpreted as the +maximum number of steps to execute. + +Example: Start running from memory location $300, but for no more than +255 steps + + symon> go 0300 ff + + +[TODO: More to come] + + +4.0 To Do --------- -- Finish core functionality. -- Finish command monitor. -- Refactor address decoding (second refactor to DRY up more). +- More extensive testing. + +- Command monitor improvements: + * Allow 'deposit' to take no argument, but auto-increment + deposited-to address with each invocation. + - Clean up JavaDoc. + +- Busses are defined by start address and length. Devices are defined + by start address and end address. They should both use start/end + address. + - Implement CMOS 65C02 instructions and NMOS / CMOS mode flag. +- Allow a flag to disable breaking to monitor on BRK. + +- Allow displaying ACIA status and dumping ACIA buffers, for + debugging. + +- Allow fine-tuning of keyboard polling interval; 500 instructions is + a lot of latency. + 4.0 Licensing ------------- diff --git a/pom.xml b/pom.xml index ca4acba..7647e05 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.loomcom.symon symon jar - snapshot + 0.1 symon http://www.loomcom.com/symon diff --git a/src/main/java/com/loomcom/symon/Cpu.java b/src/main/java/com/loomcom/symon/Cpu.java index 2b6fe20..cdf6680 100644 --- a/src/main/java/com/loomcom/symon/Cpu.java +++ b/src/main/java/com/loomcom/symon/Cpu.java @@ -50,7 +50,6 @@ public class Cpu implements InstructionTable { private int irAddressMode; // Bits 3-5 of IR: [ | | |X|X|X| | ] private int irOpMode; // Bits 6-7 of IR: [ | | | | | |X|X] private int effectiveAddress; - private int effectiveData; /* Internal scratch space */ private int lo = 0, hi = 0; // Used in address calculation @@ -149,24 +148,20 @@ public class Cpu implements InstructionTable { // Get the data from the effective address (if any) effectiveAddress = 0; - effectiveData = 0; switch(irOpMode) { case 0: case 2: switch(irAddressMode) { case 0: // #Immediate - effectiveData = args[0]; break; case 1: // Zero Page effectiveAddress = args[0]; - effectiveData = bus.read(effectiveAddress); break; case 2: // Accumulator - ignored break; case 3: // Absolute effectiveAddress = address(args[0], args[1]); - effectiveData = bus.read(effectiveAddress); break; case 5: // Zero Page,X / Zero Page,Y if (ir == 0x96 || ir == 0xb6) { @@ -174,7 +169,6 @@ public class Cpu implements InstructionTable { } else { effectiveAddress = zpxAddress(args[0]); } - effectiveData = bus.read(effectiveAddress); break; case 7: // Absolute,X / Absolute,Y if (ir == 0xbe) { @@ -182,7 +176,6 @@ public class Cpu implements InstructionTable { } else { effectiveAddress = xAddress(args[0], args[1]); } - effectiveData = bus.read(effectiveAddress); break; } break; @@ -191,37 +184,29 @@ public class Cpu implements InstructionTable { case 0: // (Zero Page,X) tmp = args[0] + getXRegister(); effectiveAddress = address(bus.read(tmp), bus.read(tmp + 1)); - effectiveData = bus.read(effectiveAddress); break; case 1: // Zero Page effectiveAddress = args[0]; - effectiveData = bus.read(effectiveAddress); break; case 2: // #Immediate effectiveAddress = -1; - effectiveData = args[0]; break; case 3: // Absolute effectiveAddress = address(args[0], args[1]); - effectiveData = bus.read(effectiveAddress); break; case 4: // (Zero Page),Y tmp = address(bus.read(args[0]), bus.read((args[0]+1)&0xff)); effectiveAddress = (tmp + getYRegister())&0xffff; - effectiveData = bus.read(effectiveAddress); break; case 5: // Zero Page,X effectiveAddress = zpxAddress(args[0]); - effectiveData = bus.read(effectiveAddress); break; case 6: // Absolute, Y effectiveAddress = yAddress(args[0], args[1]); - effectiveData = bus.read(effectiveAddress); break; case 7: // Absolute, X effectiveAddress = xAddress(args[0], args[1]); - effectiveData = bus.read(effectiveAddress); break; } break; @@ -402,15 +387,18 @@ public class Cpu implements InstructionTable { /** ORA - Logical Inclusive Or ******************************************/ + case 0x09: // #Immediate + a |= args[0]; + setArithmeticFlags(a); + break; case 0x01: // (Zero Page,X) case 0x05: // Zero Page - case 0x09: // #Immediate case 0x0d: // Absolute case 0x11: // (Zero Page),Y case 0x15: // Zero Page,X case 0x19: // Absolute,Y case 0x1d: // Absolute,X - a |= effectiveData; + a |= bus.read(effectiveAddress); setArithmeticFlags(a); break; @@ -424,7 +412,7 @@ public class Cpu implements InstructionTable { case 0x0e: // Absolute case 0x16: // Zero Page,X case 0x1e: // Absolute,X - tmp = asl(effectiveData); + tmp = asl(bus.read(effectiveAddress)); bus.write(effectiveAddress, tmp); setArithmeticFlags(tmp); break; @@ -433,7 +421,7 @@ public class Cpu implements InstructionTable { /** BIT - Bit Test ******************************************************/ case 0x24: // Zero Page case 0x2c: // Absolute - tmp = a & effectiveData; + tmp = a & bus.read(effectiveAddress); setZeroFlag(tmp == 0); setNegativeFlag((tmp & 0x80) != 0); setOverflowFlag((tmp & 0x40) != 0); @@ -441,15 +429,18 @@ public class Cpu implements InstructionTable { /** AND - Logical AND ***************************************************/ + case 0x29: // #Immediate + a &= args[0]; + setArithmeticFlags(a); + break; case 0x21: // (Zero Page,X) case 0x25: // Zero Page - case 0x29: // #Immediate case 0x2d: // Absolute case 0x31: // (Zero Page),Y case 0x35: // Zero Page,X case 0x39: // Absolute,Y case 0x3d: // Absolute,X - a &= effectiveData; + a &= bus.read(effectiveAddress); setArithmeticFlags(a); break; @@ -463,22 +454,25 @@ public class Cpu implements InstructionTable { case 0x2e: // Absolute case 0x36: // Zero Page,X case 0x3e: // Absolute,X - tmp = rol(effectiveData); + tmp = rol(bus.read(effectiveAddress)); bus.write(effectiveAddress, tmp); setArithmeticFlags(tmp); break; /** EOR - Exclusive OR **************************************************/ + case 0x49: // #Immediate + a ^= args[0]; + setArithmeticFlags(a); + break; case 0x41: // (Zero Page,X) case 0x45: // Zero Page - case 0x49: // Immediate case 0x4d: // Absolute case 0x51: // (Zero Page,Y) case 0x55: // Zero Page,X case 0x59: // Absolute,Y case 0x5d: // Absolute,X - a ^= effectiveData; + a ^= bus.read(effectiveAddress); setArithmeticFlags(a); break; @@ -492,25 +486,31 @@ public class Cpu implements InstructionTable { case 0x4e: // Absolute case 0x56: // Zero Page,X case 0x5e: // Absolute,X - tmp = lsr(effectiveData); + tmp = lsr(bus.read(effectiveAddress)); bus.write(effectiveAddress, tmp); setArithmeticFlags(tmp); break; /** ADC - Add with Carry ************************************************/ + case 0x69: // #Immediate + if (decimalModeFlag) { + a = adcDecimal(a, args[0]); + } else { + a = adc(a, args[0]); + } + break; case 0x61: // (Zero Page,X) case 0x65: // Zero Page - case 0x69: // Immediate case 0x6d: // Absolute case 0x71: // (Zero Page),Y case 0x75: // Zero Page,X case 0x79: // Absolute,Y case 0x7d: // Absolute,X if (decimalModeFlag) { - a = adcDecimal(a, effectiveData); + a = adcDecimal(a, bus.read(effectiveAddress)); } else { - a = adc(a, effectiveData); + a = adc(a, bus.read(effectiveAddress)); } break; @@ -524,7 +524,7 @@ public class Cpu implements InstructionTable { case 0x6e: // Absolute case 0x76: // Zero Page,X case 0x7e: // Absolute,X - tmp = ror(effectiveData); + tmp = ror(bus.read(effectiveAddress)); bus.write(effectiveAddress, tmp); setArithmeticFlags(tmp); break; @@ -562,59 +562,72 @@ public class Cpu implements InstructionTable { /** LDY - Load Y Register ***********************************************/ - case 0xa0: // Immediate + case 0xa0: // #Immediate + y = args[0]; + setArithmeticFlags(y); + break; case 0xa4: // Zero Page case 0xac: // Absolute case 0xb4: // Zero Page,X case 0xbc: // Absolute,X - y = effectiveData; + y = bus.read(effectiveAddress); setArithmeticFlags(y); break; /** LDX - Load X Register ***********************************************/ - case 0xa2: // Immediate + case 0xa2: // #Immediate + x = args[0]; + setArithmeticFlags(x); + break; case 0xa6: // Zero Page case 0xae: // Absolute case 0xb6: // Zero Page,Y case 0xbe: // Absolute,Y - x = effectiveData; + x = bus.read(effectiveAddress); setArithmeticFlags(x); break; /** LDA - Load Accumulator **********************************************/ + case 0xa9: // #Immediate + a = args[0]; + setArithmeticFlags(a); + break; case 0xa1: // (Zero Page,X) case 0xa5: // Zero Page - case 0xa9: // Immediate case 0xad: // Absolute case 0xb1: // (Zero Page),Y case 0xb5: // Zero Page,X case 0xb9: // Absolute,Y case 0xbd: // Absolute,X - a = effectiveData; + a = bus.read(effectiveAddress); setArithmeticFlags(a); break; /** CPY - Compare Y Register ********************************************/ - case 0xc0: // Immediate + case 0xc0: // #Immediate + cmp(y, args[0]); + break; case 0xc4: // Zero Page case 0xcc: // Absolute - cmp(y, effectiveData); + cmp(y, bus.read(effectiveAddress)); break; /** CMP - Compare Accumulator *******************************************/ + case 0xc9: // #Immediate + cmp(a, args[0]); + break; case 0xc1: // (Zero Page,X) case 0xc5: // Zero Page - case 0xc9: // #Immediate case 0xcd: // Absolute case 0xd1: // (Zero Page),Y case 0xd5: // Zero Page,X case 0xd9: // Absolute,Y case 0xdd: // Absolute,X - cmp(a, effectiveData); + cmp(a, bus.read(effectiveAddress)); break; @@ -623,33 +636,41 @@ public class Cpu implements InstructionTable { case 0xce: // Absolute case 0xd6: // Zero Page,X case 0xde: // Absolute,X - tmp = --effectiveData & 0xff; + tmp = (bus.read(effectiveAddress) - 1) & 0xff; bus.write(effectiveAddress, tmp); setArithmeticFlags(tmp); break; /** CPX - Compare X Register ********************************************/ - case 0xe0: // Immediate + case 0xe0: // #Immediate + cmp(x, args[0]); + break; case 0xe4: // Zero Page case 0xec: // Absolute - cmp(x, effectiveData); + cmp(x, bus.read(effectiveAddress)); break; /** SBC - Subtract with Carry (Borrow) **********************************/ + case 0xe9: // #Immediate + if (decimalModeFlag) { + a = sbcDecimal(a, args[0]); + } else { + a = sbc(a, args[0]); + } + break; case 0xe1: // (Zero Page,X) case 0xe5: // Zero Page - case 0xe9: // Immediate case 0xed: // Absolute case 0xf1: // (Zero Page),Y case 0xf5: // Zero Page,X case 0xf9: // Absolute,Y case 0xfd: // Absolute,X if (decimalModeFlag) { - a = sbcDecimal(a, effectiveData); + a = sbcDecimal(a, bus.read(effectiveAddress)); } else { - a = sbc(a, effectiveData); + a = sbc(a, bus.read(effectiveAddress)); } break; @@ -659,7 +680,7 @@ public class Cpu implements InstructionTable { case 0xee: // Absolute case 0xf6: // Zero Page,X case 0xfe: // Absolute,X - tmp = ++effectiveData & 0xff; + tmp = (bus.read(effectiveAddress) + 1) & 0xff; bus.write(effectiveAddress, tmp); setArithmeticFlags(tmp); break; diff --git a/src/main/java/com/loomcom/symon/Simulator.java b/src/main/java/com/loomcom/symon/Simulator.java index 338f6cd..88145a7 100644 --- a/src/main/java/com/loomcom/symon/Simulator.java +++ b/src/main/java/com/loomcom/symon/Simulator.java @@ -21,24 +21,50 @@ public class Simulator { * correct IO devices. */ private Bus bus; - + + /** + * The ACIA, used for charater in/out. + * + * By default, the simulator uses base address c000 for the ACIA. + */ + private Acia acia; + private BufferedReader in; private BufferedWriter out; - + /* If true, trace execution of the CPU */ private boolean trace = false; private int nextExamineAddress = 0; + private static final int BUS_BOTTOM = 0x0000; + private static final int BUS_TOP = 0xffff; + + private static final int ACIA_BASE = 0xc000; + + private static final int MEMORY_BASE = 0x0000; + private static final int MEMORY_SIZE = 0xc000; // 48 KB + + private static final int ROM_BASE = 0xe000; + private static final int ROM_SIZE = 0x2000; // 8 KB + public Simulator() throws MemoryRangeException { - cpu = new Cpu(); - bus = new Bus(0x0000, 0xffff); + this.bus = new Bus(BUS_BOTTOM, BUS_TOP); + this.cpu = new Cpu(); + this.acia = new Acia(ACIA_BASE); + bus.addCpu(cpu); - bus.addDevice(new Memory(0x0000, 0x10000)); + bus.addDevice(new Memory(MEMORY_BASE, MEMORY_SIZE, false)); + bus.addDevice(acia); + // TODO: This should be read-only memory. Add a method + // to allow one-time initialization of ROM with a loaded + // ROM binary file. + bus.addDevice(new Memory(ROM_BASE, ROM_SIZE, false)); + this.in = new BufferedReader(new InputStreamReader(System.in)); this.out = new BufferedWriter(new OutputStreamWriter(System.out)); } - public void run() throws MemoryAccessException { + public void run() throws MemoryAccessException, FifoUnderrunException { try { greeting(); prompt(); @@ -57,12 +83,14 @@ public class Simulator { System.exit(1); } } - + /** * Dispatch the command. */ - public void dispatch(String commandLine) - throws MemoryAccessException, IOException, CommandFormatException { + public void dispatch(String commandLine) throws MemoryAccessException, + IOException, + CommandFormatException, + FifoUnderrunException { Command c = new Command(commandLine); String cmd = c.getCommand(); if (cmd != null) { @@ -72,7 +100,7 @@ public class Simulator { doGetState(); } else if (cmd.startsWith("se")) { doSet(c); - } else if (cmd.startsWith("r")) { + } else if (cmd.startsWith("r")) { doReset(); } else if (cmd.startsWith("e")) { doExamine(c); @@ -93,7 +121,7 @@ public class Simulator { } } } - + public void doHelp(Command c) throws IOException { writeLine("Symon 6502 Simulator"); writeLine(""); @@ -102,16 +130,16 @@ public class Simulator { writeLine("Note that 'go' clears the BREAK processor status flag."); writeLine(""); writeLine("h Show this help file."); - writeLine("g [address] [steps] Start running at address, or at PC."); writeLine("e [start] [end] Examine memory at PC, start, or start-end."); writeLine("d
Deposit data into address."); writeLine("f Fill memory with data."); - writeLine("reset Reset simulator."); writeLine("set {pc,a,x,y} [data] Set register to data value."); - writeLine("stat Show CPU state."); - writeLine("step [address] Step once, optionally starting at address."); - writeLine("trace Toggle trace."); writeLine("load
Load binary file at address."); + writeLine("g [address] [steps] Start running at address, or at PC."); + writeLine("step [address] Step once, optionally starting at address."); + writeLine("stat Show CPU state."); + writeLine("reset Reset simulator."); + writeLine("trace Toggle trace."); writeLine("q (or Control-D) Quit.\n"); } @@ -119,25 +147,26 @@ public class Simulator { writeLine(cpu.toString()); writeLine("Trace is " + (trace ? "on" : "off")); } - - public void doLoad(Command c) throws IOException, MemoryAccessException, - CommandFormatException { + + public void doLoad(Command c) throws IOException, + MemoryAccessException, + CommandFormatException { if (c.numArgs() != 2) { throw new CommandFormatException("load
"); } - + File binFile = new File(c.getArg(0)); int address = stringToWord(c.getArg(1)); - + if (!binFile.exists()) { throw new CommandFormatException("File '" + binFile + "' does not exist."); } writeLine("Loading file '" + binFile + "' at address " + String.format("%04x", address) + "..."); - + int bytesLoaded = 0; - + FileInputStream fis = new FileInputStream(binFile); try { @@ -149,13 +178,13 @@ public class Simulator { } finally { fis.close(); } - + writeLine("Loaded " + bytesLoaded + " (" + String.format("$%04x", bytesLoaded) + ") bytes"); } - - public void doSet(Command c) throws MemoryAccessException, - CommandFormatException { + + public void doSet(Command c) throws MemoryAccessException, + CommandFormatException { if (c.numArgs() != 2) { throw new CommandFormatException("set {a, x, y, pc} "); } @@ -177,9 +206,10 @@ public class Simulator { throw new CommandFormatException("Illegal address"); } } - - public void doExamine(Command c) throws IOException, MemoryAccessException, - CommandFormatException { + + public void doExamine(Command c) throws IOException, + MemoryAccessException, + CommandFormatException { try { if (c.numArgs() == 2) { int startAddress = stringToWord(c.getArgs()[0]); @@ -206,15 +236,15 @@ public class Simulator { bus.read(nextExamineAddress))); nextExamineAddress++; } else { - throw new CommandFormatException("e [start [end]]"); + throw new CommandFormatException("e [start [end]]"); } } catch (NumberFormatException ex) { throw new CommandFormatException("Illegal Address"); } } - + public void doDeposit(Command c) throws MemoryAccessException, - CommandFormatException { + CommandFormatException { if (c.numArgs() != 2) { throw new CommandFormatException("d [address] [data]"); } @@ -226,9 +256,9 @@ public class Simulator { throw new CommandFormatException("Illegal Address"); } } - + public void doFill(Command c) throws MemoryAccessException, - CommandFormatException { + CommandFormatException { if (c.numArgs() != 3) { throw new CommandFormatException("f [start] [end] [data]"); } @@ -244,9 +274,11 @@ public class Simulator { throw new CommandFormatException("Illegal Address"); } } - - public void doStep(Command c) throws IOException, MemoryAccessException, - CommandFormatException { + + public void doStep(Command c) throws IOException, + MemoryAccessException, + FifoUnderrunException, + CommandFormatException { try { if (c.numArgs() > 0) { cpu.setProgramCounter(stringToWord(c.getArg(1))); @@ -257,16 +289,21 @@ public class Simulator { throw new CommandFormatException("Illegal Address"); } } - - public void doGo(Command c) throws IOException, MemoryAccessException, - CommandFormatException { + + public void doGo(Command c) throws IOException, + MemoryAccessException, + FifoUnderrunException, + CommandFormatException { + int readChar; + int stepCount = 0; + if (c.numArgs() > 2) { throw new CommandFormatException("g [address] [steps]"); } try { int start = 0; int steps = -1; - + if (c.numArgs() > 0) { start = stringToWord(c.getArg(0)); } else { @@ -275,18 +312,56 @@ public class Simulator { 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); + outer: while (!cpu.getBreakFlag() && (steps == -1 || steps-- > 0)) { cpu.step(); if (trace) { writeLine(cpu.toString()); } + // Wake up and scan keyboard every 500 steps + if (stepCount++ >= 500) { + // Reset step count + stepCount = 0; + + // + // Do output if available. + // + while (acia.hasTxChar()) { + out.write(acia.txRead()); + out.flush(); + } + + // + // Consume input if available. + // + // NOTE: On UNIX systems, System.in.available() returns 0 + // until Enter is pressed. So to interrupt we must ALWAYS + // type "^E". Sucks hard. But such is life. + if (System.in.available() > 0) { + while ((readChar = in.read()) > -1) { + // Keep consuming unless ^E is found. + // + // TODO: This will probably lead to a lot of spurious keyboard + // entry. Gotta keep an eye on that. + // + if (readChar == 0x05) { + break outer; + } else { + // Buffer keyboard input into the simulated ACIA's + // read buffer. + acia.rxWrite(readChar); + } + } + } + + } } if (!trace) { writeLine(cpu.toString()); @@ -295,12 +370,12 @@ public class Simulator { throw new CommandFormatException("Illegal Address"); } } - + 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; @@ -309,14 +384,15 @@ public class Simulator { /** * Main simulator routine. */ - public static void main(String[] args) throws MemoryAccessException { + public static void main(String[] args) throws MemoryAccessException, + FifoUnderrunException { try { new Simulator().run(); } catch (MemoryRangeException ex) { System.err.println("Error: " + ex.toString()); } } - + /******************************************************************* * Private *******************************************************************/ @@ -328,15 +404,15 @@ public class Simulator { 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 6502 Simulator. Type 'h' for help."); } @@ -364,7 +440,7 @@ public class Simulator { 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. @@ -386,15 +462,15 @@ public class Simulator { } } } - + public String getCommand() { return command; } - + public String[] getArgs() { return args; } - + public String getArg(int argNum) { if (argNum > args.length - 1) { return null; @@ -402,13 +478,13 @@ public class Simulator { 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/Acia.java b/src/main/java/com/loomcom/symon/devices/Acia.java new file mode 100644 index 0000000..8dbae36 --- /dev/null +++ b/src/main/java/com/loomcom/symon/devices/Acia.java @@ -0,0 +1,124 @@ +package com.loomcom.symon.devices; + +import com.loomcom.symon.exceptions.*; +import com.loomcom.symon.util.*; + +/** + * This is a simulation of the MOS 6551 ACIA, with limited + * functionality. Interrupts are not supported. + * + * Unlike a 16550 UART, the 6551 ACIA has only one-byte transmit and + * receive buffers. It is the programmer's responsibility to check the + * status (full or empty) for transmit and receive buffers before + * writing / reading. However, in the simulation we maintain two + * small buffers of 256 characters, since we only wake up to check for + * keyboard input and do output every 500 instructions. + */ +public class Acia extends Device { + + public static final int ACIA_SIZE = 4; + public static final int BUF_LEN = 256; + + static final int DATA_REG = 0; + static final int STAT_REG = 1; + static final int CMND_REG = 2; + static final int CTRL_REG = 3; + + /** Register addresses */ + private int baseAddress; + + /** Registers. These are ignored in the current implementation. */ + private int commandRegister; + private int controlRegister; + + /** Read/Write buffers */ + private FifoRingBuffer rxBuffer = new FifoRingBuffer(BUF_LEN); + private FifoRingBuffer txBuffer = new FifoRingBuffer(BUF_LEN); + + public Acia(int address) throws MemoryRangeException { + super(address, ACIA_SIZE, "ACIA"); + this.baseAddress = address; + } + + @Override + public int read(int address) throws MemoryAccessException { + switch (address) { + case DATA_REG: + try { + return rxRead(); + } catch (FifoUnderrunException ex) { + throw new MemoryAccessException("Buffer underrun"); + } + case STAT_REG: + return ((rxBuffer.isEmpty() ? 0x00 : 0x08) | + (txBuffer.isEmpty() ? 0x10 : 0x00)); + case CMND_REG: + return commandRegister; + case CTRL_REG: + return controlRegister; + default: + throw new MemoryAccessException("No register."); + } + } + + @Override + public void write(int address, int data) throws MemoryAccessException { + switch (address) { + case 0: + txWrite(data); + break; + case 1: + reset(); + break; + case 2: + commandRegister = data; + break; + case 3: + controlRegister = data; + break; + default: + throw new MemoryAccessException("No register."); + } + } + + @Override + public String toString() { + return "ACIA@" + String.format("%04X", baseAddress); + } + + public int rxRead() throws FifoUnderrunException { + return rxBuffer.pop(); + } + + public void rxWrite(int data) { + rxBuffer.push(data); + } + + public int txRead() throws FifoUnderrunException { + return txBuffer.pop(); + } + + public void txWrite(int data) { + txBuffer.push(data); + } + + /** + * @return true if there is character data in the TX register. + */ + public boolean hasTxChar() { + return !txBuffer.isEmpty(); + } + + /** + * @return true if there is character data in the RX register. + */ + public boolean hasRxChar() { + return !rxBuffer.isEmpty(); + } + + private void reset() { + txBuffer.reset(); + rxBuffer.reset(); + } + +} diff --git a/src/main/java/com/loomcom/symon/devices/Memory.java b/src/main/java/com/loomcom/symon/devices/Memory.java index ef377f3..555a94a 100644 --- a/src/main/java/com/loomcom/symon/devices/Memory.java +++ b/src/main/java/com/loomcom/symon/devices/Memory.java @@ -9,17 +9,18 @@ public class Memory extends Device { private boolean readOnly; private int[] mem; + /* Initialize all locations to 0x00 (BRK) */ + private static final int DEFAULT_FILL = 0x00; + public Memory(int address, int size, boolean readOnly) - throws MemoryRangeException { - super(address, size, "RW Memory"); + throws MemoryRangeException { + super(address, size, (readOnly ? "RO Memory" : "RW Memory")); this.readOnly = readOnly; this.mem = new int[size]; - // Initialize all locations to 0x00 (BRK) - Arrays.fill(this.mem, 0x00); + Arrays.fill(this.mem, DEFAULT_FILL); } - public Memory(int address, int size) - throws MemoryRangeException { + public Memory(int address, int size) throws MemoryRangeException { this(address, size, false); } diff --git a/src/main/java/com/loomcom/symon/exceptions/FifoUnderrunException.java b/src/main/java/com/loomcom/symon/exceptions/FifoUnderrunException.java new file mode 100644 index 0000000..0ffb158 --- /dev/null +++ b/src/main/java/com/loomcom/symon/exceptions/FifoUnderrunException.java @@ -0,0 +1,7 @@ +package com.loomcom.symon.exceptions; + +public class FifoUnderrunException extends Exception { + public FifoUnderrunException(String msg) { + super(msg); + } +} \ No newline at end of file diff --git a/src/main/java/com/loomcom/symon/util/FifoRingBuffer.java b/src/main/java/com/loomcom/symon/util/FifoRingBuffer.java new file mode 100644 index 0000000..52d193a --- /dev/null +++ b/src/main/java/com/loomcom/symon/util/FifoRingBuffer.java @@ -0,0 +1,78 @@ +package com.loomcom.symon.util; + +import com.loomcom.symon.exceptions.*; + +/** + * A very simple and efficient FIFO ring buffer implementation backed + * by an array. It can only hold only integers. + */ +public class FifoRingBuffer { + + private int[] fifoBuffer; + private int readPtr = 0; + private int writePtr = 0; + private int size = 0; + + public FifoRingBuffer(int size) { + if (size <= 0) { + throw new RuntimeException("Cannot create a FifoRingBuffer with size <= 0."); + } + this.size = size; + fifoBuffer = new int[size]; + } + + public int peek() throws FifoUnderrunException { + if (isEmpty()) { + throw new FifoUnderrunException("Buffer Underrun"); + } + return fifoBuffer[readPtr]; + } + + public int pop() throws FifoUnderrunException { + if (isEmpty()) { + throw new FifoUnderrunException("Buffer Underrun"); + } + int val = fifoBuffer[readPtr]; + incrementReadPointer(); + return val; + } + + public boolean isEmpty() { + return(readPtr == writePtr); + } + + public boolean isFull() { + return((readPtr == 0 && writePtr == (size - 1)) || + writePtr == (readPtr - 1)); + } + + public void push(int val) { + fifoBuffer[writePtr] = val; + incrementWritePointer(); + } + + public void reset() { + readPtr = 0; + writePtr = 0; + } + + private void incrementWritePointer() { + if (++writePtr == size) { + writePtr = 0; + } + if (writePtr == readPtr) { + incrementReadPointer(); + } + } + + private void incrementReadPointer() { + if (++readPtr == size) { + readPtr = 0; + } + } + + public String toString() { + return "[FifoRingBuffer: size=" + size + "]"; + } +} + diff --git a/src/test/java/com/loomcom/symon/AciaTest.java b/src/test/java/com/loomcom/symon/AciaTest.java new file mode 100644 index 0000000..9081d9d --- /dev/null +++ b/src/test/java/com/loomcom/symon/AciaTest.java @@ -0,0 +1,122 @@ +package com.loomcom.symon; + +import org.junit.*; + +import com.loomcom.symon.devices.Acia; +import com.loomcom.symon.exceptions.FifoUnderrunException; + +import static org.junit.Assert.*; + +public class AciaTest { + + @Test + public void newAciaShouldHaveTxEmptyStatus() throws Exception { + Acia acia = new Acia(0x000); + + assertEquals(0x10, acia.read(0x0001)); + } + + @Test + public void aciaShouldHaveTxEmptyStatusOffIfTxHasData() throws Exception { + Acia acia = new Acia(0x000); + + acia.txWrite('a'); + assertEquals(0x00, acia.read(0x0001)); + } + + @Test + public void aciaShouldHaveRxFullStatusOffIfRxHasData() throws Exception { + Acia acia = new Acia(0x000); + + acia.rxWrite('a'); + assertEquals(0x18, acia.read(0x0001)); + } + + @Test + public void aciaShouldHaveTxEmptyAndRxFullStatusOffIfRxAndTxHaveData() + throws Exception { + Acia acia = new Acia(0x000); + + acia.rxWrite('a'); + acia.txWrite('b'); + + assertEquals(0x08, acia.read(0x0001)); + } + + @Test + public void readingBuffersUntilEmptyShouldResetStatus() + throws Exception { + Acia acia = new Acia(0x0000); + + acia.rxWrite('a'); + acia.txWrite('b'); + + assertEquals(0x08, acia.read(0x0001)); + + assertEquals('a', acia.rxRead()); + assertEquals('b', acia.txRead()); + + assertEquals(0x10, acia.read(0x0001)); + } + + @Test + public void readingPastEmptyRxBufferShouldThrowException() + throws Exception { + Acia acia = new Acia(0x0000); + + acia.rxWrite('a'); + assertEquals(0x18, acia.read(0x0001)); // not empty + + assertEquals('a', acia.rxRead()); + assertEquals(0x10, acia.read(0x0001)); // becomes empty + + // Should raise (note: I prefer this style to @Test(expected=...) + // because it allows much finer grained control over asserting + // exactly where the exception is expected to be raised.) + try { + // Should cause an underrun + acia.rxRead(); + fail("Should have raised FifoUnderrunException."); + } catch (FifoUnderrunException ex) {} + + assertEquals(0x10, acia.read(0x0001)); // still again + + for (int i = 0; i < Acia.BUF_LEN; i++) { + acia.rxWrite('a'); + } + + // Should NOT cause an overrun + acia.rxWrite('a'); // nothing thrown + } + + @Test + public void readingPastEmptyTxBufferShouldThrowException() + throws Exception { + Acia acia = new Acia(0x0000); + + acia.txWrite('a'); + assertEquals(0x00, acia.read(0x0001)); // not empty + + assertEquals('a', acia.txRead()); + assertEquals(0x10, acia.read(0x0001)); // becomes empty + + // Should raise (note: I prefer this style to @Test(expected=...) + // because it allows much finer grained control over asserting + // exactly where the exception is expected to be raised.) + try { + // Should cause an underrun + acia.txRead(); + fail("Should have raised FifoUnderrunException."); + } catch (FifoUnderrunException ex) {} + + assertEquals(0x10, acia.read(0x0001)); // still empty + + for (int i = 0; i < Acia.BUF_LEN; i++) { + acia.txWrite('a'); + } + + // Should NOT cause an overrun + acia.txWrite('a'); // Nothing thrown. + } + +} diff --git a/src/test/java/com/loomcom/symon/CpuIndirectIndexedModeTest.java b/src/test/java/com/loomcom/symon/CpuIndirectIndexedModeTest.java index 3ec80f0..3f40542 100644 --- a/src/test/java/com/loomcom/symon/CpuIndirectIndexedModeTest.java +++ b/src/test/java/com/loomcom/symon/CpuIndirectIndexedModeTest.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.*;