2022-06-18 23:54:33 +00:00
|
|
|
/** @jest-environment jsdom */
|
2022-06-20 02:52:06 +00:00
|
|
|
import fs from 'fs';
|
|
|
|
|
2022-06-18 23:54:33 +00:00
|
|
|
import Apple2IO from 'js/apple2io';
|
|
|
|
import DiskII, { Callbacks } from 'js/cards/disk2';
|
|
|
|
import CPU6502 from 'js/cpu6502';
|
Split disk data out into its own record (#158)
* Harmonize drive and disk type hierarchies
Before, the `XXXDrive` and `XXXDisk` type hierarchies were similar,
but not exactly the same. For example, `encoding` and `format` were
missing on some `XXXDisk` types where they existed on the `XXXDrive`
type. This change attempts to bring the hierarchies closer together.
However, the biggest visible consequence is the introduction of the
`FLOPPY_FORMATS` array and its associated `FloppyFormat` type. This
replaces `NIBBLE_FORMATS` in most places. A couple of new type guards
for disk formats and disks have been added as well.
All tests pass, everything compiles with no errors, and both WOZ and
nibble format disks load in the emulator.
* Move disk data to a `disk` field in the drive
Before, disk data was mixed in with state about the drive itself (like
track, motor phase, etc.). This made it hard to know exactly what data
was necessary for different image formats.
Now, the disk data is in a `disk` field whose type depends on the
drive type. This makes responisbility a bit easier.
One oddity, though, is that the `Drive` has metadata _and_ the `Disk`
has metadata. When a disk is in the drive, these should be `===`, but
when there is no disk in the drive, obviously only the drive metadata
is set.
All tests pass, everything compiles, and both WOZ and nibble disks
work in the emulator (both preact and classic).
* Squash the `Drive` type hierarchy
Before, the type of the drive depended on the type of the disk in the
drive. Thus, `NibbleDrive` contained a `NibbleDisk` and a `WozDrive`
contained a `WozDisk`. With the extraction of the disk data to a
single field, this type hierarchy makes no sense. Instead, it
suffices to check the type of the disk.
This change removes the `NibbleDrive` and `WozDrive` types and type
guards, checking the disk type where necessary. This change also
introduces the `NoFloppyDisk` type to represent the lack of a
disk. This allows the drive to have metadata, for one.
All tests pass, everything compiles, and both WOZ and nibble disks
work locally.
* Use more destructuring assignment
Now, more places use constructs like:
```TypeScript
const { metadata, readOnly, track, head, phase, dirty } = drive;
return {
disk: getDiskState(drive.disk),
metadata: {...metadata},
readOnly,
track,
head,
phase,
dirty,
};
```
* Remove the `Disk` object from the `Drive` object
This change splits out the disk objects into a record parallel to the
drive objects. The idea is that the `Drive` structure becomes a
representation of the state of the drive that is separate from the
disk image actually in the drive. This helps in an upcoming
refactoring.
This also changes the default empty disks to be writable. While odd,
the write protect switch should be in the "off" position since there
is no disk pressing on it.
Finally, `insertDisk` now resets the head position to 0 since there is
no way of preserving the head position across disks. (Even in the real
world, the motor-off delay plus spindle spin-down would make it
impossible to know the disk head position with any accuracy.)
2022-09-17 13:41:35 +00:00
|
|
|
import { DriveNumber, NibbleDisk, WozDisk } from 'js/formats/types';
|
2022-06-20 02:52:06 +00:00
|
|
|
import { byte } from 'js/types';
|
2022-06-23 13:38:36 +00:00
|
|
|
import { toHex } from 'js/util';
|
2022-06-18 23:54:33 +00:00
|
|
|
import { VideoModes } from 'js/videomodes';
|
|
|
|
import { mocked } from 'ts-jest/utils';
|
|
|
|
import { BYTES_BY_SECTOR_IMAGE, BYTES_BY_TRACK_IMAGE } from '../formats/testdata/16sector';
|
|
|
|
|
|
|
|
jest.mock('js/apple2io');
|
|
|
|
jest.mock('js/videomodes');
|
|
|
|
|
|
|
|
type Phase = 0 | 1 | 2 | 3; // not exported from DiskII
|
|
|
|
|
|
|
|
const STEPS_PER_TRACK = 4;
|
|
|
|
const PHASES_PER_TRACK = 2;
|
|
|
|
|
|
|
|
function setTrack(diskII: DiskII, track: number) {
|
|
|
|
const initialState = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
initialState.drives[1].track = track * STEPS_PER_TRACK;
|
|
|
|
initialState.drives[1].phase = (track * PHASES_PER_TRACK) % 4 as Phase;
|
2022-06-18 23:54:33 +00:00
|
|
|
diskII.setState(initialState);
|
|
|
|
}
|
|
|
|
|
|
|
|
function setWriteProtected(diskII: DiskII, isWriteProtected: boolean) {
|
|
|
|
const initialState = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
initialState.drives[1].readOnly = isWriteProtected;
|
2022-06-18 23:54:33 +00:00
|
|
|
diskII.setState(initialState);
|
|
|
|
}
|
|
|
|
|
|
|
|
describe('DiskII', () => {
|
|
|
|
const mockApple2IO = new Apple2IO({} as unknown as CPU6502, {} as unknown as VideoModes);
|
|
|
|
const callbacks: Callbacks = {
|
|
|
|
driveLight: jest.fn(),
|
|
|
|
dirty: jest.fn(),
|
|
|
|
label: jest.fn(),
|
|
|
|
};
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
jest.resetAllMocks();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('is constructable', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
expect(diskII).not.toBeNull();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('round-trips the state when there are no changes', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
|
|
|
diskII.setState(state);
|
|
|
|
|
|
|
|
expect(diskII.getState()).toEqual(state);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('round-trips the state when there are changes', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
diskII.setBinary(2, 'BYTES_BY_SECTOR', 'po', BYTES_BY_SECTOR_IMAGE);
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
|
|
|
// These are just arbitrary changes, not an exhaustive list of fields.
|
2022-10-01 18:15:57 +00:00
|
|
|
state.skip = 1;
|
|
|
|
state.controllerState.drive = 2;
|
Floppy controller refactorings 1 (#155)
* Add `DiskMetada` to the `Disk` interface
Before, metadata about the image, such as name, side, etc. was mixed
in with actual disk image information. This change breaks that
information into a separate structure called `DiskMetadata`.
Currently, the only two fields are `name` and `side`, but the idea is
that more fields could be added as necessary, like a description, a
scan of the disk or label, etc. In a follow-on change, the default
write-protection status will come from the metadata as well.
The current implementation copies the metadata when saving/restoring
state, loading disk images, etc. In the future, the metadata should
passed around until the format is required to change (like saving one
disk image format as another). Likewise, in the future, in may be
desirable to be able to override the disk image metadata with
user-supplied metadata. This could be use, for example, to
temporarily add or remove write-protection from a disk image.
All existing tests pass and the emulator builds with no errors.
* Rename `writeMode` to `q7`
Before, nibble disk emulation used the `writeMode` field to keep track
of whether the drive should be read from or written to, but the WOZ
emulation used `q7` to keep track of the same state.
This change renames `writeMode` to `q7` because it more accurately
reflects the state of the Disk II controller as specified in the
manuals, DOS source, and, especially, _Understanding the Apple //e_ by
Jim Sather.
* Remove the coil state
Before, `q` captured the state of the coils. But it was never read.
This change just deletes it.
* Use the bootstrap and sequencer ROMs with indirection
Before, the contents of the bootstrap ROM and sequencer ROM were set
directly on fields of the controller. These were not saved or
restored with the state in `getState` and `setState`. (It would have
been very space inefficient if they had).
Now, these ROMs are used from constants indexed by the number of
sectors the card supports. This, in turn, means that if the number of
sectors is saved with the state, it can be easily restored.
* Split out the Disk II controller state
This change factors the emulated hardware state into a separate
structure in the Disk II controller. The idea is that this hardware
state will be able to be shared with the WOZ and nibble disk code
instead of sharing _all_ of the controller state (like callbacks and
so forth).
* Factor out disk insertion
Before, several places in the code essentially inserted a new disk
image into the drive, which similar—but not always exactly the
same—code. Now there is an `insertDisk` method that is responsible
for inserting a new `FloppyDisk`.
All tests pass, everything compiles, manually tested nibble disks and
WOZ disks.
2022-09-01 01:55:01 +00:00
|
|
|
state.controllerState.latch = 0x42;
|
|
|
|
state.controllerState.on = true;
|
|
|
|
state.controllerState.q7 = true;
|
Split disk data out into its own record (#158)
* Harmonize drive and disk type hierarchies
Before, the `XXXDrive` and `XXXDisk` type hierarchies were similar,
but not exactly the same. For example, `encoding` and `format` were
missing on some `XXXDisk` types where they existed on the `XXXDrive`
type. This change attempts to bring the hierarchies closer together.
However, the biggest visible consequence is the introduction of the
`FLOPPY_FORMATS` array and its associated `FloppyFormat` type. This
replaces `NIBBLE_FORMATS` in most places. A couple of new type guards
for disk formats and disks have been added as well.
All tests pass, everything compiles with no errors, and both WOZ and
nibble format disks load in the emulator.
* Move disk data to a `disk` field in the drive
Before, disk data was mixed in with state about the drive itself (like
track, motor phase, etc.). This made it hard to know exactly what data
was necessary for different image formats.
Now, the disk data is in a `disk` field whose type depends on the
drive type. This makes responisbility a bit easier.
One oddity, though, is that the `Drive` has metadata _and_ the `Disk`
has metadata. When a disk is in the drive, these should be `===`, but
when there is no disk in the drive, obviously only the drive metadata
is set.
All tests pass, everything compiles, and both WOZ and nibble disks
work in the emulator (both preact and classic).
* Squash the `Drive` type hierarchy
Before, the type of the drive depended on the type of the disk in the
drive. Thus, `NibbleDrive` contained a `NibbleDisk` and a `WozDrive`
contained a `WozDisk`. With the extraction of the disk data to a
single field, this type hierarchy makes no sense. Instead, it
suffices to check the type of the disk.
This change removes the `NibbleDrive` and `WozDrive` types and type
guards, checking the disk type where necessary. This change also
introduces the `NoFloppyDisk` type to represent the lack of a
disk. This allows the drive to have metadata, for one.
All tests pass, everything compiles, and both WOZ and nibble disks
work locally.
* Use more destructuring assignment
Now, more places use constructs like:
```TypeScript
const { metadata, readOnly, track, head, phase, dirty } = drive;
return {
disk: getDiskState(drive.disk),
metadata: {...metadata},
readOnly,
track,
head,
phase,
dirty,
};
```
* Remove the `Disk` object from the `Drive` object
This change splits out the disk objects into a record parallel to the
drive objects. The idea is that the `Drive` structure becomes a
representation of the state of the drive that is separate from the
disk image actually in the drive. This helps in an upcoming
refactoring.
This also changes the default empty disks to be writable. While odd,
the write protect switch should be in the "off" position since there
is no disk pressing on it.
Finally, `insertDisk` now resets the head position to 0 since there is
no way of preserving the head position across disks. (Even in the real
world, the motor-off delay plus spindle spin-down would make it
impossible to know the disk head position with any accuracy.)
2022-09-17 13:41:35 +00:00
|
|
|
const disk2 = state.drives[2].disk as NibbleDisk;
|
|
|
|
disk2.tracks[14][12] = 0x80;
|
2022-08-31 16:06:38 +00:00
|
|
|
state.drives[2].head = 1000;
|
|
|
|
state.drives[2].phase = 3;
|
2022-06-18 23:54:33 +00:00
|
|
|
diskII.setState(state);
|
|
|
|
|
|
|
|
expect(diskII.getState()).toEqual(state);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('calls all of the callbacks when state is restored', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
jest.resetAllMocks();
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
|
|
|
diskII.setState(state);
|
|
|
|
|
|
|
|
expect(callbacks.driveLight).toHaveBeenCalledTimes(2);
|
|
|
|
expect(callbacks.driveLight).toHaveBeenCalledWith(1, false);
|
|
|
|
expect(callbacks.driveLight).toHaveBeenCalledWith(2, false);
|
|
|
|
|
|
|
|
expect(callbacks.label).toHaveBeenCalledTimes(2);
|
|
|
|
expect(callbacks.label).toHaveBeenCalledWith(1, 'BYTES_BY_TRACK', undefined);
|
|
|
|
expect(callbacks.label).toHaveBeenCalledWith(2, 'Disk 2', undefined);
|
|
|
|
|
|
|
|
expect(callbacks.dirty).toHaveBeenCalledTimes(2);
|
2022-10-01 18:15:57 +00:00
|
|
|
expect(callbacks.dirty).toHaveBeenCalledWith(1, true);
|
2022-06-18 23:54:33 +00:00
|
|
|
expect(callbacks.dirty).toHaveBeenCalledWith(2, false);
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('drive lights', () => {
|
|
|
|
it('turns on drive light 1 when the motor is turned on', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
|
|
|
|
expect(callbacks.driveLight).toBeCalledTimes(1);
|
|
|
|
expect(callbacks.driveLight).toBeCalledWith(1, true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('turns off drive light 1 when the motor is turned off', () => {
|
|
|
|
jest.useFakeTimers();
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
mocked(callbacks.driveLight).mockReset();
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x88); // turn off the motor
|
|
|
|
|
|
|
|
jest.runAllTimers();
|
|
|
|
expect(callbacks.driveLight).toBeCalledTimes(1);
|
|
|
|
expect(callbacks.driveLight).toBeCalledWith(1, false);
|
|
|
|
jest.useRealTimers();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('turns on drive light 2 when drive 2 is selected and the motor is turned on', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x8B); // select drive 2
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
|
|
|
|
expect(callbacks.driveLight).toBeCalledTimes(1);
|
|
|
|
expect(callbacks.driveLight).toBeCalledWith(2, true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('turns off drive light 2 when drive 2 is selected and the motor is turned off', () => {
|
|
|
|
jest.useFakeTimers();
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.ioSwitch(0x8B); // select drive 2
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
mocked(callbacks.driveLight).mockReset();
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x88); // turn off the motor
|
|
|
|
|
|
|
|
jest.runAllTimers();
|
|
|
|
expect(callbacks.driveLight).toBeCalledTimes(1);
|
|
|
|
expect(callbacks.driveLight).toBeCalledWith(2, false);
|
|
|
|
jest.useRealTimers();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('turns off drive light 1 and turns on drive light two when drive 2 is selected', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x8B); // select drive 2
|
|
|
|
|
|
|
|
expect(callbacks.driveLight).toBeCalledTimes(3);
|
|
|
|
expect(callbacks.driveLight).toHaveBeenNthCalledWith(1, 1, true);
|
|
|
|
expect(callbacks.driveLight).toHaveBeenNthCalledWith(2, 1, false);
|
|
|
|
expect(callbacks.driveLight).toHaveBeenNthCalledWith(3, 2, true);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('head positioning', () => {
|
|
|
|
it('does not allow head positioning when the drive is off', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x81); // coil 0 on
|
|
|
|
diskII.ioSwitch(0x83); // coil 1 on
|
|
|
|
diskII.ioSwitch(0x80); // coil 0 off
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x82); // coil 1 off
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].phase).toBe(0);
|
|
|
|
expect(state.drives[1].track).toBe(0);
|
2022-06-18 23:54:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('allows head positioning when the drive is on', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x81); // coil 0 on
|
|
|
|
diskII.ioSwitch(0x83); // coil 1 on
|
|
|
|
diskII.ioSwitch(0x80); // coil 0 off
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x82); // coil 1 off
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].phase).toBe(2);
|
|
|
|
expect(state.drives[1].track).toBe(4);
|
2022-06-18 23:54:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('moves the head to track 2 from track 0 when all phases are cycled', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x81); // coil 0 on
|
|
|
|
diskII.ioSwitch(0x83); // coil 1 on
|
|
|
|
diskII.ioSwitch(0x80); // coil 0 off
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x82); // coil 1 off
|
|
|
|
diskII.ioSwitch(0x87); // coil 3 on
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
diskII.ioSwitch(0x81); // coil 0 on
|
|
|
|
diskII.ioSwitch(0x86); // coil 3 off
|
|
|
|
diskII.ioSwitch(0x80); // coil 0 off
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].phase).toBe(0);
|
|
|
|
expect(state.drives[1].track).toBe(2 * STEPS_PER_TRACK);
|
2022-06-18 23:54:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('moves the head to track 10 from track 8 when all phases are cycled', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
setTrack(diskII, 8);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x81); // coil 0 on
|
|
|
|
diskII.ioSwitch(0x83); // coil 1 on
|
|
|
|
diskII.ioSwitch(0x80); // coil 0 off
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x82); // coil 1 off
|
|
|
|
diskII.ioSwitch(0x87); // coil 3 on
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
diskII.ioSwitch(0x81); // coil 0 on
|
|
|
|
diskII.ioSwitch(0x86); // coil 3 off
|
|
|
|
diskII.ioSwitch(0x80); // coil 0 off
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].phase).toBe(0);
|
|
|
|
expect(state.drives[1].track).toBe(10 * STEPS_PER_TRACK);
|
2022-06-18 23:54:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('stops the head at track 34', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
setTrack(diskII, 33);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x87); // coil 3 on
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
diskII.ioSwitch(0x81); // coil 0 on
|
|
|
|
diskII.ioSwitch(0x86); // coil 3 off
|
|
|
|
diskII.ioSwitch(0x83); // coil 1 on
|
|
|
|
diskII.ioSwitch(0x80); // coil 0 off
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
2022-06-20 02:52:06 +00:00
|
|
|
diskII.ioSwitch(0x82); // coil 1 off
|
2022-06-18 23:54:33 +00:00
|
|
|
diskII.ioSwitch(0x87); // coil 3 on
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
diskII.ioSwitch(0x81); // coil 0 on
|
|
|
|
diskII.ioSwitch(0x86); // coil 3 off
|
|
|
|
diskII.ioSwitch(0x80); // coil 0 off
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].phase).toBe(0);
|
2022-06-18 23:54:33 +00:00
|
|
|
// The emulated Disk II puts data for track n on the
|
|
|
|
// 4 quarter-tracks starting with n * STEPS_PER_TRACK.
|
|
|
|
// On a real Disk II, the data would likely be on 3
|
|
|
|
// quarter-tracks starting with n * STEPS_PER_TRACK - 1,
|
|
|
|
// leaving 1 essentially blank quarter track at the
|
|
|
|
// half-track.
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].track).toBe(35 * STEPS_PER_TRACK - 1);
|
2022-06-18 23:54:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('moves a half track when only one phase is activated', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
setTrack(diskII, 15);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x87); // coil 3 on
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
diskII.ioSwitch(0x86); // coil 3 off
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].phase).toBe(3);
|
|
|
|
expect(state.drives[1].track).toBe(15 * STEPS_PER_TRACK + 2);
|
2022-06-18 23:54:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('moves backward one track when phases are cycled in reverse', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
setTrack(diskII, 15);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x83); // coil 1 on
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
diskII.ioSwitch(0x81); // coil 0 on
|
|
|
|
diskII.ioSwitch(0x82); // coil 1 off
|
|
|
|
diskII.ioSwitch(0x80); // coil 0 off
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].phase).toBe(0);
|
|
|
|
expect(state.drives[1].track).toBe(14 * STEPS_PER_TRACK);
|
2022-06-18 23:54:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('moves backward two tracks when all phases are cycled in reverse', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
setTrack(diskII, 15);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x83); // coil 1 on
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
diskII.ioSwitch(0x81); // coil 0 on
|
|
|
|
diskII.ioSwitch(0x82); // coil 1 off
|
|
|
|
diskII.ioSwitch(0x87); // coil 3 on
|
|
|
|
diskII.ioSwitch(0x80); // coil 0 off
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x86); // coil 3 off
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].phase).toBe(2);
|
|
|
|
expect(state.drives[1].track).toBe(13 * STEPS_PER_TRACK);
|
2022-06-18 23:54:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('does not move backwards past track 0', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
setTrack(diskII, 1);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x83); // coil 1 on
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
diskII.ioSwitch(0x81); // coil 0 on
|
|
|
|
diskII.ioSwitch(0x82); // coil 1 off
|
|
|
|
diskII.ioSwitch(0x87); // coil 3 on
|
|
|
|
diskII.ioSwitch(0x80); // coil 0 off
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x86); // coil 3 off
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].phase).toBe(2);
|
|
|
|
expect(state.drives[1].track).toBe(0);
|
2022-06-18 23:54:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('moves backward one half track', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
setTrack(diskII, 15);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x83); // coil 1 on
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
diskII.ioSwitch(0x82); // coil 1 off
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].phase).toBe(1);
|
|
|
|
expect(state.drives[1].track).toBe(14.5 * STEPS_PER_TRACK);
|
2022-06-18 23:54:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// The emulated Disk II is not able to step quarter tracks because
|
|
|
|
// it does not track when phases are turned off.
|
|
|
|
// eslint-disable-next-line jest/no-disabled-tests
|
|
|
|
it.skip('moves a quarter track when two neighboring phases are activated and held', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
setTrack(diskII, 15);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x87); // coil 3 on
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].phase).toBe(3);
|
|
|
|
expect(state.drives[1].track).toBe(15 * STEPS_PER_TRACK + 1);
|
2022-06-18 23:54:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// The emulated Disk II is not able to step quarter tracks because
|
|
|
|
// it does not track when phases are turned off.
|
|
|
|
// eslint-disable-next-line jest/no-disabled-tests
|
|
|
|
it.skip('moves backward one quarter track', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
setTrack(diskII, 15);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x83); // coil 1 on
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].phase).toBe(1);
|
|
|
|
expect(state.drives[1].track).toBe(14.25 * STEPS_PER_TRACK);
|
2022-06-18 23:54:33 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('reading nibble-based disks', () => {
|
|
|
|
it('spins the disk', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x8e); // read mode
|
|
|
|
|
|
|
|
// Just check for changing nibbles
|
|
|
|
let spinning = false;
|
|
|
|
const firstNibble = diskII.ioSwitch(0x8c); // read data
|
|
|
|
for (let i = 0; i < 512; i++) {
|
|
|
|
const thisNibble = diskII.ioSwitch(0x8c); // read data
|
|
|
|
if (thisNibble >= 0x80 && firstNibble !== thisNibble) {
|
|
|
|
spinning = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
expect(spinning).toBeTruthy();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('after reading the data, the data register is set to zero', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x8e); // read mode
|
|
|
|
|
|
|
|
// Find address field prolog
|
|
|
|
let nibble = diskII.ioSwitch(0x8c); // read data
|
|
|
|
for (let i = 0; i < 512 && nibble !== 0xD5; i++) {
|
|
|
|
nibble = diskII.ioSwitch(0x8c); // read data
|
|
|
|
}
|
|
|
|
expect(nibble).toBe(0xD5);
|
|
|
|
nibble = diskII.ioSwitch(0x8c); // read data
|
|
|
|
// expect next read to be a zero because the sequencer is waiting
|
|
|
|
// for data
|
|
|
|
expect(nibble).toBe(0x00);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('after reading the data, then zero, there is new data', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x8e); // read mode
|
|
|
|
|
|
|
|
// Find address field prolog
|
|
|
|
let nibble = diskII.ioSwitch(0x8c); // read data
|
|
|
|
for (let i = 0; i < 512 && nibble !== 0xD5; i++) {
|
|
|
|
nibble = diskII.ioSwitch(0x8c); // read data
|
|
|
|
}
|
|
|
|
expect(nibble).toBe(0xD5);
|
|
|
|
nibble = diskII.ioSwitch(0x8c); // read data
|
|
|
|
// expect next read to be a zero
|
|
|
|
expect(nibble).toBe(0x00);
|
|
|
|
// expect next read to be new data
|
|
|
|
nibble = diskII.ioSwitch(0x8c); // read data
|
|
|
|
expect(nibble).toBe(0xAA);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('read write protect status', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
setWriteProtected(diskII, true);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x8E); // read mode
|
|
|
|
diskII.ioSwitch(0x8D); // read write protect if read
|
|
|
|
const isWriteProtected = diskII.ioSwitch(0x8E); // read data
|
|
|
|
|
|
|
|
expect(isWriteProtected).toBe(0xff);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('writing nibble-based disks', () => {
|
|
|
|
it('writes a nibble to the disk', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
Split disk data out into its own record (#158)
* Harmonize drive and disk type hierarchies
Before, the `XXXDrive` and `XXXDisk` type hierarchies were similar,
but not exactly the same. For example, `encoding` and `format` were
missing on some `XXXDisk` types where they existed on the `XXXDrive`
type. This change attempts to bring the hierarchies closer together.
However, the biggest visible consequence is the introduction of the
`FLOPPY_FORMATS` array and its associated `FloppyFormat` type. This
replaces `NIBBLE_FORMATS` in most places. A couple of new type guards
for disk formats and disks have been added as well.
All tests pass, everything compiles with no errors, and both WOZ and
nibble format disks load in the emulator.
* Move disk data to a `disk` field in the drive
Before, disk data was mixed in with state about the drive itself (like
track, motor phase, etc.). This made it hard to know exactly what data
was necessary for different image formats.
Now, the disk data is in a `disk` field whose type depends on the
drive type. This makes responisbility a bit easier.
One oddity, though, is that the `Drive` has metadata _and_ the `Disk`
has metadata. When a disk is in the drive, these should be `===`, but
when there is no disk in the drive, obviously only the drive metadata
is set.
All tests pass, everything compiles, and both WOZ and nibble disks
work in the emulator (both preact and classic).
* Squash the `Drive` type hierarchy
Before, the type of the drive depended on the type of the disk in the
drive. Thus, `NibbleDrive` contained a `NibbleDisk` and a `WozDrive`
contained a `WozDisk`. With the extraction of the disk data to a
single field, this type hierarchy makes no sense. Instead, it
suffices to check the type of the disk.
This change removes the `NibbleDrive` and `WozDrive` types and type
guards, checking the disk type where necessary. This change also
introduces the `NoFloppyDisk` type to represent the lack of a
disk. This allows the drive to have metadata, for one.
All tests pass, everything compiles, and both WOZ and nibble disks
work locally.
* Use more destructuring assignment
Now, more places use constructs like:
```TypeScript
const { metadata, readOnly, track, head, phase, dirty } = drive;
return {
disk: getDiskState(drive.disk),
metadata: {...metadata},
readOnly,
track,
head,
phase,
dirty,
};
```
* Remove the `Disk` object from the `Drive` object
This change splits out the disk objects into a record parallel to the
drive objects. The idea is that the `Drive` structure becomes a
representation of the state of the drive that is separate from the
disk image actually in the drive. This helps in an upcoming
refactoring.
This also changes the default empty disks to be writable. While odd,
the write protect switch should be in the "off" position since there
is no disk pressing on it.
Finally, `insertDisk` now resets the head position to 0 since there is
no way of preserving the head position across disks. (Even in the real
world, the motor-off delay plus spindle spin-down would make it
impossible to know the disk head position with any accuracy.)
2022-09-17 13:41:35 +00:00
|
|
|
let disk1 = diskII.getState().drives[1].disk as NibbleDisk;
|
|
|
|
let track0 = disk1.tracks[0];
|
2022-06-18 23:54:33 +00:00
|
|
|
expect(track0[0]).toBe(0xFF);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x8F, 0x80); // write
|
|
|
|
diskII.ioSwitch(0x8C); // shift
|
|
|
|
|
Split disk data out into its own record (#158)
* Harmonize drive and disk type hierarchies
Before, the `XXXDrive` and `XXXDisk` type hierarchies were similar,
but not exactly the same. For example, `encoding` and `format` were
missing on some `XXXDisk` types where they existed on the `XXXDrive`
type. This change attempts to bring the hierarchies closer together.
However, the biggest visible consequence is the introduction of the
`FLOPPY_FORMATS` array and its associated `FloppyFormat` type. This
replaces `NIBBLE_FORMATS` in most places. A couple of new type guards
for disk formats and disks have been added as well.
All tests pass, everything compiles with no errors, and both WOZ and
nibble format disks load in the emulator.
* Move disk data to a `disk` field in the drive
Before, disk data was mixed in with state about the drive itself (like
track, motor phase, etc.). This made it hard to know exactly what data
was necessary for different image formats.
Now, the disk data is in a `disk` field whose type depends on the
drive type. This makes responisbility a bit easier.
One oddity, though, is that the `Drive` has metadata _and_ the `Disk`
has metadata. When a disk is in the drive, these should be `===`, but
when there is no disk in the drive, obviously only the drive metadata
is set.
All tests pass, everything compiles, and both WOZ and nibble disks
work in the emulator (both preact and classic).
* Squash the `Drive` type hierarchy
Before, the type of the drive depended on the type of the disk in the
drive. Thus, `NibbleDrive` contained a `NibbleDisk` and a `WozDrive`
contained a `WozDisk`. With the extraction of the disk data to a
single field, this type hierarchy makes no sense. Instead, it
suffices to check the type of the disk.
This change removes the `NibbleDrive` and `WozDrive` types and type
guards, checking the disk type where necessary. This change also
introduces the `NoFloppyDisk` type to represent the lack of a
disk. This allows the drive to have metadata, for one.
All tests pass, everything compiles, and both WOZ and nibble disks
work locally.
* Use more destructuring assignment
Now, more places use constructs like:
```TypeScript
const { metadata, readOnly, track, head, phase, dirty } = drive;
return {
disk: getDiskState(drive.disk),
metadata: {...metadata},
readOnly,
track,
head,
phase,
dirty,
};
```
* Remove the `Disk` object from the `Drive` object
This change splits out the disk objects into a record parallel to the
drive objects. The idea is that the `Drive` structure becomes a
representation of the state of the drive that is separate from the
disk image actually in the drive. This helps in an upcoming
refactoring.
This also changes the default empty disks to be writable. While odd,
the write protect switch should be in the "off" position since there
is no disk pressing on it.
Finally, `insertDisk` now resets the head position to 0 since there is
no way of preserving the head position across disks. (Even in the real
world, the motor-off delay plus spindle spin-down would make it
impossible to know the disk head position with any accuracy.)
2022-09-17 13:41:35 +00:00
|
|
|
disk1 = diskII.getState().drives[1].disk as NibbleDisk;
|
|
|
|
track0 = disk1.tracks[0];
|
2022-06-18 23:54:33 +00:00
|
|
|
expect(track0[0]).toBe(0x80);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('writes two nibbles to the disk', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
Split disk data out into its own record (#158)
* Harmonize drive and disk type hierarchies
Before, the `XXXDrive` and `XXXDisk` type hierarchies were similar,
but not exactly the same. For example, `encoding` and `format` were
missing on some `XXXDisk` types where they existed on the `XXXDrive`
type. This change attempts to bring the hierarchies closer together.
However, the biggest visible consequence is the introduction of the
`FLOPPY_FORMATS` array and its associated `FloppyFormat` type. This
replaces `NIBBLE_FORMATS` in most places. A couple of new type guards
for disk formats and disks have been added as well.
All tests pass, everything compiles with no errors, and both WOZ and
nibble format disks load in the emulator.
* Move disk data to a `disk` field in the drive
Before, disk data was mixed in with state about the drive itself (like
track, motor phase, etc.). This made it hard to know exactly what data
was necessary for different image formats.
Now, the disk data is in a `disk` field whose type depends on the
drive type. This makes responisbility a bit easier.
One oddity, though, is that the `Drive` has metadata _and_ the `Disk`
has metadata. When a disk is in the drive, these should be `===`, but
when there is no disk in the drive, obviously only the drive metadata
is set.
All tests pass, everything compiles, and both WOZ and nibble disks
work in the emulator (both preact and classic).
* Squash the `Drive` type hierarchy
Before, the type of the drive depended on the type of the disk in the
drive. Thus, `NibbleDrive` contained a `NibbleDisk` and a `WozDrive`
contained a `WozDisk`. With the extraction of the disk data to a
single field, this type hierarchy makes no sense. Instead, it
suffices to check the type of the disk.
This change removes the `NibbleDrive` and `WozDrive` types and type
guards, checking the disk type where necessary. This change also
introduces the `NoFloppyDisk` type to represent the lack of a
disk. This allows the drive to have metadata, for one.
All tests pass, everything compiles, and both WOZ and nibble disks
work locally.
* Use more destructuring assignment
Now, more places use constructs like:
```TypeScript
const { metadata, readOnly, track, head, phase, dirty } = drive;
return {
disk: getDiskState(drive.disk),
metadata: {...metadata},
readOnly,
track,
head,
phase,
dirty,
};
```
* Remove the `Disk` object from the `Drive` object
This change splits out the disk objects into a record parallel to the
drive objects. The idea is that the `Drive` structure becomes a
representation of the state of the drive that is separate from the
disk image actually in the drive. This helps in an upcoming
refactoring.
This also changes the default empty disks to be writable. While odd,
the write protect switch should be in the "off" position since there
is no disk pressing on it.
Finally, `insertDisk` now resets the head position to 0 since there is
no way of preserving the head position across disks. (Even in the real
world, the motor-off delay plus spindle spin-down would make it
impossible to know the disk head position with any accuracy.)
2022-09-17 13:41:35 +00:00
|
|
|
let disk1 = diskII.getState().drives[1].disk as NibbleDisk;
|
|
|
|
let track0 = disk1.tracks[0];
|
2022-06-18 23:54:33 +00:00
|
|
|
expect(track0[0]).toBe(0xFF);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x8F, 0x80); // write
|
|
|
|
diskII.ioSwitch(0x8C); // shift
|
|
|
|
diskII.ioSwitch(0x8F, 0x81); // write
|
|
|
|
diskII.ioSwitch(0x8C); // shift
|
|
|
|
|
Split disk data out into its own record (#158)
* Harmonize drive and disk type hierarchies
Before, the `XXXDrive` and `XXXDisk` type hierarchies were similar,
but not exactly the same. For example, `encoding` and `format` were
missing on some `XXXDisk` types where they existed on the `XXXDrive`
type. This change attempts to bring the hierarchies closer together.
However, the biggest visible consequence is the introduction of the
`FLOPPY_FORMATS` array and its associated `FloppyFormat` type. This
replaces `NIBBLE_FORMATS` in most places. A couple of new type guards
for disk formats and disks have been added as well.
All tests pass, everything compiles with no errors, and both WOZ and
nibble format disks load in the emulator.
* Move disk data to a `disk` field in the drive
Before, disk data was mixed in with state about the drive itself (like
track, motor phase, etc.). This made it hard to know exactly what data
was necessary for different image formats.
Now, the disk data is in a `disk` field whose type depends on the
drive type. This makes responisbility a bit easier.
One oddity, though, is that the `Drive` has metadata _and_ the `Disk`
has metadata. When a disk is in the drive, these should be `===`, but
when there is no disk in the drive, obviously only the drive metadata
is set.
All tests pass, everything compiles, and both WOZ and nibble disks
work in the emulator (both preact and classic).
* Squash the `Drive` type hierarchy
Before, the type of the drive depended on the type of the disk in the
drive. Thus, `NibbleDrive` contained a `NibbleDisk` and a `WozDrive`
contained a `WozDisk`. With the extraction of the disk data to a
single field, this type hierarchy makes no sense. Instead, it
suffices to check the type of the disk.
This change removes the `NibbleDrive` and `WozDrive` types and type
guards, checking the disk type where necessary. This change also
introduces the `NoFloppyDisk` type to represent the lack of a
disk. This allows the drive to have metadata, for one.
All tests pass, everything compiles, and both WOZ and nibble disks
work locally.
* Use more destructuring assignment
Now, more places use constructs like:
```TypeScript
const { metadata, readOnly, track, head, phase, dirty } = drive;
return {
disk: getDiskState(drive.disk),
metadata: {...metadata},
readOnly,
track,
head,
phase,
dirty,
};
```
* Remove the `Disk` object from the `Drive` object
This change splits out the disk objects into a record parallel to the
drive objects. The idea is that the `Drive` structure becomes a
representation of the state of the drive that is separate from the
disk image actually in the drive. This helps in an upcoming
refactoring.
This also changes the default empty disks to be writable. While odd,
the write protect switch should be in the "off" position since there
is no disk pressing on it.
Finally, `insertDisk` now resets the head position to 0 since there is
no way of preserving the head position across disks. (Even in the real
world, the motor-off delay plus spindle spin-down would make it
impossible to know the disk head position with any accuracy.)
2022-09-17 13:41:35 +00:00
|
|
|
disk1 = diskII.getState().drives[1].disk as NibbleDisk;
|
|
|
|
track0 = disk1.tracks[0];
|
2022-06-18 23:54:33 +00:00
|
|
|
expect(track0[0]).toBe(0x80);
|
|
|
|
expect(track0[1]).toBe(0x81);
|
|
|
|
});
|
2022-06-20 02:52:06 +00:00
|
|
|
|
|
|
|
it('sets disk state to dirty and calls the dirty callback when written', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'BYTES_BY_TRACK', 'po', BYTES_BY_TRACK_IMAGE);
|
|
|
|
let state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
state.drives[1].dirty = false;
|
2022-06-20 02:52:06 +00:00
|
|
|
diskII.setState(state);
|
|
|
|
jest.resetAllMocks();
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x8F, 0x80); // write
|
|
|
|
diskII.ioSwitch(0x8C); // shift
|
|
|
|
|
|
|
|
expect(callbacks.dirty).toHaveBeenCalledTimes(1);
|
|
|
|
expect(callbacks.dirty).toHaveBeenCalledWith(1, true);
|
|
|
|
|
|
|
|
state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].dirty).toBeTruthy();
|
2022-06-20 02:52:06 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('reading WOZ-based disks', () => {
|
|
|
|
const DOS33_SYSTEM_MASTER_IMAGE =
|
|
|
|
fs.readFileSync('test/js/cards/data/DOS 3.3 System Master.woz').buffer;
|
|
|
|
|
|
|
|
it('accepts WOZ-based disks', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
|
|
|
|
|
|
|
|
expect(true).toBeTruthy();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('stops the head at the end of the image', () => {
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
|
|
|
|
setTrack(diskII, 33);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
|
|
diskII.ioSwitch(0x87); // coil 3 on
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
diskII.ioSwitch(0x81); // coil 0 on
|
|
|
|
diskII.ioSwitch(0x86); // coil 3 off
|
|
|
|
diskII.ioSwitch(0x83); // coil 1 on
|
|
|
|
diskII.ioSwitch(0x80); // coil 0 off
|
|
|
|
diskII.ioSwitch(0x85); // coil 2 on
|
|
|
|
diskII.ioSwitch(0x82); // coil 1 off
|
|
|
|
}
|
|
|
|
diskII.ioSwitch(0x84); // coil 2 off
|
|
|
|
|
|
|
|
const state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].phase).toBe(2);
|
2022-06-20 02:52:06 +00:00
|
|
|
// For WOZ images, the number of tracks is the number in the image.
|
|
|
|
// The DOS3.3 System Master was imaged on a 40 track drive, so it
|
|
|
|
// has data for all 40 tracks, even though the last few are garbage.
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].track).toBe(40 * STEPS_PER_TRACK - 1);
|
2022-06-20 02:52:06 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('spins the disk when motor is on', () => {
|
|
|
|
let cycles: number = 0;
|
|
|
|
mocked(mockApple2IO).cycles.mockImplementation(() => cycles);
|
|
|
|
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
|
|
|
|
|
|
|
|
let state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].head).toBe(0);
|
2022-06-20 02:52:06 +00:00
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
cycles += 10;
|
|
|
|
diskII.tick();
|
|
|
|
|
|
|
|
state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].head).toBeGreaterThan(0);
|
2022-06-20 02:52:06 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('does not spin the disk when motor is off', () => {
|
|
|
|
let cycles: number = 0;
|
|
|
|
mocked(mockApple2IO).cycles.mockImplementation(() => cycles);
|
|
|
|
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
|
|
|
|
|
|
|
|
let state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].head).toBe(0);
|
2022-06-20 02:52:06 +00:00
|
|
|
|
|
|
|
cycles += 10;
|
|
|
|
diskII.tick();
|
|
|
|
|
|
|
|
state = diskII.getState();
|
2022-08-31 16:06:38 +00:00
|
|
|
expect(state.drives[1].head).toBe(0);
|
2022-06-20 02:52:06 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('reads an FF sync byte from the beginning of the image', () => {
|
|
|
|
let cycles: number = 0;
|
|
|
|
mocked(mockApple2IO).cycles.mockImplementation(() => cycles);
|
|
|
|
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x8e); // read mode
|
|
|
|
|
|
|
|
// The initial bytes in the image are: FF 3F CF F3
|
|
|
|
// making the bit stream:
|
|
|
|
//
|
|
|
|
// 1111 1111 0011 1111 1100 1111 1111 0011
|
|
|
|
//
|
|
|
|
// That's three FF sync bytes in a row. Assuming
|
|
|
|
// the sequencer is in state 2, each sync byte takes
|
|
|
|
// 32 clock cycles to read, is held for 8 clock
|
|
|
|
// cycles while the extra zeros are shifted in, then
|
|
|
|
// is held 8 more clock cycles while the sequencer
|
|
|
|
// reads the next two bits.
|
|
|
|
cycles += 40; // shift 10 bits
|
|
|
|
const nibble = diskII.ioSwitch(0x8c); // read data
|
|
|
|
expect(nibble).toBe(0xFF);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('reads several FF sync bytes', () => {
|
|
|
|
let cycles: number = 0;
|
|
|
|
mocked(mockApple2IO).cycles.mockImplementation(() => cycles);
|
|
|
|
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x8e); // read mode
|
|
|
|
|
|
|
|
// The initial bytes in the image are: FF 3F CF F3
|
|
|
|
// making the bit stream:
|
|
|
|
//
|
|
|
|
// 1111 1111 0011 1111 1100 1111 1111 0011
|
|
|
|
//
|
|
|
|
// That's three FF sync bytes in a row. Assuming
|
|
|
|
// the sequencer is in state 2, each sync byte takes
|
|
|
|
// 32 clock cycles to read, is held for 8 clock
|
|
|
|
// cycles while the extra zeros are shifted in, then
|
|
|
|
// is held 8 more clock cycles while the sequencer
|
|
|
|
// reads the next two bits. This means that 3 sync
|
|
|
|
// bytes will be available for 3 * 40 + 8 cycles.
|
|
|
|
for (let i = 0; i < 3 * 40 + 8; i++) {
|
|
|
|
cycles++;
|
|
|
|
const nibble = diskII.ioSwitch(0x8c); // read data
|
|
|
|
if (nibble & 0x80) {
|
|
|
|
// Nibbles are only valid when the high bit is set.
|
|
|
|
// eslint-disable-next-line jest/no-conditional-expect
|
|
|
|
expect(nibble).toBe(0xFF);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
it('reads random garbage on uninitialized tracks', () => {
|
|
|
|
let cycles: number = 0;
|
|
|
|
mocked(mockApple2IO).cycles.mockImplementation(() => cycles);
|
|
|
|
|
|
|
|
const diskII = new DiskII(mockApple2IO, callbacks);
|
|
|
|
diskII.setBinary(1, 'DOS 3.3 System Master', 'woz', DOS33_SYSTEM_MASTER_IMAGE);
|
|
|
|
|
|
|
|
// Step to track 0.5
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
diskII.ioSwitch(0x81); // coil 0 on
|
|
|
|
diskII.ioSwitch(0x83); // coil 1 on
|
|
|
|
diskII.ioSwitch(0x80); // coil 0 off
|
|
|
|
diskII.ioSwitch(0x82); // coil 1 off
|
|
|
|
diskII.ioSwitch(0x8e); // read mode
|
|
|
|
|
|
|
|
// Read 5 nibbles
|
|
|
|
const nibbles: byte[] = [];
|
|
|
|
let read = false;
|
|
|
|
while (nibbles.length < 5) {
|
|
|
|
cycles++;
|
|
|
|
const nibble = diskII.ioSwitch(0x8c); // read data
|
|
|
|
const qa = nibble & 0x80;
|
|
|
|
if (qa && !read) {
|
|
|
|
nibbles.push(nibble);
|
|
|
|
read = true;
|
|
|
|
}
|
|
|
|
if (!qa && read) {
|
|
|
|
read = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Test that the first doesn't equal any of the others.
|
|
|
|
// (Yes, this test could fail with some bad luck.)
|
|
|
|
let equal = false;
|
|
|
|
for (let i = 1; i < 5; i++) {
|
|
|
|
equal ||= nibbles[0] === nibbles[i];
|
|
|
|
}
|
|
|
|
expect(equal).not.toBeTruthy();
|
|
|
|
});
|
2022-06-23 13:38:36 +00:00
|
|
|
|
|
|
|
it('disk spins at a consistent speed', () => {
|
|
|
|
const reader = new TestDiskReader(1, 'DOS 3.3 System Master', DOS33_SYSTEM_MASTER_IMAGE, mockApple2IO, callbacks);
|
|
|
|
|
|
|
|
reader.diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
reader.diskII.ioSwitch(0x8e); // read mode
|
|
|
|
|
|
|
|
// Find track 0, sector 0
|
|
|
|
reader.findSector(0);
|
|
|
|
// Save the start cycles
|
|
|
|
let lastCycles = mockApple2IO.cycles();
|
|
|
|
// Find track 0, sector 0 again
|
|
|
|
reader.findSector(0);
|
|
|
|
let currentCycles = reader.cycles;
|
|
|
|
expect(currentCycles - lastCycles).toBe(201216);
|
|
|
|
lastCycles = currentCycles;
|
|
|
|
// Find track 0, sector 0 once again
|
|
|
|
reader.findSector(0);
|
|
|
|
currentCycles = reader.cycles;
|
|
|
|
expect(currentCycles - lastCycles).toBe(201216);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('writing WOZ-based disks', () => {
|
|
|
|
const DOS33_SYSTEM_MASTER_IMAGE =
|
|
|
|
fs.readFileSync('test/js/cards/data/DOS 3.3 System Master.woz').buffer;
|
|
|
|
|
|
|
|
it('can write something', () => {
|
|
|
|
const reader = new TestDiskReader(1, 'DOS 3.3 System Master', DOS33_SYSTEM_MASTER_IMAGE, mockApple2IO, callbacks);
|
|
|
|
const diskII = reader.diskII;
|
|
|
|
const before = reader.rawTracks();
|
|
|
|
|
|
|
|
diskII.ioSwitch(0x89); // turn on the motor
|
|
|
|
|
|
|
|
// emulate STA $C08F,X (5 CPU cycles)
|
|
|
|
reader.cycles += 4; // op + load address + work
|
|
|
|
diskII.tick();
|
|
|
|
reader.cycles += 1;
|
|
|
|
diskII.ioSwitch(0x8F, 0x80); // write
|
|
|
|
// read $C08C,X
|
|
|
|
reader.cycles += 4; // op + load address + work
|
|
|
|
diskII.tick();
|
|
|
|
reader.cycles += 1;
|
|
|
|
diskII.ioSwitch(0x8C); // shift
|
|
|
|
|
|
|
|
reader.cycles += 29; // wait
|
|
|
|
diskII.tick(); // nop (make sure the change is applied)
|
|
|
|
|
|
|
|
const after = reader.rawTracks();
|
|
|
|
expect(before).not.toEqual(after);
|
|
|
|
});
|
2022-06-18 23:54:33 +00:00
|
|
|
});
|
|
|
|
});
|
2022-06-23 13:38:36 +00:00
|
|
|
|
|
|
|
class TestDiskReader {
|
|
|
|
cycles: number = 0;
|
|
|
|
nibbles = 0;
|
|
|
|
diskII: DiskII;
|
|
|
|
|
2022-10-01 18:15:57 +00:00
|
|
|
constructor(drive: DriveNumber, label: string, image: ArrayBufferLike, apple2IO: Apple2IO, callbacks: Callbacks) {
|
2022-06-23 13:38:36 +00:00
|
|
|
mocked(apple2IO).cycles.mockImplementation(() => this.cycles);
|
|
|
|
|
|
|
|
this.diskII = new DiskII(apple2IO, callbacks);
|
2022-10-01 18:15:57 +00:00
|
|
|
this.diskII.setBinary(drive, label, 'woz', image);
|
2022-06-23 13:38:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
readNibble(): byte {
|
|
|
|
let result: number = 0;
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
|
|
this.cycles++;
|
|
|
|
const nibble = this.diskII.ioSwitch(0x8c); // read data
|
|
|
|
if (nibble & 0x80) {
|
|
|
|
result = nibble;
|
|
|
|
} else if (result & 0x80) {
|
|
|
|
this.nibbles++;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw new Error('Did not find a nibble in 100 clock cycles');
|
|
|
|
}
|
|
|
|
|
|
|
|
findAddressField() {
|
|
|
|
let s = '';
|
|
|
|
for (let i = 0; i < 600; i++) {
|
|
|
|
let nibble = this.readNibble();
|
|
|
|
if (nibble !== 0xD5) {
|
|
|
|
s += ` ${toHex(nibble)}`;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
nibble = this.readNibble();
|
|
|
|
if (nibble !== 0xAA) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
nibble = this.readNibble();
|
|
|
|
if (nibble !== 0x96) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
throw new Error(`Did not find an address field in 500 nibbles: ${s}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
nextSector() {
|
|
|
|
this.findAddressField();
|
|
|
|
const volume = (this.readNibble() << 1 | 1) & this.readNibble();
|
|
|
|
const track = (this.readNibble() << 1 | 1) & this.readNibble();
|
|
|
|
const sector = (this.readNibble() << 1 | 1) & this.readNibble();
|
|
|
|
// console.log(`vol: ${volume} trk: ${track} sec: ${thisSector} ${this.diskII.head()} ${this.nibbles}`);
|
|
|
|
return { volume, track, sector };
|
|
|
|
}
|
|
|
|
|
|
|
|
findSector(sector: byte) {
|
|
|
|
for (let i = 0; i < 32; i++) {
|
|
|
|
const { sector: thisSector } = this.nextSector();
|
|
|
|
if (sector === thisSector) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw new Error(`Did not find sector ${sector} in 32 sectors`);
|
|
|
|
}
|
|
|
|
|
|
|
|
rawTracks() {
|
|
|
|
// NOTE(flan): Hack to access private properties.
|
2022-10-01 18:15:57 +00:00
|
|
|
const disk = (this.diskII as unknown as { curDisk: WozDisk }).curDisk;
|
2022-06-23 13:38:36 +00:00
|
|
|
const result: Uint8Array[] = [];
|
Split disk data out into its own record (#158)
* Harmonize drive and disk type hierarchies
Before, the `XXXDrive` and `XXXDisk` type hierarchies were similar,
but not exactly the same. For example, `encoding` and `format` were
missing on some `XXXDisk` types where they existed on the `XXXDrive`
type. This change attempts to bring the hierarchies closer together.
However, the biggest visible consequence is the introduction of the
`FLOPPY_FORMATS` array and its associated `FloppyFormat` type. This
replaces `NIBBLE_FORMATS` in most places. A couple of new type guards
for disk formats and disks have been added as well.
All tests pass, everything compiles with no errors, and both WOZ and
nibble format disks load in the emulator.
* Move disk data to a `disk` field in the drive
Before, disk data was mixed in with state about the drive itself (like
track, motor phase, etc.). This made it hard to know exactly what data
was necessary for different image formats.
Now, the disk data is in a `disk` field whose type depends on the
drive type. This makes responisbility a bit easier.
One oddity, though, is that the `Drive` has metadata _and_ the `Disk`
has metadata. When a disk is in the drive, these should be `===`, but
when there is no disk in the drive, obviously only the drive metadata
is set.
All tests pass, everything compiles, and both WOZ and nibble disks
work in the emulator (both preact and classic).
* Squash the `Drive` type hierarchy
Before, the type of the drive depended on the type of the disk in the
drive. Thus, `NibbleDrive` contained a `NibbleDisk` and a `WozDrive`
contained a `WozDisk`. With the extraction of the disk data to a
single field, this type hierarchy makes no sense. Instead, it
suffices to check the type of the disk.
This change removes the `NibbleDrive` and `WozDrive` types and type
guards, checking the disk type where necessary. This change also
introduces the `NoFloppyDisk` type to represent the lack of a
disk. This allows the drive to have metadata, for one.
All tests pass, everything compiles, and both WOZ and nibble disks
work locally.
* Use more destructuring assignment
Now, more places use constructs like:
```TypeScript
const { metadata, readOnly, track, head, phase, dirty } = drive;
return {
disk: getDiskState(drive.disk),
metadata: {...metadata},
readOnly,
track,
head,
phase,
dirty,
};
```
* Remove the `Disk` object from the `Drive` object
This change splits out the disk objects into a record parallel to the
drive objects. The idea is that the `Drive` structure becomes a
representation of the state of the drive that is separate from the
disk image actually in the drive. This helps in an upcoming
refactoring.
This also changes the default empty disks to be writable. While odd,
the write protect switch should be in the "off" position since there
is no disk pressing on it.
Finally, `insertDisk` now resets the head position to 0 since there is
no way of preserving the head position across disks. (Even in the real
world, the motor-off delay plus spindle spin-down would make it
impossible to know the disk head position with any accuracy.)
2022-09-17 13:41:35 +00:00
|
|
|
for (let i = 0; i < disk.rawTracks.length; i++) {
|
|
|
|
result[i] = disk.rawTracks[i].slice(0);
|
2022-06-23 13:38:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|