mirror of
https://github.com/TomHarte/CLK.git
synced 2025-11-02 18:16:08 +00:00
274 lines
8.3 KiB
C++
274 lines
8.3 KiB
C++
//
|
|
// Flywheel.hpp
|
|
// Clock Signal
|
|
//
|
|
// Created by Thomas Harte on 11/02/2016.
|
|
// Copyright 2016 Thomas Harte. All rights reserved.
|
|
//
|
|
|
|
#pragma once
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cstdlib>
|
|
#include <cstdint>
|
|
#include <utility>
|
|
|
|
namespace Outputs::CRT {
|
|
|
|
/*!
|
|
Provides timing for a two-phase signal consisting of a retrace phase followed by a scan phase,
|
|
announcing the start and end of retrace and providing the abiliy to read the current
|
|
scanning position.
|
|
|
|
The @c Flywheel will attempt to converge with timing implied by synchronisation pulses.
|
|
*/
|
|
struct Flywheel {
|
|
/*!
|
|
Constructs an instance of @c Flywheel.
|
|
|
|
@param standard_period The expected amount of time between one synchronisation and the next.
|
|
@param retrace_time The amount of time it takes to complete a retrace.
|
|
@param sync_error_window The permitted deviation of sync timings from the norm.
|
|
*/
|
|
Flywheel(const int standard_period, const int retrace_time, const int sync_error_window) noexcept :
|
|
standard_period_(standard_period),
|
|
retrace_time_(retrace_time),
|
|
sync_error_window_(sync_error_window),
|
|
counter_before_retrace_(standard_period - retrace_time),
|
|
expected_next_sync_(standard_period) {}
|
|
|
|
Flywheel() = default;
|
|
|
|
enum SyncEvent {
|
|
None,
|
|
StartRetrace,
|
|
EndRetrace
|
|
};
|
|
/*!
|
|
@param sync_is_requested @c true indicates that the flywheel should act as though having
|
|
received a synchronisation request now; @c false indicates no such event was detected.
|
|
|
|
@param cycles_to_run_for The maximum number of cycles to look ahead.
|
|
|
|
@returns A pair of the next synchronisation event and number of cycles until it occurs.
|
|
*/
|
|
std::pair<SyncEvent, int> next_event_in_period(
|
|
const bool sync_is_requested,
|
|
const int cycles_to_run_for
|
|
) {
|
|
// Calculates the next expected value for an event given the current expectation and the actual value.
|
|
//
|
|
// In practice this is a weighted mix of the two values, with the exact weighting affecting how
|
|
// quickly the flywheel adjusts to new input. It's a IIR lowpass filter.
|
|
constexpr auto mix = [](const int expected, const int actual) {
|
|
return (expected + actual) >> 1;
|
|
};
|
|
|
|
// A debugging helper.
|
|
constexpr auto require_positive = [](const int value) {
|
|
assert(value >= 0);
|
|
return value;
|
|
};
|
|
|
|
// If sync is signalled _now_, consider adjusting expected_next_sync_.
|
|
if(sync_is_requested) {
|
|
const auto last_sync = expected_next_sync_;
|
|
if(counter_ < sync_error_window_ || counter_ > expected_next_sync_ - sync_error_window_) {
|
|
const int time_now = (counter_ < sync_error_window_) ? expected_next_sync_ + counter_ : counter_;
|
|
expected_next_sync_ = mix(expected_next_sync_, time_now);
|
|
} else {
|
|
++number_of_surprises_;
|
|
|
|
if(counter_ < retrace_time_ + (expected_next_sync_ >> 1)) {
|
|
expected_next_sync_ = mix(expected_next_sync_, standard_period_ + sync_error_window_);
|
|
} else {
|
|
expected_next_sync_ = mix(expected_next_sync_, standard_period_ - sync_error_window_);
|
|
}
|
|
}
|
|
last_adjustment_ = expected_next_sync_ - last_sync;
|
|
}
|
|
|
|
SyncEvent proposed_event = SyncEvent::None;
|
|
int proposed_sync_time = cycles_to_run_for;
|
|
|
|
// End an ongoing retrace?
|
|
if(counter_ < retrace_time_ && counter_ + proposed_sync_time >= retrace_time_) {
|
|
proposed_sync_time = require_positive(retrace_time_ - counter_);
|
|
proposed_event = SyncEvent::EndRetrace;
|
|
}
|
|
|
|
// Start a retrace?
|
|
if(counter_ + proposed_sync_time >= expected_next_sync_) {
|
|
// A change in expectations above may have moved the expected sync time to before now.
|
|
// If so, just start sync ASAP.
|
|
proposed_sync_time = std::max(0, expected_next_sync_ - counter_);
|
|
proposed_event = SyncEvent::StartRetrace;
|
|
}
|
|
|
|
return std::make_pair(proposed_event, proposed_sync_time);
|
|
}
|
|
|
|
/*!
|
|
Advances a nominated amount of time, applying a previously returned synchronisation event
|
|
at the end of that period.
|
|
|
|
@param cycles_advanced The amount of time to run for.
|
|
|
|
@param event The synchronisation event to apply after that period.
|
|
*/
|
|
void apply_event(const int cycles_advanced, const SyncEvent event) {
|
|
// In debug builds, perform a sanity check for counter overflow.
|
|
#ifndef NDEBUG
|
|
const int old_counter = counter_;
|
|
#endif
|
|
counter_ += cycles_advanced;
|
|
assert(old_counter <= counter_);
|
|
|
|
switch(event) {
|
|
default: return;
|
|
case StartRetrace:
|
|
counter_before_retrace_ = counter_ - retrace_time_;
|
|
counter_ = 0;
|
|
return;
|
|
}
|
|
}
|
|
|
|
/*!
|
|
Returns the current output position; while in retrace this will go down towards 0, while in scan
|
|
it will go upward.
|
|
|
|
@returns The current output position.
|
|
*/
|
|
int current_output_position() const {
|
|
if(counter_ < retrace_time_) {
|
|
const int retrace_distance = int((int64_t(counter_) * int64_t(standard_period_)) / int64_t(retrace_time_));
|
|
if(retrace_distance > counter_before_retrace_) return 0;
|
|
return counter_before_retrace_ - retrace_distance;
|
|
}
|
|
|
|
return counter_ - retrace_time_;
|
|
}
|
|
|
|
/*!
|
|
Returns the current 'phase' — 0 is the start of the display; a count up to 0 from a negative number represents
|
|
the retrace period and it will then count up to @c locked_scan_period().
|
|
|
|
@returns The current output position.
|
|
*/
|
|
int current_phase() const {
|
|
return counter_ - retrace_time_;
|
|
}
|
|
|
|
/*!
|
|
@returns the amount of time since retrace last began. Time then counts monotonically up from zero.
|
|
*/
|
|
int current_time() const {
|
|
return counter_;
|
|
}
|
|
|
|
/*!
|
|
@returns whether the output is currently retracing.
|
|
*/
|
|
bool is_in_retrace() const {
|
|
return counter_ < retrace_time_;
|
|
}
|
|
|
|
/*!
|
|
@returns the expected length of the scan period (excluding retrace).
|
|
*/
|
|
int scan_period() const {
|
|
return standard_period_ - retrace_time_;
|
|
}
|
|
|
|
/*!
|
|
@returns the actual length of the scan period (excluding retrace).
|
|
*/
|
|
int locked_scan_period() const {
|
|
return expected_next_sync_ - retrace_time_;
|
|
}
|
|
|
|
/*!
|
|
@returns the expected length of a complete scan and retrace cycle.
|
|
*/
|
|
int standard_period() const {
|
|
return standard_period_;
|
|
}
|
|
|
|
/*!
|
|
@returns the actual current period for a complete scan (including retrace).
|
|
*/
|
|
int locked_period() const {
|
|
return expected_next_sync_;
|
|
}
|
|
|
|
/*!
|
|
@returns the amount by which the @c locked_period was adjusted, the last time that an adjustment was applied.
|
|
*/
|
|
int last_period_adjustment() const {
|
|
return last_adjustment_;
|
|
}
|
|
|
|
/*!
|
|
@returns the number of synchronisation events that have seemed surprising since the last time this method was called;
|
|
a low number indicates good synchronisation.
|
|
*/
|
|
int get_and_reset_number_of_surprises() {
|
|
const int result = number_of_surprises_;
|
|
number_of_surprises_ = 0;
|
|
return result;
|
|
}
|
|
|
|
/*!
|
|
@returns A count of the number of retraces so far performed.
|
|
*/
|
|
int number_of_retraces() const {
|
|
return number_of_retraces_;
|
|
}
|
|
|
|
/*!
|
|
@returns The amount of time this flywheel spends in retrace, as supplied at construction.
|
|
*/
|
|
int retrace_period() const {
|
|
return retrace_time_;
|
|
}
|
|
|
|
/*!
|
|
@returns `true` if a sync is expected soon or if the time at which it was expected (or received) was recent.
|
|
*/
|
|
bool is_near_expected_sync() const {
|
|
return
|
|
(counter_ < (standard_period_ / 100)) ||
|
|
(counter_ >= expected_next_sync_ - (standard_period_ / 100));
|
|
}
|
|
|
|
private:
|
|
int standard_period_; // Idealised length of time between syncs.
|
|
int retrace_time_; // Amount of time it takes to perform a retrace.
|
|
int sync_error_window_; // The window either side of the next expected sync in which other syncs are accepted.
|
|
|
|
int counter_ = 0; // Time since the _start_ of the last sync.
|
|
int counter_before_retrace_; // Value of counter_ immediately before retrace began.
|
|
int expected_next_sync_; // Current expection of when the next sync will occur (implying velocity).
|
|
|
|
int number_of_surprises_ = 0; // Count of surprising syncs.
|
|
int number_of_retraces_ = 0; // Numer of retraces to date.
|
|
|
|
int last_adjustment_ = 0; // The amount by which expected_next_sync_ was adjusted at the last sync.
|
|
|
|
/*
|
|
Implementation notes:
|
|
|
|
Retrace takes a fixed amount of time and runs during [0, _retrace_time).
|
|
|
|
For the current line, scan then occurs from [_retrace_time, _expected_next_sync), at which point
|
|
retrace begins and the internal counter is reset.
|
|
|
|
All synchronisation events that occur within (-_sync_error_window, _sync_error_window) of the
|
|
expected synchronisation time will cause a proportional adjustment in the expected time for the next
|
|
synchronisation. Other synchronisation events are clamped as though they occurred in that range.
|
|
*/
|
|
};
|
|
|
|
}
|