mirror of
https://github.com/badvision/jace.git
synced 2024-09-15 21:55:10 +00:00
commit
f34ba40ff0
@ -31,6 +31,7 @@ import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
public class ConfigurationUIController {
|
||||
@ -221,9 +222,9 @@ public class ConfigurationUIController {
|
||||
return Optional.of(row);
|
||||
}
|
||||
|
||||
private void editKeyboardShortcut(ConfigNode node, String actionName, Text widget) {
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
// private void editKeyboardShortcut(ConfigNode node, String actionName, Text widget) {
|
||||
// throw new UnsupportedOperationException("Not supported yet.");
|
||||
// }
|
||||
|
||||
@SuppressWarnings("all")
|
||||
private Node buildEditField(ConfigNode node, String settingName, Serializable value) {
|
||||
@ -247,13 +248,40 @@ public class ConfigurationUIController {
|
||||
return buildTextField(node, settingName, value, null);
|
||||
}
|
||||
} else if (type.equals(File.class)) {
|
||||
// TODO: Add file support!
|
||||
return buildFileField(node, settingName, value);
|
||||
} else if (ISelection.class.isAssignableFrom(type)) {
|
||||
return buildDynamicSelectComponent(node, settingName, value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// NOTE: This was written but not tested/used currently. Test before using!
|
||||
private Node buildFileField(ConfigNode node, String settingName, Serializable value) {
|
||||
// Create a label that shows the name of the file and lets you select a file when the label is clicked.
|
||||
HBox hbox = new HBox();
|
||||
Label label = new Label(value == null ? "" : ((File) value).getName());
|
||||
label.setMinWidth(150.0);
|
||||
label.getStyleClass().add("setting-file-label");
|
||||
label.setOnMouseClicked((e) -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
File file = fileChooser.showOpenDialog(label.getScene().getWindow());
|
||||
if (file != null) {
|
||||
node.setFieldValue(settingName, file);
|
||||
label.setText(file.getName());
|
||||
}
|
||||
});
|
||||
hbox.getChildren().add(label);
|
||||
// Add a button that lets you clear the file selection.
|
||||
Label clearButton = new Label("Clear");
|
||||
clearButton.getStyleClass().add("setting-file-clear");
|
||||
clearButton.setOnMouseClicked((e) -> {
|
||||
node.setFieldValue(settingName, null);
|
||||
label.setText("");
|
||||
});
|
||||
return hbox;
|
||||
|
||||
}
|
||||
|
||||
private Node buildTextField(ConfigNode node, String settingName, Serializable value, String validationPattern) {
|
||||
TextField widget = new TextField(String.valueOf(value));
|
||||
widget.textProperty().addListener((e) -> node.setFieldValue(settingName, widget.getText()));
|
||||
|
103
src/main/java/jace/hardware/Votrax.java
Normal file
103
src/main/java/jace/hardware/Votrax.java
Normal file
@ -0,0 +1,103 @@
|
||||
package jace.hardware;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class Votrax {
|
||||
// This is a speech synthesizer based on the Votrax SC-02
|
||||
// There are 2 sound generators, one for the voice and one for the noise
|
||||
// The voice generator is a saw-tooth wave generator at a frequency determined by the voice frequency register
|
||||
// The noise generator is a pseudo-random noise generator
|
||||
|
||||
// The Votrax has 5 filters that can be applied to the voice generator, controlled by a filter frequency register
|
||||
// The fifth filter takes both the voice and noise generators as input, but other filters only take the voice generator
|
||||
// There is also a final high-pass filter that can be applied to the output of the voice and noise generators
|
||||
|
||||
// There is a phoneme register which controls the phoneme to be spoken (0-63)
|
||||
// There is a duration register which controls the duration of the phoneme (0-3)
|
||||
// There is a rate register which controls the rate of speech (0-15)
|
||||
// There is an inflection register which controls the inflection of the voice (0-15)
|
||||
|
||||
// For each phoneme there are 8 bytes that control the filters and sound generator levels
|
||||
byte[] phonemeData = new byte[64 * 8];
|
||||
// Phoneme chart:
|
||||
// 00: PA (pause)
|
||||
// 01: E (mEEt)
|
||||
// 02: E1 (bEnt)
|
||||
// 03: Y (bEfore)
|
||||
// 04: Y1 (Year)
|
||||
// 05: AY (plEAse)
|
||||
// 06: IE (anY)
|
||||
// 07: I (sIx)
|
||||
// 08: A (mAde)
|
||||
// 09: A1 (cAre)
|
||||
// 0a: EH (nEst)
|
||||
// 0b: EH1 (bElt)
|
||||
// 0c: AE (dAd)
|
||||
// 0d: AE1 (After)
|
||||
// 0e: AH (gOt)
|
||||
// 0f: AH1 (fAther)
|
||||
// 10: AW (Office)
|
||||
// 11: O (stOre)
|
||||
// 12: OU (bOAt)
|
||||
// 13: OO (lOOk)
|
||||
// 14: IU (yOU)
|
||||
// 15: IU1 (cOUld)
|
||||
// 16: U (tUne)
|
||||
// 17: U1 (cartOOn)
|
||||
// 18: UH (wOnder)
|
||||
// 19: UH1 (lOve)
|
||||
// 1a: UH2 (whAt)
|
||||
// 1b: UH3 (nUt)
|
||||
// 1c: ER (bIRd)
|
||||
// 1d: R (Roof)
|
||||
// 1e: R1 (Rug)
|
||||
// 1f: R2 (mutteR -- German)
|
||||
// 20: L (Lift)
|
||||
// 21: L1 (pLay)
|
||||
// 22: LF (faLL)
|
||||
// 23: W (Water)
|
||||
// 24: B (Bag)
|
||||
// 25: D (paiD)
|
||||
// 26: KV (taG)
|
||||
// 27: P (Pen)
|
||||
// 28: T (Tart)
|
||||
// 29: K (Kit)
|
||||
// 2a: HV - Hold Vocal
|
||||
// 2b: HVC - Hold Vocal Closure
|
||||
// 2c: HF - (Heart)
|
||||
// 2d: HFC - Hold Frictave Closure
|
||||
// 2e: HN - Hold Nasal
|
||||
// 2f: Z (Zero)
|
||||
// 30: S (Same)
|
||||
// 31: J (meaSure)
|
||||
// 32: SCH (SHip)
|
||||
// 33: V (Very)
|
||||
// 34: F (Four)
|
||||
// 35: THV (THere)
|
||||
// 36: TH (wiTH)
|
||||
// 37: M (More)
|
||||
// 38: N (NiNe)
|
||||
// 39: NG (raNG)
|
||||
// 3a: :A (mAErchen -- German)
|
||||
// 3b: :OH (lOwe - French)
|
||||
// 3c: :U (fUEnf -- German)
|
||||
// 3d: :UH (menU -- French)
|
||||
// 3e: E2 (bittE -- German)
|
||||
// 3f: LB (Lube)
|
||||
|
||||
public void loadPhonemeData() {
|
||||
InputStream romFile = Votrax.class.getResourceAsStream("/jace/data/sc01a.bin");
|
||||
if (romFile == null) {
|
||||
throw new RuntimeException("Cannot find Votrax SC-01A ROM");
|
||||
}
|
||||
// Load into phonemeData
|
||||
try {
|
||||
if (romFile.read(phonemeData) != phonemeData.length) {
|
||||
throw new RuntimeException("Bad Votrax SC-01A ROM size");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException("Error loading Votrax SC-01A ROM", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -152,7 +152,6 @@ public class DirectoryNode extends DiskNode implements FileFilter {
|
||||
end = start + ENTRIES_PER_BLOCK;
|
||||
}
|
||||
for (int i = start; i < end && i < directoryEntries.size(); i++, offset += FILE_ENTRY_SIZE) {
|
||||
// TODO: Add any parts that are not file entries.
|
||||
// System.out.println("Entry "+i+": "+children.get(i).getName()+"; offset "+offset);
|
||||
generateFileEntry(buffer, offset, i);
|
||||
}
|
||||
|
BIN
src/main/resources/jace/data/sc01a.bin
Normal file
BIN
src/main/resources/jace/data/sc01a.bin
Normal file
Binary file not shown.
@ -13,4 +13,4 @@ public abstract class AbstractFXTest {
|
||||
Platform.startup(() -> {});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
44
src/test/java/jace/ProgramException.java
Normal file
44
src/test/java/jace/ProgramException.java
Normal file
@ -0,0 +1,44 @@
|
||||
package jace;
|
||||
|
||||
import jace.apple2e.MOS65C02;
|
||||
|
||||
public class ProgramException extends Exception {
|
||||
int breakpointNumber;
|
||||
String processorStats;
|
||||
String programLocation;
|
||||
public ProgramException(String message, int breakpointNumber) {
|
||||
super(message.replaceAll("<<.*>>", ""));
|
||||
this.breakpointNumber = breakpointNumber;
|
||||
this.processorStats = Emulator.withComputer(c-> ((MOS65C02) c.getCpu()).getState(), "N/A");
|
||||
// Look for a string pattern <<programLocation>> in the message and extract if found
|
||||
int start = message.indexOf("<<");
|
||||
if (start != -1) {
|
||||
int end = message.indexOf(">>", start);
|
||||
if (end != -1) {
|
||||
this.programLocation = message.substring(start + 2, end);
|
||||
}
|
||||
} else {
|
||||
this.programLocation = "N/A";
|
||||
}
|
||||
}
|
||||
public int getBreakpointNumber() {
|
||||
return breakpointNumber;
|
||||
}
|
||||
public String getProcessorStats() {
|
||||
return processorStats;
|
||||
}
|
||||
public String getProgramLocation() {
|
||||
return programLocation;
|
||||
}
|
||||
public String getMessage() {
|
||||
String message = super.getMessage();
|
||||
if (getBreakpointNumber() >= 0) {
|
||||
message += " at breakpoint " + getBreakpointNumber();
|
||||
}
|
||||
message += " \nStats: " + getProcessorStats();
|
||||
if (getProgramLocation() != null) {
|
||||
message += " \n at " + getProgramLocation();
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
465
src/test/java/jace/TestProgram.java
Normal file
465
src/test/java/jace/TestProgram.java
Normal file
@ -0,0 +1,465 @@
|
||||
package jace;
|
||||
|
||||
import static jace.TestUtils.runAssemblyCode;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jace.apple2e.Full65C02Test;
|
||||
import jace.apple2e.MOS65C02;
|
||||
import jace.core.Computer;
|
||||
|
||||
public class TestProgram {
|
||||
// Tests could be run in any order so it is really important that all registers/flags are preserved!
|
||||
public static enum Flag {
|
||||
CARRY_SET("BCS +", "Carry should be set"),
|
||||
CARRY_CLEAR("BCC +", "Carry should be clear"),
|
||||
ZERO_SET("BEQ +", "Zero should be set"),
|
||||
IS_ZERO("BEQ +", "Zero should be clear"),
|
||||
ZERO_CLEAR("BNE +", "Zero should be clear"),
|
||||
NOT_ZERO("BNE +", "Zero should be clear"),
|
||||
NEGATIVE("BMI +", "Negative should be set"),
|
||||
POSITIVE("BPL +", "Negative should be clear"),
|
||||
OVERFLOW_SET("BVS +", "Overflow should be set"),
|
||||
OVERFLOW_CLEAR("BVC +", "Overflow should be clear"),
|
||||
DECIMAL_SET("""
|
||||
PHP
|
||||
PHA
|
||||
PHP
|
||||
PLA
|
||||
BIT #%00001000
|
||||
BEQ ++
|
||||
PLA
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""", "Decimal should be set"),
|
||||
DECIMAL_CLEAR("""
|
||||
PHP
|
||||
PHA
|
||||
PHP
|
||||
PLA
|
||||
BIT #%00001000
|
||||
BNE ++
|
||||
PLA
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""", "Decimal should be clear"),
|
||||
INTERRUPT_SET("""
|
||||
PHP
|
||||
PHA
|
||||
PHP
|
||||
PLA
|
||||
BIT #%00000100
|
||||
BEQ ++
|
||||
PLA
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""", "Interrupt should be set"),
|
||||
INTERRUPT_CLEAR("""
|
||||
PHP
|
||||
PHA
|
||||
PHP
|
||||
PLA
|
||||
BIT #%00000100
|
||||
BNE ++
|
||||
PLA
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""", "Interrupt should be clear"),;
|
||||
String code;
|
||||
String condition;
|
||||
Flag(String code, String condition) {
|
||||
this.code = code;
|
||||
this.condition = condition;
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<String> lines = new ArrayList<>();
|
||||
Consumer<Byte> timerHandler;
|
||||
Consumer<Byte> tickCountHandler;
|
||||
Consumer<Byte> errorHandler;
|
||||
Consumer<Byte> stopHandler;
|
||||
Consumer<Byte> progressHandler;
|
||||
Consumer<Byte> traceHandler;
|
||||
|
||||
public TestProgram() {
|
||||
lines.add(TestProgram.UNIT_TEST_MACROS);
|
||||
}
|
||||
|
||||
public TestProgram(String line1) {
|
||||
this();
|
||||
lines.add(line1);
|
||||
}
|
||||
|
||||
public TestProgram(String line1, int tickCount) {
|
||||
this();
|
||||
assertTimed(line1, tickCount);
|
||||
}
|
||||
|
||||
int tickCount = 0;
|
||||
|
||||
int timerStart = 0;
|
||||
int timerLastMark = 0;
|
||||
int timerEnd = 0;
|
||||
int timerLastEllapsed = 0;
|
||||
int lastBreakpoint = -1;
|
||||
int maxTicks = 10000;
|
||||
boolean programCompleted = false;
|
||||
boolean programReportedError = false;
|
||||
boolean programRunning = false;
|
||||
List<String> errors = new ArrayList<>();
|
||||
List<Integer> timings = new ArrayList<>();
|
||||
|
||||
ProgramException lastError = null;
|
||||
public static String UNIT_TEST_MACROS = """
|
||||
!cpu 65c02
|
||||
!macro extendedOp .code, .val {!byte $FC, .code, .val}
|
||||
!macro startTimer {+extendedOp $10, $80}
|
||||
!macro markTimer {+extendedOp $10, $81}
|
||||
!macro stopTimer {+extendedOp $10, $82}
|
||||
!macro assertTicks .ticks {+extendedOp $11, .ticks}
|
||||
!macro stop .p1, .p2 {
|
||||
+extendedOp .p1, .p2
|
||||
+extendedOp $13, $ff
|
||||
+traceOff
|
||||
-
|
||||
JMP -
|
||||
}
|
||||
!macro throwError .errorCode {+stop $12, .errorCode}
|
||||
!macro recordError .errorCode {+extendedOp $12, .errorCode}
|
||||
!macro success {+stop $14, $ff}
|
||||
!macro breakpoint .num {+extendedOp $14, .num}
|
||||
!macro traceOn {+extendedOp $15, $01}
|
||||
!macro traceOff {+extendedOp $15, $00}
|
||||
!macro resetRegs {
|
||||
LDA #0
|
||||
LDX #0
|
||||
LDY #0
|
||||
PHA
|
||||
PLP
|
||||
TXS
|
||||
PHP
|
||||
}
|
||||
+resetRegs
|
||||
""";
|
||||
public static String INDENT = " ";
|
||||
|
||||
public void attach() {
|
||||
timerHandler = this::handleTimer;
|
||||
tickCountHandler = this::countAndCompareTicks;
|
||||
errorHandler = this::recordError;
|
||||
stopHandler = b->stop();
|
||||
progressHandler = this::recordProgress;
|
||||
traceHandler = this::handleTrace;
|
||||
|
||||
Emulator.withComputer(c-> {
|
||||
MOS65C02 cpu = (MOS65C02) c.getCpu();
|
||||
cpu.registerExtendedCommandHandler(0x10, timerHandler);
|
||||
cpu.registerExtendedCommandHandler(0x11, tickCountHandler);
|
||||
cpu.registerExtendedCommandHandler(0x12, errorHandler);
|
||||
cpu.registerExtendedCommandHandler(0x13, stopHandler);
|
||||
cpu.registerExtendedCommandHandler(0x14, progressHandler);
|
||||
cpu.registerExtendedCommandHandler(0x15, traceHandler);
|
||||
});
|
||||
}
|
||||
|
||||
public void detach() {
|
||||
Emulator.withComputer(c-> {
|
||||
MOS65C02 cpu = (MOS65C02) c.getCpu();
|
||||
cpu.unregisterExtendedCommandHandler(timerHandler);
|
||||
cpu.unregisterExtendedCommandHandler(tickCountHandler);
|
||||
cpu.unregisterExtendedCommandHandler(errorHandler);
|
||||
cpu.unregisterExtendedCommandHandler(stopHandler);
|
||||
cpu.unregisterExtendedCommandHandler(progressHandler);
|
||||
cpu.unregisterExtendedCommandHandler(traceHandler);
|
||||
});
|
||||
}
|
||||
|
||||
private void handleTimer(byte val) {
|
||||
switch (val) {
|
||||
case (byte)0x80:
|
||||
timerStart = tickCount;
|
||||
timerLastMark = tickCount;
|
||||
break;
|
||||
case (byte)0x82:
|
||||
timerEnd = tickCount;
|
||||
// Fall through
|
||||
case (byte)0x81:
|
||||
// Don't count the time spent on the timer commands!
|
||||
timerLastEllapsed = (tickCount - timerLastMark) - 4;
|
||||
timerLastMark = tickCount;
|
||||
break;
|
||||
default:
|
||||
lastError = new ProgramException("Unknown timer command %s".formatted(val), lastBreakpoint);
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void countAndCompareTicks(byte val) {
|
||||
int expectedTickCountNum = val;
|
||||
int expectedTickCount = timings.get(expectedTickCountNum);
|
||||
String errorMessage = lastError != null ? lastError.getProgramLocation() : "";
|
||||
if (timerLastEllapsed != expectedTickCount) {
|
||||
lastError = new ProgramException("Expected %s ticks, instead counted %s <<%s>>".formatted(expectedTickCount, timerLastEllapsed, errorMessage), lastBreakpoint);
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void recordError(byte v) {
|
||||
int val = v & 0x0ff;
|
||||
|
||||
if (val >= 0 && val < errors.size()) {
|
||||
lastError = new ProgramException(errors.get(val), lastBreakpoint);
|
||||
} else if (val == 255) {
|
||||
lastError = null;
|
||||
} else {
|
||||
lastError = new ProgramException("Error %s".formatted(val), lastBreakpoint);
|
||||
}
|
||||
}
|
||||
|
||||
public TestProgram defineError(String error) {
|
||||
errors.add(error);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
programReportedError = lastError != null;
|
||||
programRunning = false;
|
||||
}
|
||||
|
||||
private void recordProgress(byte val) {
|
||||
if (val == (byte)0xff) {
|
||||
programCompleted = true;
|
||||
return;
|
||||
} else {
|
||||
lastBreakpoint = val;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTrace(byte val) {
|
||||
if (val == (byte)0x01) {
|
||||
System.out.println("Trace on");
|
||||
Full65C02Test.cpu.setTraceEnabled(true);
|
||||
} else {
|
||||
System.out.println("Trace off");
|
||||
Full65C02Test.cpu.setTraceEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
public TestProgram assertTimed(String line, int ticks) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
int errNum = errors.size();
|
||||
errors.add("Expected %s ticks for %s <<%s>>".formatted(ticks, line, caller));
|
||||
int timingNum = timings.size();
|
||||
timings.add(ticks);
|
||||
lines.add(INDENT+"+startTimer");
|
||||
lines.add(line);
|
||||
lines.add(INDENT+"+markTimer");
|
||||
lines.add(INDENT+"+recordError %s".formatted(errNum));
|
||||
lines.add(INDENT+"+assertTicks %s ; Check for %s cycles".formatted(timingNum, ticks));
|
||||
lines.add(INDENT+"+recordError %s".formatted(255));
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestProgram add(String line) {
|
||||
lines.add(line);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestProgram assertFlags(TestProgram.Flag... flags) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
for (TestProgram.Flag flag : flags)
|
||||
_test(TestProgram.INDENT + flag.code, flag.condition + "<<" + caller + ">>");
|
||||
return this;
|
||||
}
|
||||
|
||||
/* Test the A register for a specific value */
|
||||
public TestProgram assertA(int val) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
String condition = "A != %s <<%s>>".formatted(Integer.toHexString(val), caller);
|
||||
_test("""
|
||||
PHP
|
||||
CMP #%s
|
||||
BNE ++
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""".formatted(val), condition + " in " + caller);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* Test X register for a specific value */
|
||||
public TestProgram assertX(int val) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
String condition = "X != %s <<%s>>".formatted(Integer.toHexString(val), caller);
|
||||
_test("""
|
||||
PHP
|
||||
CPX #%s
|
||||
BNE ++
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""".formatted(val), condition);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* Test Y register for a specific value */
|
||||
public TestProgram assertY(int val) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
String condition = "Y != %s <<%s>>".formatted(Integer.toHexString(val), caller);
|
||||
_test("""
|
||||
PHP
|
||||
CPY #%s
|
||||
BNE ++
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
""".formatted(val), condition);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* Test an address for a specific value. If the value is incorrect, leave it in A for inspection */
|
||||
public TestProgram assertAddrVal(int address, int val) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
String condition = "$%s != %s <<%s>>".formatted(Integer.toHexString(address), Integer.toHexString(val), caller);
|
||||
_test("""
|
||||
PHP
|
||||
PHA
|
||||
LDA $%s
|
||||
CMP #%s
|
||||
BNE ++
|
||||
PLA
|
||||
PLP
|
||||
BRA +
|
||||
++ ; Error
|
||||
LDA $%s
|
||||
""".formatted(Integer.toHexString(address), val, val), condition);
|
||||
return this;
|
||||
}
|
||||
|
||||
/* Use provided code to test a condition. If successful it should jump or branch to +
|
||||
* If unsuccessful the error condition will be reported.
|
||||
*
|
||||
* @param code The code to test
|
||||
* @param condition The condition to report if the test fails
|
||||
*/
|
||||
public TestProgram test(String code, String condition) {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
String caller = stackTrace[2].toString();
|
||||
_test(code, condition + "<<" + caller + ">>");
|
||||
return this;
|
||||
}
|
||||
|
||||
private void _test(String code, String condition) {
|
||||
int errorNum = errors.size();
|
||||
errors.add(condition);
|
||||
lines.add("""
|
||||
; << Test %s
|
||||
%s
|
||||
+throwError %s
|
||||
+ ; >> Test """.formatted(condition, code, errorNum));
|
||||
}
|
||||
|
||||
/**
|
||||
* Note the current breakpoint, helpful in understanding error locations
|
||||
*
|
||||
* @param num Error number to report in any error that occurs
|
||||
*/
|
||||
public TestProgram breakpoint(int num) {
|
||||
lines.add(INDENT + "+breakpoint %s".formatted(num));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the registers to 0, clear the stack, and clear flags
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public TestProgram resetRegisters() {
|
||||
lines.add(INDENT + "+resetRegs");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn on or off tracing
|
||||
*
|
||||
* @param state True to turn on tracing, false to turn it off
|
||||
*/
|
||||
public TestProgram setTrace(boolean state) {
|
||||
lines.add(INDENT +"+trace%s".formatted(state ? "On" : "Off"));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the program as unassembled code
|
||||
*
|
||||
* @return The program as a string
|
||||
*/
|
||||
public String build() {
|
||||
lines.add(INDENT +"+success");
|
||||
return String.join("\n", lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the program for a specific number of ticks, or until it completes (whichever comes first)
|
||||
*
|
||||
* @param ticks The number of ticks to run the program
|
||||
* @throws ProgramException If the program reports an error
|
||||
*/
|
||||
public void runForTicks(int ticks) throws ProgramException {
|
||||
this.maxTicks = ticks;
|
||||
run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the program until it completes, reports an error, or reaches the maximum number of ticks
|
||||
*
|
||||
* @throws ProgramException If the program reports an error
|
||||
*/
|
||||
public void run() throws ProgramException {
|
||||
Computer computer = Emulator.withComputer(c->c, null);
|
||||
MOS65C02 cpu = (MOS65C02) computer.getCpu();
|
||||
|
||||
attach();
|
||||
programRunning = true;
|
||||
String program = build();
|
||||
// We have to run the program more carefully so just load it first
|
||||
try {
|
||||
runAssemblyCode(program, 0);
|
||||
} catch (Exception e) {
|
||||
throw new ProgramException(e.getMessage() + "\n" + program, -1);
|
||||
}
|
||||
cpu.resume();
|
||||
try {
|
||||
for (int i=0; i < maxTicks; i++) {
|
||||
cpu.doTick();
|
||||
tickCount++;
|
||||
if (programReportedError) {
|
||||
throw lastError;
|
||||
}
|
||||
if (!programRunning) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cpu.suspend();
|
||||
detach();
|
||||
}
|
||||
assertFalse("Test reported an error", programReportedError);
|
||||
assertFalse("Program never ended fully after " + tickCount + " ticks; got to breakpoint " + lastBreakpoint, programRunning);
|
||||
assertTrue("Test did not complete fully", programCompleted);
|
||||
}
|
||||
}
|
83
src/test/java/jace/TestUtils.java
Normal file
83
src/test/java/jace/TestUtils.java
Normal file
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2023 org.badvision.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package jace;
|
||||
|
||||
import jace.core.CPU;
|
||||
import jace.core.Computer;
|
||||
import jace.core.Device;
|
||||
import jace.core.Utility;
|
||||
import jace.ide.HeadlessProgram;
|
||||
import jace.ide.Program;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author brobert
|
||||
*/
|
||||
public class TestUtils {
|
||||
private TestUtils() {
|
||||
// Utility class has no constructor
|
||||
}
|
||||
|
||||
public static void initComputer() {
|
||||
Utility.setHeadlessMode(true);
|
||||
Emulator.withComputer(Computer::reconfigure);
|
||||
}
|
||||
|
||||
public static void assemble(String code, int addr) throws Exception {
|
||||
runAssemblyCode(code, addr, 0);
|
||||
}
|
||||
|
||||
public static void runAssemblyCode(String code, int ticks) throws Exception {
|
||||
runAssemblyCode(code, 0x6000, ticks);
|
||||
}
|
||||
|
||||
public static void runAssemblyCode(String code, int addr, int ticks) throws Exception {
|
||||
CPU cpu = Emulator.withComputer(c->c.getCpu(), null);
|
||||
HeadlessProgram program = new HeadlessProgram(Program.DocumentType.assembly);
|
||||
program.setValue("*=$"+Integer.toHexString(addr)+"\n "+code+"\n NOP\n RTS");
|
||||
program.execute();
|
||||
if (ticks > 0) {
|
||||
cpu.resume();
|
||||
for (int i=0; i < ticks; i++) {
|
||||
cpu.doTick();
|
||||
}
|
||||
cpu.suspend();
|
||||
}
|
||||
}
|
||||
|
||||
public static Device createSimpleDevice(Runnable r, String name) {
|
||||
return new Device() {
|
||||
@Override
|
||||
public void tick() {
|
||||
r.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getShortName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reconfigure() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDeviceName() {
|
||||
return name;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
74
src/test/java/jace/apple2e/CycleCountTest.java
Normal file
74
src/test/java/jace/apple2e/CycleCountTest.java
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2023 org.badvision.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package jace.apple2e;
|
||||
|
||||
import static jace.TestUtils.initComputer;
|
||||
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.ProgramException;
|
||||
import jace.TestProgram;
|
||||
import jace.core.SoundMixer;
|
||||
|
||||
|
||||
/**
|
||||
* More advanced cycle counting tests. These help ensure CPU runs correctly so things
|
||||
* like vapor lock and speaker sound work as expected.
|
||||
* @author brobert
|
||||
*/
|
||||
public class CycleCountTest {
|
||||
|
||||
@BeforeClass
|
||||
public static void setupClass() {
|
||||
initComputer();
|
||||
SoundMixer.MUTE = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Composite test which ensures the speaker beep is the right pitch.
|
||||
* Test that the wait routine for beep cycles correctly.
|
||||
* Calling WAIT with A=#$c (12) should take 535 cycles
|
||||
* according to the tech ref notes: =1/2*(26+27*A+5*A^2) where A = 12 (0x0c)
|
||||
* The BELL routine has an additional 12 cycles per iteration plus 1 extra cycle in the first iteration.
|
||||
* e.g. 2 iterations take 1093 cycles
|
||||
*
|
||||
* @throws ProgramException
|
||||
*/
|
||||
@Test
|
||||
public void testDirectBeeperCycleCount() throws ProgramException {
|
||||
new TestProgram("""
|
||||
SPKR = $C030
|
||||
jmp BELL
|
||||
WAIT sec
|
||||
WAIT2 pha
|
||||
WAIT3 sbc #$01
|
||||
bne WAIT3
|
||||
pla
|
||||
sbc #$01
|
||||
bne WAIT2
|
||||
rts
|
||||
BELL +markTimer
|
||||
ldy #$02
|
||||
BELL2 lda #$0c
|
||||
jsr WAIT
|
||||
lda SPKR
|
||||
dey
|
||||
bne BELL2
|
||||
""", 1093).run();
|
||||
}
|
||||
|
||||
}
|
804
src/test/java/jace/apple2e/Full65C02Test.java
Normal file
804
src/test/java/jace/apple2e/Full65C02Test.java
Normal file
@ -0,0 +1,804 @@
|
||||
/*
|
||||
* Copyright 2024 Brendan Robert
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package jace.apple2e;
|
||||
|
||||
import static jace.TestProgram.Flag.CARRY_CLEAR;
|
||||
import static jace.TestProgram.Flag.CARRY_SET;
|
||||
import static jace.TestProgram.Flag.DECIMAL_CLEAR;
|
||||
import static jace.TestProgram.Flag.DECIMAL_SET;
|
||||
import static jace.TestProgram.Flag.INTERRUPT_CLEAR;
|
||||
import static jace.TestProgram.Flag.INTERRUPT_SET;
|
||||
import static jace.TestProgram.Flag.IS_ZERO;
|
||||
import static jace.TestProgram.Flag.NEGATIVE;
|
||||
import static jace.TestProgram.Flag.NOT_ZERO;
|
||||
import static jace.TestProgram.Flag.OVERFLOW_CLEAR;
|
||||
import static jace.TestProgram.Flag.OVERFLOW_SET;
|
||||
import static jace.TestProgram.Flag.POSITIVE;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
|
||||
import jace.Emulator;
|
||||
import jace.ProgramException;
|
||||
import jace.TestProgram;
|
||||
import jace.TestUtils;
|
||||
import jace.core.Computer;
|
||||
import jace.core.RAMEvent.TYPE;
|
||||
import jace.core.SoundMixer;
|
||||
|
||||
/**
|
||||
* Basic test functionality to assert correct 6502 decode and execution.
|
||||
*
|
||||
* @author blurry
|
||||
*/
|
||||
public class Full65C02Test {
|
||||
|
||||
static Computer computer;
|
||||
public static MOS65C02 cpu;
|
||||
static RAM128k ram;
|
||||
|
||||
@BeforeClass
|
||||
public static void setupClass() {
|
||||
TestUtils.initComputer();
|
||||
SoundMixer.MUTE = true;
|
||||
computer = Emulator.withComputer(c->c, null);
|
||||
cpu = (MOS65C02) computer.getCpu();
|
||||
ram = (RAM128k) computer.getMemory();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void teardownClass() {
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
computer.pause();
|
||||
cpu.clearState();
|
||||
}
|
||||
|
||||
@Test
|
||||
/* ADC: All CPU flags/modes */
|
||||
public void testAdditionCPUFlags() throws ProgramException {
|
||||
new TestProgram()
|
||||
// Add 1 w/o carry; 1+1 = 2
|
||||
.assertTimed("ADC #1", 2)
|
||||
.assertA(1)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Add 1 w/ carry 1+0+c = 2
|
||||
.add("SEC")
|
||||
.assertTimed("ADC #0", 2)
|
||||
.assertA(2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: ADD 8 w/o carry; 2+8 = 10
|
||||
.add("SED")
|
||||
.assertTimed("ADC #8", 3)
|
||||
.assertA(0x10)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: ADD 9 w/ carry; 10+9+c = 20
|
||||
.add("SEC")
|
||||
.assertTimed("ADC #09", 3)
|
||||
.assertA(0x20)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: Overflow check; 20 + 99 + C = 20 (carry, no overflow)
|
||||
.add("""
|
||||
SED
|
||||
SEC
|
||||
LDA #$20
|
||||
ADC #$99
|
||||
""")
|
||||
.assertA(0x20)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: Overflow check; 20 + 64 + C = 85 (overflow)
|
||||
.add("ADC #$64")
|
||||
.assertA(0x85)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_SET, NEGATIVE)
|
||||
// Overflow check; 0x7F + 0x01 = 0x80 (overflow)
|
||||
.add("CLD")
|
||||
.add("LDA #$7F")
|
||||
.assertTimed("ADC #1", 2)
|
||||
.assertA(0x80)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_SET, NEGATIVE)
|
||||
.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
/* ADC: All addressing modes -- these are used in many opcodes so we mostly just need to test here */
|
||||
public void testAdditionAddressingModes() throws ProgramException {
|
||||
// Start test by filling zero page and $1000...$10FF with 0...255
|
||||
new TestProgram("""
|
||||
LDX #0
|
||||
- TXA
|
||||
STA $00,X
|
||||
STA $1000,X
|
||||
INX
|
||||
BNE -
|
||||
LDA #1
|
||||
""")
|
||||
// 1: AB,X
|
||||
.add("LDX #$7F")
|
||||
.assertTimed("ADC $1000,X", 4)
|
||||
.assertA(0x80)
|
||||
.assertFlags(OVERFLOW_SET, CARRY_CLEAR, NEGATIVE)
|
||||
// 2: AB,Y
|
||||
.add("LDY #$20")
|
||||
.assertTimed("ADC $1000,Y", 4)
|
||||
.assertA(0xA0)
|
||||
.assertFlags(OVERFLOW_CLEAR, CARRY_CLEAR, NEGATIVE)
|
||||
// 3: izp ($09) == 0x100f ==> A+=f
|
||||
.assertTimed("ADC ($0f)", 5)
|
||||
.assertA(0xAF)
|
||||
// 4: izx ($00,x) where X=f = 0x100f ==> A+=f
|
||||
.add("LDX #$0F")
|
||||
.assertTimed("ADC ($00,x)", 6)
|
||||
.assertA(0xBE)
|
||||
// 5: izy ($00),y where Y=20 = 0x102F ==> A+=2F
|
||||
.add("LDY #$21")
|
||||
.assertTimed("ADC ($0F),y", 5)
|
||||
.assertA(0xEE)
|
||||
// 6: zpx $00,x where X=10 ==> A+=10
|
||||
.add("LDX #$10")
|
||||
.assertTimed("ADC $00,x", 4)
|
||||
.assertA(0xFE)
|
||||
// 7: zp $01 ==> A+=01
|
||||
.assertTimed("ADC $01", 3)
|
||||
.assertA(0xFF)
|
||||
// 8: abs $1001 ==> A+=01
|
||||
.assertTimed("ADC $1001", 4)
|
||||
.assertA(0x00)
|
||||
.assertFlags(IS_ZERO, OVERFLOW_CLEAR, CARRY_SET, POSITIVE)
|
||||
// Now check boundary conditions on indexed addressing for timing differences
|
||||
.add("LDX #$FF")
|
||||
.assertTimed("ADC $10FF,X", 5)
|
||||
.add("LDY #$FF")
|
||||
.assertTimed("ADC $10FF,Y", 5)
|
||||
.assertTimed("ADC ($0F),Y", 6)
|
||||
.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
/* ABS: All CPU flags/modes */
|
||||
public void testSubtraction() throws ProgramException {
|
||||
new TestProgram("SEC")
|
||||
// 0-1 = -1
|
||||
.assertTimed("SBC #1", 2)
|
||||
.assertA(0xFF)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE)
|
||||
// 127 - -1 = 128 (Overflow)
|
||||
.add("SEC")
|
||||
.add("LDA #$7F")
|
||||
.assertTimed("SBC #$FF", 2)
|
||||
.assertA(0x80)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_SET, NEGATIVE)
|
||||
// -128 - 1 = -129 (overflow)
|
||||
.add("SEC")
|
||||
.add("LDA #$80")
|
||||
.assertTimed("SBC #$01", 2)
|
||||
.assertA(0x7F)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_SET, POSITIVE)
|
||||
// 20-10=10 (no overflow)
|
||||
.add("LDA #$30")
|
||||
.assertTimed("SBC #$10", 2)
|
||||
.assertA(0x20)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: 20-5=15
|
||||
.add("SED")
|
||||
.assertTimed("SBC #$05", 3)
|
||||
.assertA(0x15)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: 0x15-0x05=0x10
|
||||
.assertTimed("SBC #$05", 3)
|
||||
.assertA(0x10)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE)
|
||||
// Decimal: 99-19=80
|
||||
.add("LDA #$99")
|
||||
.assertTimed("SBC #$19", 3)
|
||||
.assertA(0x80)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE)
|
||||
// Decimal: 99-50=49 (unintuitively causes overflow)
|
||||
.add("LDA #$99")
|
||||
.assertTimed("SBC #$50", 3)
|
||||
.assertA(0x49)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_SET, POSITIVE)
|
||||
// Decimal: 19 - 22 = 97 (arithmetic underflow clears carry)
|
||||
.add("LDA #$19")
|
||||
.assertTimed("SBC #$22", 3)
|
||||
.assertA(0x97)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE)
|
||||
// Test cycle counts for other addressing modes
|
||||
.add("CLD")
|
||||
.assertTimed("SBC $1000", 4)
|
||||
.assertTimed("SBC $1000,x", 4)
|
||||
.assertTimed("SBC $1000,y", 4)
|
||||
.assertTimed("SBC ($00)", 5)
|
||||
.assertTimed("SBC ($00,X)", 6)
|
||||
.assertTimed("SBC ($00),Y", 5)
|
||||
.assertTimed("SBC $00", 3)
|
||||
.assertTimed("SBC $00,X", 4)
|
||||
.run();
|
||||
}
|
||||
|
||||
@Test
|
||||
/* Full test of ADC and SBC with binary coded decimal mode */
|
||||
public void testBCD() throws ProgramException, URISyntaxException, IOException {
|
||||
Path resource = Paths.get(getClass().getResource("/jace/bcd_test.asm").toURI());
|
||||
String testCode = Files.readString(resource);
|
||||
TestProgram test = new TestProgram(testCode);
|
||||
test.defineError("Error when performing ADC operation");
|
||||
test.defineError("Error when performing SBC operation");
|
||||
try {
|
||||
test.runForTicks(50000000);
|
||||
} catch (ProgramException e) {
|
||||
// Dump memory from 0x0006 to 0x0015
|
||||
for (int i = 0x0006; i <= 0x0015; i++) {
|
||||
System.out.printf("%04X: %02X\n", i, ram.read(i, TYPE.READ_DATA, false, false));
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
/* Test of the processor flags */
|
||||
public void testFlags() throws ProgramException {
|
||||
new TestProgram()
|
||||
// Test explicit flag set/clear commands (and the tests by way of this cover all branch instructions)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("SEC", 2)
|
||||
.assertFlags(CARRY_SET, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("CLC", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("SED", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_SET, INTERRUPT_CLEAR)
|
||||
.assertTimed("CLD", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("SEI", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_SET)
|
||||
.assertTimed("CLI", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
// Set overflow flag by hacking the P register
|
||||
.add("""
|
||||
LDA #%01000000
|
||||
PHA
|
||||
PLP
|
||||
""")
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_SET, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("CLV", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
// Test Zero and Negative flags (within reason, the ADC/SBC tests cover these more thoroughly)
|
||||
.assertTimed("LDA #0",2 )
|
||||
.assertFlags(CARRY_CLEAR, IS_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("LDA #$ff", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("LDY #0", 2)
|
||||
.assertFlags(CARRY_CLEAR, IS_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("LDY #$ff", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("LDX #0", 2)
|
||||
.assertFlags(CARRY_CLEAR, IS_ZERO, OVERFLOW_CLEAR, POSITIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.assertTimed("LDX #$ff", 2)
|
||||
.assertFlags(CARRY_CLEAR, NOT_ZERO, OVERFLOW_CLEAR, NEGATIVE, DECIMAL_CLEAR, INTERRUPT_CLEAR)
|
||||
.run();
|
||||
}
|
||||
|
||||
/* Test stack operations */
|
||||
@Test
|
||||
public void testStack() throws ProgramException {
|
||||
new TestProgram()
|
||||
.assertTimed("TSX", 2)
|
||||
.assertX(0xFF)
|
||||
.assertTimed("LDA #255", 2)
|
||||
.assertTimed("PHA", 3)
|
||||
.assertTimed("LDX #11", 2)
|
||||
.assertTimed("PHX", 3)
|
||||
.assertTimed("LDY #12", 2)
|
||||
.assertTimed("PHY", 3)
|
||||
.assertTimed("PHP", 3)
|
||||
.assertTimed("TSX", 2)
|
||||
.assertX(0xFB)
|
||||
.assertTimed("PLP", 4)
|
||||
.add("LDA #0")
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
.assertTimed("PLA", 4)
|
||||
.assertA(12)
|
||||
.assertFlags(NOT_ZERO, POSITIVE)
|
||||
.add("LDY #$FF")
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("PLY", 4)
|
||||
.assertY(11)
|
||||
.assertFlags(NOT_ZERO, POSITIVE)
|
||||
.assertTimed("PLX", 4)
|
||||
.assertX(255)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.run();
|
||||
}
|
||||
|
||||
/* Test logic operations */
|
||||
@Test
|
||||
public void testLogic() throws ProgramException {
|
||||
// OR, AND, EOR
|
||||
new TestProgram()
|
||||
.assertTimed("LDA #0x55", 2)
|
||||
.assertTimed("AND #0xAA", 2)
|
||||
.assertA(0x00)
|
||||
.assertFlags(IS_ZERO, POSITIVE)
|
||||
.assertTimed("LDA #0x55", 2)
|
||||
.assertTimed("ORA #0xAA", 2)
|
||||
.assertA(0xFF)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.assertTimed("LDA #0x55", 2)
|
||||
.assertTimed("EOR #0xFF", 2)
|
||||
.assertA(0xAA)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE)
|
||||
.run();
|
||||
TestProgram cycleCounts = new TestProgram();
|
||||
for (String opcode : new String[] {"AND", "ORA", "EOR"}) {
|
||||
cycleCounts.assertTimed(opcode + " $1000", 4)
|
||||
.assertTimed(opcode + " $1000,x", 4)
|
||||
.assertTimed(opcode + " $1000,y", 4)
|
||||
.assertTimed(opcode + " ($00)", 5)
|
||||
.assertTimed(opcode + " ($00,X)", 6)
|
||||
.assertTimed(opcode + " ($00),Y", 5)
|
||||
.assertTimed(opcode + " $00", 3)
|
||||
.assertTimed(opcode + " $00,X", 4);
|
||||
}
|
||||
cycleCounts.run();
|
||||
// ASL
|
||||
new TestProgram()
|
||||
.assertTimed("LDA #0x55", 2)
|
||||
.add("STA $1000")
|
||||
.assertTimed("ASL", 2)
|
||||
.assertA(0xAA)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR)
|
||||
.assertTimed("ASL", 2)
|
||||
.assertA(0x54)
|
||||
.assertFlags(NOT_ZERO, POSITIVE, CARRY_SET)
|
||||
.assertTimed("ASL $1000", 6)
|
||||
.assertAddrVal(0x1000, 0xAA)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, CARRY_CLEAR)
|
||||
.assertTimed("ASL $1000,x", 7)
|
||||
.assertTimed("ASL $00", 5)
|
||||
.assertTimed("ASL $00,x", 6)
|
||||
.run();
|
||||
// LSR
|
||||
new TestProgram()
|
||||
.assertTimed("LDA #0x55", 2)
|
||||
.add("STA $1000")
|
||||
.assertTimed("LSR", 2)
|
||||
.assertA(0x2A)
|
||||
.assertFlags(NOT_ZERO, POSITIVE, CARRY_SET)
|
||||
.assertTimed("LSR", 2)
|
||||
.assertA(0x15)
|
||||
.assertFlags(NOT_ZERO, POSITIVE, CARRY_CLEAR)
|
||||
.assertTimed("LSR $1000", 6)
|
||||
.assertAddrVal(0x1000, 0x2A)
|
||||
.assertFlags(NOT_ZERO, POSITIVE, CARRY_SET)
|
||||
.assertTimed("LSR $1000,x", 7)
|
||||
.assertTimed("LSR $00", 5)
|
||||
.assertTimed("LSR $00,x", 6)
|
||||
.run();
|
||||
// BIT
|
||||
new TestProgram()
|
||||
.add("LDA #$FF")
|
||||
.add("STA $FF")
|
||||
.assertTimed("BIT #0", 2)
|
||||
.assertFlags(IS_ZERO, POSITIVE, OVERFLOW_CLEAR)
|
||||
.assertTimed("BIT #$FF", 2)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, OVERFLOW_CLEAR)
|
||||
.assertTimed("BIT $FF", 3)
|
||||
.assertFlags(NOT_ZERO, NEGATIVE, OVERFLOW_SET)
|
||||
.add("CLV")
|
||||
.add("LDA #$40")
|
||||
.assertTimed("BIT #$40", 2)
|
||||
.assertFlags(NOT_ZERO, POSITIVE, OVERFLOW_CLEAR)
|
||||
.assertTimed("BIT #$80", 2)
|
||||
.assertFlags(IS_ZERO, NEGATIVE, OVERFLOW_CLEAR)
|
||||
.assertTimed("BIT $1000", 4)
|
||||
.assertTimed("BIT $1000,x", 4)
|
||||
.assertTimed("BIT $00,X", 4)
|
||||
.run();
|
||||
// ROL |