From 67fd6787a6b54a57b9002ef034037063c44661b1 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Wed, 7 Apr 2021 21:57:40 -0400 Subject: [PATCH] Builds what I think I need to validate Z80 address, MREQ, IOREQ and RFSH. --- .../Clock Signal.xcodeproj/project.pbxproj | 4 + .../Clock SignalTests/Z80ContentionTests.mm | 194 ++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 OSBindings/Mac/Clock SignalTests/Z80ContentionTests.mm diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index afce1bf6c..6db645a89 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -917,6 +917,7 @@ 4BDA00E022E644AF00AC3CD0 /* CSROMReceiverView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BDA00DF22E644AF00AC3CD0 /* CSROMReceiverView.m */; }; 4BDA00E422E663B900AC3CD0 /* NSData+CRC32.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BDA00E222E663B900AC3CD0 /* NSData+CRC32.m */; }; 4BDA00E622E699B000AC3CD0 /* CSMachine.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A53961D117D36003C6002 /* CSMachine.mm */; }; + 4BDA8235261E8E000021AA19 /* Z80ContentionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BDA8234261E8E000021AA19 /* Z80ContentionTests.mm */; }; 4BDACBEC22FFA5D20045EF7E /* ncr5380.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BDACBEA22FFA5D20045EF7E /* ncr5380.cpp */; }; 4BDACBED22FFA5D20045EF7E /* ncr5380.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BDACBEA22FFA5D20045EF7E /* ncr5380.cpp */; }; 4BDB61EB2032806E0048AF91 /* CSAtari2600.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A539A1D117D36003C6002 /* CSAtari2600.mm */; }; @@ -1934,6 +1935,7 @@ 4BDA00DF22E644AF00AC3CD0 /* CSROMReceiverView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CSROMReceiverView.m; sourceTree = ""; }; 4BDA00E222E663B900AC3CD0 /* NSData+CRC32.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+CRC32.m"; sourceTree = ""; }; 4BDA00E322E663B900AC3CD0 /* NSData+CRC32.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+CRC32.h"; sourceTree = ""; }; + 4BDA8234261E8E000021AA19 /* Z80ContentionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = Z80ContentionTests.mm; sourceTree = ""; }; 4BDACBEA22FFA5D20045EF7E /* ncr5380.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ncr5380.cpp; sourceTree = ""; }; 4BDACBEB22FFA5D20045EF7E /* ncr5380.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ncr5380.hpp; sourceTree = ""; }; 4BDB3D8522833321002D3CEE /* Keyboard.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Keyboard.hpp; sourceTree = ""; }; @@ -3964,6 +3966,7 @@ 4B2AF8681E513FC20027EE29 /* TIATests.mm */, 4B1D08051E0F7A1100763741 /* TimeTests.mm */, 4BEE4BD325A26E2B00011BD2 /* x86DecoderTests.mm */, + 4BDA8234261E8E000021AA19 /* Z80ContentionTests.mm */, 4BB73EB81B587A5100552FC2 /* Info.plist */, 4BC9E1ED1D23449A003FCEE4 /* 6502InterruptTests.swift */, 4B92EAC91B7C112B00246143 /* 6502TimingTests.swift */, @@ -5649,6 +5652,7 @@ 4B9D0C4B22C7D70A00DE1AD3 /* 68000BCDTests.mm in Sources */, 4B778F5E23A5F3230000D260 /* Oric.cpp in Sources */, 4B3BA0C31D318AEC005DD7A7 /* C1540Tests.swift in Sources */, + 4BDA8235261E8E000021AA19 /* Z80ContentionTests.mm in Sources */, 4B778F1A23A5ED320000D260 /* Video.cpp in Sources */, 4B778F3B23A5F1650000D260 /* KeyboardMachine.cpp in Sources */, 4B778F2E23A5F09E0000D260 /* IRQDelegatePortHandler.cpp in Sources */, diff --git a/OSBindings/Mac/Clock SignalTests/Z80ContentionTests.mm b/OSBindings/Mac/Clock SignalTests/Z80ContentionTests.mm new file mode 100644 index 000000000..12dc0be6a --- /dev/null +++ b/OSBindings/Mac/Clock SignalTests/Z80ContentionTests.mm @@ -0,0 +1,194 @@ +// +// 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 { + + CapturingZ80(const std::initializer_list &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. + // + // I think the source I'm using also implicitly assumes that refresh + // addresses are outside of the contended area, and doesn't check them. + // So unlike the actual ULA I'm also ignoring any address while refresh + // is asserted. + int count = -1; + uint16_t address = 0; + auto contention = contentions.begin(); + + const auto bus_records = z80.cycle_records(); + for(const auto &record: bus_records) { + ++count; + + if( + !count || // i.e. is at start. + (&record == &bus_records.back()) || // i.e. is at end. + !(record.mreq || record.refresh) // i.e. beginning of a new contention. + ) { + if(count) { + XCTAssertNotEqual(contention, contentions.end()); + XCTAssertEqual(contention->address, address); + XCTAssertEqual(contention->length, count); + ++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. + int count = -1; + uint16_t address = 0; + auto contention = contentions.begin(); + + const auto bus_records = z80.bus_records(); + + for(size_t c = 0; c < bus_records.size(); c += 2) { + const bool is_leading_edge = !bus_records[c].mreq && bus_records[c+1].mreq && !bus_records[c].refresh; + + ++count; + if( + !count || // i.e. is at start. + (c == bus_records.size() - 2) || // i.e. is at end. + is_leading_edge // i.e. beginning of a new contention. + ) { + if(count) { + XCTAssertNotEqual(contention, contentions.end()); + XCTAssertEqual(contention->address, address); + XCTAssertEqual(contention->length, count); + ++contention; + } + + count = 1; + address = bus_records[c].address; + } + } + + XCTAssertEqual(contention, contentions.end()); +} + +// MARK: - Opcode tests. + +- (void)testNOP { + CapturingZ80 z80({0x00}); + z80.run_for(4); + + [self validate48Contention:{{initial_pc, 4}} z80:z80]; + [self validatePlus3Contention:{{initial_pc, 4}} z80:z80]; +} + +@end