diff --git a/Components/1770/1770.cpp b/Components/1770/1770.cpp index fc85d4f93..96db03aeb 100644 --- a/Components/1770/1770.cpp +++ b/Components/1770/1770.cpp @@ -263,7 +263,7 @@ void WD1770::process_write_completed() { } #define WAIT_FOR_EVENT(mask) resume_point_ = __LINE__; interesting_event_mask_ = mask; return; case __LINE__: -#define WAIT_FOR_TIME(ms) resume_point_ = __LINE__; interesting_event_mask_ = Event::Timer; delay_time_ = ms * 8000; if(delay_time_) return; case __LINE__: +#define WAIT_FOR_TIME(ms) resume_point_ = __LINE__; interesting_event_mask_ = Event::Timer; delay_time_ = ms * 8000; case __LINE__: if(delay_time_) return; #define WAIT_FOR_BYTES(count) resume_point_ = __LINE__; interesting_event_mask_ = Event::Token; distance_into_section_ = 0; return; case __LINE__: if(latest_token_.type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = Event::Token; return; } #define BEGIN_SECTION() switch(resume_point_) { default: #define END_SECTION() 0; } diff --git a/Components/8272/i8272.cpp b/Components/8272/i8272.cpp new file mode 100644 index 000000000..a98aab74e --- /dev/null +++ b/Components/8272/i8272.cpp @@ -0,0 +1,462 @@ +// +// i8272.cpp +// Clock Signal +// +// Created by Thomas Harte on 05/08/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#include "i8272.hpp" + +#include + +using namespace Intel; + +namespace { +const uint8_t StatusRQM = 0x80; // Set: ready to send or receive from processor. +const uint8_t StatusDIO = 0x40; // Set: data is expected to be taken from the 8272 by the processor. +const uint8_t StatusNDM = 0x20; // Set: the execution phase of a data transfer command is ongoing and DMA mode is disabled. +const uint8_t StatusCB = 0x10; // Set: the FDC is busy. +//const uint8_t StatusD3B = 0x08; // Set: drive 3 is seeking. +//const uint8_t StatusD2B = 0x04; // Set: drive 2 is seeking. +//const uint8_t StatusD1B = 0x02; // Set: drive 1 is seeking. +//const uint8_t StatusD0B = 0x01; // Set: drive 0 is seeking. +} + +i8272::i8272(Cycles clock_rate, int clock_rate_multiplier, int revolutions_per_minute) : + Storage::Disk::MFMController(clock_rate, clock_rate_multiplier, revolutions_per_minute), + main_status_(StatusRQM), + interesting_event_mask_((int)Event8272::CommandByte), + resume_point_(0), + delay_time_(0), + status_{0, 0, 0} { + posit_event((int)Event8272::CommandByte); +} + +void i8272::run_for(Cycles cycles) { + Storage::Disk::MFMController::run_for(cycles); + + // check for an expired timer + if(delay_time_ > 0) { + if(cycles.as_int() >= delay_time_) { + delay_time_ = 0; + posit_event((int)Event8272::Timer); + } else { + delay_time_ -= cycles.as_int(); + } + } + + // update seek status of any drives presently seeking + if(main_status_ & 0xf) { + for(int c = 0; c < 4; c++) { + if(drives_[c].phase == Drive::Seeking) { + drives_[c].step_rate_counter += cycles.as_int(); + int steps = drives_[c].step_rate_counter / (8000 * step_rate_time_); + drives_[c].step_rate_counter %= (8000 * step_rate_time_); + while(steps--) { + // Perform a step. + int direction = (drives_[c].target_head_position < drives_[c].head_position) ? -1 : 1; + drives_[c].drive->step(direction); + drives_[c].head_position += direction; + + // Check for completion. + if(seek_is_satisfied(c)) { + drives_[c].phase = Drive::CompletedSeeking; + if(drives_[c].target_head_position == -1) drives_[c].head_position = 0; + break; + } + } + } + } + } +} + +void i8272::set_register(int address, uint8_t value) { + // don't consider attempted sets to the status register + if(!address) return; + + // if not ready for commands, do nothing + if(!(main_status_ & StatusRQM)) return; + + // accumulate latest byte in the command byte sequence + command_.push_back(value); + posit_event((int)Event8272::CommandByte); +} + +uint8_t i8272::get_register(int address) { + if(address) { +// printf("8272 get data\n"); + + if(result_stack_.empty()) return 0xff; + uint8_t result = result_stack_.back(); + result_stack_.pop_back(); + if(result_stack_.empty()) posit_event((int)Event8272::ResultEmpty); + + return result; + } else { +// printf("Main status: %02x\n", main_status_); + return main_status_; + } +} + +void i8272::set_disk(std::shared_ptr disk, int drive) { + if(drive < 4 && drive >= 0) { + drives_[drive].drive->set_disk(disk); + } +} + +#define BEGIN_SECTION() switch(resume_point_) { default: +#define END_SECTION() } + +#define WAIT_FOR_EVENT(mask) resume_point_ = __LINE__; interesting_event_mask_ = (int)mask; return; case __LINE__: + +#define PASTE(x, y) x##y +#define CONCAT(x, y) PASTE(x, y) + +#define FIND_HEADER() \ + CONCAT(find_header, __LINE__): WAIT_FOR_EVENT((int)Event::Token | (int)Event::IndexHole); \ + if(event_type == (int)Event::IndexHole) index_hole_limit_--; \ + else if(get_latest_token().type == Token::ID) goto CONCAT(header_found, __LINE__); \ + \ + if(index_hole_limit_) goto CONCAT(find_header, __LINE__); \ + CONCAT(header_found, __LINE__): 0;\ + +#define FIND_DATA() \ + CONCAT(find_data, __LINE__): WAIT_FOR_EVENT((int)Event::Token | (int)Event::IndexHole); \ + if(event_type == (int)Event::Token && get_latest_token().type != Token::Data) goto CONCAT(find_data, __LINE__); + +#define READ_HEADER() \ + distance_into_section_ = 0; \ + set_data_mode(Reading); \ + CONCAT(read_header, __LINE__): WAIT_FOR_EVENT(Event::Token); \ + header_[distance_into_section_] = get_latest_token().byte_value; \ + distance_into_section_++; \ + if(distance_into_section_ < 6) goto CONCAT(read_header, __LINE__); \ + set_data_mode(Scanning); + +#define SET_DRIVE_HEAD_MFM() \ + if(!dma_mode_) main_status_ |= StatusNDM; \ + set_drive(drives_[command_[1]&3].drive); \ + set_is_double_density(command_[0] & 0x40); \ + invalidate_track(); + +void i8272::posit_event(int event_type) { + if(!(interesting_event_mask_ & event_type)) return; + interesting_event_mask_ &= ~event_type; + + BEGIN_SECTION(); + + // Resets busy and non-DMA execution, clears the command buffer, sets the data mode to scanning and flows + // into wait_for_complete_command_sequence. + wait_for_command: + set_data_mode(Storage::Disk::MFMController::DataMode::Scanning); + main_status_ &= ~(StatusCB | StatusNDM); + command_.clear(); + + // Sets the data request bit, and waits for a byte. Then sets the busy bit. Continues accepting bytes + // until it has a quantity that make up an entire command, then resets the data request bit and + // branches to that command. + wait_for_complete_command_sequence: + main_status_ |= StatusRQM; + WAIT_FOR_EVENT(Event8272::CommandByte) + main_status_ |= StatusCB; + + switch(command_[0] & 0x1f) { + case 0x06: // read data + if(command_.size() < 9) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto read_data; + + case 0x0b: // read deleted data + if(command_.size() < 9) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto read_deleted_data; + + case 0x05: // write data + if(command_.size() < 9) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto write_data; + + case 0x09: // write deleted data + if(command_.size() < 9) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto write_deleted_data; + + case 0x02: // read track + if(command_.size() < 9) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto read_track; + + case 0x0a: // read ID + if(command_.size() < 2) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto read_id; + + case 0x0d: // format track + if(command_.size() < 6) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto format_track; + + case 0x11: // scan low + if(command_.size() < 9) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto scan_low; + + case 0x19: // scan low or equal + if(command_.size() < 9) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto scan_low_or_equal; + + case 0x1d: // scan high or equal + if(command_.size() < 9) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto scan_high_or_equal; + + case 0x07: // recalibrate + if(command_.size() < 2) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto recalibrate; + + case 0x08: // sense interrupt status + main_status_ &= ~StatusRQM; + goto sense_interrupt_status; + + case 0x03: // specify + if(command_.size() < 3) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto specify; + + case 0x04: // sense drive status + if(command_.size() < 2) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto sense_drive_status; + + case 0x0f: // seek + if(command_.size() < 3) goto wait_for_complete_command_sequence; + main_status_ &= ~StatusRQM; + goto seek; + + default: // invalid + main_status_ &= ~StatusRQM; + goto invalid; + } + + // Performs the read data command. + read_data: + printf("Read data, sector %02x %02x %02x %02x\n", command_[2], command_[3], command_[4], command_[5]); + + // Establishes the drive and head being addressed, and whether in double density mode; populates the internal + // cylinder, head, sector and size registers from the command stream. + SET_DRIVE_HEAD_MFM(); + cylinder_ = command_[2]; + head_ = command_[3]; + sector_ = command_[4]; + size_ = command_[5]; + + // Sets a maximum index hole limit of 2 then performs a find header/read header loop, continuing either until + // the index hole limit is breached or a sector is found with a cylinder, head, sector and size equal to the + // values in the internal registers. + index_hole_limit_ = 2; + find_next_sector: + FIND_HEADER(); + if(!index_hole_limit_) goto read_data_not_found; + READ_HEADER(); + if(header_[0] != cylinder_ || header_[1] != head_ || header_[2] != sector_ || header_[3] != size_) goto find_next_sector; + + // Finds the next data block and sets data mode to reading. + FIND_DATA(); + distance_into_section_ = 0; + set_data_mode(Reading); + + // Waits for the next token, then supplies it to the CPU by: (i) setting data request and direction; and (ii) resetting + // data request once the byte has been taken. Continues until all bytes have been read. + // + // TODO: signal if the CPU is too slow and missed a byte; at the minute it'll just silently miss. Also allow for other + // ways that sector size might have been specified. + get_byte: + WAIT_FOR_EVENT(Event::Token); + result_stack_.push_back(get_latest_token().byte_value); + distance_into_section_++; + main_status_ |= StatusRQM | StatusDIO; + WAIT_FOR_EVENT(Event8272::ResultEmpty); + main_status_ &= ~StatusRQM; + if(distance_into_section_ < (128 << size_)) goto get_byte; + + // read CRC, without transferring it + WAIT_FOR_EVENT(Event::Token); + WAIT_FOR_EVENT(Event::Token); + + // For a final result phase, post the standard ST0, ST1, ST2, C, H, R, N + goto post_st012chrn; + + // Execution reaches here if two index holes were discovered before a matching sector — i.e. the data wasn't found. + // In that case set appropriate error flags and post the results. + read_data_not_found: + printf("Not found\n"); + + status_[1] |= 0x4; + status_[0] = 0x40; // (status_[0] & ~0xc0) | + goto post_st012chrn; + + read_deleted_data: + printf("Read deleted data unimplemented!!\n"); + goto wait_for_command; + + write_data: + printf("Write data unimplemented!!\n"); + goto wait_for_command; + + write_deleted_data: + printf("Write deleted data unimplemented!!\n"); + goto wait_for_command; + + read_track: + printf("Read track unimplemented!!\n"); + goto wait_for_command; + + // Performs the read ID command. + read_id: + // Establishes the drive and head being addressed, and whether in double density mode. + printf("Read ID\n"); + SET_DRIVE_HEAD_MFM(); + + // Sets a maximum index hole limit of 2 then waits either until it finds a header mark or sees too many index holes. + // If a header mark is found, reads in the following bytes that produce a header. Otherwise branches to data not found. + index_hole_limit_ = 2; + read_id_find_next_sector: + FIND_HEADER(); + if(!index_hole_limit_) goto read_data_not_found; + READ_HEADER(); + + // Sets internal registers from the discovered header and posts the standard ST0, ST1, ST2, C, H, R, N. + cylinder_ = header_[0]; + head_ = header_[1]; + sector_ = header_[2]; + size_ = header_[3]; + + goto post_st012chrn; + + format_track: + printf("Fromat track unimplemented!!\n"); + goto wait_for_command; + + scan_low: + printf("Scan low unimplemented!!\n"); + goto wait_for_command; + + scan_low_or_equal: + printf("Scan low or equal unimplemented!!\n"); + goto wait_for_command; + + scan_high_or_equal: + printf("Scan high or equal unimplemented!!\n"); + goto wait_for_command; + + // Performs both recalibrate and seek commands. These commands occur asynchronously, so the actual work + // occurs in ::run_for; this merely establishes that seeking should be ongoing. + recalibrate: + seek: + printf((command_.size() > 2) ? "Seek\n" : "Recalibrate\n"); + + // Declines to act if a seek is already ongoing; otherwise resets all status registers, sets the drive + // into seeking mode, sets the drive's main status seeking bit, and sets the target head position: for + // a recalibrate the target is -1 and ::run_for knows that -1 means the terminal condition is the drive + // returning that its at track zero, and that it should reset the drive's current position once reached. + if(drives_[command_[1]&3].phase != Drive::Seeking) { + status_[0] = status_[1] = status_[2] = 0; + int drive = command_[1]&3; + drives_[drive].phase = Drive::Seeking; + drives_[drive].steps_taken = 0; + drives_[drive].target_head_position = (command_.size() > 2) ? command_[2] : -1; + drives_[drive].step_rate_counter = 0; + + // Check whether any steps are even needed. + if(seek_is_satisfied(drive)) { + drives_[drive].phase = Drive::CompletedSeeking; + } else { + main_status_ |= 1 << (command_[1]&3); + } + } + goto wait_for_command; + + // Performs sense interrupt status. + sense_interrupt_status: + printf("Sense interrupt status\n"); + { + // Find the first drive that is in the CompletedSeeking state. + int found_drive = -1; + for(int c = 0; c < 4; c++) { + if(drives_[c].phase == Drive::CompletedSeeking) { + found_drive = c; + break; + } + } + + // If a drive was found, return its results. Otherwise return a single 0x80. + if(found_drive != -1) { + drives_[found_drive].phase = Drive::NotSeeking; + status_[0] = (uint8_t)found_drive | 0x20; + main_status_ &= ~(1 << found_drive); + + result_stack_.push_back(drives_[found_drive].head_position); + result_stack_.push_back(status_[0]); + } else { + result_stack_.push_back(0x80); + } + } + goto post_result; + + // Performs specify. + specify: + // Just store the values, and terminate the command. + step_rate_time_ = command_[1] &0xf0; // i.e. 16 to 240m + head_unload_time_ = command_[1] & 0x0f; // i.e. 1 to 16ms + head_load_time_ = command_[2] & ~1; // i.e. 2 to 254 ms in increments of 2ms + dma_mode_ = !(command_[2] & 1); + goto wait_for_command; + + sense_drive_status: + printf("Sense drive status unimplemented!!\n"); + goto wait_for_command; + + // Performs any invalid command. + invalid: + // A no-op, but posts ST0. + result_stack_.push_back(status_[0]); + goto post_result; + + // Posts ST0, ST1, ST2, C, H, R and N as a result phase. + post_st012chrn: + result_stack_.push_back(size_); + result_stack_.push_back(sector_); + result_stack_.push_back(head_); + result_stack_.push_back(cylinder_); + + result_stack_.push_back(status_[2]); + result_stack_.push_back(status_[1]); + result_stack_.push_back(status_[0]); + + goto post_result; + + // Posts whatever is in result_stack_ as a result phase. Be aware that it is a stack — the + // last thing in it will be returned first. + post_result: + // Set ready to send data to the processor, no longer in non-DMA execution phase. + main_status_ |= StatusRQM | StatusDIO; + main_status_ &= ~StatusNDM; + + // The actual stuff of unwinding result_stack_ is handled by ::get_register; wait + // until the processor has read all result bytes. + WAIT_FOR_EVENT(Event8272::ResultEmpty); + + // Reset data direction and end the command. + main_status_ &= ~StatusDIO; + goto wait_for_command; + + END_SECTION() +} + +bool i8272::seek_is_satisfied(int drive) { + return (drives_[drive].target_head_position == drives_[drive].head_position) || + (drives_[drive].target_head_position == -1 && drives_[drive].drive->get_is_track_zero()); +} diff --git a/Components/8272/i8272.hpp b/Components/8272/i8272.hpp new file mode 100644 index 000000000..ddc27d933 --- /dev/null +++ b/Components/8272/i8272.hpp @@ -0,0 +1,83 @@ +// +// i8272.hpp +// Clock Signal +// +// Created by Thomas Harte on 05/08/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#ifndef i8272_hpp +#define i8272_hpp + +#include "../../Storage/Disk/MFMDiskController.hpp" +#include "../../Storage/Disk/Drive.hpp" + +#include +#include + +namespace Intel { + +class i8272: public Storage::Disk::MFMController { + public: + i8272(Cycles clock_rate, int clock_rate_multiplier, int revolutions_per_minute); + + void run_for(Cycles); + + void set_register(int address, uint8_t value); + uint8_t get_register(int address); + + void set_disk(std::shared_ptr disk, int drive); + + private: + void posit_event(int type); + uint8_t main_status_; + uint8_t status_[3]; + + std::vector command_; + std::vector result_stack_; + + enum class Event8272: int { + CommandByte = (1 << 3), + Timer = (1 << 4), + ResultEmpty = (1 << 5), + }; + + int interesting_event_mask_; + int resume_point_; + int delay_time_; + + int step_rate_time_; + int head_unload_time_; + int head_load_time_; + bool dma_mode_; + + struct Drive { + uint8_t head_position; + + enum Phase { + NotSeeking, + Seeking, + CompletedSeeking + } phase; + int step_rate_counter; + int steps_taken; + int target_head_position; // either an actual number, or -1 to indicate to step until track zero + + std::shared_ptr drive; + + Drive() : head_position(0), phase(NotSeeking), drive(new Storage::Disk::Drive) {}; + } drives_[4]; + + uint8_t header_[6]; + int distance_into_section_; + int index_hole_limit_; + + uint8_t cylinder_, head_, sector_, size_; + + bool seek_is_satisfied(int drive); +}; + +} + + +#endif /* i8272_hpp */ diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index f3c36b523..4df26302e 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -10,9 +10,10 @@ #include "../../Processors/Z80/Z80.hpp" -#include "../../Components/8255/i8255.hpp" -#include "../../Components/AY38910/AY38910.hpp" #include "../../Components/6845/CRTC6845.hpp" +#include "../../Components/8255/i8255.hpp" +#include "../../Components/8272/i8272.hpp" +#include "../../Components/AY38910/AY38910.hpp" #include "../../Storage/Tape/Tape.hpp" @@ -427,6 +428,19 @@ struct KeyboardState { uint8_t rows[10]; }; +/*! + Wraps the 8272 so as to provide proper clocking and RPM counts, and just directly + exposes motor control, applying the same value to all drives. +*/ +class FDC: public Intel::i8272 { + public: + FDC() : i8272(Cycles(8000000), 16, 300) {} + + void set_motor_on(bool on) { + Intel::i8272::set_motor_on(on); + } +}; + /*! Provides the mechanism of receipt for input and output of the 8255's various ports. */ @@ -545,6 +559,9 @@ class ConcreteMachine: // Pump the AY. ay_.run_for(cycle.length); + // Clock the FDC, if connected, using a lazy scale by two + if(has_fdc_) fdc_.run_for(Cycles(cycle.length.as_int())); + // Stop now if no action is strictly required. if(!cycle.is_terminal()) return HalfCycles(0); @@ -567,8 +584,10 @@ class ConcreteMachine: case 1: crtc_bus_handler_.set_colour(*cycle.value & 0x1f); break; case 2: // Perform ROM paging. - read_pointers_[0] = (*cycle.value & 4) ? &ram_[0] : os_.data(); - read_pointers_[3] = (*cycle.value & 8) ? &ram_[49152] : basic_.data(); + read_pointers_[0] = (*cycle.value & 4) ? &ram_[0] : roms_[rom_model_].data(); + + upper_rom_is_paged_ = !(*cycle.value & 8); + read_pointers_[3] = upper_rom_is_paged_ ? roms_[upper_rom_].data() : &ram_[49152]; // Reset the interrupt timer if requested. if(*cycle.value & 0x10) interrupt_timer_.reset_count(); @@ -580,6 +599,12 @@ class ConcreteMachine: } } + // Check for an upper ROM selection + if(has_fdc_ && !(address&0x2000)) { + upper_rom_ = (*cycle.value == 7) ? ROMType::AMSDOS : rom_model_ + 1; + if(upper_rom_is_paged_) read_pointers_[3] = roms_[upper_rom_].data(); + } + // Check for a CRTC access if(!(address & 0x4000)) { switch((address >> 8) & 3) { @@ -593,6 +618,16 @@ class ConcreteMachine: if(!(address & 0x800)) { i8255_.set_register((address >> 8) & 3, *cycle.value); } + + // Check for an FDC access + if(has_fdc_ && (address & 0x580) == 0x100) { + fdc_.set_register(address & 1, *cycle.value); + } + + // Check for a disk motor access + if(has_fdc_ && !(address & 0x580)) { + fdc_.set_motor_on(!!(*cycle.value)); + } break; case CPU::Z80::PartialMachineCycle::Input: // Default to nothing answering @@ -611,6 +646,11 @@ class ConcreteMachine: if(!(address & 0x800)) { *cycle.value = i8255_.get_register((address >> 8) & 3); } + + // Check for an FDC access + if(has_fdc_ && (address & 0x580) == 0x100) { + *cycle.value = fdc_.get_register(address & 1); + } break; case CPU::Z80::PartialMachineCycle::Interrupt: @@ -666,11 +706,28 @@ class ConcreteMachine: /// The ConfigurationTarget entry point; should configure this meachine as described by @c target. void configure_as_target(const StaticAnalyser::Target &target) { - // Establish reset memory map as per machine model (or, for now, as a hard-wired 464) - read_pointers_[0] = os_.data(); + switch(target.amstradcpc.model) { + case StaticAnalyser::AmstradCPCModel::CPC464: + rom_model_ = ROMType::OS464; + has_fdc_ = false; + break; + case StaticAnalyser::AmstradCPCModel::CPC664: + rom_model_ = ROMType::OS664; + has_fdc_ = true; + break; + case StaticAnalyser::AmstradCPCModel::CPC6128: + rom_model_ = ROMType::OS6128; + has_fdc_ = true; + break; + } + + // Establish default memory map + upper_rom_is_paged_ = true; + upper_rom_ = rom_model_ + 1; + read_pointers_[0] = roms_[rom_model_].data(); read_pointers_[1] = &ram_[16384]; read_pointers_[2] = &ram_[32768]; - read_pointers_[3] = basic_.data(); + read_pointers_[3] = roms_[upper_rom_].data(); write_pointers_[0] = &ram_[0]; write_pointers_[1] = &ram_[16384]; @@ -681,16 +738,19 @@ class ConcreteMachine: if(!target.tapes.empty()) { tape_player_.set_tape(target.tapes.front()); } + + // Insert up to four disks. + int c = 0; + for(auto &disk : target.disks) { + fdc_.set_disk(disk, c); + c++; + if(c == 4) break; + } } // See header; provides the system ROMs. void set_rom(ROMType type, std::vector data) { - // Keep only the two ROMs that are currently of interest. - switch(type) { - case ROMType::OS464: os_ = data; break; - case ROMType::BASIC464: basic_ = data; break; - default: break; - } + roms_[(int)type] = data; } // See header; sets a key as either pressed or released. @@ -715,6 +775,8 @@ class ConcreteMachine: i8255PortHandler i8255_port_handler_; Intel::i8255::i8255 i8255_; + FDC fdc_; + InterruptTimer interrupt_timer_; Storage::Tape::BinaryTapePlayer tape_player_; @@ -722,8 +784,12 @@ class ConcreteMachine: HalfCycles crtc_counter_; HalfCycles half_cycles_since_ay_update_; - uint8_t ram_[65536]; - std::vector os_, basic_; + uint8_t ram_[128 * 1024]; + std::vector roms_[7]; + int rom_model_; + bool has_fdc_; + bool upper_rom_is_paged_; + int upper_rom_; uint8_t *read_pointers_[4]; uint8_t *write_pointers_[4]; diff --git a/Machines/AmstradCPC/AmstradCPC.hpp b/Machines/AmstradCPC/AmstradCPC.hpp index b3afc8e52..50ddeeda3 100644 --- a/Machines/AmstradCPC/AmstradCPC.hpp +++ b/Machines/AmstradCPC/AmstradCPC.hpp @@ -18,9 +18,10 @@ namespace AmstradCPC { -enum ROMType: uint8_t { - OS464, OS664, OS6128, - BASIC464, BASIC664, BASIC6128, +enum ROMType: int { + OS464 = 0, BASIC464, + OS664, BASIC664, + OS6128, BASIC6128, AMSDOS }; diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 8170445ee..1522bb3df 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -395,6 +395,8 @@ 4BB73EB71B587A5100552FC2 /* AllSuiteATests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EB61B587A5100552FC2 /* AllSuiteATests.swift */; }; 4BB73EC21B587A5100552FC2 /* Clock_SignalUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EC11B587A5100552FC2 /* Clock_SignalUITests.swift */; }; 4BBB14311CD2CECE00BDB55C /* IntermediateShader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB142F1CD2CECE00BDB55C /* IntermediateShader.cpp */; }; + 4BBC951E1F368D83008F4C34 /* i8272.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBC951C1F368D83008F4C34 /* i8272.cpp */; }; + 4BBC95221F36B16C008F4C34 /* MFMDiskController.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBC95201F36B16C008F4C34 /* MFMDiskController.cpp */; }; 4BBF49AF1ED2880200AB3669 /* FUSETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF49AE1ED2880200AB3669 /* FUSETests.swift */; }; 4BBF99141C8FBA6F0075DAFB /* TextureBuilder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF99081C8FBA6F0075DAFB /* TextureBuilder.cpp */; }; 4BBF99151C8FBA6F0075DAFB /* CRTOpenGL.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF990A1C8FBA6F0075DAFB /* CRTOpenGL.cpp */; }; @@ -954,6 +956,10 @@ 4BBB142F1CD2CECE00BDB55C /* IntermediateShader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = IntermediateShader.cpp; sourceTree = ""; }; 4BBB14301CD2CECE00BDB55C /* IntermediateShader.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = IntermediateShader.hpp; sourceTree = ""; }; 4BBC34241D2208B100FFC9DF /* CSFastLoading.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSFastLoading.h; sourceTree = ""; }; + 4BBC951C1F368D83008F4C34 /* i8272.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = i8272.cpp; path = 8272/i8272.cpp; sourceTree = ""; }; + 4BBC951D1F368D83008F4C34 /* i8272.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = i8272.hpp; path = 8272/i8272.hpp; sourceTree = ""; }; + 4BBC95201F36B16C008F4C34 /* MFMDiskController.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MFMDiskController.cpp; sourceTree = ""; }; + 4BBC95211F36B16C008F4C34 /* MFMDiskController.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MFMDiskController.hpp; sourceTree = ""; }; 4BBF49AE1ED2880200AB3669 /* FUSETests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FUSETests.swift; sourceTree = ""; }; 4BBF99081C8FBA6F0075DAFB /* TextureBuilder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TextureBuilder.cpp; sourceTree = ""; }; 4BBF99091C8FBA6F0075DAFB /* TextureBuilder.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TextureBuilder.hpp; sourceTree = ""; }; @@ -1525,6 +1531,7 @@ 4BAB62AB1D3272D200DF5BA0 /* Disk.cpp */, 4B6C73BB1D387AE500AFCFCA /* DiskController.cpp */, 4B30512B1D989E2200B4FED8 /* Drive.cpp */, + 4BBC95201F36B16C008F4C34 /* MFMDiskController.cpp */, 4B3F1B441E0388D200DB26EE /* PCMPatchedTrack.cpp */, 4B121F961E060CF000BFDA12 /* PCMSegment.cpp */, 4BAB62B61D3302CA00DF5BA0 /* PCMTrack.cpp */, @@ -1532,6 +1539,7 @@ 4BAB62AC1D3272D200DF5BA0 /* Disk.hpp */, 4B6C73BC1D387AE500AFCFCA /* DiskController.hpp */, 4B30512C1D989E2200B4FED8 /* Drive.hpp */, + 4BBC95211F36B16C008F4C34 /* MFMDiskController.hpp */, 4B3F1B451E0388D200DB26EE /* PCMPatchedTrack.hpp */, 4B121F971E060CF000BFDA12 /* PCMSegment.hpp */, 4BAB62B71D3302CA00DF5BA0 /* PCMTrack.hpp */, @@ -1982,6 +1990,15 @@ path = ../../Processors; sourceTree = ""; }; + 4BBC951F1F368D87008F4C34 /* 8272 */ = { + isa = PBXGroup; + children = ( + 4BBC951C1F368D83008F4C34 /* i8272.cpp */, + 4BBC951D1F368D83008F4C34 /* i8272.hpp */, + ); + name = 8272; + sourceTree = ""; + }; 4BBF49B41ED2881600AB3669 /* FUSE */ = { isa = PBXGroup; children = ( @@ -2048,6 +2065,7 @@ 4B4A762D1DB1A35C007AAE2E /* AY38910 */, 4BE845221F2FF7F400A5EA22 /* 6845 */, 4BD9137C1F3115AC009BCF85 /* 8255 */, + 4BBC951F1F368D87008F4C34 /* 8272 */, ); name = Components; path = ../../Components; @@ -2732,12 +2750,14 @@ 4BF829631D8F536B001BAE39 /* SSD.cpp in Sources */, 4B2E2D9D1C3A070400138695 /* Electron.cpp in Sources */, 4B3940E71DA83C8300427841 /* AsyncTaskQueue.cpp in Sources */, + 4BBC95221F36B16C008F4C34 /* MFMDiskController.cpp in Sources */, 4BAB62B81D3302CA00DF5BA0 /* PCMTrack.cpp in Sources */, 4B69FB3D1C4D908A00B5F0AA /* Tape.cpp in Sources */, 4B8378DF1F33675F005CA9E4 /* CharacterMapper.cpp in Sources */, 4B8FE2291DA1EDDF0090D3CE /* ElectronOptionsPanel.swift in Sources */, 4B55CE5D1C3B7D6F0093A61B /* CSOpenGLView.m in Sources */, 4BB697CB1D4B6D3E00248BDF /* TimedEventLoop.cpp in Sources */, + 4BBC951E1F368D83008F4C34 /* i8272.cpp in Sources */, 4BF1354C1D6D2C300054B2EA /* StaticAnalyser.cpp in Sources */, 4B4A76301DB1A3FA007AAE2E /* AY38910.cpp in Sources */, 4B2A53A31D117D36003C6002 /* CSVic20.mm in Sources */, diff --git a/StaticAnalyser/AmstradCPC/StaticAnalyser.cpp b/StaticAnalyser/AmstradCPC/StaticAnalyser.cpp index b66621960..1c176b332 100644 --- a/StaticAnalyser/AmstradCPC/StaticAnalyser.cpp +++ b/StaticAnalyser/AmstradCPC/StaticAnalyser.cpp @@ -19,5 +19,8 @@ void StaticAnalyser::AmstradCPC::AddTargets( target.disks = disks; target.tapes = tapes; target.cartridges = cartridges; + + target.amstradcpc.model = target.disks.empty() ? AmstradCPCModel::CPC464 : AmstradCPCModel::CPC664; + destination.push_back(target); } diff --git a/StaticAnalyser/StaticAnalyser.hpp b/StaticAnalyser/StaticAnalyser.hpp index ac1aca5ee..df63a43fa 100644 --- a/StaticAnalyser/StaticAnalyser.hpp +++ b/StaticAnalyser/StaticAnalyser.hpp @@ -46,6 +46,12 @@ enum class ZX8081MemoryModel { SixtyFourKB }; +enum class AmstradCPCModel { + CPC464, + CPC664, + CPC6128 +}; + /*! A list of disks, tapes and cartridges plus information about the machine to which to attach them and its configuration, and instructions on how to launch the software attached, plus a measure of confidence in this target's correctness. @@ -61,6 +67,8 @@ struct Target { } machine; float probability; + // TODO: this is too C-like a solution; make Target a base class and + // turn the following into information held by more specific subclasses. union { struct { bool has_adfs; @@ -87,6 +95,10 @@ struct Target { ZX8081MemoryModel memory_model; bool isZX81; } zx8081; + + struct { + AmstradCPCModel model; + } amstradcpc; }; std::string loadingCommand; diff --git a/Storage/Disk/DiskController.cpp b/Storage/Disk/DiskController.cpp index 46098ea74..5429da94f 100644 --- a/Storage/Disk/DiskController.cpp +++ b/Storage/Disk/DiskController.cpp @@ -11,8 +11,8 @@ using namespace Storage::Disk; -Controller::Controller(int clock_rate, int clock_rate_multiplier, int revolutions_per_minute) : - clock_rate_(clock_rate * clock_rate_multiplier), +Controller::Controller(Cycles clock_rate, int clock_rate_multiplier, int revolutions_per_minute) : + clock_rate_(clock_rate.as_int() * clock_rate_multiplier), clock_rate_multiplier_(clock_rate_multiplier), rotational_multiplier_(60, revolutions_per_minute), @@ -21,7 +21,7 @@ Controller::Controller(int clock_rate, int clock_rate_multiplier, int revolution is_reading_(true), - TimedEventLoop((unsigned int)(clock_rate * clock_rate_multiplier)) { + TimedEventLoop((unsigned int)(clock_rate.as_int() * clock_rate_multiplier)) { // seed this class with a PLL, any PLL, so that it's safe to assume non-nullptr later Time one(1); set_expected_bit_length(one); diff --git a/Storage/Disk/DiskController.hpp b/Storage/Disk/DiskController.hpp index 311e9b3d5..59c111f59 100644 --- a/Storage/Disk/DiskController.hpp +++ b/Storage/Disk/DiskController.hpp @@ -14,6 +14,7 @@ #include "PCMSegment.hpp" #include "PCMPatchedTrack.hpp" #include "../TimedEventLoop.hpp" +#include "../../ClockReceiver/ClockReceiver.hpp" namespace Storage { namespace Disk { @@ -33,7 +34,7 @@ class Controller: public DigitalPhaseLockedLoop::Delegate, public TimedEventLoop Constructs a @c DiskDrive that will be run at @c clock_rate and runs its PLL at @c clock_rate*clock_rate_multiplier, spinning inserted disks at @c revolutions_per_minute. */ - Controller(int clock_rate, int clock_rate_multiplier, int revolutions_per_minute); + Controller(Cycles clock_rate, int clock_rate_multiplier, int revolutions_per_minute); /*! Communicates to the PLL the expected length of a bit as a fraction of a second. diff --git a/Storage/Disk/Formats/CPCDSK.cpp b/Storage/Disk/Formats/CPCDSK.cpp index 2e185dda6..99f25132e 100644 --- a/Storage/Disk/Formats/CPCDSK.cpp +++ b/Storage/Disk/Formats/CPCDSK.cpp @@ -16,6 +16,7 @@ CPCDSK::CPCDSK(const char *file_name) : Storage::FileHolder(file_name), is_extended_(false) { if(!check_signature("MV - CPC", 8)) { is_extended_ = true; + fseek(file_, 0, SEEK_SET); if(!check_signature("EXTENDED", 8)) throw ErrorNotCPCDSK; } @@ -26,6 +27,8 @@ CPCDSK::CPCDSK(const char *file_name) : head_count_ = (unsigned int)fgetc(file_); if(is_extended_) { + // Skip two unused bytes and grab the track size table. + fseek(file_, 2, SEEK_CUR); for(unsigned int c = 0; c < head_position_count_ * head_count_; c++) { track_sizes_.push_back((size_t)(fgetc(file_) << 8)); } @@ -59,6 +62,7 @@ std::shared_ptr CPCDSK::get_uncached_track_at_position(unsigned int head, unsigned int t = 0; while(t < chronological_track && t < track_sizes_.size()) { file_offset += track_sizes_[t]; + t++; } } else { // Tracks are a fixed size in the original DSK file format. @@ -79,48 +83,71 @@ std::shared_ptr CPCDSK::get_uncached_track_at_position(unsigned int head, uint8_t track; uint8_t side; uint8_t sector; - size_t length; + uint8_t length; uint8_t status1; uint8_t status2; + size_t actual_length; }; std::vector sector_infos; while(number_of_sectors--) { - SectorInfo new_sector; + SectorInfo sector_info; - new_sector.track = (uint8_t)fgetc(file_); - new_sector.side = (uint8_t)fgetc(file_); - new_sector.sector = (uint8_t)fgetc(file_); - new_sector.length = (size_t)(128 << fgetc(file_)); - if(new_sector.length == 0x2000) new_sector.length = 0x1800; - new_sector.status1 = (uint8_t)fgetc(file_); - new_sector.status2 = (uint8_t)fgetc(file_); - fseek(file_, 2, SEEK_CUR); + sector_info.track = (uint8_t)fgetc(file_); + sector_info.side = (uint8_t)fgetc(file_); + sector_info.sector = (uint8_t)fgetc(file_); + sector_info.length = (uint8_t)fgetc(file_); + sector_info.status1 = (uint8_t)fgetc(file_); + sector_info.status2 = (uint8_t)fgetc(file_); + sector_info.actual_length = fgetc16le(); - sector_infos.push_back(new_sector); + sector_infos.push_back(sector_info); } // Get the sectors. fseek(file_, file_offset + 0x100, SEEK_SET); - if(is_extended_) { - // TODO: everything about extended disk images - } else { - std::vector sectors; - for(auto §or_info : sector_infos) { - Storage::Encodings::MFM::Sector new_sector; - new_sector.track = sector_info.track; - new_sector.side = sector_info.side; - new_sector.sector = sector_info.sector; - new_sector.data.resize(sector_info.length); - fread(new_sector.data.data(), sizeof(uint8_t), sector_info.length, file_); + std::vector sectors; + for(auto §or_info : sector_infos) { + Storage::Encodings::MFM::Sector new_sector; + new_sector.track = sector_info.track; + new_sector.side = sector_info.side; + new_sector.sector = sector_info.sector; - // TODO: obey the status bytes, somehow (?) + size_t data_size; + if(is_extended_) { + data_size = sector_info.actual_length; + } else { + data_size = (size_t)(128 << sector_info.length); + if(data_size == 0x2000) data_size = 0x1800; + } + new_sector.data.resize(data_size); + fread(new_sector.data.data(), sizeof(uint8_t), data_size, file_); - sectors.push_back(std::move(new_sector)); + // TODO: obey the status bytes, somehow (?) + if(sector_info.status1 || sector_info.status2) { + if(sector_info.status1 & 0x08) { + // The CRC failed in the ID field. + } + + if(sector_info.status2 & 0x20) { + // The CRC failed in the data field. + } + + if(sector_info.status2 & 0x40) { + // This sector is marked as deleted. + } + + if(sector_info.status2 & 0x01) { + // Data field wasn't found. + } + + printf("Unhandled: status errors\n"); } - // TODO: supply gay 3 length and filler byte - if(sectors.size()) return Storage::Encodings::MFM::GetMFMTrackWithSectors(sectors); + sectors.push_back(std::move(new_sector)); } + // TODO: supply gay 3 length and filler byte + if(sectors.size()) return Storage::Encodings::MFM::GetMFMTrackWithSectors(sectors); + return nullptr; } diff --git a/Storage/Disk/MFMDiskController.cpp b/Storage/Disk/MFMDiskController.cpp new file mode 100644 index 000000000..d9663f398 --- /dev/null +++ b/Storage/Disk/MFMDiskController.cpp @@ -0,0 +1,155 @@ +// +// MFMDiskController.cpp +// Clock Signal +// +// Created by Thomas Harte on 05/08/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#include "MFMDiskController.hpp" + +#include "../../Storage/Disk/Encodings/MFM.hpp" + +using namespace Storage::Disk; + +MFMController::MFMController(Cycles clock_rate, int clock_rate_multiplier, int revolutions_per_minute) : + Storage::Disk::Controller(clock_rate, clock_rate_multiplier, revolutions_per_minute), + crc_generator_(0x1021, 0xffff), + data_mode_(DataMode::Scanning), + is_awaiting_marker_value_(false) { +} + +void MFMController::process_index_hole() { + posit_event((int)Event::IndexHole); +} + +void MFMController::process_write_completed() { + posit_event((int)Event::DataWritten); +} + +void MFMController::set_is_double_density(bool is_double_density) { + is_double_density_ = is_double_density; + Storage::Time bit_length; + bit_length.length = 1; + bit_length.clock_rate = is_double_density ? 500000 : 250000; + set_expected_bit_length(bit_length); + + if(!is_double_density) is_awaiting_marker_value_ = false; +} + +bool MFMController::get_is_double_density() { + return is_double_density_; +} + +void MFMController::set_data_mode(DataMode mode) { + data_mode_ = mode; +} + +MFMController::Token MFMController::get_latest_token() { + return latest_token_; +} + +NumberTheory::CRC16 &MFMController::get_crc_generator() { + return crc_generator_; +} + +void MFMController::process_input_bit(int value, unsigned int cycles_since_index_hole) { + if(data_mode_ == DataMode::Writing) return; + + shift_register_ = (shift_register_ << 1) | value; + bits_since_token_++; + + if(data_mode_ == DataMode::Scanning) { + Token::Type token_type = Token::Byte; + if(!is_double_density_) { + switch(shift_register_ & 0xffff) { + case Storage::Encodings::MFM::FMIndexAddressMark: + token_type = Token::Index; + crc_generator_.reset(); + crc_generator_.add(latest_token_.byte_value = Storage::Encodings::MFM::IndexAddressByte); + break; + case Storage::Encodings::MFM::FMIDAddressMark: + token_type = Token::ID; + crc_generator_.reset(); + crc_generator_.add(latest_token_.byte_value = Storage::Encodings::MFM::IDAddressByte); + break; + case Storage::Encodings::MFM::FMDataAddressMark: + token_type = Token::Data; + crc_generator_.reset(); + crc_generator_.add(latest_token_.byte_value = Storage::Encodings::MFM::DataAddressByte); + break; + case Storage::Encodings::MFM::FMDeletedDataAddressMark: + token_type = Token::DeletedData; + crc_generator_.reset(); + crc_generator_.add(latest_token_.byte_value = Storage::Encodings::MFM::DeletedDataAddressByte); + break; + default: + break; + } + } else { + switch(shift_register_ & 0xffff) { + case Storage::Encodings::MFM::MFMIndexSync: + bits_since_token_ = 0; + is_awaiting_marker_value_ = true; + + token_type = Token::Sync; + latest_token_.byte_value = Storage::Encodings::MFM::MFMIndexSyncByteValue; + break; + case Storage::Encodings::MFM::MFMSync: + bits_since_token_ = 0; + is_awaiting_marker_value_ = true; + crc_generator_.set_value(Storage::Encodings::MFM::MFMPostSyncCRCValue); + + token_type = Token::Sync; + latest_token_.byte_value = Storage::Encodings::MFM::MFMSyncByteValue; + break; + default: + break; + } + } + + if(token_type != Token::Byte) { + latest_token_.type = token_type; + bits_since_token_ = 0; + posit_event((int)Event::Token); + return; + } + } + + if(bits_since_token_ == 16) { + latest_token_.type = Token::Byte; + latest_token_.byte_value = (uint8_t)( + ((shift_register_ & 0x0001) >> 0) | + ((shift_register_ & 0x0004) >> 1) | + ((shift_register_ & 0x0010) >> 2) | + ((shift_register_ & 0x0040) >> 3) | + ((shift_register_ & 0x0100) >> 4) | + ((shift_register_ & 0x0400) >> 5) | + ((shift_register_ & 0x1000) >> 6) | + ((shift_register_ & 0x4000) >> 7)); + bits_since_token_ = 0; + + if(is_awaiting_marker_value_ && is_double_density_) { + is_awaiting_marker_value_ = false; + switch(latest_token_.byte_value) { + case Storage::Encodings::MFM::IndexAddressByte: + latest_token_.type = Token::Index; + break; + case Storage::Encodings::MFM::IDAddressByte: + latest_token_.type = Token::ID; + break; + case Storage::Encodings::MFM::DataAddressByte: + latest_token_.type = Token::Data; + break; + case Storage::Encodings::MFM::DeletedDataAddressByte: + latest_token_.type = Token::DeletedData; + break; + default: break; + } + } + + crc_generator_.add(latest_token_.byte_value); + posit_event((int)Event::Token); + return; + } +} diff --git a/Storage/Disk/MFMDiskController.hpp b/Storage/Disk/MFMDiskController.hpp new file mode 100644 index 000000000..e32e697a2 --- /dev/null +++ b/Storage/Disk/MFMDiskController.hpp @@ -0,0 +1,118 @@ +// +// MFMDiskController.hpp +// Clock Signal +// +// Created by Thomas Harte on 05/08/2017. +// Copyright © 2017 Thomas Harte. All rights reserved. +// + +#ifndef MFMDiskController_hpp +#define MFMDiskController_hpp + +#include "DiskController.hpp" +#include "../../NumberTheory/CRC.hpp" +#include "../../ClockReceiver/ClockReceiver.hpp" + +namespace Storage { +namespace Disk { + +/*! + Extends Controller with a built-in shift register and FM/MFM decoding logic, + being able to post event messages to subclasses. +*/ +class MFMController: public Controller { + public: + MFMController(Cycles clock_rate, int clock_rate_multiplier, int revolutions_per_minute); + + protected: + /// Indicates whether the controller should try to decode double-density MFM content, or single-density FM content. + void set_is_double_density(bool); + + /// @returns @c true if currently decoding MFM content; @c false otherwise. + bool get_is_double_density(); + + enum DataMode { + /// When the controller is scanning it will obey all synchronisation marks found, even if in the middle of data. + Scanning, + /// When the controller is reading it will ignore synchronisation marks and simply return a new token every sixteen PLL clocks. + Reading, + /// When the controller is writing, it will replace the underlying data with that which has been enqueued, posting Event::DataWritten when the queue is empty. + Writing + }; + /// Sets the current data mode. + void set_data_mode(DataMode); + + /*! + Describes a token found in the incoming PLL bit stream. Tokens can be one of: + + Index: the bit pattern usually encoded at the start of a track to denote the position of the index hole; + ID: the pattern that begins an ID section, i.e. a sector header, announcing sector number, track number, etc. + Data: the pattern that begins a data section, i.e. sector contents. + DeletedData: the pattern that begins a deleted data section, i.e. deleted sector contents. + + Sync: MFM only; the same synchronisation mark is used in MFM to denote the bottom three of the four types + of token listed above; this class combines notification of that mark and the distinct index sync mark. + Both are followed by a byte to indicate type. When scanning an MFM stream, subclasses will receive an + announcement of sync followed by an announcement of one of the above four types of token. + + Byte: reports reading of an ordinary byte, with expected timing bits. + + When the data mode is set to 'reading', only Byte tokens are returned; detection of the other kinds of token + is suppressed. Controllers will likely want to switch data mode when receiving ID and sector contents, as + spurious sync signals can otherwise be found in ordinary data, causing framing errors. + */ + struct Token { + enum Type { + Index, ID, Data, DeletedData, Sync, Byte + } type; + uint8_t byte_value; + }; + /// @returns The most-recently read token from the surface of the disk. + Token get_latest_token(); + + /// @returns The controller's CRC generator. This is automatically fed during reading. + NumberTheory::CRC16 &get_crc_generator(); + + // Events + enum class Event: int { + Token = (1 << 0), // Indicates recognition of a new token in the flux stream. Use get_latest_token() for more details. + IndexHole = (1 << 1), // Indicates the passing of a physical index hole. + DataWritten = (1 << 2), // Indicates that all queued bits have been written + }; + + /*! + Subclasses should implement this. It is called every time a new @c Event is discovered in the incoming data stream. + Therefore it is called to announce when: + + (i) a new token is discovered in the incoming stream: an index, ID, data or deleted data, a sync mark or a new byte of data. + (ii) the index hole passes; or + (iii) the queue of data to be written has been exhausted. + */ + virtual void posit_event(int type) = 0; + + private: + // Storage::Disk::Controller + virtual void process_input_bit(int value, unsigned int cycles_since_index_hole); + virtual void process_index_hole(); + virtual void process_write_completed(); + + // PLL input state + int bits_since_token_; + int shift_register_; + bool is_awaiting_marker_value_; + + // input configuration + bool is_double_density_; + DataMode data_mode_; + + // output + Token latest_token_; + + // CRC generator + NumberTheory::CRC16 crc_generator_; +}; + +} +} + +#endif /* MFMDiskController_hpp */