New floppy access logic with improved timing.

This commit is contained in:
Maxim Poliakovski 2022-11-17 14:17:25 +01:00
parent 447941abe0
commit a4ff58e9ee
4 changed files with 270 additions and 101 deletions

View File

@ -21,6 +21,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
/** @file Macintosh Superdrive emulation. */
#include <core/timermanager.h>
#include <devices/floppy/floppyimg.h>
#include <devices/floppy/superdrive.h>
#include <loguru.hpp>
@ -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,

View File

@ -24,6 +24,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
#ifndef MAC_SUPERDRIVE_H
#define MAC_SUPERDRIVE_H
#include <core/timermanager.h>
#include <devices/common/hwcomponent.h>
#include <devices/floppy/floppyimg.h>
@ -31,6 +32,19 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
#include <memory>
#include <string>
// 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<FloppyImgConverter> img_conv;

View File

@ -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<MacSuperdrive::MacSuperDrive>
@ -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<uint64_t>(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<uint64_t>(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()

View File

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