Split drivers into different files
Before, all of the disk drivers were in the `disk2.ts` file. With this change, they are now in separate files with a common `types.ts` file for shared types. Now, none of the drives depend on the `disk2.ts` except the WOZ driver that needs access to the sequencer rom. (This may be moved in a future refactoring.)
This commit is contained in:
parent
630a3e9d38
commit
a7bf5d025d
|
@ -2,7 +2,6 @@ import { base64_encode } from '../base64';
|
||||||
import type {
|
import type {
|
||||||
byte,
|
byte,
|
||||||
Card,
|
Card,
|
||||||
nibble,
|
|
||||||
ReadonlyUint8Array
|
ReadonlyUint8Array
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
|
@ -10,7 +9,7 @@ import {
|
||||||
DISK_PROCESSED, DriveNumber, DRIVE_NUMBERS, FloppyDisk,
|
DISK_PROCESSED, DriveNumber, DRIVE_NUMBERS, FloppyDisk,
|
||||||
FloppyFormat, FormatWorkerMessage,
|
FloppyFormat, FormatWorkerMessage,
|
||||||
FormatWorkerResponse, isNibbleDisk, isNoFloppyDisk, isWozDisk, JSONDisk, MassStorage,
|
FormatWorkerResponse, isNibbleDisk, isNoFloppyDisk, isWozDisk, JSONDisk, MassStorage,
|
||||||
MassStorageData, NibbleDisk, NibbleFormat, NoFloppyDisk, NO_DISK, PROCESS_BINARY, PROCESS_JSON, PROCESS_JSON_DISK, SupportedSectors, WozDisk
|
MassStorageData, NibbleDisk, NibbleFormat, NO_DISK, PROCESS_BINARY, PROCESS_JSON, PROCESS_JSON_DISK, SupportedSectors, WozDisk
|
||||||
} from '../formats/types';
|
} from '../formats/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -19,11 +18,16 @@ import {
|
||||||
} from '../formats/create_disk';
|
} from '../formats/create_disk';
|
||||||
|
|
||||||
import { jsonDecode, jsonEncode, readSector, _D13O, _DO, _PO } from '../formats/format_utils';
|
import { jsonDecode, jsonEncode, readSector, _D13O, _DO, _PO } from '../formats/format_utils';
|
||||||
import { toHex } from '../util';
|
|
||||||
|
|
||||||
import Apple2IO from '../apple2io';
|
import Apple2IO from '../apple2io';
|
||||||
|
|
||||||
import { BOOTSTRAP_ROM_13, BOOTSTRAP_ROM_16 } from '../roms/cards/disk2';
|
import { BOOTSTRAP_ROM_13, BOOTSTRAP_ROM_16 } from '../roms/cards/disk2';
|
||||||
|
|
||||||
|
import { EmptyDriver } from './drivers/EmptyDriver';
|
||||||
|
import { NibbleDiskDriver } from './drivers/NibbleDiskDriver';
|
||||||
|
import { ControllerState, DiskDriver, Drive, DriverState, Phase } from './drivers/types';
|
||||||
|
import { WozDiskDriver } from './drivers/WozDiskDriver';
|
||||||
|
|
||||||
/** Softswitch locations */
|
/** Softswitch locations */
|
||||||
const LOC = {
|
const LOC = {
|
||||||
// Disk II Controller Commands
|
// Disk II Controller Commands
|
||||||
|
@ -111,7 +115,7 @@ const SEQUENCER_ROM_16 = [
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** Contents of the P6 sequencer ROM. */
|
/** Contents of the P6 sequencer ROM. */
|
||||||
const SEQUENCER_ROM: Record<SupportedSectors, ReadonlyArray<byte>> = {
|
export const SEQUENCER_ROM: Record<SupportedSectors, ReadonlyArray<byte>> = {
|
||||||
13: SEQUENCER_ROM_13,
|
13: SEQUENCER_ROM_13,
|
||||||
16: SEQUENCER_ROM_16,
|
16: SEQUENCER_ROM_16,
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -122,10 +126,6 @@ const BOOTSTRAP_ROM: Record<SupportedSectors, ReadonlyUint8Array> = {
|
||||||
16: BOOTSTRAP_ROM_16,
|
16: BOOTSTRAP_ROM_16,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type LssClockCycle = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
|
||||||
type LssState = nibble;
|
|
||||||
type Phase = 0 | 1 | 2 | 3;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How far the head moves, in quarter tracks, when in phase X and phase Y is
|
* How far the head moves, in quarter tracks, when in phase X and phase Y is
|
||||||
* activated. For example, if in phase 0 (top row), turning on phase 3 would
|
* activated. For example, if in phase 0 (top row), turning on phase 3 would
|
||||||
|
@ -155,49 +155,6 @@ const PHASE_DELTA = [
|
||||||
[1, -2, -1, 0]
|
[1, -2, -1, 0]
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* State of the controller.
|
|
||||||
*/
|
|
||||||
interface ControllerState {
|
|
||||||
/** Sectors supported by the controller. */
|
|
||||||
sectors: SupportedSectors;
|
|
||||||
|
|
||||||
/** Is the active drive powered on? */
|
|
||||||
on: boolean;
|
|
||||||
|
|
||||||
/** The active drive. */
|
|
||||||
driveNo: DriveNumber;
|
|
||||||
|
|
||||||
/** The 8-cycle LSS clock. */
|
|
||||||
clock: LssClockCycle;
|
|
||||||
/** Current state of the Logic State Sequencer. */
|
|
||||||
state: nibble;
|
|
||||||
|
|
||||||
/** Q6 (Shift/Load) */
|
|
||||||
q6: boolean;
|
|
||||||
/** Q7 (Read/Write) */
|
|
||||||
q7: boolean;
|
|
||||||
|
|
||||||
/** Last data from the disk drive. */
|
|
||||||
latch: byte;
|
|
||||||
/** Last data written by the CPU to card softswitch 0x8D. */
|
|
||||||
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. */
|
/** Callbacks triggered by events of the drive or controller. */
|
||||||
export interface Callbacks {
|
export interface Callbacks {
|
||||||
/** Called when a drive turns on or off. */
|
/** Called when a drive turns on or off. */
|
||||||
|
@ -212,20 +169,6 @@ export interface Callbacks {
|
||||||
label: (driveNo: DriveNumber, name?: string, side?: string) => void;
|
label: (driveNo: DriveNumber, name?: string, side?: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Common information for Nibble and WOZ disks. */
|
|
||||||
interface Drive {
|
|
||||||
/** Whether the drive write protect is on. */
|
|
||||||
readOnly: boolean;
|
|
||||||
/** Quarter track position of read/write head. */
|
|
||||||
track: byte;
|
|
||||||
/** Position of the head on the track. */
|
|
||||||
head: byte;
|
|
||||||
/** Current active coil in the head stepper motor. */
|
|
||||||
phase: Phase;
|
|
||||||
/** Whether the drive has been written to since it was loaded. */
|
|
||||||
dirty: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DriveState {
|
interface DriveState {
|
||||||
disk: FloppyDisk;
|
disk: FloppyDisk;
|
||||||
driver: DriverState;
|
driver: DriverState;
|
||||||
|
@ -237,17 +180,11 @@ interface DriveState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** State of the controller for saving/restoring. */
|
/** State of the controller for saving/restoring. */
|
||||||
// TODO(flan): It's unclear whether reusing ControllerState here is a good idea.
|
|
||||||
interface State {
|
interface State {
|
||||||
drives: DriveState[];
|
drives: DriveState[];
|
||||||
driver: DriverState[];
|
|
||||||
controllerState: ControllerState;
|
controllerState: ControllerState;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDiskState(disk: NoFloppyDisk): NoFloppyDisk;
|
|
||||||
function getDiskState(disk: NibbleDisk): NibbleDisk;
|
|
||||||
function getDiskState(disk: WozDisk): WozDisk;
|
|
||||||
function getDiskState(disk: FloppyDisk): FloppyDisk;
|
|
||||||
function getDiskState(disk: FloppyDisk): FloppyDisk {
|
function getDiskState(disk: FloppyDisk): FloppyDisk {
|
||||||
if (isNoFloppyDisk(disk)) {
|
if (isNoFloppyDisk(disk)) {
|
||||||
const { encoding, metadata, readOnly } = disk;
|
const { encoding, metadata, readOnly } = disk;
|
||||||
|
@ -294,429 +231,6 @@ function getDiskState(disk: FloppyDisk): FloppyDisk {
|
||||||
throw new Error('Unknown drive state');
|
throw new Error('Unknown drive state');
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EmptyDriverState { }
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (this.drive.track > 34) {
|
|
||||||
this.drive.track = 34;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getState() {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(_state: EmptyDriverState): void {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class BaseDiskDriver implements DiskDriver {
|
|
||||||
constructor(
|
|
||||||
protected readonly driveNo: 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.driveNo === this.driveNo;
|
|
||||||
}
|
|
||||||
|
|
||||||
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: number = 0;
|
|
||||||
/** Number of nibbles reads since the drive was turned on. */
|
|
||||||
private nibbleCount: number = 0;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
driveNo: DriveNumber,
|
|
||||||
drive: Drive,
|
|
||||||
readonly disk: NibbleDisk,
|
|
||||||
controller: ControllerState,
|
|
||||||
private readonly onDirty: () => void) {
|
|
||||||
super(driveNo, 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: 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
|
|
||||||
* "Freaking Out Like a MC3470" in the WOZ spec.
|
|
||||||
*/
|
|
||||||
private zeros = 0;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
driveNo: DriveNumber,
|
|
||||||
drive: Drive,
|
|
||||||
readonly disk: WozDisk,
|
|
||||||
controller: ControllerState,
|
|
||||||
private readonly onDirty: () => void,
|
|
||||||
private readonly io: Apple2IO) {
|
|
||||||
super(driveNo, drive, disk, controller);
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
onDriveOff(): void {
|
|
||||||
// nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Spin the disk under the read/write head for WOZ images.
|
|
||||||
*
|
|
||||||
* This implementation emulates every clock cycle of the 2 MHz
|
|
||||||
* sequencer since the last time it was called in order to
|
|
||||||
* determine the current state. Because this is called on
|
|
||||||
* every access to the softswitches, the data in the latch
|
|
||||||
* will be correct on every read.
|
|
||||||
*
|
|
||||||
* The emulation of the disk makes a few simplifying assumptions:
|
|
||||||
*
|
|
||||||
* * The motor turns on instantly.
|
|
||||||
* * The head moves tracks instantly.
|
|
||||||
* * The length (in bits) of each track of the WOZ image
|
|
||||||
* represents one full rotation of the disk and that each
|
|
||||||
* bit is evenly spaced.
|
|
||||||
* * Writing will not change the track length. This means
|
|
||||||
* that short tracks stay short.
|
|
||||||
* * The read head picks up the next bit when the sequencer
|
|
||||||
* clock === 4.
|
|
||||||
* * Head position X on track T is equivalent to head position
|
|
||||||
* X on track T′. (This is not the recommendation in the WOZ
|
|
||||||
* spec.)
|
|
||||||
* * Unspecified tracks contain a single zero bit. (A very
|
|
||||||
* short track, indeed!)
|
|
||||||
* * Two zero bits are sufficient to cause the MC3470 to freak
|
|
||||||
* out. When freaking out, it returns 0 and 1 with equal
|
|
||||||
* probability.
|
|
||||||
* * Any softswitch changes happen before `moveHead`. This is
|
|
||||||
* important because it means that if the clock is ever
|
|
||||||
* advanced more than one cycle between calls, the
|
|
||||||
* softswitch changes will appear to happen at the very
|
|
||||||
* beginning, not just before the last cycle.
|
|
||||||
*/
|
|
||||||
private moveHead() {
|
|
||||||
// TODO(flan): Short-circuit if the drive is not on.
|
|
||||||
const cycles = this.io.cycles();
|
|
||||||
|
|
||||||
// Spin the disk the number of elapsed cycles since last call
|
|
||||||
let workCycles = (cycles - this.lastCycles) * 2;
|
|
||||||
this.lastCycles = cycles;
|
|
||||||
|
|
||||||
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 =
|
|
||||||
disk.rawTracks[disk.trackMap[drive.track]] || [0];
|
|
||||||
|
|
||||||
while (workCycles-- > 0) {
|
|
||||||
let pulse: number = 0;
|
|
||||||
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) {
|
|
||||||
const r = Math.random();
|
|
||||||
pulse = r >= 0.5 ? 1 : 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.zeros = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let idx = 0;
|
|
||||||
idx |= pulse ? 0x00 : 0x01;
|
|
||||||
idx |= controller.latch & 0x80 ? 0x02 : 0x00;
|
|
||||||
idx |= controller.q6 ? 0x04 : 0x00;
|
|
||||||
idx |= controller.q7 ? 0x08 : 0x00;
|
|
||||||
idx |= this.state << 4;
|
|
||||||
|
|
||||||
const command = SEQUENCER_ROM[controller.sectors][idx];
|
|
||||||
|
|
||||||
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
|
|
||||||
controller.latch = 0;
|
|
||||||
break;
|
|
||||||
case 0x8: // NOP
|
|
||||||
break;
|
|
||||||
case 0x9: // SL0
|
|
||||||
controller.latch = (controller.latch << 1) & 0xff;
|
|
||||||
break;
|
|
||||||
case 0xA: // SR
|
|
||||||
controller.latch >>= 1;
|
|
||||||
if (this.isWriteProtected()) {
|
|
||||||
controller.latch |= 0x80;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 0xB: // LD
|
|
||||||
controller.latch = controller.bus;
|
|
||||||
this.debug('Loading', toHex(controller.latch), 'from bus');
|
|
||||||
break;
|
|
||||||
case 0xD: // SL1
|
|
||||||
controller.latch = ((controller.latch << 1) | 0x01) & 0xff;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.debug(`unknown command: ${toHex(command & 0xf)}`);
|
|
||||||
}
|
|
||||||
this.state = (command >> 4 & 0xF) as LssState;
|
|
||||||
|
|
||||||
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 (++drive.head >= track.length) {
|
|
||||||
drive.head = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (++this.clock > 7) {
|
|
||||||
this.clock = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tick(): void {
|
|
||||||
this.moveHead();
|
|
||||||
}
|
|
||||||
|
|
||||||
onQ6High(_readMode: boolean): void {
|
|
||||||
// nothing?
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
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.
|
* Emulates the 16-sector and 13-sector versions of the Disk ][ drive and controller.
|
||||||
*/
|
*/
|
||||||
|
@ -1015,7 +529,6 @@ export default class DiskII implements Card<State>, MassStorage<NibbleFormat> {
|
||||||
getState(): State {
|
getState(): State {
|
||||||
const result = {
|
const result = {
|
||||||
drives: [] as DriveState[],
|
drives: [] as DriveState[],
|
||||||
driver: [] as DriverState[],
|
|
||||||
controllerState: { ...this.state },
|
controllerState: { ...this.state },
|
||||||
};
|
};
|
||||||
result.drives[1] = this.getDriveState(1);
|
result.drives[1] = this.getDriveState(1);
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { DriveNumber, NibbleDisk, WozDisk } from '../../formats/types';
|
||||||
|
import { ControllerState, DiskDriver, Drive, DriverState } from './types';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common logic for both `NibbleDiskDriver` and `WozDiskDriver`.
|
||||||
|
*/
|
||||||
|
export abstract class BaseDiskDriver implements DiskDriver {
|
||||||
|
constructor(
|
||||||
|
protected readonly driveNo: 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;
|
||||||
|
|
||||||
|
/** Returns `true` if the controller is on and this drive is selected. */
|
||||||
|
isOn(): boolean {
|
||||||
|
return this.controller.on && this.controller.driveNo === this.driveNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns `true` if the drive's write protect switch is enabled. */
|
||||||
|
isWriteProtected(): boolean {
|
||||||
|
return this.drive.readOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the current state of the driver as a serializable object. */
|
||||||
|
abstract getState(): DriverState;
|
||||||
|
|
||||||
|
/** Sets the state of the driver from the given `state`. */
|
||||||
|
abstract setState(state: DriverState): void;
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { DiskDriver, Drive, DriverState } from './types';
|
||||||
|
|
||||||
|
/** Returned state for an empty drive. */
|
||||||
|
export interface EmptyDriverState extends DriverState { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Driver for empty drives. This implementation does nothing except keep
|
||||||
|
* the head clamped between tracks 0 and 34.
|
||||||
|
*/
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
if (this.drive.track > 34) {
|
||||||
|
this.drive.track = 34;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getState() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(_state: EmptyDriverState): void {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { DriveNumber, NibbleDisk } from '../../formats/types';
|
||||||
|
import { BaseDiskDriver } from './BaseDiskDriver';
|
||||||
|
import { ControllerState, Drive, DriverState } from './types';
|
||||||
|
|
||||||
|
interface NibbleDiskDriverState extends DriverState {
|
||||||
|
skip: number;
|
||||||
|
nibbleCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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: number = 0;
|
||||||
|
/** Number of nibbles reads since the drive was turned on. */
|
||||||
|
private nibbleCount: number = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
driveNo: DriveNumber,
|
||||||
|
drive: Drive,
|
||||||
|
readonly disk: NibbleDisk,
|
||||||
|
controller: ControllerState,
|
||||||
|
private readonly onDirty: () => void) {
|
||||||
|
super(driveNo, 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,225 @@
|
||||||
|
import Apple2IO from '../../apple2io';
|
||||||
|
import { DriveNumber, WozDisk } from '../../formats/types';
|
||||||
|
import { toHex } from '../../util';
|
||||||
|
import { SEQUENCER_ROM } from '../disk2';
|
||||||
|
import { BaseDiskDriver } from './BaseDiskDriver';
|
||||||
|
import { ControllerState, Drive, DriverState, LssClockCycle, LssState } from './types';
|
||||||
|
|
||||||
|
interface WozDiskDriverState extends DriverState {
|
||||||
|
clock: LssClockCycle;
|
||||||
|
state: LssState;
|
||||||
|
lastCycles: number;
|
||||||
|
zeros: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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: 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
|
||||||
|
* "Freaking Out Like a MC3470" in the WOZ spec.
|
||||||
|
*/
|
||||||
|
private zeros = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
driveNo: DriveNumber,
|
||||||
|
drive: Drive,
|
||||||
|
readonly disk: WozDisk,
|
||||||
|
controller: ControllerState,
|
||||||
|
private readonly onDirty: () => void,
|
||||||
|
private readonly io: Apple2IO) {
|
||||||
|
super(driveNo, drive, disk, controller);
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDriveOff(): void {
|
||||||
|
// nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spin the disk under the read/write head for WOZ images.
|
||||||
|
*
|
||||||
|
* This implementation emulates every clock cycle of the 2 MHz
|
||||||
|
* sequencer since the last time it was called in order to
|
||||||
|
* determine the current state. Because this is called on
|
||||||
|
* every access to the softswitches, the data in the latch
|
||||||
|
* will be correct on every read.
|
||||||
|
*
|
||||||
|
* The emulation of the disk makes a few simplifying assumptions:
|
||||||
|
*
|
||||||
|
* * The motor turns on instantly.
|
||||||
|
* * The head moves tracks instantly.
|
||||||
|
* * The length (in bits) of each track of the WOZ image
|
||||||
|
* represents one full rotation of the disk and that each
|
||||||
|
* bit is evenly spaced.
|
||||||
|
* * Writing will not change the track length. This means
|
||||||
|
* that short tracks stay short.
|
||||||
|
* * The read head picks up the next bit when the sequencer
|
||||||
|
* clock === 4.
|
||||||
|
* * Head position X on track T is equivalent to head position
|
||||||
|
* X on track T′. (This is not the recommendation in the WOZ
|
||||||
|
* spec.)
|
||||||
|
* * Unspecified tracks contain a single zero bit. (A very
|
||||||
|
* short track, indeed!)
|
||||||
|
* * Two zero bits are sufficient to cause the MC3470 to freak
|
||||||
|
* out. When freaking out, it returns 0 and 1 with equal
|
||||||
|
* probability.
|
||||||
|
* * Any softswitch changes happen before `moveHead`. This is
|
||||||
|
* important because it means that if the clock is ever
|
||||||
|
* advanced more than one cycle between calls, the
|
||||||
|
* softswitch changes will appear to happen at the very
|
||||||
|
* beginning, not just before the last cycle.
|
||||||
|
*/
|
||||||
|
private moveHead() {
|
||||||
|
// TODO(flan): Short-circuit if the drive is not on.
|
||||||
|
const cycles = this.io.cycles();
|
||||||
|
|
||||||
|
// Spin the disk the number of elapsed cycles since last call
|
||||||
|
let workCycles = (cycles - this.lastCycles) * 2;
|
||||||
|
this.lastCycles = cycles;
|
||||||
|
|
||||||
|
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 = disk.rawTracks[disk.trackMap[drive.track]] || [0];
|
||||||
|
|
||||||
|
while (workCycles-- > 0) {
|
||||||
|
let pulse: number = 0;
|
||||||
|
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) {
|
||||||
|
const r = Math.random();
|
||||||
|
pulse = r >= 0.5 ? 1 : 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.zeros = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
idx |= pulse ? 0x00 : 0x01;
|
||||||
|
idx |= controller.latch & 0x80 ? 0x02 : 0x00;
|
||||||
|
idx |= controller.q6 ? 0x04 : 0x00;
|
||||||
|
idx |= controller.q7 ? 0x08 : 0x00;
|
||||||
|
idx |= this.state << 4;
|
||||||
|
|
||||||
|
const command = SEQUENCER_ROM[controller.sectors][idx];
|
||||||
|
|
||||||
|
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
|
||||||
|
controller.latch = 0;
|
||||||
|
break;
|
||||||
|
case 0x8: // NOP
|
||||||
|
break;
|
||||||
|
case 0x9: // SL0
|
||||||
|
controller.latch = (controller.latch << 1) & 0xff;
|
||||||
|
break;
|
||||||
|
case 0xA: // SR
|
||||||
|
controller.latch >>= 1;
|
||||||
|
if (this.isWriteProtected()) {
|
||||||
|
controller.latch |= 0x80;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 0xB: // LD
|
||||||
|
controller.latch = controller.bus;
|
||||||
|
this.debug('Loading', toHex(controller.latch), 'from bus');
|
||||||
|
break;
|
||||||
|
case 0xD: // SL1
|
||||||
|
controller.latch = ((controller.latch << 1) | 0x01) & 0xff;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.debug(`unknown command: ${toHex(command & 0xf)}`);
|
||||||
|
}
|
||||||
|
this.state = (command >> 4 & 0xF) as LssState;
|
||||||
|
|
||||||
|
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 (++drive.head >= track.length) {
|
||||||
|
drive.head = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (++this.clock > 7) {
|
||||||
|
this.clock = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tick(): void {
|
||||||
|
this.moveHead();
|
||||||
|
}
|
||||||
|
|
||||||
|
onQ6High(_readMode: boolean): void {
|
||||||
|
// nothing?
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { DriveNumber, SupportedSectors } from 'js/formats/types';
|
||||||
|
import { byte, nibble } from 'js/types';
|
||||||
|
|
||||||
|
export type LssClockCycle = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||||
|
export type LssState = nibble;
|
||||||
|
|
||||||
|
export type Phase = 0 | 1 | 2 | 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State of the controller.
|
||||||
|
*/
|
||||||
|
export interface ControllerState {
|
||||||
|
/** Sectors supported by the controller. */
|
||||||
|
sectors: SupportedSectors;
|
||||||
|
|
||||||
|
/** Is the active drive powered on? */
|
||||||
|
on: boolean;
|
||||||
|
|
||||||
|
/** The active drive. */
|
||||||
|
driveNo: DriveNumber;
|
||||||
|
|
||||||
|
/** The 8-cycle LSS clock. */
|
||||||
|
clock: LssClockCycle;
|
||||||
|
|
||||||
|
/** Current state of the Logic State Sequencer. */
|
||||||
|
state: LssState;
|
||||||
|
|
||||||
|
/** Q6 (Shift/Load) */
|
||||||
|
q6: boolean;
|
||||||
|
/** Q7 (Read/Write) */
|
||||||
|
q7: boolean;
|
||||||
|
|
||||||
|
/** Last data from the disk drive. */
|
||||||
|
latch: byte;
|
||||||
|
/** Last data written by the CPU to card softswitch 0x8D. */
|
||||||
|
bus: byte;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Common information for Nibble and WOZ disks. */
|
||||||
|
export interface Drive {
|
||||||
|
/** Whether the drive write protect is on. */
|
||||||
|
readOnly: boolean;
|
||||||
|
/** Quarter track position of read/write head. */
|
||||||
|
track: byte;
|
||||||
|
/** Position of the head on the track. */
|
||||||
|
head: byte;
|
||||||
|
/** Current active coil in the head stepper motor. */
|
||||||
|
phase: Phase;
|
||||||
|
/** Whether the drive has been written to since it was loaded. */
|
||||||
|
dirty: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Base interface for disk driver states. */
|
||||||
|
export interface DriverState {}
|
||||||
|
|
||||||
|
/** Interface for drivers for various disk types. */
|
||||||
|
export interface DiskDriver {
|
||||||
|
tick(): void;
|
||||||
|
onQ6Low(): void;
|
||||||
|
onQ6High(readMode: boolean): void;
|
||||||
|
onDriveOn(): void;
|
||||||
|
onDriveOff(): void;
|
||||||
|
clampTrack(): void;
|
||||||
|
getState(): DriverState;
|
||||||
|
setState(state: DriverState): void;
|
||||||
|
}
|
Loading…
Reference in New Issue