diff --git a/samples/echo.asm b/samples/echo.asm index 759e27a..3ab97e0 100644 --- a/samples/echo.asm +++ b/samples/echo.asm @@ -13,8 +13,8 @@ start: cli lda #$09 sta iocmd ; Set command status - lda #$16 - sta ioctrl ; 0 stop bits, 8 bit word, 300 baud + lda #$1a + sta ioctrl ; 0 stop bits, 8 bit word, 2400 baud ;; Load a character from the keyboard and store it into ;; the accumulator diff --git a/samples/echo.prg b/samples/echo.prg index e0190cd..8ed8ad5 100644 Binary files a/samples/echo.prg and b/samples/echo.prg differ diff --git a/samples/hello.asm b/samples/hello.asm index a1738cc..4916ba7 100644 --- a/samples/hello.asm +++ b/samples/hello.asm @@ -13,20 +13,19 @@ 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 + lda #$1a + sta ioctrl ; 0 stop bits, 8 bit word, 2400 baud -loop: lda string,x - beq start ; If A is 0, loop back and start again +init: ldx #$00 ; Initialize index -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. +loop: lda iostatus + and #$10 ; Is the tx register empty? + beq loop ; No, wait for it to empty + lda string,x ; Otherwise, load the string pointer + beq init ; If the char is 0, re-init + sta iobase ; Otherwise, transmit - inx - jmp loop ; Repeat. + inx ; Increment string pointer. + jmp loop ; Repeat write. string: .byte "Hello, 6502 world! ", 0 diff --git a/samples/hello.prg b/samples/hello.prg index a4f561a..dc26662 100644 Binary files a/samples/hello.prg and b/samples/hello.prg differ diff --git a/src/main/java/com/loomcom/symon/Cpu.java b/src/main/java/com/loomcom/symon/Cpu.java index 10dee96..b024618 100644 --- a/src/main/java/com/loomcom/symon/Cpu.java +++ b/src/main/java/com/loomcom/symon/Cpu.java @@ -1260,7 +1260,7 @@ public class Cpu implements InstructionTable { return String.format("$%04X", addr); } - public String getARegisterStatus() { + public String getAccumulatorStatus() { return String.format("$%02X", a); } @@ -1276,6 +1276,10 @@ public class Cpu implements InstructionTable { return String.format("$%04X", pc); } + public String getStackPointerStatus() { + return String.format("$%02X", sp); + } + /** * Returns a string representing the CPU state. */ @@ -1284,10 +1288,11 @@ public class Cpu implements InstructionTable { StringBuffer sb = new StringBuffer(String.format("$%04X", addr) + " "); sb.append(String.format("%-14s", opcode)); - sb.append("A=" + String.format("$%02X", a) + " "); - sb.append("X=" + String.format("$%02X", x) + " "); - sb.append("Y=" + String.format("$%02X", y) + " "); - sb.append("PC=" + String.format("$%04X", pc) + " "); + sb.append("A=" + getAccumulatorStatus() + " "); + sb.append("X=" + getXRegisterStatus() + " "); + sb.append("Y=" + getYRegisterStatus() + " "); + sb.append("PC=" + getProgramCounterStatus() + " "); + sb.append("SP=" + getStackPointerStatus() + " "); sb.append("P=" + getProcessorStatusString()); return sb.toString(); } diff --git a/src/main/java/com/loomcom/symon/Simulator.java b/src/main/java/com/loomcom/symon/Simulator.java index 704229a..3fb1c86 100644 --- a/src/main/java/com/loomcom/symon/Simulator.java +++ b/src/main/java/com/loomcom/symon/Simulator.java @@ -33,12 +33,21 @@ public class Simulator implements ActionListener, Observer { private static final int ROM_BASE = 0xe000; private static final int ROM_SIZE = 0x2000; // 8 KB - public static final int ACIA_BASE = 0xc000; + private static final int ACIA_BASE = 0xc000; + + // The delay in microseconds between steps. + private static final int DELAY_BETWEEN_STEPS_NS = 1000; // 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 + // to refresh the view on every simulated 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; + // + // Since we're simulating a 1MHz 6502 here, we have a 1 us delay between steps. Setting this to 10000 + // should give us a status update every 10 ms. + // + // TODO: Work around the event dispatch thread with custom painting code instead of relying on Swing. + // + private static final int MAX_STEPS_BETWEEN_UPDATES = 10000; private final static Logger logger = Logger.getLogger(Simulator.class.getName()); @@ -131,13 +140,16 @@ public class Simulator implements ActionListener, Observer { buttonContainer.add(stepButton); buttonContainer.add(resetButton); - // Left side - console and buttons + // Left side - console controlsContainer.add(console, BorderLayout.PAGE_START); - controlsContainer.add(buttonContainer, BorderLayout.PAGE_END); - mainWindow.getContentPane().add(controlsContainer, BorderLayout.LINE_START); + + // Right side - status pane mainWindow.getContentPane().add(statusPane, BorderLayout.LINE_END); + // Bottom - buttons. + mainWindow.getContentPane().add(buttonContainer, BorderLayout.PAGE_END); + runStopButton.addActionListener(this); stepButton.addActionListener(this); resetButton.addActionListener(this); @@ -325,6 +337,8 @@ public class Simulator implements ActionListener, Observer { */ private void step() throws MemoryAccessException { + delayLoop(); + cpu.step(); // Read from the ACIA and immediately update the console if there's @@ -419,6 +433,16 @@ public class Simulator implements ActionListener, Observer { } } + /* + * Perform a busy-loop for DELAY_BETWEEN_STEPS_NS nanoseconds + */ + private void delayLoop() { + long startTime = System.nanoTime(); + long stopTime = startTime + DELAY_BETWEEN_STEPS_NS; + // Busy loop + while (System.nanoTime() < stopTime) { ; } + } + /** * The main run thread. */ diff --git a/src/main/java/com/loomcom/symon/devices/Acia.java b/src/main/java/com/loomcom/symon/devices/Acia.java index 9cb0b32..9630cd8 100644 --- a/src/main/java/com/loomcom/symon/devices/Acia.java +++ b/src/main/java/com/loomcom/symon/devices/Acia.java @@ -2,6 +2,9 @@ package com.loomcom.symon.devices; import com.loomcom.symon.exceptions.*; +import java.util.logging.Level; +import java.util.logging.Logger; + /** * This is a simulation of the MOS 6551 ACIA, with limited @@ -34,14 +37,19 @@ public class Acia extends Device { private boolean overrun = false; + private long lastTxWrite = 0; + private long lastRxRead = 0; + private int baudRate = 0; + private long baudRateDelay = 0; + /** * Read/Write buffers */ private int rxChar = 0; private int txChar = 0; - private boolean rxFull = false; - private boolean txFull = false; + private boolean rxFull = false; + private boolean txEmpty = true; public Acia(int address) throws MemoryRangeException { super(address, ACIA_SIZE, "ACIA"); @@ -54,10 +62,7 @@ public class Acia extends Device { case DATA_REG: return rxRead(); case STAT_REG: - // TODO: Overrun, Parity Error, Framing Error, - // DTR, DSR, and Interrupt flags. - return ((rxFull ? 0x08 : 0x00) | - (txFull ? 0x00 : 0x10)); + return statusReg(); case CMND_REG: return commandRegister; case CTRL_REG: @@ -80,19 +85,137 @@ public class Acia extends Device { commandRegister = data; break; case 3: - controlRegister = data; + setControlRegister(data); break; default: throw new MemoryAccessException("No register."); } } + /** + * Set the control register and associated state. + * + * @param data + */ + public void setControlRegister(int data) { + this.controlRegister = data; + + // If the value of the data is 0, this is a request to reset, + // otherwise it's a control update. + + if (data == 0) { + reset(); + } else { + // Mask the lower three bits to get the baud rate. + int baudSelector = data & 0x0f; + switch (baudSelector) { + case 0: + baudRate = 0; + break; + case 1: + baudRate = 50; + break; + case 2: + baudRate = 75; + break; + case 3: + baudRate = 110; // Real rate is actually 109.92 + break; + case 4: + baudRate = 135; // Real rate is actually 134.58 + break; + case 5: + baudRate = 150; + break; + case 6: + baudRate = 300; + break; + case 7: + baudRate = 600; + break; + case 8: + baudRate = 1200; + break; + case 9: + baudRate = 1800; + break; + case 10: + baudRate = 2400; + break; + case 11: + baudRate = 3600; + break; + case 12: + baudRate = 4800; + break; + case 13: + baudRate = 7200; + break; + case 14: + baudRate = 9600; + break; + case 15: + baudRate = 19200; + break; + } + + // Recalculate the baud rate delay. + baudRateDelay = calculateBaudRateDelay(); + } + } + + /* + * Calculate the delay in nanoseconds between successive read/write operations, based on the + * configured baud rate. + */ + private long calculateBaudRateDelay() { + if (baudRate > 0) { + // This is a pretty rough approximation based on 8 bits per character, + // and 1/baudRate per bit. + return (long)((1.0 / baudRate) * 1000000000 * 8); + } else { + return 0; + } + } + + /** + * @return The simulated baud rate in bps. + */ + public int getBaudRate() { + return baudRate; + } + + /** + * Set the baud rate of the simulated ACIA. + * + * @param rate The baud rate in bps. 0 means no simulated baud rate delay. + */ + public void setBaudRate(int rate) { + this.baudRate = rate; + } + + /** + * @return The contents of the status register. + */ + public int statusReg() { + // TODO: Overrun, Parity Error, Framing Error, DTR, DSR, and Interrupt flags. + int stat = 0; + if (rxFull && System.nanoTime() >= (lastRxRead + baudRateDelay)) { + stat |= 0x08; + } + if (txEmpty && System.nanoTime() >= (lastTxWrite + baudRateDelay)) { + stat |= 0x10; + } + return stat; + } + @Override public String toString() { return "ACIA@" + String.format("%04X", baseAddress); } public synchronized int rxRead() { + lastRxRead = System.nanoTime(); rxFull = false; return rxChar; } @@ -103,12 +226,13 @@ public class Acia extends Device { } public synchronized int txRead() { - txFull = false; + txEmpty = true; return txChar; } public synchronized void txWrite(int data) { - txFull = true; + lastTxWrite = System.nanoTime(); + txEmpty = false; txChar = data; } @@ -116,7 +240,7 @@ public class Acia extends Device { * @return true if there is character data in the TX register. */ public boolean hasTxChar() { - return txFull; + return !txEmpty; } /** @@ -128,7 +252,7 @@ public class Acia extends Device { private synchronized void reset() { txChar = 0; - txFull = false; + txEmpty = true; rxChar = 0; rxFull = false; } diff --git a/src/main/java/com/loomcom/symon/ui/StatusPanel.java b/src/main/java/com/loomcom/symon/ui/StatusPanel.java index e28bd95..b0defe7 100644 --- a/src/main/java/com/loomcom/symon/ui/StatusPanel.java +++ b/src/main/java/com/loomcom/symon/ui/StatusPanel.java @@ -6,7 +6,7 @@ package com.loomcom.symon.ui; import com.loomcom.symon.Cpu; import javax.swing.*; -import javax.swing.border.Border; +import javax.swing.border.*; import java.awt.*; public class StatusPanel extends JPanel { @@ -36,6 +36,7 @@ public class StatusPanel extends JPanel { private JTextField opcodeField; private JTextField pcField; + private JTextField spField; private JTextField aField; private JTextField xField; private JTextField yField; @@ -43,26 +44,29 @@ public class StatusPanel extends JPanel { private final JLabel opcodeLabel; private final JLabel pcLabel; + private final JLabel spLabel; private final JLabel aLabel; private final JLabel xLabel; private final JLabel yLabel; // private final JLabel stepCountLabel; - private static final int WIDTH = 140; - private static final int HEIGHT = 200; + 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); public StatusPanel() { super(); - Dimension dimensions = new Dimension(WIDTH, HEIGHT); + Border emptyBorder = BorderFactory.createEmptyBorder(EMPTY_BORDER, EMPTY_BORDER, + EMPTY_BORDER, EMPTY_BORDER); + Border etchedBorder = BorderFactory.createEtchedBorder(EtchedBorder.LOWERED); - setMinimumSize(dimensions); - setPreferredSize(dimensions); - setMaximumSize(dimensions); + setBorder(BorderFactory.createCompoundBorder(emptyBorder, etchedBorder)); - setLayout(new GridLayout(13,1)); + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); JPanel statusFlagsPanel = new JPanel(); + statusFlagsPanel.setAlignmentX(LEFT_ALIGNMENT); carryOn = new ImageIcon(this.getClass().getResource("images/C_on.png")); carryOff = new ImageIcon(this.getClass().getResource("images/C_off.png")); @@ -88,50 +92,6 @@ public class StatusPanel 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); - - opcodeField = new JTextField(""); - pcField = new JTextField(""); - aField = new JTextField(""); - xField = new JTextField(""); - yField = new JTextField(""); -// stepCountField = new JTextField(""); - - opcodeField.setEditable(false); - pcField.setEditable(false); - aField.setEditable(false); - xField.setEditable(false); - yField.setEditable(false); -// stepCountField.setEditable(false); - - 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)); - - 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)); - statusFlagsPanel.add(negativeFlagLabel); statusFlagsPanel.add(overflowFlagLabel); statusFlagsPanel.add(breakFlagLabel); @@ -140,11 +100,30 @@ public class StatusPanel extends JPanel { statusFlagsPanel.add(zeroFlagLabel); 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"); + + opcodeField = makeTextField(); + pcField = makeTextField(); + spField = makeTextField(); + aField = makeTextField(); + xField = makeTextField(); + yField = makeTextField(); +// stepCountField = new JTextField(""); + add(statusFlagsPanel); add(opcodeLabel); add(opcodeField); add(pcLabel); add(pcField); + add(spLabel); + add(spField); add(aLabel); add(aField); add(xLabel); @@ -153,8 +132,6 @@ public class StatusPanel extends JPanel { add(yField); // add(stepCountLabel); // add(stepCountField); - - setBorder(BorderFactory.createBevelBorder(3)); } /** @@ -177,7 +154,8 @@ public class StatusPanel extends JPanel { // Update the register and address displays opcodeField.setText(cpu.getOpcodeStatus()); pcField.setText(cpu.getProgramCounterStatus()); - aField.setText(cpu.getARegisterStatus()); + spField.setText(cpu.getStackPointerStatus()); + aField.setText(cpu.getAccumulatorStatus()); xField.setText(cpu.getXRegisterStatus()); yField.setText(cpu.getYRegisterStatus()); // stepCountField.setText(Long.toString(cpu.getStepCounter())); @@ -242,4 +220,19 @@ public class StatusPanel extends JPanel { return imageIcon; } + private JLabel makeLabel(String text) { + JLabel label = new JLabel(text); + label.setAlignmentX(LEFT_ALIGNMENT); + label.setBorder(LABEL_BORDER); + label.setFont(LABEL_FONT); + return label; + } + + private JTextField makeTextField() { + JTextField textField = new JTextField(""); + textField.setAlignmentX(LEFT_ALIGNMENT); + textField.setEditable(false); + return textField; + } + } diff --git a/src/test/java/com/loomcom/symon/AciaTest.java b/src/test/java/com/loomcom/symon/AciaTest.java index 72b6425..2319ca5 100644 --- a/src/test/java/com/loomcom/symon/AciaTest.java +++ b/src/test/java/com/loomcom/symon/AciaTest.java @@ -8,55 +8,54 @@ import com.loomcom.symon.exceptions.FifoUnderrunException; import static org.junit.Assert.*; public class AciaTest { + @Test + public void newAciaShouldHaveTxEmptyStatus() throws Exception { + Acia acia = new Acia(0x000); - @Test - public void newAciaShouldHaveTxEmptyStatus() throws Exception { - Acia acia = new Acia(0x000); + assertEquals(0x10, acia.read(0x0001)); + } - assertEquals(0x10, acia.read(0x0001)); - } + @Test + public void aciaShouldHaveTxEmptyStatusOffIfTxHasData() throws Exception { + Acia acia = new Acia(0x000); - @Test - public void aciaShouldHaveTxEmptyStatusOffIfTxHasData() throws Exception { - Acia acia = new Acia(0x000); + acia.txWrite('a'); + assertEquals(0x00, acia.read(0x0001)); + } - acia.txWrite('a'); - assertEquals(0x00, acia.read(0x0001)); - } + @Test + public void aciaShouldHaveRxFullStatusOffIfRxHasData() throws Exception { + Acia acia = new Acia(0x000); - @Test - public void aciaShouldHaveRxFullStatusOffIfRxHasData() throws Exception { - Acia acia = new Acia(0x000); + acia.rxWrite('a'); + assertEquals(0x18, acia.read(0x0001)); + } - acia.rxWrite('a'); - assertEquals(0x18, acia.read(0x0001)); - } + @Test + public void aciaShouldHaveTxEmptyAndRxFullStatusOffIfRxAndTxHaveData() + throws Exception { + Acia acia = new Acia(0x000); - @Test - public void aciaShouldHaveTxEmptyAndRxFullStatusOffIfRxAndTxHaveData() - throws Exception { - Acia acia = new Acia(0x000); + acia.rxWrite('a'); + acia.txWrite('b'); - acia.rxWrite('a'); - acia.txWrite('b'); + assertEquals(0x08, acia.read(0x0001)); + } - assertEquals(0x08, acia.read(0x0001)); - } + @Test + public void readingBuffersShouldResetStatus() + throws Exception { + Acia acia = new Acia(0x0000); - @Test - public void readingBuffersShouldResetStatus() - throws Exception { - Acia acia = new Acia(0x0000); + acia.rxWrite('a'); + acia.txWrite('b'); - acia.rxWrite('a'); - acia.txWrite('b'); - assertEquals(0x08, acia.read(0x0001)); + assertEquals(0x08, acia.read(0x0001)); - assertEquals('a', acia.rxRead()); - assertEquals('b', acia.txRead()); - - assertEquals(0x10, acia.read(0x0001)); - } + assertEquals('a', acia.rxRead()); + assertEquals('b', acia.txRead()); + assertEquals(0x10, acia.read(0x0001)); + } }