diff --git a/NumberTheory/Factors.cpp b/NumberTheory/Factors.cpp index 067615653..4c44cfbfc 100644 --- a/NumberTheory/Factors.cpp +++ b/NumberTheory/Factors.cpp @@ -30,5 +30,5 @@ unsigned int NumberTheory::greatest_common_divisor(unsigned int a, unsigned int unsigned int NumberTheory::least_common_multiple(unsigned int a, unsigned int b) { unsigned int gcd = greatest_common_divisor(a, b); - return (a*b) / gcd; + return (a / gcd) * (b / gcd); } diff --git a/NumberTheory/Factors.hpp b/NumberTheory/Factors.hpp index bdc92c011..d2c280a11 100644 --- a/NumberTheory/Factors.hpp +++ b/NumberTheory/Factors.hpp @@ -10,7 +10,15 @@ #define Factors_hpp namespace NumberTheory { + /*! + @returns The greatest common divisor of @c a and @c b as computed by Euclid's algorithm. + */ unsigned int greatest_common_divisor(unsigned int a, unsigned int b); + + /*! + @returns The least common multiple of @c a and @c b computed indirectly via Euclid's greatest + common divisor algorithm. + */ unsigned int least_common_multiple(unsigned int a, unsigned int b); } diff --git a/Storage/Disk/DiskDrive.hpp b/Storage/Disk/DiskDrive.hpp index 7d3cb560a..c207a2729 100644 --- a/Storage/Disk/DiskDrive.hpp +++ b/Storage/Disk/DiskDrive.hpp @@ -15,26 +15,74 @@ namespace Storage { +/*! + Provides the shell for emulating a disk drive — something that takes a disk and has a drive head + that steps between tracks, using a phase locked loop ('PLL') to decode a bit stream from the surface of + the disk. + + Partly abstract; it is expected that subclasses will provide methods to deal with receiving a newly-recognised + bit from the PLL and with crossing the index hole. + + TODO: double sided disks, communication of head size and permissible stepping extents, appropriate + simulation of gain. +*/ class DiskDrive: public DigitalPhaseLockedLoop::Delegate, public TimedEventLoop { public: + /*! + Constructs a @c DiskDrive that will be run at @c clock_rate and runs its PLL at @c clock_rate*clock_rate_multiplier, + spinning inserted disks at @c revolutions_per_minute. + */ DiskDrive(unsigned int clock_rate, unsigned int clock_rate_multiplier, unsigned int revolutions_per_minute); + /*! + Communicates to the PLL the expected length of a bit. + */ void set_expected_bit_length(Time bit_length); + /*! + Inserts @c disk into the drive. + */ void set_disk(std::shared_ptr disk); + + /*! + @returns @c true if a disk is currently inserted; @c false otherwise. + */ bool has_disk(); + /*! + Advances the drive by @c number_of_cycles cycles. + */ void run_for_cycles(int number_of_cycles); + /*! + @returns @c true if the drive head is currently at track zero; @c false otherwise. + */ bool get_is_track_zero(); + + /*! + Steps the disk head the specified number of tracks. Positive numbers step inwards, negative numbers + step outwards. + */ void step(int direction); + + /*! + Enables or disables the disk motor. + */ void set_motor_on(bool motor_on); // to satisfy DigitalPhaseLockedLoop::Delegate void digital_phase_locked_loop_output_bit(int value); protected: + /*! + Should be implemented by subclasses; communicates each bit that the PLL recognises, also specifying + the amount of time since the index hole was last seen. + */ virtual void process_input_bit(int value, unsigned int cycles_since_index_hole) = 0; + + /*! + Should be implemented by subcalsses; communicates that the index hole has been reached. + */ virtual void process_index_hole() = 0; // for TimedEventLoop diff --git a/Storage/Disk/Encodings/CommodoreGCR.cpp b/Storage/Disk/Encodings/CommodoreGCR.cpp index 36667141d..fb756a345 100644 --- a/Storage/Disk/Encodings/CommodoreGCR.cpp +++ b/Storage/Disk/Encodings/CommodoreGCR.cpp @@ -18,3 +18,34 @@ Time Storage::Encodings::CommodoreGCR::length_of_a_bit_in_time_zone(unsigned int duration.clock_rate = 4000000; return duration; } + +unsigned int Storage::Encodings::CommodoreGCR::encoding_for_nibble(uint8_t nibble) +{ + switch(nibble & 0xf) + { + case 0x0: return 0x0a; + case 0x1: return 0x0b; + case 0x2: return 0x12; + case 0x3: return 0x13; + case 0x4: return 0x0e; + case 0x5: return 0x0f; + case 0x6: return 0x16; + case 0x7: return 0x17; + case 0x8: return 0x09; + case 0x9: return 0x19; + case 0xa: return 0x1a; + case 0xb: return 0x1b; + case 0xc: return 0x0d; + case 0xd: return 0x1d; + case 0xe: return 0x1e; + case 0xf: return 0x15; + + // for the benefit of the compiler; clearly unreachable + default: return 0xff; + } +} + +unsigned int Storage::Encodings::CommodoreGCR::encoding_for_byte(uint8_t byte) +{ + return encoding_for_nibble(byte) | (encoding_for_nibble(byte >> 4) << 5); +} diff --git a/Storage/Disk/Encodings/CommodoreGCR.hpp b/Storage/Disk/Encodings/CommodoreGCR.hpp index 0617af963..38c32efe7 100644 --- a/Storage/Disk/Encodings/CommodoreGCR.hpp +++ b/Storage/Disk/Encodings/CommodoreGCR.hpp @@ -10,11 +10,27 @@ #define CommodoreGCR_hpp #include "../../Storage.hpp" +#include namespace Storage { namespace Encodings { + namespace CommodoreGCR { + /*! + @returns the proportion of a second that each bit of data within the specified @c time_zone + should idiomatically occupy. + */ Time length_of_a_bit_in_time_zone(unsigned int time_zone); + + /*! + @returns the five-bit GCR encoding for the low four bits of @c nibble. + */ + unsigned int encoding_for_nibble(uint8_t nibble); + + /*! + @returns the ten-bit GCR encoding for @c byte. + */ + unsigned int encoding_for_byte(uint8_t byte); } } } diff --git a/Storage/Disk/Formats/G64.cpp b/Storage/Disk/Formats/G64.cpp index d68d8c299..475537929 100644 --- a/Storage/Disk/Formats/G64.cpp +++ b/Storage/Disk/Formats/G64.cpp @@ -19,15 +19,15 @@ G64::G64(const char *file_name) _file = fopen(file_name, "rb"); if(!_file) - throw ErrorNotGCR; + throw ErrorCantOpen; // read and check the file signature char signature[8]; if(fread(signature, 1, 8, _file) != 8) - throw ErrorNotGCR; + throw ErrorNotG64; if(memcmp(signature, "GCR-1541", 8)) - throw ErrorNotGCR; + throw ErrorNotG64; // check the version number int version = fgetc(_file); @@ -40,9 +40,6 @@ G64::G64(const char *file_name) _number_of_tracks = (uint8_t)fgetc(_file); _maximum_track_size = (uint16_t)fgetc(_file); _maximum_track_size |= (uint16_t)fgetc(_file) << 8; - -// for(size_t c = 0; c < _number_of_tracks; c++) -// get_track_at_position(c); } G64::~G64() diff --git a/Storage/Disk/Formats/G64.hpp b/Storage/Disk/Formats/G64.hpp index 406453039..957c83c42 100644 --- a/Storage/Disk/Formats/G64.hpp +++ b/Storage/Disk/Formats/G64.hpp @@ -13,16 +13,28 @@ namespace Storage { +/*! + Provies a @c Disk containing a G64 disk image — a raw but perfectly-clocked GCR stream. +*/ class G64: public Disk { public: + /*! + Construct a @c G64 containing content from the file with name @c file_name. + + @throws ErrorCantOpen if this file can't be opened. + @throws ErrorNotG64 if the file doesn't appear to contain a .G64 format image. + @throws ErrorUnknownVersion if this file appears to be a .G64 but has an unrecognised version number. + */ G64(const char *file_name); ~G64(); enum { - ErrorNotGCR, + ErrorCantOpen, + ErrorNotG64, ErrorUnknownVersion }; + // implemented to satisfy @c Disk unsigned int get_head_position_count(); std::shared_ptr get_track_at_position(unsigned int position); diff --git a/Storage/Storage.hpp b/Storage/Storage.hpp index 91abe794e..5797fba34 100644 --- a/Storage/Storage.hpp +++ b/Storage/Storage.hpp @@ -13,9 +13,17 @@ namespace Storage { +/*! + Contains either an absolute time or a time interval, described as a quotient, in terms of a + clock rate to which the time is relative and its length in cycles based on that clock rate. +*/ struct Time { unsigned int length, clock_rate; + /*! + Reduces this @c Time to its simplest form — eliminates all common factors from @c length + and @c clock_rate. + */ inline void simplify() { unsigned int common_divisor = NumberTheory::greatest_common_divisor(length, clock_rate); @@ -23,6 +31,9 @@ struct Time { clock_rate /= common_divisor; } + /*! + Returns the floating point conversion of this @c Time. This will often be less precise. + */ inline float get_float() { return (float)length / (float)clock_rate; diff --git a/Storage/Tape/Formats/CommodoreTAP.hpp b/Storage/Tape/Formats/CommodoreTAP.hpp index d840516f7..b76055259 100644 --- a/Storage/Tape/Formats/CommodoreTAP.hpp +++ b/Storage/Tape/Formats/CommodoreTAP.hpp @@ -14,18 +14,27 @@ namespace Storage { +/*! + Provides a @c Tape containing a Commodore-format tape image, which is simply a timed list of zero crossings. +*/ class CommodoreTAP: public Tape { public: + /*! + Constructs a @c CommodoreTAP containing content from the file with name @c file_name. + + @throws ErrorNotCommodoreTAP if this file could not be opened and recognised as a valid Commodore-format TAP. + */ CommodoreTAP(const char *file_name); ~CommodoreTAP(); - Pulse get_next_pulse(); - void reset(); - enum { ErrorNotCommodoreTAP }; + // implemented to satisfy @c Tape + Pulse get_next_pulse(); + void reset(); + private: FILE *_file; bool _updated_layout; diff --git a/Storage/Tape/Formats/TapeUEF.hpp b/Storage/Tape/Formats/TapeUEF.hpp index bb7676e41..e80a8e7d2 100644 --- a/Storage/Tape/Formats/TapeUEF.hpp +++ b/Storage/Tape/Formats/TapeUEF.hpp @@ -15,18 +15,27 @@ namespace Storage { +/*! + Provides a @c Tape containing a UEF tape image. +*/ class UEF : public Tape { public: + /*! + Constructs a @c UEF containing content from the file with name @c file_name. + + @throws ErrorNotUEF if this file could not be opened and recognised as a valid UEF. + */ UEF(const char *file_name); ~UEF(); - Pulse get_next_pulse(); - void reset(); - enum { ErrorNotUEF }; + // implemented to satisfy @c Tape + Pulse get_next_pulse(); + void reset(); + private: gzFile _file; unsigned int _time_base; diff --git a/Storage/TimedEventLoop.hpp b/Storage/TimedEventLoop.hpp index 232ff8c6d..33beaa35c 100644 --- a/Storage/TimedEventLoop.hpp +++ b/Storage/TimedEventLoop.hpp @@ -16,18 +16,63 @@ namespace Storage { + /*! + Provides a mechanism for arbitrarily timed events to be processed according to a fixed-base + discrete clock signal, ensuring correct timing. + + Subclasses are responsible for calling @c set_next_event_time_interval to establish the time + until a next event; @c process_next_event will be called when that event occurs, with progression + determined via @c run_for_cycles. + + Due to the aggregation of total timing information between events — e.g. if an event loop has + a clock rate of 1000 ticks per second and a steady stream of events that occur 10,000 times a second, + bookkeeping is necessary to ensure that 10 events are triggered per tick — subclasses should call + @c reset_timer if there is a discontinuity in events. + + Subclasses may also call @c jump_to_next_event to cause the next event to be communicated instantly. + + Subclasses are therefore expected to call @c set_next_event_time_interval upon obtaining an event stream, + and again in response to each call to @c process_next_event while events are ongoing. They may use + @c reset_timer to initiate a distinctly-timed stream or @c jump_to_next_event to short-circuit the timing + loop and fast forward immediately to the next event. + */ class TimedEventLoop { public: + /*! + Constructs a timed event loop that will be clocked at @c input_clock_rate. + */ TimedEventLoop(unsigned int input_clock_rate); + + /*! + Advances the event loop by @c number_of_cycles cycles. + */ void run_for_cycles(int number_of_cycles); protected: - void reset_timer(); + /*! + Sets the time interval, as a proportion of a second, until the next event should be triggered. + */ void set_next_event_time_interval(Time interval); - void jump_to_next_event(); + /*! + Communicates that the next event is triggered. A subclass will idiomatically process that event + and make a fresh call to @c set_next_event_time_interval to keep the event loop running. + */ virtual void process_next_event() = 0; + /*! + Resets timing, throwing away any current internal state. So clears any fractional ticks + that the event loop is currently tracking. + */ + void reset_timer(); + + /*! + Causes an immediate call to @c process_next_event and a call to @c reset_timer with the + net effect of processing the current event immediately and fast forwarding exactly to the + start of the interval prior to the next event. + */ + void jump_to_next_event(); + private: unsigned int _input_clock_rate; Time _event_interval;