diff --git a/README.md b/README.md index 1ea5ed8..b06d3d2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ SYMON - A 6502 System Simulator **NOTE: THIS SOFTWARE IS UNDER ACTIVE DEVELOPMENT. Feedback is welcome!** -**Version:** 0.9.0 +**Version:** 0.9.0.1 **Last Updated:** 29 December, 2013 @@ -22,7 +22,7 @@ in Java. Its core goals are accuracy, ease of development, clear documentation, and extensive test suites for validating correctness. Symon simulates a complete system with a 1 MHz NMOS 6502, 32KB of RAM, -16KB of ROM, a 6551 ACIA, and a 6522 VIA. +16KB of ROM, a 6551 ACIA, a 6522 VIA, and a 6545 CRTC. Symon has extensive unit tests to verify correctness, and fully passes Klaus Dormann's 6502 Functional Test Suite as of version 0.8.2 @@ -42,8 +42,11 @@ for more information about this functional test suite). - `$0000`--`$7FFF`: 32KB RAM - `$8000`--`$800F`: 6522 VIA - `$8800`--`$8803`: 6551 ACIA (Serial Console) + - `$9000`--`$9001`: 6545 CRTC - `$C000`--`$FFFF`: 16KB ROM +The CRT Controller uses memory address `$7000` as the start of Video memory. + ### 3.2 Serial Console and CPU Status ![Serial Console] (https://github.com/sethm/symon/raw/master/screenshots/console.png) @@ -95,9 +98,6 @@ By default, the 40 x 25 character display uses video memory located at base addr This means that the memory from address `$7000` (28672 decimal) to `$73E8` (29672 decimal) is directly mapped to video. -The CRTC emulation is very rough around the edges at the moment. Only the following registers -are supported: - - Address Register (at address `$9000`) - R1: Horizontal Displayed Columns - R6: Vertical Displayed Rows @@ -106,10 +106,21 @@ are supported: - R11: Cursor End Scan Line - R12: Display Start Address (High Byte) - R13: Display Start Address (Low Byte) - - R14: Cursor Position (High Byte) [Read Only] - - R15: Cursor Position (Low Byte) [Read Only] + - R14: Cursor Position (High Byte) + - R15: Cursor Position (Low Byte) -In particular, please note that the Status register is not implemented yet. Still, the feature is ready for some testing and playing with. +Although the simulation is pretty good, there are a few key differences between +the simulated 6545 and a real 6545: + + - The simulated 6545 supports only the straight binary addressing mode of the real 6545, + and not the Row/Column addressing mode. + - The simulated 6545 has full 16 bit addressing, where the real 6545 has only + a 14-bit address bus. + - The simulation is done at a whole-frame level, meaning that lots of + 6545 programming tricks that were achieved by updating the frame address + during vertical and horizontal sync times are not achievable. There is no way + (for example) to change the Display Start Address (R12 and R13) while a + frame is being drawn. For more information on the 6545 CRTC and its programming model, please see the following resources @@ -118,6 +129,17 @@ For more information on the 6545 CRTC and its programming model, please see the - [MOS 6545 Datasheet (PDF)] (http://www.6502.org/users/andre/hwinfo/crtc/crtc.html) +#### 3.6.1 Example BASIC Program to test Video + +This program will fill the video screen with all printable characters. + + 10 J = 0 + 20 FOR I = 28672 TO 29672 + 30 POKE I,J + 40 IF J < 255 THEN J = J + 1 ELSE J = 0 + 50 NEXT I + 60 END + ## 4.0 Usage ### 4.1 Building diff --git a/pom.xml b/pom.xml index 15572c5..1903320 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.loomcom.symon symon jar - 0.9.0 + 0.9.0.1 symon http://www.loomcom.com/symon @@ -44,7 +44,13 @@ com.grahamedgecombe.jterminal jterminal - 1.0.2.2-loomcom + 1.0.2.3-loomcom + + + org.mockito + mockito-all + 1.9.5 + test @@ -125,47 +131,6 @@ - - - org.codehaus.mojo - cobertura-maven-plugin - 2.6 - - - false - - - com.loomcom.symon.* - 90 - 90 - - - - - - com/loomcom/symon/*.class - - - - - - clean - pre-site - - clean - - - - instrument - site - - instrument - cobertura - check - - - - org.codehaus.mojo exec-maven-plugin @@ -183,15 +148,4 @@ - - - - - org.codehaus.mojo - cobertura-maven-plugin - 2.6 - - - - diff --git a/src/main/java/com/loomcom/symon/Bus.java b/src/main/java/com/loomcom/symon/Bus.java index 3ba26d1..0122593 100644 --- a/src/main/java/com/loomcom/symon/Bus.java +++ b/src/main/java/com/loomcom/symon/Bus.java @@ -23,10 +23,12 @@ package com.loomcom.symon; -import java.util.*; +import com.loomcom.symon.devices.Device; +import com.loomcom.symon.exceptions.MemoryAccessException; +import com.loomcom.symon.exceptions.MemoryRangeException; -import com.loomcom.symon.devices.*; -import com.loomcom.symon.exceptions.*; +import java.util.SortedSet; +import java.util.TreeSet; /** * The Bus ties the whole thing together, man. diff --git a/src/main/java/com/loomcom/symon/devices/Crtc.java b/src/main/java/com/loomcom/symon/devices/Crtc.java index d9d1b12..0adc848 100644 --- a/src/main/java/com/loomcom/symon/devices/Crtc.java +++ b/src/main/java/com/loomcom/symon/devices/Crtc.java @@ -17,7 +17,7 @@ public class Crtc extends Device { // Memory locations in the CRTC address space public static final int REGISTER_SELECT = 0; - public static final int REGISTER_WRITE = 1; + public static final int REGISTER_RW = 1; // Registers public static final int HORIZONTAL_DISPLAYED = 1; @@ -90,25 +90,26 @@ public class Crtc extends Device { case REGISTER_SELECT: setCurrentRegister(data); break; - case REGISTER_WRITE: + case REGISTER_RW: writeRegisterValue(data); break; - default: - throw new MemoryAccessException("No such address."); } - - notifyListeners(); } @Override public int read(int address) throws MemoryAccessException { switch (address) { - case REGISTER_SELECT: - return status(); - case REGISTER_WRITE: - return 0; + case REGISTER_RW: + switch (currentRegister) { + case CURSOR_POSITION_LOW: + return cursorPosition & 0xff; + case CURSOR_POSITION_HIGH: + return cursorPosition >> 8; + default: + return 0; + } default: - throw new MemoryAccessException("No such address."); + return 0; } } @@ -121,10 +122,6 @@ public class Crtc extends Device { return memory.getDmaAccess(); } - private int status() { - return 0; - } - public int getHorizontalDisplayed() { return horizontalDisplayed; } @@ -169,7 +166,10 @@ public class Crtc extends Device { this.currentRegister = registerNumber; } - private void writeRegisterValue(int data) { + private void writeRegisterValue(int data) throws MemoryAccessException { + int oldStartAddress = startAddress; + int oldCursorPosition = cursorPosition; + switch (currentRegister) { case HORIZONTAL_DISPLAYED: horizontalDisplayed = data; @@ -213,24 +213,31 @@ public class Crtc extends Device { break; case DISPLAY_START_HIGH: startAddress = ((data & 0xff) << 8) | (startAddress & 0x00ff); - // TODO: bounds checking. break; case DISPLAY_START_LOW: startAddress = ((data & 0xff) | (startAddress & 0xff00)); - // TODO: bounds checking. break; case CURSOR_POSITION_HIGH: cursorPosition = ((data & 0xff) << 8) | (cursorPosition & 0x00ff); - // TODO: bounds checking. break; case CURSOR_POSITION_LOW: - // TODO: bounds checking. cursorPosition = (data & 0xff) | (cursorPosition & 0xff00); break; default: break; } + if (startAddress + pageSize > memory.endAddress()) { + startAddress = oldStartAddress; + throw new MemoryAccessException("Cannot draw screen starting at selected address."); + } + + if (cursorPosition > memory.endAddress()) { + cursorPosition = oldCursorPosition; + throw new MemoryAccessException("Cannot position cursor past end of memory."); + } + + notifyListeners(); } } diff --git a/src/test/java/com/loomcom/symon/CrtcTest.java b/src/test/java/com/loomcom/symon/CrtcTest.java new file mode 100644 index 0000000..3caaf9f --- /dev/null +++ b/src/test/java/com/loomcom/symon/CrtcTest.java @@ -0,0 +1,343 @@ +package com.loomcom.symon; + +import com.loomcom.symon.devices.Crtc; +import com.loomcom.symon.devices.DeviceChangeListener; +import com.loomcom.symon.devices.Memory; +import com.loomcom.symon.exceptions.MemoryAccessException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class CrtcTest { + + Crtc crtc; + + @Mock + DeviceChangeListener changeListener; + @Mock + Memory memory; + + @Before + public void createDevices() throws Exception { + crtc = new Crtc(0x9000, memory); + crtc.registerListener(changeListener); + + when(memory.startAddress()).thenReturn(0); + when(memory.endAddress()).thenReturn(0x7fff); + } + + @Test + public void selectingRegisterDoesNotTriggedrCallback() throws Exception { + crtc.write(0, 1); + + verify(changeListener, never()).deviceStateChanged(); + } + + @Test + public void shouldChangeHorizontalDisplayed() throws Exception { + crtc.write(0, 1); + + crtc.write(1, 80); + assertEquals(80, crtc.getHorizontalDisplayed()); + + crtc.write(1, 40); + assertEquals(40, crtc.getHorizontalDisplayed()); + } + + @Test + public void changeHorizontalDisplayedShouldTriggerCallback() throws Exception { + crtc.write(0, 1); + + crtc.write(1, 80); + verify(changeListener, times(1)).deviceStateChanged(); + } + + + @Test + public void shouldChangeVerticalDisplayed() throws Exception { + crtc.write(0, 6); + + crtc.write(1, 23); + assertEquals(23, crtc.getVerticalDisplayed()); + + crtc.write(1, 26); + assertEquals(26, crtc.getVerticalDisplayed()); + } + + @Test + public void changeVerticalDisplayedShouldTriggerCallback() throws Exception { + crtc.write(0, 6); + + crtc.write(1, 23); + + verify(changeListener, times(1)).deviceStateChanged(); + } + + @Test + public void shouldChangeScanLinesPerRow() throws Exception { + crtc.write(0, 9); // Select register 9, Scan Line + + crtc.write(1, 3); + assertEquals(3, crtc.getScanLinesPerRow()); + + crtc.write(1, 5); + assertEquals(5, crtc.getScanLinesPerRow()); + + crtc.write(1, 9); + assertEquals(9, crtc.getScanLinesPerRow()); + } + + @Test + public void changeScanLinesPerRowShouldTriggerCallback() throws Exception { + crtc.write(0, 9); // Select register 9, Scan Line + + crtc.write(1, 3); + + verify(changeListener, times(1)).deviceStateChanged(); + } + + @Test + public void shouldChangeCursorStartLine() throws Exception { + crtc.write(0, 10); + + crtc.write(1, 0); + assertEquals(0, crtc.getCursorStartLine()); + + crtc.write(1, 1); + assertEquals(1, crtc.getCursorStartLine()); + + crtc.write(1, 4); + assertEquals(4, crtc.getCursorStartLine()); + } + + @Test + public void changeCursorStartLineShouldTriggerCallback() throws Exception { + crtc.write(0, 10); + + crtc.write(1, 5); + + verify(changeListener, times(1)).deviceStateChanged(); + } + + @Test + public void cursorStartLineRegisterChangesCursorVisibility() throws Exception { + crtc.write(0, 10); + + crtc.write(1, 0x00); // Start line 0, no blinking. + + assertEquals(0, crtc.getCursorStartLine()); + assertEquals(0, crtc.getCursorBlinkRate()); + assertTrue(crtc.isCursorEnabled()); + + crtc.write(1, 0x23); // Start line 3, no cursor. + + assertEquals(3, crtc.getCursorStartLine()); + assertEquals(0, crtc.getCursorBlinkRate()); + assertFalse(crtc.isCursorEnabled()); + } + + @Test + public void cursorStartLineRegisterChangesCursorBlinkRate() throws Exception { + crtc.write(0, 10); + + crtc.write(1, 0x40); // Start line 0, 500ms blink delay + + assertEquals(0, crtc.getCursorStartLine()); + assertEquals(500, crtc.getCursorBlinkRate()); + assertTrue(crtc.isCursorEnabled()); + + crtc.write(1, 0x62); // Start line 3, 1000ms blink delay + + assertEquals(2, crtc.getCursorStartLine()); + assertEquals(1000, crtc.getCursorBlinkRate()); + assertTrue(crtc.isCursorEnabled()); + } + + + @Test + public void shouldChangeCursorStopLine() throws Exception { + crtc.write(0, 11); + + crtc.write(1, 0); + assertEquals(0, crtc.getCursorStopLine()); + + crtc.write(1, 3); + assertEquals(3, crtc.getCursorStopLine()); + + crtc.write(1, 6); + assertEquals(6, crtc.getCursorStopLine()); + } + + @Test + public void changeCursorStopLineShouldTriggerCallback() throws Exception { + crtc.write(0, 11); + + crtc.write(1, 7); + + verify(changeListener, times(1)).deviceStateChanged(); + } + + @Test + public void shouldChangeScreenStartAddressHighByte() throws Exception { + crtc.write(0, 12); + + crtc.write(1, 0x00); + assertEquals(0x00, crtc.getStartAddress() >> 8); + + crtc.write(1, 0x30); + assertEquals(0x30, crtc.getStartAddress() >> 8); + + crtc.write(1, 0x6f); + assertEquals(0x6f, crtc.getStartAddress() >> 8); + } + + @Test + public void changeScreenStartAddressHighByteShouldTriggerCallback() throws Exception { + crtc.write(0, 12); + + crtc.write(1, 0x30); + + verify(changeListener, times(1)).deviceStateChanged(); + } + + @Test + public void shouldChangeScreenStartAddressLowByte() throws Exception { + crtc.write(0, 13); + + crtc.write(1, 0x00); + assertEquals(0x00, crtc.getStartAddress() & 0xff); + + crtc.write(1, 0x11); + assertEquals(0x11, crtc.getStartAddress() & 0xff); + + crtc.write(1, 0xff); + assertEquals(0xff, crtc.getStartAddress() & 0xff); + } + + @Test + public void changeScreenStartAddressLowByteShouldTriggerCallback() throws Exception { + crtc.write(0, 13); + + crtc.write(1, 0xff); + + verify(changeListener, times(1)).deviceStateChanged(); + } + + + @Test(expected = MemoryAccessException.class) + public void shouldThrowMemoryAccessExceptionIfPageOutOfRange() throws Exception { + crtc.write(0, 12); + + crtc.write(1, 0x7f); // Page of text will extend beyond 0x7fff + } + + @Test + public void readingStartAddressShouldDoNothing() throws Exception { + crtc.write(0, 12); // High byte + + crtc.write(1, 0x03); + assertEquals(0, crtc.read(1)); + + crtc.write(1, 0x70); + assertEquals(0, crtc.read(1)); + + + crtc.write(0, 13); // Low byte + + crtc.write(1, 0xff); + assertEquals(0, crtc.read(1)); + + crtc.write(1, 0x0e); + assertEquals(0, crtc.read(1)); + } + + + @Test + public void shouldChangeCursorPositionHighByte() throws Exception { + crtc.write(0, 14); // Select register 14 + + crtc.write(1, 0x73); // Set high cursor byte to $73 + assertEquals(0x73, crtc.getCursorPosition() >> 8); + + crtc.write(1, 0x3f); + assertEquals(0x3f, crtc.getCursorPosition() >> 8); + + crtc.write(1, 0x7f); + assertEquals(0x7f, crtc.getCursorPosition() >> 8); + } + + + @Test + public void shouldBeAbleToReadCursorPositionHighByte() throws Exception { + crtc.write(0, 14); + + crtc.write(1, 0x3f); + assertEquals(0x3f, crtc.read(1)); + + crtc.write(1, 0x70); + assertEquals(0x70, crtc.read(1)); + } + + @Test + public void changeCursorPositionHighByteShouldTriggerCallback() throws Exception { + crtc.write(0, 14); + + crtc.write(1, 0x73); + + verify(changeListener, times(1)).deviceStateChanged(); + } + + + @Test + public void shouldChangeCursorPositionLowByte() throws Exception { + crtc.write(0, 15); // Select register 15 + + crtc.write(1, 0x00); // Set low cursor byte to $00 + assertEquals(0x00, crtc.getCursorPosition() & 0xff); + + crtc.write(1, 0x1f); // Set low cursor byte to $1f + assertEquals(0x1f, crtc.getCursorPosition() & 0xff); + + crtc.write(1, 0xff); // Set low cursor byte to $ff + assertEquals(0xff, crtc.getCursorPosition() & 0xff); + } + + @Test + public void shouldBeAbleToReadCursorPositionLowByte() throws Exception { + crtc.write(0, 15); + + crtc.write(1, 0x00); + assertEquals(0x00, crtc.read(1)); + + crtc.write(1, 0x1f); + assertEquals(0x1f, crtc.read(1)); + + crtc.write(1, 0xff); + assertEquals(0xff, crtc.read(1)); + } + + + @Test + public void changeCursorPositionLowByteShouldTriggerCallback() throws Exception { + crtc.write(0, 15); + + crtc.write(1, 0x01); + + verify(changeListener, times(1)).deviceStateChanged(); + } + + @Test(expected = MemoryAccessException.class) + public void shouldThrowMemoryAccessExceptionIfCursorGoesOutOfRange() throws Exception { + crtc.write(0, 14); // Select register 14 + crtc.write(1, 0x80); // Can't position cursor + } + +}