// // Z80ContentionTests.cpp // Clock SignalTests // // Created by Thomas Harte on 7/4/2021. // Copyright © 2021 Thomas Harte. All rights reserved. // #import #include "../../../Processors/Z80/Z80.hpp" namespace { static constexpr uint16_t initial_pc = 0; struct CapturingZ80: public CPU::Z80::BusHandler { template CapturingZ80(const Collection &code) : z80_(*this) { // Take a copy of the code. std::copy(code.begin(), code.end(), ram_); // Skip the three cycles the Z80 spends on a reset, and // purge them from the record. run_for(3); bus_records_.clear(); // Set the refresh address to the EE page. z80_.set_value_of_register(CPU::Z80::Register::I, 0xe0); } void run_for(int cycles) { z80_.run_for(HalfCycles(Cycles(cycles))); } /// A record of the state of the address bus, MREQ, IOREQ and RFSH lines, /// upon every clock transition. struct BusRecord { uint16_t address = 0xffff; bool mreq = false, ioreq = false, refresh = false; }; HalfCycles perform_machine_cycle(const CPU::Z80::PartialMachineCycle &cycle) { // Log the activity. const uint8_t* const bus_state = cycle.bus_state(); for(int c = 0; c < cycle.length.as(); c++) { bus_records_.emplace_back(); // TODO: I think everything tested here should have an address, // but am currently unsure whether the reset program puts the // address bus in high impedance, as bus req/ack does. if(cycle.address) { bus_records_.back().address = *cycle.address; } bus_records_.back().mreq = bus_state[c] & CPU::Z80::PartialMachineCycle::Line::MREQ; bus_records_.back().ioreq = bus_state[c] & CPU::Z80::PartialMachineCycle::Line::IOREQ; bus_records_.back().refresh = bus_state[c] & CPU::Z80::PartialMachineCycle::Line::RFSH; } // Provide only reads. if( cycle.operation == CPU::Z80::PartialMachineCycle::Read || cycle.operation == CPU::Z80::PartialMachineCycle::ReadOpcode ) { *cycle.value = ram_[*cycle.address]; } return HalfCycles(0); } const std::vector &bus_records() const { return bus_records_; } std::vector cycle_records() const { std::vector cycle_records; for(size_t c = 0; c < bus_records_.size(); c += 2) { cycle_records.push_back(bus_records_[c]); } return cycle_records; } private: CPU::Z80::Processor z80_; uint8_t ram_[65536]; std::vector bus_records_; }; } @interface Z80ContentionTests : XCTestCase @end /*! Tests the Z80's MREQ, IOREQ and address outputs for correlation to those observed by ZX Spectrum users in the software-side documentation of contended memory timings. */ @implementation Z80ContentionTests struct ContentionCheck { uint16_t address; int length; }; /*! Checks that the accumulated bus activity in @c z80 matches the expectations given in @c contentions if processed by a Sinclair 48k or 128k ULA. */ - (void)validate48Contention:(const std::initializer_list &)contentions z80:(const CapturingZ80 &)z80 { // 48[/128]k contention logic: triggered on address alone, _unless_ // MREQ is also active. auto contention = contentions.begin(); const auto bus_records = z80.cycle_records(); int count = 0; uint16_t address = bus_records.front().address; for(const auto &record: bus_records) { ++count; const bool is_final = &record == &bus_records.back(); if( &record != &bus_records.front() && ( // i.e. not at front. is_final || !record.mreq // i.e. beginning of a new contention. ) ) { XCTAssertNotEqual(contention, contentions.end()); XCTAssertEqual(contention->address, address); XCTAssertEqual(contention->length, count - !is_final); // Subtract 1 if not at end, because in that case this cycle // is one after the previous group ended. ++contention; count = 1; address = record.address; } } XCTAssertEqual(contention, contentions.end()); } /*! Checks that the accumulated bus activity in @c z80 matches the expectations given in @c contentions if processed by an Amstrad gate array. */ - (void)validatePlus3Contention:(const std::initializer_list &)contentions z80:(const CapturingZ80 &)z80 { // +3 contention logic: triggered by the leading edge of MREQ, sans refresh. auto contention = contentions.begin(); const auto bus_records = z80.bus_records(); int count = 0; uint16_t address = bus_records.front().address; for(size_t c = 0; c < bus_records.size(); c += 2) { ++count; const bool is_leading_edge = !bus_records[c].mreq && bus_records[c+1].mreq && !bus_records[c].refresh; const bool is_final = c == bus_records.size() - 2; if( c && ( // i.e. not at front. is_final || is_leading_edge // i.e. beginning of a new contention. ) ) { XCTAssertNotEqual(contention, contentions.end()); XCTAssertEqual(contention->address, address); XCTAssertEqual(contention->length, count - !is_final); // See validate48Contention for exposition. ++contention; count = 1; address = bus_records[c].address; } } XCTAssertEqual(contention, contentions.end()); } // MARK: - Opcode tests. - (void)testSimpleOneBytes { for(uint8_t opcode : { 0x00, // NOP // LD r, r'. 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6f, // ALO a, r 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbf, // INC/DEC r 0x04, 0x05, 0x0c, 0x0d, 0x14, 0x15, 0x1c, 0x1d, 0x24, 0x25, 0x2c, 0x2d, 0xd9, // EXX 0x08, // EX AF, AF' 0xeb, // EX DE, HL 0x27, // DAA 0x2f, // CPL 0x3f, // CCF 0x37, // SCF 0xf3, // DI 0xfb, // EI 0x17, // RLA 0x1f, // RRA 0x07, // RLCA 0x0f, // RRCA 0xe9, // JP (HL) }) { const std::initializer_list opcodes = {opcode}; CapturingZ80 z80(opcodes); z80.run_for(4); [self validate48Contention:{{initial_pc, 4}} z80:z80]; [self validatePlus3Contention:{{initial_pc, 4}} z80:z80]; } } - (void)testSimpleTwoBytes { // This group should apparently also include 'NOPD'. Neither I nor any other // page I could find seems to have heard of 'NOPD'. for(const auto &sequence : std::vector>{ // SRO d (i.e. RLC, RRC, RL, RR, SLA, SRA, SRL and SLL) {0xcb, 0x00}, {0xcb, 0x01}, {0xcb, 0x02}, {0xcb, 0x03}, {0xcb, 0x04}, {0xcb, 0x05}, {0xcb, 0x07}, {0xcb, 0x08}, {0xcb, 0x09}, {0xcb, 0x0a}, {0xcb, 0x0b}, {0xcb, 0x0c}, {0xcb, 0x0d}, {0xcb, 0x0f}, {0xcb, 0x10}, {0xcb, 0x11}, {0xcb, 0x12}, {0xcb, 0x13}, {0xcb, 0x14}, {0xcb, 0x15}, {0xcb, 0x17}, {0xcb, 0x18}, {0xcb, 0x19}, {0xcb, 0x1a}, {0xcb, 0x1b}, {0xcb, 0x1c}, {0xcb, 0x1d}, {0xcb, 0x1f}, {0xcb, 0x20}, {0xcb, 0x21}, {0xcb, 0x22}, {0xcb, 0x23}, {0xcb, 0x24}, {0xcb, 0x25}, {0xcb, 0x27}, {0xcb, 0x28}, {0xcb, 0x29}, {0xcb, 0x2a}, {0xcb, 0x2b}, {0xcb, 0x2c}, {0xcb, 0x2d}, {0xcb, 0x2f}, {0xcb, 0x30}, {0xcb, 0x31}, {0xcb, 0x32}, {0xcb, 0x33}, {0xcb, 0x34}, {0xcb, 0x35}, {0xcb, 0x37}, {0xcb, 0x38}, {0xcb, 0x39}, {0xcb, 0x3a}, {0xcb, 0x3b}, {0xcb, 0x3c}, {0xcb, 0x3d}, {0xcb, 0x3f}, // BIT b, r {0xcb, 0x40}, {0xcb, 0x41}, {0xcb, 0x42}, {0xcb, 0x43}, {0xcb, 0x44}, {0xcb, 0x45}, {0xcb, 0x47}, {0xcb, 0x48}, {0xcb, 0x49}, {0xcb, 0x4a}, {0xcb, 0x4b}, {0xcb, 0x4c}, {0xcb, 0x4d}, {0xcb, 0x4f}, {0xcb, 0x50}, {0xcb, 0x51}, {0xcb, 0x52}, {0xcb, 0x53}, {0xcb, 0x54}, {0xcb, 0x55}, {0xcb, 0x57}, {0xcb, 0x58}, {0xcb, 0x59}, {0xcb, 0x5a}, {0xcb, 0x5b}, {0xcb, 0x5c}, {0xcb, 0x5d}, {0xcb, 0x5f}, {0xcb, 0x60}, {0xcb, 0x61}, {0xcb, 0x62}, {0xcb, 0x63}, {0xcb, 0x64}, {0xcb, 0x65}, {0xcb, 0x67}, {0xcb, 0x68}, {0xcb, 0x69}, {0xcb, 0x6a}, {0xcb, 0x6b}, {0xcb, 0x6c}, {0xcb, 0x6d}, {0xcb, 0x6f}, {0xcb, 0x70}, {0xcb, 0x71}, {0xcb, 0x72}, {0xcb, 0x73}, {0xcb, 0x74}, {0xcb, 0x75}, {0xcb, 0x77}, {0xcb, 0x78}, {0xcb, 0x79}, {0xcb, 0x7a}, {0xcb, 0x7b}, {0xcb, 0x7c}, {0xcb, 0x7d}, {0xcb, 0x7f}, // RES b, r {0xcb, 0x80}, {0xcb, 0x81}, {0xcb, 0x82}, {0xcb, 0x83}, {0xcb, 0x84}, {0xcb, 0x85}, {0xcb, 0x87}, {0xcb, 0x88}, {0xcb, 0x89}, {0xcb, 0x8a}, {0xcb, 0x8b}, {0xcb, 0x8c}, {0xcb, 0x8d}, {0xcb, 0x8f}, {0xcb, 0x90}, {0xcb, 0x91}, {0xcb, 0x92}, {0xcb, 0x93}, {0xcb, 0x94}, {0xcb, 0x95}, {0xcb, 0x97}, {0xcb, 0x98}, {0xcb, 0x99}, {0xcb, 0x9a}, {0xcb, 0x9b}, {0xcb, 0x9c}, {0xcb, 0x9d}, {0xcb, 0x9f}, {0xcb, 0xa0}, {0xcb, 0xa1}, {0xcb, 0xa2}, {0xcb, 0xa3}, {0xcb, 0xa4}, {0xcb, 0xa5}, {0xcb, 0xa7}, {0xcb, 0xa8}, {0xcb, 0xa9}, {0xcb, 0xaa}, {0xcb, 0xab}, {0xcb, 0xac}, {0xcb, 0xad}, {0xcb, 0xaf}, {0xcb, 0xb0}, {0xcb, 0xb1}, {0xcb, 0xb2}, {0xcb, 0xb3}, {0xcb, 0xb4}, {0xcb, 0xb5}, {0xcb, 0xb7}, {0xcb, 0xb8}, {0xcb, 0xb9}, {0xcb, 0xba}, {0xcb, 0xbb}, {0xcb, 0xbc}, {0xcb, 0xbd}, {0xcb, 0xbf}, // SET b, r {0xcb, 0xc0}, {0xcb, 0xc1}, {0xcb, 0xc2}, {0xcb, 0xc3}, {0xcb, 0xc4}, {0xcb, 0xc5}, {0xcb, 0xc7}, {0xcb, 0xc8}, {0xcb, 0xc9}, {0xcb, 0xca}, {0xcb, 0xcb}, {0xcb, 0xcc}, {0xcb, 0xcd}, {0xcb, 0xcf}, {0xcb, 0xc0}, {0xcb, 0xd1}, {0xcb, 0xd2}, {0xcb, 0xd3}, {0xcb, 0xd4}, {0xcb, 0xd5}, {0xcb, 0xd7}, {0xcb, 0xd8}, {0xcb, 0xd9}, {0xcb, 0xda}, {0xcb, 0xdb}, {0xcb, 0xdc}, {0xcb, 0xdd}, {0xcb, 0xdf}, {0xcb, 0xe0}, {0xcb, 0xe1}, {0xcb, 0xe2}, {0xcb, 0xe3}, {0xcb, 0xe4}, {0xcb, 0xe5}, {0xcb, 0xe7}, {0xcb, 0xe8}, {0xcb, 0xe9}, {0xcb, 0xea}, {0xcb, 0xeb}, {0xcb, 0xec}, {0xcb, 0xed}, {0xcb, 0xef}, {0xcb, 0xf0}, {0xcb, 0xf1}, {0xcb, 0xf2}, {0xcb, 0xf3}, {0xcb, 0xf4}, {0xcb, 0xf5}, {0xcb, 0xf7}, {0xcb, 0xf8}, {0xcb, 0xf9}, {0xcb, 0xfa}, {0xcb, 0xfb}, {0xcb, 0xfc}, {0xcb, 0xfd}, {0xcb, 0xff}, // NEG {0xed, 0x04}, {0xed, 0x0c}, {0xed, 0x14}, {0xed, 0x1c}, {0xed, 0x24}, {0xed, 0x2c}, {0xed, 0x34}, {0xed, 0x3c}, // IM 0/1/2 {0xed, 0x06}, {0xed, 0x0e}, {0xed, 0x16}, {0xed, 0x1e}, {0xed, 0x26}, {0xed, 0x2e}, {0xed, 0x36}, {0xed, 0x3e}, }) { CapturingZ80 z80(sequence); z80.run_for(8); [self validate48Contention:{{initial_pc, 4}, {initial_pc+1, 4}} z80:z80]; [self validatePlus3Contention:{{initial_pc, 4}, {initial_pc+1, 4}} z80:z80]; } } @end