/* test-Z80 ______ ______ ______ /\___ \/\ __ \\ __ \ ____ \/__/ /\_\ __ \\ \/\ \ ________________________________________________ | /\_____\\_____\\_____\ | | Zilog \/_____//_____//_____/ CPU Emulator - Testing Tool | | Copyright (C) 2021-2026 Manuel Sainz de Baranda y Goñi. | | | | This program is free software: you can redistribute it and/or modify it | | under the terms of the GNU General Public License as published by the Free | | Software Foundation, either version 3 of the License, or (at your option) | | any later version. | | | | This program is distributed in the hope that it will be useful, but | | WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY | | or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | | for more details. | | | | You should have received a copy of the GNU General Public License along | | with this program. If not, see . | | | '=============================================================================*/ #ifdef TEST_Z80_WITH_ARCHIVE_EXTRACTION # include # include # include #endif #include #include #include #include #include #include #include /* MARK: - Types */ typedef struct { /* Name of the archive if the file is compressed; `Z_NULL` otherwise. */ char const *archive_name; /* Name of the file, or path to the file inside the archive if the file * is compressed. */ char const *file_path; /* Total number of clock cycles executed when the test passes. */ zusize cycles[1 + (Z_USIZE_WIDTH < 64)]; /* FNV-1 hash of the entire text output when the test passes * (i.e., of all bytes sent by the program to the print routine). */ zuint32 hash; /* Memory address to jump to in order to start executing the program. * The lower byte is zeroed to obtain the address where to load the * executable code. */ zuint16 start_address; /* Value of the PC register once the program completes. */ zuint16 exit_address; /* Size of the file. */ zuint16 file_size; /* Size of the executable code. */ zuint16 code_size; /* Offset of the executable code inside the file. */ zuint8 code_offset; /* Format of the program. */ zuint8 format; /* Number of lines printed when the test passes. */ zuint8 lines; /* Rightmost position reached by the cursor when the test passes. */ zuint8 columns; } Test; #define TEST_FORMAT_CPM 0 /* CP/M program in COM format. */ #define TEST_FORMAT_HARSTON 1 /* Z80 Instruction Set Exerciser for Spectrum. */ #define TEST_FORMAT_RAK 2 /* Patrik Rak's Zilog Z80 CPU Test Suite. */ #define TEST_FORMAT_WOODMASS 3 /* Mark Woodmass' Z80 Test Suite. */ /* MARK: - Global Variables */ #if Z_USIZE_WIDTH < 64 # define CYCLES(high, low) {Z_USIZE(0x##low), Z_USIZE(0x##high)} #else # define CYCLES(high, low) {Z_USIZE(0x##high##low)} #endif static Test const tests[22] = { {"Yaze v1.10 (1998-01-28)(Cringle, Frank D.)(Sources)[!].tar.gz", "yaze-1.10/test/zexdoc.com", CYCLES(A, E19F287A) /* 46,734,977,146 */, Z_UINT32(0xEDE3CB62), Z_UINT16(0x0100), Z_UINT16( 0), 8704, 8704, 0, TEST_FORMAT_CPM, 68, 34}, {Z_NULL, "Z80 Documented Instruction Set Exerciser for Spectrum (2018)(Harston, Jonathan Graham)[!].tap", CYCLES(A, E4E22836) /* 46,789,699,638 */, Z_UINT32(0x9F8B1839), Z_UINT16(0x8000), Z_UINT16(0x803D), 8716, 8624, 91, TEST_FORMAT_HARSTON, 69, 32}, {"Yaze v1.10 (1998-01-28)(Cringle, Frank D.)(Sources)[!].tar.gz", "yaze-1.10/test/zexall.com", CYCLES(A, E19F287A) /* 46,734,977,146 */, Z_UINT32(0xEDE3CB62), Z_UINT16(0x0100), Z_UINT16( 0), 8704, 8704, 0, TEST_FORMAT_CPM, 68, 34}, {Z_NULL, "Z80 Full Instruction Set Exerciser for Spectrum (2009)(Bobrowski, Jan)[!].tap", CYCLES(A, E4E1B837) /* 46,789,670,967 */, Z_UINT32(0xD4910BEE), Z_UINT16(0x8000), Z_UINT16(0x803D), 8656, 8547, 108, TEST_FORMAT_HARSTON, 69, 31}, {Z_NULL, "Z80 Full Instruction Set Exerciser for Spectrum (2011)(Bobrowski, Jan)(Narrowed to BIT Instructions)[!].tap", CYCLES(0, 4F67AEDF) /* 1,332,195,039 */, Z_UINT32(0x680D4830), Z_UINT16(0x8000), Z_UINT16(0x803D), 8656, 8547, 108, TEST_FORMAT_HARSTON, 4, 31}, {Z_NULL, "Z80 Full Instruction Set Exerciser for Spectrum (2017-0x)(Harston, Jonathan Graham)[!].tap", CYCLES(A, E4E20746) /* 46,789,691,206 */, Z_UINT32(0x9F50D128), Z_UINT16(0x8000), Z_UINT16(0x803D), 8704, 8612, 91, TEST_FORMAT_HARSTON, 69, 32}, {Z_NULL, "Z80 Full Instruction Set Exerciser for Spectrum (2018)(Harston, Jonathan Graham)[!].tap", CYCLES(A, E4E22836) /* 46,789,699,638 */, Z_UINT32(0x9F50D128), Z_UINT16(0x8000), Z_UINT16(0x803D), 8716, 8624, 91, TEST_FORMAT_HARSTON, 69, 32}, {"Z80 Instruction Set Exerciser for Spectrum 2 v0.1 (2012-11-27)(Rak, Patrik)[!].zip", "zexall2-0.1/zexall2.tap", CYCLES(C, 18A43876) /* 51,953,023,094 */, Z_UINT32(0x05C746F7), Z_UINT16(0x8000), Z_UINT16(0x8040), 9316, 9228, 87, TEST_FORMAT_HARSTON, 76, 31}, {Z_NULL, "Z80 Test Suite (2008)(Woodmass, Mark)[!].tap", CYCLES(0, 9C3040EF) /* 2,620,408,047 */, Z_UINT32(0xF787CA8E), Z_UINT16(0x8057), Z_UINT16(0x80E6), 5573, 5452, 120, TEST_FORMAT_WOODMASS, 50, 32}, {Z_NULL, "Z80 Test Suite (2008)(Woodmass, Mark)[!].tap", CYCLES(0, 0308BF63) /* 50,904,931 */, Z_UINT32(0xF5AE5140), Z_UINT16(0x8049), Z_UINT16(0x80E6), 5573, 5452, 120, TEST_FORMAT_WOODMASS, 61, 32}, {"Zilog Z80 CPU Test Suite v1.0 (2012-12-08)(Rak, Patrik)[!].zip", "z80test-1.0/z80full.tap", CYCLES(0, 4303ABE0) /* 1,124,314,097 */, Z_UINT32(0xB8707D12), Z_UINT16(0x8000), Z_UINT16( 0), 13758, 13666, 91, TEST_FORMAT_RAK, 156, 32}, {"Zilog Z80 CPU Test Suite v1.0 (2012-12-08)(Rak, Patrik)[!].zip", "z80test-1.0/z80doc.tap", CYCLES(0, 436E8254) /* 1,131,315,813 */, Z_UINT32(0x9E9DD1F5), Z_UINT16(0x8000), Z_UINT16( 0), 13758, 13666, 91, TEST_FORMAT_RAK, 156, 32}, {"Zilog Z80 CPU Test Suite v1.0 (2012-12-08)(Rak, Patrik)[!].zip", "z80test-1.0/z80flags.tap", CYCLES(0, 20ED11CB) /* 552,407,516 */, Z_UINT32(0x27CB27A2), Z_UINT16(0x8000), Z_UINT16( 0), 13758, 13666, 91, TEST_FORMAT_RAK, 156, 32}, {"Zilog Z80 CPU Test Suite v1.0 (2012-12-08)(Rak, Patrik)[!].zip", "z80test-1.0/z80docflags.tap", CYCLES(0, 2110B9A0) /* 554,744,241 */, Z_UINT32(0x3966C46C), Z_UINT16(0x8000), Z_UINT16( 0), 13758, 13666, 91, TEST_FORMAT_RAK, 156, 32}, {"Zilog Z80 CPU Test Suite v1.0 (2012-12-08)(Rak, Patrik)[!].zip", "z80test-1.0/z80ccf.tap", CYCLES(0, 23AB74B9) /* 598,439,114 */, Z_UINT32(0xB34ED107), Z_UINT16(0x8000), Z_UINT16( 0), 14219, 14127, 91, TEST_FORMAT_RAK, 156, 32}, {"Zilog Z80 CPU Test Suite v1.0 (2012-12-08)(Rak, Patrik)[!].zip", "z80test-1.0/z80memptr.tap", CYCLES(0, 215CF3AC) /* 559,739,837 */, Z_UINT32(0x840ACD96), Z_UINT16(0x8000), Z_UINT16( 0), 13758, 13666, 91, TEST_FORMAT_RAK, 156, 32}, {"Zilog Z80 CPU Test Suite v1.2a (2023-12-02)(Rak, Patrik)[!].zip", "z80test-1.2a/z80full.tap", CYCLES(0, 4382DC59) /* 1,132,649,578 */, Z_UINT32(0x4C578BC6), Z_UINT16(0x8000), Z_UINT16( 0), 14390, 14298, 91, TEST_FORMAT_RAK, 164, 32}, {"Zilog Z80 CPU Test Suite v1.2a (2023-12-02)(Rak, Patrik)[!].zip", "z80test-1.2a/z80doc.tap", CYCLES(0, 43EE72BD) /* 1,139,700,430 */, Z_UINT32(0x02114A09), Z_UINT16(0x8000), Z_UINT16( 0), 14390, 14298, 91, TEST_FORMAT_RAK, 164, 32}, {"Zilog Z80 CPU Test Suite v1.2a (2023-12-02)(Rak, Patrik)[!].zip", "z80test-1.2a/z80flags.tap", CYCLES(0, 212F17C4) /* 556,734,421 */, Z_UINT32(0x91826856), Z_UINT16(0x8000), Z_UINT16( 0), 14390, 14298, 91, TEST_FORMAT_RAK, 164, 32}, {"Zilog Z80 CPU Test Suite v1.2a (2023-12-02)(Rak, Patrik)[!].zip", "z80test-1.2a/z80docflags.tap", CYCLES(0, 2152FFC9) /* 559,087,578 */, Z_UINT32(0x408190F0), Z_UINT16(0x8000), Z_UINT16( 0), 14390, 14298, 91, TEST_FORMAT_RAK, 164, 32}, {"Zilog Z80 CPU Test Suite v1.2a (2023-12-02)(Rak, Patrik)[!].zip", "z80test-1.2a/z80ccf.tap", CYCLES(0, 23F34E32) /* 603,147,843 */, Z_UINT32(0x27FF6693), Z_UINT16(0x8000), Z_UINT16( 0), 14875, 14783, 91, TEST_FORMAT_RAK, 164, 32}, {"Zilog Z80 CPU Test Suite v1.2a (2023-12-02)(Rak, Patrik)[!].zip", "z80test-1.2a/z80memptr.tap", CYCLES(0, 219FC265) /* 564,118,134 */, Z_UINT32(0xDB7B18AA), Z_UINT16(0x8000), Z_UINT16( 0), 14390, 14298, 91, TEST_FORMAT_RAK, 164, 32}}; #undef CYCLES static struct {char const *key; zuint8 options;} const cpu_models[4] = { {"zilog-nmos", Z80_MODEL_ZILOG_NMOS}, {"zilog-cmos", Z80_MODEL_ZILOG_CMOS}, {"nec-nmos", Z80_MODEL_NEC_NMOS }, {"st-cmos", Z80_MODEL_ST_CMOS }}; static char const new_line[2] = "\n"; /*----------------------------------------------------------------------------. | `in_values` holds the bytes returned when reading from I/O ports: | | [0]: Byte read from even I/O ports (specified with the `--in-even` option). | | [1]: Byte read from odd I/O ports (specified with the `--in-odd` option). | | The default values correspond to those of a Sinclair ZX Spectrum 48K with | | no devices connected. | '============================================================================*/ static zuint8 in_values[2] = {191, 255}; /*---------------------------------------------------------------------------. | The search paths specified with the `--path` option are collected into the | | `search_paths` array of size `search_path_count`. `path_buffer` is used to | | compose complete file paths in the format "/". | '===========================================================================*/ static char* path_buffer = Z_NULL; static char** search_paths = Z_NULL; static zuint search_path_count = 0; /*--------------------------------------------------------------------------. | `verbosity` contains the verbosity level specified with the `--verbosity` | | option; `show_test_output` indicates whether the text output of the tests | | should be printed; `test_spacing` points to the string to print between | | each test: either "\n" or "". | '==========================================================================*/ static zuint8 verbosity = 4; static zbool show_test_output; static char const* test_spacing; /*---------------------------------------------------. | Instance of the Z80 emulator and 64 KiB of memory. | '===================================================*/ static Z80 cpu; static zuint8 memory[Z_USIZE(65536)]; /*----------------------------------------------------------------------------. | `run` points to the function used to run the emulator: either `z80_execute` | | or `z80_run`, depending on whether the `--run` option was specified. | '============================================================================*/ #ifdef TEST_Z80_WITH_EXECUTE static zusize (* run)(Z80 *self, zusize cycles) = z80_execute; # define RUN run #else # define RUN z80_run #endif /*-----------------------------------------------------------------------------. | `completed` indicates whether the test has reached its exit address; `lines` | | is incremented each time the test prints a new line; `cursor_x` holds the X | | position of the cursor in the current line; `columns` tracks the rightmost | | position reached by the cursor during the test; and `hash` stores the FNV-1 | | hash of all bytes sent by the test to the print routine. | '=============================================================================*/ static zbool completed; static zusize lines, cursor_x, columns; static zuint32 hash; /*-----------------------------------------------------------------------------. | `zx_spectrum_print_hook_address` contains the address of the hook that | | intercepts the routine called by the test to print characters. When a | | character (17h) is printed, the `zx_spectrum_tab` counter is set to 2 to | | indicate that the incoming and bytes must be processed | | before continuing to print characters. `zx_spectrum_bad_character` is set to | | `Z_TRUE` if the test prints any unsupported control characters. | | | | For more information about the TAB control sequence of the ZX Spectrum, see: | | * Sinclair Research (1983). "Sinclair ZX Spectrum BASIC Programming" 2nd | | ed., pp. 103, 196. | | * Ardley, Neil (1984). "ZX Spectrum + User Guide" (Dorling Kindersley; | | Sinclair Research. ISBN 0863180809), pp. 67-68. | | | | These three variables are used exclusively for ZX Spectrum tests. | '=============================================================================*/ static zuint16 zx_spectrum_print_hook_address; static zuint zx_spectrum_tab; static zbool zx_spectrum_bad_character; /* MARK: - CPU Callbacks: Auxiliary Functions */ static void lf(void) { if (show_test_output) putchar('\n'); if (cursor_x > columns) columns = cursor_x; cursor_x = 0; lines++; } /* MARK: - CPU Callbacks: Common */ static zuint8 cpu_read(void *context, zuint16 address) { Z_UNUSED(context) return memory[address]; } static zuint8 cpu_in(void *context, zuint16 port) { Z_UNUSED(context) return in_values[port & 1]; } static void cpu_out(void *context, zuint16 port, zuint8 value) {Z_UNUSED(context) Z_UNUSED(port) Z_UNUSED(value)} static void cpu_halt(void *context, zuint8 state) { Z_UNUSED(context) Z_UNUSED(state) completed = Z_TRUE; z80_break(&cpu); } /* MARK: - CPU Callbacks: CP/M */ static void cpm_cpu_write(void *context, zuint16 address, zuint8 value) { Z_UNUSED(context) memory[address] = value; } static zuint8 cpm_cpu_hook(void *context, zuint16 address) { zuint8 character; Z_UNUSED(context) if (address != 5) return Z80_NOP; /* BDOS function 2 (C_WRITE) - Console output */ if (Z80_C(cpu) == 2) { hash = Z_FNV1_32_UPDATE(hash, character = Z80_E(cpu)); switch (character) { case 0x0A: /* LF */ lf(); case 0x0D: /* CR */ break; default: if (show_test_output) putchar(character); cursor_x++; } } /* BDOS function 9 (C_WRITESTR) - Output string */ else if (Z80_C(cpu) == 9) { zuint16 i = Z80_DE(cpu); zuint c = 255; while (c--) { hash = Z_FNV1_32_UPDATE(hash, character = memory[i++]); switch (character) { case 0x24: /* $ */ return Z80_RET; case 0x0A: /* LF */ lf(); case 0x0D: /* CR */ break; default: if (show_test_output) putchar(character); cursor_x++; } } if (show_test_output) puts(" [TRUNCATED]"); } return Z80_RET; } /* MARK: - CPU Callbacks: ZX Spectrum */ static void zx_spectrum_cpu_write(void *context, zuint16 address, zuint8 value) { Z_UNUSED(context) if (address > 0x3FFF) memory[address] = value; } static zuint8 zx_spectrum_cpu_hook(void *context, zuint16 address) { zuint8 character; Z_UNUSED(context) if (address != zx_spectrum_print_hook_address) return Z80_NOP; hash = Z_FNV1_32_UPDATE(hash, character = Z80_A(cpu)); if (!zx_spectrum_tab) switch (character) { case 0x0D: /* ENTER */ lf(); break; case 0x17: /* TAB */ zx_spectrum_tab = 2; break; case 0x7F: /* © */ if (show_test_output) printf("©"); cursor_x++; break; default: if (character >= 32 && character < 127) { if (show_test_output) putchar(character); cursor_x++; } else zx_spectrum_bad_character = Z_TRUE; } else if (--zx_spectrum_tab) { zuint c = character & (32 - 1), x = cursor_x & (32 - 1); if (c < x) lf(); else cursor_x += (c -= x); if (show_test_output) while (c--) putchar(' '); } return Z80_RET; } /* Only needed for Woody's Z80 Test Suite. */ static zuint8 zx_spectrum_cpu_fetch_opcode(void *context, zuint16 address) { Z_UNUSED(context) return address == 0x1601 /* 1601: THE 'CHAN_OPEN' SUBROUTINE */ ? Z80_RET : memory[address]; } /* MARK: - Main Section */ static char const *compose_path(char const *base_path, char const *file_path) { zusize base_path_size; if (base_path == Z_NULL) return file_path; base_path_size = strlen(base_path); memcpy(path_buffer, base_path, base_path_size); path_buffer[base_path_size] = '/'; strcpy(path_buffer + base_path_size + 1, file_path); return path_buffer; } static zbool load_file( char const* base_path, char const* file_path, zuint16 file_size, zuint16 offset, zuint16 size, void* buffer ) { zbool success = Z_FALSE; FILE *file = fopen(compose_path(base_path, file_path), "rb"); if (file != Z_NULL) { if ( !fseek(file, 0, SEEK_END) && (zulong)ftell(file) == file_size && !fseek(file, offset, SEEK_SET) && fread(buffer, size, 1, file) == 1 ) success = Z_TRUE; fclose(file); } return success; } static zbool load_test(char const *search_path, Test const *test, void *buffer) { # ifdef TEST_Z80_WITH_ARCHIVE_EXTRACTION zbool success = load_file( search_path, test->file_path, test->file_size, test->code_offset, test->code_size, buffer); if (!success && test->archive_name != Z_NULL) { search_path = compose_path(search_path, test->archive_name); /* .tar.gz */ if (strrchr(test->archive_name, '.')[1] == 'g') { gzFile gz = gzopen(search_path, "rb"); if (gz != Z_NULL) { union {zuint8 data[Z_TAR_BLOCK_SIZE]; Z_TARPOSIXHeader header;} block; while (!gzeof(gz)) { char *end; zulong file_size; if (gzread(gz, block.data, Z_TAR_BLOCK_SIZE) != Z_TAR_BLOCK_SIZE) break; block.header.size[Z_ARRAY_SIZE(block.header.size) - 1] = 0; file_size = strtoul((char const *)block.header.size, &end, 8); if ((zuint8 *)end == block.header.size || *end) break; if (!strcmp(test->file_path, (char const *)block.header.name)) { success = file_size == test->file_size && gzseek(gz, (z_off_t)test->code_offset, SEEK_CUR) != -1 && (zuint)gzread(gz, buffer, test->code_size) == test->code_size; break; } if (file_size % Z_TAR_BLOCK_SIZE) file_size += Z_TAR_BLOCK_SIZE - (file_size % Z_TAR_BLOCK_SIZE); for (; file_size > (zulong)(Z_SINT_MAXIMUM / 2); file_size -= (zulong)(Z_SINT_MAXIMUM / 2) ) if (gzseek(gz, (z_off_t)(Z_SINT_MAXIMUM / 2), SEEK_CUR) == -1) goto close_gz; if (gzseek(gz, (z_off_t)file_size, SEEK_CUR) == -1) break; } close_gz: gzclose(gz); } } /* .zip */ else { int error; zip_t *zip = zip_open(search_path, ZIP_RDONLY | ZIP_CHECKCONS, &error); if (zip != Z_NULL) { zip_file_t *file; zip_stat_t stat; if ( !zip_stat(zip, test->file_path, ZIP_FL_ENC_STRICT, &stat) && (stat.valid & ZIP_STAT_SIZE) && stat.size == test->file_size && (file = zip_fopen(zip, test->file_path, 0)) != Z_NULL ) { if ( zip_fread(file, buffer, test->code_offset) == test->code_offset && zip_fread(file, buffer, test->code_size ) == test->code_size ) success = Z_TRUE; zip_fclose(file); } zip_close(zip); } } } return success; # else return load_file( search_path, test->file_path, test->file_size, test->code_offset, test->code_size, buffer); # endif } static zuint8 run_test(int test_index) { char const *failure; Test const *test = &tests[test_index]; zuint16 start_address = test->start_address; zusize cycles; zuint i; # if Z_USIZE_WIDTH < 64 zuint32 j; # endif if (verbosity) { printf("[%02d] ", test_index); if (verbosity >= 2) { if (test->archive_name == Z_NULL) printf("%s", test->file_path); else printf("%s/%s", test->archive_name, test->file_path); printf(verbosity == 2 ? "... " : "\n* Loading program... "); } } memset(memory, 0, Z_USIZE(65536)); for ( i = search_path_count; i && !load_test(search_paths[i - 1], test, &memory[start_address & Z_UINT16(0xFF00)]); i-- ); if (!i && !load_test(Z_NULL, test, &memory[start_address & Z_UINT16(0xFF00)])) { failure = "program"; error_loading_file: if (verbosity > 2) printf("Error\n%s", test_spacing); else if (verbosity) printf("Error: Cannot load %s\n%s", failure, test_spacing); return Z_FALSE; } if (verbosity >= 3) puts("OK"); z80_power(&cpu, Z_TRUE); if (test->format == TEST_FORMAT_CPM) { cpu.fetch_opcode = cpu_read; cpu.write = cpm_cpu_write; cpu.hook = cpm_cpu_hook; memory[0] = Z80_HALT; memory[5] = Z80_HOOK; /* PRINT */ } else { if (test->format == TEST_FORMAT_WOODMASS) { if (verbosity >= 3) printf("* Loading firmware... "); for ( i = search_path_count; i && !load_file(search_paths[i - 1], "ZX Spectrum.rom", 16384, 0, 16384, memory); i-- ); if (!i && !load_file(Z_NULL, "ZX Spectrum.rom", 16384, 0, 16384, memory)) { failure = "firmware"; goto error_loading_file; } if (verbosity >= 3) puts("OK"); cpu.fetch_opcode = zx_spectrum_cpu_fetch_opcode; Z80_SP(cpu) = 0x7FE8; Z80_AF(cpu) = 0x3222; /* 0010: THE 'PRINT A CHARACTER' RESTART */ memory[0x0010] = Z80_JP_WORD; /* jp PRINT */ memory[0x0011] = 0xF2; memory[0x0012] = 0x70; /* 70F2: PRINT */ memory[zx_spectrum_print_hook_address = 0x70F2] = Z80_HOOK; } else { cpu.fetch_opcode = cpu_read; if (test->format == TEST_FORMAT_RAK) Z80_SP(cpu) -= 2; /* 0010: THE 'PRINT A CHARACTER' RESTART */ memory[zx_spectrum_print_hook_address = 0x0010] = Z80_HOOK; /* 1601: THE 'CHAN_OPEN' SUBROUTINE */ memory[0x1601] = Z80_RET; } cpu.write = zx_spectrum_cpu_write; cpu.hook = zx_spectrum_cpu_hook; cpu.i = 0x3F; /* 0D6B: THE 'CLS' COMMAND ROUTINE */ memory[0x0D6B] = Z80_RET; } memory[test->exit_address] = Z80_HALT; Z80_PC(cpu) = start_address; hash = Z_FNV1_32_INITIALIZER; lines = columns = cursor_x = 0; zx_spectrum_tab = 0; zx_spectrum_bad_character = completed = Z_FALSE; if (verbosity >= 3) printf("* Running program%s", show_test_output ? ":\n\n" : "... "); # if Z_USIZE_WIDTH < 64 j = 0; for (i = 0; i < test->cycles[1];) { cycles = RUN(&cpu, Z_UINT32_MAXIMUM / 2); if (completed) { if ((j += (zuint32)cycles) < cycles) i++; cycles = j; goto check_results; } if ((j += (zuint32)cycles) < cycles) i++; } cycles = RUN(&cpu, test->cycles[0] + Z_UINT32(0x10000000) - j) + j; check_results: # else cycles = RUN(&cpu, test->cycles[0] + Z_USIZE(0x10000000)); # endif if (cursor_x > columns) columns = cursor_x; /*--------------------------------------------------------------------. | The test is considered passed if it has reached its exit address at | | the correct clock cycle, has not printed any unsupported characters | | and has produced the expected output within the correct margins. | '====================================================================*/ if (!completed) failure = "Aborted due to exceeding the clock cycle limit"; else if ( zx_spectrum_bad_character || hash != test->hash || lines != test->lines || columns != test->columns ) failure = "Incorrect behavior detected"; else if ( # if Z_USIZE_WIDTH < 64 i != test->cycles[1] || # endif cycles != test->cycles[0] ) failure = "Incorrect number of clock cycles"; else failure = Z_NULL; if (verbosity) { if (show_test_output) { zuint has_final_new_line = test->format == TEST_FORMAT_RAK; if (failure == Z_NULL) puts(&new_line[has_final_new_line]); else printf( "%s> Test failed: %s.\n\n", &new_line[!lines || (completed && has_final_new_line)], failure); } else { if (failure == Z_NULL) printf("Passed\n%s", test_spacing); else printf("Failed: %s\n%s", failure, test_spacing); } } return failure == Z_NULL; } static zbool string_is_option(char const *string, char const *option) {return (*string == *option && string[1] == '\0') || !strcmp(string, &option[1]);} static zbool string_to_uint8(char const *string, zuint8 maximum, zuint8 *value) { char *end; zulong parsed = strtoul(string, &end, 0); if (end == string || *end || parsed > maximum) return Z_FALSE; *value = (zuint8)parsed; return Z_TRUE; } int main(int argc, char **argv) { zbool all = Z_FALSE; zusize maximum_search_path_size = 0; zuint32 tests_run; int j, i = 0; char const *string; # define option string # define invalid string /* [0] = Number of tests failed. [1] = Number of tests passed. */ zuint results[2] = {0, 0}; /* The default CPU model to emulate is Zilog NMOS. */ cpu.options = Z80_MODEL_ZILOG_NMOS; /* Parse command line arguments. */ while (++i < argc && *argv[i] == '-') { option = &argv[i][1]; if (string_is_option(option, "V-version")) { puts( "test-Z80 v" Z80_LIBRARY_VERSION_STRING "\n" "Copyright (C) 2021-2026 Manuel Sainz de Baranda y Goñi.\n" "Released under the terms of the GNU General Public License v3."); goto exit_without_error; } else if (string_is_option(option, "h-help")) { puts( "Usage:\n" " test-Z80 [options] --all [...]\n" " test-Z80 [options] ...\n" "\n" "Options:\n" " -V, --version Print version information and exit.\n" " -0, --in-even (0..255) Set the byte read from even I/O ports [default: 191].\n" " -1, --in-odd (0..255) Set the byte read from odd I/O ports [default: 255].\n" " -a, --all Run all tests.\n" " -h, --help Print this help message and exit.\n" " -m, --model Specify the CPU model to emulate.\n" " -p, --path Add a path where to look for the required files.\n" # ifdef TEST_Z80_WITH_EXECUTE " -r, --run Emulate using `z80_run` instead of `z80_execute`.\n" # endif " -v, --verbosity (0..4) Set the verbosity level [default: 4].\n" "\n" "CPU models:\n" " zilog-nmos Zilog NMOS [default]\n" " zilog-cmos Zilog CMOS\n" " nec-nmos NEC NMOS\n" " st-cmos SGS-Thomson CMOS\n" "\n" "Tests:\n" " Versions of the Z80 Documented Instruction Set Exerciser:\n" " 00 CP/M ~ Cringle, Frank D. (1998-01-28).\n" " 01 ZX Spectrum ~ Harston, Jonathan Graham (2018).\n" " Versions of the Z80 Full Instruction Set Exerciser:\n" " 02 CP/M ~ Cringle, Frank D. (1998-01-28).\n" " 03 ZX Spectrum ~ Bobrowski, Jan (2009).\n" " 04 ZX Spectrum ~ Bobrowski, Jan (2011). Narrowed to `bit` instructions.\n" " 05 ZX Spectrum ~ Harston, Jonathan Graham (2017).\n" " 06 ZX Spectrum ~ Harston, Jonathan Graham (2018).\n" " 07 ZX Spectrum ~ Rak, Patrik (2012-11-27).\n" " Z80 Test Suite ~ ZX Spectrum ~ Woodmass, Mark (2008):\n" " 08 Flags test.\n" " 09 MEMPTR test.\n" " Zilog Z80 CPU Test Suite v1.0 ~ ZX Spectrum ~ Rak, Patrik (2012-12-08):\n" " 10 Tests all flags and registers.\n" " 11 Tests all registers, but only officially documented flags.\n" " 12 Tests all flags, ignores registers.\n" " 13 Tests documented flags only, ignores registers.\n" " 14 Tests all flags after executing `ccf` after each instruction.\n" " 15 Tests all flags after executing `bit N,(hl)` after each instruction.\n" " Zilog Z80 CPU Test Suite v1.2a ~ ZX Spectrum ~ Rak, Patrik (2023-12-02):\n" " 16 Tests all flags and registers.\n" " 17 Tests all registers, but only officially documented flags.\n" " 18 Tests all flags, ignores registers.\n" " 19 Tests documented flags only, ignores registers.\n" " 20 Tests all flags after executing `ccf` after each instruction.\n" " 21 Tests all flags after executing `bit N,(hl)` after each instruction.\n" "\n" "Email bug reports to \n" "Open issues at "); goto exit_without_error; } else if (string_is_option(option, "0-in-even")) { if (++i == argc) goto incomplete_option; if (!string_to_uint8(argv[i], 255, &in_values[0])) goto invalid_io_value; } else if (string_is_option(option, "1-in-odd")) { if (++i == argc) goto incomplete_option; if (!string_to_uint8(argv[i], 255, &in_values[1])) goto invalid_io_value; } else if (string_is_option(option, "a-all")) all = Z_TRUE; else if (string_is_option(option, "m-model")) { if (++i == argc) goto incomplete_option; for (j = 0; j < (int)Z_ARRAY_SIZE(cpu_models); j++) if (!strcmp(argv[i], cpu_models[j].key)) { cpu.options = cpu_models[j].options; goto cpu_model_found; } invalid = "CPU model"; goto invalid_argument; cpu_model_found: continue; } else if (string_is_option(option, "p-path")) { char **p; zusize s; if (++i == argc) goto incomplete_option; if (!(s = strlen(argv[i]))) { invalid = "path"; goto invalid_argument; } if (s > maximum_search_path_size) maximum_search_path_size = s; if ((p = realloc(search_paths, (search_path_count + 1) * sizeof(char *))) == Z_NULL) goto cannot_allocate_memory; search_paths = p; search_paths[search_path_count++] = argv[i]; } # ifdef TEST_Z80_WITH_EXECUTE else if (string_is_option(option, "r-run")) run = z80_run; # endif else if (string_is_option(option, "v-verbosity")) { if (++i == argc) goto incomplete_option; if (!string_to_uint8(argv[i], 4, &verbosity)) { invalid = "verbosity level"; goto invalid_argument; } } else { invalid = "option"; goto invalid_argument; } } /*---------------------------------------------------. | The user must specify at least one test number or, | | alternatively, pass the `--all` option. | '===================================================*/ if (i == argc && !all) { fputs("test-Z80: No test specified.\n", stderr); goto bad_syntax; } /*-----------------------------------------------------. | All test numbers specified by the user must be valid | | and must be decimal. Leading zeros are allowed. | '=====================================================*/ for (j = i; i < argc; i++) { char *end; if (strtoul(argv[i], &end, 10) >= Z_ARRAY_SIZE(tests) || end == argv[i] || *end) { invalid = "test number"; goto invalid_argument; } } if ( search_path_count && (path_buffer = malloc(maximum_search_path_size + 110)) == Z_NULL ) { cannot_allocate_memory: fputs("test-Z80: Cannot allocate memory.\n", stderr); goto exit_with_error; } /*----------------------------------------------------------------. | Disable buffering on stdout when the verbosity level is greater | | than 0 to ensure progress messages are displayed immediately. | '================================================================*/ if (verbosity) setvbuf(stdout, Z_NULL, _IONBF, 0); /*--------------------------------------------------------------. | The output of the test programs is only printed at verbosity | | level 4. For levels 3 and 4, an additional line feed is added | | between tests to ensure proper formatting. | '==============================================================*/ show_test_output = verbosity == 4; test_spacing = &new_line[verbosity < 3]; /*---------------------------------------------------------------------. | Initialize the Z80 emulator: | | * `context` is not needed, as global variables are used. | | * `fetch` and `read` use the same function because it is unnecessary | | to distinguish between different types of M-cycles when reading | | memory, as the test programs do not require precision down to the | | T-state level. | | * `nmia`, `inta`, and `int_fetch` will never be called because the | | test programs do not require interrupts. | | * `nop`, `ld_i_a`, `ld_r_a`, `reti`, `retn`, and `illegal` are also | | not needed. | | * `fetch_opcode`, `write`, and `hook` will be set by `run_test`. | '=====================================================================*/ cpu.context = Z_NULL; cpu.fetch = cpu.read = cpu_read; cpu.in = cpu_in; cpu.out = cpu_out; cpu.halt = cpu_halt; cpu.nop = cpu.nmia = cpu.inta = cpu.int_fetch = Z_NULL; cpu.ld_i_a = cpu.ld_r_a = cpu.reti = cpu.retn = Z_NULL; cpu.illegal = Z_NULL; /*------------------------------------------------------------. | Run the tests whose numbers have been specified. Then, if | | the user has passed the `--all` option, run all the others. | '============================================================*/ for (tests_run = 0; j < argc;) { tests_run |= Z_UINT32(1) << (i = atoi(argv[j++])); results[run_test(i)]++; } if (all) for (i = 0; i < (int)Z_ARRAY_SIZE(tests); i++) if (!(tests_run & (Z_UINT32(1) << i))) results[run_test(i)]++; /* Print the results. */ printf( "%sResults%s: %u test%s passed, %u failed.\n", &new_line[!verbosity || *test_spacing], show_test_output ? " summary" : "", results[1], results[1] == 1 ? "" : "s", results[0]); exit_without_error: free(search_paths); free(path_buffer); return results[0] ? -1 : 0; incomplete_option: fprintf(stderr, "test-Z80: Incomplete option: %s\n", argv[i - 1]); goto bad_syntax; invalid_io_value: invalid = "I/O value"; invalid_argument: if (*argv[i] != '\0') fprintf(stderr, "test-Z80: Invalid %s: %s\n", invalid, argv[i]); else fprintf(stderr, "test-Z80: Bad syntax: Empty %s.\n", invalid); bad_syntax: fputs("test-Z80: Type 'test-Z80 -h' for help.\n", stderr); exit_with_error: free(search_paths); free(path_buffer); return -1; } /* test-Z80.c EOF */