1
0
mirror of https://github.com/sethm/symon.git synced 2024-06-01 08:41:32 +00:00

Checkpoint of my Swing UI refactoring. Usable for output-only programs, but only barely.

This commit is contained in:
Seth Morabito 2012-05-28 22:18:35 -07:00
parent 5ce81ef82c
commit ed36690e9e
13 changed files with 776 additions and 659 deletions

View File

@ -81,7 +81,7 @@
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.loomcom.symon.MainWindow</mainClass>
<mainClass>com.loomcom.symon.Simulator</mainClass>
</manifest>
</archive>
</configuration>
@ -114,7 +114,7 @@
<configuration>
<archive>
<manifest>
<mainClass>com.loomcom.symon.MainWindow</mainClass>
<mainClass>com.loomcom.symon.Simulator</mainClass>
<packageName>com.loomcom.symon</packageName>
</manifest>
<manifestEntries>

View File

@ -3,18 +3,30 @@
;;
.alias cr $0d
.alias lf $0a
.alias out $c000
.alias iobase $c000
.alias iostatus [iobase + 1]
.alias iocmd [iobase + 2]
.alias ioctrl [iobase + 3]
.org $0300
start: ldx #$00
start: cli
lda #$09
sta iocmd ; Set command status
lda #$16
sta ioctrl ; 0 stop bits, 8 bit word, 300 baud
ldx #$00 ; Initialize index
loop: lda string,x
beq start ; If A is 0, loop back and start again
sta out ; Otherwise, store into output
inx ; Increment X
write: lda iostatus
and #$10 ; Load ACIA status. Is output buffer empty?
beq write ; If not, loop back and try again,
lda string,x
sta iobase ; Otherwise, write to output.
inx
jmp loop ; Repeat.
string: .byte "Hello, 6502 world! ", 0

View File

@ -111,6 +111,7 @@ public class Cpu implements InstructionTable {
decimalModeFlag = false;
breakFlag = false;
overflowFlag = false;
zeroFlag = false;
// Clear illegal opcode trap.
opTrap = false;
@ -1375,7 +1376,7 @@ public class Cpu implements InstructionTable {
return (zp + getYRegister()) & 0xff;
}
void setResetVector(int address) throws MemoryAccessException {
public void setResetVector(int address) throws MemoryAccessException {
bus.write(RST_VECTOR_H, (address & 0xff00) >>> 8);
bus.write(RST_VECTOR_L, address & 0x00ff);
}

View File

@ -1,247 +0,0 @@
package com.loomcom.symon;
import com.loomcom.symon.exceptions.MemoryAccessException;
import com.loomcom.symon.exceptions.MemoryRangeException;
import com.loomcom.symon.exceptions.SymonException;
import com.loomcom.symon.ui.Console;
import com.loomcom.symon.ui.StatusPane;
import com.loomcom.symon.ui.UiUpdater;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
public class MainWindow extends JFrame implements ActionListener {
// Hard coded for now.
public static final int PROGRAM_START_ADDRESS = 0x0300;
private final static Logger logger = Logger.getLogger(MainWindow.class.getName());
private final Simulator simulator;
private Thread simulatorThread;
private final Console console;
private final StatusPane statusPane;
private final JButton loadButton;
private final JButton runButton;
private final JButton stepButton;
private final JButton resetButton;
private final JButton quitButton;
private final JFileChooser fileChooser;
private final UiUpdater uiUpdater;
public MainWindow(final Simulator simulator) {
this.setLayout(new BorderLayout());
this.simulator = simulator;
// UI components used for I/O.
this.console = new Console();
this.statusPane = new StatusPane(simulator.getCpu());
JPanel buttonContainer = new JPanel();
JPanel controlsContainer = new JPanel();
// File Chooser
fileChooser = new JFileChooser();
buttonContainer.setLayout(new FlowLayout());
controlsContainer.setLayout(new BorderLayout());
this.loadButton = new JButton("Load");
this.runButton = new JButton("Run");
this.stepButton = new JButton("Step");
this.resetButton = new JButton("Reset");
this.quitButton = new JButton("Quit");
buttonContainer.add(loadButton);
buttonContainer.add(runButton);
buttonContainer.add(stepButton);
buttonContainer.add(resetButton);
buttonContainer.add(quitButton);
controlsContainer.add(buttonContainer, BorderLayout.PAGE_START);
controlsContainer.add(statusPane, BorderLayout.PAGE_END);
console.setBorder(BorderFactory.createBevelBorder(1));
console.setBackground(Color.BLACK);
console.setForeground(Color.WHITE);
console.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 20));
getContentPane().add(console, BorderLayout.CENTER);
getContentPane().add(controlsContainer, BorderLayout.PAGE_END);
quitButton.addActionListener(this);
runButton.addActionListener(this);
stepButton.addActionListener(this);
resetButton.addActionListener(this);
loadButton.addActionListener(this);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
console.requestFocus();
// Finally create the UI Updater and hand a reference to the Simulator.
this.uiUpdater = new UiUpdater(this);
// The simulator must always have a reference to the same UI updater.
this.simulator.setUiUpdater(uiUpdater);
}
public void actionPerformed(ActionEvent actionEvent) {
if (actionEvent.getSource() == loadButton) {
handleProgramLoad();
} else if (actionEvent.getSource() == resetButton) {
handleReset();
} else if (actionEvent.getSource() == stepButton) {
handleStep();
} else if (actionEvent.getSource() == quitButton) {
handleQuit();
} else if (actionEvent.getSource() == runButton) {
if (simulator.isRunning()) {
stop();
} else {
start();
}
}
}
private void handleProgramLoad() {
try {
int retVal = fileChooser.showOpenDialog(MainWindow.this);
if (retVal == JFileChooser.APPROVE_OPTION) {
File f = fileChooser.getSelectedFile();
if (f.canRead()) {
long fileSize = f.length();
if (fileSize > simulator.memorySize()) {
throw new IOException("Program will not fit in available memory.");
} else {
byte[] program = new byte[(int) fileSize];
int i = 0;
FileInputStream fis = new FileInputStream(f);
BufferedInputStream bis = new BufferedInputStream(fis);
DataInputStream dis = new DataInputStream(bis);
while (dis.available() != 0) {
program[i++] = dis.readByte();
}
// Now load the program at the starting address.
simulator.loadProgram(program, PROGRAM_START_ADDRESS);
// Reset (but don't clear memory, naturally)
simulator.reset();
// Update status and clear the screen
console.reset();
uiUpdater.updateUi();
}
}
}
} catch (IOException ex) {
logger.log(Level.SEVERE, "Unable to read file: " + ex.getMessage());
ex.printStackTrace();
} catch (MemoryAccessException ex) {
logger.log(Level.SEVERE, "Memory access error loading program");
ex.printStackTrace();
}
}
private void handleReset() {
if (simulator.isRunning()) {
stop();
}
try {
logger.log(Level.INFO, "Reset requested. Resetting CPU and clearing memory.");
// Reset and clear memory
simulator.reset();
simulator.clearMemory();
// Clear the console.
console.reset();
// Update status.
uiUpdater.updateUi();
} catch (MemoryAccessException ex) {
logger.log(Level.SEVERE, "Exception during simulator reset: " + ex.getMessage());
ex.printStackTrace();
}
}
private void handleStep() {
try {
simulator.step();
// The simulator is lazy about updating the UI for
// performance reasons, so always request an immediate update after stepping manually.
uiUpdater.updateUi();
} catch (SymonException ex) {
logger.log(Level.SEVERE, "Exception during simulator step: " + ex.getMessage());
ex.printStackTrace();
}
}
private void handleQuit() {
// TODO: Clean up and exit properly
System.exit(0);
}
private void stop() {
// Allow step while the simulator is stopped
simulator.requestStop();
if (simulatorThread != null) {
simulatorThread.interrupt();
simulatorThread = null;
}
}
private void start() {
simulatorThread = new Thread(simulator);
simulatorThread.start();
}
public Console getConsole() {
return this.console;
}
public StatusPane getStatusPane() {
return statusPane;
}
public JButton getRunButton() {
return runButton;
}
public JButton getLoadButton() {
return loadButton;
}
public JButton getStepButton() {
return stepButton;
}
public static void main(String args[]) {
try {
// Create the simulated system
Simulator simulator = new Simulator();
// Create the main UI window
MainWindow app = new MainWindow(simulator);
// Pack and display main window
app.pack();
app.setVisible(true);
// Reset the simulator.
simulator.reset();
} catch (MemoryAccessException e) {
e.printStackTrace();
} catch (MemoryRangeException e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,18 @@
package com.loomcom.symon;
import javax.swing.*;
public interface Preferences {
public static final int DEFAULT_PROGRAM_LOAD_ADDRESS = 0x0300;
public static final int DEFAULT_ACIA_ADDRESS = 0xc000;
public JDialog getDialog();
public int getProgramStartAddress();
public int getAciaAddress();
public void updateUi();
}

View File

@ -1,34 +1,28 @@
package com.loomcom.symon;
import java.util.logging.*;
import com.loomcom.symon.devices.*;
import com.loomcom.symon.exceptions.*;
import com.loomcom.symon.ui.UiUpdater;
import com.loomcom.symon.devices.Acia;
import com.loomcom.symon.devices.Memory;
import com.loomcom.symon.exceptions.MemoryAccessException;
import com.loomcom.symon.exceptions.MemoryRangeException;
import com.loomcom.symon.exceptions.SymonException;
import com.loomcom.symon.ui.PreferencesDialog;
import com.loomcom.symon.ui.StatusPanel;
import com.loomcom.symon.ui.Console;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.*;
import java.util.Observable;
import java.util.Observer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
/**
* 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 implements Runnable {
private final static Logger logger = Logger.getLogger(Simulator.class.getName());
private Bus bus;
private Cpu cpu;
private Acia acia;
private Memory ram;
private Memory rom;
private boolean isRunning = false;
private int updatesRequested = 0;
private UiUpdater uiUpdater;
public class Simulator implements ActionListener, Observer {
// Constants used by the simulated system. These define the memory map.
private static final int BUS_BOTTOM = 0x0000;
private static final int BUS_TOP = 0xffff;
@ -40,22 +34,316 @@ public class Simulator implements Runnable {
public static final int ACIA_BASE = 0xc000;
private static final int MAX_REQUESTS_BETWEEN_UPDATES = 25000;
// Since it is very expensive to update the UI with Swing's Event Dispatch Thread, we can't afford
// to refresh the view on every simualted clock cycle. Instead, we will only refresh the view after this
// number of steps when running normally.
private static final int MAX_STEPS_BETWEEN_UPDATES = 15000;
private final static Logger logger = Logger.getLogger(Simulator.class.getName());
// The simulated peripherals
private final Bus bus;
private final Cpu cpu;
private final Acia acia;
private final Memory ram;
private final Memory rom;
// A counter to keep track of the number of UI updates that have been
// requested
private int stepsSinceLastUpdate = 0;
private JFrame mainWindow;
private RunLoop runLoop;
private Console console;
private StatusPanel statusPane;
private JButton runStopButton;
private JButton stepButton;
private JButton resetButton;
// TODO: loadMenuItem seriously violates encapsulation!
// A far better solution would be to extend JMenu and add callback
// methods to enable and disable menus as required.
// Menu Items
private JMenuItem loadMenuItem;
private JFileChooser fileChooser;
private Preferences preferences;
private StringBuffer aciaBuffer;
public Simulator() throws MemoryRangeException {
this.aciaBuffer = new StringBuffer(512);
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!
// TODO: Load this ROM from a file, of course!
this.rom = new Memory(ROM_BASE, ROM_SIZE, false);
bus.addCpu(cpu);
bus.addDevice(acia);
bus.addDevice(ram);
bus.addDevice(rom);
}
public void loadProgram(byte[] program, int startAddress) throws MemoryAccessException {
/**
* Display the main simulator UI.
*/
public void createAndShowUi() {
mainWindow = new JFrame();
mainWindow.setTitle("Symon 6502 Simulator");
mainWindow.setResizable(false);
mainWindow.getContentPane().setLayout(new BorderLayout());
// The Menu
mainWindow.setJMenuBar(createMenuBar());
// UI components used for I/O.
this.console = new com.loomcom.symon.ui.Console();
this.statusPane = new StatusPanel();
// File Chooser
fileChooser = new JFileChooser();
preferences = new PreferencesDialog(mainWindow, true);
// Panel for Console and Buttons
JPanel controlsContainer = new JPanel();
JPanel buttonContainer = new JPanel();
Dimension buttonPanelSize = new Dimension(console.getWidth(), 36);
buttonContainer.setMinimumSize(buttonPanelSize);
buttonContainer.setMaximumSize(buttonPanelSize);
buttonContainer.setPreferredSize(buttonPanelSize);
controlsContainer.setLayout(new BorderLayout());
buttonContainer.setLayout(new FlowLayout());
runStopButton = new JButton("Run");
stepButton = new JButton("Step");
resetButton = new JButton("Reset");
buttonContainer.add(runStopButton);
buttonContainer.add(stepButton);
buttonContainer.add(resetButton);
// Left side - console and buttons
controlsContainer.add(console, BorderLayout.PAGE_START);
controlsContainer.add(buttonContainer, BorderLayout.PAGE_END);
mainWindow.getContentPane().add(controlsContainer, BorderLayout.LINE_START);
mainWindow.getContentPane().add(statusPane, BorderLayout.LINE_END);
runStopButton.addActionListener(this);
stepButton.addActionListener(this);
resetButton.addActionListener(this);
mainWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
console.requestFocus();
mainWindow.pack();
mainWindow.setVisible(true);
}
private JMenuBar createMenuBar() {
JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu("File");
menuBar.add(fileMenu);
loadMenuItem = new JMenuItem("Load Program");
loadMenuItem.setMnemonic(KeyEvent.VK_L);
JMenuItem prefsItem = new JMenuItem("Preferences...");
prefsItem.setMnemonic(KeyEvent.VK_P);
JMenuItem quitItem = new JMenuItem("Quit");
quitItem.setMnemonic(KeyEvent.VK_Q);
loadMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
handleProgramLoad();
}
});
prefsItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
showAndUpdatePreferences();
}
});
quitItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
handleQuit();
}
});
fileMenu.add(loadMenuItem);
fileMenu.add(prefsItem);
fileMenu.add(quitItem);
return menuBar;
}
public void showAndUpdatePreferences() {
preferences.getDialog().setVisible(true);
}
/**
* Receive an ActionEvent from the UI, and act on it.
*/
public void actionPerformed(ActionEvent actionEvent) {
if (actionEvent.getSource() == resetButton) {
handleReset();
} else if (actionEvent.getSource() == stepButton) {
handleStep();
} else if (actionEvent.getSource() == runStopButton) {
if (runLoop != null && runLoop.isRunning()) {
runLoop.requestStop();
runLoop.interrupt();
runLoop = null;
} else {
// Spin up the new run loop
runLoop = new RunLoop();
runLoop.start();
}
}
}
/**
* Display a file chooser prompting the user to load a binary program.
* After the user selects a file, read it in starting at PROGRAM_START_ADDRESS.
*/
private void handleProgramLoad() {
try {
int retVal = fileChooser.showOpenDialog(mainWindow);
if (retVal == JFileChooser.APPROVE_OPTION) {
File f = fileChooser.getSelectedFile();
if (f.canRead()) {
long fileSize = f.length();
if (fileSize > MEMORY_SIZE) {
throw new IOException("Program will not fit in available memory.");
} else {
byte[] program = new byte[(int) fileSize];
int i = 0;
FileInputStream fis = new FileInputStream(f);
BufferedInputStream bis = new BufferedInputStream(fis);
DataInputStream dis = new DataInputStream(bis);
while (dis.available() != 0) {
program[i++] = dis.readByte();
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
console.reset();
}
});
// Now load the program at the starting address.
loadProgram(program, preferences.getProgramStartAddress());
}
}
}
} catch (IOException ex) {
logger.log(Level.SEVERE, "Unable to read file: " + ex.getMessage());
ex.printStackTrace();
} catch (MemoryAccessException ex) {
logger.log(Level.SEVERE, "Memory access error loading program");
ex.printStackTrace();
}
}
private void handleReset() {
if (runLoop != null && runLoop.isRunning()) {
runLoop.requestStop();
runLoop.interrupt();
runLoop = null;
}
try {
logger.log(Level.INFO, "Reset requested. Resetting CPU and clearing memory.");
// Reset and clear memory
cpu.reset();
ram.fill(0x00);
// Clear the console.
console.reset();
// Update status.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
updateConsoleAndStatus(cpu);
}
});
} catch (MemoryAccessException ex) {
logger.log(Level.SEVERE, "Exception during simulator reset: " + ex.getMessage());
ex.printStackTrace();
}
}
/**
* Step once, and immediately refresh the UI.
*/
private void handleStep() {
try {
step();
// The simulator is lazy about updating the UI for performance reasons, so always request an
// immediate update after stepping manually.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
updateConsoleAndStatus(cpu);
}
});
} catch (SymonException ex) {
logger.log(Level.SEVERE, "Exception during simulator step: " + ex.getMessage());
ex.printStackTrace();
}
}
/**
* Handle a request to quit.
*/
private void handleQuit() {
if (runLoop != null && runLoop.isRunning()) {
runLoop.requestStop();
runLoop.interrupt();
}
System.exit(0);
}
/**
* Perform a single step of the simulated system.
*/
private void step() throws MemoryAccessException {
cpu.step();
// Read from the ACIA
while (acia.hasTxChar()) {
aciaBuffer.append((char) acia.txRead());
}
// 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 (stepsSinceLastUpdate++ > MAX_STEPS_BETWEEN_UPDATES) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
updateConsoleAndStatus(cpu);
}
});
stepsSinceLastUpdate = 0;
}
}
/**
* Load a program into memory at the simulatorDidStart address.
*/
private void loadProgram(byte[] program, int startAddress) throws MemoryAccessException {
cpu.setResetVector(startAddress);
int addr = startAddress, i;
@ -65,93 +353,117 @@ public class Simulator implements Runnable {
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 {
// After loading, be sure to reset and
// Reset (but don't clear memory, naturally)
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;
// Immediately update the UI.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
uiUpdater.simulatorDidStart();
updateConsoleAndStatus(cpu);
}
});
try {
while (isRunning && !cpu.getBreakFlag()) {
step();
}
} catch (SymonException ex) {
logger.log(Level.SEVERE, "Exception in main simulator run thread. Exiting run.");
ex.printStackTrace();
}
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 step() throws MemoryAccessException, FifoUnderrunException {
cpu.step();
// 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());
/**
* Update the UI with the latest state of the simulator.
*/
private void updateConsoleAndStatus(Cpu cpu) {
// If we have accumulated any ACIA output in our temporary
// buffer since the last UI update, write it to the console,
// then clear the buffer.
if (aciaBuffer.length() > 0) {
console.print(aciaBuffer.toString());
console.repaint();
aciaBuffer.delete(0, aciaBuffer.length());
}
// 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) {
// Now update the state
statusPane.updateState(cpu);
}
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
// Create the main UI window
Simulator simulator = new Simulator();
simulator.createAndShowUi();
// Reset the simulator.
simulator.handleReset();
} catch (MemoryRangeException e) {
e.printStackTrace();
}
}
});
}
/**
* The configuration has changed. Re-load.
*
* @param observable
* @param o
*/
public void update(Observable observable, Object o) {
// Instance equality should work here, there is only one instance.
if (observable == preferences) {
// TODO: Update system based on state. (i.e., update ACIA address, and raise a dialog if it
// overlaps with anything)
}
}
/**
* The main run thread.
*/
class RunLoop extends Thread {
private boolean isRunning = false;
public boolean isRunning() {
return isRunning;
}
public void requestStop() {
isRunning = false;
}
public void run() {
logger.log(Level.INFO, "Starting main run loop.");
isRunning = true;
SwingUtilities.invokeLater(new Runnable() {
public void run() {
uiUpdater.updateUi();
// Don't allow step while the simulator is running
stepButton.setEnabled(false);
loadMenuItem.setEnabled(false);
// Toggle the state of the run button
runStopButton.setText("Stop");
}
});
updatesRequested = 0;
try {
// TODO: Interrupts - both software and hardware. i.e., jump to address stored in FFFE/FFFF on BRK.
while (isRunning && !cpu.getBreakFlag()) {
step();
}
} catch (SymonException ex) {
logger.log(Level.SEVERE, "Exception in main simulator run thread. Exiting run.");
ex.printStackTrace();
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// Allow step while the simulator is stopped
stepButton.setEnabled(true);
loadMenuItem.setEnabled(true);
runStopButton.setText("Run");
// Update state.
updateConsoleAndStatus(cpu);
}
});
logger.log(Level.INFO, "Exiting main run loop. BREAK=" + cpu.getBreakBit() + "; RUN_FLAG=" + isRunning);
isRunning = false;
}
}
}
}

View File

@ -1,124 +1,132 @@
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.
*
* <p/>
* 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.
* writing / reading.
*/
public class Acia extends Device {
public static final int ACIA_SIZE = 4;
public static final int BUF_LEN = 256;
public static final int ACIA_SIZE = 4;
static final int DATA_REG = 0;
static final int STAT_REG = 1;
static final int CMND_REG = 2;
static final int CTRL_REG = 3;
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;
/**
* Register addresses
*/
private int baseAddress;
/** Registers. These are ignored in the current implementation. */
private int commandRegister;
private int controlRegister;
/**
* 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);
/**
* Read/Write buffers
*/
private int rxChar = 0;
private int txChar = 0;
public Acia(int address) throws MemoryRangeException {
super(address, ACIA_SIZE, "ACIA");
this.baseAddress = address;
}
private boolean rxFull = false;
private boolean txFull = false;
@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.");
public Acia(int address) throws MemoryRangeException {
super(address, ACIA_SIZE, "ACIA");
this.baseAddress = address;
}
}
@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 int read(int address) throws MemoryAccessException {
switch (address) {
case DATA_REG:
return rxRead();
case STAT_REG:
return ((rxFull ? 0x08 : 0x00) |
(txFull ? 0x00 : 0x10));
case CMND_REG:
return commandRegister;
case CTRL_REG:
return controlRegister;
default:
throw new MemoryAccessException("No register.");
}
}
}
@Override
public String toString() {
return "ACIA@" + String.format("%04X", baseAddress);
}
@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.");
}
}
public int rxRead() throws FifoUnderrunException {
return rxBuffer.pop();
}
@Override
public String toString() {
return "ACIA@" + String.format("%04X", baseAddress);
}
public void rxWrite(int data) {
rxBuffer.push(data);
}
public synchronized int rxRead() {
rxFull = false;
return rxChar;
}
public int txRead() throws FifoUnderrunException {
return txBuffer.pop();
}
public synchronized void rxWrite(int data) {
rxFull = true;
rxChar = data;
}
public void txWrite(int data) {
txBuffer.push(data);
}
public synchronized int txRead() {
txFull = false;
return txChar;
}
/**
* @return true if there is character data in the TX register.
*/
public boolean hasTxChar() {
return !txBuffer.isEmpty();
}
public synchronized void txWrite(int data) {
txFull = true;
txChar = data;
}
/**
* @return true if there is character data in the RX register.
*/
public boolean hasRxChar() {
return !rxBuffer.isEmpty();
}
/**
* @return true if there is character data in the TX register.
*/
public boolean hasTxChar() {
return txFull;
}
private void reset() {
txBuffer.reset();
rxBuffer.reset();
}
/**
* @return true if there is character data in the RX register.
*/
public boolean hasRxChar() {
return rxFull;
}
private synchronized void reset() {
txChar = 0;
txFull = false;
rxChar = 0;
rxFull = false;
}
}

View File

@ -7,6 +7,10 @@ import java.awt.event.MouseListener;
import com.grahamedgecombe.jterminal.JTerminal;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.border.Border;
/**
* The Console is a simulated 80 column x 24 row VT-100 terminal attached to
* the ACIA of the system. It provides basic keyboard I/O to Symon.
@ -14,11 +18,14 @@ import com.grahamedgecombe.jterminal.JTerminal;
public class Console extends JTerminal implements KeyListener, MouseListener {
// TODO: Pass in io threads, read and write to ACIA.
public Console() {
super();
addKeyListener(this);
addMouseListener(this);
Border emptyBorder = BorderFactory.createEmptyBorder(5, 5, 5, 0);
Border bevelBorder = BorderFactory.createBevelBorder(BevelBorder.LOWERED);
Border compoundBorder = BorderFactory.createCompoundBorder(emptyBorder, bevelBorder);
this.setBorder(compoundBorder);
}
public void reset() {

View File

@ -0,0 +1,124 @@
package com.loomcom.symon.ui;
import com.loomcom.symon.Preferences;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Observable;
public class PreferencesDialog extends Observable implements Preferences {
private final JDialog dialog;
private JTextField aciaAddressField;
private JTextField programLoadAddressField;
private int programLoadAddress = DEFAULT_PROGRAM_LOAD_ADDRESS;
private int aciaAddress = DEFAULT_ACIA_ADDRESS;
public PreferencesDialog(Frame parent, boolean modal) {
this.dialog = new JDialog(parent, modal);
initComponents();
updateUi();
}
public JDialog getDialog() {
return dialog;
}
private void initComponents() {
dialog.setTitle("Preferences");
Container contents = dialog.getContentPane();
JPanel settingsContainer = new JPanel();
JPanel buttonsContainer = new JPanel();
GridBagLayout layout = new GridBagLayout();
settingsContainer.setLayout(layout);
final JLabel aciaAddressLabel = new JLabel("ACIA Address");
final JLabel programLoadAddressLabel = new JLabel("Program Load Address");
aciaAddressField = new JTextField(8);
programLoadAddressField = new JTextField(8);
aciaAddressLabel.setLabelFor(aciaAddressField);
programLoadAddressLabel.setLabelFor(programLoadAddressField);
GridBagConstraints constraints = new GridBagConstraints();
constraints.insets = new Insets(5, 5, 5, 5);
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 0;
constraints.gridy = 0;
settingsContainer.add(aciaAddressLabel, constraints);
constraints.gridx = 1;
settingsContainer.add(aciaAddressField, constraints);
constraints.gridy = 1;
constraints.gridx = 0;
settingsContainer.add(programLoadAddressLabel, constraints);
constraints.gridx = 1;
settingsContainer.add(programLoadAddressField, constraints);
JButton applyButton = new JButton("Apply");
JButton cancelButton = new JButton("Cancel");
cancelButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
updateUi();
dialog.setVisible(false);
}
});
applyButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
programLoadAddress = hexToInt(programLoadAddressField.getText());
aciaAddress = hexToInt(aciaAddressField.getText());
updateUi();
notifyObservers();
dialog.setVisible(false);
}
});
buttonsContainer.add(applyButton);
buttonsContainer.add(cancelButton);
contents.add(settingsContainer, BorderLayout.PAGE_START);
contents.add(buttonsContainer, BorderLayout.PAGE_END);
dialog.pack();
}
private String intToHex(int i) {
return String.format("%04x", i);
}
private int hexToInt(String s) {
try {
return Integer.parseInt(s, 16);
} catch (NumberFormatException ex) {
// TODO: Handle Displaying error.
return 0;
}
}
public int getProgramStartAddress() {
return programLoadAddress;
}
public int getAciaAddress() {
return aciaAddress;
}
public void updateUi() {
aciaAddressField.setText(intToHex(aciaAddress));
programLoadAddressField.setText(intToHex(programLoadAddress));
}
}

View File

@ -6,12 +6,10 @@ package com.loomcom.symon.ui;
import com.loomcom.symon.Cpu;
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
public class StatusPane extends JPanel {
// The CPU to ask for state information.
private final Cpu cpu;
public class StatusPanel extends JPanel {
private final ImageIcon carryOn;
private final ImageIcon carryOff;
@ -36,27 +34,36 @@ public class StatusPane extends JPanel {
private final JLabel overflowFlagLabel;
private final JLabel negativeFlagLabel;
private JTextField opcodeField;
private JTextField pcField;
private JTextField aField;
private JTextField xField;
private JTextField yField;
// private JTextField stepCountField;
private final JLabel opcodeLabel;
private final JLabel pcLabel;
private final JLabel aLabel;
private final JLabel xLabel;
private final JLabel yLabel;
private final JLabel stepCountLabel;
// private final JLabel stepCountLabel;
private static final int WIDTH = 134;
private static final int HEIGHT = 27;
private static final int WIDTH = 140;
private static final int HEIGHT = 200;
public StatusPane(Cpu cpu) {
public StatusPanel() {
super();
this.cpu = cpu;
Dimension dimensions = new Dimension(WIDTH, HEIGHT);
setMinimumSize(dimensions);
setPreferredSize(dimensions);
setMaximumSize(dimensions);
setLayout(new GridLayout(13,1));
JPanel statusFlagsPanel = new JPanel();
carryOn = new ImageIcon(this.getClass().getResource("images/C_on.png"));
carryOff = new ImageIcon(this.getClass().getResource("images/C_off.png"));
zeroOn = new ImageIcon(this.getClass().getResource("images/Z_on.png"));
@ -81,52 +88,83 @@ public class StatusPane extends JPanel {
overflowFlagLabel = new JLabel(overflowOff, JLabel.CENTER);
negativeFlagLabel = new JLabel(negativeOff, JLabel.CENTER);
Border emptyBorder = BorderFactory.createEmptyBorder(8, 6, 0, 6);
// Create and add register and address labels
opcodeLabel = new JLabel("Instruction");
opcodeLabel.setBorder(emptyBorder);
pcLabel = new JLabel("Program Counter");
pcLabel.setBorder(emptyBorder);
aLabel = new JLabel("Accumulator");
aLabel.setBorder(emptyBorder);
xLabel = new JLabel("X Register");
xLabel.setBorder(emptyBorder);
yLabel = new JLabel("Y Register");
yLabel.setBorder(emptyBorder);
// stepCountLabel = new JLabel("Steps");
// stepCountLabel.setBorder(emptyBorder);
this.opcodeLabel = new JLabel();
this.pcLabel = new JLabel();
this.aLabel = new JLabel();
this.xLabel = new JLabel();
this.yLabel = new JLabel();
this.stepCountLabel = new JLabel();
opcodeField = new JTextField("");
pcField = new JTextField("");
aField = new JTextField("");
xField = new JTextField("");
yField = new JTextField("");
// stepCountField = new JTextField("");
this.opcodeLabel.setMinimumSize(new Dimension(100, 20));
this.pcLabel.setMinimumSize(new Dimension(80, 20));
this.aLabel.setMinimumSize(new Dimension(60, 20));
this.xLabel.setMinimumSize(new Dimension(60, 20));
this.yLabel.setMinimumSize(new Dimension(60, 20));
this.stepCountLabel.setMinimumSize(new Dimension(120, 20));
opcodeField.setEditable(false);
pcField.setEditable(false);
aField.setEditable(false);
xField.setEditable(false);
yField.setEditable(false);
// stepCountField.setEditable(false);
this.opcodeLabel.setPreferredSize(new Dimension(100, 20));
this.pcLabel.setPreferredSize(new Dimension(80, 20));
this.aLabel.setPreferredSize(new Dimension(60, 20));
this.xLabel.setPreferredSize(new Dimension(60, 20));
this.yLabel.setPreferredSize(new Dimension(60, 20));
this.stepCountLabel.setPreferredSize(new Dimension(120, 20));
opcodeLabel.setMinimumSize(new Dimension(100, 20));
pcLabel.setMinimumSize(new Dimension(80, 20));
aLabel.setMinimumSize(new Dimension(60, 20));
xLabel.setMinimumSize(new Dimension(60, 20));
yLabel.setMinimumSize(new Dimension(60, 20));
// stepCountLabel.setMinimumSize(new Dimension(120, 20));
this.setLayout(new FlowLayout());
opcodeLabel.setPreferredSize(new Dimension(100, 20));
pcLabel.setPreferredSize(new Dimension(80, 20));
aLabel.setPreferredSize(new Dimension(60, 20));
xLabel.setPreferredSize(new Dimension(60, 20));
yLabel.setPreferredSize(new Dimension(60, 20));
// stepCountLabel.setPreferredSize(new Dimension(120, 20));
this.add(negativeFlagLabel);
this.add(overflowFlagLabel);
this.add(breakFlagLabel);
this.add(decimalModeFlagLabel);
this.add(irqDisableFlagLabel);
this.add(zeroFlagLabel);
this.add(carryFlagLabel);
statusFlagsPanel.add(negativeFlagLabel);
statusFlagsPanel.add(overflowFlagLabel);
statusFlagsPanel.add(breakFlagLabel);
statusFlagsPanel.add(decimalModeFlagLabel);
statusFlagsPanel.add(irqDisableFlagLabel);
statusFlagsPanel.add(zeroFlagLabel);
statusFlagsPanel.add(carryFlagLabel);
this.add(opcodeLabel);
this.add(pcLabel);
this.add(aLabel);
this.add(xLabel);
this.add(yLabel);
this.add(stepCountLabel);
add(statusFlagsPanel);
add(opcodeLabel);
add(opcodeField);
add(pcLabel);
add(pcField);
add(aLabel);
add(aField);
add(xLabel);
add(xField);
add(yLabel);
add(yField);
// add(stepCountLabel);
// add(stepCountField);
updateState();
setBorder(BorderFactory.createBevelBorder(3));
}
public void updateState() {
/**
* Update the display based on the current state of the CPU.
*
* @param cpu The simulated 6502 CPU.
*/
public void updateState(Cpu cpu) {
// Update the Processor Status Flag display
int state = this.cpu.getProcessorStatus();
int state = cpu.getProcessorStatus();
carryFlagLabel.setIcon(iconForFlag(state, 0));
zeroFlagLabel.setIcon(iconForFlag(state, 1));
@ -137,12 +175,12 @@ public class StatusPane extends JPanel {
negativeFlagLabel.setIcon(iconForFlag(state, 7));
// Update the register and address displays
opcodeLabel.setText(cpu.getOpcodeStatus());
pcLabel.setText(cpu.getProgramCounterStatus());
aLabel.setText(cpu.getARegisterStatus());
xLabel.setText(cpu.getXRegisterStatus());
yLabel.setText(cpu.getYRegisterStatus());
stepCountLabel.setText(Long.toString(cpu.getStepCounter()));
opcodeField.setText(cpu.getOpcodeStatus());
pcField.setText(cpu.getProgramCounterStatus());
aField.setText(cpu.getARegisterStatus());
xField.setText(cpu.getXRegisterStatus());
yField.setText(cpu.getYRegisterStatus());
// stepCountField.setText(Long.toString(cpu.getStepCounter()));
repaint();
}

View File

@ -1,63 +0,0 @@
package com.loomcom.symon.ui;
import com.loomcom.symon.MainWindow;
/**
* Update the console and status display. All direct manipulation of the
* main UI should go through this class. When not called from the Swing
* event dispatch thread, these methods should be called using
* SwingUtilities.invokeLater.
*/
public class UiUpdater {
private final Console console;
private final MainWindow mainWindow;
private final StatusPane statusPane;
private final StringBuffer data;
public UiUpdater(MainWindow mainWindow) {
this.mainWindow = mainWindow;
this.console = mainWindow.getConsole();
this.statusPane = mainWindow.getStatusPane();
this.data = new StringBuffer();
}
public void consoleWrite(int i) {
data.append((char) i);
}
/**
* Callback called by the simulator before exiting its run method.
*/
public void simulatorDidStop() {
mainWindow.getStepButton().setEnabled(true);
mainWindow.getLoadButton().setEnabled(true);
mainWindow.getRunButton().setText("Run");
updateUi();
}
/**
* Callback called by the simulator when entering its run method.
*/
public void simulatorDidStart() {
// Don't allow step while the simulator is running
mainWindow.getStepButton().setEnabled(false);
mainWindow.getLoadButton().setEnabled(false);
// Toggle the state of the run button
mainWindow.getRunButton().setText("Stop");
updateUi();
}
public void updateUi() {
// Update the console with any text
if (data.length() > 0) {
console.getModel().print(data.toString());
console.repaint();
// Clear the buffer
data.delete(0, data.length());
}
// Update the status UI.
statusPane.updateState();
}
}

View File

@ -44,7 +44,7 @@ public class AciaTest {
}
@Test
public void readingBuffersUntilEmptyShouldResetStatus()
public void readingBuffersShouldResetStatus()
throws Exception {
Acia acia = new Acia(0x0000);
@ -59,64 +59,4 @@ public class AciaTest {
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.
}
}

View File

@ -1,33 +0,0 @@
package com.loomcom.symon;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
/**
* Unit test for the j6502 Simulator class.
*/
public class SimulatorTest extends TestCase {
/**
* Create the test case
*
* @param testName name of the test case
*/
public SimulatorTest(String testName) {
super(testName);
}
/**
* @return the suite of tests being tested
*/
public static Test suite() {
return new TestSuite(SimulatorTest.class);
}
/**
* Rigourous Test :-)
*/
public void testSimulator() {
assertTrue(true);
}
}