apple2js/js/cards/smartport.ts

605 lines
18 KiB
TypeScript
Raw Normal View History

import { debug, toHex } from '../util';
import { rom as smartPortRom } from '../roms/cards/smartport';
import { Card, Restorable, byte, word, rom } from '../types';
import { MassStorage, BlockDisk, ENCODING_BLOCK, BlockFormat, MassStorageData } from '../formats/types';
import CPU6502, { CpuState, flags } from '../cpu6502';
import { create2MGFromBlockDisk, HeaderData, read2MGHeader } from '../formats/2mg';
import createBlockDisk from '../formats/block';
import { ProDOSVolume } from '../formats/prodos';
import { dump } from '../formats/prodos/utils';
import { DriveNumber } from '../formats/types';
2021-12-23 04:47:36 +00:00
const ID = 'SMARTPORT.J.S';
export interface SmartPortState {
2022-05-10 15:04:20 +00:00
disks: BlockDisk[];
}
export interface SmartPortOptions {
block: boolean;
}
export interface Callbacks {
driveLight: (drive: DriveNumber, on: boolean) => void;
dirty: (drive: DriveNumber, dirty: boolean) => void;
label: (drive: DriveNumber, name?: string, side?: string) => void;
}
class Address {
lo: byte;
hi: byte;
constructor(private cpu: CPU6502, a: byte | word, b?: byte) {
if (b === undefined) {
this.lo = a & 0xff;
this.hi = a >> 8;
} else {
this.lo = a;
this.hi = b;
}
}
loByte() {
return this.lo;
}
hiByte() {
return this.hi;
}
inc(val: byte) {
return new Address(this.cpu, ((this.hi << 8 | this.lo) + val) & 0xffff);
}
readByte() {
return this.cpu.read(this.hi, this.lo);
}
readWord() {
const readLo = this.readByte();
const readHi = this.inc(1).readByte();
return readHi << 8 | readLo;
}
readAddress() {
const readLo = this.readByte();
const readHi = this.inc(1).readByte();
return new Address(this.cpu, readLo, readHi);
}
writeByte(val: byte) {
this.cpu.write(this.hi, this.lo, val);
}
writeWord(val: word) {
this.writeByte(val & 0xff);
this.inc(1).writeByte(val >> 8);
}
writeAddress(val: Address) {
this.writeByte(val.loByte());
this.inc(1).writeByte(val.hiByte());
}
toString() {
return '$' + toHex(this.hi) + toHex(this.lo);
}
}
// ProDOS zero page locations
const COMMAND = 0x42;
const UNIT = 0x43;
const ADDRESS_LO = 0x44;
// const ADDRESS_HI = 0x45;
const BLOCK_LO = 0x46;
// const BLOCK_HI = 0x47;
2021-07-10 18:44:10 +00:00
// const IO_ERROR = 0x27;
const NO_DEVICE_CONNECTED = 0x28;
const WRITE_PROTECTED = 0x2B;
const DEVICE_OFFLINE = 0x2F;
// const VOLUME_DIRECTORY_NOT_FOUND = 0x45;
// const NOT_A_PRODOS_DISK = 0x52;
// const VOLUME_CONTROL_BLOCK_FULL = 0x55;
// const BAD_BUFFER_ADDRESS = 0x56;
// const DUPLICATE_VOLUME_ONLINE = 0x57;
2021-12-23 04:47:36 +00:00
// Type: Device
// $00: Memory Expansion Card (RAM disk)
// $01: 3.5" disk
// $02: ProFile-type hard disk
// $03: Generic SCSI
// $04: ROM disk
// $05: SCSI CD-ROM
// $06: SCSI tape or other SCSI sequential device
// $07: SCSI hard disk
const DEVICE_TYPE_SCSI_HD = 0x07;
// $08: Reserved
// $09: SCSI printer
// $0A: 5-1/4" disk
// $0B: Reserved
// $0C: Reserved
// $0D: Printer
// $0E: Clock
// $0F: Modem
export default class SmartPort implements Card, MassStorage<BlockFormat>, Restorable<SmartPortState> {
private rom: rom;
private disks: BlockDisk[] = [];
private busy: boolean[] = [];
private busyTimeout: ReturnType<typeof setTimeout>[] = [];
private ext: string[] = [];
private metadata: Array<HeaderData|null> = [];
constructor(
private cpu: CPU6502,
private callbacks: Callbacks | null,
options: SmartPortOptions
) {
if (options?.block) {
2022-05-18 15:19:45 +00:00
const dumbPortRom = new Uint8Array(smartPortRom);
dumbPortRom[0x07] = 0x3C;
this.rom = dumbPortRom;
debug('DumbPort card');
} else {
debug('SmartPort card');
2021-03-14 00:08:24 +00:00
this.rom = smartPortRom;
}
}
2022-05-18 02:08:28 +00:00
private debug(..._args: unknown[]) {
// debug.apply(this, arguments);
}
private driveLight(drive: DriveNumber) {
if (!this.busy[drive]) {
this.busy[drive] = true;
this.callbacks?.driveLight(drive, true);
}
clearTimeout(this.busyTimeout[drive]);
this.busyTimeout[drive] = setTimeout(() => {
this.busy[drive] = false;
this.callbacks?.driveLight(drive, false);
}, 100);
}
/*
* dumpBlock
*/
dumpBlock(drive: DriveNumber, block: number) {
let result = '';
let b;
let jdx;
for (let idx = 0; idx < 32; idx++) {
result += toHex(idx << 4, 4) + ': ';
for (jdx = 0; jdx < 16; jdx++) {
b = this.disks[drive].blocks[block][idx * 16 + jdx];
2022-05-18 15:19:45 +00:00
if (jdx === 8) {
result += ' ';
}
result += toHex(b) + ' ';
}
result += ' ';
for (jdx = 0; jdx < 16; jdx++) {
b = this.disks[drive].blocks[block][idx * 16 + jdx] & 0x7f;
2022-05-18 15:19:45 +00:00
if (jdx === 8) {
result += ' ';
}
if (b >= 0x20 && b < 0x7f) {
result += String.fromCharCode(b);
} else {
result += '.';
}
}
result += '\n';
}
return result;
}
/*
* getDeviceInfo
*/
getDeviceInfo(state: CpuState, drive: DriveNumber) {
if (this.disks[drive]) {
const blocks = this.disks[drive].blocks.length;
state.x = blocks & 0xff;
state.y = blocks >> 8;
state.a = 0;
state.s &= ~flags.C;
} else {
2021-07-10 18:44:10 +00:00
state.a = NO_DEVICE_CONNECTED;
state.s |= flags.C;
}
}
/*
* readBlock
*/
readBlock(state: CpuState, drive: DriveNumber, block: number, buffer: Address) {
this.debug(`read drive=${drive}`);
this.debug(`read buffer=${buffer.toString()}`);
this.debug(`read block=$${toHex(block)}`);
if (!this.disks[drive]?.blocks.length) {
debug('Drive', drive, 'is empty');
2021-07-10 18:44:10 +00:00
state.a = DEVICE_OFFLINE;
state.s |= flags.C;
return;
}
// debug('read', '\n' + dumpBlock(drive, block));
this.driveLight(drive);
for (let idx = 0; idx < 512; idx++) {
buffer.writeByte(this.disks[drive].blocks[block][idx]);
buffer = buffer.inc(1);
}
state.a = 0;
2021-07-10 18:44:10 +00:00
state.s &= ~flags.C;
}
/*
* writeBlock
*/
writeBlock(state: CpuState, drive: DriveNumber, block: number, buffer: Address) {
this.debug(`write drive=${drive}`);
this.debug(`write buffer=${buffer.toString()}`);
this.debug(`write block=$${toHex(block)}`);
if (!this.disks[drive]?.blocks.length) {
debug('Drive', drive, 'is empty');
2021-07-10 18:44:10 +00:00
state.a = DEVICE_OFFLINE;
state.s |= flags.C;
return;
}
if (this.disks[drive].readOnly) {
debug('Drive', drive, 'is write protected');
state.a = WRITE_PROTECTED;
state.s |= flags.C;
return;
}
// debug('write', '\n' + dumpBlock(drive, block));
this.driveLight(drive);
for (let idx = 0; idx < 512; idx++) {
this.disks[drive].blocks[block][idx] = buffer.readByte();
buffer = buffer.inc(1);
}
state.a = 0;
2022-06-13 02:39:03 +00:00
state.s &= ~flags.C;
}
/*
* formatDevice
*/
formatDevice(state: CpuState, drive: DriveNumber) {
2021-07-10 18:44:10 +00:00
if (!this.disks[drive]?.blocks.length) {
debug('Drive', drive, 'is empty');
state.a = DEVICE_OFFLINE;
state.s |= flags.C;
return;
}
if (this.disks[drive].readOnly) {
debug('Drive', drive, 'is write protected');
state.a = WRITE_PROTECTED;
state.s |= flags.C;
return;
}
for (let idx = 0; idx < this.disks[drive].blocks.length; idx++) {
this.disks[drive].blocks[idx] = new Uint8Array();
for (let jdx = 0; jdx < 512; jdx++) {
this.disks[drive].blocks[idx][jdx] = 0;
}
}
state.a = 0;
2021-07-10 18:44:10 +00:00
state.s &= flags.C;
}
private access(off: byte, val: byte) {
let result;
const readMode = val === undefined;
switch (off & 0x8f) {
case 0x80:
if (readMode) {
result = 0;
for (let idx = 0; idx < this.disks.length; idx++) {
result <<= 1;
if (this.disks[idx]) {
result |= 0x01;
}
}
}
break;
}
return result;
}
/*
* Interface
*/
ioSwitch(off: byte, val: byte) {
return this.access(off, val);
}
read(_page: byte, off: byte) {
const state = this.cpu.getState();
let cmd;
let unit;
let buffer;
let block;
const blockOff = this.rom[0xff];
const smartOff = blockOff + 3;
if (off === blockOff && this.cpu.getSync()) { // Regular block device entry POINT
this.debug('block device entry');
cmd = this.cpu.read(0x00, COMMAND);
unit = this.cpu.read(0x00, UNIT);
const bufferAddr = new Address(this.cpu, ADDRESS_LO);
const blockAddr = new Address(this.cpu, BLOCK_LO);
const drive = (unit & 0x80) ? 2 : 1;
const driveSlot = (unit & 0x70) >> 4;
buffer = bufferAddr.readAddress();
block = blockAddr.readWord();
this.debug(`cmd=${cmd}`);
this.debug('unit=$' + toHex(unit));
this.debug(`slot=${driveSlot} drive=${drive}`);
this.debug(`buffer=${buffer.toString()} block=$${toHex(block)}`);
switch (cmd) {
case 0: // INFO
this.getDeviceInfo(state, drive);
break;
case 1: // READ
this.readBlock(state, drive, block, buffer);
break;
case 2: // WRITE
this.writeBlock(state, drive, block, buffer);
break;
case 3: // FORMAT
this.formatDevice(state, drive);
break;
}
2022-05-18 15:19:45 +00:00
} else if (off === smartOff && this.cpu.getSync()) {
this.debug('smartport entry');
const stackAddr = new Address(this.cpu, state.sp + 1, 0x01);
let blocks;
const retVal = stackAddr.readAddress();
this.debug(`return=${retVal.toString()}`);
const cmdBlockAddr = retVal.inc(1);
cmd = cmdBlockAddr.readByte();
const cmdListAddr = cmdBlockAddr.inc(1).readAddress();
this.debug(`cmd=${cmd}`);
this.debug(`cmdListAddr=${cmdListAddr.toString()}`);
stackAddr.writeAddress(retVal.inc(3));
const parameterCount = cmdListAddr.readByte();
unit = cmdListAddr.inc(1).readByte();
const drive = unit ? 2 : 1;
buffer = cmdListAddr.inc(2).readAddress();
let status;
this.debug(`parameterCount=${parameterCount}`);
switch (cmd) {
case 0x00: // INFO
status = cmdListAddr.inc(4).readByte();
this.debug(`info unit=${unit}`);
this.debug(`info buffer=${buffer.toString()}`);
this.debug(`info status=${status}`);
switch (unit) {
case 0:
switch (status) {
case 0:
2021-07-10 18:44:10 +00:00
buffer.writeByte(2); // two devices
buffer.inc(1).writeByte(1 << 6); // no interrupts
2021-12-23 04:47:36 +00:00
buffer.inc(2).writeByte(0x2); // Other vendor
buffer.inc(3).writeByte(0x0); // Other vendor
buffer.inc(4).writeByte(0); // reserved
buffer.inc(5).writeByte(0); // reserved
buffer.inc(6).writeByte(0); // reserved
buffer.inc(7).writeByte(0); // reserved
state.x = 8;
state.y = 0;
state.a = 0;
2021-07-10 18:44:10 +00:00
state.s &= ~flags.C;
break;
}
break;
default: // Unit 1
switch (status) {
case 0:
2021-12-23 04:47:36 +00:00
blocks = this.disks[unit]?.blocks.length ?? 0;
buffer.writeByte(0xf0); // W/R Block device in drive
buffer.inc(1).writeByte(blocks & 0xff); // 1600 blocks
buffer.inc(2).writeByte((blocks & 0xff00) >> 8);
buffer.inc(3).writeByte((blocks & 0xff0000) >> 16);
state.x = 4;
state.y = 0;
state.a = 0;
2021-07-10 18:44:10 +00:00
state.s &= ~flags.C;
break;
2021-12-23 04:47:36 +00:00
case 3:
blocks = this.disks[unit]?.blocks.length ?? 0;
buffer.writeByte(0xf0); // W/R Block device in drive
buffer.inc(1).writeByte(blocks & 0xff); // Blocks low byte
buffer.inc(2).writeByte((blocks & 0xff00) >> 8); // Blocks middle byte
buffer.inc(3).writeByte((blocks & 0xff0000) >> 16); // Blocks high byte
buffer.inc(4).writeByte(ID.length); // Vendor ID length
for (let idx = 0; idx < ID.length; idx++) { // Vendor ID
buffer.inc(5 + idx).writeByte(ID.charCodeAt(idx));
}
buffer.inc(21).writeByte(DEVICE_TYPE_SCSI_HD); // Device Type
buffer.inc(22).writeByte(0x0); // Device Subtype
buffer.inc(23).writeWord(0x0101); // Version
state.x = 24;
state.y = 0;
state.a = 0;
state.s &= ~flags.C;
break;
}
break;
}
state.a = 0;
2021-07-10 18:44:10 +00:00
state.s &= ~flags.C;
break;
case 0x01: // READ BLOCK
block = cmdListAddr.inc(4).readWord();
this.readBlock(state, drive, block, buffer);
break;
case 0x02: // WRITE BLOCK
block = cmdListAddr.inc(4).readWord();
this.writeBlock(state, drive, block, buffer);
break;
case 0x03: // FORMAT
this.formatDevice(state, drive);
break;
case 0x04: // CONTROL
break;
case 0x05: // INIT
break;
case 0x06: // OPEN
break;
case 0x07: // CLOSE
break;
case 0x08: // READ
break;
case 0x09: // WRITE
break;
}
}
this.cpu.setState(state);
return this.rom[off];
}
write() {
// not writable
}
getState() {
return {
disks: this.disks.map(
(disk) => {
const result: BlockDisk = {
blocks: disk.blocks.map(
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
readOnly: disk.readOnly,
name: disk.name,
};
return result;
}
)
};
}
setState(state: SmartPortState) {
this.disks = state.disks.map(
(disk) => {
const result: BlockDisk = {
blocks: disk.blocks.map(
(block) => new Uint8Array(block)
),
encoding: ENCODING_BLOCK,
readOnly: disk.readOnly,
name: disk.name,
};
return result;
}
);
}
setBinary(drive: DriveNumber, name: string, fmt: string, rawData: ArrayBuffer) {
const volume = 254;
const readOnly = false;
2022-05-18 15:19:45 +00:00
if (fmt === '2mg') {
const header = read2MGHeader(rawData);
this.metadata[drive] = header;
const { bytes, offset } = header;
rawData = rawData.slice(offset, offset + bytes);
} else {
this.metadata[drive] = null;
}
const options = {
rawData,
name,
readOnly,
volume,
};
this.ext[drive] = fmt;
this.disks[drive] = createBlockDisk(options);
this.callbacks?.label(drive, name);
const prodos = new ProDOSVolume(this.disks[drive]);
dump(prodos);
return true;
}
getBinary(drive: number): MassStorageData | null {
if (!this.disks[drive]) {
return null;
}
const disk = this.disks[drive];
const ext = this.ext[drive];
const { name } = disk;
let data: ArrayBuffer;
if (ext === '2mg') {
data = create2MGFromBlockDisk(this.metadata[drive], disk);
} else {
const { blocks } = disk;
const byteArray = new Uint8Array(blocks.length * 512);
for (let idx = 0; idx < blocks.length; idx++) {
byteArray.set(blocks[idx], idx * 512);
}
data = byteArray.buffer;
}
return {
name,
ext,
data,
};
}
}