diff --git a/Machines/Enterprise/Dave.cpp b/Machines/Enterprise/Dave.cpp index 6bad6d676..39a152630 100644 --- a/Machines/Enterprise/Dave.cpp +++ b/Machines/Enterprise/Dave.cpp @@ -225,21 +225,11 @@ void TimedInterruptSource::write(uint16_t address, uint8_t value) { channels_[address >> 1].reload = uint16_t((channels_[address >> 1].reload & 0x00ff) | ((value & 0xf) << 8)); break; - case 7: { + case 7: channels_[0].sync = value & 0x01; channels_[1].sync = value & 0x02; - - const InterruptRate rate = InterruptRate((value >> 5) & 3); - if(rate != rate_) { - rate_ = rate; - - if(rate_ >= InterruptRate::ToneGenerator0) { - programmable_level_ = channels_[int(rate_) - int(InterruptRate::ToneGenerator0)].level; - } else { - programmable_offset_ = programmble_reload(rate_); - } - } - } break; + rate_ = InterruptRate((value >> 5) & 3); + break; case 31: global_divider_ = Cycles(2 + ((value >> 1)&1)); @@ -269,6 +259,7 @@ void TimedInterruptSource::update_channel(int c, bool is_linked, int decrement) // from high to low is amongst the included flips. if(is_linked && num_flips + channels_[c].level >= 2) { interrupts_ |= uint8_t(Interrupt::VariableFrequency); + programmable_level_ ^= true; } channels_[c].level ^= (num_flips & 1); @@ -287,67 +278,59 @@ void TimedInterruptSource::run_for(Cycles duration) { return; } - // Update the 1Hz interrupt. - one_hz_offset_ -= cycles; - if(one_hz_offset_ <= Cycles(0)) { + // Update the two-second counter, from which the 1Hz, 50Hz and 1000Hz signals + // are derived. + const int previous_counter = two_second_counter_; + two_second_counter_ = (two_second_counter_ + cycles.as()) % 500'000; + + // Check for a 1Hz rollover. + if(previous_counter / 250'000 != two_second_counter_ / 250'000) { interrupts_ |= uint8_t(Interrupt::OneHz); - one_hz_offset_ += clock_rate; + } + + // Check for 1kHz or 50Hz rollover; + switch(rate_) { + default: break; + case InterruptRate::OnekHz: + if(previous_counter / 250 != two_second_counter_ / 250) { + interrupts_ |= uint8_t(Interrupt::VariableFrequency); + programmable_level_ ^= true; + } + break; + case InterruptRate::FiftyHz: + if(previous_counter / 5'000 != two_second_counter_ / 5'000) { + interrupts_ |= uint8_t(Interrupt::VariableFrequency); + programmable_level_ ^= true; + } + break; } // Update the two tone channels. update_channel(0, rate_ == InterruptRate::ToneGenerator0, cycles.as()); update_channel(1, rate_ == InterruptRate::ToneGenerator1, cycles.as()); - - // Update the programmable-frequency interrupt. - if(rate_ < InterruptRate::ToneGenerator0) { - programmable_offset_ -= cycles.as(); - if(programmable_offset_ <= 0) { - if(programmable_level_) { - interrupts_ |= uint8_t(Interrupt::VariableFrequency); - } - programmable_level_ ^= true; - programmable_offset_ = programmble_reload(rate_); - } - } } Cycles TimedInterruptSource::get_next_sequence_point() const { - int result = one_hz_offset_.as(); - + // Since both the 1kHz and 50Hz timers are integer dividers of the 1Hz timer, there's no need + // to factor that one in when determining the next sequence point for either of those. switch(rate_) { - case InterruptRate::OnekHz: - case InterruptRate::FiftyHz: - result = std::min(result, programmable_offset_ + (!programmable_level_) * programmble_reload(rate_)); - break; + default: + case InterruptRate::OnekHz: return Cycles(250 - (two_second_counter_ % 250)); + case InterruptRate::FiftyHz: return Cycles(5000 - (two_second_counter_ % 5000)); + case InterruptRate::ToneGenerator0: case InterruptRate::ToneGenerator1: { const auto &channel = channels_[int(rate_) - int(InterruptRate::ToneGenerator0)]; const int cycles_until_interrupt = channel.value + 1 + (!channel.level) * (channel.reload + 1); - result = std::min(result, cycles_until_interrupt); - } break; - } - return Cycles(result); + return Cycles(std::min( + 250'000 - (two_second_counter_ % 250'000), + cycles_until_interrupt + )); + } + } } uint8_t TimedInterruptSource::get_divider_state() { - bool programmable_flag = false; - switch(rate_) { - case InterruptRate::OnekHz: - case InterruptRate::FiftyHz: - programmable_flag = programmable_level_; - break; - case InterruptRate::ToneGenerator0: - programmable_flag = channels_[0].level; - break; - case InterruptRate::ToneGenerator1: - programmable_flag = channels_[1].level; - break; - } - - // one_hz_offset_ counts downwards, so when it crosses the halfway mark - // it enters the high part of its wave. - return - (one_hz_offset_ < half_clock_rate ? 0x4 : 0x0) | - (programmable_flag ? 0x1 : 0x0); + return uint8_t((two_second_counter_ / 250'000) * 4 | programmable_level_); } diff --git a/Machines/Enterprise/Dave.hpp b/Machines/Enterprise/Dave.hpp index 760fd05a4..e2443779f 100644 --- a/Machines/Enterprise/Dave.hpp +++ b/Machines/Enterprise/Dave.hpp @@ -152,7 +152,7 @@ class TimedInterruptSource { static constexpr Cycles half_clock_rate{125000}; // Global divider (i.e. 8MHz/12Mhz switch). - Cycles global_divider_; + Cycles global_divider_ = Cycles(2); Cycles run_length_; // Interrupts that have fired since get_new_interrupts() @@ -160,7 +160,7 @@ class TimedInterruptSource { uint8_t interrupts_ = 0; // A counter for the 1Hz interrupt. - Cycles one_hz_offset_ = clock_rate; + int two_second_counter_ = 0; // A counter specific to the 1kHz and 50Hz timers, if in use. enum class InterruptRate { @@ -169,7 +169,6 @@ class TimedInterruptSource { ToneGenerator0, ToneGenerator1, } rate_ = InterruptRate::OnekHz; - int programmable_offset_ = programmble_reload(InterruptRate::OnekHz); bool programmable_level_ = false; // A local duplicate of the counting state of the first two audio @@ -181,13 +180,6 @@ class TimedInterruptSource { bool level = false; } channels_[2]; void update_channel(int c, bool is_linked, int decrement); - static constexpr int programmble_reload(InterruptRate rate) { - switch(rate) { - default: return 0; - case InterruptRate::OnekHz: return 125; - case InterruptRate::FiftyHz: return 2500; - } - } }; } diff --git a/OSBindings/Mac/Clock SignalTests/EnterpriseDaveTests.mm b/OSBindings/Mac/Clock SignalTests/EnterpriseDaveTests.mm index 5d491c48e..cd8aa3ba8 100644 --- a/OSBindings/Mac/Clock SignalTests/EnterpriseDaveTests.mm +++ b/OSBindings/Mac/Clock SignalTests/EnterpriseDaveTests.mm @@ -23,78 +23,80 @@ _interruptSource = std::make_unique(); } -- (void)performTestExpectedToggles:(int)expectedToggles expectedInterrupts:(int)expectedInterrupts { - // Check that the programmable timer flag toggles at a rate - // of 2kHz, causing 1000 interrupts, and that sequence points - // are properly predicted. +/// Tests that the programmable timer flag toggles and produces interrupts +/// at the rate specified, and that the flag toggles when interrupts are signalled. +- (void)performTestExpectedInterrupts:(double)expectedInterruptsPerSecond mode:(int)mode { + // If a programmable timer mode is requested, synchronise both channels. + if(mode >= 2) { + _interruptSource->write(0xa7, 3); + _interruptSource->run_for(Cycles(2)); + } + + // Set mode (and disable sync, if it was applied). + _interruptSource->write(0xa7, mode << 5); + int toggles = 0; int interrupts = 0; - uint8_t dividerState = _interruptSource->get_divider_state(); + uint8_t dividerState = _interruptSource->get_divider_state() & 1; int nextSequencePoint = _interruptSource->get_next_sequence_point().as(); - for(int c = 0; c < 250000; c++) { - // Advance one cycle. Clock is 250,000 Hz. - _interruptSource->run_for(Cycles(1)); - - const uint8_t newDividerState = _interruptSource->get_divider_state(); - if((dividerState^newDividerState)&0x1) { - ++toggles; - } - dividerState = newDividerState; + for(int c = 0; c < 250000 * 5; c++) { + // Advance one cycle. Clock is 500,000 Hz. + _interruptSource->run_for(Cycles(2)); --nextSequencePoint; + // Check for a status bit change. + const uint8_t newDividerState = _interruptSource->get_divider_state(); + const bool didToggle = (dividerState^newDividerState)&0x1; + dividerState = newDividerState; + toggles += didToggle; + // Check for the relevant interrupt. const uint8_t newInterrupts = _interruptSource->get_new_interrupts(); - if(newInterrupts & 0x02) { - ++interrupts; + if(newInterrupts) { XCTAssertEqual(nextSequencePoint, 0); - XCTAssertEqual(dividerState&0x1, 0); nextSequencePoint = _interruptSource->get_next_sequence_point().as(); - } - // Failing that, confirm that the other interrupt happend. - if(!nextSequencePoint) { - XCTAssertTrue(newInterrupts & 0x08); - nextSequencePoint = _interruptSource->get_next_sequence_point().as(); + if(newInterrupts & 0x02) { + ++interrupts; + XCTAssertTrue(didToggle); + } else { + // Failing that, confirm that the other interrupt happend. + XCTAssertTrue(newInterrupts & 0x08); + } } XCTAssertEqual(nextSequencePoint, _interruptSource->get_next_sequence_point().as(), @"At cycle %d", c); } - XCTAssertEqual(toggles, expectedToggles); - XCTAssertEqual(interrupts, expectedInterrupts); + XCTAssertEqual(toggles, int(expectedInterruptsPerSecond * 5.0)); + XCTAssertEqual(interrupts, int(expectedInterruptsPerSecond * 5.0)); } - (void)test1kHzTimer { - // Set 1kHz timer. - _interruptSource->write(7, 0 << 5); - [self performTestExpectedToggles:2000 expectedInterrupts:1000]; + [self performTestExpectedInterrupts:1000.0 mode:0]; } - (void)test50HzTimer { - // Set 50Hz timer. - _interruptSource->write(7, 1 << 5); - [self performTestExpectedToggles:100 expectedInterrupts:50]; + [self performTestExpectedInterrupts:50.0 mode:1]; } - (void)testTone0Timer { // Set tone generator 0 as the interrupt source, with a divider of 137; // apply sync momentarily. - _interruptSource->write(7, 2 << 5); _interruptSource->write(0, 137); _interruptSource->write(1, 0); - [self performTestExpectedToggles:250000/138 expectedInterrupts:250000/(138*2)]; + [self performTestExpectedInterrupts:250000.0/(138.0 * 2.0) mode:2]; } - (void)testTone1Timer { // Set tone generator 1 as the interrupt source, with a divider of 961; // apply sync momentarily. - _interruptSource->write(7, 3 << 5); _interruptSource->write(2, 961 & 0xff); _interruptSource->write(3, (961 >> 8) & 0xff); - [self performTestExpectedToggles:250000/961 expectedInterrupts:250000/(961*2)]; + [self performTestExpectedInterrupts:250000.0/(962.0 * 2.0) mode:3]; } @end