mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
Merge pull request #161 from iflan/split-out-drivers
Split handling of nibble disks and WOZ disks into separate drivers
This commit is contained in:
commit
0be830784a
@ -132,15 +132,16 @@ const SEQUENCER_ROM_16 = [
|
||||
const SEQUENCER_ROM: Record<SupportedSectors, ReadonlyArray<byte>> = {
|
||||
13: SEQUENCER_ROM_13,
|
||||
16: SEQUENCER_ROM_16,
|
||||
};
|
||||
} as const;
|
||||
|
||||
/** Contents of the P5 ROM at 0xCnXX. */
|
||||
const BOOTSTRAP_ROM: Record<SupportedSectors, ReadonlyUint8Array> = {
|
||||
13: BOOTSTRAP_ROM_13,
|
||||
16: BOOTSTRAP_ROM_16,
|
||||
};
|
||||
} as const;
|
||||
|
||||
type LssClockCycle = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
type LssState = nibble;
|
||||
type Phase = 0 | 1 | 2 | 3;
|
||||
|
||||
/**
|
||||
@ -175,7 +176,7 @@ const PHASE_DELTA = [
|
||||
/**
|
||||
* State of the controller.
|
||||
*/
|
||||
interface ControllerState {
|
||||
interface ControllerState {
|
||||
/** Sectors supported by the controller. */
|
||||
sectors: SupportedSectors;
|
||||
|
||||
@ -201,6 +202,20 @@ const PHASE_DELTA = [
|
||||
bus: byte;
|
||||
}
|
||||
|
||||
/** Interface for drivers for various disk types. */
|
||||
interface DiskDriver {
|
||||
tick(): void;
|
||||
onQ6Low(): void;
|
||||
onQ6High(readMode: boolean): void;
|
||||
onDriveOn(): void;
|
||||
onDriveOff(): void;
|
||||
clampTrack(): void;
|
||||
getState(): DriverState;
|
||||
setState(state: DriverState): void;
|
||||
}
|
||||
|
||||
type DriverState = EmptyDriverState | NibbleDiskDriverState | WozDiskDriverState;
|
||||
|
||||
/** Callbacks triggered by events of the drive or controller. */
|
||||
export interface Callbacks {
|
||||
/** Called when a drive turns on or off. */
|
||||
@ -231,6 +246,7 @@ interface Drive {
|
||||
|
||||
interface DriveState {
|
||||
disk: FloppyDisk;
|
||||
driver: DriverState;
|
||||
readOnly: boolean;
|
||||
track: byte;
|
||||
head: byte;
|
||||
@ -242,7 +258,7 @@ interface DriveState {
|
||||
// TODO(flan): It's unclear whether reusing ControllerState here is a good idea.
|
||||
interface State {
|
||||
drives: DriveState[];
|
||||
skip: number;
|
||||
driver: DriverState[];
|
||||
controllerState: ControllerState;
|
||||
}
|
||||
|
||||
@ -255,7 +271,7 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
|
||||
const { encoding, metadata, readOnly } = disk;
|
||||
return {
|
||||
encoding,
|
||||
metadata: {...metadata},
|
||||
metadata: { ...metadata },
|
||||
readOnly,
|
||||
};
|
||||
}
|
||||
@ -296,61 +312,223 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
|
||||
throw new Error('Unknown drive state');
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
|
||||
*/
|
||||
export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
interface EmptyDriverState { }
|
||||
|
||||
private drives: Record<DriveNumber, Drive> = {
|
||||
1: { // Drive 1
|
||||
track: 0,
|
||||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false,
|
||||
},
|
||||
2: { // Drive 2
|
||||
track: 0,
|
||||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false,
|
||||
class EmptyDriver implements DiskDriver {
|
||||
constructor(private readonly drive: Drive) { }
|
||||
|
||||
tick(): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
onQ6Low(): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
onQ6High(_readMode: boolean): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
onDriveOn(): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
onDriveOff(): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
clampTrack(): void {
|
||||
// For empty drives, the emulator clamps the track to 0 to 34,
|
||||
// but real Disk II drives can seek past track 34 by at least a
|
||||
// half track, usually a full track. Some 3rd party drives can
|
||||
// seek to track 39.
|
||||
if (this.drive.track < 0) {
|
||||
this.drive.track = 0;
|
||||
}
|
||||
};
|
||||
|
||||
private disks: Record<DriveNumber, FloppyDisk> = {
|
||||
1: {
|
||||
encoding: NO_DISK,
|
||||
readOnly: false,
|
||||
metadata: { name: 'Disk 1' },
|
||||
},
|
||||
2: {
|
||||
encoding: NO_DISK,
|
||||
readOnly: false,
|
||||
metadata: { name: 'Disk 2' },
|
||||
if (this.drive.track > 34) {
|
||||
this.drive.track = 34;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private state: ControllerState;
|
||||
getState() {
|
||||
return {};
|
||||
}
|
||||
|
||||
setState(_state: EmptyDriverState): void {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BaseDiskDriver implements DiskDriver {
|
||||
constructor(
|
||||
protected readonly driveNumber: DriveNumber,
|
||||
protected readonly drive: Drive,
|
||||
protected readonly disk: NibbleDisk | WozDisk,
|
||||
protected readonly controller: ControllerState) { }
|
||||
|
||||
/** Called frequently to ensure the disk is spinning. */
|
||||
abstract tick(): void;
|
||||
|
||||
/** Called when Q6 is set LOW. */
|
||||
abstract onQ6Low(): void;
|
||||
|
||||
/** Called when Q6 is set HIGH. */
|
||||
abstract onQ6High(readMode: boolean): void;
|
||||
|
||||
/**
|
||||
* Called when drive is turned on. This is guaranteed to be called
|
||||
* only when the associated drive is toggled from off to on. This
|
||||
* is also guaranteed to be called when a new disk is inserted when
|
||||
* the drive is already on.
|
||||
*/
|
||||
abstract onDriveOn(): void;
|
||||
|
||||
/**
|
||||
* Called when drive is turned off. This is guaranteed to be called
|
||||
* only when the associated drive is toggled from on to off.
|
||||
*/
|
||||
abstract onDriveOff(): void;
|
||||
|
||||
debug(..._args: unknown[]) {
|
||||
// debug(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every time the head moves to clamp the track to a valid
|
||||
* range.
|
||||
*/
|
||||
abstract clampTrack(): void;
|
||||
|
||||
isOn(): boolean {
|
||||
return this.controller.on && this.controller.drive === this.driveNumber;
|
||||
}
|
||||
|
||||
isWriteProtected(): boolean {
|
||||
return this.drive.readOnly;
|
||||
}
|
||||
|
||||
abstract getState(): DriverState;
|
||||
|
||||
abstract setState(state: DriverState): void;
|
||||
}
|
||||
|
||||
interface NibbleDiskDriverState {
|
||||
skip: number;
|
||||
nibbleCount: number;
|
||||
}
|
||||
|
||||
class NibbleDiskDriver extends BaseDiskDriver {
|
||||
/**
|
||||
* When `1`, the next nibble will be available for read; when `0`,
|
||||
* the card is pretending to wait for data to be shifted in by the
|
||||
* sequencer.
|
||||
*/
|
||||
private skip = 0;
|
||||
/** Drive off timeout id or null. */
|
||||
private offTimeout: number | null = null;
|
||||
/** Current drive object. Must only be set by `updateActiveDrive()`. */
|
||||
private curDrive: Drive;
|
||||
/** Current disk object. Must only be set by `updateActiveDrive()`. */
|
||||
private curDisk: FloppyDisk;
|
||||
private skip: number = 0;
|
||||
/** Number of nibbles reads since the drive was turned on. */
|
||||
private nibbleCount: number = 0;
|
||||
|
||||
/** Nibbles read this on cycle */
|
||||
private nibbleCount = 0;
|
||||
constructor(
|
||||
driveNumber: DriveNumber,
|
||||
drive: Drive,
|
||||
readonly disk: NibbleDisk,
|
||||
controller: ControllerState,
|
||||
private readonly onDirty: () => void) {
|
||||
super(driveNumber, drive, disk, controller);
|
||||
}
|
||||
|
||||
tick(): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
onQ6Low(): void {
|
||||
const drive = this.drive;
|
||||
const disk = this.disk;
|
||||
if (this.isOn() && (this.skip || this.controller.q7)) {
|
||||
const track = disk.tracks[drive.track >> 2];
|
||||
if (track && track.length) {
|
||||
if (drive.head >= track.length) {
|
||||
drive.head = 0;
|
||||
}
|
||||
|
||||
if (this.controller.q7) {
|
||||
const writeProtected = disk.readOnly;
|
||||
if (!writeProtected) {
|
||||
track[drive.head] = this.controller.bus;
|
||||
drive.dirty = true;
|
||||
this.onDirty();
|
||||
}
|
||||
} else {
|
||||
this.controller.latch = track[drive.head];
|
||||
this.nibbleCount++;
|
||||
}
|
||||
|
||||
++drive.head;
|
||||
}
|
||||
} else {
|
||||
this.controller.latch = 0;
|
||||
}
|
||||
this.skip = (++this.skip % 2);
|
||||
}
|
||||
|
||||
onQ6High(readMode: boolean): void {
|
||||
const drive = this.drive;
|
||||
if (readMode && !this.controller.q7) {
|
||||
const writeProtected = drive.readOnly;
|
||||
if (writeProtected) {
|
||||
this.controller.latch = 0xff;
|
||||
this.debug('Setting readOnly');
|
||||
} else {
|
||||
this.controller.latch >>= 1;
|
||||
this.debug('Clearing readOnly');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDriveOn(): void {
|
||||
this.nibbleCount = 0;
|
||||
}
|
||||
|
||||
onDriveOff(): void {
|
||||
this.debug('nibbles read', this.nibbleCount);
|
||||
}
|
||||
|
||||
clampTrack(): void {
|
||||
// For NibbleDisks, the emulator clamps the track to the available
|
||||
// range.
|
||||
if (this.drive.track < 0) {
|
||||
this.drive.track = 0;
|
||||
}
|
||||
const lastTrack = 35 * 4 - 1;
|
||||
if (this.drive.track > lastTrack) {
|
||||
this.drive.track = lastTrack;
|
||||
}
|
||||
}
|
||||
|
||||
getState(): NibbleDiskDriverState {
|
||||
const { skip, nibbleCount } = this;
|
||||
return { skip, nibbleCount };
|
||||
}
|
||||
|
||||
setState(state: NibbleDiskDriverState) {
|
||||
this.skip = state.skip;
|
||||
this.nibbleCount = state.nibbleCount;
|
||||
}
|
||||
}
|
||||
|
||||
interface WozDiskDriverState {
|
||||
clock: LssClockCycle;
|
||||
state: LssState;
|
||||
lastCycles: number;
|
||||
zeros: number;
|
||||
}
|
||||
|
||||
class WozDiskDriver extends BaseDiskDriver {
|
||||
/** Logic state sequencer clock cycle. */
|
||||
private clock: LssClockCycle;
|
||||
/** Logic state sequencer state. */
|
||||
private state: LssState;
|
||||
/** Current CPU cycle count. */
|
||||
private lastCycles = 0;
|
||||
private lastCycles: number = 0;
|
||||
/**
|
||||
* Number of zeros read in a row. The Disk ][ can only read two zeros in a
|
||||
* row reliably; above that and the drive starts reporting garbage. See
|
||||
@ -358,47 +536,30 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
*/
|
||||
private zeros = 0;
|
||||
|
||||
private worker: Worker;
|
||||
constructor(
|
||||
driveNumber: DriveNumber,
|
||||
drive: Drive,
|
||||
readonly disk: WozDisk,
|
||||
controller: ControllerState,
|
||||
private readonly onDirty: () => void,
|
||||
private readonly io: Apple2IO) {
|
||||
super(driveNumber, drive, disk, controller);
|
||||
|
||||
/** Builds a new Disk ][ card. */
|
||||
constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors: SupportedSectors = 16) {
|
||||
this.debug('Disk ][');
|
||||
// From the example in UtA2e, p. 9-29, col. 1, para. 1., this is
|
||||
// essentially the start of the sequencer loop and produces
|
||||
// correctly synced nibbles immediately. Starting at state 0
|
||||
// would introduce a spurrious 1 in the latch at the beginning,
|
||||
// which requires reading several more sync bytes to sync up.
|
||||
this.state = 2;
|
||||
this.clock = 0;
|
||||
}
|
||||
|
||||
onDriveOn(): void {
|
||||
this.lastCycles = this.io.cycles();
|
||||
this.state = {
|
||||
sectors,
|
||||
bus: 0,
|
||||
latch: 0,
|
||||
drive: 1,
|
||||
on: false,
|
||||
q6: false,
|
||||
q7: false,
|
||||
clock: 0,
|
||||
// From the example in UtA2e, p. 9-29, col. 1, para. 1., this is
|
||||
// essentially the start of the sequencer loop and produces
|
||||
// correctly synced nibbles immediately. Starting at state 0
|
||||
// would introduce a spurrious 1 in the latch at the beginning,
|
||||
// which requires reading several more sync bytes to sync up.
|
||||
state: 2,
|
||||
};
|
||||
|
||||
this.updateActiveDrive();
|
||||
|
||||
this.initWorker();
|
||||
}
|
||||
|
||||
/** Updates the active drive based on the controller state. */
|
||||
private updateActiveDrive() {
|
||||
this.curDrive = this.drives[this.state.drive];
|
||||
this.curDisk = this.disks[this.state.drive];
|
||||
}
|
||||
|
||||
private debug(..._args: unknown[]) {
|
||||
// debug(..._args);
|
||||
}
|
||||
|
||||
public head(): number {
|
||||
return this.curDrive.head;
|
||||
onDriveOff(): void {
|
||||
// nothing
|
||||
}
|
||||
|
||||
/**
|
||||
@ -443,22 +604,27 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
let workCycles = (cycles - this.lastCycles) * 2;
|
||||
this.lastCycles = cycles;
|
||||
|
||||
if (!isWozDisk(this.curDisk)) {
|
||||
return;
|
||||
}
|
||||
const drive = this.drive;
|
||||
const disk = this.disk;
|
||||
const controller = this.controller;
|
||||
|
||||
// TODO(flan): Improve unformatted track behavior. The WOZ
|
||||
// documentation suggests using an empty track of 6400 bytes
|
||||
// (51,200 bits).
|
||||
const track =
|
||||
this.curDisk.rawTracks[this.curDisk.trackMap[this.curDrive.track]] || [0];
|
||||
|
||||
const state = this.state;
|
||||
disk.rawTracks[disk.trackMap[drive.track]] || [0];
|
||||
|
||||
while (workCycles-- > 0) {
|
||||
let pulse: number = 0;
|
||||
if (state.clock === 4) {
|
||||
pulse = track[this.curDrive.head];
|
||||
if (this.clock === 4) {
|
||||
pulse = track[drive.head];
|
||||
if (!pulse) {
|
||||
// More than 2 zeros can not be read reliably.
|
||||
// TODO(flan): Revisit with the new MC3470
|
||||
// suggested 4-bit window behavior.
|
||||
if (++this.zeros > 2) {
|
||||
pulse = Math.random() >= 0.5 ? 1 : 0;
|
||||
const r = Math.random();
|
||||
pulse = r >= 0.5 ? 1 : 0;
|
||||
}
|
||||
} else {
|
||||
this.zeros = 0;
|
||||
@ -467,91 +633,197 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
|
||||
let idx = 0;
|
||||
idx |= pulse ? 0x00 : 0x01;
|
||||
idx |= state.latch & 0x80 ? 0x02 : 0x00;
|
||||
idx |= state.q6 ? 0x04 : 0x00;
|
||||
idx |= state.q7 ? 0x08 : 0x00;
|
||||
idx |= state.state << 4;
|
||||
idx |= controller.latch & 0x80 ? 0x02 : 0x00;
|
||||
idx |= controller.q6 ? 0x04 : 0x00;
|
||||
idx |= controller.q7 ? 0x08 : 0x00;
|
||||
idx |= this.state << 4;
|
||||
|
||||
const command = SEQUENCER_ROM[this.sectors][idx];
|
||||
const command = SEQUENCER_ROM[controller.sectors][idx];
|
||||
|
||||
this.debug(`clock: ${state.clock} state: ${toHex(state.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${state.q6} latch: ${toHex(state.latch)}`);
|
||||
this.debug(`clock: ${this.clock} state: ${toHex(this.state)} pulse: ${pulse} command: ${toHex(command)} q6: ${controller.q6} latch: ${toHex(controller.latch)}`);
|
||||
|
||||
switch (command & 0xf) {
|
||||
case 0x0: // CLR
|
||||
state.latch = 0;
|
||||
controller.latch = 0;
|
||||
break;
|
||||
case 0x8: // NOP
|
||||
break;
|
||||
case 0x9: // SL0
|
||||
state.latch = (state.latch << 1) & 0xff;
|
||||
controller.latch = (controller.latch << 1) & 0xff;
|
||||
break;
|
||||
case 0xA: // SR
|
||||
state.latch >>= 1;
|
||||
if (this.curDrive.readOnly) {
|
||||
state.latch |= 0x80;
|
||||
controller.latch >>= 1;
|
||||
if (this.isWriteProtected()) {
|
||||
controller.latch |= 0x80;
|
||||
}
|
||||
break;
|
||||
case 0xB: // LD
|
||||
state.latch = state.bus;
|
||||
this.debug('Loading', toHex(state.latch), 'from bus');
|
||||
controller.latch = controller.bus;
|
||||
this.debug('Loading', toHex(controller.latch), 'from bus');
|
||||
break;
|
||||
case 0xD: // SL1
|
||||
state.latch = ((state.latch << 1) | 0x01) & 0xff;
|
||||
controller.latch = ((controller.latch << 1) | 0x01) & 0xff;
|
||||
break;
|
||||
default:
|
||||
this.debug(`unknown command: ${toHex(command & 0xf)}`);
|
||||
}
|
||||
state.state = (command >> 4 & 0xF) as nibble;
|
||||
this.state = (command >> 4 & 0xF) as LssState;
|
||||
|
||||
if (state.clock === 4) {
|
||||
if (state.on) {
|
||||
if (state.q7) {
|
||||
track[this.curDrive.head] = state.state & 0x8 ? 0x01 : 0x00;
|
||||
this.debug('Wrote', state.state & 0x8 ? 0x01 : 0x00);
|
||||
if (this.clock === 4) {
|
||||
if (this.isOn()) {
|
||||
if (controller.q7) {
|
||||
// TODO(flan): This assumes that writes are happening in
|
||||
// a "friendly" way, namely where the track was originally
|
||||
// written. To do this correctly, the virtual head should
|
||||
// actually keep track of the current quarter track plus
|
||||
// the one on each side. Then, when writing, it should
|
||||
// check that all three are actually the same rawTrack. If
|
||||
// they aren't, then the trackMap has to be updated as
|
||||
// well.
|
||||
track[drive.head] = this.state & 0x8 ? 0x01 : 0x00;
|
||||
this.debug('Wrote', this.state & 0x8 ? 0x01 : 0x00);
|
||||
drive.dirty = true;
|
||||
this.onDirty();
|
||||
}
|
||||
|
||||
if (++this.curDrive.head >= track.length) {
|
||||
this.curDrive.head = 0;
|
||||
if (++drive.head >= track.length) {
|
||||
drive.head = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (++state.clock > 7) {
|
||||
state.clock = 0;
|
||||
if (++this.clock > 7) {
|
||||
this.clock = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only called for non-WOZ disks
|
||||
private readWriteNext() {
|
||||
if (!isNibbleDisk(this.curDisk)) {
|
||||
return;
|
||||
}
|
||||
const state = this.state;
|
||||
if (state.on && (this.skip || state.q7)) {
|
||||
const track = this.curDisk.tracks[this.curDrive.track >> 2];
|
||||
if (track && track.length) {
|
||||
if (this.curDrive.head >= track.length) {
|
||||
this.curDrive.head = 0;
|
||||
}
|
||||
tick(): void {
|
||||
this.moveHead();
|
||||
}
|
||||
|
||||
if (state.q7) {
|
||||
if (!this.curDrive.readOnly) {
|
||||
track[this.curDrive.head] = state.bus;
|
||||
if (!this.curDrive.dirty) {
|
||||
this.updateDirty(state.drive, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state.latch = track[this.curDrive.head];
|
||||
}
|
||||
onQ6High(_readMode: boolean): void {
|
||||
// nothing?
|
||||
}
|
||||
|
||||
++this.curDrive.head;
|
||||
}
|
||||
} else {
|
||||
state.latch = 0;
|
||||
onQ6Low(): void {
|
||||
// nothing?
|
||||
}
|
||||
|
||||
clampTrack(): void {
|
||||
// For NibbleDisks, the emulator clamps the track to the available
|
||||
// range.
|
||||
if (this.drive.track < 0) {
|
||||
this.drive.track = 0;
|
||||
}
|
||||
this.skip = (++this.skip % 2);
|
||||
const lastTrack = this.disk.trackMap.length - 1;
|
||||
if (this.drive.track > lastTrack) {
|
||||
this.drive.track = lastTrack;
|
||||
}
|
||||
}
|
||||
|
||||
getState(): WozDiskDriverState {
|
||||
const { clock, state, lastCycles, zeros } = this;
|
||||
return { clock, state, lastCycles, zeros };
|
||||
}
|
||||
|
||||
setState(state: WozDiskDriverState) {
|
||||
this.clock = state.clock;
|
||||
this.state = state.state;
|
||||
this.lastCycles = state.lastCycles;
|
||||
this.zeros = state.zeros;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
|
||||
*/
|
||||
export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
|
||||
private drives: Record<DriveNumber, Drive> = {
|
||||
1: { // Drive 1
|
||||
track: 0,
|
||||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false,
|
||||
},
|
||||
2: { // Drive 2
|
||||
track: 0,
|
||||
head: 0,
|
||||
phase: 0,
|
||||
readOnly: false,
|
||||
dirty: false,
|
||||
}
|
||||
};
|
||||
|
||||
private disks: Record<DriveNumber, FloppyDisk> = {
|
||||
1: {
|
||||
encoding: NO_DISK,
|
||||
readOnly: false,
|
||||
metadata: { name: 'Disk 1' },
|
||||
},
|
||||
2: {
|
||||
encoding: NO_DISK,
|
||||
readOnly: false,
|
||||
metadata: { name: 'Disk 2' },
|
||||
}
|
||||
};
|
||||
|
||||
private driver: Record<DriveNumber, DiskDriver> = {
|
||||
1: new EmptyDriver(this.drives[1]),
|
||||
2: new EmptyDriver(this.drives[2]),
|
||||
};
|
||||
|
||||
private state: ControllerState;
|
||||
|
||||
/** Drive off timeout id or null. */
|
||||
private offTimeout: number | null = null;
|
||||
/** Current drive object. Must only be set by `updateActiveDrive()`. */
|
||||
private curDrive: Drive;
|
||||
/** Current driver object. Must only be set by `updateAcivetDrive()`. */
|
||||
private curDriver: DiskDriver;
|
||||
|
||||
private worker: Worker;
|
||||
|
||||
/** Builds a new Disk ][ card. */
|
||||
constructor(private io: Apple2IO, private callbacks: Callbacks, private sectors: SupportedSectors = 16) {
|
||||
this.debug('Disk ][');
|
||||
|
||||
this.state = {
|
||||
sectors,
|
||||
bus: 0,
|
||||
latch: 0,
|
||||
drive: 1,
|
||||
on: false,
|
||||
q6: false,
|
||||
q7: false,
|
||||
clock: 0,
|
||||
// From the example in UtA2e, p. 9-29, col. 1, para. 1., this is
|
||||
// essentially the start of the sequencer loop and produces
|
||||
// correctly synced nibbles immediately. Starting at state 0
|
||||
// would introduce a spurrious 1 in the latch at the beginning,
|
||||
// which requires reading several more sync bytes to sync up.
|
||||
state: 2,
|
||||
};
|
||||
|
||||
this.updateActiveDrive();
|
||||
|
||||
this.initWorker();
|
||||
}
|
||||
|
||||
/** Updates the active drive based on the controller state. */
|
||||
private updateActiveDrive() {
|
||||
this.curDrive = this.drives[this.state.drive];
|
||||
this.curDriver = this.driver[this.state.drive];
|
||||
}
|
||||
|
||||
private debug(..._args: unknown[]) {
|
||||
// debug(..._args);
|
||||
}
|
||||
|
||||
public head(): number {
|
||||
return this.curDrive.head;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -579,21 +851,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
this.curDrive.phase = phase;
|
||||
}
|
||||
|
||||
// The emulator clamps the track to the valid track range available
|
||||
// in the image, but real Disk II drives can seek past track 34 by
|
||||
// at least a half track, usually a full track. Some 3rd party
|
||||
// drives can seek to track 39.
|
||||
const maxTrack = isNibbleDisk(this.curDisk)
|
||||
? this.curDisk.tracks.length * 4 - 1
|
||||
: (isWozDisk(this.curDisk)
|
||||
? this.curDisk.trackMap.length - 1
|
||||
: 0);
|
||||
if (this.curDrive.track > maxTrack) {
|
||||
this.curDrive.track = maxTrack;
|
||||
}
|
||||
if (this.curDrive.track < 0x0) {
|
||||
this.curDrive.track = 0x0;
|
||||
}
|
||||
this.curDriver.clampTrack();
|
||||
|
||||
// debug(
|
||||
// 'Drive', _drive, 'track', toHex(_cur.track >> 2) + '.' + (_cur.track & 0x3),
|
||||
@ -641,7 +899,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
this.debug('Drive Off');
|
||||
state.on = false;
|
||||
this.callbacks.driveLight(state.drive, false);
|
||||
this.debug('nibbles read', this.nibbleCount);
|
||||
this.curDriver.onDriveOff();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
@ -654,10 +912,9 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
}
|
||||
if (!state.on) {
|
||||
this.debug('Drive On');
|
||||
this.nibbleCount = 0;
|
||||
state.on = true;
|
||||
this.lastCycles = this.io.cycles();
|
||||
this.callbacks.driveLight(state.drive, true);
|
||||
this.curDriver.onDriveOn();
|
||||
}
|
||||
break;
|
||||
|
||||
@ -682,30 +939,12 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
|
||||
case LOC.DRIVEREAD: // 0x0c (Q6L) Shift
|
||||
state.q6 = false;
|
||||
if (state.q7) {
|
||||
this.debug('clearing _q6/SHIFT');
|
||||
}
|
||||
if (isNibbleDisk(this.curDisk)) {
|
||||
this.readWriteNext();
|
||||
}
|
||||
this.curDriver.onQ6Low();
|
||||
break;
|
||||
|
||||
case LOC.DRIVEWRITE: // 0x0d (Q6H) LOAD
|
||||
state.q6 = true;
|
||||
if (state.q7) {
|
||||
this.debug('setting _q6/LOAD');
|
||||
}
|
||||
if (isNibbleDisk(this.curDisk)) {
|
||||
if (readMode && !state.q7) {
|
||||
if (this.curDrive.readOnly) {
|
||||
state.latch = 0xff;
|
||||
this.debug('Setting readOnly');
|
||||
} else {
|
||||
state.latch = state.latch >> 1;
|
||||
this.debug('Clearing readOnly');
|
||||
}
|
||||
}
|
||||
}
|
||||
this.curDriver.onQ6High(readMode);
|
||||
break;
|
||||
|
||||
case LOC.DRIVEREADMODE: // 0x0e (Q7L)
|
||||
@ -721,7 +960,7 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
break;
|
||||
}
|
||||
|
||||
this.moveHead();
|
||||
this.tick();
|
||||
|
||||
if (readMode) {
|
||||
// According to UtAIIe, p. 9-13 to 9-14, any even address can be
|
||||
@ -729,9 +968,6 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
// also cause conflicts with the disk controller commands.
|
||||
if ((off & 0x01) === 0) {
|
||||
result = state.latch;
|
||||
if (result & 0x80) {
|
||||
this.nibbleCount++;
|
||||
}
|
||||
} else {
|
||||
result = 0;
|
||||
}
|
||||
@ -775,27 +1011,29 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
}
|
||||
|
||||
tick() {
|
||||
this.moveHead();
|
||||
this.curDriver.tick();
|
||||
}
|
||||
|
||||
private getDriveState(drive: DriveNumber): DriveState {
|
||||
const curDrive = this.drives[drive];
|
||||
const curDisk = this.disks[drive];
|
||||
const curDriver = this.driver[drive];
|
||||
const { readOnly, track, head, phase, dirty } = curDrive;
|
||||
return {
|
||||
disk: getDiskState(curDisk),
|
||||
driver: curDriver.getState(),
|
||||
readOnly,
|
||||
track,
|
||||
head,
|
||||
phase,
|
||||
dirty,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
getState(): State {
|
||||
const result = {
|
||||
drives: [] as DriveState[],
|
||||
skip: this.skip,
|
||||
driver: [] as DriverState[],
|
||||
controllerState: { ...this.state },
|
||||
};
|
||||
result.drives[1] = this.getDriveState(1);
|
||||
@ -813,12 +1051,13 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
readOnly,
|
||||
dirty,
|
||||
};
|
||||
this.disks[drive] = getDiskState(state.disk);
|
||||
const disk = getDiskState(state.disk);
|
||||
this.setDiskInternal(drive, disk);
|
||||
this.driver[drive].setState(state.driver);
|
||||
}
|
||||
|
||||
|
||||
|
||||
setState(state: State) {
|
||||
this.skip = state.skip;
|
||||
this.state = { ...state.controllerState };
|
||||
for (const d of DRIVE_NUMBERS) {
|
||||
this.setDriveState(d, state.drives[d]);
|
||||
@ -957,12 +1196,38 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||
}
|
||||
}
|
||||
|
||||
private insertDisk(drive: DriveNumber, disk: FloppyDisk) {
|
||||
private setDiskInternal(drive: DriveNumber, disk: FloppyDisk) {
|
||||
this.disks[drive] = disk;
|
||||
this.drives[drive].head = 0;
|
||||
if (isNoFloppyDisk(disk)) {
|
||||
this.driver[drive] = new EmptyDriver(this.drives[drive]);
|
||||
} else if (isNibbleDisk(disk)) {
|
||||
this.driver[drive] =
|
||||
new NibbleDiskDriver(
|
||||
drive,
|
||||
this.drives[drive],
|
||||
disk,
|
||||
this.state,
|
||||
() => this.updateDirty(drive, true));
|
||||
} else if (isWozDisk(disk)) {
|
||||
this.driver[drive] =
|
||||
new WozDiskDriver(
|
||||
drive,
|
||||
this.drives[drive],
|
||||
disk,
|
||||
this.state,
|
||||
() => this.updateDirty(drive, true),
|
||||
this.io);
|
||||
} else {
|
||||
throw new Error(`Unknown disk format ${disk.encoding}`);
|
||||
}
|
||||
this.updateActiveDrive();
|
||||
}
|
||||
|
||||
private insertDisk(drive: DriveNumber, disk: FloppyDisk) {
|
||||
this.setDiskInternal(drive, disk);
|
||||
this.drives[drive].head = 0;
|
||||
const { name, side } = disk.metadata;
|
||||
this.updateDirty(drive, true);
|
||||
this.updateDirty(drive, this.drives[drive].dirty);
|
||||
this.callbacks.label(drive, name, side);
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ describe('DiskII', () => {
|
||||
|
||||
const state = diskII.getState();
|
||||
// These are just arbitrary changes, not an exhaustive list of fields.
|
||||
state.skip = 1;
|
||||
(state.drives[1].driver as {skip:number}).skip = 1;
|
||||
state.controllerState.drive = 2;
|
||||
state.controllerState.latch = 0x42;
|
||||
state.controllerState.on = true;
|
||||
@ -97,7 +97,7 @@ describe('DiskII', () => {
|
||||
expect(callbacks.label).toHaveBeenCalledWith(2, 'Disk 2', undefined);
|
||||
|
||||
expect(callbacks.dirty).toHaveBeenCalledTimes(2);
|
||||
expect(callbacks.dirty).toHaveBeenCalledWith(1, true);
|
||||
expect(callbacks.dirty).toHaveBeenCalledWith(1, false);
|
||||
expect(callbacks.dirty).toHaveBeenCalledWith(2, false);
|
||||
});
|
||||
|
||||
@ -824,7 +824,7 @@ class TestDiskReader {
|
||||
|
||||
rawTracks() {
|
||||
// NOTE(flan): Hack to access private properties.
|
||||
const disk = (this.diskII as unknown as { curDisk: WozDisk }).curDisk;
|
||||
const disk = (this.diskII as unknown as { disks: WozDisk[] }).disks[1];
|
||||
const result: Uint8Array[] = [];
|
||||
for (let i = 0; i < disk.rawTracks.length; i++) {
|
||||
result[i] = disk.rawTracks[i].slice(0);
|
||||
|
Loading…
Reference in New Issue
Block a user