// // IWM.cpp // Clock Signal // // Created by Thomas Harte on 05/05/2019. // Copyright © 2019 Thomas Harte. All rights reserved. // #include "IWM.hpp" #include "../../Outputs/Log.hpp" using namespace Apple; namespace { constexpr int CA0 = 1 << 0; constexpr int CA1 = 1 << 1; constexpr int CA2 = 1 << 2; constexpr int LSTRB = 1 << 3; constexpr int ENABLE = 1 << 4; constexpr int DRIVESEL = 1 << 5; /* This means drive select, like on the original Disk II. */ constexpr int Q6 = 1 << 6; constexpr int Q7 = 1 << 7; constexpr int SEL = 1 << 8; /* This is an additional input, not available on a Disk II, with a confusingly-similar name to SELECT but a distinct purpose. */ } IWM::IWM(int clock_rate) : clock_rate_(clock_rate) {} // MARK: - Bus accessors uint8_t IWM::read(int address) { access(address); // Per Inside Macintosh: // // "Before you can read from any of the disk registers you must set up the state of the IWM so that it // can pass the data through to the MC68000's address space where you'll be able to read it. To do that, // you must first turn off Q7 by reading or writing dBase+q7L. Then turn on Q6 by accessing dBase+q6H. // After that, the IWM will be able to pass data from the disk's RD/SENSE line through to you." // // My understanding: // // Q6 = 1, Q7 = 0 reads the status register. The meaning of the top 'SENSE' bit is then determined by // the CA0,1,2 and SEL switches as described in Inside Macintosh, summarised above as RD/SENSE. if(address&1) { return 0xff; } switch(state_ & (Q6 | Q7 | ENABLE)) { default: LOG("[IWM] Invalid read\n"); return 0xff; // "Read all 1s". // printf("Reading all 1s\n"); // return 0xff; case 0: case ENABLE: { /* Read data register. Zeroing afterwards is a guess. */ const auto result = data_register_; if(data_register_ & 0x80) { // printf("\n\nIWM:%02x\n\n", data_register_); // printf("."); data_register_ = 0; } // LOG("Reading data register: " << PADHEX(2) << int(result)); return result; } case Q6: case Q6|ENABLE: { /* [If A = 0], Read status register: bits 0-4: same as mode register. bit 5: 1 = either /ENBL1 or /ENBL2 is currently low. bit 6: 1 = MZ (reserved for future compatibility; should always be read as 0). bit 7: 1 = SENSE input high; 0 = SENSE input low. (/ENBL1 is low when the first drive's motor is on; /ENBL2 is low when the second drive's motor is on. If the 1-second timer is enabled, motors remain on for one second after being programmatically disabled.) */ return uint8_t( (mode_&0x1f) | ((state_ & ENABLE) ? 0x20 : 0x00) | (sense() & 0x80) ); } break; case Q7: case Q7|ENABLE: /* Read write-handshake register: bits 0-5: reserved for future use (currently read as 1). bit 6: 1 = write state (0 = underrun has occurred; 1 = no underrun so far). bit 7: 1 = write data buffer ready for data (1 = ready; 0 = busy). */ // LOG("Reading write handshake: " << PADHEX(2) << (0x3f | write_handshake_)); return 0x3f | write_handshake_; } return 0xff; } void IWM::write(int address, uint8_t input) { access(address); switch(state_ & (Q6 | Q7 | ENABLE)) { default: break; case Q7|Q6: /* Write mode register: bit 0: 1 = latch mode (should be set in asynchronous mode). bit 1: 0 = synchronous handshake protocol; 1 = asynchronous. bit 2: 0 = 1-second on-board timer enable; 1 = timer disable. bit 3: 0 = slow mode; 1 = fast mode. bit 4: 0 = 7Mhz; 1 = 8Mhz (7 or 8 mHz clock descriptor). bit 5: 1 = test mode; 0 = normal operation. bit 6: 1 = MZ-reset. bit 7: reserved for future expansion. */ mode_ = input; switch(mode_ & 0x18) { case 0x00: bit_length_ = Cycles(24); break; // slow mode, 7Mhz case 0x08: bit_length_ = Cycles(12); break; // fast mode, 7Mhz case 0x10: bit_length_ = Cycles(32); break; // slow mode, 8Mhz case 0x18: bit_length_ = Cycles(16); break; // fast mode, 8Mhz } LOG("IWM mode is now " << PADHEX(2) << int(mode_)); break; case Q7|Q6|ENABLE: // Write data register. next_output_ = input; write_handshake_ &= ~0x80; break; } } // MARK: - Switch access void IWM::access(int address) { // Keep a record of switch state; bits in state_ // should correlate with the anonymous namespace constants // defined at the top of this file — CA0, CA1, etc. address &= 0xf; const auto mask = 1 << (address >> 1); const auto old_state = state_; if(address & 1) { state_ |= mask; } else { state_ &= ~mask; } // React appropriately to ENABLE and DRIVESEL changes, and changes into/out of write mode. if(old_state != state_) { push_drive_state(); switch(mask) { default: break; case ENABLE: if(address & 1) { if(drives_[active_drive_]) drives_[active_drive_]->set_enabled(true); } else { // If the 1-second delay is enabled, set up a timer for that. if(!(mode_ & 4)) { cycles_until_disable_ = Cycles(clock_rate_); } else { if(drives_[active_drive_]) drives_[active_drive_]->set_enabled(false); } } break; case DRIVESEL: { const int new_drive = address & 1; if(new_drive != active_drive_) { if(drives_[active_drive_]) drives_[active_drive_]->set_enabled(false); active_drive_ = new_drive; if(drives_[active_drive_]) { drives_[active_drive_]->set_enabled(state_ & ENABLE || (cycles_until_disable_ > Cycles(0))); push_drive_state(); } } } break; case Q6: case Q7: select_shift_mode(); break; } } } void IWM::set_select(bool enabled) { // Store SEL as an extra state bit. if(enabled) state_ |= SEL; else state_ &= ~SEL; push_drive_state(); } void IWM::push_drive_state() { if(drives_[active_drive_]) { const uint8_t drive_control_lines = ((state_ & CA0) ? IWMDrive::CA0 : 0) | ((state_ & CA1) ? IWMDrive::CA1 : 0) | ((state_ & CA2) ? IWMDrive::CA2 : 0) | ((state_ & SEL) ? IWMDrive::SEL : 0) | ((state_ & LSTRB) ? IWMDrive::LSTRB : 0); drives_[active_drive_]->set_control_lines(drive_control_lines); } } // MARK: - Active logic void IWM::run_for(const Cycles cycles) { // Check for a timeout of the motor-off timer. if(cycles_until_disable_ > Cycles(0)) { cycles_until_disable_ -= cycles; if(cycles_until_disable_ <= Cycles(0)) { cycles_until_disable_ = Cycles(0); if(drives_[active_drive_]) drives_[active_drive_]->set_enabled(false); } } // Activity otherwise depends on mode and motor state. auto integer_cycles = cycles.as_integral(); switch(shift_mode_) { case ShiftMode::Reading: { // Per the IWM patent, column 7, around line 35 onwards: "The expected time // is widened by approximately one-half an interval before and after the // expected time since the data is not precisely spaced when read due to // variations in drive speed and other external factors". The error_margin // here implements the 'after' part of that contract. const auto error_margin = Cycles(bit_length_.as_integral() >> 1); if(drive_is_rotating_[active_drive_]) { while(integer_cycles--) { drives_[active_drive_]->run_for(Cycles(1)); ++cycles_since_shift_; if(cycles_since_shift_ == bit_length_ + error_margin) { propose_shift(0); } } } else { while(cycles_since_shift_ + integer_cycles >= bit_length_ + error_margin) { const auto run_length = bit_length_ + error_margin - cycles_since_shift_; integer_cycles -= run_length.as_integral(); cycles_since_shift_ += run_length; propose_shift(0); } cycles_since_shift_ += Cycles(integer_cycles); } } break; case ShiftMode::Writing: if(drives_[active_drive_]->is_writing()) { while(cycles_since_shift_ + integer_cycles >= bit_length_) { const auto cycles_until_write = bit_length_ - cycles_since_shift_; drives_[active_drive_]->run_for(cycles_until_write); // Output a flux transition if the top bit is set. drives_[active_drive_]->write_bit(shift_register_ & 0x80); shift_register_ <<= 1; integer_cycles -= cycles_until_write.as_integral(); cycles_since_shift_ = Cycles(0); --output_bits_remaining_; if(!output_bits_remaining_) { if(!(write_handshake_ & 0x80)) { write_handshake_ |= 0x80; shift_register_ = next_output_; output_bits_remaining_ = 8; // LOG("Next byte: " << PADHEX(2) << int(shift_register_)); } else { write_handshake_ &= ~0x40; drives_[active_drive_]->end_writing(); // printf("\n"); LOG("Overrun; done."); select_shift_mode(); } } } cycles_since_shift_ = integer_cycles; if(integer_cycles) { drives_[active_drive_]->run_for(cycles_since_shift_); } } else { drives_[active_drive_]->run_for(cycles); } break; case ShiftMode::CheckingWriteProtect: if(integer_cycles < 8) { shift_register_ = (shift_register_ >> integer_cycles) | (sense() & (0xff << (8 - integer_cycles))); } else { shift_register_ = sense(); } [[fallthrough]]; default: if(drive_is_rotating_[active_drive_]) drives_[active_drive_]->run_for(cycles); break; } } void IWM::select_shift_mode() { // Don't allow an ongoing write to be interrupted. if(shift_mode_ == ShiftMode::Writing && drives_[active_drive_] && drives_[active_drive_]->is_writing()) return; const auto old_shift_mode = shift_mode_; switch(state_ & (Q6|Q7)) { default: shift_mode_ = ShiftMode::CheckingWriteProtect; break; case 0: shift_mode_ = ShiftMode::Reading; break; case Q7: // "The IWM is put into the write state by a transition from the write protect sense state to the // write load state". if(shift_mode_ == ShiftMode::CheckingWriteProtect) shift_mode_ = ShiftMode::Writing; break; } // If writing mode just began, set the drive into write mode and cue up the first output byte. if(drives_[active_drive_] && old_shift_mode != ShiftMode::Writing && shift_mode_ == ShiftMode::Writing) { drives_[active_drive_]->begin_writing(Storage::Time(1, clock_rate_ / bit_length_.as_integral()), false); shift_register_ = next_output_; write_handshake_ |= 0x80 | 0x40; output_bits_remaining_ = 8; LOG("Seeding output with " << PADHEX(2) << shift_register_); } } uint8_t IWM::sense() { return drives_[active_drive_] ? (drives_[active_drive_]->read() ? 0xff : 0x00) : 0xff; } void IWM::process_event(const Storage::Disk::Drive::Event &event) { if(shift_mode_ != ShiftMode::Reading) return; switch(event.type) { case Storage::Disk::Track::Event::IndexHole: return; case Storage::Disk::Track::Event::FluxTransition: propose_shift(1); break; } } void IWM::propose_shift(uint8_t bit) { // TODO: synchronous mode. // LOG("Shifting input"); // See above for text from the IWM patent, column 7, around line 35 onwards. // The error_margin here implements the 'before' part of that contract. // // Basic effective logic: if at least 1 is fozund in the bit_length_ cycles centred // on the current expected bit delivery time as implied by cycles_since_shift_, // shift in a 1 and start a new window wherever the first found 1 was. // // If no 1s are found, shift in a 0 and don't alter expectations as to window placement. const auto error_margin = Cycles(bit_length_.as_integral() >> 1); if(bit && cycles_since_shift_ < error_margin) return; shift_register_ = uint8_t((shift_register_ << 1) | bit); if(shift_register_ & 0x80) { data_register_ = shift_register_; shift_register_ = 0; } if(bit) cycles_since_shift_ = Cycles(0); else cycles_since_shift_ -= bit_length_; } void IWM::set_drive(int slot, IWMDrive *drive) { drives_[slot] = drive; drive->set_event_delegate(this); drive->set_clocking_hint_observer(this); } void IWM::set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) { const bool is_rotating = clocking != ClockingHint::Preference::None; if(component == static_cast(drives_[0])) { drive_is_rotating_[0] = is_rotating; } else { drive_is_rotating_[1] = is_rotating; } } void IWM::set_activity_observer(Activity::Observer *observer) { if(drives_[0]) drives_[0]->set_activity_observer(observer, "Internal Floppy", true); if(drives_[1]) drives_[1]->set_activity_observer(observer, "External Floppy", true); }