diff --git a/README.md b/README.md index ef7ad49..10a9f9e 100644 --- a/README.md +++ b/README.md @@ -47,20 +47,33 @@ Maven will build Symon, run unit tests, and produce a jar file in the Symon is meant to be invoked directly from the jar file. To run with Java 1.5 or greater, just type: - $ java -jar symon-0.6-jar-with-dependencies.jar + $ java -jar symon-0.6-snapshot.jar When Symon is running, you should be presented with a simple graphical interface. +### 3.2 ROM images -### 3.2 Loading A Program +The simulator requires a 16KB ROM image loaded at address $C000 to $FFFF to +work properly. Without a ROM in memory, the simulator will not be able to +reset, since the reset vector for the 6502 is located in this address space. -Programs in the form of raw binary object files can be loaded directly -into memory with the "Load" button. +By default, any 16KB file named 'rom.bin' that exists in the same directory +where Symon is launched will be loaded as a ROM image. ROM images can also +be swapped out at run-time with the "Load ROM Image..." in the File menu. -Right now, all programs are loaded starting at addres $0300. After -loading, the simulated CPU's reset vector is loaded with the values -$00, $03, and the CPU is reset. +The "samples" directory contains a ROM image named 'ehbasic.rom', containing +Lee Davison's Enhanced 6502 BASIC. This serves as a good starting point for +exploration. + +### 3.3 Loading A Program + +In addition to ROM images, programs in the form of raw binary object files can +be loaded directly into memory from "Load Program..." in the File menu. + +Programs are loaded starting at addres $0300. After loading the program, the +simulated CPU's reset vector is loaded with the values $00, $03, and the CPU is +reset. There are two very simple sample program in the "samples" directory, for testing. @@ -69,33 +82,23 @@ for testing. - 'hello.prg' will continuously print "Hello, 6502 World!" to the console. -The sample directory also contains a ROM image of Lee Davison's -Ehanced 6502 BASIC. For instructions on loading the rom, please see -the README file in that directory. - -### 3.3 ROM files - -Any 12KB file named 'rom.bin' that exists in the same directory where -Symon is launched will be loaded at address $d000. If the file is -larger than 12KB, loading will fail. This functionality will be -improved in a future release! - ### 3.4 Running After loading a program or ROM image, clicking "Run" will start the simulator -running at address $0300. - +running. ## 4.0 To Do +- Feedback (in the form of dialogs, status bar, etc). + +- Better ROM image handling + - Better debugging tools from the UI, including memory inspection, disassembly, breakpoints, and execution tracing. -- Better ROM loading (and re-loading) - - More accurate timing. -- Interrupt handling! +- Smarter interrupt handling. - UI needs a ton more polish. @@ -109,8 +112,6 @@ running at address $0300. - 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. diff --git a/pom.xml b/pom.xml index 03e1c2b..7c869e5 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ com.grahamedgecombe.jterminal jterminal - 1.0.2.1-loomcom + 1.0.2.2-loomcom diff --git a/samples/echo.asm b/samples/echo.asm index 3ad09a4..8081a31 100644 --- a/samples/echo.asm +++ b/samples/echo.asm @@ -3,7 +3,7 @@ ;; -.alias iobase $8000 +.alias iobase $8800 .alias iostatus [iobase + 1] .alias iocmd [iobase + 2] .alias ioctrl [iobase + 3] @@ -11,7 +11,7 @@ .org $0300 start: cli - lda #$09 + lda #$0b sta iocmd ; Set command status lda #$1a sta ioctrl ; 0 stop bits, 8 bit word, 2400 baud diff --git a/samples/echo.prg b/samples/echo.prg index ab5f1f7..a3e4ead 100644 Binary files a/samples/echo.prg and b/samples/echo.prg differ diff --git a/samples/ehbasic.rom b/samples/ehbasic.rom index 8e5526e..251fc84 100644 Binary files a/samples/ehbasic.rom and b/samples/ehbasic.rom differ diff --git a/samples/ehbasic/min_mon.asm b/samples/ehbasic/min_mon.asm index 693078a..d151a34 100644 --- a/samples/ehbasic/min_mon.asm +++ b/samples/ehbasic/min_mon.asm @@ -37,10 +37,12 @@ RES_vec ; Initialize the ACIA ACIA_init - LDA #$09 - STA ACIAcommand - LDA #$1A ; Set output for 8-N-1 2400 - STA ACIAcontrol + LDA #$00 + STA ACIAstatus ; Soft reset + LDA #$0B + STA ACIAcommand ; Parity disabled, IRQ disabled + LDA #$1E + STA ACIAcontrol ; Set output for 8-N-1 9600 ; set up vectors and interrupt code, copy them to page 2 @@ -77,25 +79,33 @@ LAB_nokey LAB_dowarm JMP LAB_WARM ; do EhBASIC warm start -; byte out to simulated ACIA +; byte out to ACIA ACIAout PHA ; save accumulator -ACIAout_w +@loop LDA ACIAstatus ; Read 6551 status AND #$10 ; Is tx buffer full? - BEQ ACIAout_w ; if not, loop back + BEQ @loop ; if not, loop back PLA ; Otherwise, restore accumulator STA ACIAdata ; write byte to 6551 RTS -; byte in from simulated ACIA - +; +; byte in from ACIA. This subroutine will also force +; all lowercase letters to be uppercase. +; ACIAin LDA ACIAstatus ; Read 6551 status AND #$08 ; BEQ LAB_nobyw ; If rx buffer empty, no byte LDA ACIAdata ; Read byte from 6551 + CMP #'a' ; Is it < 'a'? + BCC @done ; Yes, we're done + CMP #'{' ; Is it >= '{'? + BCS @done ; Yes, we're done + AND #$5f ; Otherwise, mask to uppercase +@done SEC ; Flag byte received RTS @@ -137,16 +147,19 @@ NMI_CODE END_CODE +; sign on string + LAB_mess - .byte $0D,$0A,"6502 EhBASIC [C]old/[W]arm ?",$00 - ; sign on string + .byte $0D,$0A,"Symon 6502 SBC (c) 2012, Seth Morabito" + .byte $0D,$0A,"[C]old/[W]arm ?",$00 + ; system vectors .segment "VECTORS" .org $FFFA - .word 0 ; NMI vector + .word NMI_vec ; NMI vector .word RES_vec ; RESET vector - .word 0 ; IRQ vector + .word IRQ_vec ; IRQ vector diff --git a/samples/hello.asm b/samples/hello.asm index 877a690..9fbd6a1 100644 --- a/samples/hello.asm +++ b/samples/hello.asm @@ -3,7 +3,7 @@ ;; -.alias iobase $8000 +.alias iobase $8800 .alias iostatus [iobase + 1] .alias iocmd [iobase + 2] .alias ioctrl [iobase + 3] @@ -11,7 +11,7 @@ .org $0300 start: cli - lda #$09 + lda #$0b sta iocmd ; Set command status lda #$1a sta ioctrl ; 0 stop bits, 8 bit word, 2400 baud diff --git a/samples/hello.prg b/samples/hello.prg index 1d24a9d..988de4c 100644 Binary files a/samples/hello.prg and b/samples/hello.prg differ diff --git a/src/main/java/com/loomcom/symon/Preferences.java b/src/main/java/com/loomcom/symon/Preferences.java index 7e56593..650cb5f 100644 --- a/src/main/java/com/loomcom/symon/Preferences.java +++ b/src/main/java/com/loomcom/symon/Preferences.java @@ -6,17 +6,17 @@ public interface Preferences { public static final int DEFAULT_PROGRAM_LOAD_ADDRESS = 0x0300; - public static final int DEFAULT_ACIA_ADDRESS = 0xc000; - public static final int DEFAULT_BORDER_WIDTH = 10; + public static final boolean DEFAULT_HALT_ON_BREAK = true; + public JDialog getDialog(); public int getProgramStartAddress(); - public int getAciaAddress(); - public int getBorderWidth(); + public boolean getHaltOnBreak(); + public void updateUi(); } diff --git a/src/main/java/com/loomcom/symon/Simulator.java b/src/main/java/com/loomcom/symon/Simulator.java index 7aeebef..ddca8b0 100644 --- a/src/main/java/com/loomcom/symon/Simulator.java +++ b/src/main/java/com/loomcom/symon/Simulator.java @@ -33,9 +33,9 @@ import com.loomcom.symon.exceptions.SymonException; import com.loomcom.symon.ui.Console; import com.loomcom.symon.ui.PreferencesDialog; import com.loomcom.symon.ui.StatusPanel; -import sun.rmi.rmic.iiop.DirectoryLoader; import javax.swing.*; +import javax.swing.border.EmptyBorder; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -55,7 +55,7 @@ import java.util.logging.Logger; * with a basic 80x25 character display. * */ -public class Simulator implements ActionListener, Observer { +public class Simulator implements Observer { // Constants used by the simulated system. These define the memory map. private static final int BUS_BOTTOM = 0x0000; @@ -75,6 +75,9 @@ public class Simulator implements ActionListener, Observer { private static final int ROM_BASE = 0xC000; private static final int ROM_SIZE = 0x4000; + private static final int DEFAULT_FONT_SIZE = 12; + private static final Font DEFAULT_FONT = new Font(Font.MONOSPACED, Font.PLAIN, DEFAULT_FONT_SIZE); + // 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 simulated clock cycle. Instead, we will only refresh the view after this // number of steps when running normally. @@ -100,25 +103,37 @@ public class Simulator implements ActionListener, Observer { // requested private int stepsSinceLastUpdate = 0; - private JFrame mainWindow; - private RunLoop runLoop; - private Console console; - private StatusPanel statusPane; + /** + * The Main Window is the primary control point for the simulator. + * It is in charge of the menu, and sub-windows. It also shows the + * CPU status at all times. + */ + private JFrame mainWindow; - private JButton runStopButton; - private JButton stepButton; - private JButton resetButton; + /** + * The Console Window is connected to the ACIA's TX and RX lines. + */ + private JFrame consoleWindow; - // The most recently read key code - private char keyBuffer; + /** + * The Trace Window shows the most recent 50,000 CPU states. + */ + private JFrame traceWindow; - // TODO: loadProgramItem seriously violates encapsulation! - // A far better solution would be to extend JMenu and add callback - // methods to enable and disable menus as required. + /** + * The Zero Page Window shows the contents of page 0. + */ + private JFrame zeroPageWindow; - // Menu Items - private JMenuItem loadProgramItem; - private JMenuItem loadRomItem; + private SimulatorMenu menuBar; + + private RunLoop runLoop; + private Console console; + private StatusPanel statusPane; + + private JButton runStopButton; + private JButton stepButton; + private JButton resetButton; private JFileChooser fileChooser; private PreferencesDialog preferences; @@ -154,10 +169,11 @@ public class Simulator implements ActionListener, Observer { mainWindow.getContentPane().setLayout(new BorderLayout()); // The Menu - mainWindow.setJMenuBar(createMenuBar()); + menuBar = new SimulatorMenu(); + mainWindow.setJMenuBar(menuBar); // UI components used for I/O. - this.console = new com.loomcom.symon.ui.Console(); + this.console = new com.loomcom.symon.ui.Console(80, 25, DEFAULT_FONT); this.statusPane = new StatusPanel(); // File Chooser @@ -166,15 +182,11 @@ public class Simulator implements ActionListener, Observer { preferences.addObserver(this); // Panel for Console and Buttons - JPanel controlsContainer = new JPanel(); + JPanel consoleContainer = 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()); + consoleContainer.setLayout(new BorderLayout()); + consoleContainer.setBorder(new EmptyBorder(10, 10, 10, 0)); buttonContainer.setLayout(new FlowLayout()); runStopButton = new JButton("Run"); @@ -186,8 +198,8 @@ public class Simulator implements ActionListener, Observer { buttonContainer.add(resetButton); // Left side - console - controlsContainer.add(console, BorderLayout.PAGE_START); - mainWindow.getContentPane().add(controlsContainer, BorderLayout.LINE_START); + consoleContainer.add(console, BorderLayout.CENTER); + mainWindow.getContentPane().add(consoleContainer, BorderLayout.LINE_START); // Right side - status pane mainWindow.getContentPane().add(statusPane, BorderLayout.LINE_END); @@ -195,9 +207,27 @@ public class Simulator implements ActionListener, Observer { // Bottom - buttons. mainWindow.getContentPane().add(buttonContainer, BorderLayout.PAGE_END); - runStopButton.addActionListener(this); - stepButton.addActionListener(this); - resetButton.addActionListener(this); + runStopButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent actionEvent) { + if (runLoop != null && runLoop.isRunning()) { + handleStop(); + } else { + handleStart(); + } + } + }); + + stepButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent actionEvent) { + handleStep(); + } + }); + + resetButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent actionEvent) { + handleReset(); + } + }); mainWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); @@ -207,78 +237,6 @@ public class Simulator implements ActionListener, Observer { mainWindow.setVisible(true); } - private JMenuBar createMenuBar() { - JMenuBar menuBar = new JMenuBar(); - - JMenu fileMenu = new JMenu("File"); - - menuBar.add(fileMenu); - - loadProgramItem = new JMenuItem("Load Program"); - loadProgramItem.setMnemonic(KeyEvent.VK_L); - - loadRomItem = new JMenuItem("Load ROM..."); - loadRomItem.setMnemonic(KeyEvent.VK_R); - - JMenuItem prefsItem = new JMenuItem("Preferences..."); - prefsItem.setMnemonic(KeyEvent.VK_P); - - JMenuItem quitItem = new JMenuItem("Quit"); - quitItem.setMnemonic(KeyEvent.VK_Q); - - loadProgramItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent actionEvent) { - handleProgramLoad(); - } - }); - - loadRomItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent actionEvent) { - handleRomLoad(); - } - }); - - prefsItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent actionEvent) { - showAndUpdatePreferences(); - } - }); - - quitItem.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent actionEvent) { - handleQuit(); - } - }); - - fileMenu.add(loadProgramItem); - fileMenu.add(loadRomItem); - 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) { - coldReset(); - } else if (actionEvent.getSource() == stepButton) { - handleStep(); - } else if (actionEvent.getSource() == runStopButton) { - if (runLoop != null && runLoop.isRunning()) { - handleStop(); - } else { - handleStart(); - } - } - } - private void handleStart() { // Shift focus to the console. console.requestFocus(); @@ -311,85 +269,10 @@ public class Simulator implements ActionListener, Observer { // TODO: Update memory window, if frame is visible. } - // TODO: Alert user of errors. - private void handleRomLoad() { - try { - int retVal = fileChooser.showOpenDialog(mainWindow); - if (retVal == JFileChooser.APPROVE_OPTION) { - File romFile = fileChooser.getSelectedFile(); - if (romFile.canRead()) { - long fileSize = romFile.length(); - - if (fileSize != ROM_SIZE) { - throw new IOException("ROM file must be exactly " + String.valueOf(fileSize) + " bytes."); - } else { - if (rom != null) { - // Unload the existing ROM image. - bus.removeDevice(rom); - } - // Load the new ROM image - rom = Memory.makeROM(ROM_BASE, ROM_SIZE, romFile); - bus.addDevice(rom); - - logger.log(Level.INFO, "ROM File `" + romFile.getName() + "' loaded at " + - String.format("0x%04X", ROM_BASE)); - } - } - } - } catch (IOException ex) { - logger.log(Level.SEVERE, "Unable to read file: " + ex.getMessage()); - ex.printStackTrace(); - } catch (MemoryRangeException ex) { - logger.log(Level.SEVERE, "Memory range error loading ROM"); - ex.printStackTrace(); - } - } - - /** - * 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. + /* + * Perform a reset. */ - 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 coldReset() { + private void handleReset() { if (runLoop != null && runLoop.isRunning()) { runLoop.requestStop(); runLoop.interrupt(); @@ -412,7 +295,6 @@ public class Simulator implements ActionListener, Observer { }); } catch (MemoryAccessException ex) { logger.log(Level.SEVERE, "Exception during simulator reset: " + ex.getMessage()); - ex.printStackTrace(); } } @@ -429,17 +311,6 @@ public class Simulator implements ActionListener, Observer { } } - /** - * 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. */ @@ -516,7 +387,7 @@ public class Simulator implements ActionListener, Observer { Simulator simulator = new Simulator(); simulator.createAndShowUi(); // Reset the simulator. - simulator.coldReset(); + simulator.handleReset(); } catch (Exception e) { e.printStackTrace(); } @@ -533,8 +404,6 @@ public class Simulator implements ActionListener, Observer { public void update(Observable observable, Object o) { // Instance equality should work here, there is only one instance. if (observable == preferences) { - // TODO: Update ACIA base address if it has changed. - int oldBorderWidth = console.getBorderWidth(); if (oldBorderWidth != preferences.getBorderWidth()) { // Resize the main window if the border width has changed. @@ -544,7 +413,6 @@ public class Simulator implements ActionListener, Observer { } } - /** * The main run thread. */ @@ -569,16 +437,14 @@ public class Simulator implements ActionListener, Observer { console.startListening(); // Don't allow step while the simulator is running stepButton.setEnabled(false); - loadProgramItem.setEnabled(false); - loadRomItem.setEnabled(false); + menuBar.simulatorDidStart(); // Toggle the state of the run button runStopButton.setText("Stop"); } }); try { - // TODO: Interrupts - both software and hardware. i.e., jump to address stored in FFFE/FFFF on BRK. - while (isRunning && !cpu.getBreakFlag()) { + while (isRunning && !(preferences.getHaltOnBreak() && cpu.getBreakFlag())) { step(); } } catch (SymonException ex) { @@ -591,16 +457,235 @@ public class Simulator implements ActionListener, Observer { console.stopListening(); // Allow step while the simulator is stopped stepButton.setEnabled(true); - loadProgramItem.setEnabled(true); - loadRomItem.setEnabled(true); + menuBar.simulatorDidStop(); runStopButton.setText("Run"); // Now update the state statusPane.updateState(cpu); } }); - logger.log(Level.INFO, "Exiting main run loop. BREAK=" + cpu.getBreakBit() + "; RUN_FLAG=" + isRunning); isRunning = false; } } + + class LoadProgramAction extends AbstractAction { + public LoadProgramAction() { + super("Load Program...", null); + putValue(SHORT_DESCRIPTION, "Load a program into memory"); + putValue(MNEMONIC_KEY, KeyEvent.VK_L); + } + + public void actionPerformed(ActionEvent actionEvent) { + // TODO: Error dialogs on failure. + 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 program file: " + ex.getMessage()); + } catch (MemoryAccessException ex) { + logger.log(Level.SEVERE, "Memory access error loading program: " + ex.getMessage()); + } + } + } + + class LoadRomAction extends AbstractAction { + public LoadRomAction() { + super("Load ROM...", null); + putValue(SHORT_DESCRIPTION, "Load a ROM image"); + putValue(MNEMONIC_KEY, KeyEvent.VK_R); + } + + public void actionPerformed(ActionEvent actionEvent) { + // TODO: Error dialogs on failure. + try { + int retVal = fileChooser.showOpenDialog(mainWindow); + if (retVal == JFileChooser.APPROVE_OPTION) { + File romFile = fileChooser.getSelectedFile(); + if (romFile.canRead()) { + long fileSize = romFile.length(); + + if (fileSize != ROM_SIZE) { + throw new IOException("ROM file must be exactly " + String.valueOf(ROM_SIZE) + " bytes."); + } else { + if (rom != null) { + // Unload the existing ROM image. + bus.removeDevice(rom); + } + // Load the new ROM image + rom = Memory.makeROM(ROM_BASE, ROM_SIZE, romFile); + bus.addDevice(rom); + + // Now, reset + cpu.reset(); + + logger.log(Level.INFO, "ROM File `" + romFile.getName() + "' loaded at " + + String.format("0x%04X", ROM_BASE)); + } + } + } + } catch (IOException ex) { + logger.log(Level.SEVERE, "Unable to read ROM file: " + ex.getMessage()); + } catch (MemoryRangeException ex) { + logger.log(Level.SEVERE, "Memory range error while loading ROM file: " + ex.getMessage()); + } catch (MemoryAccessException ex) { + logger.log(Level.SEVERE, "Memory access error while loading ROM file: " + ex.getMessage()); + } + } + } + + class ShowPrefsAction extends AbstractAction { + public ShowPrefsAction() { + super("Preferences...", null); + putValue(SHORT_DESCRIPTION, "Show Preferences Dialog"); + putValue(MNEMONIC_KEY, KeyEvent.VK_P); + } + + public void actionPerformed(ActionEvent actionEvent) { + preferences.getDialog().setVisible(true); + } + } + + class QuitAction extends AbstractAction { + public QuitAction() { + super("Quit", null); + putValue(SHORT_DESCRIPTION, "Exit the Simulator"); + putValue(MNEMONIC_KEY, KeyEvent.VK_Q); + } + + public void actionPerformed(ActionEvent actionEvent) { + if (runLoop != null && runLoop.isRunning()) { + runLoop.requestStop(); + runLoop.interrupt(); + } + System.exit(0); + } + } + + class SetFontAction extends AbstractAction { + private int size; + + public SetFontAction(int size) { + super(Integer.toString(size) + " pt", null); + this.size = size; + putValue(SHORT_DESCRIPTION, "Set font to " + Integer.toString(size) + "pt."); + } + + public void actionPerformed(ActionEvent actionEvent) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + console.setFont(new Font("Monospaced", Font.PLAIN, size)); + mainWindow.pack(); + } + }); + } + } + + class SimulatorMenu extends JMenuBar { + // Menu Items + private JMenuItem loadProgramItem; + private JMenuItem loadRomItem; + + /** + * Create a new SimulatorMenu instance. + */ + public SimulatorMenu() { + initMenu(); + } + + /** + * Disable menu items that should not be available during simulator execution. + */ + public void simulatorDidStart() { + loadProgramItem.setEnabled(false); + loadRomItem.setEnabled(false); + } + + /** + * Enable menu items that should be available while the simulator is stopped. + */ + public void simulatorDidStop() { + loadProgramItem.setEnabled(true); + loadRomItem.setEnabled(true); + } + + private void initMenu() { + /* + * File Menu + */ + + JMenu fileMenu = new JMenu("File"); + + loadProgramItem = new JMenuItem(new LoadProgramAction()); + loadRomItem = new JMenuItem(new LoadRomAction()); + JMenuItem prefsItem = new JMenuItem(new ShowPrefsAction()); + JMenuItem quitItem = new JMenuItem(new QuitAction()); + + fileMenu.add(loadProgramItem); + fileMenu.add(loadRomItem); + fileMenu.add(prefsItem); + fileMenu.add(quitItem); + + add(fileMenu); + + /* + * View Menu + */ + + JMenu viewMenu = new JMenu("View"); + JMenu fontSubMenu = new JMenu("Font Size"); + ButtonGroup group = new ButtonGroup(); + + makeFontSizeMenuItem(10, fontSubMenu, group); + makeFontSizeMenuItem(11, fontSubMenu, group); + makeFontSizeMenuItem(12, fontSubMenu, group); + makeFontSizeMenuItem(13, fontSubMenu, group); + makeFontSizeMenuItem(14, fontSubMenu, group); + makeFontSizeMenuItem(15, fontSubMenu, group); + makeFontSizeMenuItem(16, fontSubMenu, group); + makeFontSizeMenuItem(17, fontSubMenu, group); + makeFontSizeMenuItem(18, fontSubMenu, group); + makeFontSizeMenuItem(19, fontSubMenu, group); + makeFontSizeMenuItem(20, fontSubMenu, group); + + viewMenu.add(fontSubMenu); + add(viewMenu); + } + + private void makeFontSizeMenuItem(int size, JMenu fontSubMenu, ButtonGroup group) { + Action action = new SetFontAction(size); + + JRadioButtonMenuItem item = new JRadioButtonMenuItem(action); + item.setSelected(size == DEFAULT_FONT_SIZE); + fontSubMenu.add(item); + group.add(item); + } + } + } diff --git a/src/main/java/com/loomcom/symon/devices/Acia.java b/src/main/java/com/loomcom/symon/devices/Acia.java index 9630cd8..25f5a8b 100644 --- a/src/main/java/com/loomcom/symon/devices/Acia.java +++ b/src/main/java/com/loomcom/symon/devices/Acia.java @@ -170,8 +170,8 @@ public class Acia extends Device { */ private long calculateBaudRateDelay() { if (baudRate > 0) { - // This is a pretty rough approximation based on 8 bits per character, - // and 1/baudRate per bit. + // TODO: This is a pretty rough approximation based on 8 bits per character, + // and 1/baudRate per bit. It could certainly be improved return (long)((1.0 / baudRate) * 1000000000 * 8); } else { return 0; diff --git a/src/main/java/com/loomcom/symon/ui/Console.java b/src/main/java/com/loomcom/symon/ui/Console.java index 370b697..e8a6623 100644 --- a/src/main/java/com/loomcom/symon/ui/Console.java +++ b/src/main/java/com/loomcom/symon/ui/Console.java @@ -1,5 +1,6 @@ package com.loomcom.symon.ui; +import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; @@ -36,12 +37,8 @@ public class Console extends JTerminal implements KeyListener, MouseListener { private FifoRingBuffer typeAheadBuffer; - public Console() { - this(DEFAULT_COLUMNS, DEFAULT_ROWS); - } - - public Console(int columns, int rows) { - super(new Vt100TerminalModel(columns, rows)); + public Console(int columns, int rows, Font font) { + super(new Vt100TerminalModel(columns, rows), font); // A small type-ahead buffer, as might be found in any real // VT100-style serial terminal. this.typeAheadBuffer = new FifoRingBuffer(128); diff --git a/src/main/java/com/loomcom/symon/ui/PreferencesDialog.java b/src/main/java/com/loomcom/symon/ui/PreferencesDialog.java index 35093f7..69ba253 100644 --- a/src/main/java/com/loomcom/symon/ui/PreferencesDialog.java +++ b/src/main/java/com/loomcom/symon/ui/PreferencesDialog.java @@ -12,13 +12,13 @@ public class PreferencesDialog extends Observable implements Preferences { private final JDialog dialog; - private JTextField aciaAddressField; + private JCheckBox haltOnBreakCheckBox; private JTextField programLoadAddressField; private JTextField borderWidthField; private int programLoadAddress = DEFAULT_PROGRAM_LOAD_ADDRESS; - private int aciaAddress = DEFAULT_ACIA_ADDRESS; private int borderWidth = DEFAULT_BORDER_WIDTH; + private boolean haltOnBreak = DEFAULT_HALT_ON_BREAK; public PreferencesDialog(Frame parent, boolean modal) { this.dialog = new JDialog(parent, modal); @@ -43,15 +43,14 @@ public class PreferencesDialog extends Observable implements Preferences { GridBagLayout layout = new GridBagLayout(); settingsContainer.setLayout(layout); - final JLabel aciaAddressLabel = new JLabel("ACIA Address"); + final JLabel haltOnBreakLabel = new JLabel("Halt on BRK"); final JLabel programLoadAddressLabel = new JLabel("Program Load Address"); final JLabel borderWidthLabel = new JLabel("Console Border Width"); - aciaAddressField = new JTextField(8); + haltOnBreakCheckBox = new JCheckBox(); programLoadAddressField = new JTextField(8); borderWidthField = new JTextField(8); - aciaAddressLabel.setLabelFor(aciaAddressField); programLoadAddressLabel.setLabelFor(programLoadAddressField); borderWidthLabel.setLabelFor(borderWidthField); @@ -61,11 +60,10 @@ public class PreferencesDialog extends Observable implements Preferences { constraints.fill = GridBagConstraints.HORIZONTAL; constraints.gridx = 0; constraints.gridy = 0; - - settingsContainer.add(aciaAddressLabel, constraints); + settingsContainer.add(haltOnBreakLabel, constraints); constraints.gridx = 1; - settingsContainer.add(aciaAddressField, constraints); + settingsContainer.add(haltOnBreakCheckBox, constraints); constraints.gridy = 1; constraints.gridx = 0; @@ -94,8 +92,8 @@ public class PreferencesDialog extends Observable implements Preferences { applyButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent actionEvent) { + haltOnBreak = haltOnBreakCheckBox.isSelected(); programLoadAddress = hexToInt(programLoadAddressField.getText()); - aciaAddress = hexToInt(aciaAddressField.getText()); borderWidth = Integer.parseInt(borderWidthField.getText()); updateUi(); // TODO: Actually check to see if values have changed, don't assume. @@ -118,10 +116,6 @@ public class PreferencesDialog extends Observable implements Preferences { return programLoadAddress; } - public int getAciaAddress() { - return aciaAddress; - } - /** * @return The width of the console border, in pixels. */ @@ -129,8 +123,15 @@ public class PreferencesDialog extends Observable implements Preferences { return borderWidth; } + /** + * @return True if 'halt on break' is desired, false otherwise. + */ + public boolean getHaltOnBreak() { + return haltOnBreak; + } + public void updateUi() { - aciaAddressField.setText(intToHex(aciaAddress)); + haltOnBreakCheckBox.setSelected(haltOnBreak); programLoadAddressField.setText(intToHex(programLoadAddress)); borderWidthField.setText(Integer.toString(borderWidth)); } diff --git a/src/main/java/com/loomcom/symon/ui/StatusPanel.java b/src/main/java/com/loomcom/symon/ui/StatusPanel.java index b0defe7..222d6e9 100644 --- a/src/main/java/com/loomcom/symon/ui/StatusPanel.java +++ b/src/main/java/com/loomcom/symon/ui/StatusPanel.java @@ -26,6 +26,7 @@ public class StatusPanel extends JPanel { private final ImageIcon negativeOn; private final ImageIcon negativeOff; + private final JLabel statusFlagsLabel; private final JLabel carryFlagLabel; private final JLabel zeroFlagLabel; private final JLabel irqDisableFlagLabel; @@ -40,7 +41,6 @@ public class StatusPanel extends JPanel { private JTextField aField; private JTextField xField; private JTextField yField; -// private JTextField stepCountField; private final JLabel opcodeLabel; private final JLabel pcLabel; @@ -48,11 +48,10 @@ public class StatusPanel extends JPanel { private final JLabel aLabel; private final JLabel xLabel; private final JLabel yLabel; -// private final JLabel stepCountLabel; - private static final int EMPTY_BORDER = 5; - private static final Border LABEL_BORDER = BorderFactory.createEmptyBorder(0, 4, 0, 0); - private static final Font LABEL_FONT = new Font("sansserif", Font.PLAIN, 11); + private static final int EMPTY_BORDER = 10; + private static final Border LABEL_BORDER = BorderFactory.createEmptyBorder(0, 5, 0, 0); + private static final Font LABEL_FONT = new Font(Font.SANS_SERIF, Font.BOLD, 12); public StatusPanel() { super(); @@ -63,7 +62,10 @@ public class StatusPanel extends JPanel { setBorder(BorderFactory.createCompoundBorder(emptyBorder, etchedBorder)); - setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + GridBagLayout layout = new GridBagLayout(); + GridBagConstraints constraints = new GridBagConstraints(); + + setLayout(layout); JPanel statusFlagsPanel = new JPanel(); statusFlagsPanel.setAlignmentX(LEFT_ALIGNMENT); @@ -101,37 +103,72 @@ public class StatusPanel extends JPanel { statusFlagsPanel.add(carryFlagLabel); // Create and add register and address labels - opcodeLabel = makeLabel("Instruction"); - pcLabel = makeLabel("Program Counter"); - spLabel = makeLabel("Stack Pointer"); - aLabel = makeLabel("Accumulator"); - xLabel = makeLabel("X Register"); - yLabel = makeLabel("Y Register"); -// stepCountLabel = new JLabel("Steps"); + statusFlagsLabel = makeLabel("Flags"); + opcodeLabel = makeLabel("IR"); + pcLabel = makeLabel("PC"); + spLabel = makeLabel("SP"); + aLabel = makeLabel("A"); + xLabel = makeLabel("X"); + yLabel = makeLabel("Y"); - opcodeField = makeTextField(); - pcField = makeTextField(); - spField = makeTextField(); - aField = makeTextField(); - xField = makeTextField(); - yField = makeTextField(); -// stepCountField = new JTextField(""); + opcodeField = makeTextField(LARGE_TEXT_FIELD_SIZE); + pcField = makeTextField(LARGE_TEXT_FIELD_SIZE); + spField = makeTextField(SMALL_TEXT_FIELD_SIZE); + aField = makeTextField(SMALL_TEXT_FIELD_SIZE); + xField = makeTextField(SMALL_TEXT_FIELD_SIZE); + yField = makeTextField(SMALL_TEXT_FIELD_SIZE); - add(statusFlagsPanel); - add(opcodeLabel); - add(opcodeField); - add(pcLabel); - add(pcField); - add(spLabel); - add(spField); - add(aLabel); - add(aField); - add(xLabel); - add(xField); - add(yLabel); - add(yField); -// add(stepCountLabel); -// add(stepCountField); + constraints.anchor = GridBagConstraints.LINE_START; + constraints.gridwidth = 2; + constraints.gridx = 0; + constraints.gridy = 0; + add(statusFlagsLabel, constraints); + constraints.gridy = 1; + add(statusFlagsPanel, constraints); + + constraints.insets = new Insets(5, 0, 0, 0); + constraints.gridy = 2; + add(opcodeLabel, constraints); + + constraints.insets = new Insets(0, 0, 0, 0); + constraints.gridy = 3; + add(opcodeField, constraints); + + constraints.insets = new Insets(5, 0, 0, 0); + constraints.gridy = 4; + add(pcLabel, constraints); + + constraints.insets = new Insets(0, 0, 0, 0); + constraints.gridy = 5; + add(pcField, constraints); + + constraints.insets = new Insets(5, 0, 0, 0); + constraints.gridwidth = 1; + constraints.gridy = 6; + add(spLabel, constraints); + constraints.gridx = 1; + add(aLabel, constraints); + + constraints.insets = new Insets(0, 0, 0, 0); + constraints.gridx = 0; + constraints.gridy = 7; + add(spField, constraints); + constraints.gridx = 1; + add(aField, constraints); + + constraints.insets = new Insets(5, 0, 0, 0); + constraints.gridx = 0; + constraints.gridy = 8; + add(yLabel, constraints); + constraints.gridx = 1; + add(xLabel, constraints); + + constraints.insets = new Insets(0, 0, 5, 0); + constraints.gridx = 0; + constraints.gridy = 9; + add(xField, constraints); + constraints.gridx = 1; + add(yField, constraints); } /** @@ -158,7 +195,6 @@ public class StatusPanel extends JPanel { aField.setText(cpu.getAccumulatorStatus()); xField.setText(cpu.getXRegisterStatus()); yField.setText(cpu.getYRegisterStatus()); -// stepCountField.setText(Long.toString(cpu.getStepCounter())); repaint(); } @@ -228,10 +264,16 @@ public class StatusPanel extends JPanel { return label; } - private JTextField makeTextField() { + private static final Dimension LARGE_TEXT_FIELD_SIZE = new Dimension(134, 22); + private static final Dimension SMALL_TEXT_FIELD_SIZE = new Dimension(65, 22); + + private JTextField makeTextField(Dimension size) { JTextField textField = new JTextField(""); textField.setAlignmentX(LEFT_ALIGNMENT); textField.setEditable(false); + textField.setMinimumSize(size); + textField.setMaximumSize(size); + textField.setPreferredSize(size); return textField; }