1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-11-23 03:32:32 +00:00

Merge pull request #150 from TomHarte/PLLParsing

Introduces PLL-driven tape parsing of Acorn tapes
This commit is contained in:
Thomas Harte 2017-07-16 19:28:51 -04:00 committed by GitHub
commit 5a6b7219b9
15 changed files with 183 additions and 149 deletions

View File

@ -17,7 +17,9 @@ Tape::Tape() :
delegate_(nullptr),
output_({.bits_remaining_until_empty = 0, .cycles_into_pulse = 0}),
last_posted_interrupt_status_(0),
interrupt_status_(0) {}
interrupt_status_(0) {
shifter_.set_delegate(this);
}
void Tape::push_tape_bit(uint16_t bit) {
data_register_ = (uint16_t)((data_register_ >> 1) | (bit << 10));
@ -71,30 +73,11 @@ uint8_t Tape::get_data_register() {
}
void Tape::process_input_pulse(Storage::Tape::Tape::Pulse pulse) {
crossings_[0] = crossings_[1];
crossings_[1] = crossings_[2];
crossings_[2] = crossings_[3];
shifter_.process_pulse(pulse);
}
crossings_[3] = Tape::Unrecognised;
if(pulse.type != Storage::Tape::Tape::Pulse::Zero) {
float pulse_length = (float)pulse.length.length / (float)pulse.length.clock_rate;
if(pulse_length >= 0.35 / 2400.0 && pulse_length < 0.7 / 1200.0) {
crossings_[3] = pulse_length > 1.0 / 3000.0 ? Tape::Long : Tape::Short;
}
// if(pulse_length >= 0.35 / 2400.0 && pulse_length < 0.7 / 2400.0) crossings_[3] = Tape::Short;
// if(pulse_length >= 0.35 / 1200.0 && pulse_length < 0.7 / 1200.0) crossings_[3] = Tape::Long;
}
if(crossings_[0] == Tape::Long && crossings_[1] == Tape::Long) {
push_tape_bit(0);
crossings_[0] = crossings_[1] = Tape::Recognised;
} else {
if(crossings_[0] == Tape::Short && crossings_[1] == Tape::Short && crossings_[2] == Tape::Short && crossings_[3] == Tape::Short) {
push_tape_bit(1);
crossings_[0] = crossings_[1] =
crossings_[2] = crossings_[3] = Tape::Recognised;
}
}
void Tape::acorn_shifter_output_bit(int value) {
push_tape_bit((uint16_t)value);
}
void Tape::run_for_cycles(unsigned int number_of_cycles) {

View File

@ -10,13 +10,16 @@
#define Electron_Tape_h
#include "../../Storage/Tape/Tape.hpp"
#include "../../Storage/Tape/Parsers/Acorn.hpp"
#include "Interrupts.hpp"
#include <cstdint>
namespace Electron {
class Tape: public Storage::Tape::TapePlayer {
class Tape:
public Storage::Tape::TapePlayer,
public Storage::Tape::Acorn::Shifter::Delegate {
public:
Tape();
@ -39,6 +42,8 @@ class Tape: public Storage::Tape::TapePlayer {
inline void set_is_enabled(bool is_enabled) { is_enabled_ = is_enabled; }
void set_is_in_input_mode(bool is_in_input_mode);
void acorn_shifter_output_bit(int value);
private:
void process_input_pulse(Storage::Tape::Tape::Pulse pulse);
inline void push_tape_bit(uint16_t bit);
@ -62,9 +67,7 @@ class Tape: public Storage::Tape::TapePlayer {
uint8_t interrupt_status_, last_posted_interrupt_status_;
Delegate *delegate_;
enum {
Long, Short, Unrecognised, Recognised
} crossings_[4];
::Storage::Tape::Acorn::Shifter shifter_;
};
}

View File

@ -102,7 +102,6 @@ static CPU::Z80::Register registerForRegister(CSTestMachineZ80Register reg) {
NSMutableArray<CSTestMachineZ80BusOperationCapture *> *_busOperationCaptures;
int _timeSeekingReadOpcode;
int _lastOpcodeTime;
}
#pragma mark - Lifecycle
@ -184,9 +183,6 @@ static CPU::Z80::Register registerForRegister(CSTestMachineZ80Register reg) {
}
- (void)testMachineDidPerformBusOperation:(CPU::Z80::PartialMachineCycle::Operation)operation address:(uint16_t)address value:(uint8_t)value timeStamp:(int)timeStamp {
int length = timeStamp - _lastOpcodeTime;
_lastOpcodeTime = timeStamp;
if(self.captureBusActivity) {
CSTestMachineZ80BusOperationCapture *capture = [[CSTestMachineZ80BusOperationCapture alloc] init];
switch(operation) {

View File

@ -12,15 +12,16 @@
using namespace Storage;
DigitalPhaseLockedLoop::DigitalPhaseLockedLoop(int clocks_per_bit, int tolerance, size_t length_of_history) :
DigitalPhaseLockedLoop::DigitalPhaseLockedLoop(int clocks_per_bit, size_t length_of_history) :
clocks_per_bit_(clocks_per_bit),
tolerance_(tolerance),
phase_(0),
window_length_(clocks_per_bit),
phase_error_pointer_(0),
phase_error_history_(length_of_history, 0) {}
offset_history_pointer_(0),
offset_history_(length_of_history, 0),
offset_(0) {}
void DigitalPhaseLockedLoop::run_for_cycles(int number_of_cycles) {
offset_ += number_of_cycles;
phase_ += number_of_cycles;
if(phase_ >= window_length_) {
int windows_crossed = phase_ / window_length_;
@ -41,25 +42,31 @@ void DigitalPhaseLockedLoop::add_pulse() {
if(!window_was_filled_) {
if(delegate_) delegate_->digital_phase_locked_loop_output_bit(1);
window_was_filled_ = true;
post_phase_error(phase_ - (window_length_ >> 1));
post_phase_offset(phase_, offset_);
offset_ = 0;
}
}
void DigitalPhaseLockedLoop::post_phase_error(int error) {
void DigitalPhaseLockedLoop::post_phase_offset(int phase, int offset) {
offset_history_[offset_history_pointer_] = offset;
offset_history_pointer_ = (offset_history_pointer_ + 1) % offset_history_.size();
// use an unweighted average of the stored offsets to compute current window size,
// bucketing them by rounding to the nearest multiple of the base clocks per bit
int total_spacing = 0;
int total_divisor = 0;
for(int offset : offset_history_) {
int multiple = (offset + (clocks_per_bit_ >> 1)) / clocks_per_bit_;
if(!multiple) continue;
total_divisor += multiple;
total_spacing += offset;
}
if(total_divisor) {
window_length_ = total_spacing / total_divisor;
}
int error = phase - (window_length_ >> 1);
// use a simple spring mechanism as a lowpass filter for phase
phase_ -= (error + 1) >> 1;
// use the average of the last few errors to affect frequency
size_t phase_error_history_size = phase_error_history_.size();
phase_error_history_[phase_error_pointer_] = error;
phase_error_pointer_ = (phase_error_pointer_ + 1)%phase_error_history_size;
int total_error = 0;
for(size_t c = 0; c < phase_error_history_size; c++) {
total_error += phase_error_history_[c];
}
int denominator = (int)(phase_error_history_size * 4);
window_length_ += (total_error + (denominator >> 1)) / denominator;
window_length_ = std::max(std::min(window_length_, clocks_per_bit_ + tolerance_), clocks_per_bit_ - tolerance_);
}

View File

@ -20,10 +20,9 @@ class DigitalPhaseLockedLoop {
Instantiates a @c DigitalPhaseLockedLoop.
@param clocks_per_bit The expected number of cycles between each bit of input.
@param tolerance The maximum tolerance for bit windows extremes will be clocks_per_bit ± tolerance.
@param length_of_history The number of historic pulses to consider in locking to phase.
*/
DigitalPhaseLockedLoop(int clocks_per_bit, int tolerance, size_t length_of_history);
DigitalPhaseLockedLoop(int clocks_per_bit, size_t length_of_history);
/*!
Runs the loop, impliedly posting no pulses during that period.
@ -51,9 +50,11 @@ class DigitalPhaseLockedLoop {
private:
Delegate *delegate_;
void post_phase_error(int error);
std::vector<int> phase_error_history_;
size_t phase_error_pointer_;
void post_phase_offset(int phase, int offset);
std::vector<int> offset_history_;
size_t offset_history_pointer_;
int offset_;
int phase_;
int window_length_;

View File

@ -166,7 +166,7 @@ void Controller::set_expected_bit_length(Time bit_length) {
// this conversion doesn't need to be exact because there's a lot of variation to be taken
// account of in rotation speed, air turbulence, etc, so a direct conversion will do
int clocks_per_bit = (int)cycles_per_bit_.get_unsigned_int();
pll_.reset(new DigitalPhaseLockedLoop(clocks_per_bit, clocks_per_bit / 5, 3));
pll_.reset(new DigitalPhaseLockedLoop(clocks_per_bit, 3));
pll_->set_delegate(this);
}

View File

@ -10,9 +10,13 @@
using namespace Storage::Tape::Acorn;
Parser::Parser() :
::Storage::Tape::Parser<WaveType, SymbolType>(),
crc_(0x1021, 0x0000) {}
namespace {
const int PLLClockRate = 1920000;
}
Parser::Parser() : crc_(0x1021, 0x0000) {
shifter_.set_delegate(this);
}
int Parser::get_next_bit(const std::shared_ptr<Storage::Tape::Tape> &tape) {
SymbolType symbol = get_next_symbol(tape);
@ -52,38 +56,38 @@ int Parser::get_next_word(const std::shared_ptr<Storage::Tape::Tape> &tape) {
void Parser::reset_crc() { crc_.reset(); }
uint16_t Parser::get_crc() { return crc_.get_value(); }
void Parser::process_pulse(Storage::Tape::Tape::Pulse pulse) {
switch(pulse.type) {
default: break;
case Storage::Tape::Tape::Pulse::High:
case Storage::Tape::Tape::Pulse::Low:
float pulse_length = pulse.length.get_float();
if(pulse_length >= 0.35 / 2400.0 && pulse_length < 0.7 / 1200.0) {
push_wave(pulse_length > 1.0 / 3000.0 ? WaveType::Long : WaveType::Short); return;
}
break;
}
push_wave(WaveType::Unrecognised);
void Parser::acorn_shifter_output_bit(int value) {
push_symbol(value ? SymbolType::One : SymbolType::Zero);
}
void Parser::inspect_waves(const std::vector<WaveType> &waves) {
if(waves.size() < 2) return;
if(waves[0] == WaveType::Long && waves[1] == WaveType::Long) {
push_symbol(SymbolType::Zero, 2);
return;
}
if(waves.size() < 4) return;
if( waves[0] == WaveType::Short &&
waves[1] == WaveType::Short &&
waves[2] == WaveType::Short &&
waves[3] == WaveType::Short) {
push_symbol(SymbolType::One, 4);
return;
}
remove_waves(1);
void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse) {
shifter_.process_pulse(pulse);
}
Shifter::Shifter() :
pll_(PLLClockRate / 4800, 15),
was_high_(false),
input_pattern_(0),
input_bit_counter_(0) {
pll_.set_delegate(this);
}
void Shifter::process_pulse(const Storage::Tape::Tape::Pulse &pulse) {
pll_.run_for_cycles((int)((float)PLLClockRate * pulse.length.get_float()));
bool is_high = pulse.type == Storage::Tape::Tape::Pulse::High;
if(is_high != was_high_) {
pll_.add_pulse();
}
was_high_ = is_high;
}
void Shifter::digital_phase_locked_loop_output_bit(int value) {
input_pattern_ = ((input_pattern_ << 1) | (unsigned int)value) & 0xf;
switch(input_pattern_) {
case 0x5: delegate_->acorn_shifter_output_bit(0); input_pattern_ = 0; break;
case 0xf: delegate_->acorn_shifter_output_bit(1); input_pattern_ = 0; break;
default: break;;
}
}

View File

@ -11,20 +11,43 @@
#include "TapeParser.hpp"
#include "../../../NumberTheory/CRC.hpp"
#include "../../Disk/DigitalPhaseLockedLoop.hpp"
namespace Storage {
namespace Tape {
namespace Acorn {
enum class WaveType {
Short, Long, Unrecognised
class Shifter: public Storage::DigitalPhaseLockedLoop::Delegate {
public:
Shifter();
void process_pulse(const Storage::Tape::Tape::Pulse &pulse);
class Delegate {
public:
virtual void acorn_shifter_output_bit(int value) = 0;
};
void set_delegate(Delegate *delegate) {
delegate_ = delegate;
}
void digital_phase_locked_loop_output_bit(int value);
private:
Storage::DigitalPhaseLockedLoop pll_;
bool was_high_;
unsigned int input_pattern_;
unsigned int input_bit_counter_;
Delegate *delegate_;
};
enum class SymbolType {
One, Zero
};
class Parser: public Storage::Tape::Parser<WaveType, SymbolType> {
class Parser: public Storage::Tape::Parser<SymbolType>, public Shifter::Delegate {
public:
Parser();
@ -35,10 +58,13 @@ class Parser: public Storage::Tape::Parser<WaveType, SymbolType> {
void reset_crc();
uint16_t get_crc();
void acorn_shifter_output_bit(int value);
void process_pulse(const Storage::Tape::Tape::Pulse &pulse);
private:
void process_pulse(Storage::Tape::Tape::Pulse pulse);
void inspect_waves(const std::vector<WaveType> &waves);
bool did_update_shifter(int new_value, int length);
NumberTheory::CRC16 crc_;
Shifter shifter_;
};
}

View File

@ -12,7 +12,7 @@
using namespace Storage::Tape::Commodore;
Parser::Parser() :
Storage::Tape::Parser<WaveType, SymbolType>(),
Storage::Tape::PulseClassificationParser<WaveType, SymbolType>(),
wave_period_(0.0f),
previous_was_high_(false),
parity_byte_(0) {}
@ -262,7 +262,7 @@ uint16_t Parser::get_next_short(const std::shared_ptr<Storage::Tape::Tape> &tape
indicates a high to low transition, inspects the time since the last transition, to produce
a long, medium, short or unrecognised wave period.
*/
void Parser::process_pulse(Storage::Tape::Tape::Pulse pulse)
void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse)
{
// The Complete Commodore Inner Space Anthology, P 97, gives half-cycle lengths of:
// short: 182µs => 0.000364s cycle

View File

@ -56,7 +56,7 @@ struct Data {
bool duplicate_matched;
};
class Parser: public Storage::Tape::Parser<WaveType, SymbolType> {
class Parser: public Storage::Tape::PulseClassificationParser<WaveType, SymbolType> {
public:
Parser();
@ -126,7 +126,7 @@ class Parser: public Storage::Tape::Parser<WaveType, SymbolType> {
indicates a high to low transition, inspects the time since the last transition, to produce
a long, medium, short or unrecognised wave period.
*/
void process_pulse(Storage::Tape::Tape::Pulse pulse);
void process_pulse(const Storage::Tape::Tape::Pulse &pulse);
bool previous_was_high_;
float wave_period_;

View File

@ -45,7 +45,7 @@ bool Parser::sync_and_get_encoding_speed(const std::shared_ptr<Storage::Tape::Ta
return false;
}
void Parser::process_pulse(Storage::Tape::Tape::Pulse pulse)
void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse)
{
const float maximum_short_length = 0.000512f;
const float maximum_medium_length = 0.000728f;

View File

@ -26,13 +26,13 @@ enum class SymbolType {
One, Zero, FoundFast, FoundSlow
};
class Parser: public Storage::Tape::Parser<WaveType, SymbolType> {
class Parser: public Storage::Tape::PulseClassificationParser<WaveType, SymbolType> {
public:
int get_next_byte(const std::shared_ptr<Storage::Tape::Tape> &tape, bool use_fast_encoding);
bool sync_and_get_encoding_speed(const std::shared_ptr<Storage::Tape::Tape> &tape);
private:
void process_pulse(Storage::Tape::Tape::Pulse pulse);
void process_pulse(const Storage::Tape::Tape::Pulse &pulse);
void inspect_waves(const std::vector<WaveType> &waves);
enum DetectionMode {

View File

@ -10,6 +10,7 @@
#define TapeParser_hpp
#include "../Tape.hpp"
#include "../../Disk/DigitalPhaseLockedLoop.hpp"
#include <memory>
#include <vector>
@ -18,18 +19,9 @@
namespace Storage {
namespace Tape {
/*!
A partly-abstract base class to help in the authorship of tape format parsers;
provides hooks for pulse classification from pulses to waves and for symbol identification from
waves.
Very optional, not intended to box in the approaches taken for analysis.
*/
template <typename WaveType, typename SymbolType> class Parser {
template <typename SymbolType> class Parser {
public:
/// Instantiates a new parser with the supplied @c tape.
Parser() : has_next_symbol_(false), error_flag_(false) {}
/// Resets the error flag.
void reset_error_flag() { error_flag_ = false; }
/// @returns @c true if an error has occurred since the error flag was last reset; @c false otherwise.
@ -59,12 +51,18 @@ template <typename WaveType, typename SymbolType> class Parser {
next_symbol_ = symbol;
}
/*!
Should be implemented by subclasses. Consumes @c pulse. Is likely either to call @c push_wave
or to take no action.
@returns `true` if there is no data left on the tape and the WaveType queue has been exhausted; `false` otherwise.
*/
virtual void process_pulse(Storage::Tape::Tape::Pulse pulse) = 0;
bool is_at_end(const std::shared_ptr<Storage::Tape::Tape> &tape) {
return tape->is_at_end() && !has_next_symbol_;
}
protected:
/*!
Should be implemented by subclasses. Consumes @c pulse.
*/
virtual void process_pulse(const Storage::Tape::Tape::Pulse &pulse) = 0;
/*!
An optional implementation for subclasses; called to announce that the tape has ended: that
@ -72,7 +70,50 @@ template <typename WaveType, typename SymbolType> class Parser {
*/
virtual void mark_end() {}
/*!
Sets @c symbol as the newly-recognised symbol.
*/
void push_symbol(SymbolType symbol) {
has_next_symbol_ = true;
next_symbol_ = symbol;
}
void set_error_flag() {
error_flag_ = true;
}
bool error_flag_;
SymbolType next_symbol_;
bool has_next_symbol_;
};
/*!
A partly-abstract base class to help in the authorship of tape format parsers;
provides hooks for receipt of pulses, which are intended to be classified into waves,
and for symbol identification from waves.
Very optional, not intended to box in the approaches taken for analysis. See also
the PLLParser.
*/
template <typename WaveType, typename SymbolType> class PulseClassificationParser: public Parser<SymbolType> {
public:
virtual void process_pulse(const Storage::Tape::Tape::Pulse &pulse) = 0;
/*
process_pulse should either call @c push_wave or to take no action.
*/
protected:
/*!
Sets @c symbol as the newly-recognised symbol and removes @c nunber_of_waves waves from the front of the list.
Expected to be called by subclasses from @c process_pulse when it recognises that the first @c number_of_waves
waves together represent @c symbol.
*/
void push_symbol(SymbolType symbol, int number_of_waves) {
Parser<SymbolType>::push_symbol(symbol);
remove_waves(number_of_waves);
}
/*!
Adds @c wave to the back of the list of recognised waves and calls @c inspect_waves to check for a new symbol.
@ -94,32 +135,7 @@ template <typename WaveType, typename SymbolType> class Parser {
wave_queue_.erase(wave_queue_.begin(), wave_queue_.begin()+number_of_waves);
}
/*!
Sets @c symbol as the newly-recognised symbol and removes @c nunber_of_waves waves from the front of the list.
Expected to be called by subclasses from @c process_pulse when it recognises that the first @c number_of_waves
waves together represent @c symbol.
*/
void push_symbol(SymbolType symbol, int number_of_waves) {
has_next_symbol_ = true;
next_symbol_ = symbol;
remove_waves(number_of_waves);
}
void set_error_flag() {
error_flag_ = true;
}
/*!
@returns `true` if there is no data left on the tape and the WaveType queue has been exhausted; `false` otherwise.
*/
bool is_at_end(const std::shared_ptr<Storage::Tape::Tape> &tape) {
return tape->is_at_end() && wave_queue_.empty() && !has_next_symbol_;
}
private:
bool error_flag_;
/*!
Should be implemented by subclasses. Inspects @c waves for a potential new symbol. If one is
found should call @c push_symbol. May wish alternatively to call @c remove_waves to have entries
@ -129,8 +145,6 @@ template <typename WaveType, typename SymbolType> class Parser {
virtual void inspect_waves(const std::vector<WaveType> &waves) = 0;
std::vector<WaveType> wave_queue_;
SymbolType next_symbol_;
bool has_next_symbol_;
};
}

View File

@ -12,7 +12,7 @@ using namespace Storage::Tape::ZX8081;
Parser::Parser() : pulse_was_high_(false), pulse_time_(0) {}
void Parser::process_pulse(Storage::Tape::Tape::Pulse pulse) {
void Parser::process_pulse(const Storage::Tape::Tape::Pulse &pulse) {
pulse_time_ += pulse.length;
bool pulse_is_high = pulse.type == Storage::Tape::Tape::Pulse::High;

View File

@ -29,7 +29,7 @@ enum class SymbolType {
One, Zero, FileGap, Unrecognised
};
class Parser: public Storage::Tape::Parser<WaveType, SymbolType> {
class Parser: public Storage::Tape::PulseClassificationParser<WaveType, SymbolType> {
public:
Parser();
@ -50,7 +50,7 @@ class Parser: public Storage::Tape::Parser<WaveType, SymbolType> {
Time pulse_time_;
void post_pulse();
void process_pulse(Storage::Tape::Tape::Pulse pulse);
void process_pulse(const Storage::Tape::Tape::Pulse &pulse);
void mark_end();
void inspect_waves(const std::vector<WaveType> &waves);