1
0
mirror of https://github.com/TomHarte/CLK.git synced 2026-01-26 06:16:22 +00:00
Files
CLK/Machines/PCCompatible/FloppyController.hpp
2025-09-11 23:29:47 -04:00

443 lines
12 KiB
C++

//
// FloppyController.hpp
// Clock Signal
//
// Created by Thomas Harte on 07/03/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "DMA.hpp"
#include "PIC.hpp"
#include "PIT.hpp"
#include "Analyser/Static/PCCompatible/Target.hpp"
#include "Components/8272/CommandDecoder.hpp"
#include "Components/8272/Results.hpp"
#include "Components/8272/Status.hpp"
#include "Outputs/Log.hpp"
#include "Storage/Disk/Track/TrackSerialiser.hpp"
#include "Storage/Disk/Encodings/MFM/Parser.hpp"
#include <algorithm>
#include <numeric>
namespace PCCompatible {
template <Analyser::Static::PCCompatible::Model model>
class FloppyController {
public:
FloppyController(
PICs<model> &pics,
DMA<model> &dma,
const int drive_count
) : pics_(pics), dma_(dma) {
// Default: one floppy drive only.
for(int c = 0; c < 4; c++) {
drives_[c].exists = drive_count > c;
}
}
void set_digital_output(const uint8_t control) {
Logger::info().append("Digital output: %02x", 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; // Possibly also enables interrupts?
const bool hold_reset = !(control & 0x04);
if(!hold_reset && hold_reset_) {
// TODO: add a delay mechanism.
reset();
}
hold_reset_ = hold_reset;
if(hold_reset_) {
pics_.pic[0].template apply_edge<6>(false);
}
}
void set_data_rate(const uint8_t control) {
Logger::info().append("Data rate: %02x", control);
}
uint8_t status() const {
const auto result = status_.main();
Logger::info().append("Status: %02x", result);
return result;
}
void write(const uint8_t value) {
// Logger::info().append("03f5 <- %02x", value);
decoder_.push_back(value);
if(decoder_.has_command()) {
using Command = Intel::i8272::Command;
switch(decoder_.command()) {
default:
Logger::error().append("TODO: implement FDC command %02x", uint8_t(decoder_.command()));
// Unimplemented:
//
// ReadTrack
// ReadID
// FormatTrack
//
// ScanLow
// ScanLowOrEqual
// ScanHighOrEqual
break;
case Command::WriteDeletedData:
case Command::WriteData: {
auto &drive = drives_[decoder_.target().drive];
Logger::info().append(
"Write %sdata to drive %d / head %d / track %d of head %d / track %d / sector %d",
decoder_.command() == Command::WriteDeletedData ? "deleted " : "",
decoder_.target().drive,
decoder_.target().head,
drive.track,
decoder_.geometry().head,
decoder_.geometry().cylinder,
decoder_.geometry().sector
);
status_.begin(decoder_);
// Just decline to write, for now.
// TODO: stop doing this.
status_.set(Intel::i8272::Status1::NotWriteable);
status_.set(Intel::i8272::Status0::BecameNotReady);
results_.serialise(
status_,
decoder_.geometry().cylinder,
decoder_.geometry().head,
decoder_.geometry().sector,
decoder_.geometry().size);
// TODO: what if head has changed?
drive.status = decoder_.drive_head();
drive.raised_interrupt = true;
pics_.pic[0].template apply_edge<6>(true);
} break;
case Command::ReadDeletedData:
case Command::ReadData: {
auto &drive = drives_[decoder_.target().drive];
Logger::info().append(
"Read %sdata from drive %d / head %d / track %d of head %d / track %d / sector %d",
decoder_.command() == Command::ReadDeletedData ? "deleted " : "",
decoder_.target().drive,
decoder_.target().head,
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) {
const auto sector = drive.sector(target.head, target.sector);
if(sector) {
// TODO: I _think_ I'm supposed to validate the rest of the address here?
for(int c = 0; c < 128 << target.size; c++) {
const auto access_result = dma_.write(2, sector->samples[0].data()[c]);
switch(access_result) {
// Default: keep going.
default: continue;
// Anything else: update flags and exit.
case AccessResult::NotAccepted:
complete = true;
status_.set(Intel::i8272::Status1::OverRun);
status_.set(Intel::i8272::Status0::AbnormalTermination);
break;
case AccessResult::AcceptedWithEOP:
complete = true;
break;
}
break;
}
++target.sector; // TODO: multitrack?
} else {
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?
drive.status = decoder_.drive_head();
drive.raised_interrupt = true;
pics_.pic[0].template apply_edge<6>(true);
} break;
case Command::ReadID: {
auto &drive = drives_[decoder_.target().drive];
const auto target = decoder_.target();
Logger::info().append(
"Read ID from drive %d / head %d / track %",
target.drive,
target.head,
drive.track
);
// TODO: should really provide a succession of different IDs.
status_.begin(decoder_);
const auto sector = drive.any_sector(target.head);
if(!sector) {
status_.set(Intel::i8272::Status1::EndOfCylinder);
status_.set(Intel::i8272::Status0::AbnormalTermination);
results_.serialise(
status_,
0,
0,
0,
0);
} else {
results_.serialise(
status_,
sector->address.track,
sector->address.side,
sector->address.sector,
sector->size);
}
drive.status = decoder_.drive_head();
drive.raised_interrupt = true;
pics_.pic[0].template apply_edge<6>(true);
} break;
case Command::Recalibrate:
case Command::Seek: {
auto &drive = drives_[decoder_.target().drive];
drive.track = decoder_.command() == Command::Seek ? decoder_.seek_target() : 0;
Logger::info().append(
"%s to %d",
decoder_.command() == Command::Seek ? "Seek" : "Recalibrate",
drive.track
);
drive.raised_interrupt = true;
drive.status = decoder_.target().drive | uint8_t(Intel::i8272::Status0::SeekEnded);
drive.ready = drive.has_disk();
pics_.pic[0].template apply_edge<6>(true);
} break;
case Command::SenseInterruptStatus: {
const auto interruptor = std::find_if(
std::begin(drives_),
std::end(drives_),
[] (const auto &drive) {
return drive.raised_interrupt;
}
);
if(interruptor != std::end(drives_)) {
last_seeking_drive_ = interruptor - std::begin(drives_);
}
auto &drive = drives_[last_seeking_drive_];
Logger::info().append(
"Sense interrupt status; picked drive %d with interrupt status %d",
last_seeking_drive_,
drive.raised_interrupt
);
status_.set_status0(drive.status);
results_.serialise(status_, drive.track);
// Clear cause-of-interrupt flags on that drive.
drive.raised_interrupt = false;
drive.status &= ~0xc0;
// Possibly lower interrupt flag.
const bool any_remaining_interrupts = std::accumulate(
std::begin(drives_),
std::end(drives_),
false,
[] (const bool flag, const auto &drive) {
return flag | drive.raised_interrupt;
}
);
if(!any_remaining_interrupts) {
pics_.pic[0].template apply_edge<6>(false);
}
} break;
case Command::Specify:
Logger::info().append("Specify");
specify_specs_ = decoder_.specify_specs();
break;
case Command::SenseDriveStatus: {
const auto &drive = drives_[decoder_.target().drive];
Logger::info().append(
"Sense drive status: drive %d / head %d; track 0 is %d, ready is %d",
decoder_.target().drive,
decoder_.target().head,
drive.track == 0,
drive.ready
);
results_.serialise(
decoder_.drive_head(),
(drive.track == 0 ? 0x10 : 0x00) |
(drive.ready ? 0x20 : 0x00) | // Ready [=> has disc and has stepped].
0x00 // Disk in drive is not read-only. [0x40]
);
} break;
case Command::Invalid:
Logger::info().append("Invalid command");
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);
}
Logger::info().append("Result read: %02x", result);
return result;
}
Logger::info().append("Result read: 80 [default]");
return 0x80;
}
void set_activity_observer(Activity::Observer *const 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<Storage::Disk::Disk> disk, const 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;
// pics_.pic[0].apply_edge<6>(true);
// }
drives_[drive].set_disk(disk);
}
private:
using Logger = Log::Logger<Log::Source::Floppy>;
void reset() {
Logger::info().append("{Reset}");
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) | uint8_t(c);
}
pics_.pic[0].template apply_edge<6>(true);
using MainStatus = Intel::i8272::MainStatus;
status_.set(MainStatus::DataReady, true);
status_.set(MainStatus::DataIsToProcessor, false);
}
PICs<model> &pics_;
DMA<model> &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; // ST0 if this drive is selected.
uint8_t track = 0;
bool motor = false;
bool exists = true;
bool ready = false;
bool has_disk() const {
return static_cast<bool>(parser_);
}
void set_disk(std::shared_ptr<Storage::Disk::Disk> image) {
parser_ = std::make_unique<Storage::Encodings::MFM::Parser>(image);
ready = false;
}
const Storage::Encodings::MFM::Sector *sector(const int head, const uint8_t sector) {
return parser_ ? parser_->sector(head, track, sector) : nullptr;
}
const Storage::Encodings::MFM::Sector *any_sector(const int head) {
return parser_ ? parser_->any_sector(head, track) : nullptr;
}
private:
std::unique_ptr<Storage::Encodings::MFM::Parser> parser_;
} drives_[4];
ssize_t last_seeking_drive_ = 0;
static std::string drive_name(const int c) {
char name[3] = "A";
name[0] += c;
return std::string("Drive ") + name;
}
Activity::Observer *observer_ = nullptr;
};
}