// // Drive.cpp // Clock Signal // // Created by Thomas Harte on 25/09/2016. // Copyright 2016 Thomas Harte. All rights reserved. // #include "Drive.hpp" #include "Track/UnformattedTrack.hpp" #include #include #include #include #include using namespace Storage::Disk; Drive::Drive(int input_clock_rate, int revolutions_per_minute, int number_of_heads, ReadyType rdy_type): Storage::TimedEventLoop(input_clock_rate), available_heads_(number_of_heads), ready_type_(rdy_type) { set_rotation_speed(revolutions_per_minute); const auto seed = static_cast(std::chrono::system_clock::now().time_since_epoch().count()); std::default_random_engine randomiser(seed); // Get at least 64 bits of random information; rounding is likey to give this a slight bias. random_source_ = 0; auto half_range = (randomiser.max() - randomiser.min()) / 2; for(int bit = 0; bit < 64; ++bit) { random_source_ <<= 1; random_source_ |= ((randomiser() - randomiser.min()) >= half_range) ? 1 : 0; } } Drive::Drive(int input_clock_rate, int number_of_heads, ReadyType rdy_type) : Drive(input_clock_rate, 300, number_of_heads, rdy_type) {} void Drive::set_rotation_speed(float revolutions_per_minute) { // Rationalise the supplied speed so that cycles_per_revolution_ is exact. cycles_per_revolution_ = int(0.5f + float(get_input_clock_rate()) * 60.0f / revolutions_per_minute); // From there derive the appropriate rotational multiplier and possibly update the // count of cycles since the index hole proportionally. const float new_rotational_multiplier = float(cycles_per_revolution_) / float(get_input_clock_rate()); cycles_since_index_hole_ *= new_rotational_multiplier / rotational_multiplier_; rotational_multiplier_ = new_rotational_multiplier; cycles_since_index_hole_ %= cycles_per_revolution_; } Drive::~Drive() { if(disk_) disk_->flush_tracks(); } void Drive::set_disk(const std::shared_ptr &disk) { if(ready_type_ == ReadyType::ShugartModifiedRDY || ready_type_ == ReadyType::IBMRDY) { is_ready_ = false; } if(disk_) disk_->flush_tracks(); disk_ = disk; has_disk_ = !!disk_; invalidate_track(); did_set_disk(); update_clocking_observer(); } bool Drive::has_disk() const { return has_disk_; } ClockingHint::Preference Drive::preferred_clocking() { return (!has_disk_ || (time_until_motor_transition == Cycles(0) && !disk_is_rotating_)) ? ClockingHint::Preference::None : ClockingHint::Preference::JustInTime; } bool Drive::get_is_track_zero() const { return head_position_ == HeadPosition(0); } void Drive::step(HeadPosition offset) { if(ready_type_ == ReadyType::IBMRDY) { is_ready_ = true; } HeadPosition old_head_position = head_position_; head_position_ += offset; if(head_position_ < HeadPosition(0)) { head_position_ = HeadPosition(0); if(observer_) observer_->announce_drive_event(drive_name_, Activity::Observer::DriveEvent::StepBelowZero); } else { if(observer_) observer_->announce_drive_event(drive_name_, Activity::Observer::DriveEvent::StepNormal); } // If the head moved, flush the old track. if(head_position_ != old_head_position) { track_ = nullptr; } // Allow a subclass to react, if desired. did_step(head_position_); } std::shared_ptr Drive::step_to(HeadPosition offset) { HeadPosition old_head_position = head_position_; head_position_ = std::max(offset, HeadPosition(0)); if(disk_ && head_position_ != old_head_position) { track_ = nullptr; setup_track(); } return track_; } void Drive::set_head(int head) { head = std::min(head, available_heads_ - 1); if(head != head_) { head_ = head; track_ = nullptr; } } int Drive::get_head_count() const { return available_heads_; } bool Drive::get_tachometer() const { // I have made a guess here that the tachometer is a symmetric square wave; // if that is correct then around 60 beats per rotation appears to be correct // to proceed beyond the speed checks I've so far uncovered. constexpr float ticks_per_rotation = 60.0f; // 56 was too low; 64 too high. return int(get_rotation() * 2.0f * ticks_per_rotation) & 1; } float Drive::get_rotation() const { return get_time_into_track(); } float Drive::get_time_into_track() const { // i.e. amount of time since the index hole was seen, as a proportion of a second, // converted to a proportion of a rotation. return float(cycles_since_index_hole_) / (float(get_input_clock_rate()) * rotational_multiplier_); } bool Drive::get_is_read_only() const { if(disk_) return disk_->get_is_read_only(); return true; } bool Drive::get_is_ready() const { return is_ready_; } void Drive::set_motor_on(bool motor_is_on) { // Do nothing if the input hasn't changed. if(motor_input_is_on_ == motor_is_on) return; motor_input_is_on_ = motor_is_on; // If this now means that the input and the actual state are in harmony, // cancel any planned change and stop. if(disk_is_rotating_ == motor_is_on) { time_until_motor_transition = Cycles(0); return; } // If this is a transition to on, start immediately. // TODO: spin-up? // TODO: momentum. if(motor_is_on) { set_disk_is_rotating(true); time_until_motor_transition = Cycles(0); return; } // This is a transition from on to off. Simulate momentum (ha!) // by delaying the time until complete standstill. if(time_until_motor_transition == Cycles(0)) time_until_motor_transition = get_input_clock_rate(); } bool Drive::get_motor_on() const { return motor_input_is_on_; } bool Drive::get_index_pulse() const { return index_pulse_remaining_ > Cycles(0); } void Drive::set_event_delegate(Storage::Disk::Drive::EventDelegate *delegate) { event_delegate_ = delegate; } void Drive::advance(const Cycles cycles) { cycles_since_index_hole_ += cycles.as_integral(); if(event_delegate_) event_delegate_->advance(cycles); } void Drive::run_for(const Cycles cycles) { // Assumed: the index pulse pulses even if the drive has stopped spinning. index_pulse_remaining_ = std::max(index_pulse_remaining_ - cycles, Cycles(0)); if(time_until_motor_transition > Cycles(0)) { if(time_until_motor_transition > cycles) { time_until_motor_transition -= cycles; } else { time_until_motor_transition = Cycles(0); set_disk_is_rotating(!disk_is_rotating_); } } if(disk_is_rotating_) { if(has_disk_) { Time zero(0); auto number_of_cycles = cycles.as_integral(); while(number_of_cycles) { auto cycles_until_next_event = get_cycles_until_next_event(); auto cycles_to_run_for = std::min(cycles_until_next_event, number_of_cycles); if(!is_reading_ && cycles_until_bits_written_ > zero) { auto write_cycles_target = cycles_until_bits_written_.get(); if(cycles_until_bits_written_.length % cycles_until_bits_written_.clock_rate) ++write_cycles_target; cycles_to_run_for = std::min(cycles_to_run_for, write_cycles_target); } number_of_cycles -= cycles_to_run_for; if(!is_reading_) { if(cycles_until_bits_written_ > zero) { Storage::Time cycles_to_run_for_time(static_cast(cycles_to_run_for)); if(cycles_until_bits_written_ <= cycles_to_run_for_time) { if(event_delegate_) event_delegate_->process_write_completed(); if(cycles_until_bits_written_ <= cycles_to_run_for_time) cycles_until_bits_written_.set_zero(); else cycles_until_bits_written_ -= cycles_to_run_for_time; } else { cycles_until_bits_written_ -= cycles_to_run_for_time; } } } TimedEventLoop::run_for(Cycles(cycles_to_run_for)); } } else { TimedEventLoop::run_for(cycles); } } } // MARK: - Track timed event loop void Drive::get_next_event(float duration_already_passed) { if(!disk_) { current_event_.type = Track::Event::IndexHole; current_event_.length = 1.0f; set_next_event_time_interval((current_event_.length - duration_already_passed) * rotational_multiplier_); return; } // Grab a new track if not already in possession of one. This will recursively call get_next_event, // supplying a proper duration_already_passed. if(!track_) { random_interval_ = 0.0f; setup_track(); return; } // If gain has now been turned up so as to generate noise, generate some noise. if(random_interval_ > 0.0f) { current_event_.type = Track::Event::FluxTransition; current_event_.length = float(2 + (random_source_&1)) / 1000000.0f; random_source_ = (random_source_ >> 1) | (random_source_ << 63); if(random_interval_ < current_event_.length) { current_event_.length = random_interval_; random_interval_ = 0.0f; } else { random_interval_ -= current_event_.length; } set_next_event_time_interval(current_event_.length); return; } if(track_) { const auto track_event = track_->get_next_event(); current_event_.type = track_event.type; current_event_.length = track_event.length.get(); } else { current_event_.length = 1.0f; current_event_.type = Track::Event::IndexHole; } // Begin a 2ms period of holding the index line pulse active if this is an index pulse event. if(current_event_.type == Track::Event::IndexHole) { index_pulse_remaining_ = Cycles((get_input_clock_rate() * 2) / 1000); } // divide interval, which is in terms of a single rotation of the disk, by rotation speed to // convert it into revolutions per second; this is achieved by multiplying by rotational_multiplier_ float interval = std::max((current_event_.length - duration_already_passed) * rotational_multiplier_, 0.0f); // An interval greater than 15ms => adjust gain up the point where noise starts happening. // Seed that up and leave a 15ms gap until it starts. constexpr float safe_gain_period = 15.0f / 1000000.0f; if(interval >= safe_gain_period) { random_interval_ = interval - safe_gain_period; interval = safe_gain_period; } set_next_event_time_interval(interval); } void Drive::process_next_event() { if(current_event_.type == Track::Event::IndexHole) { ++ready_index_count_; if(ready_index_count_ == 2 && (ready_type_ == ReadyType::ShugartRDY || ready_type_ == ReadyType::ShugartModifiedRDY)) { is_ready_ = true; } cycles_since_index_hole_ = 0; } if( event_delegate_ && (current_event_.type == Track::Event::IndexHole || is_reading_) ){ event_delegate_->process_event(current_event_); } get_next_event(0.0f); } // MARK: - Track management std::shared_ptr Drive::get_track() { if(disk_) return disk_->get_track_at_position(Track::Address(head_, head_position_)); return nullptr; } void Drive::set_track(const std::shared_ptr &track) { if(disk_) disk_->set_track_at_position(Track::Address(head_, head_position_), track); } void Drive::setup_track() { track_ = get_track(); if(!track_) { track_ = std::make_shared(); } float offset = 0.0f; const auto track_time_now = get_time_into_track(); const auto time_found = track_->seek_to(Time(track_time_now)).get(); // `time_found` can be greater than `track_time_now` if limited precision caused rounding. if(time_found <= track_time_now) { offset = track_time_now - time_found; } // Reseed cycles_since_index_hole_; 99.99% of the time it'll still be correct as is, // but if the track has rounded one way or the other it may now be very slightly adrift. cycles_since_index_hole_ = (int((time_found + offset) * cycles_per_revolution_)) % cycles_per_revolution_; get_next_event(offset); } void Drive::invalidate_track() { random_interval_ = 0.0f; track_ = nullptr; if(patched_track_) { set_track(patched_track_); patched_track_ = nullptr; } } // MARK: - Writing void Drive::begin_writing(Time bit_length, bool clamp_to_index_hole) { // Do nothing if already writing. // TODO: cope properly if there's no disk to write to. if(!is_reading_ || !disk_) return; // Get a copy of the track if that hasn't happened yet. if(!track_) { setup_track(); } // Store the relevant parameters, and kick off writing. is_reading_ = false; clamp_writing_to_index_hole_ = clamp_to_index_hole; cycles_per_bit_ = Storage::Time(int(get_input_clock_rate())) * bit_length; cycles_per_bit_.simplify(); write_segment_.length_of_a_bit = bit_length / Time(rotational_multiplier_); write_segment_.data.clear(); write_start_time_ = Time(get_time_into_track()); } void Drive::write_bit(bool value) { write_segment_.data.push_back(value); cycles_until_bits_written_ += cycles_per_bit_; } void Drive::end_writing() { // If the user modifies a track, it's scaled up to a "high" resolution and modifications // are plotted on top of that. // // "High" is defined as: two samples per clock relative to an idiomatic // 8Mhz disk controller and 300RPM disk speed. const size_t high_resolution_track_rate = 3200000; if(!is_reading_) { is_reading_ = true; if(!patched_track_) { // Avoid creating a new patched track if this one is already patched patched_track_ = std::dynamic_pointer_cast(track_); if(!patched_track_ || !patched_track_->is_resampled_clone()) { Track *const tr = track_.get(); patched_track_.reset(PCMTrack::resampled_clone(tr, high_resolution_track_rate)); } } patched_track_->add_segment(write_start_time_, write_segment_, clamp_writing_to_index_hole_); cycles_since_index_hole_ %= cycles_per_revolution_; invalidate_track(); } } bool Drive::is_writing() const { return !is_reading_; } void Drive::set_disk_is_rotating(bool is_rotating) { disk_is_rotating_ = is_rotating; if(observer_) { observer_->set_drive_motor_status(drive_name_, disk_is_rotating_); if(announce_motor_led_) { observer_->set_led_status(drive_name_, disk_is_rotating_); } } if(!is_rotating) { if(ready_type_ == ReadyType::ShugartRDY) { is_ready_ = false; } ready_index_count_ = 0; if(disk_) disk_->flush_tracks(); } update_clocking_observer(); } void Drive::set_activity_observer(Activity::Observer *observer, const std::string &name, bool add_motor_led) { observer_ = observer; announce_motor_led_ = add_motor_led; if(observer) { drive_name_ = name; observer->register_drive(drive_name_); observer->set_drive_motor_status(drive_name_, disk_is_rotating_); if(add_motor_led) { observer->register_led(drive_name_); observer->set_led_status(drive_name_, disk_is_rotating_); } } }