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;