1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-16 22:28:57 +00:00
CLK/OSBindings/Mac/Clock SignalTests/Z80ContentionTests.mm
2021-04-08 22:14:53 -04:00

370 lines
12 KiB
Plaintext

//
// Z80ContentionTests.cpp
// Clock SignalTests
//
// Created by Thomas Harte on 7/4/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#import <XCTest/XCTest.h>
#include "../../../Processors/Z80/Z80.hpp"
namespace {
static constexpr uint16_t initial_pc = 0x0000;
static constexpr uint16_t initial_ir = 0xe000;
struct CapturingZ80: public CPU::Z80::BusHandler {
template <typename Collection> CapturingZ80(const Collection &code) : z80_(*this) {
// Take a copy of the code.
std::copy(code.begin(), code.end(), ram_);
code_length_ = uint16_t(code.size());
// 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 and set A to 0x80.
z80_.set_value_of_register(CPU::Z80::Register::I, 0xe0);
z80_.set_value_of_register(CPU::Z80::Register::A, 0x80);
}
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<int>(); 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];
XCTAssert(cycle.operation != CPU::Z80::PartialMachineCycle::ReadOpcode || *cycle.address < code_length_);
}
return HalfCycles(0);
}
const std::vector<BusRecord> &bus_records() const {
return bus_records_;
}
std::vector<BusRecord> cycle_records() const {
std::vector<BusRecord> 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<CapturingZ80, false, false> z80_;
uint8_t ram_[65536];
uint16_t code_length_ = 0;
std::vector<BusRecord> 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<ContentionCheck> &)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(size_t c = 0; c < bus_records.size(); c++) {
++count;
if(
c && // i.e. not at front.
!bus_records[c].mreq // i.e. beginning of a new contention.
) {
XCTAssertNotEqual(contention, contentions.end());
XCTAssertEqual(contention->address, address);
XCTAssertEqual(contention->length, count - 1);
++contention;
count = 1;
address = bus_records[c].address;
}
}
XCTAssertEqual(contention->address, address);
XCTAssertEqual(contention->length, count);
++contention;
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<ContentionCheck> &)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;
if(
c && // i.e. not at front.
is_leading_edge // i.e. beginning of a new contention.
) {
XCTAssertNotEqual(contention, contentions.end());
XCTAssertEqual(contention->address, address, "Address mismatch at half-cycle %zu", c);
XCTAssertEqual(contention->length, count - 1, "Length mismatch at half-cycle %zu", c);
++contention;
count = 1;
address = bus_records[c].address;
}
}
XCTAssertEqual(contention->address, address, "Address mismatch at end");
XCTAssertEqual(contention->length, count, "Length mismatch at end");
++contention;
XCTAssertEqual(contention, contentions.end(), "Not all supplied contentions used");
}
// 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<uint8_t> 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<std::vector<uint8_t>>{
// 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, 0x44}, {0xed, 0x4c}, {0xed, 0x54}, {0xed, 0x5c},
{0xed, 0x64}, {0xed, 0x6c}, {0xed, 0x74}, {0xed, 0x7c},
// IM 0/1/2
{0xed, 0x46}, {0xed, 0x4e}, {0xed, 0x56}, {0xed, 0x5e},
{0xed, 0x66}, {0xed, 0x6e}, {0xed, 0x66}, {0xed, 0x6e},
}) {
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];
}
}
- (void)testAIR {
for(const auto &sequence : std::vector<std::vector<uint8_t>>{
{0xed, 0x57}, // LD A, I
{0xed, 0x5f}, // LD A, R
{0xed, 0x47}, // LD I, A
{0xed, 0x4f}, // LD R, A
}) {
CapturingZ80 z80(sequence);
z80.run_for(9);
[self validate48Contention:{{initial_pc, 4}, {initial_pc+1, 4}, {initial_ir+1, 1}} z80:z80];
[self validatePlus3Contention:{{initial_pc, 4}, {initial_pc+1, 5}} z80:z80];
}
}
- (void)testINCDEC16 {
for(uint8_t opcode : {
// INC rr
0x03, 0x13, 0x23, 0x33,
// DEC rr
0x0b, 0x1b, 0x2b, 0x3b,
// LD SP, HL
0xf9,
}) {
const std::initializer_list<uint8_t> opcodes = {opcode};
CapturingZ80 z80(opcodes);
z80.run_for(6);
[self validate48Contention:{{initial_pc, 4}, {initial_ir, 1}, {initial_ir, 1}} z80:z80];
[self validatePlus3Contention:{{initial_pc, 6}} z80:z80];
}
}
@end