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
+ }
+
+}