First work toward moving to a Swing UI for the simulator.
This commit is contained in:
1 parent
b44859e096
commit
a1d07bf223
31 files changed
+2765
-2437
No files matched your search
@@ -1,490 +1,157 @@
|
||||
package com.loomcom.symon;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.logging.*;
|
||||
|
||||
import com.loomcom.symon.devices.*;
|
||||
import com.loomcom.symon.exceptions.*;
|
||||
import com.loomcom.symon.ui.UiUpdater;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
/**
|
||||
* Main control class for the J6502 Simulator.
|
||||
* Main entry point and control for the Symon Simulator.
|
||||
* This class is responsible for creating the UI and starting
|
||||
* the IO threads that pass data to and from the simulated
|
||||
* Bus and Cpu.
|
||||
*/
|
||||
public class Simulator {
|
||||
public class Simulator implements Runnable {
|
||||
|
||||
/**
|
||||
* The CPU itself.
|
||||
*/
|
||||
private Cpu cpu;
|
||||
private final static Logger logger = Logger.getLogger(Simulator.class.getName());
|
||||
|
||||
/**
|
||||
* The Bus responsible for routing memory read/write requests to the
|
||||
* correct IO devices.
|
||||
*/
|
||||
private Bus bus;
|
||||
private Bus bus;
|
||||
private Cpu cpu;
|
||||
private Acia acia;
|
||||
private Memory ram;
|
||||
private Memory rom;
|
||||
private boolean isRunning = false;
|
||||
|
||||
/**
|
||||
* The ACIA, used for charater in/out.
|
||||
*
|
||||
* By default, the simulator uses base address c000 for the ACIA.
|
||||
*/
|
||||
private Acia acia;
|
||||
private int updatesRequested = 0;
|
||||
|
||||
private BufferedReader in;
|
||||
private BufferedWriter out;
|
||||
private UiUpdater uiUpdater;
|
||||
|
||||
/* 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 BUS_BOTTOM = 0x0000;
|
||||
private static final int BUS_TOP = 0xffff;
|
||||
private static final int MEMORY_BASE = 0x0000;
|
||||
private static final int MEMORY_SIZE = 0xc000; // 48 KB
|
||||
|
||||
private static final int ACIA_BASE = 0xc000;
|
||||
private static final int ROM_BASE = 0xe000;
|
||||
private static final int ROM_SIZE = 0x2000; // 8 KB
|
||||
|
||||
private static final int MEMORY_BASE = 0x0000;
|
||||
private static final int MEMORY_SIZE = 0xc000; // 48 KB
|
||||
public static final int ACIA_BASE = 0xc000;
|
||||
|
||||
private static final int ROM_BASE = 0xe000;
|
||||
private static final int ROM_SIZE = 0x2000; // 8 KB
|
||||
private static final int MAX_REQUESTS_BETWEEN_UPDATES = 25000;
|
||||
|
||||
public Simulator() throws MemoryRangeException {
|
||||
this.bus = new Bus(BUS_BOTTOM, BUS_TOP);
|
||||
this.cpu = new Cpu();
|
||||
this.acia = new Acia(ACIA_BASE);
|
||||
public Simulator() throws MemoryRangeException {
|
||||
this.acia = new Acia(ACIA_BASE);
|
||||
this.bus = new Bus(BUS_BOTTOM, BUS_TOP);
|
||||
this.cpu = new Cpu();
|
||||
this.ram = new Memory(MEMORY_BASE, MEMORY_SIZE, false);
|
||||
// TODO: Load this ROM from a file, naturally!
|
||||
this.rom = new Memory(ROM_BASE, ROM_SIZE, false);
|
||||
bus.addCpu(cpu);
|
||||
bus.addDevice(acia);
|
||||
bus.addDevice(ram);
|
||||
bus.addDevice(rom);
|
||||
}
|
||||
|
||||
bus.addCpu(cpu);
|
||||
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));
|
||||
public void loadProgram(byte[] program, int startAddress) throws MemoryAccessException {
|
||||
cpu.setResetVector(startAddress);
|
||||
|
||||
this.in = new BufferedReader(new InputStreamReader(System.in));
|
||||
this.out = new BufferedWriter(new OutputStreamWriter(System.out));
|
||||
}
|
||||
int addr = startAddress, i;
|
||||
for (i = 0; i < program.length; i++) {
|
||||
bus.write(addr++, program[i] & 0xff);
|
||||
}
|
||||
|
||||
logger.log(Level.INFO, "Loaded " + i + " bytes at address 0x" +
|
||||
Integer.toString(startAddress, 16));
|
||||
}
|
||||
|
||||
public void setUiUpdater(UiUpdater uiUpdater) {
|
||||
this.uiUpdater = uiUpdater;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return isRunning;
|
||||
}
|
||||
|
||||
public void requestStop() {
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
public void reset() throws MemoryAccessException {
|
||||
cpu.reset();
|
||||
}
|
||||
|
||||
public void clearMemory() {
|
||||
ram.fill(0x00);
|
||||
}
|
||||
|
||||
public int getProcessorStatus() {
|
||||
return cpu.getProcessorStatus();
|
||||
}
|
||||
|
||||
public Cpu getCpu() {
|
||||
return this.cpu;
|
||||
}
|
||||
|
||||
public long memorySize() {
|
||||
return MEMORY_SIZE;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
logger.log(Level.INFO, "Entering 'run' on main Simulator thread");
|
||||
isRunning = true;
|
||||
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
uiUpdater.simulatorDidStart();
|
||||
}
|
||||
});
|
||||
|
||||
public void run() throws MemoryAccessException, FifoUnderrunException {
|
||||
try {
|
||||
greeting();
|
||||
prompt();
|
||||
String command = null;
|
||||
while (!shouldQuit(command = readLine())) {
|
||||
try {
|
||||
dispatch(command);
|
||||
} catch (CommandFormatException ex) {
|
||||
writeLine(ex.getMessage());
|
||||
}
|
||||
prompt();
|
||||
}
|
||||
writeLine("\nGoodbye!");
|
||||
} catch (IOException ex) {
|
||||
System.err.println("Error: " + ex.toString());
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the command.
|
||||
*/
|
||||
public void dispatch(String commandLine) throws MemoryAccessException,
|
||||
IOException,
|
||||
CommandFormatException,
|
||||
FifoUnderrunException {
|
||||
Command c = new Command(commandLine);
|
||||
String cmd = c.getCommand();
|
||||
if (cmd != null) {
|
||||
if (cmd.startsWith("ste")) {
|
||||
doStep(c);
|
||||
} else if (cmd.startsWith("sta")) {
|
||||
doGetState();
|
||||
} else if (cmd.startsWith("se")) {
|
||||
doSet(c);
|
||||
} 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 if (cmd.startsWith("l")) {
|
||||
doLoad(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.");
|
||||
writeLine("Commands may be short or long (e.g. 'e' or 'ex' or 'examine').");
|
||||
writeLine("Note that 'go' clears the BREAK processor status flag.");
|
||||
writeLine("");
|
||||
writeLine("h Show this help file.");
|
||||
writeLine("e [start] [end] Examine memory at PC, start, or start-end.");
|
||||
writeLine("d <address> <data> Deposit data into address.");
|
||||
writeLine("f <start> <end> <data> Fill memory with data.");
|
||||
writeLine("set {pc,a,x,y} [data] Set register to data value.");
|
||||
writeLine("load <file> <address> 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");
|
||||
}
|
||||
|
||||
public void doGetState() throws IOException, MemoryAccessException {
|
||||
writeLine(cpu.toString());
|
||||
writeLine("Trace is " + (trace ? "on" : "off"));
|
||||
}
|
||||
|
||||
public void doLoad(Command c) throws IOException,
|
||||
MemoryAccessException,
|
||||
CommandFormatException {
|
||||
if (c.numArgs() != 2) {
|
||||
throw new CommandFormatException("load <file> <address>");
|
||||
}
|
||||
|
||||
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 {
|
||||
int b = 0;
|
||||
while ((b = fis.read()) > -1 && address <= bus.endAddress()) {
|
||||
bus.write(address++, b);
|
||||
bytesLoaded++;
|
||||
}
|
||||
} finally {
|
||||
fis.close();
|
||||
}
|
||||
|
||||
writeLine("Loaded " + bytesLoaded + " (" +
|
||||
String.format("$%04x", bytesLoaded) + ") bytes");
|
||||
}
|
||||
|
||||
public void doSet(Command c) throws MemoryAccessException,
|
||||
CommandFormatException {
|
||||
if (c.numArgs() != 2) {
|
||||
throw new CommandFormatException("set {a, x, y, pc} <value>");
|
||||
}
|
||||
try {
|
||||
String reg = c.getArg(0).toLowerCase();
|
||||
String data = c.getArg(1);
|
||||
if ("a".equals(reg)) {
|
||||
cpu.setAccumulator(stringToByte(data));
|
||||
} else if ("x".equals(reg)) {
|
||||
cpu.setXRegister(stringToByte(data));
|
||||
} else if ("y".equals(reg)) {
|
||||
cpu.setYRegister(stringToByte(data));
|
||||
} else if ("pc".equals(reg)) {
|
||||
cpu.setProgramCounter(stringToWord(data));
|
||||
} else {
|
||||
throw new CommandFormatException("set {a, x, y, pc} <value>");
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new CommandFormatException("Illegal address");
|
||||
}
|
||||
}
|
||||
|
||||
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++ < 16 && startAddress <= endAddress) {
|
||||
line.append(String.format("%02x ", bus.read(startAddress++)));
|
||||
if (numBytes % 8 == 0) {
|
||||
line.append(" ");
|
||||
while (isRunning && !cpu.getBreakFlag()) {
|
||||
step();
|
||||
}
|
||||
}
|
||||
writeLine(line.toString());
|
||||
} catch (SymonException ex) {
|
||||
logger.log(Level.SEVERE, "Exception in main simulator run thread. Exiting run.");
|
||||
ex.printStackTrace();
|
||||
}
|
||||
nextExamineAddress = endAddress + 1;
|
||||
} else if (c.numArgs() == 1) {
|
||||
int address = stringToWord(c.getArgs()[0]);
|
||||
writeLine(String.format("%04x %02x", address, bus.read(address)));
|
||||
nextExamineAddress = address + 1;
|
||||
} else if (c.numArgs() == 0) {
|
||||
writeLine(String.format("%04x %02x", nextExamineAddress,
|
||||
bus.read(nextExamineAddress)));
|
||||
nextExamineAddress++;
|
||||
} else {
|
||||
throw new CommandFormatException("e [start [end]]");
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new CommandFormatException("Illegal Address");
|
||||
|
||||
logger.log(Level.INFO, "Exiting 'run'. BREAK=" + cpu.getBreakBit() + "; RUN_FLAG=" + isRunning);
|
||||
isRunning = false;
|
||||
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
uiUpdater.simulatorDidStop();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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("Illegal Address");
|
||||
}
|
||||
}
|
||||
|
||||
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("Illegal Address");
|
||||
}
|
||||
}
|
||||
public void step() throws MemoryAccessException, FifoUnderrunException {
|
||||
|
||||
public void doStep(Command c) throws IOException,
|
||||
MemoryAccessException,
|
||||
FifoUnderrunException,
|
||||
CommandFormatException {
|
||||
try {
|
||||
if (c.numArgs() > 0) {
|
||||
cpu.setProgramCounter(stringToWord(c.getArg(1)));
|
||||
}
|
||||
cpu.step();
|
||||
writeLine(cpu.toString()); // Always show status after stepping
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new CommandFormatException("Illegal Address");
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
start = cpu.getProgramCounter();
|
||||
}
|
||||
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());
|
||||
|
||||
// TODO: ACIA interrupt handling. For now, poll ACIA on each step.
|
||||
|
||||
// Read from the ACIA and add to the UiUpdater buffer
|
||||
while (acia.hasTxChar()) {
|
||||
uiUpdater.consoleWrite(acia.txRead());
|
||||
}
|
||||
// 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<enter>". 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is a very expensive update, and we're doing it without
|
||||
// a delay, so we don't want to overwhelm the Swing event processing thread
|
||||
// with requests. Limit the number of ui updates that can be performed.
|
||||
if (updatesRequested++ > MAX_REQUESTS_BETWEEN_UPDATES) {
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
uiUpdater.updateUi();
|
||||
}
|
||||
});
|
||||
updatesRequested = 0;
|
||||
}
|
||||
}
|
||||
if (!trace) {
|
||||
writeLine(cpu.toString());
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main simulator routine.
|
||||
*/
|
||||
public static void main(String[] args) throws MemoryAccessException,
|
||||
FifoUnderrunException {
|
||||
try {
|
||||
new Simulator().run();
|
||||
} catch (MemoryRangeException ex) {
|
||||
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 6502 Simulator. Type 'h' for help.");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user