1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-02 20:30:00 +00:00

Merge pull request #178 from TomHarte/CPC664

Introduces the beginning seeds 8272 emulation, and therefore floppy emulation for the CPC
This commit is contained in:
Thomas Harte 2017-08-07 18:39:33 -04:00 committed by GitHub
commit 4427e9a254
13 changed files with 997 additions and 49 deletions

View File

@ -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; }

462
Components/8272/i8272.cpp Normal file
View File

@ -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 <cstdio>
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<Storage::Disk::Disk> 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());
}

83
Components/8272/i8272.hpp Normal file
View File

@ -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 <cstdint>
#include <vector>
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<Storage::Disk::Disk> disk, int drive);
private:
void posit_event(int type);
uint8_t main_status_;
uint8_t status_[3];
std::vector<uint8_t> command_;
std::vector<uint8_t> 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<Storage::Disk::Drive> 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 */

View File

@ -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<uint8_t> 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<i8255PortHandler> 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<uint8_t> os_, basic_;
uint8_t ram_[128 * 1024];
std::vector<uint8_t> 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];

View File

@ -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
};

View File

@ -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 = "<group>"; };
4BBB14301CD2CECE00BDB55C /* IntermediateShader.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = IntermediateShader.hpp; sourceTree = "<group>"; };
4BBC34241D2208B100FFC9DF /* CSFastLoading.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSFastLoading.h; sourceTree = "<group>"; };
4BBC951C1F368D83008F4C34 /* i8272.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = i8272.cpp; path = 8272/i8272.cpp; sourceTree = "<group>"; };
4BBC951D1F368D83008F4C34 /* i8272.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = i8272.hpp; path = 8272/i8272.hpp; sourceTree = "<group>"; };
4BBC95201F36B16C008F4C34 /* MFMDiskController.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MFMDiskController.cpp; sourceTree = "<group>"; };
4BBC95211F36B16C008F4C34 /* MFMDiskController.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MFMDiskController.hpp; sourceTree = "<group>"; };
4BBF49AE1ED2880200AB3669 /* FUSETests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FUSETests.swift; sourceTree = "<group>"; };
4BBF99081C8FBA6F0075DAFB /* TextureBuilder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TextureBuilder.cpp; sourceTree = "<group>"; };
4BBF99091C8FBA6F0075DAFB /* TextureBuilder.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TextureBuilder.hpp; sourceTree = "<group>"; };
@ -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 = "<group>";
};
4BBC951F1F368D87008F4C34 /* 8272 */ = {
isa = PBXGroup;
children = (
4BBC951C1F368D83008F4C34 /* i8272.cpp */,
4BBC951D1F368D83008F4C34 /* i8272.hpp */,
);
name = 8272;
sourceTree = "<group>";
};
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 */,

View File

@ -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);
}

View File

@ -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;

View File

@ -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);

View File

@ -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.

View File

@ -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<Track> 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<Track> 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<SectorInfo> 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<Storage::Encodings::MFM::Sector> sectors;
for(auto &sector_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<Storage::Encodings::MFM::Sector> sectors;
for(auto &sector_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;
}

View File

@ -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;
}
}

View File

@ -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 */