diff --git a/devices/floppy/superdrive.cpp b/devices/floppy/superdrive.cpp index 6cfc5b6..0205c96 100644 --- a/devices/floppy/superdrive.cpp +++ b/devices/floppy/superdrive.cpp @@ -21,6 +21,7 @@ along with this program. If not, see . /** @file Macintosh Superdrive emulation. */ +#include #include #include #include @@ -42,12 +43,15 @@ MacSuperDrive::MacSuperDrive() void MacSuperDrive::reset_params() { - this->media_kind = MediaKind::high_density; - this->has_disk = 0; // drive is empty - this->drive_mode = RecMethod::MFM; // assume MFM mode by default - this->motor_stat = 0; // spindle motor is off - this->cur_track = 0; // current head position - this->is_ready = 0; // drive not ready + this->media_kind = MediaKind::high_density; + this->has_disk = 0; // drive is empty + this->motor_stat = 0; // spindle motor is off + this->motor_on_time = 0; + this->cur_track = 0; // current head position + this->is_ready = 0; // drive not ready + + // come up in the MFM mode by default + this->switch_drive_mode(RecMethod::MFM); } void MacSuperDrive::command(uint8_t addr, uint8_t value) @@ -73,9 +77,15 @@ void MacSuperDrive::command(uint8_t addr, uint8_t value) if (this->motor_stat != new_motor_stat) { this->motor_stat = new_motor_stat; if (new_motor_stat) { + this->motor_on_time = TimerManager::get_instance()->current_time_ns(); + this->track_start_time = 0; + this->sector_start_time = 0; + this->init_track_search(-1); this->is_ready = 1; LOG_F(INFO, "Superdrive: turn spindle motor on"); } else { + this->motor_on_time = 0; + this->is_ready = 0; LOG_F(INFO, "Superdrive: turn spindle motor off"); } } @@ -114,8 +124,7 @@ uint8_t MacSuperDrive::status(uint8_t addr) case StatusAddr::Eject_Latch: return this->eject_latch; case StatusAddr::Select_Head_0: - this->cur_head = 0; - return 1; // not sure what should be returned here + return this->cur_head = 0; case StatusAddr::MFM_Support: return 1; // Superdrive does support MFM encoding scheme case StatusAddr::Double_Sided: @@ -129,8 +138,7 @@ uint8_t MacSuperDrive::status(uint8_t addr) case StatusAddr::Track_Zero: return this->track_zero ^ 1; // reverse logic case StatusAddr::Select_Head_1: - this->cur_head = 1; - return 1; // not sure what should be returned here + return this->cur_head = 1; case StatusAddr::Drive_Mode: return this->drive_mode; case StatusAddr::Drive_Ready: @@ -216,39 +224,138 @@ void MacSuperDrive::switch_drive_mode(int mode) this->track2lblk[trk] = trk * sectors_per_track * 2; } + // set up disk timing parameters + this->index_delay = MFM_INDX_MARK_DELAY; + this->addr_mark_delay = MFM_ADR_MARK_DELAY; + + if (this->media_kind) { + this->sector_delay = MFM_HD_SECTOR_DELAY; + this->eot_delay = MFM_HD_EOT_DELAY; + } else { + this->sector_delay = MFM_DD_SECTOR_DELAY; + this->eot_delay = MFM_DD_EOT_DELAY; + } + this->drive_mode = RecMethod::MFM; } } double MacSuperDrive::get_current_track_delay() { - return (1.0f / (this->rpm_per_track[this->cur_track] / 60)); -} - -double MacSuperDrive::get_sector_delay() -{ - return this->get_current_track_delay() / this->sectors_per_track[this->cur_track]; + return 60.0f / this->rpm_per_track[this->cur_track]; } void MacSuperDrive::init_track_search(int pos) { if (pos == -1) { // pick random sector number - this->cur_sector = this->sectors_per_track[this->cur_track] / 2; + uint64_t seed = TimerManager::get_instance()->current_time_ns() >> 8; + this->cur_sector = seed % this->sectors_per_track[this->cur_track]; + LOG_F(9, "Superdrive: current sector number set to %d", this->cur_sector); } else { this->cur_sector = pos; } } -SectorHdr MacSuperDrive::next_sector_header() +uint64_t MacSuperDrive::sync_to_disk() { - this->cur_sector++; - if (this->cur_sector >= this->sectors_per_track[this->cur_track]) { - this->cur_sector = 0; + uint64_t cur_time_ns, track_time_ns; + + uint64_t track_delay = this->get_current_track_delay() * NS_PER_SEC; + + // look how much ns have been elapsed since the last motor enabling + cur_time_ns = TimerManager::get_instance()->current_time_ns() - this->motor_on_time; + + if (!this->track_start_time || + (cur_time_ns - this->track_start_time) >= track_delay) { + // subtract full disk revolutions + track_time_ns = cur_time_ns % track_delay; + + this->track_start_time = cur_time_ns - track_time_ns; + + // return delay until the first address mark if we're currently + // looking at the end of track + if (track_time_ns >= this->eot_delay) { + this->next_sector = 0; + // CHEAT: don't account for the remaining time of the current track + return this->index_delay + this->addr_mark_delay; + } + + // return delay until the first address mark + // if the first address mark was not reached yet + if (track_time_ns < this->index_delay) { + this->next_sector = 0; + return this->index_delay + this->addr_mark_delay - track_time_ns; + } + } else { + track_time_ns = cur_time_ns - this->track_start_time; } + // subtract index field delay + track_time_ns -= this->index_delay; + + // calculate current sector number from timestamp + int cur_sect_num = this->cur_sector = track_time_ns / this->sector_delay; + + this->sector_start_time = this->track_start_time + cur_sect_num * this->sector_delay + + this->index_delay; + + if (this->cur_sector + 1 >= this->sectors_per_track[this->cur_track]) { + uint64_t diff = track_delay - (cur_time_ns - this->track_start_time); + this->next_sector = 0; + this->track_start_time = 0; + this->sector_start_time = 0; + return diff + this->index_delay + this->addr_mark_delay; + } else { + this->next_sector = this->cur_sector + 1; + uint64_t diff = cur_time_ns - this->sector_start_time; + this->sector_start_time += this->sector_delay; + return this->sector_delay - diff + this->addr_mark_delay; + } +} + +uint64_t MacSuperDrive::next_addr_mark_delay(uint8_t *next_sect_num) +{ + uint64_t delay; + + if (this->cur_sector + 1 >= this->sectors_per_track[this->cur_track]) { + this->next_sector = 0; + delay = this->index_delay + this->addr_mark_delay; + } else { + this->next_sector = this->cur_sector + 1; + delay = this->addr_mark_delay; + } + + if (next_sect_num) { + *next_sect_num = this->next_sector + ((this->rec_method == RecMethod::MFM) ? 1 : 0); + } + + return delay; +} + +uint64_t MacSuperDrive::next_sector_delay() +{ + if (this->cur_sector + 1 >= this->sectors_per_track[this->cur_track]) { + this->next_sector = 0; + return this->sector_delay + this->index_delay + this->addr_mark_delay; + } + + this->next_sector = this->cur_sector + 1; + + return this->sector_delay - this->addr_mark_delay; +} + +uint64_t MacSuperDrive::sector_data_delay() +{ + return MFM_SECT_DATA_DELAY; +} + +SectorHdr MacSuperDrive::current_sector_header() +{ + this->cur_sector = this->next_sector; + // MFM sector numbering is 1-based so we need to bump sector number - int sector_num = this->cur_sector + ((this->rec_method == RecMethod::MFM) ? 1 : 0); + uint8_t sector_num = this->cur_sector + ((this->rec_method == RecMethod::MFM) ? 1 : 0); return SectorHdr { this->cur_track, diff --git a/devices/floppy/superdrive.h b/devices/floppy/superdrive.h index 4994664..3c9d821 100644 --- a/devices/floppy/superdrive.h +++ b/devices/floppy/superdrive.h @@ -24,6 +24,7 @@ along with this program. If not, see . #ifndef MAC_SUPERDRIVE_H #define MAC_SUPERDRIVE_H +#include #include #include @@ -31,6 +32,19 @@ along with this program. If not, see . #include #include +// convert number of bytes to disk time = nbytes * bits_per_byte * 2 us +#define MFM_BYTES_TO_DISK_TIME(bytes) USECS_TO_NSECS((bytes) * 8 * 2) + +// timing constans for MFM disks +constexpr uint32_t MFM_INDX_MARK_DELAY = MFM_BYTES_TO_DISK_TIME(146); +constexpr uint32_t MFM_ADR_MARK_DELAY = MFM_BYTES_TO_DISK_TIME(22); +constexpr uint32_t MFM_SECT_DATA_DELAY = MFM_BYTES_TO_DISK_TIME(514); +constexpr uint32_t MFM_DD_SECTOR_DELAY = MFM_BYTES_TO_DISK_TIME(658); +constexpr uint32_t MFM_HD_SECTOR_DELAY = MFM_BYTES_TO_DISK_TIME(675); + +constexpr uint32_t MFM_DD_EOT_DELAY = MFM_BYTES_TO_DISK_TIME( 6250 - 182); +constexpr uint32_t MFM_HD_EOT_DELAY = MFM_BYTES_TO_DISK_TIME(12500 - 204); + namespace MacSuperdrive { /** Apple Drive status request addresses. */ @@ -74,10 +88,10 @@ enum RecMethod : int { }; typedef struct SectorHdr { - int track; - int side; - int sector; - int format; + int track; + int side; + int sector; + int format; } SectorHdr; class MacSuperDrive : public HWComponent { @@ -88,11 +102,16 @@ public: void command(uint8_t addr, uint8_t value); uint8_t status(uint8_t addr); int insert_disk(std::string& img_path, int write_flag); - double get_current_track_delay(); - double get_sector_delay(); void init_track_search(int pos); - SectorHdr next_sector_header(); + uint64_t sync_to_disk(); + uint64_t next_addr_mark_delay(uint8_t *next_sect_num); + uint64_t next_sector_delay(); + SectorHdr current_sector_header(); char* get_sector_data_ptr(int sector_num); + uint64_t sector_data_delay(); + + double get_current_track_delay(); + double get_address_mark_delay(); protected: void reset_params(); @@ -100,27 +119,38 @@ protected: void switch_drive_mode(int mode); private: - uint8_t has_disk; - uint8_t eject_latch; - uint8_t motor_stat; // spindle motor status: 1 - on, 0 - off - uint8_t drive_mode; // drive mode: 0 - GCR, 1 - MFM - uint8_t is_ready; - uint8_t track_zero; // 1 - if head is at track zero - int step_dir; // step direction -1/+1 - int cur_track; // track number the head is currently at - int cur_head; // current head number: 1 - upper, 0 - lower - int cur_sector; // current sector number + uint8_t has_disk; + uint8_t eject_latch; + uint8_t motor_stat; // spindle motor status: 1 - on, 0 - off + uint8_t drive_mode; // drive mode: 0 - GCR, 1 - MFM + uint8_t is_ready; + uint8_t track_zero; // 1 - if head is at track zero + int step_dir; // step direction -1/+1 + int cur_track; // track number the head is currently at + int cur_head; // current head number: 1 - upper, 0 - lower + int cur_sector; // current sector number + int next_sector; // next sector number + + uint64_t motor_on_time = 0; // time in ns the spindle motor was switched on + uint64_t track_start_time = 0; + uint64_t sector_start_time = 0; // physical parameters of the currently inserted disk - uint8_t media_kind; - uint8_t wr_protect; - uint8_t format_byte; - int rec_method; - int num_tracks; - int num_sides; - int sectors_per_track[80]; - int rpm_per_track[80]; - int track2lblk[80]; // convert track number to first logical block number + uint8_t media_kind; + uint8_t wr_protect; + uint8_t format_byte; + int rec_method; + int num_tracks; + int num_sides; + int sectors_per_track[80]; + int rpm_per_track[80]; + int track2lblk[80]; // convert track number to first logical block number + + // timing parameters for MFM disks + uint32_t index_delay; // number of ns needed to read the index mark + uint32_t addr_mark_delay; // number of ns needed to read an address index mark + uint32_t sector_delay; // number of ns needed to read a sector + uint32_t eot_delay; // number of ns until end of track gap std::unique_ptr img_conv; diff --git a/devices/floppy/swim3.cpp b/devices/floppy/swim3.cpp index 4cb8bd2..cdf9cd9 100644 --- a/devices/floppy/swim3.cpp +++ b/devices/floppy/swim3.cpp @@ -51,6 +51,8 @@ Swim3Ctrl::Swim3Ctrl() this->xfer_cnt = 0; this->first_sec = 0xFF; + this->cur_state = SWIM3_IDLE; + // Attach virtual Superdrive to the internal drive connector // TODO: make SWIM3/drive wiring user selectable this->int_drive = std::unique_ptr @@ -75,7 +77,7 @@ int Swim3Ctrl::device_postinit() uint8_t Swim3Ctrl::read(uint8_t reg_offset) { - uint8_t status_addr, status_val, old_int_flags, old_error; + uint8_t status_addr, rddata_val, old_int_flags, old_error; switch(reg_offset) { case Swim3Reg::Error: @@ -89,13 +91,13 @@ uint8_t Swim3Ctrl::read(uint8_t reg_offset) case Swim3Reg::Handshake_Mode1: if (this->mode_reg & 2) { // internal drive? status_addr = ((this->mode_reg & 0x20) >> 2) | (this->phase_lines & 7); - status_val = this->int_drive->status(status_addr) & 1; + rddata_val = this->int_drive->status(status_addr) & 1; - // transfer status_val to both bit 2 (RDDATA) and bit 3 (SENSE) + // transfer rddata_val to both bit 2 (RDDATA) and bit 3 (SENSE) // because those signals seem to be historically wired together - return ((status_val << 2) | (status_val << 3)); + return (rddata_val << 2) | (rddata_val << 3); } - return 4; + return 0xC; // report both RdData & Sense high case Swim3Reg::Interrupt_Flags: old_int_flags = this->int_flags; this->int_flags = 0; // read from this register clears all flags @@ -121,21 +123,27 @@ uint8_t Swim3Ctrl::read(uint8_t reg_offset) void Swim3Ctrl::write(uint8_t reg_offset, uint8_t value) { - uint8_t old_mode_reg; + uint8_t old_mode_reg, status_addr; switch(reg_offset) { + case Swim3Reg::Timer: + LOG_F(INFO, "SWIM3: writing %d to the Timer register", value); + break; case Swim3Reg::Param_Data: this->pram = value; break; case Swim3Reg::Phase: this->phase_lines = value & 0xF; - if (value & 8) { - if (this->mode_reg & 2) { // internal drive? + if (this->phase_lines & 8) { // CA3 aka LSTRB high -> sending a command to the drive + if (this->mode_reg & 2) { // if internal drive is selected this->int_drive->command( ((this->mode_reg & 0x20) >> 3) | (this->phase_lines & 3), (value >> 2) & 1 ); } + } else if (this->phase_lines == 4 && (this->mode_reg & 2)) { + status_addr = ((this->mode_reg & 0x20) >> 2) | (this->phase_lines & 7); + this->rd_line = this->int_drive->status(status_addr) & 1; } break; case Swim3Reg::Setup: @@ -278,53 +286,66 @@ void Swim3Ctrl::start_disk_access() this->mode_reg |= SWIM3_GO; LOG_F(9, "SWIM3: disk access started!"); - if (this->first_sec == 0xFF) { - // $FF means no sector to match -> - // generate ID_read interrups as long as the GO bit is set - this->int_drive->init_track_search(-1); // start at random sector - } else { - this->cur_sector = this->first_sec; + this->target_sect = this->first_sec; + + this->access_timer_id = TimerManager::get_instance()->add_oneshot_timer( + this->int_drive->sync_to_disk(), + [this]() { + this->cur_state = SWIM3_ADDR_MARK_SEARCH; + this->disk_access(); + } + ); +} + +void Swim3Ctrl::disk_access() +{ + MacSuperdrive::SectorHdr hdr; + uint64_t delay; + + switch(this->cur_state) { + case SWIM3_ADDR_MARK_SEARCH: + hdr = this->int_drive->current_sector_header(); + // update the corresponding SWIM3 registers + this->cur_track = ((hdr.side & 1) << 7) | (hdr.track & 0x7F); + this->cur_sector = 0x80 /* CRC/checksum valid */ | (hdr.sector & 0x7F); + this->format = hdr.format; + // generate ID_read interrupt + this->int_flags |= INT_ID_READ; + update_irq(); + if ((this->cur_sector & 0x7F) == this->target_sect) { + // sector matches -> transfer its data + this->cur_state = SWIM3_DATA_XFER; + delay = this->int_drive->sector_data_delay(); + } else { + // move to next address mark + this->cur_state = SWIM3_ADDR_MARK_SEARCH; + delay = this->int_drive->next_sector_delay(); + } + break; + case SWIM3_DATA_XFER: + // transfer sector data over DMA + this->dma_ch->push_data(this->int_drive->get_sector_data_ptr(this->cur_sector & 0x7F), 512); + if (--this->xfer_cnt == 0) { + this->stop_disk_access(); + // generate sector_done interrupt + this->int_flags |= INT_SECT_DONE; + update_irq(); + return; + } + this->cur_state = SWIM3_ADDR_MARK_SEARCH; + delay = this->int_drive->next_addr_mark_delay(&this->target_sect); + break; + default: + LOG_F(ERROR, "SWIM3: unknown disk access phase 0x%X", this->cur_state); + return; } - // HACK: figure out from bits in int_mask register which kind of disk access is requested - if (this->int_mask & INT_ID_READ) { // read address header - this->access_timer_id = TimerManager::get_instance()->add_cyclic_timer( - static_cast(this->int_drive->get_sector_delay() * NS_PER_SEC + 0.5f), - [this]() { - // get next sector's address field - MacSuperdrive::SectorHdr addr = this->int_drive->next_sector_header(); - // set up the corresponding SWIM3 registers - this->cur_track = ((addr.side & 1) << 7) | (addr.track & 0x7F); - this->cur_sector = 0x80 /* CRC/checksum valid */ | (addr.sector & 0x7F); - this->format = addr.format; - // generate ID_read interrupt - this->int_flags |= INT_ID_READ; - update_irq(); - } - ); - } else { // otherwise, read sector data - this->access_timer_id = TimerManager::get_instance()->add_cyclic_timer( - static_cast(this->int_drive->get_sector_delay() * NS_PER_SEC + 0.5f), - [this]() { - // transfer sector data over DMA - this->dma_ch->push_data(this->int_drive->get_sector_data_ptr(this->cur_sector), 512); - - // get next address field - MacSuperdrive::SectorHdr addr = this->int_drive->next_sector_header(); - // set up the corresponding SWIM3 registers - this->cur_track = ((addr.side & 1) << 7) | (addr.track & 0x7F); - this->cur_sector = 0x80 /* CRC/checksum valid */ | (addr.sector & 0x7F); - this->format = addr.format; - - if (--this->xfer_cnt == 0) { - this->stop_disk_access(); - // generate sector_done interrupt - this->int_flags |= INT_SECT_DONE; - update_irq(); - } - } - ); - } + this->access_timer_id = TimerManager::get_instance()->add_oneshot_timer( + delay, + [this]() { + this->disk_access(); + } + ); } void Swim3Ctrl::stop_disk_access() diff --git a/devices/floppy/swim3.h b/devices/floppy/swim3.h index 9813f67..ed235a4 100644 --- a/devices/floppy/swim3.h +++ b/devices/floppy/swim3.h @@ -69,6 +69,13 @@ enum { INT_SECT_DONE = 0x08, }; +// SWIM3 internal states. +enum { + SWIM3_IDLE, + SWIM3_ADDR_MARK_SEARCH, + SWIM3_DATA_XFER, +}; + class Swim3Ctrl : public HWComponent { public: Swim3Ctrl(); @@ -94,6 +101,7 @@ protected: void do_step(); void stop_stepping(); void start_disk_access(); + void disk_access(); void stop_disk_access(); private: @@ -112,10 +120,13 @@ private: uint8_t step_count; uint8_t cur_track; uint8_t cur_sector; + uint8_t target_sect; uint8_t format; // format byte from the last GCR/MFM address field uint8_t first_sec; uint8_t xfer_cnt; uint8_t gap_size; + uint8_t rd_line; + int cur_state; int step_timer_id = 0; int access_timer_id = 0;