// // PCCompatible.cpp // Clock Signal // // Created by Thomas Harte on 15/11/2023. // Copyright © 2023 Thomas Harte. All rights reserved. // #include "PCCompatible.hpp" #include "CGA.hpp" #include "DMA.hpp" #include "KeyboardMapper.hpp" #include "MDA.hpp" #include "Memory.hpp" #include "PIC.hpp" #include "PIT.hpp" #include "RTC.hpp" #include "../../InstructionSets/x86/Decoder.hpp" #include "../../InstructionSets/x86/Flags.hpp" #include "../../InstructionSets/x86/Instruction.hpp" #include "../../InstructionSets/x86/Perform.hpp" #include "../../Components/8255/i8255.hpp" #include "../../Components/8272/CommandDecoder.hpp" #include "../../Components/8272/Results.hpp" #include "../../Components/8272/Status.hpp" #include "../../Components/AudioToggle/AudioToggle.hpp" #include "../../Numeric/RegisterSizes.hpp" #include "../../Outputs/CRT/CRT.hpp" #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" #include "../../Storage/Disk/Track/TrackSerialiser.hpp" #include "../../Storage/Disk/Encodings/MFM/Constants.hpp" #include "../../Storage/Disk/Encodings/MFM/SegmentParser.hpp" #include "../AudioProducer.hpp" #include "../KeyboardMachine.hpp" #include "../MediaTarget.hpp" #include "../ScanProducer.hpp" #include "../TimedMachine.hpp" #include "../../Analyser/Static/PCCompatible/Target.hpp" #include #include namespace PCCompatible { using VideoAdaptor = Analyser::Static::PCCompatible::Target::VideoAdaptor; template struct Adaptor; template <> struct Adaptor { using type = MDA; }; template <> struct Adaptor { using type = CGA; }; class FloppyController { public: FloppyController(PIC &pic, DMA &dma, int drive_count) : pic_(pic), dma_(dma) { // Default: one floppy drive only. for(int c = 0; c < 4; c++) { drives_[c].exists = drive_count > c; } } void set_digital_output(uint8_t control) { // b7, b6, b5, b4: enable motor for drive 4, 3, 2, 1; // b3: 1 => enable DMA; 0 => disable; // b2: 1 => enable FDC; 0 => hold at reset; // b1, b0: drive select (usurps FDC?) drives_[0].motor = control & 0x10; drives_[1].motor = control & 0x20; drives_[2].motor = control & 0x40; drives_[3].motor = control & 0x80; if(observer_) { for(int c = 0; c < 4; c++) { if(drives_[c].exists) observer_->set_led_status(drive_name(c), drives_[c].motor); } } enable_dma_ = control & 0x08; const bool hold_reset = !(control & 0x04); if(!hold_reset && hold_reset_) { // TODO: add a delay mechanism. reset(); } hold_reset_ = hold_reset; if(hold_reset_) { pic_.apply_edge<6>(false); } } uint8_t status() const { return status_.main(); } void write(uint8_t value) { decoder_.push_back(value); if(decoder_.has_command()) { using Command = Intel::i8272::Command; switch(decoder_.command()) { default: printf("TODO: implement FDC command %d\n", uint8_t(decoder_.command())); break; case Command::ReadData: { printf("FDC: Read from drive %d / head %d / track %d of head %d / track %d / sector %d\n", decoder_.target().drive, decoder_.target().head, drives_[decoder_.target().drive].track, decoder_.geometry().head, decoder_.geometry().cylinder, decoder_.geometry().sector); status_.begin(decoder_); // Search for a matching sector. auto target = decoder_.geometry(); bool complete = false; while(!complete) { bool found_sector = false; for(auto &pair: drives_[decoder_.target().drive].sectors(decoder_.target().head)) { if( (pair.second.address.track == target.cylinder) && (pair.second.address.sector == target.sector) && (pair.second.address.side == target.head) && (pair.second.size == target.size) ) { found_sector = true; bool wrote_in_full = true; for(int c = 0; c < 128 << target.size; c++) { const auto access_result = dma_.write(2, pair.second.samples[0].data()[c]); switch(access_result) { default: break; case AccessResult::NotAccepted: complete = true; wrote_in_full = false; break; case AccessResult::AcceptedWithEOP: complete = true; break; } if(access_result != AccessResult::Accepted) { break; } } if(!wrote_in_full) { status_.set(Intel::i8272::Status1::OverRun); status_.set(Intel::i8272::Status0::AbnormalTermination); break; } ++target.sector; // TODO: multitrack? break; } } if(!found_sector) { status_.set(Intel::i8272::Status1::EndOfCylinder); status_.set(Intel::i8272::Status0::AbnormalTermination); break; } } results_.serialise( status_, decoder_.geometry().cylinder, decoder_.geometry().head, decoder_.geometry().sector, decoder_.geometry().size); // TODO: what if head has changed? drives_[decoder_.target().drive].status = decoder_.drive_head(); drives_[decoder_.target().drive].raised_interrupt = true; pic_.apply_edge<6>(true); } break; case Command::Recalibrate: drives_[decoder_.target().drive].track = 0; drives_[decoder_.target().drive].raised_interrupt = true; drives_[decoder_.target().drive].status = decoder_.target().drive | uint8_t(Intel::i8272::Status0::SeekEnded); pic_.apply_edge<6>(true); break; case Command::Seek: drives_[decoder_.target().drive].track = decoder_.seek_target(); drives_[decoder_.target().drive].raised_interrupt = true; drives_[decoder_.target().drive].status = decoder_.drive_head() | uint8_t(Intel::i8272::Status0::SeekEnded); pic_.apply_edge<6>(true); break; case Command::SenseInterruptStatus: { int c = 0; for(; c < 4; c++) { if(drives_[c].raised_interrupt) { drives_[c].raised_interrupt = false; status_.set_status0(drives_[c].status); results_.serialise(status_, drives_[0].track); } } bool any_remaining_interrupts = false; for(; c < 4; c++) { any_remaining_interrupts |= drives_[c].raised_interrupt; } if(!any_remaining_interrupts) { pic_.apply_edge<6>(false); } } break; case Command::Specify: specify_specs_ = decoder_.specify_specs(); break; // case Command::SenseDriveStatus: { // } break; case Command::Invalid: results_.serialise_none(); break; } decoder_.clear(); // If there are any results to provide, set data direction and data ready. if(!results_.empty()) { using MainStatus = Intel::i8272::MainStatus; status_.set(MainStatus::DataIsToProcessor, true); status_.set(MainStatus::DataReady, true); status_.set(MainStatus::CommandInProgress, true); } } } uint8_t read() { using MainStatus = Intel::i8272::MainStatus; if(status_.get(MainStatus::DataIsToProcessor)) { const uint8_t result = results_.next(); if(results_.empty()) { status_.set(MainStatus::DataIsToProcessor, false); status_.set(MainStatus::CommandInProgress, false); } return result; } return 0x80; } void set_activity_observer(Activity::Observer *observer) { observer_ = observer; for(int c = 0; c < 4; c++) { if(drives_[c].exists) { observer_->register_led(drive_name(c), 0); } } } void set_disk(std::shared_ptr disk, int drive) { // if(drives_[drive].has_disk()) { // // TODO: drive should only transition to unready if it was ready in the first place. // drives_[drive].status = uint8_t(Intel::i8272::Status0::BecameNotReady); // drives_[drive].raised_interrupt = true; // pic_.apply_edge<6>(true); // } drives_[drive].set_disk(disk); } private: void reset() { printf("FDC reset\n"); decoder_.clear(); status_.reset(); // Necessary to pass GlaBIOS' POST test, but: why? // // Cf. INT_13_0_2 and the CMP AL, 11000000B following a CALL FDC_WAIT_SENSE. for(int c = 0; c < 4; c++) { drives_[c].raised_interrupt = true; drives_[c].status = uint8_t(Intel::i8272::Status0::BecameNotReady); } pic_.apply_edge<6>(true); using MainStatus = Intel::i8272::MainStatus; status_.set(MainStatus::DataReady, true); status_.set(MainStatus::DataIsToProcessor, false); } PIC &pic_; DMA &dma_; bool hold_reset_ = false; bool enable_dma_ = false; Intel::i8272::CommandDecoder decoder_; Intel::i8272::Status status_; Intel::i8272::Results results_; Intel::i8272::CommandDecoder::SpecifySpecs specify_specs_; struct DriveStatus { public: bool raised_interrupt = false; uint8_t status = 0; uint8_t track = 0; bool motor = false; bool exists = true; bool has_disk() const { return bool(disk); } void set_disk(std::shared_ptr image) { disk = image; cached.clear(); } Storage::Encodings::MFM::SectorMap §ors(bool side) { if(cached.track == track && cached.side == side) { return cached.sectors; } cached.track = track; cached.side = side; cached.sectors.clear(); if(!disk) { return cached.sectors; } auto raw_track = disk->get_track_at_position( Storage::Disk::Track::Address( side, Storage::Disk::HeadPosition(track) ) ); if(!raw_track) { return cached.sectors; } const bool is_double_density = true; // TODO: use MFM flag here. auto serialisation = Storage::Disk::track_serialisation( *raw_track, is_double_density ? Storage::Encodings::MFM::MFMBitLength : Storage::Encodings::MFM::FMBitLength ); cached.sectors = Storage::Encodings::MFM::sectors_from_segment(std::move(serialisation), is_double_density); return cached.sectors; } private: struct { uint8_t track = 0xff; bool side; Storage::Encodings::MFM::SectorMap sectors; void clear() { track = 0xff; sectors.clear(); } } cached; std::shared_ptr disk; } drives_[4]; static std::string drive_name(int c) { char name[3] = "A"; name[0] += c; return std::string("Drive ") + name; } Activity::Observer *observer_ = nullptr; }; class KeyboardController { public: KeyboardController(PIC &pic) : pic_(pic) {} // KB Status Port 61h high bits: //; 01 - normal operation. wait for keypress, when one comes in, //; force data line low (forcing keyboard to buffer additional //; keypresses) and raise IRQ1 high //; 11 - stop forcing data line low. lower IRQ1 and don't raise it again. //; drop all incoming keypresses on the floor. //; 10 - lower IRQ1 and force clock line low, resetting keyboard //; 00 - force clock line low, resetting keyboard, but on a 01->00 transition, //; IRQ1 would remain high void set_mode(uint8_t mode) { mode_ = Mode(mode); switch(mode_) { case Mode::NormalOperation: break; case Mode::NoIRQsIgnoreInput: pic_.apply_edge<1>(false); break; case Mode::ClearIRQReset: pic_.apply_edge<1>(false); [[fallthrough]]; case Mode::Reset: reset_delay_ = 5; // Arbitrarily. break; } } void run_for(Cycles cycles) { if(reset_delay_ <= 0) { return; } reset_delay_ -= cycles.as(); if(reset_delay_ <= 0) { input_.clear(); post(0xaa); } } uint8_t read() { pic_.apply_edge<1>(false); if(input_.empty()) { return 0; } const uint8_t key = input_.front(); input_.erase(input_.begin()); if(!input_.empty()) { pic_.apply_edge<1>(true); } return key; } void post(uint8_t value) { if(mode_ == Mode::NoIRQsIgnoreInput) { return; } input_.push_back(value); pic_.apply_edge<1>(true); } private: enum class Mode { NormalOperation = 0b01, NoIRQsIgnoreInput = 0b11, ClearIRQReset = 0b10, Reset = 0b00, } mode_; std::vector input_; PIC &pic_; int reset_delay_ = 0; }; struct PCSpeaker { PCSpeaker() : toggle(queue), speaker(toggle) {} void update() { speaker.run_for(queue, cycles_since_update); cycles_since_update = 0; } void set_pit(bool pit_input) { pit_input_ = pit_input; set_level(); } void set_control(bool pit_mask, bool level) { pit_mask_ = pit_mask; level_ = level; set_level(); } void set_level() { // TODO: I think pit_mask_ actually acts as the gate input to the PIT. const bool new_output = (!pit_mask_ | pit_input_) & level_; if(new_output != output_) { update(); toggle.set_output(new_output); output_ = new_output; } } Concurrency::AsyncTaskQueue queue; Audio::Toggle toggle; Outputs::Speaker::PullLowpass speaker; Cycles cycles_since_update = 0; bool pit_input_ = false; bool pit_mask_ = false; bool level_ = false; bool output_ = false; }; class PITObserver { public: PITObserver(PIC &pic, PCSpeaker &speaker) : pic_(pic), speaker_(speaker) {} template void update_output(bool new_level) { switch(channel) { default: break; case 0: pic_.apply_edge<0>(new_level); break; case 2: speaker_.set_pit(new_level); break; } } private: PIC &pic_; PCSpeaker &speaker_; // TODO: // // channel 0 is connected to IRQ 0; // channel 1 is used for DRAM refresh (presumably connected to DMA?); // channel 2 is gated by a PPI output and feeds into the speaker. }; using PIT = i8253; class i8255PortHandler : public Intel::i8255::PortHandler { public: i8255PortHandler(PCSpeaker &speaker, KeyboardController &keyboard, VideoAdaptor adaptor, int drive_count) : speaker_(speaker), keyboard_(keyboard) { // High switches: // // b3, b2: drive count; 00 = 1, 01 = 2, etc // b1, b0: video mode (00 = ROM; 01 = CGA40; 10 = CGA80; 11 = MDA) switch(adaptor) { default: break; case VideoAdaptor::MDA: high_switches_ |= 0b11; break; case VideoAdaptor::CGA: high_switches_ |= 0b10; break; // Assume 80 columns. } high_switches_ |= uint8_t(drive_count << 2); // Low switches: // // b3, b2: RAM on motherboard (64 * bit pattern) // b1: 1 => FPU present; 0 => absent; // b0: 1 => floppy drive present; 0 => absent. low_switches_ |= 0b1100; // Assume maximum RAM. if(drive_count) low_switches_ |= 0xb0001; } void set_value(int port, uint8_t value) { switch(port) { case 1: // b7: 0 => enable keyboard read (and IRQ); 1 => don't; // b6: 0 => hold keyboard clock low; 1 => don't; // b5: 1 => disable IO check; 0 => don't; // b4: 1 => disable memory parity check; 0 => don't; // b3: [5150] cassette motor control; [5160] high or low switches select; // b2: [5150] high or low switches select; [5160] 1 => disable turbo mode; // b1, b0: speaker control. enable_keyboard_ = !(value & 0x80); keyboard_.set_mode(value >> 6); use_high_switches_ = value & 0x08; speaker_.set_control(value & 0x01, value & 0x02); break; } } uint8_t get_value(int port) { switch(port) { case 0: return enable_keyboard_ ? keyboard_.read() : uint8_t((high_switches_ << 4) | low_switches_); // Guesses that switches is high and low combined as below. case 2: // b7: 1 => memory parity error; 0 => none; // b6: 1 => IO channel error; 0 => none; // b5: timer 2 output; [TODO] // b4: cassette data input; [TODO] // b3...b0: whichever of the high and low switches is selected. return use_high_switches_ ? high_switches_ : low_switches_; } return 0; }; private: uint8_t high_switches_ = 0; uint8_t low_switches_ = 0; bool use_high_switches_ = false; PCSpeaker &speaker_; KeyboardController &keyboard_; bool enable_keyboard_ = false; }; using PPI = Intel::i8255::i8255; template class IO { public: IO(PIT &pit, DMA &dma, PPI &ppi, PIC &pic, typename Adaptor