Give the disk four drives, behind one controller
SEVERAL DISKS ARE ONE CONTROLLER AND NOT SEVERAL DEVICES, and the instruction set decided that rather than taste. A port is an immediate byte inside the OUT that names it - portOut takes it from Program Memory - so a program cannot compute one. "The disk on port 0x20 plus drive times four" is not something this machine can say, and two disks as two devices would mean a branch on the drive number in all eleven places sbfs.asm names a disk port. A drive register is what a floppy controller has always been. 0x24 Drive, which the block, command and status registers refer to 0x25 Drives, read only: how many are plugged in --disk given more than once fills them in order. What is per drive is the image, its size and its write protection; the block register, the status and the one buffer belong to the controller, which is the same division real hardware makes. A drive that is not there is refused rather than wrapped, because wrapping means a program asking for a drive this machine has not got quietly reading the one it has - the same shape of fault as taking a bank number somebody else was using. An EMPTY drive is a different thing and is selectable: a controller has its drives whether or not there are disks in them, and reading one fails with the error bit the way an empty drive should. Changing drives finishes whatever the one being left was in the middle of. A transfer waits for the clock, so one may be owed at any moment, and running it against the disk that is arriving would be a fault with no owner. Also stops parseOptions setting its defaults field by field. It was nine assignments beside a struct, and a list beside a thing drifts from the thing: adding two fields left them holding whatever was on the stack, so a machine given one disk was told it already had four drives. It is one zeroing now, and a default that is not nought can be written under it where it reads as the exception. That struct growing a field once before left Voyager linked against an object that disagreed about its size. Nothing in CosmOS uses any of this yet. The mount record is next. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
3b650cabcd
commit
b1538e0618
+98
-35
@@ -764,8 +764,20 @@ uint8_t refusingPort(void) {
|
||||
// something the host does on its behalf. A disk that understood filenames would be the
|
||||
// emulator doing the work and the machine pretending it had.
|
||||
|
||||
static FILE *diskImage = NULL;
|
||||
static uint32_t diskBlockCount = 0;
|
||||
// ---- What belongs to a drive, and what belongs to the controller ----
|
||||
//
|
||||
// A disk is write protected and has a size; a controller has a block register, a status and
|
||||
// one buffer. So these three are per drive and everything below is not - which is the same
|
||||
// division a real controller makes, and the reason the buffer holding whichever drive was
|
||||
// last read is correct rather than a shortcut.
|
||||
static FILE *diskImage[DISK_DRIVE_COUNT];
|
||||
static uint32_t diskBlockCount[DISK_DRIVE_COUNT];
|
||||
static uint8_t diskProtected[DISK_DRIVE_COUNT];
|
||||
|
||||
// Which one the registers refer to, and how many are plugged in at all.
|
||||
static uint8_t diskDrive = 0;
|
||||
static uint8_t diskDrives = 0;
|
||||
|
||||
static uint8_t diskBuffer[DISK_BLOCK_BYTES];
|
||||
|
||||
// ---- A disk that takes time ----
|
||||
@@ -778,82 +790,96 @@ static uint8_t diskBuffer[DISK_BLOCK_BYTES];
|
||||
// The machine's clock as devices see it, which the emulator advances as the CPU spends
|
||||
// cycles. A device says when it will be finished in these, and is believed.
|
||||
static void diskTransfer(uint8_t command);
|
||||
static void diskSettle(void);
|
||||
|
||||
static unsigned long diskLatency = 0;
|
||||
static unsigned long diskReadyAt = 0;
|
||||
static uint8_t diskPending = 0;
|
||||
static uint16_t diskBlock = 0;
|
||||
static uint8_t diskStatus = 0;
|
||||
static uint8_t diskProtected = 0;
|
||||
|
||||
// Attaching gives the next free drive number, so the order they are named on the command
|
||||
// line is the order the machine has them in.
|
||||
uint8_t attachDisk(const char *path, uint8_t writeProtect) {
|
||||
diskProtected = writeProtect ? 1 : 0;
|
||||
diskImage = fopen(path, "r+b");
|
||||
if (diskImage == NULL) {
|
||||
if (diskDrives >= DISK_DRIVE_COUNT) {
|
||||
fprintf(stderr, "Error: This machine has %d drives.\n", DISK_DRIVE_COUNT);
|
||||
return 1;
|
||||
}
|
||||
const uint8_t at = diskDrives;
|
||||
diskProtected[at] = writeProtect ? 1 : 0;
|
||||
diskImage[at] = fopen(path, "r+b");
|
||||
if (diskImage[at] == NULL) {
|
||||
// It may be there and simply not writable, which is a read only disk rather than
|
||||
// a missing one. Try that before deciding to make a new one.
|
||||
diskImage = fopen(path, "rb");
|
||||
if (diskImage != NULL) {
|
||||
diskProtected = 1;
|
||||
diskImage[at] = fopen(path, "rb");
|
||||
if (diskImage[at] != NULL) {
|
||||
diskProtected[at] = 1;
|
||||
}
|
||||
}
|
||||
if (diskImage == NULL) {
|
||||
if (diskImage[at] == NULL) {
|
||||
// Nothing there, so make one. A fresh image is zeroes, which is what an unwritten
|
||||
// block should read as.
|
||||
diskImage = fopen(path, "w+b");
|
||||
if (diskImage == NULL) {
|
||||
diskImage[at] = fopen(path, "w+b");
|
||||
if (diskImage[at] == NULL) {
|
||||
fprintf(stderr, "Error: Couldn't open or create the disk image: %s\n", path);
|
||||
return 1;
|
||||
}
|
||||
static const uint8_t empty[DISK_BLOCK_BYTES] = {0};
|
||||
for (uint32_t i = 0; i < DISK_DEFAULT_BLOCKS; i++) {
|
||||
if (fwrite(empty, 1, DISK_BLOCK_BYTES, diskImage) != DISK_BLOCK_BYTES) {
|
||||
if (fwrite(empty, 1, DISK_BLOCK_BYTES, diskImage[at]) != DISK_BLOCK_BYTES) {
|
||||
fprintf(stderr, "Error: Couldn't write the disk image: %s\n", path);
|
||||
fclose(diskImage);
|
||||
diskImage = NULL;
|
||||
fclose(diskImage[at]);
|
||||
diskImage[at] = NULL;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fseek(diskImage, 0, SEEK_END) != 0) {
|
||||
if (fseek(diskImage[at], 0, SEEK_END) != 0) {
|
||||
fprintf(stderr, "Error: Couldn't measure the disk image: %s\n", path);
|
||||
fclose(diskImage);
|
||||
diskImage = NULL;
|
||||
fclose(diskImage[at]);
|
||||
diskImage[at] = NULL;
|
||||
return 1;
|
||||
}
|
||||
long size = ftell(diskImage);
|
||||
long size = ftell(diskImage[at]);
|
||||
// A part written block at the end is not a block, so it is not counted.
|
||||
diskBlockCount = (size > 0) ? (uint32_t)(size / DISK_BLOCK_BYTES) : 0;
|
||||
diskBlockCount[at] = (size > 0) ? (uint32_t)(size / DISK_BLOCK_BYTES) : 0;
|
||||
// The protect bit is a standing property, so it reads true before anything has been
|
||||
// asked of the disk rather than only after a write has been turned away.
|
||||
diskStatus = diskProtected ? DISK_STATUS_PROTECTED : 0;
|
||||
// The protect bit is a standing property of the drive now selected, so it reads true
|
||||
// before anything has been asked of it rather than only after a write is turned away.
|
||||
diskDrives++;
|
||||
diskStatus = diskProtected[diskDrive] ? DISK_STATUS_PROTECTED : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void detachDisk(void) {
|
||||
if (diskImage != NULL) {
|
||||
fclose(diskImage);
|
||||
diskImage = NULL;
|
||||
for (int at = 0; at < DISK_DRIVE_COUNT; at++) {
|
||||
if (diskImage[at] != NULL) {
|
||||
fclose(diskImage[at]);
|
||||
diskImage[at] = NULL;
|
||||
}
|
||||
}
|
||||
diskDrives = 0;
|
||||
diskDrive = 0;
|
||||
}
|
||||
|
||||
// Reads or writes the block the block registers name. The line goes up either way: the
|
||||
// operation finished, and whether it worked is what Status is for.
|
||||
static void diskCommand(uint8_t command) {
|
||||
// The protect bit describes the disk rather than the operation, so it survives.
|
||||
diskStatus = diskProtected ? DISK_STATUS_PROTECTED : 0;
|
||||
if (command == DISK_COMMAND_WRITE && diskProtected) {
|
||||
diskStatus = diskProtected[diskDrive] ? DISK_STATUS_PROTECTED : 0;
|
||||
if (command == DISK_COMMAND_WRITE && diskProtected[diskDrive]) {
|
||||
diskStatus |= DISK_STATUS_ERROR;
|
||||
raiseInterrupt(PORT_DISK);
|
||||
return;
|
||||
}
|
||||
if (diskImage == NULL || diskBlock >= diskBlockCount) {
|
||||
if (diskImage[diskDrive] == NULL || diskBlock >= diskBlockCount[diskDrive]) {
|
||||
diskStatus |= DISK_STATUS_ERROR;
|
||||
raiseInterrupt(PORT_DISK);
|
||||
return;
|
||||
}
|
||||
long offset = (long)diskBlock * DISK_BLOCK_BYTES;
|
||||
if (fseek(diskImage, offset, SEEK_SET) != 0) {
|
||||
if (fseek(diskImage[diskDrive], offset, SEEK_SET) != 0) {
|
||||
diskStatus |= DISK_STATUS_ERROR;
|
||||
raiseInterrupt(PORT_DISK);
|
||||
return;
|
||||
@@ -876,16 +902,32 @@ static void diskCommand(uint8_t command) {
|
||||
// The transfer itself, whenever it happens to happen. The seek is done here rather than at
|
||||
// the command, because nothing else may touch the image in between and doing it twice is
|
||||
// the same answer.
|
||||
// ---- Finishing what a drive was in the middle of ----
|
||||
//
|
||||
// A transfer waits for the clock, so at any moment one may be owed. Changing drives with one
|
||||
// outstanding would run it against the disk that is arriving instead of the one that asked,
|
||||
// so the drive register calls this first and the transfer happens now.
|
||||
//
|
||||
// The waiting is what is given up, not the work. A program that changes drives without
|
||||
// looking at the status bit has not lost anything it had asked for.
|
||||
static void diskSettle(void) {
|
||||
if (diskPending) {
|
||||
const uint8_t command = diskPending;
|
||||
diskPending = 0;
|
||||
diskTransfer(command);
|
||||
}
|
||||
}
|
||||
|
||||
static void diskTransfer(uint8_t command) {
|
||||
size_t moved = 0;
|
||||
long offset = (long)diskBlock * DISK_BLOCK_BYTES;
|
||||
if (fseek(diskImage, offset, SEEK_SET) != 0) {
|
||||
if (fseek(diskImage[diskDrive], offset, SEEK_SET) != 0) {
|
||||
diskStatus |= DISK_STATUS_ERROR;
|
||||
} else if (command == DISK_COMMAND_READ) {
|
||||
moved = fread(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
|
||||
moved = fread(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage[diskDrive]);
|
||||
} else {
|
||||
moved = fwrite(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
|
||||
fflush(diskImage);
|
||||
moved = fwrite(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage[diskDrive]);
|
||||
fflush(diskImage[diskDrive]);
|
||||
}
|
||||
if (moved != DISK_BLOCK_BYTES) {
|
||||
diskStatus |= DISK_STATUS_ERROR;
|
||||
@@ -907,9 +949,7 @@ void deviceTick(unsigned long now) {
|
||||
// so the same program makes the same sound in the same cycles.
|
||||
soundTick(now);
|
||||
if (diskPending && now >= diskReadyAt) {
|
||||
uint8_t command = diskPending;
|
||||
diskPending = 0;
|
||||
diskTransfer(command);
|
||||
diskSettle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1100,6 +1140,27 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
|
||||
case DISK_BLOCK_HIGH: diskBlock = (uint16_t)(DataByte << 8) | (diskBlock & 0x00FF); break;
|
||||
case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break;
|
||||
case DISK_COMMAND: diskCommand(DataByte); break;
|
||||
case DISK_DRIVE:
|
||||
// ---- Choosing which disk the registers mean ----
|
||||
//
|
||||
// Whatever the drive was doing is collected first. A controller told to change
|
||||
// drives in the middle of a transfer has no good answer, and the transfer it was
|
||||
// part way through belongs to the drive being left.
|
||||
//
|
||||
// A number past the end selects nothing rather than wrapping to drive 0. Wrapping
|
||||
// would mean a program asking for a drive that is not there quietly reading the
|
||||
// one that is, which is the same shape of fault as the bank number Grid took: it
|
||||
// succeeds, and the wrong disk answers. So the selection stands and every read of
|
||||
// it says so.
|
||||
diskSettle();
|
||||
if (DataByte < DISK_DRIVE_COUNT) {
|
||||
diskDrive = DataByte;
|
||||
diskStatus = diskProtected[diskDrive] ? DISK_STATUS_PROTECTED : 0;
|
||||
}
|
||||
break;
|
||||
case DISK_DRIVES:
|
||||
// Read only: how many drives there are is a fact about the machine.
|
||||
break;
|
||||
case PORT_MACHINE:
|
||||
// Asked for here and acted on between instructions, because a device cannot
|
||||
// restart the machine from inside the instruction that asked: the CPU is part
|
||||
@@ -1176,6 +1237,8 @@ uint8_t InputHandler(uint8_t Address) {
|
||||
break;
|
||||
case DISK_BLOCK_HIGH: return (uint8_t)(diskBlock >> 8);
|
||||
case DISK_BLOCK_LOW: return (uint8_t)(diskBlock & 0xFF);
|
||||
case DISK_DRIVE: return diskDrive;
|
||||
case DISK_DRIVES: return diskDrives;
|
||||
case DISK_STATUS:
|
||||
// ---- Looking is what answers it ----
|
||||
//
|
||||
|
||||
+19
-1
@@ -66,11 +66,29 @@
|
||||
// that spans more than one port raises its line on its base, which is the rule the
|
||||
// machine has not needed until now: the controller spans sixteen and never interrupts.
|
||||
#define PORT_DISK 0x20
|
||||
#define PORT_DISK_TOP 0x23
|
||||
#define PORT_DISK_TOP 0x25
|
||||
#define DISK_BLOCK_HIGH 0x20
|
||||
#define DISK_BLOCK_LOW 0x21
|
||||
#define DISK_COMMAND 0x22
|
||||
#define DISK_STATUS 0x23
|
||||
|
||||
// ---- Several disks, one controller ----
|
||||
//
|
||||
// NOT SEVERAL DEVICES, and the instruction set is why. A port is an immediate byte inside the
|
||||
// OUT that names it, so a program cannot compute one - "the disk on port 0x20 + drive * 4" is
|
||||
// not something this machine can say. Two disks as two devices would mean a branch per access
|
||||
// in every one of the eleven places the filesystem names a disk port.
|
||||
//
|
||||
// So it is one controller with a drive register, which is what the machines this one is
|
||||
// pretending to be actually had: one floppy controller and four drives behind it. The block,
|
||||
// command and status registers all refer to whichever drive was last selected, and so does
|
||||
// the single buffer - which is honest, and which means a program that changes drives knows
|
||||
// the buffer no longer holds what it thought.
|
||||
#define DISK_DRIVE 0x24
|
||||
#define DISK_DRIVES 0x25
|
||||
|
||||
// Four is a floppy controller's worth. The cost of another is a file handle.
|
||||
#define DISK_DRIVE_COUNT 4
|
||||
// ---- The screen ----
|
||||
//
|
||||
// Sixteen ports, like the controller, and it interrupts on its base the way the disk
|
||||
|
||||
@@ -187,8 +187,14 @@ uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *pro
|
||||
fprintf(stderr, "Error: The boot ROM is not a boot image.\n");
|
||||
return MACHINE_ERROR;
|
||||
}
|
||||
if (options->disk != NULL && attachDisk(options->disk, options->writeProtect)) {
|
||||
return MACHINE_ERROR;
|
||||
// Every drive named, in the order it was named. Write protection is the machine's rather
|
||||
// than a drive's for now: a tab on one floppy and not another is a thing to add when
|
||||
// somebody wants it, and pretending otherwise here would be a promise the option cannot
|
||||
// keep.
|
||||
for (int at = 0; at < options->diskCount; at++) {
|
||||
if (attachDisk(options->disks[at], options->writeProtect)) {
|
||||
return MACHINE_ERROR;
|
||||
}
|
||||
}
|
||||
// The screen starts blank, and starts blank again on a warm restart: video memory is
|
||||
// the device's, and a reset that left last program's screen up would be a reset that
|
||||
|
||||
+23
-10
@@ -57,15 +57,17 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
||||
int opt;
|
||||
int option_index = 0;
|
||||
|
||||
options->debug = 0;
|
||||
options->fast = 0;
|
||||
options->cycles = 0;
|
||||
options->disk = NULL;
|
||||
options->writeProtect = 0;
|
||||
options->diskCycles = 0;
|
||||
options->screen = NULL;
|
||||
options->keyboard = NULL;
|
||||
options->sound = NULL;
|
||||
// ---- Everything off, in one line rather than nine ----
|
||||
//
|
||||
// This was a list of assignments, one per field, and a list beside a struct drifts from
|
||||
// the struct: adding `disks` and `diskCount` left them holding whatever was on the stack,
|
||||
// so a machine given one disk was told it already had four drives. The same struct
|
||||
// growing a field once before left Voyager linking against an object that disagreed
|
||||
// about its size.
|
||||
//
|
||||
// Every default here is nought or nothing, and a default that is not can be written
|
||||
// below this line where it will be read as the exception it is.
|
||||
*options = (EmulatorOptions){0};
|
||||
|
||||
// Parse options
|
||||
while ((opt = getopt_long(argc, argv, "dc:fhD:WL:S:K:N:", long_options, &option_index)) != -1) {
|
||||
@@ -90,7 +92,18 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) {
|
||||
options->fast = 1;
|
||||
break;
|
||||
case 'D':
|
||||
options->disk = optarg;
|
||||
// Each one is the next drive. The first is also left in `disk`, because a
|
||||
// machine with one disk is what almost every caller means and reading it
|
||||
// that way keeps them all unchanged.
|
||||
if (options->diskCount >= DISK_DRIVE_COUNT) {
|
||||
fprintf(stderr, "Error: This machine has %d drives.\n",
|
||||
DISK_DRIVE_COUNT);
|
||||
return 1;
|
||||
}
|
||||
options->disks[options->diskCount++] = optarg;
|
||||
if (options->disk == NULL) {
|
||||
options->disk = optarg;
|
||||
}
|
||||
break;
|
||||
case 'W':
|
||||
options->writeProtect = 1;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#define UTILITY_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "io.h"
|
||||
#include "cpu.h"
|
||||
|
||||
// Results of reading the command line.
|
||||
@@ -20,7 +21,14 @@ typedef struct {
|
||||
uint8_t fast; // Ignore the cycle rate and run as fast as the host allows.
|
||||
unsigned long cycles; // Stop after this many cycles. Zero means run until the program halts.
|
||||
unsigned long diskCycles; // How long a block move takes. Zero is instant, and the default.
|
||||
// ---- The drives, in the order they were named ----
|
||||
//
|
||||
// --disk given more than once fills them in turn, so the first is drive 0 and the machine
|
||||
// has as many as were asked for. One name is the ordinary case and reads exactly as it
|
||||
// did when there could only be one.
|
||||
const char *disk; // Disk image to attach, or NULL for a machine with no disk.
|
||||
const char *disks[DISK_DRIVE_COUNT];
|
||||
int diskCount;
|
||||
uint8_t writeProtect; // Attach the disk read only, the way a tab on a floppy would.
|
||||
const char *screen; // Where to save a picture of the screen when the machine stops.
|
||||
const char *keyboard; // Feed the console from this file as a keyboard, not a terminal.
|
||||
|
||||
Reference in New Issue
Block a user