diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index f32842b..4af0041 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -12,6 +12,7 @@
+
@@ -22,7 +23,41 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -58,41 +93,46 @@
- {
+ "keyToString": {
+ "ASKED_ADD_EXTERNAL_FILES": "true",
+ "RunOnceActivity.OpenProjectViewOnStart": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.cidr.known.project.marker": "true",
+ "WebServerToolWindowFactoryState": "false",
+ "cf.first.check.clang-format": "false",
+ "cidr.known.project.marker": "true",
+ "com.jfrog.conanplugin.addconansupport": "true",
+ "com.jfrog.conanplugin.automanage.cmake.advanced.settings": "true",
+ "com.jfrog.conanplugin.conanexecutable": "conan",
+ "com.jfrog.conanplugin.hasbeensetup": "true",
+ "git-widget-placeholder": "master",
+ "last_opened_file_path": "/home/william/Dev/ETS/LOG710/Lab2/CMakeLists.txt",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "settings.editor.selected.configurable": "preferences.pluginManager",
+ "structure.view.defaults.are.configured": "true",
+ "vue.rearranger.settings.migration": "true"
}
-}]]>
+}
+
-
+
+
+
+
+
@@ -111,6 +151,11 @@
+
+
+
+
+
@@ -238,6 +283,7 @@
+
@@ -267,6 +313,8 @@
+
+
@@ -276,7 +324,15 @@
1701018709236
-
+
+
+ 1701463073548
+
+
+
+ 1701463073548
+
+
@@ -291,6 +347,18 @@
-
+
+
+
+
+
+
+
+ file://$PROJECT_DIR$/rom/rom.c
+ 32
+
+
+
+
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f188b6f..2da751d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -3,15 +3,18 @@ project(NESEmulator VERSION 0.1)
add_subdirectory(cpu)
add_subdirectory(mappers)
+add_subdirectory(rom)
+
list(APPEND EXTRA_INCLUDES
"${PROJECT_SOURCE_DIR}/cpu"
- "${PROJECT_SOURCE_DIR}/mappers")
+ "${PROJECT_SOURCE_DIR}/mappers"
+ "${PROJECT_SOURCE_DIR}/rom")
add_executable(NESEmulator main.c)
find_package(log.c)
-target_link_libraries(NESEmulator CPU Mappers log.c::log.c)
+target_link_libraries(NESEmulator CPU Mappers ROM log.c::log.c)
target_include_directories(NESEmulator PUBLIC
"${PROJECT_BINARY_DIR}"
${EXTRA_INCLUDES})
diff --git a/cpu/cpu.h b/cpu/cpu.h
index eaad67b..faaea60 100644
--- a/cpu/cpu.h
+++ b/cpu/cpu.h
@@ -6,6 +6,7 @@
#define CPU_CPU_H
#include
+#include "memory.h"
// Reference: https://www.nesdev.org/wiki/Status_flags
#define CPU_STATUS_CARRY_MASK 0x01
diff --git a/cpu/op.c b/cpu/op.c
index d94e36c..a548ecf 100644
--- a/cpu/op.c
+++ b/cpu/op.c
@@ -1,17 +1,17 @@
#include
-#include
#include
+#include
#include "op.h"
#include "cpu.h"
-#include "../include/cpu.h"
// Reference: https://www.nesdev.org/wiki/CPU_unofficial_opcodes
// https://www.middle-engine.com/blog/posts/2020/06/23/programming-the-nes-the-6502-in-detail
#define IS_OP_CODE_MODE(op, op_code, addr_mode) \
- case op_code: \
- op_ ## op(ADDR_MODE_ ## addr_mode); \
+ case op_code: \
+ log_debug("OP: %s", "op"); \
+ op_ ## op(ADDR_MODE_ ## addr_mode); \
break;
#define IS_OP_CODE(op, op_code) \
@@ -254,7 +254,7 @@ void add_with_carry(byte value) {
cpu_get_registers()->accumulator = acc;
cpu_set_flag(overflow, CPU_STATUS_CARRY_MASK);
- cpu_set_flag(is_sign_overflow(acc, value, result));
+ cpu_set_flag(is_sign_overflow(acc, value, result), CPU_STATUS_OVERFLOW_MASK);
set_acl_flags(result);
}
diff --git a/cpu/ram.h b/cpu/ram.h
index 2e9a31c..169e999 100644
--- a/cpu/ram.h
+++ b/cpu/ram.h
@@ -2,6 +2,8 @@
// Created by william on 30/09/23.
//
+#include "cpu.h"
+
#ifndef NESEMULATOR_RAM_H
#define NESEMULATOR_RAM_H
@@ -10,9 +12,6 @@
typedef unsigned short address;
-void init_ram();
-void clean_ram();
-
void ram_set_byte(address addr, byte byte);
byte ram_get_byte(address addr);
word ram_get_word(address addr);
diff --git a/include/rom.h b/include/rom.h
new file mode 100644
index 0000000..62f67d3
--- /dev/null
+++ b/include/rom.h
@@ -0,0 +1,16 @@
+//
+// Created by william on 12/2/23.
+//
+
+#ifndef NESEMULATOR_ROM_H
+#define NESEMULATOR_ROM_H
+
+typedef struct {
+ char* prg_rom;
+ char* chr_rom;
+ void* header;
+} Rom;
+
+int read_rom(char* path);
+
+#endif //NESEMULATOR_ROM_H
\ No newline at end of file
diff --git a/main.c b/main.c
index 28c0284..b1059a9 100644
--- a/main.c
+++ b/main.c
@@ -15,19 +15,14 @@
*
* =====================================================================================
*/
-#include
#include
-#include "include/cpu.h"
-#include "ram.h"
+#include "include/rom.h"
int main() {
- init_ram();
+ char *rom_path = "../tests/cpu_exec_space/test_cpu_exec_space_ppuio.nes";
+ read_rom(rom_path);
- ram_set_byte(0x10, 0xf);
- ram_get_byte(0xffff);
-
- clean_ram();
- return -1;
+ return EXIT_SUCCESS;
}
diff --git a/rom/CMakeLists.txt b/rom/CMakeLists.txt
new file mode 100644
index 0000000..10170f3
--- /dev/null
+++ b/rom/CMakeLists.txt
@@ -0,0 +1,6 @@
+add_library(ROM
+ rom.c
+ ines.c)
+
+find_package(log.c)
+target_link_libraries(ROM log.c::log.c)
\ No newline at end of file
diff --git a/rom/ines.c b/rom/ines.c
new file mode 100644
index 0000000..63eaf71
--- /dev/null
+++ b/rom/ines.c
@@ -0,0 +1,140 @@
+//
+// Created by william on 12/2/23.
+//
+
+#include
+#include
+#include
+#include
+#include "../include/rom.h"
+
+// Flag 6
+#define NES_HEADER_FLAG_MIRRORING 0x01
+#define NES_HEADER_FLAG_BATTERY 0x02
+#define NES_HEADER_FLAG_TRAINER 0x04
+#define NES_HEADER_FLAG_IGNORE_MIRRORING 0x08
+// Lower nybble of mapper number
+#define NES_HEADER_FLAG_LOWER_MAPPER_NUMBER 0xf0
+
+// Flag 7
+#define NES_HEADER_FLAG_VS_UNISYSTEM 0x01
+#define NES_HEADER_FLAG_PLAYCHOICE_10 0x02
+// Upper nybble of mapper number, not used in iNes 1.0 format
+#define NES_HEADER_FLAG_UPPER_MAPPER_NUMBER 0xf0
+
+// Flag 9
+#define NES_HEADER_FLAG_TV_SYSTEM 0x01
+
+// Flag 10
+#define NES_HEADER_FLAG_PRG_RAM 0x10
+#define NES_HEADER_FLAG_BUS_CONFLICT 0x20
+
+struct INesHeaderFlags {
+/* Mirroring: 0: horizontal (vertical arrangement) (CIRAM A10 = PPU A11)
+ 1: vertical (horizontal arrangement) (CIRAM A10 = PPU A10) */
+ bool nametable_mirrored;
+// 1: Ignore mirroring control or above mirroring bit; instead provide four-screen VRAM
+ bool ignore_mirroring;
+// 1: Cartridge contains battery-backed PRG RAM ($6000-7FFF) or other persistent memory
+ bool has_battery;
+// 1: 512-byte trainer at $7000-$71FF (stored before PRG data)
+ bool has_trainer;
+ bool vs_unisystem;
+// PlayChoice-10 (8 KB of Hint Screen data stored after CHR data)
+ bool playchoice_10;
+ bool tv_system;
+ bool has_prg_ram;
+ bool has_bus_conflict;
+};
+
+typedef struct {
+ // PRG ROM size in 16KB units
+ unsigned char prg_rom_size;
+ unsigned char prg_ram_size;
+ // CHR ROM size in 8KB units
+ unsigned char chr_rom_size;
+ unsigned char mapper_number;
+ struct INesHeaderFlags flags;
+} INesHeader;
+
+bool rom_is_ines(const char header[16]) {
+ return header[0] == 'N' && header[1] == 'E' && header[2] == 'S';
+}
+
+INesHeader read_header(const char header_buf[16]) {
+ INesHeader header;
+
+ unsigned char flag6 = header_buf[6];
+ unsigned char flag7 = header_buf[7];
+ unsigned char flag9 = header_buf[9];
+ unsigned char flag10 = header_buf[10];
+
+ header.flags.nametable_mirrored = flag6 & NES_HEADER_FLAG_MIRRORING;
+ header.flags.ignore_mirroring = flag6 & NES_HEADER_FLAG_IGNORE_MIRRORING;
+ header.flags.has_battery = flag6 & NES_HEADER_FLAG_BATTERY;
+ header.flags.has_trainer = flag6 & NES_HEADER_FLAG_TRAINER;
+ header.flags.vs_unisystem = flag7 & NES_HEADER_FLAG_VS_UNISYSTEM;
+ header.flags.playchoice_10 = flag7 & NES_HEADER_FLAG_PLAYCHOICE_10;
+ header.flags.tv_system = flag9 & NES_HEADER_FLAG_TV_SYSTEM;
+ header.flags.has_prg_ram = flag10 & NES_HEADER_FLAG_PRG_RAM | 0x01;
+ header.flags.has_bus_conflict = flag10 & NES_HEADER_FLAG_BUS_CONFLICT;
+
+ header.prg_rom_size = header_buf[4];
+ header.prg_ram_size = header_buf[8];
+ header.chr_rom_size = header_buf[5];
+ header.mapper_number = (flag6 & NES_HEADER_FLAG_LOWER_MAPPER_NUMBER) >> 4;
+
+ if (!header.prg_ram_size) {
+ // For compatibility, a value of 0 unit of PRG RAM infers 8KB, or 1 unit.
+ header.prg_ram_size = 1;
+ }
+
+ log_debug("=== iNes ROM Header ===");
+ log_debug("PRG: %dx16KB (ROM), %dx8KB (RAM)",
+ header.prg_rom_size,
+ header.prg_ram_size);
+ log_debug("CHR: %dx8KB (ROM)",
+ header.chr_rom_size);
+ log_debug("Mapper number: %d",
+ header.mapper_number);
+ log_debug("Nametable mirrored: %d", header.flags.nametable_mirrored);
+ log_debug("Ignore mirroring: %d", header.flags.ignore_mirroring);
+ log_debug("Has PRG RAM: %d", header.flags.has_prg_ram);
+ log_debug("Has bus conflict: %d", header.flags.has_bus_conflict);
+ log_debug("Has battery: %d", header.flags.has_battery);
+ log_debug("Has trainer: %d", header.flags.has_trainer);
+ log_debug("VS Unisystem: %d", header.flags.vs_unisystem);
+ log_debug("Playchoice 10: %d", header.flags.playchoice_10);
+ log_debug("TV System: %d", header.flags.tv_system);
+
+ return header;
+}
+
+bool rom_nes_read(const char header_buf[16], FILE *file, Rom *rom) {
+ INesHeader header = read_header(header_buf);
+ rom->header = &header;
+
+ // We don't support the trainer, so we skip ahead by 512 bytes if needed.
+ if (header.flags.has_trainer && !fseek(file, 512, SEEK_CUR)) {
+ perror("Failed to seek ahead of trainer ROM section");
+ return false;
+ }
+
+ unsigned int prg_rom_size = header.prg_rom_size * 16384;
+ rom->prg_rom = (char *) malloc(prg_rom_size * sizeof(char));
+ if (fread(rom->prg_rom, sizeof(char), prg_rom_size, file) < prg_rom_size) {
+ perror("Failed to read PRG ROM");
+ return false;
+ }
+
+ if (header.chr_rom_size > 0) {
+ unsigned int chr_rom_size = header.chr_rom_size * 8192;
+ rom->chr_rom = (char *) malloc(chr_rom_size * sizeof(char));
+ if (fread(rom->chr_rom, sizeof(char), chr_rom_size, file) < chr_rom_size) {
+ perror("Failed to read CHR ROM");
+ return false;
+ }
+ }
+
+ return true;
+}
\ No newline at end of file
diff --git a/rom/rom.c b/rom/rom.c
new file mode 100644
index 0000000..82a29a2
--- /dev/null
+++ b/rom/rom.c
@@ -0,0 +1,54 @@
+//
+// Created by william on 12/2/23.
+//
+
+#include
+#include
+
+#include "../include/rom.h"
+#include "ines.c"
+
+#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
+
+void rom_init(Rom *rom) {
+ rom->header = NULL;
+ rom->prg_rom = NULL;
+ rom->chr_rom = NULL;
+}
+
+void rom_uninit(Rom *rom) {
+ free(rom->prg_rom);
+ free(rom->chr_rom);
+}
+
+int read_rom(char *path) {
+ FILE *file = fopen(path, "r");
+ if (!file) {
+ perror("Failed to open ROM");
+ return EXIT_FAILURE;
+ }
+
+ char header_buffer[16] = {0};
+ size_t read_size = fread(header_buffer, sizeof(char), ARRAY_SIZE(header_buffer), file);
+ if (read_size < ARRAY_SIZE(header_buffer)) {
+ perror("Failed to read ROM");
+ return EXIT_FAILURE;
+ }
+
+ if (!rom_is_ines(header_buffer)) {
+ perror("Only iNes ROMs are supported");
+ return EXIT_FAILURE;
+ }
+
+ Rom rom;
+ rom_init(&rom);
+ rom_nes_read(header_buffer, file, &rom);
+ rom_uninit(&rom);
+
+ if (fclose(file) != 0) {
+ perror("Failed to close ROM file");
+ return EXIT_FAILURE;
+ }
+
+ return 0;
+}
\ No newline at end of file
diff --git a/tests/cpu_exec_space/readme.txt b/tests/cpu_exec_space/readme.txt
new file mode 100644
index 0000000..4eaf8ae
--- /dev/null
+++ b/tests/cpu_exec_space/readme.txt
@@ -0,0 +1,158 @@
+NES Memory Execution Tests
+----------------------------------
+These tests verify that the CPU can execute code from any possible
+memory location, even if that is mapped as I/O space.
+
+In addition, two obscure side effects are tested:
+
+1. The PPU open bus. Any write to PPU will update the open bus.
+ Reading from 2002 updates the low 5 bits. Reading from 2007
+ updates 8 bits. The open bus is shown in any addresss/bit
+ that the PPU does not write to. Read from 2000, you get open bus.
+ Read from 2006, ditto. Read from 2002, you get that in high 3 bits.
+ Additionally, the open bus decays automatically to zero in about one
+ second if not refreshed.
+ This test requires that a value written to $2003 can be read back
+ from $2001 within a time window of one or two frames.
+
+2. One-byte opcodes must issue a dummy read to the byte immediately
+ following that opcode. The CPU always does a fetch of the second
+ byte, before it has even begun executing the opcode in the first
+ place.
+
+Additionally, the following PPU features must be working properly:
+
+1. PPU memory writes and reads through $2006/$2007
+2. The address high/low toggle reset at $2002
+3. A single write through $2006 must not affect the address used by $2007
+4. NMI should fire sometimes to salvage a broken program, if the JSR/JMP
+ never reaches its intended destination. (Only required in the
+ test IF the CPU and/or open bus are not working properly.)
+
+The test is done FIVE times: Once with JSR $2001, again with JMP $2001,
+and then with RTS (with target address of $2001), and then with a JMP
+that expects to return with an RTI opcode. Finally, with a regular
+JSR, but the return from the code is done through a BRK instruction.
+
+Tests and results:
+
+ #2: PPU memory access through $2007 does not work properly. (Use other tests to determine the exact problem.)
+ #3: PPU open bus implementation is missing or incomplete: A write to $2003, followed by a read from $2001 should return the same value as was written.
+ #4: The RTS at $2001 was never executed. (If NMI has not been implemented in the emulator, the symptom of this failure is that the program crashes and does not output either "Fail" nor "Passed").
+ #5: An RTS opcode should still do a dummy fetch of the next opcode. (The same goes for all one-byte opcodes, really.)
+ #6: I have no idea what happened, but the test did not work as supposed to. In any case, the problem is in the PPU.
+ #7: A jump to $2001 should never execute code from $8001 / $9001 / $A001 / $B001 / $C001 / $D001 / $E001.
+ #8: Okay, the test passed when JSR was used, but NOT when the opcode was JMP. I definitely did not think any emulator would trigger this result.
+ #9: Your PPU is broken in mind-defyingly random ways.
+ #10: RTS to $2001 never returned. This message never gets displayed.
+ #11: The test passed when JSR was used, and when JMP was used, but NOT when RTS was used. Caught ya! Paranoia wins.
+ #12: Your PPU gave up reason at the last moment.
+ #13: JMP to $2001 never returned. Again, this message never gets displayed.
+ #14: An RTI opcode should still do a dummy fetch of the next opcode. (The same goes for all one-byte opcodes, really.)
+ #15: An RTI opcode should not destroy the PPU. Somehow that still appears to be the case here.
+ #16: IRQ occurred uncalled
+ #17: JSR to $2001 never returned. (Never displayed)
+ #18: The BRK instruction should issue an automatic fetch of the byte that follows right after the BRK. (The same goes for all one-byte opcodes, but with BRK it should be a bit more obvious than with others.)
+ #19: A BRK opcode should not destroy the PPU. Somehow that still appears to be the case here.
+
+
+Expected output:
+ TEST:test_cpu_exec_space_ppuio
+ This program verifies that the
+ CPU can execute code from any
+ possible location that it can
+ address, including I/O space.
+
+ In addition, it will be tested
+ that an RTS instruction does a
+ dummy read of the byte that
+ immediately follows the
+ instructions.
+
+ JSR+RTS TEST OK
+ JMP+RTS TEST OK
+ RTS+RTS TEST OK
+ JMP+RTI TEST OK
+ JMP+BRK TEST OK
+
+ Passed
+
+Expected output in the other test:
+
+ TEST: test_cpu_exec_space_apu
+ This program verifies that the
+ CPU can execute code from any
+ possible location that it can
+ address, including I/O space.
+
+ In this test, it is also
+ verified that not only all
+ write-only APU I/O ports
+ return the open bus, but
+ also the unallocated I/O
+ space in $4018..$40FF.
+
+ 40FF 40
+ Passed
+
+
+
+Flashes, clicks, other glitches
+-------------------------------
+If a test prints "passed", it passed, even if there were some flashes or
+odd sounds. Only a test which prints "done" at the end requires that you
+watch/listen while it runs in order to determine whether it passed. Such
+tests involve things which the CPU cannot directly test.
+
+
+Alternate output
+----------------
+Tests generally print information on screen, but also report the final
+result audibly, and output text to memory, in case the PPU doesn't work
+or there isn't one, as in an NSF or a NES emulator early in development.
+
+After the tests are done, the final result is reported as a series of
+beeps (see below). For NSF builds, any important diagnostic bytes are
+also reported as beeps, before the final result.
+
+
+Output at $6000
+---------------
+All text output is written starting at $6004, with a zero-byte
+terminator at the end. As more text is written, the terminator is moved
+forward, so an emulator can print the current text at any time.
+
+The text output may include ANSI color codes, which take the form of
+an esc character ($1B), an opening bracket ('['), and a sequence of
+numbers and semicolon characters, terminated by a non-digit character ('m').
+
+The test status is written to $6000. $80 means the test is running, $81
+means the test needs the reset button pressed, but delayed by at least
+100 msec from now. $00-$7F means the test has completed and given that
+result code.
+
+To allow an emulator to know when one of these tests is running and the
+data at $6000+ is valid, as opposed to some other NES program, $DE $B0
+$G1 is written to $6001-$6003.
+
+
+Audible output
+--------------
+A byte is reported as a series of tones. The code is in binary, with a
+low tone for 0 and a high tone for 1, and with leading zeroes skipped.
+The first tone is always a zero. A final code of 0 means passed, 1 means
+failure, and 2 or higher indicates a specific reason. See the source
+code of the test for more information about the meaning of a test code.
+They are found after the set_test macro. For example, the cause of test
+code 3 would be found in a line containing set_test 3. Examples:
+
+ Tones Binary Decimal Meaning
+ - - - - - - - - - - - - - - - - - - - -
+ low 0 0 passed
+ low high 01 1 failed
+ low high low 010 2 error 2
+
+
+--
+Shay Green
+Joel Yliluoma
diff --git a/tests/cpu_exec_space/source/common/ascii_1.chr b/tests/cpu_exec_space/source/common/ascii_1.chr
new file mode 100644
index 0000000..3366c25
Binary files /dev/null and b/tests/cpu_exec_space/source/common/ascii_1.chr differ
diff --git a/tests/cpu_exec_space/source/common/ascii_2.chr b/tests/cpu_exec_space/source/common/ascii_2.chr
new file mode 100644
index 0000000..8a8f74f
Binary files /dev/null and b/tests/cpu_exec_space/source/common/ascii_2.chr differ
diff --git a/tests/cpu_exec_space/source/common/ascii_3.chr b/tests/cpu_exec_space/source/common/ascii_3.chr
new file mode 100644
index 0000000..2c5b26b
Binary files /dev/null and b/tests/cpu_exec_space/source/common/ascii_3.chr differ
diff --git a/tests/cpu_exec_space/source/common/build_rom.s b/tests/cpu_exec_space/source/common/build_rom.s
new file mode 100644
index 0000000..e852ef5
--- /dev/null
+++ b/tests/cpu_exec_space/source/common/build_rom.s
@@ -0,0 +1,96 @@
+; Builds program as iNES ROM
+
+; Default is 16K PRG and 8K CHR ROM, NROM (0)
+
+.if 0 ; Options to set before .include "shell.inc":
+CHR_RAM=1 ; Use CHR-RAM instead of CHR-ROM
+CART_WRAM=1 ; Use mapper that supports 8K WRAM in cart
+CUSTOM_MAPPER=n ; Specify mapper number
+.endif
+
+.ifndef CUSTOM_MAPPER
+ .ifdef CART_WRAM
+ CUSTOM_MAPPER = 2 ; UNROM
+ .else
+ CUSTOM_MAPPER = 0 ; NROM
+ .endif
+.endif
+
+;;;; iNES header
+.ifndef CUSTOM_HEADER
+ .segment "HEADER"
+ .byte $4E,$45,$53,26 ; "NES" EOF
+
+ .ifdef CHR_RAM
+ .byte 2,0 ; 32K PRG, CHR RAM
+ .else
+ .byte 2,1 ; 32K PRG, 8K CHR
+ .endif
+
+ .byte CUSTOM_MAPPER*$10+$01 ; vertical mirroring
+.endif
+
+.ifndef CUSTOM_VECTORS
+ .segment "VECTORS"
+ .word -1&$FFFF,-1&$FFFF,-1&$FFFF, nmi, reset, irq
+.endif
+
+;;;; CHR-RAM/ROM
+.ifdef CHR_RAM
+ .define CHARS "CHARS_PRG"
+ .segment CHARS
+ ascii_chr:
+
+ .segment "CHARS_PRG_ASCII"
+ .align $200
+ .incbin "ascii.chr"
+ ascii_chr_end:
+.else
+ .define CHARS "CHARS"
+ .segment "CHARS_ASCII"
+ ;.align $200
+ .incbin "ascii_3.chr"
+ ;.align $200
+ .incbin "ascii_2.chr"
+ ;.align $200
+ .incbin "ascii_1.chr"
+ .res $E00
+.endif
+
+;.segment CHARS
+ ;.res $10,0
+
+;;;; Shell
+.ifndef NEED_CONSOLE
+ NEED_CONSOLE=1
+.endif
+
+; Move code to $C000
+;.segment "DMC"
+; .res $4000
+
+.include "shell.s"
+
+std_reset:
+ lda #0
+ sta PPUCTRL
+ sta PPUMASK
+ jmp run_shell
+
+init_runtime:
+ .ifdef CHR_RAM
+ load_ascii_chr
+ .endif
+ rts
+
+post_exit:
+ jsr set_final_result
+ jmp forever
+
+; This helps devcart recover after running test.
+; It is never executed by test ROM.
+.segment "LOADER"
+ .incbin "devcart.bin"
+
+.code
+.align 256
diff --git a/tests/cpu_exec_space/source/common/colors.inc b/tests/cpu_exec_space/source/common/colors.inc
new file mode 100644
index 0000000..3e019c1
--- /dev/null
+++ b/tests/cpu_exec_space/source/common/colors.inc
@@ -0,0 +1,59 @@
+.define color2 $A0
+.define color1 $40
+.define color3 $E0
+
+zp_res color_ptr,2
+
+.pushseg
+.segment "RODATA"
+Color1Esc: .byte 27, "[0;33m", 0
+Color2Esc: .byte 27, "[1;34m", 0
+Color3Esc: .byte 27, "[0;37m", 0
+.segment "LIB"
+
+TextColor1:
+ pha
+ setw color_ptr, Color1Esc
+ lda #color1
+ bne ColorPrint_Sub
+TextColor2:
+ pha
+ setw color_ptr, Color2Esc
+ lda #color2
+ bne ColorPrint_Sub
+TextColor3:
+ pha
+ setw color_ptr, Color3Esc
+ lda #color3
+ColorPrint_Sub:
+ sta text_color
+ tya
+ pha
+ ldy #0
+@loop:
+ lda (color_ptr),y
+ beq :+
+ jsr write_text_out
+ incw color_ptr
+ bne @loop
+: pla
+ tay
+ pla
+ rts
+
+
+
+.popseg
+
+
+
+.macro text_color1
+ jsr TextColor1
+.endmacro
+.macro text_color2
+ jsr TextColor2
+.endmacro
+.macro text_white
+ jsr TextColor3
+.endmacro
+
diff --git a/tests/cpu_exec_space/source/common/console.s b/tests/cpu_exec_space/source/common/console.s
new file mode 100644
index 0000000..9a257d5
--- /dev/null
+++ b/tests/cpu_exec_space/source/common/console.s
@@ -0,0 +1,282 @@
+; Scrolling text console with line wrapping, 30x29 characters.
+; Buffers lines for speed. Will work even if PPU doesn't
+; support scrolling (until text reaches bottom). Keeps border
+; along bottom in case TV cuts it off.
+;
+; Defers most initialization until first newline, at which
+; point it clears nametable and makes palette non-black.
+;
+; ** ASCII font must already be in CHR, and mirroring
+; must be vertical or single-screen.
+
+; Number of characters of margin on left and right, to avoid
+; text getting cut off by common TVs
+console_margin = 1
+
+console_buf_size = 32
+console_width = console_buf_size - (console_margin*2)
+
+zp_byte console_pos
+zp_byte console_scroll
+zp_byte console_temp
+zp_byte text_color
+
+bss_res console_buf,console_buf_size
+
+
+; Initializes console
+console_init:
+ ; Flag that console hasn't been initialized
+ setb console_scroll,-1&$FF
+ lda #0
+ sta text_color
+ jmp console_clear_line_
+
+
+; Hides console by blacking palette and disabling PPU.
+; Preserved: A, X, Y
+console_hide:
+ pha
+ txa
+ pha
+ tay
+ pha
+
+ jsr console_wait_vbl_
+ setb PPUMASK,0
+ lda #$0F
+ tax
+ tay
+ jsr console_load_palette_
+ pla
+ tay
+ pla
+ tax
+ pla
+ rts
+
+
+console_wait_vbl_:
+ lda console_scroll
+ cmp #-1&$FF
+ jne wait_vbl_optional
+
+ ; Deferred initialization of PPU until first use of console
+
+ ; In case PPU doesn't support scrolling, start a
+ ; couple of lines down
+ setb console_scroll,16
+
+ jsr console_hide
+ txa
+ pha
+
+ ; Fill nametable with spaces
+ setb PPUADDR,$20
+ setb PPUADDR,$00
+ ldx #240
+ ;lda #$E0
+ ;sta text_color
+ lda #0
+: sta PPUDATA
+ sta PPUDATA
+ sta PPUDATA
+ sta PPUDATA
+ dex
+ bne :-
+
+ ; Clear attributes
+ lda #0
+ ldx #$40
+: sta PPUDATA
+ dex
+ bne :-
+
+ pla
+ tax
+ jmp console_show
+
+
+; Shows console display
+; Preserved: X, Y
+console_show:
+ pha
+
+ txa
+ pha
+ tay
+ pha
+
+ jsr console_wait_vbl_
+ setb PPUMASK,PPUMASK_BG0
+
+ lda #$22 ; red
+ ldx #$27 ; green
+ ldy #$30 ; white
+ jsr console_load_palette_
+
+ pla
+ tay
+ pla
+ tax
+
+ jmp console_apply_scroll_
+
+
+; Shows console display
+; Preserved: X, Y
+console_show_nowait:
+ pha
+
+ txa
+ pha
+ tay
+ pha
+
+ setb PPUMASK,PPUMASK_BG0
+
+ lda #$22 ; red
+ ldx #$27 ; green
+ ldy #$30 ; white
+ jsr console_load_palette_
+
+ pla
+ tay
+ pla
+ tax
+
+ jmp console_apply_scroll_
+
+
+console_load_palette_:
+ pha
+ setb PPUADDR,$3F
+ setb PPUADDR,$00
+ setb PPUDATA,$0F ; black
+ pla
+ sta PPUDATA
+ stx PPUDATA
+ sty PPUDATA
+ rts
+
+
+; Prints char A to console. Will not appear until
+; a newline or flush occurs.
+; Preserved: A, X, Y
+console_print:
+ cmp #10
+ beq console_newline
+
+ stx console_temp
+
+ ; Newline if buf full and next char isn't space
+ ldx console_pos
+ bpl :+
+ cmp #' '
+ beq @ignore_space
+ ldx console_temp
+ jsr console_newline
+ stx console_temp
+ ldx console_pos
+:
+ ; Write to buffer
+ clc
+ adc text_color
+ sta console_buf+console_margin,x
+ dex
+ stx console_pos
+
+@ignore_space:
+ ldx console_temp
+ rts
+
+
+
+
+; Displays current line and starts new one
+; Preserved: A, X, Y
+console_newline:
+ pha
+ jsr console_wait_vbl_
+ jsr console_flush_
+ jsr console_clear_line_
+
+ ; Scroll up 8 pixels and clear one line AHEAD
+ lda console_scroll
+ jsr console_add_8_to_scroll_
+ sta console_scroll
+ jsr console_add_8_to_scroll_
+ jsr console_flush_a
+ jmp console_apply_scroll_
+
+
+; A = (A + 8) % 240
+console_add_8_to_scroll_:
+ cmp #240-8
+ bcc :+
+ adc #16-1;+1 for set carry
+: adc #8
+ rts
+
+
+console_clear_line_:
+ stx console_temp
+
+ ; Start new clear line
+ lda #0
+ ldx #console_buf_size-1
+: sta console_buf,x
+ dex
+ bpl :-
+ ldx #console_width-1
+ stx console_pos
+
+ ldx console_temp
+ rts
+
+
+; Displays current line's contents without scrolling.
+; Preserved: A, X, Y
+console_flush:
+ pha
+ jsr console_wait_vbl_
+ jsr console_flush_
+console_apply_scroll_:
+ lda #0
+ sta PPUADDR
+ sta PPUADDR
+
+ sta PPUSCROLL
+ lda console_scroll
+ jsr console_add_8_to_scroll_
+ jsr console_add_8_to_scroll_
+ sta PPUSCROLL
+
+ pla
+ rts
+
+console_flush_:
+ lda console_scroll
+console_flush_a:
+ ; Address line in nametable
+ sta console_temp
+ lda #$08
+ asl console_temp
+ rol a
+ asl console_temp
+ rol a
+ sta PPUADDR
+ lda console_temp
+ sta PPUADDR
+
+ ; Copy line
+ stx console_temp
+ ldx #console_buf_size-1
+: lda console_buf,x
+ sta PPUDATA
+ dex
+ bpl :-
+ ldx console_temp
+
+ rts
+
diff --git a/tests/cpu_exec_space/source/common/crc.s b/tests/cpu_exec_space/source/common/crc.s
new file mode 100644
index 0000000..de96c2a
--- /dev/null
+++ b/tests/cpu_exec_space/source/common/crc.s
@@ -0,0 +1,118 @@
+; CRC-32 checksum calculation
+
+zp_res checksum,4
+zp_byte checksum_temp
+zp_byte checksum_off_
+
+; Turns CRC updating on/off. Allows nesting.
+; Preserved: A, X, Y
+crc_off:
+ dec checksum_off_
+ rts
+
+crc_on: inc checksum_off_
+ beq :+
+ jpl internal_error ; catch unbalanced crc calls
+: rts
+
+
+; Initializes checksum module. Might initialize tables
+; in the future.
+init_crc:
+ jmp reset_crc
+
+
+; Clears checksum and turns it on
+; Preserved: X, Y
+reset_crc:
+ lda #0
+ sta checksum_off_
+ lda #$FF
+ sta checksum
+ sta checksum + 1
+ sta checksum + 2
+ sta checksum + 3
+ rts
+
+
+; Updates checksum with byte in A (unless disabled via crc_off)
+; Preserved: A, X, Y
+; Time: 357 clocks average
+update_crc:
+ bit checksum_off_
+ bmi update_crc_off
+update_crc_:
+ pha
+ stx checksum_temp
+ eor checksum
+ ldx #8
+@bit: lsr checksum+3
+ ror checksum+2
+ ror checksum+1
+ ror a
+ bcc :+
+ sta checksum
+ lda checksum+3
+ eor #$ED
+ sta checksum+3
+ lda checksum+2
+ eor #$B8
+ sta checksum+2
+ lda checksum+1
+ eor #$83
+ sta checksum+1
+ lda checksum
+ eor #$20
+: dex
+ bne @bit
+ sta checksum
+ ldx checksum_temp
+ pla
+update_crc_off:
+ rts
+
+
+; Prints checksum as 8-character hex value
+print_crc:
+ jsr crc_off
+
+ ; Print complement
+ ldx #3
+: lda checksum,x
+ eor #$FF
+ jsr print_hex
+ dex
+ bpl :-
+
+ jmp crc_on
+
+
+; EQ if checksum matches CRC
+; Out: A=0 and EQ if match, A>0 and NE if different
+; Preserved: X, Y
+.macro is_crc crc
+ jsr_with_addr is_crc_,{.dword crc}
+.endmacro
+
+is_crc_:
+ tya
+ pha
+
+ ; Compare with complemented checksum
+ ldy #3
+: lda (ptr),y
+ sec
+ adc checksum,y
+ bne @wrong
+ dey
+ bpl :-
+ pla
+ tay
+ lda #0
+ rts
+
+@wrong:
+ pla
+ tay
+ lda #1
+ rts
diff --git a/tests/cpu_exec_space/source/common/delay.s b/tests/cpu_exec_space/source/common/delay.s
new file mode 100644
index 0000000..5645a34
--- /dev/null
+++ b/tests/cpu_exec_space/source/common/delay.s
@@ -0,0 +1,190 @@
+; Delays in CPU clocks, milliseconds, etc. All routines are re-entrant
+; (no global data). No routines touch X or Y during execution.
+; Code generated by macros is relocatable; it contains no JMPs to itself.
+
+zp_byte delay_temp_ ; only written to
+
+; Delays n clocks, from 2 to 16777215
+; Preserved: A, X, Y, flags
+.macro delay n
+ .if (n) < 0 .or (n) = 1 .or (n) > 16777215
+ .error "Delay out of range"
+ .endif
+ delay_ (n)
+.endmacro
+
+
+; Delays n milliseconds (1/1000 second)
+; n can range from 0 to 1100.
+; Preserved: A, X, Y, flags
+.macro delay_msec n
+ .if (n) < 0 .or (n) > 1100
+ .error "time out of range"
+ .endif
+ delay ((n)*CLOCK_RATE+500)/1000
+.endmacro
+
+
+; Delays n microseconds (1/1000000 second).
+; n can range from 0 to 100000.
+; Preserved: A, X, Y, flags
+.macro delay_usec n
+ .if (n) < 0 .or (n) > 100000
+ .error "time out of range"
+ .endif
+ delay ((n)*((CLOCK_RATE+50)/100)+5000)/10000
+.endmacro
+
+.align 64
+
+; Delays A clocks + overhead
+; Preserved: X, Y
+; Time: A+25 clocks (including JSR)
+: sbc #7 ; carry set by CMP
+delay_a_25_clocks:
+ cmp #7
+ bcs :- ; do multiples of 7
+ lsr a ; bit 0
+ bcs :+
+: ; A=clocks/2, either 0,1,2,3
+ beq @zero ; 0: 5
+ lsr a
+ beq :+ ; 1: 7
+ bcc :+ ; 2: 9
+@zero: bne :+ ; 3: 11
+: rts ; (thanks to dclxvi for the algorithm)
+
+
+; Delays A*256 clocks + overhead
+; Preserved: X, Y
+; Time: A*256+16 clocks (including JSR)
+delay_256a_16_clocks:
+ cmp #0
+ bne :+
+ rts
+delay_256a_11_clocks_:
+: pha
+ lda #256-19-22
+ jsr delay_a_25_clocks
+ pla
+ clc
+ adc #-1&$FF
+ bne :-
+ rts
+
+
+; Delays A*65536 clocks + overhead
+; Preserved: X, Y
+; Time: A*65536+16 clocks (including JSR)
+delay_65536a_16_clocks:
+ cmp #0
+ bne :+
+ rts
+delay_65536a_11_clocks_:
+: pha
+ lda #256-19-22-13
+ jsr delay_a_25_clocks
+ lda #255
+ jsr delay_256a_11_clocks_
+ pla
+ clc
+ adc #-1&$FF
+ bne :-
+ rts
+
+max_short_delay = 41
+
+ ; delay_short_ macro jumps into these
+ .res (max_short_delay-12)/2,$EA ; NOP
+delay_unrolled_:
+ rts
+
+.macro delay_short_ n
+ .if n < 0 .or n = 1 .or n > max_short_delay
+ .error "Internal delay error"
+ .endif
+ .if n = 0
+ ; nothing
+ .elseif n = 2
+ nop
+ .elseif n = 3
+ sta 65536+17
+ lda #^(n - 15)
+ jsr delay_65536a_11_clocks_
+ ; +2 ensures remaining clocks is never 1
+ delay_nosave_ (((n - 15) & $FFFF) + 2)
+ .elseif n > 255+27
+ lda #>(n - 15)
+ jsr delay_256a_11_clocks_
+ ; +2 ensures remaining clocks is never 1
+ delay_nosave_ (<(n - 15) + 2)
+ .elseif n >= 27
+ lda #<(n - 27)
+ jsr delay_a_25_clocks
+ .else
+ delay_short_ n
+ .endif
+.endmacro
+
+.macro delay_ n
+ .if n > max_short_delay
+ php
+ pha
+ delay_nosave_ (n - 14)
+ pla
+ plp
+ .else
+ delay_short_ n
+ .endif
+.endmacro
+
diff --git a/tests/cpu_exec_space/source/common/devcart.bin b/tests/cpu_exec_space/source/common/devcart.bin
new file mode 100644
index 0000000..e8958c6
Binary files /dev/null and b/tests/cpu_exec_space/source/common/devcart.bin differ
diff --git a/tests/cpu_exec_space/source/common/macros.inc b/tests/cpu_exec_space/source/common/macros.inc
new file mode 100644
index 0000000..52982a6
--- /dev/null
+++ b/tests/cpu_exec_space/source/common/macros.inc
@@ -0,0 +1,169 @@
+; jxx equivalents to bxx
+.macpack longbranch
+
+; blt, bge equivalents to bcc, bcs
+.define blt bcc
+.define bge bcs
+.define jge jcs
+.define jlt jcc
+
+; Puts data in another segment
+.macro seg_data seg,data
+ .pushseg
+ .segment seg
+ data
+ .popseg
+.endmacro
+
+; Reserves size bytes in zeropage/bss for name.
+; If size is omitted, reserves one byte.
+.macro zp_res name,size
+ .ifblank size
+ zp_res name,1
+ .else
+ seg_data "ZEROPAGE",{name: .res size}
+ .endif
+.endmacro
+
+.macro bss_res name,size
+ .ifblank size
+ bss_res name,1
+ .else
+ seg_data "BSS",{name: .res size}
+ .endif
+.endmacro
+
+.macro nv_res name,size
+ .ifblank size
+ nv_res name,1
+ .else
+ seg_data "NVRAM",{name: .res size}
+ .endif
+.endmacro
+
+; Reserves one byte in zeropage for name (very common)
+.macro zp_byte name
+ seg_data "ZEROPAGE",{name: .res 1}
+.endmacro
+
+; Passes constant data to routine in addr
+; Preserved: A, X, Y
+.macro jsr_with_addr routine,data
+ .local Addr
+ pha
+ lda #Addr
+ sta addr+1
+ pla
+ jsr routine
+ seg_data "RODATA",{Addr: data}
+.endmacro
+
+; Calls routine multiple times, with A having the
+; value 'start' the first time, 'start+step' the
+; second time, up to 'end' for the last time.
+.macro for_loop routine,start,end,step
+ lda #start
+: pha
+ jsr routine
+ pla
+ clc
+ adc #step
+ cmp #<((end)+(step))
+ bne :-
+.endmacro
+
+; Calls routine n times. The value of A in the routine
+; counts from 0 to n-1.
+.macro loop_n_times routine,n
+ for_loop routine,0,n-1,+1
+.endmacro
+
+; Same as for_loop, except uses 16-bit value in YX.
+; -256 <= step <= 255
+.macro for_loop16 routine,start,end,step
+.if (step) < -256 || (step) > 255
+ .error "Step must be within -256 to 255"
+.endif
+ ldy #>(start)
+ lda #<(start)
+: tax
+ pha
+ tya
+ pha
+ jsr routine
+ pla
+ tay
+ pla
+ clc
+ adc #step
+.if (step) > 0
+ bcc :+
+ iny
+.else
+ bcs :+
+ dey
+.endif
+: cmp #<((end)+(step))
+ bne :--
+ cpy #>((end)+(step))
+ bne :--
+.endmacro
+
+; Copies byte from in to out
+; Preserved: X, Y
+.macro mov out, in
+ lda in
+ sta out
+.endmacro
+
+; Stores byte at addr
+; Preserved: X, Y
+.macro setb addr, byte
+ lda #byte
+ sta addr
+.endmacro
+
+; Stores word at addr
+; Preserved: X, Y
+.macro setw addr, word
+ lda #<(word)
+ sta addr
+ lda #>(word)
+ sta addr+1
+.endmacro
+
+; Loads XY with 16-bit immediate or value at address
+.macro ldxy Arg
+ .if .match( .left( 1, {Arg} ), # )
+ ldy #<(.right( .tcount( {Arg} )-1, {Arg} ))
+ ldx #>(.right( .tcount( {Arg} )-1, {Arg} ))
+ .else
+ ldy (Arg)
+ ldx (Arg)+1
+ .endif
+.endmacro
+
+; Increments word at Addr and sets Z flag appropriately
+; Preserved: A, X, Y
+.macro incw Addr
+ .local @incw_skip ; doesn't work, so HOW THE HELL DO YOU MAKE A LOCAL LABEL IN A MACRO THAT DOESN"T DISTURB INVOKING CODE< HUH?????? POS
+ inc Addr
+ bne @incw_skip
+ inc Addr+1
+@incw_skip:
+.endmacro
+
+; Increments XY as 16-bit register, in CONSTANT time.
+; Z flag set based on entire result.
+; Preserved: A
+; Time: 7 clocks
+.macro incxy7
+ iny ; 2
+ beq *+4 ; 3
+ ; -1
+ bne *+3 ; 3
+ ; -1
+ inx ; 2
+.endmacro
diff --git a/tests/cpu_exec_space/source/common/neshw.inc b/tests/cpu_exec_space/source/common/neshw.inc
new file mode 100644
index 0000000..bdd4136
--- /dev/null
+++ b/tests/cpu_exec_space/source/common/neshw.inc
@@ -0,0 +1,37 @@
+; NES I/O locations and masks
+
+; Clocks per second
+.ifndef CLOCK_RATE
+ CLOCK_RATE = 1789773 ; NTSC
+; CLOCK_RATE = 1662607 ; PAL
+.endif
+
+.ifndef BUILD_NSF
+
+; PPU
+PPUCTRL = $2000
+PPUMASK = $2001
+PPUSTATUS = $2002
+SPRADDR = $2003
+SPRDATA = $2004
+PPUSCROLL = $2005
+PPUADDR = $2006
+PPUDATA = $2007
+SPRDMA = $4014
+
+PPUCTRL_NMI = $80
+PPUMASK_BG0 = $0A
+PPUCTRL_8X8 = $00
+PPUCTRL_8X16 = $20
+PPUMASK_SPR = $14
+PPUMASK_BG0CLIP = $08
+
+.endif
+
+; APU
+SNDCHN = $4015
+JOY1 = $4016
+JOY2 = $4017
+SNDMODE = $4017
+
+SNDMODE_NOIRQ = $40
diff --git a/tests/cpu_exec_space/source/common/ppu.s b/tests/cpu_exec_space/source/common/ppu.s
new file mode 100644
index 0000000..21cad12
--- /dev/null
+++ b/tests/cpu_exec_space/source/common/ppu.s
@@ -0,0 +1,142 @@
+; PPU utilities
+
+bss_res ppu_not_present
+
+; Sets PPUADDR to w
+; Preserved: X, Y
+.macro set_ppuaddr w
+ bit PPUSTATUS
+ setb PPUADDR,>w
+ setb PPUADDR, 1789773
+ .error "Currently only supports NTSC"
+ .endif
+ delay ((n)*341)/3
+.endmacro
+
+
+; Waits for VBL then disables PPU rendering.
+; Preserved: A, X, Y
+disable_rendering:
+ pha
+ jsr wait_vbl_optional
+ setb PPUMASK,0
+ pla
+ rts
+
+
+; Fills first nametable with $00
+; Preserved: Y
+clear_nametable:
+ ldx #$20
+ bne clear_nametable_
+
+clear_nametable2:
+ ldx #$24
+clear_nametable_:
+ lda #0
+ jsr fill_screen_
+
+ ; Clear pattern table
+ ldx #64
+: sta PPUDATA
+ dex
+ bne :-
+ rts
+
+
+; Fills screen with tile A
+; Preserved: A, Y
+fill_screen:
+ ldx #$20
+ bne fill_screen_
+
+; Same as fill_screen, but fills other nametable
+fill_screen2:
+ ldx #$24
+fill_screen_:
+ stx PPUADDR
+ ldx #$00
+ stx PPUADDR
+ ldx #240
+: sta PPUDATA
+ sta PPUDATA
+ sta PPUDATA
+ sta PPUDATA
+ dex
+ bne :-
+ rts
+
+
+; Fills palette with $0F
+; Preserved: Y
+clear_palette:
+ set_ppuaddr $3F00
+ ldx #$20
+ lda #$0F
+: sta PPUDATA
+ dex
+ bne :-
+
+
+; Fills OAM with $FF
+; Preserved: Y
+clear_oam:
+ lda #$FF
+
+; Fills OAM with A
+; Preserved: A, Y
+fill_oam:
+ ldx #0
+ stx SPRADDR
+: sta SPRDATA
+ dex
+ bne :-
+ rts
+
+
+; Initializes wait_vbl_optional. Must be called before
+; using it.
+.align 32
+init_wait_vbl:
+ ; Wait for VBL flag to be set, or ~60000
+ ; clocks (2 frames) to pass
+ ldy #24
+ ldx #1
+ bit PPUSTATUS
+: bit PPUSTATUS
+ bmi @set
+ dex
+ bne :-
+ dey
+ bpl :-
+@set:
+ ; Be sure flag didn't stay set (in case
+ ; PPUSTATUS always has high bit set)
+ tya
+ ora PPUSTATUS
+ sta ppu_not_present
+ rts
+
+
+; Same as wait_vbl, but returns immediately if PPU
+; isn't working or doesn't support VBL flag
+; Preserved: A, X, Y
+.align 16
+wait_vbl_optional:
+ bit ppu_not_present
+ bmi :++
+ ; FALL THROUGH
+
+; Clears VBL flag then waits for it to be set.
+; Preserved: A, X, Y
+wait_vbl:
+ bit PPUSTATUS
+: bit PPUSTATUS
+ bpl :-
+: rts
diff --git a/tests/cpu_exec_space/source/common/print.s b/tests/cpu_exec_space/source/common/print.s
new file mode 100644
index 0000000..aba22a7
--- /dev/null
+++ b/tests/cpu_exec_space/source/common/print.s
@@ -0,0 +1,380 @@
+; Prints values in various ways to output,
+; including numbers and strings.
+
+newline = 10
+
+zp_byte print_temp_
+
+; Prints indicated register to console as two hex
+; chars and space
+; Preserved: A, X, Y, flags
+print_a:
+ php
+ pha
+print_reg_:
+ jsr print_hex
+ lda #' '
+ jsr print_char_
+ pla
+ plp
+ rts
+
+print_x:
+ php
+ pha
+ txa
+ jmp print_reg_
+
+print_y:
+ php
+ pha
+ tya
+ jmp print_reg_
+
+print_p:
+ php
+ pha
+ php
+ pla
+ jmp print_reg_
+
+print_s:
+ php
+ pha
+ txa
+ tsx
+ inx
+ inx
+ inx
+ inx
+ jsr print_x
+ tax
+ pla
+ plp
+ rts
+
+
+; Prints A as two hex characters, NO space after
+; Preserved: A, X, Y
+print_hex:
+ jsr update_crc
+
+ pha
+ lsr a
+ lsr a
+ lsr a
+ lsr a
+ jsr print_hex_nibble
+ pla
+
+ pha
+ and #$0F
+ jsr print_hex_nibble
+ pla
+ rts
+
+print_hex_nibble:
+ cmp #10
+ blt @digit
+ adc #6;+1 since carry is set
+@digit: adc #'0'
+ jmp print_char_
+
+
+; Prints character and updates checksum UNLESS
+; it's a newline.
+; Preserved: A, X, Y
+print_char:
+ cmp #newline
+ beq :+
+ jsr update_crc
+: pha
+ jsr print_char_
+ pla
+ rts
+
+
+; Prints space. Does NOT update checksum.
+; Preserved: A, X, Y
+print_space:
+ pha
+ lda #' '
+ jsr print_char_
+ pla
+ rts
+
+
+; Advances to next line. Does NOT update checksum.
+; Preserved: A, X, Y
+print_newline:
+ pha
+ lda #newline
+ jsr print_char_
+ pla
+ rts
+
+
+; Prints string
+; Preserved: A, X, Y
+.macro print_str str,str2,str3,str4,str5,str6,str7,str8,str9,str10,str11,str12,str13,str14,str15
+ jsr print_str_
+ .byte str
+ .ifnblank str2
+ .byte str2
+ .endif
+ .ifnblank str3
+ .byte str3
+ .endif
+ .ifnblank str4
+ .byte str4
+ .endif
+ .ifnblank str5
+ .byte str5
+ .endif
+ .ifnblank str6
+ .byte str6
+ .endif
+ .ifnblank str7
+ .byte str7
+ .endif
+ .ifnblank str8
+ .byte str8
+ .endif
+ .ifnblank str9
+ .byte str9
+ .endif
+ .ifnblank str10
+ .byte str10
+ .endif
+ .ifnblank str11
+ .byte str11
+ .endif
+ .ifnblank str12
+ .byte str12
+ .endif
+ .ifnblank str13
+ .byte str13
+ .endif
+ .ifnblank str14
+ .byte str14
+ .endif
+ .ifnblank str15
+ .byte str15
+ .endif
+ .byte 0
+.endmacro
+
+
+print_str_:
+ sta print_temp_
+
+ pla
+ sta addr
+ pla
+ sta addr+1
+
+ jsr inc_addr
+ jsr print_str_addr
+
+ lda print_temp_
+ jmp (addr)
+
+
+; Prints string at addr and leaves addr pointing to
+; byte AFTER zero terminator.
+; Preserved: A, X, Y
+print_str_addr:
+ pha
+ tya
+ pha
+
+ ldy #0
+ beq :+ ; always taken
+@loop: jsr print_char
+ jsr inc_addr
+: lda (addr),y
+ bne @loop
+
+ pla
+ tay
+ pla
+ ; FALL THROUGH
+
+; Increments 16-bit value in addr.
+; Preserved: A, X, Y
+inc_addr:
+ inc addr
+ beq :+
+ rts
+: inc addr+1
+ rts
+
+
+
+.pushseg
+.segment "RODATA"
+ ; >= 60000 ? (EA60)
+ ; >= 50000 ? (C350)
+ ; >= 40000 ? (9C40)
+ ; >= 30000 ? (7530)
+ ; >= 20000 ? (4E20)
+ ; >= 10000 ? (2710)
+digit10000_hi: .byte $00,$27,$4E,$75,$9C,$C3,$EA
+digit10000_lo: .byte $00,$10,$20,$30,$40,$50,$60
+ ; >= 9000 ? (2328 (hex))
+ ; >= 8000 ? (1F40 (hex))
+ ; >= 7000 ? (1B58 (hex))
+ ; >= 6000 ? (1770 (hex))
+ ; >= 5000 ? (1388 (hex))
+ ; >= 4000 ? (FA0 (hex))
+ ; >= 3000 ? (BB8 (hex))
+ ; >= 2000 ? (7D0 (hex))
+ ; >= 1000 ? (3E8 (hex))
+digit1000_hi: .byte $00,$03,$07,$0B,$0F,$13,$17,$1B,$1F,$23
+digit1000_lo: .byte $00,$E8,$D0,$B8,$A0,$88,$70,$58,$40,$28
+; >= 900 ? (384 (hex))
+; >= 800 ? (320 (hex))
+; >= 700 ? (2BC (hex))
+; >= 600 ? (258 (hex))
+; >= 500 ? (1F4 (hex))
+; >= 400 ? (190 (hex))
+; >= 300 ? (12C (hex))
+; >= 200 ? (C8 (hex))
+; >= 100 ? (64 (hex))
+digit100_hi: .byte $00,$00,$00,$01,$01,$01,$02,$02,$03,$03
+digit100_lo: .byte $00,$64,$C8,$2C,$90,$F4,$58,$BC,$20,$84
+.popseg
+
+.macro dec16_comparew table_hi, table_lo
+ .local @lt
+ cmp table_hi,y
+ bcc @lt
+ bne @lt ; only test the lo-part if hi-part is equal
+ pha
+ txa
+ cmp table_lo,y
+ pla
+@lt:
+.endmacro
+.macro do_digit table_hi, table_lo
+ pha
+ ; print Y as digit; put X in A and do SEC for subtraction
+ jsr @print_dec16_helper
+ sbc table_lo,y
+ tax
+ pla
+ sbc table_hi,y
+.endmacro
+
+; Prints A:X as 2-5 digit decimal value, NO space after.
+; A = high 8 bits, X = low 8 bits.
+print_dec16:
+ ora #0
+ beq @less_than_256
+
+ ldy #6
+ sty print_temp_
+
+ ; TODO: Use binary search?
+: dec16_comparew digit10000_hi,digit10000_lo
+ bcs @got10000
+ dey
+ bne :-
+ ;cpy print_temp_
+ ;beq @got10000
+@cont_1000:
+ ldy #9
+: dec16_comparew digit1000_hi,digit1000_lo
+ bcs @got1000
+ dey
+ bne :- ; Y = 0.
+ cpy print_temp_ ; zero print_temp_ = print zero-digits
+ beq @got1000
+@cont_100:
+ ldy #9
+: dec16_comparew digit100_hi,digit100_lo
+ bcs @got100
+ dey
+ bne :-
+ cpy print_temp_
+ beq @got100
+@got10000:
+ do_digit digit10000_hi,digit10000_lo
+ ; value is now 0000..9999
+ ldy #0
+ sty print_temp_
+ beq @cont_1000
+@got1000:
+ do_digit digit1000_hi,digit1000_lo
+ ; value is now 000..999
+ ldy #0
+ sty print_temp_
+ beq @cont_100
+@got100:
+ do_digit digit100_hi,digit100_lo
+ ; value is now 00..99
+ txa
+ jmp print_dec_00_99
+@less_than_256:
+ txa
+ jmp print_dec
+@print_dec16_helper:
+ tya
+ jsr print_digit
+ txa
+ sec
+ rts
+; Prints A as 2-3 digit decimal value, NO space after.
+; Preserved: Y
+print_dec:
+ ; Hundreds
+ cmp #10
+ blt print_digit
+ cmp #100
+ blt print_dec_00_99
+ ldx #'0'-1
+: inx
+ sbc #100
+ bge :-
+ adc #100
+ jsr print_char_x
+
+ ; Tens
+print_dec_00_99:
+ sec
+ ldx #'0'-1
+: inx
+ sbc #10
+ bge :-
+ adc #10
+ jsr print_char_x
+ ; Ones
+print_digit:
+ ora #'0'
+ jmp print_char
+ ; Print a single digit
+print_char_x:
+ pha
+ txa
+ jsr print_char
+ pla
+ rts
+
+
+; Prints one of two characters based on condition.
+; SEC; print_cc bcs,'C','-' prints 'C'.
+; Preserved: A, X, Y, flags
+.macro print_cc cond,yes,no
+ ; Avoids labels since they're not local
+ ; to macros in ca65.
+ php
+ pha
+ cond *+6
+ lda #no
+ bne *+4
+ lda #yes
+ jsr print_char
+ pla
+ plp
+.endmacro
+
diff --git a/tests/cpu_exec_space/source/common/shell.inc b/tests/cpu_exec_space/source/common/shell.inc
new file mode 100644
index 0000000..0f2557c
--- /dev/null
+++ b/tests/cpu_exec_space/source/common/shell.inc
@@ -0,0 +1,32 @@
+; Included at beginning of program
+
+.ifdef CUSTOM_PREFIX
+ .include "custom_prefix.s"
+.endif
+
+; Sub-test in a multi-test ROM
+.ifdef BUILD_MULTI
+ .include "build_multi.s"
+.else
+
+; NSF music file
+.ifdef BUILD_NSF
+ .include "build_nsf.s"
+.endif
+
+; Devcart
+.ifdef BUILD_DEVCART
+ .include "build_devcart.s"
+.endif
+
+; NES internal RAM
+.ifdef BUILD_NOCART
+ .include "build_nocart.s"
+.endif
+
+; NES ROM (default)
+.ifndef SHELL_INCLUDED
+ .include "build_rom.s"
+.endif
+
+.endif ; .ifdef BUILD_MULTI
diff --git a/tests/cpu_exec_space/source/common/shell.s b/tests/cpu_exec_space/source/common/shell.s
new file mode 100644
index 0000000..76cbe22
--- /dev/null
+++ b/tests/cpu_exec_space/source/common/shell.s
@@ -0,0 +1,331 @@
+; Common routines and runtime
+
+; Detect inclusion loops (otherwise ca65 goes crazy)
+.ifdef SHELL_INCLUDED
+ .error "shell.s included twice"
+ .end
+.endif
+SHELL_INCLUDED = 1
+
+;**** Special globals ****
+
+; Temporary variables that ANY routine might modify, so
+; only use them between routine calls.
+temp = <$A
+temp2 = <$B
+temp3 = <$C
+addr = <$E
+ptr = addr
+
+.segment "NVRAM"
+ ; Beginning of variables not cleared at startup
+ nvram_begin:
+
+;**** Code segment setup ****
+
+.segment "RODATA"
+ ; Any user code which runs off end might end up here,
+ ; so catch that mistake.
+ nop ; in case there was three-byte opcode before this
+ nop
+ jmp internal_error
+
+; Move code to $E200 ($200 bytes for text output)
+;.segment "DMC"
+; .res $2200
+
+; Devcart corrupts byte at $E000 when powering off
+.segment "CODE"
+ nop
+
+;**** Common routines ****
+
+.include "macros.inc"
+.include "neshw.inc"
+.include "print.s"
+.include "delay.s"
+.include "crc.s"
+.include "testing.s"
+
+.ifdef NEED_CONSOLE
+ .include "console.s"
+.else
+ ; Stubs so code doesn't have to care whether
+ ; console exists
+ console_init:
+ console_show:
+ console_hide:
+ console_print:
+ console_flush:
+ rts
+.endif
+
+.ifndef CUSTOM_PRINT
+ .include "text_out.s"
+
+ print_char_:
+ jsr write_text_out
+ jmp console_print
+
+ stop_capture:
+ rts
+
+.endif
+
+;**** Shell core ****
+
+.ifndef CUSTOM_RESET
+ reset:
+ sei
+ jmp std_reset
+.endif
+
+
+; Sets up hardware then runs main
+run_shell:
+ sei
+ cld ; unnecessary on NES, but might help on clone
+ ldx #$FF
+ txs
+ jsr init_shell
+ set_test $FF
+ jmp run_main
+
+
+; Initializes shell
+init_shell:
+ jsr clear_ram
+ jsr init_wait_vbl ; waits for VBL once here,
+ jsr wait_vbl_optional ; so only need to wait once more
+ jsr init_text_out
+ jsr init_testing
+ jsr init_runtime
+ jsr console_init
+ rts
+
+
+; Runs main in consistent PPU/APU environment, then exits
+; with code 0
+run_main:
+ jsr pre_main
+ jsr main
+ lda #0
+ jmp exit
+
+
+; Sets up environment for main to run in
+pre_main:
+
+.ifndef BUILD_NSF
+ jsr disable_rendering
+ setb PPUCTRL,0
+ jsr clear_palette
+ jsr clear_nametable
+ jsr clear_nametable2
+ jsr clear_oam
+.endif
+
+ lda #$34
+ pha
+ lda #0
+ tax
+ tay
+ jsr wait_vbl_optional
+ plp
+ sta SNDMODE
+ rts
+
+
+.ifndef CUSTOM_EXIT
+ exit:
+.endif
+
+; Reports result and ends program
+std_exit:
+ sei
+ cld
+ ldx #$FF
+ txs
+ pha
+
+ setb SNDCHN,0
+ .ifndef BUILD_NSF
+ setb PPUCTRL,0
+ .endif
+
+ pla
+ pha
+ jsr report_result
+ ;jsr clear_nvram ; TODO: was this needed for anything?
+ pla
+ jmp post_exit
+
+
+; Reports final result code in A
+report_result:
+ jsr :+
+ jmp play_byte
+
+: jsr print_newline
+ jsr console_show
+
+ ; 0: ""
+ cmp #1
+ bge :+
+ rts
+:
+ ; 1: "Failed"
+ bne :+
+ print_str {"Failed",newline}
+ rts
+
+ ; n: "Failed #n"
+: print_str "Failed #"
+ jsr print_dec
+ jsr print_newline
+ rts
+
+;**** Other routines ****
+
+; Reports internal error and exits program
+internal_error:
+ print_str newline,"Internal error"
+ lda #255
+ jmp exit
+
+
+.import __NVRAM_LOAD__, __NVRAM_SIZE__
+
+; Clears $0-($100+S) and nv_ram_end-$7FF
+clear_ram:
+ lda #0
+
+ ; Main pages
+ tax
+: sta 0,x
+ sta $300,x
+ sta $400,x
+ sta $500,x
+ sta $600,x
+ sta $700,x
+ inx
+ bne :-
+
+ ; Stack except that above stack pointer
+ tsx
+ inx
+: dex
+ sta $100,x
+ bne :-
+
+ ; BSS except nvram
+ ldx #<__NVRAM_SIZE__
+: sta __NVRAM_LOAD__,x
+ inx
+ bne :-
+
+ rts
+
+
+; Clears nvram
+clear_nvram:
+ ldx #<__NVRAM_SIZE__
+ beq @empty
+ lda #0
+: dex
+ sta __NVRAM_LOAD__,x
+ bne :-
+@empty:
+ rts
+
+
+; Prints filename and newline, if available, otherwise nothing.
+; Preserved: A, X, Y
+print_filename:
+ .ifdef FILENAME_KNOWN
+ pha
+ jsr print_newline
+ setw addr,filename
+ jsr print_str_addr
+ jsr print_newline
+ pla
+ .endif
+ rts
+
+.pushseg
+.segment "RODATA"
+ ; Filename terminated with zero byte.
+ filename:
+ .ifdef FILENAME_KNOWN
+ .incbin "ram:nes_temp"
+ .endif
+ .byte 0
+.popseg
+
+
+;**** ROM-specific ****
+.ifndef BUILD_NSF
+
+.include "ppu.s"
+
+avoid_silent_nsf:
+play_byte:
+ rts
+
+; Loads ASCII font into CHR RAM
+.macro load_ascii_chr
+ bit PPUSTATUS
+ setb PPUADDR,$00
+ setb PPUADDR,$00
+ setb addr,ascii_chr
+ ldy #0
+@page:
+ stx addr+1
+: lda (addr),y
+ sta PPUDATA
+ iny
+ bne :-
+ inx
+ cpx #>ascii_chr_end
+ bne @page
+.endmacro
+
+; Disables interrupts and loops forever
+.ifndef CUSTOM_FOREVER
+forever:
+ sei
+ lda #0
+ sta PPUCTRL
+: beq :-
+ .res $10,$EA ; room for code to run loader
+.endif
+
+
+; Default NMI
+.ifndef CUSTOM_NMI
+ zp_byte nmi_count
+
+ nmi:
+ inc nmi_count
+ rti
+
+ ; Waits for NMI. Must be using NMI handler that increments
+ ; nmi_count, with NMI enabled.
+ ; Preserved: X, Y
+ wait_nmi:
+ lda nmi_count
+ : cmp nmi_count
+ beq :-
+ rts
+.endif
+
+
+; Default IRQ
+.ifndef CUSTOM_IRQ
+ irq:
+ bit SNDCHN ; clear APU IRQ flag
+ rti
+.endif
+
+.endif
diff --git a/tests/cpu_exec_space/source/common/testing.s b/tests/cpu_exec_space/source/common/testing.s
new file mode 100644
index 0000000..ba41f03
--- /dev/null
+++ b/tests/cpu_exec_space/source/common/testing.s
@@ -0,0 +1,106 @@
+; Utilities for writing test ROMs
+
+; In NVRAM so these can be used before initializing runtime,
+; then runtime initialized without clearing them
+nv_res test_code ; code of current test
+nv_res test_name,2 ; address of name of current test, or 0 of none
+
+
+; Sets current test code and optional name. Also resets
+; checksum.
+; Preserved: A, X, Y
+.macro set_test code,name
+ pha
+ lda #code
+ jsr set_test_
+ .ifblank name
+ setb test_name+1,0
+ .else
+ .local Addr
+ setw test_name,Addr
+ seg_data "RODATA",{Addr: .byte name,0}
+ .endif
+ pla
+.endmacro
+
+set_test_:
+ sta test_code
+ jmp reset_crc
+
+
+; Initializes testing module
+init_testing:
+ jmp init_crc
+
+
+; Reports that all tests passed
+tests_passed:
+ jsr print_filename
+ print_str newline,"Passed"
+ lda #0
+ jmp exit
+
+
+; Reports "Done" if set_test has never been used,
+; "Passed" if set_test 0 was last used, or
+; failure if set_test n was last used.
+tests_done:
+ ldx test_code
+ jeq tests_passed
+ inx
+ bne test_failed
+ jsr print_filename
+ print_str newline,"Done"
+ lda #0
+ jmp exit
+
+
+; Reports that the current test failed. Prints code and
+; name last set with set_test, or just "Failed" if none
+; have been set yet.
+test_failed:
+ ldx test_code
+
+ ; Treat $FF as 1, in case it wasn't ever set
+ inx
+ bne :+
+ inx
+ stx test_code
+:
+ ; If code >= 2, print name
+ cpx #2-1 ; -1 due to inx above
+ blt :+
+ lda test_name+1
+ beq :+
+ jsr print_newline
+ sta addr+1
+ lda test_name
+ sta addr
+ jsr print_str_addr
+ jsr print_newline
+:
+ jsr print_filename
+
+ ; End program
+ lda test_code
+ jmp exit
+
+
+; If checksum doesn't match expected, reports failed test.
+; Clears checksum afterwards.
+; Preserved: A, X, Y
+.macro check_crc expected
+ jsr_with_addr check_crc_,{.dword expected}
+.endmacro
+
+check_crc_:
+ pha
+ jsr is_crc_
+ bne :+
+ jsr reset_crc
+ pla
+ rts
+
+: jsr print_newline
+ jsr print_crc
+ jmp test_failed
diff --git a/tests/cpu_exec_space/source/common/text_out.s b/tests/cpu_exec_space/source/common/text_out.s
new file mode 100644
index 0000000..3a4137f
--- /dev/null
+++ b/tests/cpu_exec_space/source/common/text_out.s
@@ -0,0 +1,61 @@
+; Text output as expanding zero-terminated string at text_out_base
+
+; The final exit result byte is written here
+final_result = $6000
+
+; Text output is written here as an expanding
+; zero-terminated string
+text_out_base = $6004
+
+bss_res text_out_temp
+zp_res text_out_addr,2
+
+init_text_out:
+ ldx #0
+
+ ; Put valid data first
+ setb text_out_base,0
+
+ lda #$80
+ jsr set_final_result
+
+ ; Now fill in signature that tells emulator there's
+ ; useful data there
+ setb text_out_base-3,$DE
+ setb text_out_base-2,$B0
+ setb text_out_base-1,$61
+
+ ldx #>text_out_base
+ stx text_out_addr+1
+ setb text_out_addr,".
+
+Several routines are available to print values and text to the console.
+Most update a running CRC-32 checksum which can be checked with
+check_crc, allowing ALL the output to be checked very easily. If the
+checksum doesn't match, it is printed, so you can run the code on a NES
+and paste the correct checksum into your code.
+
+The default is to build an iNES ROM, with other build types that I
+haven't documented (devcart, sub-test of a multi-test ROM, NSF music
+file). My nes.cfg file puts the code at $E000 since my devcart requires
+it, and I don't want the normal ROM to differ in any way from what I've
+tested.
+
+Library routines are organized by function into several files, each with
+short documentation. Each routine may also optionally list registers
+which are preserved, rather than those which are modified (trashed) as
+is more commonly done. This is because it's best for the caller to
+assume that ALL registers are NOT preserved unless noted.
+
+Some macros are used to make common operations more convenient. The left
+is equivalent to the right:
+
+ Macro Equivalent
+ -------------------------------------
+ blt bcc
+
+ bge bcs
+
+ jne label beq skip
+ jmp label
+ skip:
+ etc.
+
+ zp_byte name .zeropage
+ name: .res 1
+ .code
+
+ zp_res name,n .zeropage
+ name: .res n
+ .code
+
+ bss_res name,n .bss
+ name: .res n
+ .code
+
+ for_loop r,b,e,s calls a routine with A set to successive values
+
+--
+Shay Green
+
+Some tests might turn the screen off and on, since that affects the
+behavior being tested. This does not indicate failure, and should be
+ignored. Only the test result reported at the end is important.
+
+The error code at the end is also reported audibly with a series of
+tones, in case the picture isn't visible for some reason. The code is in
+binary, with a low tone indicating 0 and a high tone 1. The first tone
+is always a zero, so you can tell the difference. A code of 0 means
+passed, 1 means failure, and 2 or higher indicates a specific reason as
+listed in the source code by the corresponding set_code line. Examples:
+
+low = 0 = passed
+low high = 1 = failed
+low high low = 2 = error 2
+low high high = 3 = error 3
+
+See the source code for more information about a particular test and why
+it might be failing. Each test has comments and correct output at top.
+--
+Shay Green
diff --git a/tests/cpu_exec_space/source/test_cpu_exec_space_apu.s b/tests/cpu_exec_space/source/test_cpu_exec_space_apu.s
new file mode 100644
index 0000000..482a5a9
--- /dev/null
+++ b/tests/cpu_exec_space/source/test_cpu_exec_space_apu.s
@@ -0,0 +1,243 @@
+; Expected output, and explanation:
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; TEST: test_cpu_exec_space_apu
+; This program verifies that the
+; CPU can execute code from any
+; possible location that it can
+; address, including I/O space.
+;
+; In this test, it is also
+; verified that not only all
+; write-only APU I/O ports
+; return the open bus, but
+; also the unallocated I/O
+; space in $4018..$40FF.
+;
+; 40FF 40
+; Passed
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;
+; Written by Joel Yliluoma - http://iki.fi/bisqwit/
+
+.segment "LIB"
+.include "shell.inc"
+.include "colors.inc"
+.segment "CODE"
+
+zp_res nmi_count
+zp_res maybe_crashed
+
+zp_res temp_code,8
+zp_res console_save,2
+
+bss_res empty,$500
+
+.macro print_str_and_ret s,s2,s3,s4,s5,s6,s7,s8,s9,s10,s11,s12,s13,s14,s15
+ print_str s,s2,s3,s4,s5,s6,s7,s8,s9,s10,s11,s12,s13,s14,s15
+ rts
+.endmacro
+.macro my_print_str s,s2,s3,s4,s5,s6,s7,s8,s9,s10,s11,s12,s13,s14,s15
+ .local Addr
+ jsr Addr
+ seg_data "RODATA",{Addr: print_str_and_ret s,s2,s3,s4,s5,s6,s7,s8,s9,s10,s11,s12,s13,s14,s15}
+.endmacro
+
+set_vram_pos:
+ ldy PPUSTATUS
+ sta PPUADDR ; poke high 6 bits
+ stx PPUADDR ; poke low 8 bits
+ rts
+
+test_failed_finish:
+ jsr crash_proof_end
+ ; Re-enable screen
+ jsr console_show
+ text_white
+ jmp test_failed
+
+open_bus_pathological_fail:
+ jmp test_failed_finish
+
+main:
+ jsr intro
+
+ ; Disable all APU channels and frame IRQ (ensure that $4015 reads back as $00)
+ lda #$00
+ sta $4015
+ lda #$40
+ sta $4017
+
+ ldx #>empty
+ ldy #temp_code
+ jsr print_hex
+ lda #(:+ )
+ pha
+ lda #<(:+ )
+ pha
+ php
+ ;
+ lda #$4C ; jmp abs
+ sta temp_code+0
+ stx temp_code+1
+ sty temp_code+2
+ jmp temp_code
+: inx
+ bne @loop
+
+ text_white
+ jsr console_show
+ jsr wait_vbl
+ jmp tests_passed
+
+
+
+
+ .pushseg
+ .segment "RODATA"
+intro: text_white
+ print_str "TEST: test_cpu_exec_space_apu",newline
+ text_color1
+ jsr print_str_
+ ; 0123456789ABCDEF0123456789ABCD
+ .byte "This program verifies that the",newline
+ .byte "CPU can execute code from any",newline
+ .byte "possible location that it can",newline
+ .byte "address, including I/O space.",newline
+ .byte newline
+ .byte "In this test, it is also",newline
+ .byte "verified that not only all",newline
+ .byte "write-only APU I/O ports",newline
+ .byte "return the open bus, but",newline
+ .byte "also the unallocated I/O",newline
+ .byte "space in $4018..$40FF.",newline
+ .byte newline,0
+ text_white
+ rts
+ .popseg
+
+
+
+
+
+nmi:
+ pha
+ lda maybe_crashed
+ beq :+
+ inc nmi_count
+ lda nmi_count
+ cmp #4
+ bcc :+
+ jmp test_failed_finish
+:
+ pla
+ rti
+
+crash_proof_begin:
+ lda #$FF
+ sta nmi_count
+ sta maybe_crashed
+
+ ; Enable NMI
+ lda #$80
+ sta $2000
+ rts
+
+crash_proof_end:
+ ; Disable NMI
+ lda #0
+ sta $2000
+ sta maybe_crashed
+ rts
+
+irq:
+ ; Presume we got here through a BRK opcode.
+ ; Presumably, that opcode was placed in $8000..$E000 to trap wrong access.
+ plp
+wrong_code_executed_somewhere:
+ text_white
+ print_str "ERROR",newline
+ text_color1
+ ; 0123456789ABCDEF0123456789ABC|
+ print_str "Mysteriously Landed at $"
+ pla
+ tax
+ pla
+ jsr print_hex
+ txa
+ jsr print_hex
+ text_white
+ jsr print_str_
+ .byte newline
+ ; 0123456789ABCDEF0123456789ABCD
+ .byte "Program flow did not follow",newline
+ .byte "the planned path, for a number",newline
+ .byte "of different possible reasons.",newline
+ .byte 0
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ set_test 2,"Failure To Obey Predetermined Execution Path"
+ jmp test_failed_finish
+
+.pushseg
+.segment "WRONG_CODE_8000"
+ .repeat $6200
+ brk
+ .endrepeat
+ ; zero-fill
+.popseg
diff --git a/tests/cpu_exec_space/source/test_cpu_exec_space_ppuio.s b/tests/cpu_exec_space/source/test_cpu_exec_space_ppuio.s
new file mode 100644
index 0000000..3b75cd5
--- /dev/null
+++ b/tests/cpu_exec_space/source/test_cpu_exec_space_ppuio.s
@@ -0,0 +1,524 @@
+; Expected output, and explanation:
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+; TEST: test_cpu_exec_space
+; This program verifies that the
+; CPU can execute code from any
+; possible location that it can
+; address, including I/O space.
+;
+; In addition, it will be tested
+; that an RTS instruction does a
+; dummy read of the byte that
+; immediately follows the
+; instructions.
+;
+; JSR+RTS TEST OK
+; JMP+RTS TEST OK
+; RTS+RTS TEST OK
+; JMP+RTI TEST OK
+; JMP+BRK TEST OK
+;
+; Passed
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;
+; Written by Joel Yliluoma - http://iki.fi/bisqwit/
+
+.segment "LIB"
+.include "shell.inc"
+.include "colors.inc"
+.segment "CODE"
+
+zp_res nmi_count
+zp_res brk_issued
+zp_res maybe_crashed
+
+.macro print_str_and_ret s,s2,s3,s4,s5,s6,s7,s8,s9,s10,s11,s12,s13,s14,s15
+ print_str s,s2,s3,s4,s5,s6,s7,s8,s9,s10,s11,s12,s13,s14,s15
+ rts
+.endmacro
+.macro my_print_str s,s2,s3,s4,s5,s6,s7,s8,s9,s10,s11,s12,s13,s14,s15
+ .local Addr
+ jsr Addr
+ seg_data "RODATA",{Addr: print_str_and_ret s,s2,s3,s4,s5,s6,s7,s8,s9,s10,s11,s12,s13,s14,s15}
+.endmacro
+
+set_vram_pos:
+ ldy PPUSTATUS
+ sta PPUADDR ; poke high 6 bits
+ stx PPUADDR ; poke low 8 bits
+ rts
+
+test_failed_finish:
+ jsr crash_proof_end
+ ; Re-enable screen
+ jsr console_show
+ text_white
+ jmp test_failed
+
+open_bus_pathological_fail:
+ jmp test_failed_finish
+
+main:
+ lda #0
+ sta brk_issued
+
+ ; Operations we will be doing are:
+ ;
+ jsr intro
+
+ text_color2
+
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ set_test 2,"PPU memory access through $2007 does not work properly. (Use other tests to determine the exact problem.)"
+ jsr console_hide
+ jsr crash_proof_begin
+
+ ; Put byte $55 at $2400 and byte $AA at $2411.
+ lda #$24
+ ldx #$00
+ jsr set_vram_pos
+ ldy #$55
+ sty PPUDATA
+ ldx #$11
+ jsr set_vram_pos
+ ldy #$AA
+ sty PPUDATA
+ ; Read from $2400 and $2411.
+ lda #$24
+ ldx #$00
+ jsr set_vram_pos
+ ldy PPUDATA ; Discard the buffered byte; load $55 into buffer.
+ ldx #$11
+ jsr set_vram_pos
+ lda PPUDATA ; Load buffer ($55); place $AA in buffer.
+ cmp #$55
+ bne test_failed_finish
+ lda PPUDATA ; Load buffer ($AA); place unknown in buffer.
+ cmp #$AA
+ bne test_failed_finish
+
+ jsr crash_proof_end
+ jsr console_show
+
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ set_test 3,"PPU open bus implementation is missing or incomplete: A write to $2003, followed by a read from $2001 should return the same value as was written."
+ jsr wait_vbl
+ lda #$B2 ; sufficiently random byte.
+ sta $2003 ; OAM index, but also populates open bus
+ eor $2001
+ bne open_bus_pathological_fail
+
+ ; Set VRAM address ($2411). This is the address we will be reading from, if the test worked properly.
+ jsr console_hide
+ jsr crash_proof_begin
+
+ lda #$24
+ ldx #$00
+ jsr set_vram_pos
+
+ ; Now, set HALF of the other address ($2400). If the test did NOT work properly, this address will be read from.
+ lda #$24
+ sta PPUADDR
+
+ ; Poke the open bus again; it was wasted earlier.
+ lda #$60 ; rts
+ sta $2003 ; OAM index, but also populates open bus
+
+ set_test 4,"The RTS at $2001 was never executed."
+
+ jsr $2001 ; should fetch opcode from $2001, and do a dummy read at $2002
+
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ set_test 5,"An RTS opcode should still do a dummy fetch of the next opcode. (The same goes for all one-byte opcodes, really.)"
+
+ ; Poke the OTHER HALF of the address ($2411). If the RTS did a dummy read at $2002, as it should,
+ ; this ends up being a first HALF of a dummy address.
+ lda #$11
+ sta PPUADDR
+
+ ; Read from PPU.
+ lda PPUDATA ; Discard the buffered byte; load something into buffer
+ lda PPUDATA ; eject buffer. We should have $2400 contents ($55).
+ pha
+ jsr crash_proof_end
+ jsr console_show
+ pla
+ cmp #$55
+ beq passed_1
+ cmp #$AA ; $AA is the expected result if the dummy read was not implemented properly.
+ beq :+
+ ;
+ ; If we got neither $55 nor $AA, there is something else wrong.
+ ;
+ jsr print_hex
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ set_test 6,"I have no idea what happened, but the test did not work as supposed to. In any case, the problem is in the PPU."
+: jmp test_failed_finish
+
+passed_1:
+ ; ********* Do the test again, this time using JMP instead of JSR
+ print_str "JSR+RTS TEST OK",newline
+
+ jsr console_hide
+ ; Set VRAM address ($2411). This is the address we will be reading from, if the test worked properly.
+ jsr crash_proof_begin
+
+ lda #$24
+ ldx #$00
+ jsr set_vram_pos
+
+ ; Now, set HALF of the other address ($2400). If the test did NOT work properly, this address will be read from.
+ lda #$24
+ sta PPUADDR
+
+ ; Poke the open bus again; it was wasted earlier.
+ lda #$60 ; rts
+ sta $2003 ; OAM index, but also populates open bus
+
+ jsr do_jmp_test
+ ; should return here!
+
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ set_test 8,"Okay, the test passed when JSR was used, but NOT when the opcode was JMP. How can an emulator possibly get this result? You may congratulate yourself now, for finding something that is even more unconventional than this test."
+
+ ; Poke the OTHER HALF of the address ($2411). If the RTS did a dummy read at $2002, as it should,
+ ; this ends up being a first HALF of a dummy address.
+ lda #$11
+ sta PPUADDR
+
+ ; Read from PPU.
+ lda PPUDATA ; Discard the buffered byte; load something into buffer
+ lda PPUDATA ; eject buffer. We should have $2400 contents ($55).
+ pha
+ jsr crash_proof_end
+ jsr console_show
+ pla
+ cmp #$55
+ beq passed_2
+ cmp #$AA ; $AA is the expected result if the dummy read was not implemented properly.
+ beq :+
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ ;
+ ; If we got neither $55 nor $AA, there is something else wrong.
+ ;
+ jsr print_hex
+ set_test 9,"Your PPU is broken in mind-defyingly random ways."
+: jmp test_failed_finish
+
+
+do_jmp_test:
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ set_test 4,"The RTS at $2001 was never executed."
+ jmp $2001 ; should fetch opcode from $2001, and do a dummy read at $2002
+
+passed_2:
+ print_str "JMP+RTS TEST OK",newline
+ ; ********* Do the test once more, this time using RTS instead of JSR
+
+ jsr console_hide
+ ; Set VRAM address ($2411). This is the address we will be reading from, if the test worked properly.
+ jsr crash_proof_begin
+
+ lda #$24
+ ldx #$00
+ jsr set_vram_pos
+
+ ; Now, set HALF of the other address ($2400). If the test did NOT work properly, this address will be read from.
+ lda #$24
+ sta PPUADDR
+
+ ; Poke the open bus again; it was wasted earlier.
+ lda #$60 ; rts
+ sta $2003 ; OAM index, but also populates open bus
+
+ jsr do_rts_test
+ ; should return here!
+
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ set_test 11,"The test passed when JSR was used, and when JMP was used, but NOT when RTS was used. Caught ya! Paranoia wins."
+
+ ; Poke the OTHER HALF of the address ($2411). If the RTS did a dummy read at $2002, as it should,
+ ; this ends up being a first HALF of a dummy address.
+ lda #$11
+ sta PPUADDR
+
+ ; Read from PPU.
+ lda PPUDATA ; Discard the buffered byte; load something into buffer
+ lda PPUDATA ; eject buffer. We should have $2400 contents ($55).
+ pha
+ jsr crash_proof_end
+ jsr console_show
+ pla
+ cmp #$55
+ beq passed_3
+ cmp #$AA ; $AA is the expected result if the dummy read was not implemented properly.
+ beq :+
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ ;
+ ; If we got neither $55 nor $AA, there is something else wrong.
+ ;
+ jsr print_hex
+ set_test 12,"Your PPU gave up reason at the last moment."
+: jmp test_failed_finish
+
+
+do_rts_test:
+ set_test 10,"RTS to $2001 never returned." ; This message never gets displayed.
+ lda #$20
+ pha
+ lda #$00
+ pha
+ rts
+
+passed_3:
+ print_str "RTS+RTS TEST OK",newline
+ ; Do the second test (JMP) once more. This time, use RTI rather than RTI.
+
+ ; Set VRAM address ($2411). This is the address we will be reading from, if the test worked properly.
+ jsr console_hide
+ jsr crash_proof_begin
+
+ lda #$24
+ ldx #$00
+ jsr set_vram_pos
+
+ ; Now, set HALF of the other address ($2400). If the test did NOT work properly, this address will be read from.
+ lda #$24
+ sta PPUADDR
+
+ ; Poke the open bus again; it was wasted earlier.
+ lda #$40 ; rti
+ sta $2003 ; OAM index, but also populates open bus
+
+ set_test 13,"JMP to $2001 never returned." ; This message never gets displayed, either.
+
+ lda #>(:+ )
+ pha
+ lda #<(:+ )
+ pha
+ php
+ jmp $2001 ; should fetch opcode from $2001, and do a dummy read at $2002
+:
+
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ set_test 14,"An RTI opcode should still do a dummy fetch of the next opcode. (The same goes for all one-byte opcodes, really.)"
+
+ ; Poke the OTHER HALF of the address ($2411). If the RTI did a dummy read at $2002, as it should,
+ ; this ends up being a first HALF of a dummy address.
+ lda #$11
+ sta PPUADDR
+
+ ; Read from PPU.
+ lda PPUDATA ; Discard the buffered byte; load something into buffer
+ lda PPUDATA ; eject buffer. We should have $2400 contents ($55).
+ pha
+ jsr crash_proof_end
+ jsr console_show
+ pla
+ cmp #$55
+ beq passed_4
+ cmp #$AA ; $AA is the expected result if the dummy read was not implemented properly.
+ beq :+
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ ;
+ ; If we got neither $55 nor $AA, there is something else wrong.
+ ;
+ jsr print_hex
+ set_test 15,"An RTI opcode should not destroy the PPU. Somehow that still appears to be the case here."
+: jmp test_failed_finish
+
+passed_4:
+ print_str "JMP+RTI TEST OK",newline
+ ; ********* Do the test again, this time using BRK instead of RTS/RTI
+
+ jsr console_hide
+ jsr crash_proof_begin
+
+ ; Set VRAM address ($2411). This is the address we will be reading from, if the test worked properly.
+ lda #$24
+ ldx #$00
+ jsr set_vram_pos
+
+ ; Now, set HALF of the other address ($2400). If the test did NOT work properly, this address will be read from.
+ lda #$24
+ sta PPUADDR
+
+ ; Poke the open bus again; it was wasted earlier.
+ lda #$00 ; brk
+ sta $2003 ; OAM index, but also populates open bus
+
+ lda #1
+ sta brk_issued
+
+ set_test 17,"JSR to $2001 never returned." ; This message never gets displayed, either.
+ jmp $2001
+ nop
+ nop
+ nop
+ nop
+returned_from_brk:
+ nop
+ nop
+ nop
+ nop
+
+ ; should return here!
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ set_test 18,"The BRK instruction should issue an automatic fetch of the byte that follows right after the BRK. (The same goes for all one-byte opcodes, but with BRK it should be a bit more obvious than with others.)"
+
+ ; Poke the OTHER HALF of the address ($2411). If the BRK did a dummy read at $2002, as it should,
+ ; this ends up being a first HALF of a dummy address.
+ lda #$11
+ sta PPUADDR
+
+ ; Read from PPU.
+ lda PPUDATA ; Discard the buffered byte; load something into buffer
+ lda PPUDATA ; eject buffer. We should have $2400 contents ($55).
+ pha
+ jsr crash_proof_end
+ jsr console_show
+ pla
+ cmp #$55
+ beq passed_5
+ cmp #$AA ; $AA is the expected result if the dummy read was not implemented properly.
+ beq :+
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ ;
+ ; If we got neither $55 nor $AA, there is something else wrong.
+ ;
+ jsr print_hex
+ set_test 19,"A BRK opcode should not destroy the PPU. Somehow that still appears to be the case here."
+: jmp test_failed_finish
+
+
+passed_5:
+ print_str "JMP+BRK TEST OK",newline
+ text_white
+ jsr console_show
+ jsr wait_vbl
+ jmp tests_passed
+
+
+
+
+ .pushseg
+ .segment "RODATA"
+intro: text_white
+ print_str "TEST:test_cpu_exec_space_ppuio",newline
+ text_color1
+ jsr print_str_
+ ; 0123456789ABCDEF0123456789ABCD
+ .byte "This program verifies that the",newline
+ .byte "CPU can execute code from any",newline
+ .byte "possible location that it can",newline
+ .byte "address, including I/O space.",newline
+ .byte newline
+ .byte "In addition, it will be tested",newline
+ .byte "that an RTS instruction does a",newline
+ .byte "dummy read of the byte that",newline
+ .byte "immediately follows the",newline
+ .byte "instructions.",newline
+ .byte newline,0
+ text_white
+ rts
+ .popseg
+
+
+ ; Prospects (bleak) of improving this test:
+ ;
+ ; $2000 is write only (writing updates open_bus, reading returns open_bus)
+ ; $2001 is write only (writing updates open_bus, reading returns open_bus)
+ ; $2002 is read only (writing updates open_bus, reading UPDATES open_bus (but only for low 5 bits))
+ ; $2003 is write only (writing updates open_bus, reading returns open_bus)
+ ; $2004 is read-write (writing updates open_bus, however for %4==2, bitmask=11100011. Reading is UNRELIABLE.)
+ ; $2005 is write only (writing updates open_bus, reading returns open_bus)
+ ; $2006 is write only (writing updates open_bus, reading returns open_bus)
+ ; $2007 is read-write (writing updates open_bus, reading UPDATES open_bus)
+
+
+irq:
+ ; Presume we got here through a BRK opcode.
+ lda brk_issued
+ beq spurious_irq
+ cmp #1
+ beq brk_successful
+ ; If we got a spurious IRQ, and already warned of it once, do a regular RTI
+ rti
+spurious_irq:
+ lda #2
+ sta brk_issued
+ set_test 16,"IRQ occurred uncalled"
+ jmp test_failed_finish
+brk_successful:
+ jmp returned_from_brk
+
+
+
+nmi:
+ pha
+ lda maybe_crashed
+ beq :+
+ inc nmi_count
+ lda nmi_count
+ cmp #4
+ bcc :+
+ jmp test_failed_finish
+:
+ pla
+ rti
+
+crash_proof_begin:
+ lda #$FF
+ sta nmi_count
+ sta maybe_crashed
+
+ ; Enable NMI
+ lda #$80
+ sta $2000
+ rts
+
+crash_proof_end:
+ ; Disable NMI
+ lda #0
+ sta $2000
+ sta maybe_crashed
+ rts
+
+wrong_code_executed_somewhere:
+ pha
+ txa
+ pha
+
+ text_white
+ print_str "ERROR",newline
+
+ text_color1
+ print_str "Mysteriously Landed at $"
+ pla
+ jsr print_hex
+ pla
+ jsr print_hex
+ jsr print_newline
+ text_color1
+ ; 0123456789ABCDEF0123456789ABC|
+ print_str "CPU thinks we are at: $"
+ pla
+ tax
+ pla
+ jsr print_hex
+ txa
+ jsr print_hex
+ ; 0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|0123456789ABCDEF0123456789ABC|
+ set_test 7,"A jump to $2001 should never execute code from anywhere else than $2001"
+ jmp test_failed_finish
+
+.pushseg
+.segment "WRONG_CODE_8000"
+ .repeat $6200/8, I
+ .byt $EA
+ lda #<( $8001+ 8*I)
+ ldx #>( $8001+ 8*I)
+ jsr wrong_code_executed_somewhere
+ .endrepeat
+ ; CODE BEGINS AT E200
+.popseg
diff --git a/tests/cpu_exec_space/test_cpu_exec_space_apu.nes b/tests/cpu_exec_space/test_cpu_exec_space_apu.nes
new file mode 100644
index 0000000..db7559f
Binary files /dev/null and b/tests/cpu_exec_space/test_cpu_exec_space_apu.nes differ
diff --git a/tests/cpu_exec_space/test_cpu_exec_space_ppuio.nes b/tests/cpu_exec_space/test_cpu_exec_space_ppuio.nes
new file mode 100644
index 0000000..1c10b6c
Binary files /dev/null and b/tests/cpu_exec_space/test_cpu_exec_space_ppuio.nes differ