mirror of
https://github.com/TomHarte/CLK.git
synced 2024-11-26 08:49:37 +00:00
Merge pull request #911 from TomHarte/48kbSpectrum
Adds the 48kb and 128kb Spectrums.
This commit is contained in:
commit
246fd9442f
@ -19,11 +19,15 @@ namespace ZXSpectrum {
|
||||
|
||||
struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> {
|
||||
ReflectableEnum(Model,
|
||||
SixteenK,
|
||||
FortyEightK,
|
||||
OneTwoEightK,
|
||||
Plus2,
|
||||
Plus2a,
|
||||
Plus3,
|
||||
);
|
||||
|
||||
Model model = Model::Plus2a;
|
||||
Model model = Model::Plus2;
|
||||
bool should_hold_enter = false;
|
||||
|
||||
Target(): Analyser::Static::Target(Machine::ZXSpectrum) {
|
||||
|
@ -18,7 +18,9 @@ namespace Sinclair {
|
||||
namespace ZXSpectrum {
|
||||
|
||||
enum class VideoTiming {
|
||||
Plus3
|
||||
FortyEightK,
|
||||
OneTwoEightK,
|
||||
Plus3,
|
||||
};
|
||||
|
||||
/*
|
||||
@ -67,48 +69,103 @@ template <VideoTiming timing> class Video {
|
||||
};
|
||||
|
||||
static constexpr Timings get_timings() {
|
||||
// Amstrad gate array timings, classic statement:
|
||||
//
|
||||
// Contention begins 14361 cycles "after interrupt" and follows the pattern [1, 0, 7, 6 5 4, 3, 2].
|
||||
// The first four bytes of video are fetched at 14365–14368 cycles, in the order [pixels, attribute, pixels, attribute].
|
||||
//
|
||||
// For my purposes:
|
||||
//
|
||||
// Video fetching always begins at 0. Since there are 311*228 = 70908 cycles per frame, and the interrupt
|
||||
// should "occur" (I assume: begin) 14365 before that, it should actually begin at 70908 - 14365 = 56543.
|
||||
//
|
||||
// Contention begins four cycles before the first video fetch, so it begins at 70904. I don't currently
|
||||
// know whether the four cycles is true across all models, so it's given here as convention_leadin.
|
||||
//
|
||||
// ... except that empirically that all seems to be two cycles off. So maybe I misunderstand what the
|
||||
// contention patterns are supposed to indicate relative to MREQ? It's frustrating that all documentation
|
||||
// I can find is vaguely in terms of contention patterns, and what they mean isn't well-defined in terms
|
||||
// of regular Z80 signalling.
|
||||
constexpr Timings result = {
|
||||
.cycles_per_line = 228 * 2,
|
||||
.lines_per_frame = 311,
|
||||
if constexpr (timing == VideoTiming::Plus3) {
|
||||
// Amstrad gate array timings, classic statement:
|
||||
//
|
||||
// Contention begins 14361 cycles "after interrupt" and follows the pattern [1, 0, 7, 6 5 4, 3, 2].
|
||||
// The first four bytes of video are fetched at 14365–14368 cycles, in the order [pixels, attribute, pixels, attribute].
|
||||
//
|
||||
// For my purposes:
|
||||
//
|
||||
// Video fetching always begins at 0. Since there are 311*228 = 70908 cycles per frame, and the interrupt
|
||||
// should "occur" (I assume: begin) 14365 before that, it should actually begin at 70908 - 14365 = 56543.
|
||||
//
|
||||
// Contention begins four cycles before the first video fetch, so it begins at 70904. I don't currently
|
||||
// know whether the four cycles is true across all models, so it's given here as convention_leadin.
|
||||
//
|
||||
// ... except that empirically that all seems to be two cycles off. So maybe I misunderstand what the
|
||||
// contention patterns are supposed to indicate relative to MREQ? It's frustrating that all documentation
|
||||
// I can find is vaguely in terms of contention patterns, and what they mean isn't well-defined in terms
|
||||
// of regular Z80 signalling.
|
||||
constexpr Timings result = {
|
||||
.cycles_per_line = 228 * 2,
|
||||
.lines_per_frame = 311,
|
||||
|
||||
// i.e. video fetching begins five cycles after the start of the
|
||||
// contended memory pattern below; that should put a clear two
|
||||
// cycles between a Z80 access and the first video fetch.
|
||||
.contention_leadin = 5 * 2,
|
||||
.contention_duration = 129 * 2,
|
||||
// i.e. video fetching begins five cycles after the start of the
|
||||
// contended memory pattern below; that should put a clear two
|
||||
// cycles between a Z80 access and the first video fetch.
|
||||
.contention_leadin = 5 * 2,
|
||||
.contention_duration = 129 * 2,
|
||||
|
||||
// i.e. interrupt is first signalled 14368 cycles before the first video fetch.
|
||||
.interrupt_time = (228*311 - 14368) * 2,
|
||||
// i.e. interrupt is first signalled 14368 cycles before the first video fetch.
|
||||
.interrupt_time = (228*311 - 14368) * 2,
|
||||
|
||||
.delays = {
|
||||
2, 1,
|
||||
0, 0,
|
||||
14, 13,
|
||||
12, 11,
|
||||
10, 9,
|
||||
8, 7,
|
||||
6, 5,
|
||||
4, 3,
|
||||
}
|
||||
};
|
||||
return result;
|
||||
.delays = {
|
||||
2, 1,
|
||||
0, 0,
|
||||
14, 13,
|
||||
12, 11,
|
||||
10, 9,
|
||||
8, 7,
|
||||
6, 5,
|
||||
4, 3,
|
||||
}
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: fix 48kb and 128kb timings, below.
|
||||
|
||||
if constexpr (timing == VideoTiming::OneTwoEightK) {
|
||||
constexpr Timings result = {
|
||||
.cycles_per_line = 228 * 2,
|
||||
.lines_per_frame = 311,
|
||||
|
||||
.contention_leadin = 2 * 2,
|
||||
.contention_duration = 128 * 2,
|
||||
|
||||
.interrupt_time = (228*311 - 14366) * 2,
|
||||
|
||||
.delays = {
|
||||
12, 11,
|
||||
10, 9,
|
||||
8, 7,
|
||||
6, 5,
|
||||
4, 3,
|
||||
2, 1,
|
||||
0, 0,
|
||||
0, 0,
|
||||
}
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if constexpr (timing == VideoTiming::FortyEightK) {
|
||||
constexpr Timings result = {
|
||||
.cycles_per_line = 224 * 2,
|
||||
.lines_per_frame = 312,
|
||||
|
||||
.contention_leadin = 2 * 2,
|
||||
.contention_duration = 128 * 2,
|
||||
|
||||
.interrupt_time = (224*312 - 14339) * 2,
|
||||
|
||||
.delays = {
|
||||
12, 11,
|
||||
10, 9,
|
||||
8, 7,
|
||||
6, 5,
|
||||
4, 3,
|
||||
2, 1,
|
||||
0, 0,
|
||||
0, 0,
|
||||
}
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: how long is the interrupt line held for?
|
||||
@ -236,9 +293,14 @@ template <VideoTiming timing> class Video {
|
||||
|
||||
if(offset >= burst_position && offset < burst_position+burst_length && end_offset > offset) {
|
||||
const int burst_duration = std::min(burst_position + burst_length, end_offset) - offset;
|
||||
crt_.output_colour_burst(burst_duration, 116, is_alternate_line_);
|
||||
|
||||
if constexpr (timing >= VideoTiming::OneTwoEightK) {
|
||||
crt_.output_colour_burst(burst_duration, 116, is_alternate_line_);
|
||||
// The colour burst phase above is an empirical guess. I need to research further.
|
||||
} else {
|
||||
crt_.output_default_colour_burst(burst_duration);
|
||||
}
|
||||
offset += burst_duration;
|
||||
// The colour burst phase above is an empirical guess. I need to research further.
|
||||
}
|
||||
|
||||
if(offset >= burst_position+burst_length && end_offset > offset) {
|
||||
@ -259,9 +321,21 @@ template <VideoTiming timing> class Video {
|
||||
crt_.output_level(duration);
|
||||
}
|
||||
|
||||
static constexpr int half_cycles_per_line() {
|
||||
if constexpr (timing == VideoTiming::FortyEightK) {
|
||||
// TODO: determine real figure here, if one exists.
|
||||
// The source I'm looking at now suggests that the theoretical
|
||||
// ideal of 224*2 ignores the real-life effects of separate
|
||||
// crystals, so I've nudged this experimentally.
|
||||
return 224*2 - 1;
|
||||
} else {
|
||||
return 227*2;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
Video() :
|
||||
crt_(227 * 2, 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2)
|
||||
crt_(half_cycles_per_line(), 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2)
|
||||
{
|
||||
// Show only the centre 80% of the TV frame.
|
||||
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
||||
@ -327,20 +401,24 @@ template <VideoTiming timing> class Video {
|
||||
*/
|
||||
uint8_t get_floating_value() const {
|
||||
constexpr auto timings = get_timings();
|
||||
const uint8_t out_of_bounds = (timing == VideoTiming::Plus3) ? last_contended_access_ : 0xff;
|
||||
|
||||
const int line = time_into_frame_ / timings.cycles_per_line;
|
||||
if(line >= 192) return 0xff;
|
||||
if(line >= 192) {
|
||||
return out_of_bounds;
|
||||
}
|
||||
|
||||
const int time_into_line = time_into_frame_ % timings.cycles_per_line;
|
||||
if(time_into_line >= 256 || (time_into_line&8)) {
|
||||
return last_contended_access_;
|
||||
return out_of_bounds;
|
||||
}
|
||||
|
||||
// The +2a and +3 always return the low bit as set.
|
||||
const uint8_t value = last_fetches_[(time_into_line >> 1) & 3];
|
||||
if constexpr (timing == VideoTiming::Plus3) {
|
||||
return last_fetches_[(time_into_line >> 1) & 3] | 1;
|
||||
return value | 1;
|
||||
}
|
||||
|
||||
return last_fetches_[(time_into_line >> 1) & 3];
|
||||
return value;
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -81,8 +81,29 @@ template<Model model> class ConcreteMachine:
|
||||
|
||||
// With only the +2a and +3 currently supported, the +3 ROM is always
|
||||
// the one required.
|
||||
const auto roms =
|
||||
rom_fetcher({ {"ZXSpectrum", "the +2a/+3 ROM", "plus3.rom", 64 * 1024, 0x96e3c17a} });
|
||||
std::vector<ROMMachine::ROM> rom_names;
|
||||
const std::string machine = "ZXSpectrum";
|
||||
switch(model) {
|
||||
case Model::SixteenK:
|
||||
case Model::FortyEightK:
|
||||
rom_names.emplace_back(machine, "the 48kb ROM", "48.rom", 16 * 1024, 0xddee531f);
|
||||
break;
|
||||
|
||||
case Model::OneTwoEightK:
|
||||
rom_names.emplace_back(machine, "the 128kb ROM", "128.rom", 32 * 1024, 0x2cbe8995);
|
||||
break;
|
||||
|
||||
case Model::Plus2:
|
||||
rom_names.emplace_back(machine, "the +2 ROM", "plus2.rom", 32 * 1024, 0xe7a517dc);
|
||||
break;
|
||||
|
||||
case Model::Plus2a:
|
||||
case Model::Plus3: {
|
||||
const std::initializer_list<uint32_t> crc32s = { 0x96e3c17a, 0xbe0d9ec4 };
|
||||
rom_names.emplace_back(machine, "the +2a/+3 ROM", "plus3.rom", 64 * 1024, crc32s);
|
||||
} break;
|
||||
}
|
||||
const auto roms = rom_fetcher(rom_names);
|
||||
if(!roms[0]) throw ROMMachine::Error::MissingROMs;
|
||||
memcpy(rom_.data(), roms[0]->data(), std::min(rom_.size(), roms[0]->size()));
|
||||
|
||||
@ -110,7 +131,7 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
static constexpr unsigned int clock_rate() {
|
||||
// constexpr unsigned int ClockRate = 3'500'000;
|
||||
constexpr unsigned int OriginalClockRate = 3'500'000;
|
||||
constexpr unsigned int Plus3ClockRate = 3'546'875; // See notes below; this is a guess.
|
||||
|
||||
// Notes on timing for the +2a and +3:
|
||||
@ -137,7 +158,7 @@ template<Model model> class ConcreteMachine:
|
||||
// the Spectrum is a PAL machine with a fixed colour phase relationship. For
|
||||
// this emulator's world, that's a first!
|
||||
|
||||
return Plus3ClockRate;
|
||||
return model < Model::OneTwoEightK ? OriginalClockRate : Plus3ClockRate;
|
||||
}
|
||||
|
||||
// MARK: - TimedMachine.
|
||||
@ -189,18 +210,90 @@ template<Model model> class ConcreteMachine:
|
||||
const uint16_t address = cycle.address ? *cycle.address : 0x0000;
|
||||
|
||||
// Apply contention if necessary.
|
||||
if(
|
||||
is_contended_[address >> 14] &&
|
||||
cycle.operation >= PartialMachineCycle::ReadOpcodeStart &&
|
||||
cycle.operation <= PartialMachineCycle::WriteStart) {
|
||||
// Assumption here: the trigger for the ULA inserting a delay is the falling edge
|
||||
if constexpr (model >= Model::Plus2a) {
|
||||
// Model applied: the trigger for the ULA inserting a delay is the falling edge
|
||||
// of MREQ, which is always half a cycle into a read or write.
|
||||
//
|
||||
// TODO: somehow provide that information in the PartialMachineCycle?
|
||||
if(
|
||||
is_contended_[address >> 14] &&
|
||||
cycle.operation >= PartialMachineCycle::ReadOpcodeStart &&
|
||||
cycle.operation <= PartialMachineCycle::WriteStart) {
|
||||
|
||||
const HalfCycles delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1));
|
||||
advance(cycle.length + delay);
|
||||
return delay;
|
||||
const HalfCycles delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1));
|
||||
advance(cycle.length + delay);
|
||||
return delay;
|
||||
}
|
||||
} else {
|
||||
switch(cycle.operation) {
|
||||
default:
|
||||
advance(cycle.length);
|
||||
return HalfCycles(0);
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::InputStart:
|
||||
case CPU::Z80::PartialMachineCycle::OutputStart: {
|
||||
// The port address is loaded prior to IOREQ being visible; a contention
|
||||
// always occurs if it is in the $4000–$8000 range regardless of current
|
||||
// memory mapping.
|
||||
HalfCycles delay;
|
||||
HalfCycles time = video_.time_since_flush() + HalfCycles(1);
|
||||
|
||||
if((address & 0xc000) == 0x4000) {
|
||||
for(int c = 0; c < ((address & 1) ? 4 : 2); c++) {
|
||||
const auto next_delay = video_.last_valid()->access_delay(time);
|
||||
delay += next_delay;
|
||||
time += next_delay + 2;
|
||||
}
|
||||
} else {
|
||||
if(!(address & 1)) {
|
||||
delay = video_.last_valid()->access_delay(time + HalfCycles(2));
|
||||
}
|
||||
}
|
||||
|
||||
advance(cycle.length + delay);
|
||||
return delay;
|
||||
}
|
||||
|
||||
case PartialMachineCycle::ReadOpcodeStart:
|
||||
case PartialMachineCycle::ReadStart:
|
||||
case PartialMachineCycle::WriteStart: {
|
||||
// These all start by loading the address bus, then set MREQ
|
||||
// half a cycle later.
|
||||
if(is_contended_[address >> 14]) {
|
||||
const HalfCycles delay = video_.last_valid()->access_delay(video_.time_since_flush() + HalfCycles(1));
|
||||
|
||||
advance(cycle.length + delay);
|
||||
return delay;
|
||||
}
|
||||
}
|
||||
|
||||
case PartialMachineCycle::Internal: {
|
||||
// Whatever's on the address bus will remain there, without IOREQ or
|
||||
// MREQ interceding, for this entire bus cycle. So apply contentions
|
||||
// all the way along.
|
||||
if(is_contended_[address >> 14]) {
|
||||
const auto half_cycles = cycle.length.as<int>();
|
||||
assert(!(half_cycles & 1));
|
||||
|
||||
HalfCycles time = video_.time_since_flush() + HalfCycles(1);
|
||||
HalfCycles delay;
|
||||
for(int c = 0; c < half_cycles; c += 2) {
|
||||
const auto next_delay = video_.last_valid()->access_delay(time);
|
||||
delay += next_delay;
|
||||
time += next_delay + 2;
|
||||
}
|
||||
|
||||
advance(cycle.length + delay);
|
||||
return delay;
|
||||
}
|
||||
}
|
||||
|
||||
case CPU::Z80::PartialMachineCycle::Input:
|
||||
case CPU::Z80::PartialMachineCycle::Output:
|
||||
case CPU::Z80::PartialMachineCycle::Read:
|
||||
case CPU::Z80::PartialMachineCycle::Write:
|
||||
case CPU::Z80::PartialMachineCycle::ReadOpcode:
|
||||
// For these, carry on into the actual handler, below.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// For all other machine cycles, model the action as happening at the end of the machine cycle;
|
||||
@ -214,7 +307,7 @@ template<Model model> class ConcreteMachine:
|
||||
// Fast loading: ROM version.
|
||||
//
|
||||
// The below patches over part of the 'LD-BYTES' routine from the 48kb ROM.
|
||||
if(use_fast_tape_hack_ && address == 0x056b && read_pointers_[0] == &rom_[0xc000]) {
|
||||
if(use_fast_tape_hack_ && address == 0x056b && read_pointers_[0] == &rom_[classic_rom_offset()]) {
|
||||
// Stop pressing enter, if neccessry.
|
||||
if(duration_to_press_enter_ > Cycles(0)) {
|
||||
duration_to_press_enter_ = Cycles(0);
|
||||
@ -228,10 +321,21 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
case PartialMachineCycle::Read:
|
||||
if constexpr (model == Model::SixteenK) {
|
||||
// Assumption: with nothing mapped above 0x8000 on the 16kb Spectrum,
|
||||
// read the floating bus.
|
||||
if(address >= 0x8000) {
|
||||
*cycle.value = video_->get_floating_value();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
*cycle.value = read_pointers_[address >> 14][address];
|
||||
|
||||
if(is_contended_[address >> 14]) {
|
||||
video_->set_last_contended_area_access(*cycle.value);
|
||||
if constexpr (model >= Model::Plus2a) {
|
||||
if(is_contended_[address >> 14]) {
|
||||
video_->set_last_contended_area_access(*cycle.value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@ -243,9 +347,11 @@ template<Model model> class ConcreteMachine:
|
||||
|
||||
write_pointers_[address >> 14][address] = *cycle.value;
|
||||
|
||||
// Fill the floating bus buffer if this write is within the contended area.
|
||||
if(is_contended_[address >> 14]) {
|
||||
video_->set_last_contended_area_access(*cycle.value);
|
||||
if constexpr (model >= Model::Plus2a) {
|
||||
// Fill the floating bus buffer if this write is within the contended area.
|
||||
if(is_contended_[address >> 14]) {
|
||||
video_->set_last_contended_area_access(*cycle.value);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@ -263,41 +369,49 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// Test for classic 128kb paging register (i.e. port 7ffd).
|
||||
if((address & 0xc002) == 0x4000) {
|
||||
port7ffd_ = *cycle.value;
|
||||
update_memory_map();
|
||||
if constexpr (model >= Model::OneTwoEightK) {
|
||||
if((address & 0xc002) == 0x4000) {
|
||||
port7ffd_ = *cycle.value;
|
||||
update_memory_map();
|
||||
|
||||
// Set the proper video base pointer.
|
||||
set_video_address();
|
||||
// Set the proper video base pointer.
|
||||
set_video_address();
|
||||
|
||||
// Potentially lock paging, _after_ the current
|
||||
// port values have taken effect.
|
||||
disable_paging_ |= *cycle.value & 0x20;
|
||||
}
|
||||
|
||||
// Test for +2a/+3 paging (i.e. port 1ffd).
|
||||
if((address & 0xf002) == 0x1000) {
|
||||
port1ffd_ = *cycle.value;
|
||||
update_memory_map();
|
||||
update_video_base();
|
||||
|
||||
if constexpr (model == Model::Plus3) {
|
||||
fdc_->set_motor_on(*cycle.value & 0x08);
|
||||
// Potentially lock paging, _after_ the current
|
||||
// port values have taken effect.
|
||||
disable_paging_ |= *cycle.value & 0x20;
|
||||
}
|
||||
}
|
||||
|
||||
if((address & 0xc002) == 0xc000) {
|
||||
// Select AY register.
|
||||
update_audio();
|
||||
GI::AY38910::Utility::select_register(ay_, *cycle.value);
|
||||
// Test for +2a/+3 paging (i.e. port 1ffd).
|
||||
if constexpr (model >= Model::Plus2a) {
|
||||
if((address & 0xf002) == 0x1000) {
|
||||
port1ffd_ = *cycle.value;
|
||||
update_memory_map();
|
||||
update_video_base();
|
||||
|
||||
if constexpr (model == Model::Plus3) {
|
||||
fdc_->set_motor_on(*cycle.value & 0x08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if((address & 0xc002) == 0x8000) {
|
||||
// Write to AY register.
|
||||
update_audio();
|
||||
GI::AY38910::Utility::write_data(ay_, *cycle.value);
|
||||
// Route to the AY if one is fitted.
|
||||
if constexpr (model >= Model::OneTwoEightK) {
|
||||
if((address & 0xc002) == 0xc000) {
|
||||
// Select AY register.
|
||||
update_audio();
|
||||
GI::AY38910::Utility::select_register(ay_, *cycle.value);
|
||||
}
|
||||
|
||||
if((address & 0xc002) == 0x8000) {
|
||||
// Write to AY register.
|
||||
update_audio();
|
||||
GI::AY38910::Utility::write_data(ay_, *cycle.value);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for FDC accesses.
|
||||
if constexpr (model == Model::Plus3) {
|
||||
switch(address) {
|
||||
default: break;
|
||||
@ -308,10 +422,13 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
break;
|
||||
|
||||
case PartialMachineCycle::Input:
|
||||
case PartialMachineCycle::Input: {
|
||||
bool did_match = false;
|
||||
*cycle.value = 0xff;
|
||||
|
||||
if(!(address&1)) {
|
||||
did_match = true;
|
||||
|
||||
// Port FE:
|
||||
//
|
||||
// address b8+: mask of keyboard lines to select
|
||||
@ -339,17 +456,23 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
if((address & 0xc002) == 0xc000) {
|
||||
// Read from AY register.
|
||||
update_audio();
|
||||
*cycle.value &= GI::AY38910::Utility::read(ay_);
|
||||
if constexpr (model >= Model::OneTwoEightK) {
|
||||
if((address & 0xc002) == 0xc000) {
|
||||
did_match = true;
|
||||
|
||||
// Read from AY register.
|
||||
update_audio();
|
||||
*cycle.value &= GI::AY38910::Utility::read(ay_);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a floating bus read; these are particularly arcane
|
||||
// on the +2a/+3. See footnote to https://spectrumforeveryone.com/technical/memory-contention-floating-bus/
|
||||
// and, much more rigorously, http://sky.relative-path.com/zx/floating_bus.html
|
||||
if(!disable_paging_ && (address & 0xf003) == 0x0001) {
|
||||
*cycle.value &= video_->get_floating_value();
|
||||
if constexpr (model >= Model::Plus2a) {
|
||||
// Check for a +2a/+3 floating bus read; these are particularly arcane.
|
||||
// See footnote to https://spectrumforeveryone.com/technical/memory-contention-floating-bus/
|
||||
// and, much more rigorously, http://sky.relative-path.com/zx/floating_bus.html
|
||||
if(!disable_paging_ && (address & 0xf003) == 0x0001) {
|
||||
*cycle.value &= video_->get_floating_value();
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (model == Model::Plus3) {
|
||||
@ -360,7 +483,13 @@ template<Model model> class ConcreteMachine:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
if constexpr (model < Model::Plus2) {
|
||||
if(!did_match) {
|
||||
*cycle.value = video_->get_floating_value();
|
||||
}
|
||||
}
|
||||
} break;
|
||||
}
|
||||
|
||||
return HalfCycles(0);
|
||||
@ -571,10 +700,14 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
void set_memory(int bank, uint8_t source) {
|
||||
is_contended_[bank] = (source >= 4 && source < 8);
|
||||
if constexpr (model >= Model::Plus2a) {
|
||||
is_contended_[bank] = (source >= 4 && source < 8);
|
||||
} else {
|
||||
is_contended_[bank] = source & 1;
|
||||
}
|
||||
pages_[bank] = source;
|
||||
|
||||
uint8_t *read = (source < 0x80) ? &ram_[source * 16384] : &rom_[(source & 0x7f) * 16384];
|
||||
uint8_t *const read = (source < 0x80) ? &ram_[source * 16384] : &rom_[(source & 0x7f) * 16384];
|
||||
const auto offset = bank*16384;
|
||||
|
||||
read_pointers_[bank] = read - offset;
|
||||
@ -607,8 +740,15 @@ template<Model model> class ConcreteMachine:
|
||||
}
|
||||
|
||||
// MARK: - Video.
|
||||
static constexpr VideoTiming video_timing = VideoTiming::Plus3;
|
||||
JustInTimeActor<Video<video_timing>> video_;
|
||||
using VideoType =
|
||||
std::conditional_t<
|
||||
model <= Model::FortyEightK, Video<VideoTiming::FortyEightK>,
|
||||
std::conditional_t<
|
||||
model <= Model::Plus2, Video<VideoTiming::OneTwoEightK>,
|
||||
Video<VideoTiming::Plus3>
|
||||
>
|
||||
>;
|
||||
JustInTimeActor<VideoType> video_;
|
||||
|
||||
// MARK: - Keyboard.
|
||||
Sinclair::ZX::Keyboard::Keyboard keyboard_;
|
||||
@ -695,6 +835,22 @@ template<Model model> class ConcreteMachine:
|
||||
return true;
|
||||
}
|
||||
|
||||
static constexpr int classic_rom_offset() {
|
||||
switch(model) {
|
||||
case Model::SixteenK:
|
||||
case Model::FortyEightK:
|
||||
return 0x0000;
|
||||
|
||||
case Model::OneTwoEightK:
|
||||
case Model::Plus2:
|
||||
return 0x4000;
|
||||
|
||||
case Model::Plus2a:
|
||||
case Model::Plus3:
|
||||
return 0xc000;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Disc.
|
||||
JustInTimeActor<Amstrad::FDC, Cycles> fdc_;
|
||||
|
||||
@ -712,8 +868,12 @@ Machine *Machine::ZXSpectrum(const Analyser::Static::Target *target, const ROMMa
|
||||
const auto zx_target = dynamic_cast<const Analyser::Static::ZXSpectrum::Target *>(target);
|
||||
|
||||
switch(zx_target->model) {
|
||||
case Model::Plus2a: return new ConcreteMachine<Model::Plus2a>(*zx_target, rom_fetcher);
|
||||
case Model::Plus3: return new ConcreteMachine<Model::Plus3>(*zx_target, rom_fetcher);
|
||||
case Model::SixteenK: return new ConcreteMachine<Model::SixteenK>(*zx_target, rom_fetcher);
|
||||
case Model::FortyEightK: return new ConcreteMachine<Model::FortyEightK>(*zx_target, rom_fetcher);
|
||||
case Model::OneTwoEightK: return new ConcreteMachine<Model::OneTwoEightK>(*zx_target, rom_fetcher);
|
||||
case Model::Plus2: return new ConcreteMachine<Model::Plus2>(*zx_target, rom_fetcher);
|
||||
case Model::Plus2a: return new ConcreteMachine<Model::Plus2a>(*zx_target, rom_fetcher);
|
||||
case Model::Plus3: return new ConcreteMachine<Model::Plus3>(*zx_target, rom_fetcher);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
|
@ -63,6 +63,10 @@ typedef NS_ENUM(NSInteger, CSMachineOricDiskInterface) {
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CSMachineSpectrumModel) {
|
||||
CSMachineSpectrumModelSixteenK,
|
||||
CSMachineSpectrumModelFortyEightK,
|
||||
CSMachineSpectrumModelOneTwoEightK,
|
||||
CSMachineSpectrumModelPlus2,
|
||||
CSMachineSpectrumModelPlus2a,
|
||||
CSMachineSpectrumModelPlus3,
|
||||
};
|
||||
|
@ -194,8 +194,12 @@
|
||||
using Target = Analyser::Static::ZXSpectrum::Target;
|
||||
auto target = std::make_unique<Target>();
|
||||
switch(model) {
|
||||
case CSMachineSpectrumModelPlus2a: target->model = Target::Model::Plus2a; break;
|
||||
case CSMachineSpectrumModelPlus3: target->model = Target::Model::Plus3; break;
|
||||
case CSMachineSpectrumModelSixteenK: target->model = Target::Model::SixteenK; break;
|
||||
case CSMachineSpectrumModelFortyEightK: target->model = Target::Model::FortyEightK; break;
|
||||
case CSMachineSpectrumModelOneTwoEightK: target->model = Target::Model::OneTwoEightK; break;
|
||||
case CSMachineSpectrumModelPlus2: target->model = Target::Model::Plus2; break;
|
||||
case CSMachineSpectrumModelPlus2a: target->model = Target::Model::Plus2a; break;
|
||||
case CSMachineSpectrumModelPlus3: target->model = Target::Model::Plus3; break;
|
||||
}
|
||||
_targets.push_back(std::move(target));
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
<windowStyleMask key="styleMask" titled="YES" documentModal="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="590" height="316"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1440"/>
|
||||
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="590" height="316"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@ -584,11 +584,11 @@ Gw
|
||||
</tabViewItem>
|
||||
<tabViewItem label="ZX81" identifier="zx81" id="Wnn-nQ-gZ6">
|
||||
<view key="view" id="bmd-gL-gzT">
|
||||
<rect key="frame" x="10" y="7" width="400" height="76"/>
|
||||
<rect key="frame" x="10" y="7" width="400" height="186"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5aO-UX-HnX">
|
||||
<rect key="frame" x="108" y="47" width="116" height="25"/>
|
||||
<rect key="frame" x="108" y="157" width="116" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Unexpanded" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="axesIndependently" inset="2" selectedItem="7QC-Ij-hES" id="d3W-Gl-3Mf">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
@ -602,7 +602,7 @@ Gw
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8tU-73-XEE">
|
||||
<rect key="frame" x="18" y="53" width="87" height="16"/>
|
||||
<rect key="frame" x="18" y="163" width="87" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Memory Size:" id="z4b-oR-Yl2">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
@ -622,24 +622,28 @@ Gw
|
||||
</tabViewItem>
|
||||
<tabViewItem label="ZX Spectrum" identifier="spectrum" id="HQv-oF-k8b">
|
||||
<view key="view" id="bMx-F6-JUb">
|
||||
<rect key="frame" x="10" y="7" width="400" height="76"/>
|
||||
<rect key="frame" x="10" y="7" width="400" height="186"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gFZ-d4-WFv">
|
||||
<rect key="frame" x="67" y="47" width="62" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="+2a" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="21" imageScaling="axesIndependently" inset="2" selectedItem="Fo7-NL-Kv5" id="tYs-sA-oek">
|
||||
<rect key="frame" x="67" y="157" width="76" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="16kb" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" tag="16" imageScaling="axesIndependently" inset="2" selectedItem="Fo7-NL-Kv5" id="tYs-sA-oek">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
<menu key="menu" id="8lt-dk-zPr">
|
||||
<items>
|
||||
<menuItem title="+2a" state="on" tag="21" id="Fo7-NL-Kv5"/>
|
||||
<menuItem title="16kb" tag="16" id="Fo7-NL-Kv5"/>
|
||||
<menuItem title="48kb" tag="48" id="xks-Rv-Umd"/>
|
||||
<menuItem title="128kb" tag="128" id="w8h-lY-JLX"/>
|
||||
<menuItem title="+2" tag="2" id="Vvu-ua-pjg"/>
|
||||
<menuItem title="+2a" state="on" tag="21" id="bFk-nC-Txe"/>
|
||||
<menuItem title="+3" tag="3" id="jwx-fZ-vXp"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="fJ3-ma-Byy">
|
||||
<rect key="frame" x="18" y="53" width="46" height="16"/>
|
||||
<rect key="frame" x="18" y="163" width="46" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Model:" id="JId-Tp-LrE">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
|
@ -298,6 +298,10 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
|
||||
case "spectrum":
|
||||
var model: CSMachineSpectrumModel = .plus2a
|
||||
switch spectrumModelTypeButton.selectedItem!.tag {
|
||||
case 16: model = .sixteenK
|
||||
case 48: model = .fortyEightK
|
||||
case 128: model = .oneTwoEightK
|
||||
case 2: model = .plus2
|
||||
case 21: model = .plus2a
|
||||
case 3: model = .plus3
|
||||
default: break
|
||||
|
@ -1258,9 +1258,13 @@ void MainWindow::start_spectrum() {
|
||||
using Target = Analyser::Static::ZXSpectrum::Target;
|
||||
auto target = std::make_unique<Target>();
|
||||
|
||||
switch(ui->oricModelComboBox->currentIndex()) {
|
||||
default: target->model = Target::Model::Plus2a; break;
|
||||
case 1: target->model = Target::Model::Plus3; break;
|
||||
switch(ui->spectrumModelComboBox->currentIndex()) {
|
||||
default: target->model = Target::Model::SixteenK; break;
|
||||
case 1: target->model = Target::Model::FortyEightK; break;
|
||||
case 2: target->model = Target::Model::OneTwoEightK; break;
|
||||
case 3: target->model = Target::Model::Plus2; break;
|
||||
case 4: target->model = Target::Model::Plus2a; break;
|
||||
case 5: target->model = Target::Model::Plus3; break;
|
||||
}
|
||||
|
||||
launchTarget(std::move(target));
|
||||
|
@ -551,6 +551,26 @@
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="spectrumModelComboBox">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>16kb</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>48kb</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>128kb</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>+2</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>+2a</string>
|
||||
|
BIN
ROMImages/ZXSpectrum/128.rom
Normal file
BIN
ROMImages/ZXSpectrum/128.rom
Normal file
Binary file not shown.
BIN
ROMImages/ZXSpectrum/48.rom
Normal file
BIN
ROMImages/ZXSpectrum/48.rom
Normal file
Binary file not shown.
BIN
ROMImages/ZXSpectrum/plus2.rom
Normal file
BIN
ROMImages/ZXSpectrum/plus2.rom
Normal file
Binary file not shown.
@ -9,3 +9,6 @@ material but retain that copyright"."
|
||||
With that in mind, Amstrad have kindly given their permission for the redistribution of their copyrighted material but retain that copyright. Material expected here, copyright Amstrad:
|
||||
|
||||
plus3.rom — the +2a/+3 ROM file, 64kb in size.
|
||||
plus2.rom – the +2 ROM file, 32kb in size.
|
||||
128.rom – the 128kb ROM file, 32kb in size.
|
||||
48.rom – the 16/48kb ROM file, 16kb in size.
|
Loading…
Reference in New Issue
Block a user