// // DiskII.cpp // Clock Signal // // Created by Thomas Harte on 20/04/2018. // Copyright 2018 Thomas Harte. All rights reserved. // #include "DiskII.hpp" #include #include using namespace Apple; namespace { const uint8_t input_command = 0x4; // i.e. Q6 const uint8_t input_mode = 0x8; // i.e. Q7 const uint8_t input_flux = 0x1; } DiskII::DiskII(const int clock_rate) : clock_rate_(clock_rate), inputs_(input_command), drives_{ // Bit of a hack here: drives are marginally slowed down compared to real drives // in order to accomodate NIB files, which usually carry more data than will // physically fit on a track once slip bits are reinserted. // // I don't like the coupling here. // TODO: resolve better, somehow. Storage::Disk::Drive{clock_rate, 295, 1}, Storage::Disk::Drive{clock_rate, 295, 1} } { drives_[0].set_clocking_hint_observer(this); drives_[1].set_clocking_hint_observer(this); drives_[active_drive_].set_event_delegate(this); } void DiskII::set_control(const Control control, const bool on) { int previous_stepper_mask = stepper_mask_; switch(control) { case Control::P0: stepper_mask_ = (stepper_mask_ & 0xe) | (on ? 0x1 : 0x0); break; case Control::P1: stepper_mask_ = (stepper_mask_ & 0xd) | (on ? 0x2 : 0x0); break; case Control::P2: stepper_mask_ = (stepper_mask_ & 0xb) | (on ? 0x4 : 0x0); break; case Control::P3: stepper_mask_ = (stepper_mask_ & 0x7) | (on ? 0x8 : 0x0); break; case Control::Motor: motor_is_enabled_ = on; drives_[active_drive_].set_motor_on(on); return; } // If the stepper magnet selections have changed, and any is on, see how // that moves the head. if(previous_stepper_mask ^ stepper_mask_ && stepper_mask_) { // Convert from a representation of bits set to the centre of pull. int direction = 0; if(stepper_mask_&1) direction += (((stepper_position_ - 0) + 4)&7) - 4; if(stepper_mask_&2) direction += (((stepper_position_ - 2) + 4)&7) - 4; if(stepper_mask_&4) direction += (((stepper_position_ - 4) + 4)&7) - 4; if(stepper_mask_&8) direction += (((stepper_position_ - 6) + 4)&7) - 4; // TODO: when adopting C++20, replace with std::popcount. int bits_set = stepper_mask_; bits_set = (bits_set & 0x5) + ((bits_set >> 1) & 0x5); bits_set = (bits_set & 0x3) + ((bits_set >> 2) & 0x3); direction /= bits_set; // Compare to the stepper position to decide whether that pulls in the current cog notch, // or grabs a later one. drives_[active_drive_].step(Storage::Disk::HeadPosition(-direction, 4)); stepper_position_ = (stepper_position_ - direction + 8) & 7; } } void DiskII::select_drive(const int drive) { if((drive&1) == active_drive_) return; drives_[active_drive_].set_event_delegate(this); drives_[active_drive_^1].set_event_delegate(nullptr); drives_[active_drive_].set_motor_on(false); active_drive_ = drive & 1; drives_[active_drive_].set_motor_on(motor_is_enabled_); } // The read pulse is controlled by a special IC that outputs a 1us pulse for every field reversal on the disk. void DiskII::run_for(const Cycles cycles) { if(preferred_clocking() == ClockingHint::Preference::None) return; auto integer_cycles = cycles.as_integral(); while(integer_cycles--) { const int address = (state_ & 0xf0) | inputs_ | ((shift_register_&0x80) >> 6); if(flux_duration_) { --flux_duration_; if(!flux_duration_) inputs_ |= input_flux; } state_ = state_machine_[size_t(address)]; switch(state_ & 0xf) { default: shift_register_ = 0; break; // clear case 0x8: case 0xc: break; // nop case 0x9: shift_register_ = uint8_t(shift_register_ << 1); break; // shift left, bringing in a zero case 0xd: shift_register_ = uint8_t((shift_register_ << 1) | 1); break; // shift left, bringing in a one case 0xa: case 0xe: // shift right, bringing in write protected status shift_register_ = (shift_register_ >> 1) | (is_write_protected() ? 0x80 : 0x00); // If the controller is in the sense write protect loop but the register will never change, // short circuit further work and return now. if(shift_register_ == (is_write_protected() ? 0xff : 0x00)) { if(!drive_is_sleeping_[0]) drives_[0].run_for(Cycles(integer_cycles)); if(!drive_is_sleeping_[1]) drives_[1].run_for(Cycles(integer_cycles)); decide_clocking_preference(); return; } break; case 0xb: case 0xf: shift_register_ = data_input_; break; // load data register from data bus } // Currently writing? if(inputs_&input_mode) { // state_ & 0x80 should be the current level sent to the disk; // therefore transitions in that bit should become flux transitions drives_[active_drive_].write_bit((state_ ^ address) & 0x80); } // TODO: surely there's a less heavyweight solution than inline updates? if(!drive_is_sleeping_[0]) drives_[0].run_for(Cycles(1)); if(!drive_is_sleeping_[1]) drives_[1].run_for(Cycles(1)); } // Per comp.sys.apple2.programmer there is a delay between the controller // motor switch being flipped and the drive motor actually switching off. // This models that, accepting overrun as a risk. if(motor_off_time_ >= 0) { motor_off_time_ -= cycles.as_integral(); if(motor_off_time_ < 0) { set_control(Control::Motor, false); } } decide_clocking_preference(); } void DiskII::decide_clocking_preference() { const ClockingHint::Preference prior_preference = clocking_preference_; // If in read mode, clocking is either: // // just-in-time, if drives are running or the shift register has any 1s in it and shifting may occur, or a flux event hasn't yet passed; or // none, given that drives are not running, the shift register has already emptied or stopped and there's no flux about to be received. if(!(inputs_ & ~input_flux)) { const bool is_stuck_at_nop = !flux_duration_ && state_machine_[(state_ & 0xf0) | inputs_ | ((shift_register_ & 0x80) >> 6)] == state_ && ((state_ & 0xf) == 0x8 || (state_ & 0xf) == 0xc); clocking_preference_ = (drive_is_sleeping_[0] && drive_is_sleeping_[1] && (!shift_register_ || is_stuck_at_nop) && (inputs_&input_flux)) ? ClockingHint::Preference::None : ClockingHint::Preference::JustInTime; } // If in writing mode, clocking is real time. if(inputs_ & input_mode) { clocking_preference_ = ClockingHint::Preference::RealTime; } // If in sense-write-protect mode, clocking is just-in-time if the shift register hasn't yet filled with the value that // corresponds to the current write protect status. Otherwise it is none. if((inputs_ & ~input_flux) == input_command) { clocking_preference_ = ((shift_register_ == (is_write_protected() ? 0xff : 0x00)) && ((state_ & 0xf) == 0xa || (state_ & 0xf) == 0xe)) ? ClockingHint::Preference::None : ClockingHint::Preference::JustInTime; } // Announce a change if there was one. if(prior_preference != clocking_preference_) update_clocking_observer(); } bool DiskII::is_write_protected() const { return (stepper_mask_ & 2) || drives_[active_drive_].get_is_read_only(); } void DiskII::set_state_machine(const std::vector &state_machine) { /* An unadulterated P6 ROM read returns values with an address formed as: state b0, state b2, state b3, pulse, Q7, Q6, shift, state b1 ... and has the top nibble of each value stored in the ROM reflected. Beneath Apple Pro-DOS uses a different order and several of the online copies are reformatted into that order. So the code below remaps into Beneath Apple Pro-DOS order if the supplied state machine isn't already in that order. */ if(state_machine[0] != 0x18) { for(size_t source_address = 0; source_address < 256; ++source_address) { // Remap into Beneath Apple Pro-DOS address form. const size_t destination_address = ((source_address&0x20) ? 0x80 : 0x00) | ((source_address&0x40) ? 0x40 : 0x00) | ((source_address&0x01) ? 0x20 : 0x00) | ((source_address&0x80) ? 0x10 : 0x00) | ((source_address&0x08) ? 0x08 : 0x00) | ((source_address&0x04) ? 0x04 : 0x00) | ((source_address&0x02) ? 0x02 : 0x00) | ((source_address&0x10) ? 0x01 : 0x00); // Store. const uint8_t source_value = state_machine[source_address]; state_machine_[destination_address] = ((source_value & 0x80) ? 0x10 : 0x0) | ((source_value & 0x40) ? 0x20 : 0x0) | ((source_value & 0x20) ? 0x40 : 0x0) | ((source_value & 0x10) ? 0x80 : 0x0) | (source_value & 0x0f); } } else { for(size_t source_address = 0; source_address < 256; ++source_address) { // Reshuffle ordering of bytes only, to retain indexing by the high nibble. const size_t destination_address = ((source_address&0x80) ? 0x80 : 0x00) | ((source_address&0x40) ? 0x40 : 0x00) | ((source_address&0x01) ? 0x20 : 0x00) | ((source_address&0x20) ? 0x10 : 0x00) | ((source_address&0x08) ? 0x08 : 0x00) | ((source_address&0x04) ? 0x04 : 0x00) | ((source_address&0x02) ? 0x02 : 0x00) | ((source_address&0x10) ? 0x01 : 0x00); state_machine_[destination_address] = state_machine[source_address]; } } } void DiskII::set_disk(const std::shared_ptr &disk, int drive) { drives_[drive].set_disk(disk); } void DiskII::process_event(const Storage::Disk::Drive::Event &event) { if(event.type == Storage::Disk::Track::Event::FluxTransition) { inputs_ &= ~input_flux; flux_duration_ = 2; // Upon detection of a flux transition, the flux flag should stay set for 1us. Emulate that as two cycles. decide_clocking_preference(); } } void DiskII::set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference) { drive_is_sleeping_[0] = drives_[0].preferred_clocking() == ClockingHint::Preference::None; drive_is_sleeping_[1] = drives_[1].preferred_clocking() == ClockingHint::Preference::None; decide_clocking_preference(); } ClockingHint::Preference DiskII::preferred_clocking() const { return clocking_preference_; } void DiskII::set_data_input(uint8_t input) { data_input_ = input; } int DiskII::read_address(int address) { switch(address & 0xf) { default: case 0x0: set_control(Control::P0, false); break; case 0x1: set_control(Control::P0, true); break; case 0x2: set_control(Control::P1, false); break; case 0x3: set_control(Control::P1, true); break; case 0x4: set_control(Control::P2, false); break; case 0x5: set_control(Control::P2, true); break; case 0x6: set_control(Control::P3, false); break; case 0x7: set_control(Control::P3, true); break; case 0x8: shift_register_ = 0; motor_off_time_ = clock_rate_; break; case 0x9: set_control(Control::Motor, true); motor_off_time_ = -1; break; case 0xa: select_drive(0); break; case 0xb: select_drive(1); break; case 0xc: inputs_ &= ~input_command; break; case 0xd: inputs_ |= input_command; break; case 0xe: if(inputs_ & input_mode) drives_[active_drive_].end_writing(); inputs_ &= ~input_mode; break; case 0xf: if(!(inputs_ & input_mode)) drives_[active_drive_].begin_writing(Storage::Time(1, int(clock_rate_)), false); inputs_ |= input_mode; break; } decide_clocking_preference(); return (address & 1) ? DidNotLoad : shift_register_; } void DiskII::set_activity_observer(Activity::Observer *observer) { drives_[0].set_activity_observer(observer, "Drive 1", true); drives_[1].set_activity_observer(observer, "Drive 2", true); } Storage::Disk::Drive &DiskII::get_drive(int index) { return drives_[index]; }