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:
Anachronaut
2026-08-31 10:10:38 -04:00
co-authored by Claude Opus 5
parent 3b650cabcd
commit b1538e0618
10 changed files with 272 additions and 50 deletions
+98 -35
View File
@@ -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 ----
//