Merge pull request #55 from badvision/votrax

Restoring tests
This commit is contained in:
Brendan Robert 2024-08-02 22:53:13 -05:00 committed by GitHub
commit f34ba40ff0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 2855 additions and 6 deletions

View File

@ -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()));

View 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);
}
}
}

View File

@ -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);
}

Binary file not shown.

View File

@ -13,4 +13,4 @@ public abstract class AbstractFXTest {
Platform.startup(() -> {});
}
}
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
};
}
}

View 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();
}
}

View 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