1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-08-17 07:29:04 +00:00
CLK/OSBindings/Mac/Clock SignalTests/Z80ContentionTests.mm
Thomas Harte 818a4dff25 Corrects ADD HL, dd test.
Or, at least, likely corrects. The bus cycle breakdown in the Z80 data sheet implies these accesses should come after completion of the refresh cycle, not during its long tail, so I think +1 is correct.
2021-04-08 22:23:15 -04:00

392 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 dd
0x03, 0x13, 0x23, 0x33,
// DEC dd
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];
}
}
- (void)testADDHLdd {
for(uint8_t opcode : {
// ADD hl, dd
0x09, 0x19, 0x29, 0x39,
}) {
const std::initializer_list<uint8_t> opcodes = {opcode};
CapturingZ80 z80(opcodes);
z80.run_for(11);
[self validate48Contention:{
{initial_pc, 4},
{initial_ir+1, 1},
{initial_ir+1, 1},
{initial_ir+1, 1},
{initial_ir+1, 1},
{initial_ir+1, 1},
{initial_ir+1, 1},
{initial_ir+1, 1},
} z80:z80];
[self validatePlus3Contention:{{initial_pc, 11}} z80:z80];
}
}
@end