diff --git a/js/cards/disk2.ts b/js/cards/disk2.ts index 9b58473..a569719 100644 --- a/js/cards/disk2.ts +++ b/js/cards/disk2.ts @@ -409,8 +409,46 @@ export default class DiskII implements Card { // debug(..._args); } - // Only used for WOZ disks + public head(): number { + return this.cur.head; + } + + /** + * 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 @@ -685,7 +723,7 @@ export default class DiskII implements Card { break; case LOC.DRIVEWRITEMODE: // 0x0f (Q7H) this.debug('Write Mode'); - this.q7 = false; + this.q7 = true; this.writeMode = true; break; diff --git a/test/js/cards/disk2.spec.ts b/test/js/cards/disk2.spec.ts index 62269b2..90df011 100644 --- a/test/js/cards/disk2.spec.ts +++ b/test/js/cards/disk2.spec.ts @@ -4,7 +4,9 @@ import fs from 'fs'; import Apple2IO from 'js/apple2io'; import DiskII, { Callbacks } from 'js/cards/disk2'; import CPU6502 from 'js/cpu6502'; +import { DriveNumber } from 'js/formats/types'; import { byte } from 'js/types'; +import { toHex } from 'js/util'; import { VideoModes } from 'js/videomodes'; import { mocked } from 'ts-jest/utils'; import { BYTES_BY_SECTOR_IMAGE, BYTES_BY_TRACK_IMAGE } from '../formats/testdata/16sector'; @@ -694,5 +696,135 @@ describe('DiskII', () => { } expect(equal).not.toBeTruthy(); }); + + 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); + }); }); }); + +class TestDiskReader { + cycles: number = 0; + nibbles = 0; + diskII: DiskII; + + constructor(drive: DriveNumber, label: string, image: ArrayBufferLike, apple2IO: Apple2IO, callbacks: Callbacks) { + mocked(apple2IO).cycles.mockImplementation(() => this.cycles); + + this.diskII = new DiskII(apple2IO, callbacks); + this.diskII.setBinary(drive, label, 'woz', image); + } + + 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. + const disk = this.diskII as unknown as { cur: { rawTracks: Uint8Array[] } }; + const result: Uint8Array[] = []; + for (let i = 0; i < disk.cur.rawTracks.length; i++) { + result[i] = disk.cur.rawTracks[i].slice(0); + } + + return result; + } +}