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
@@ -0,0 +1,75 @@
|
|||||||
|
; Choosing which disk the registers mean.
|
||||||
|
;
|
||||||
|
; SEVERAL DISKS ARE ONE CONTROLLER AND NOT SEVERAL DEVICES, and the instruction set is the
|
||||||
|
; reason. A port is an immediate byte inside the OUT that names it, so a program cannot
|
||||||
|
; compute one - "the disk on port 0x20 plus drive times four" is not a thing this machine can
|
||||||
|
; say. Two disks as two devices would mean a branch per access in every place the filesystem
|
||||||
|
; names a disk port.
|
||||||
|
;
|
||||||
|
; So the drive is a register, the way a floppy controller has always done it.
|
||||||
|
;
|
||||||
|
; Written by Anachronaut
|
||||||
|
|
||||||
|
#Program
|
||||||
|
|
||||||
|
start:
|
||||||
|
; How many are plugged in. A fact about the machine, so it is read only.
|
||||||
|
INA 0x25
|
||||||
|
INIB 0d48
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
OUTQ 0x00
|
||||||
|
|
||||||
|
INIA 0d1
|
||||||
|
OUTA 0x24
|
||||||
|
INA 0x24
|
||||||
|
CCF
|
||||||
|
ADD ; B is still 48, from the count above.
|
||||||
|
OUTQ 0x00 ; It took.
|
||||||
|
|
||||||
|
; ---- A drive that is not there is refused, not wrapped ----
|
||||||
|
;
|
||||||
|
; Wrapping to drive 0 would mean a program asking for a drive this machine does not have
|
||||||
|
; quietly reading the one it does. That is the same shape of fault as taking a bank number
|
||||||
|
; somebody else is using: it succeeds, and the wrong disk answers.
|
||||||
|
INIA 0d9
|
||||||
|
OUTA 0x24
|
||||||
|
INA 0x24
|
||||||
|
CCF
|
||||||
|
ADD
|
||||||
|
OUTQ 0x00 ; Still 1.
|
||||||
|
|
||||||
|
; ---- And reading follows the selection ----
|
||||||
|
;
|
||||||
|
; Drive 1 has no disk in it here, which is not the same as not existing: the controller has
|
||||||
|
; four drives whether or not there are disks in them, so this selects, reads, and fails.
|
||||||
|
INIA 0d1
|
||||||
|
OUTA 0x24
|
||||||
|
RSTA
|
||||||
|
OUTA 0x20
|
||||||
|
OUTA 0x21
|
||||||
|
INIA 0x01
|
||||||
|
OUTA 0x22
|
||||||
|
waitDisk:
|
||||||
|
INA 0x23
|
||||||
|
INIB 0x01
|
||||||
|
AND
|
||||||
|
BNQ waitDisk
|
||||||
|
INA 0x23
|
||||||
|
INIB 0x02
|
||||||
|
AND
|
||||||
|
BRQ readWorked
|
||||||
|
INIA 0d78 ; 'N', which is what an empty drive should give.
|
||||||
|
OUTA 0x00
|
||||||
|
BRI done
|
||||||
|
readWorked:
|
||||||
|
INIA 0d89
|
||||||
|
OUTA 0x00
|
||||||
|
done:
|
||||||
|
INIA 0d10
|
||||||
|
OUTA 0x00
|
||||||
|
HALT
|
||||||
|
|
||||||
|
#Vectors
|
||||||
|
|
||||||
|
Boot start
|
||||||
+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
|
// 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.
|
// emulator doing the work and the machine pretending it had.
|
||||||
|
|
||||||
static FILE *diskImage = NULL;
|
// ---- What belongs to a drive, and what belongs to the controller ----
|
||||||
static uint32_t diskBlockCount = 0;
|
//
|
||||||
|
// 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];
|
static uint8_t diskBuffer[DISK_BLOCK_BYTES];
|
||||||
|
|
||||||
// ---- A disk that takes time ----
|
// ---- 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
|
// 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.
|
// cycles. A device says when it will be finished in these, and is believed.
|
||||||
static void diskTransfer(uint8_t command);
|
static void diskTransfer(uint8_t command);
|
||||||
|
static void diskSettle(void);
|
||||||
|
|
||||||
static unsigned long diskLatency = 0;
|
static unsigned long diskLatency = 0;
|
||||||
static unsigned long diskReadyAt = 0;
|
static unsigned long diskReadyAt = 0;
|
||||||
static uint8_t diskPending = 0;
|
static uint8_t diskPending = 0;
|
||||||
static uint16_t diskBlock = 0;
|
static uint16_t diskBlock = 0;
|
||||||
static uint8_t diskStatus = 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) {
|
uint8_t attachDisk(const char *path, uint8_t writeProtect) {
|
||||||
diskProtected = writeProtect ? 1 : 0;
|
if (diskDrives >= DISK_DRIVE_COUNT) {
|
||||||
diskImage = fopen(path, "r+b");
|
fprintf(stderr, "Error: This machine has %d drives.\n", DISK_DRIVE_COUNT);
|
||||||
if (diskImage == NULL) {
|
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
|
// 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.
|
// a missing one. Try that before deciding to make a new one.
|
||||||
diskImage = fopen(path, "rb");
|
diskImage[at] = fopen(path, "rb");
|
||||||
if (diskImage != NULL) {
|
if (diskImage[at] != NULL) {
|
||||||
diskProtected = 1;
|
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
|
// Nothing there, so make one. A fresh image is zeroes, which is what an unwritten
|
||||||
// block should read as.
|
// block should read as.
|
||||||
diskImage = fopen(path, "w+b");
|
diskImage[at] = fopen(path, "w+b");
|
||||||
if (diskImage == NULL) {
|
if (diskImage[at] == NULL) {
|
||||||
fprintf(stderr, "Error: Couldn't open or create the disk image: %s\n", path);
|
fprintf(stderr, "Error: Couldn't open or create the disk image: %s\n", path);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
static const uint8_t empty[DISK_BLOCK_BYTES] = {0};
|
static const uint8_t empty[DISK_BLOCK_BYTES] = {0};
|
||||||
for (uint32_t i = 0; i < DISK_DEFAULT_BLOCKS; i++) {
|
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);
|
fprintf(stderr, "Error: Couldn't write the disk image: %s\n", path);
|
||||||
fclose(diskImage);
|
fclose(diskImage[at]);
|
||||||
diskImage = NULL;
|
diskImage[at] = NULL;
|
||||||
return 1;
|
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);
|
fprintf(stderr, "Error: Couldn't measure the disk image: %s\n", path);
|
||||||
fclose(diskImage);
|
fclose(diskImage[at]);
|
||||||
diskImage = NULL;
|
diskImage[at] = NULL;
|
||||||
return 1;
|
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.
|
// 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
|
// 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.
|
// 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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void detachDisk(void) {
|
void detachDisk(void) {
|
||||||
if (diskImage != NULL) {
|
for (int at = 0; at < DISK_DRIVE_COUNT; at++) {
|
||||||
fclose(diskImage);
|
if (diskImage[at] != NULL) {
|
||||||
diskImage = 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
|
// 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.
|
// operation finished, and whether it worked is what Status is for.
|
||||||
static void diskCommand(uint8_t command) {
|
static void diskCommand(uint8_t command) {
|
||||||
// The protect bit describes the disk rather than the operation, so it survives.
|
// The protect bit describes the disk rather than the operation, so it survives.
|
||||||
diskStatus = diskProtected ? DISK_STATUS_PROTECTED : 0;
|
diskStatus = diskProtected[diskDrive] ? DISK_STATUS_PROTECTED : 0;
|
||||||
if (command == DISK_COMMAND_WRITE && diskProtected) {
|
if (command == DISK_COMMAND_WRITE && diskProtected[diskDrive]) {
|
||||||
diskStatus |= DISK_STATUS_ERROR;
|
diskStatus |= DISK_STATUS_ERROR;
|
||||||
raiseInterrupt(PORT_DISK);
|
raiseInterrupt(PORT_DISK);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (diskImage == NULL || diskBlock >= diskBlockCount) {
|
if (diskImage[diskDrive] == NULL || diskBlock >= diskBlockCount[diskDrive]) {
|
||||||
diskStatus |= DISK_STATUS_ERROR;
|
diskStatus |= DISK_STATUS_ERROR;
|
||||||
raiseInterrupt(PORT_DISK);
|
raiseInterrupt(PORT_DISK);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
long offset = (long)diskBlock * DISK_BLOCK_BYTES;
|
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;
|
diskStatus |= DISK_STATUS_ERROR;
|
||||||
raiseInterrupt(PORT_DISK);
|
raiseInterrupt(PORT_DISK);
|
||||||
return;
|
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 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 command, because nothing else may touch the image in between and doing it twice is
|
||||||
// the same answer.
|
// 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) {
|
static void diskTransfer(uint8_t command) {
|
||||||
size_t moved = 0;
|
size_t moved = 0;
|
||||||
long offset = (long)diskBlock * DISK_BLOCK_BYTES;
|
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;
|
diskStatus |= DISK_STATUS_ERROR;
|
||||||
} else if (command == DISK_COMMAND_READ) {
|
} else if (command == DISK_COMMAND_READ) {
|
||||||
moved = fread(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
|
moved = fread(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage[diskDrive]);
|
||||||
} else {
|
} else {
|
||||||
moved = fwrite(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage);
|
moved = fwrite(diskBuffer, 1, DISK_BLOCK_BYTES, diskImage[diskDrive]);
|
||||||
fflush(diskImage);
|
fflush(diskImage[diskDrive]);
|
||||||
}
|
}
|
||||||
if (moved != DISK_BLOCK_BYTES) {
|
if (moved != DISK_BLOCK_BYTES) {
|
||||||
diskStatus |= DISK_STATUS_ERROR;
|
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.
|
// so the same program makes the same sound in the same cycles.
|
||||||
soundTick(now);
|
soundTick(now);
|
||||||
if (diskPending && now >= diskReadyAt) {
|
if (diskPending && now >= diskReadyAt) {
|
||||||
uint8_t command = diskPending;
|
diskSettle();
|
||||||
diskPending = 0;
|
|
||||||
diskTransfer(command);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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_HIGH: diskBlock = (uint16_t)(DataByte << 8) | (diskBlock & 0x00FF); break;
|
||||||
case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break;
|
case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break;
|
||||||
case DISK_COMMAND: diskCommand(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:
|
case PORT_MACHINE:
|
||||||
// Asked for here and acted on between instructions, because a device cannot
|
// 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
|
// restart the machine from inside the instruction that asked: the CPU is part
|
||||||
@@ -1176,6 +1237,8 @@ uint8_t InputHandler(uint8_t Address) {
|
|||||||
break;
|
break;
|
||||||
case DISK_BLOCK_HIGH: return (uint8_t)(diskBlock >> 8);
|
case DISK_BLOCK_HIGH: return (uint8_t)(diskBlock >> 8);
|
||||||
case DISK_BLOCK_LOW: return (uint8_t)(diskBlock & 0xFF);
|
case DISK_BLOCK_LOW: return (uint8_t)(diskBlock & 0xFF);
|
||||||
|
case DISK_DRIVE: return diskDrive;
|
||||||
|
case DISK_DRIVES: return diskDrives;
|
||||||
case DISK_STATUS:
|
case DISK_STATUS:
|
||||||
// ---- Looking is what answers it ----
|
// ---- 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
|
// 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.
|
// machine has not needed until now: the controller spans sixteen and never interrupts.
|
||||||
#define PORT_DISK 0x20
|
#define PORT_DISK 0x20
|
||||||
#define PORT_DISK_TOP 0x23
|
#define PORT_DISK_TOP 0x25
|
||||||
#define DISK_BLOCK_HIGH 0x20
|
#define DISK_BLOCK_HIGH 0x20
|
||||||
#define DISK_BLOCK_LOW 0x21
|
#define DISK_BLOCK_LOW 0x21
|
||||||
#define DISK_COMMAND 0x22
|
#define DISK_COMMAND 0x22
|
||||||
#define DISK_STATUS 0x23
|
#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 ----
|
// ---- The screen ----
|
||||||
//
|
//
|
||||||
// Sixteen ports, like the controller, and it interrupts on its base the way the disk
|
// 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");
|
fprintf(stderr, "Error: The boot ROM is not a boot image.\n");
|
||||||
return MACHINE_ERROR;
|
return MACHINE_ERROR;
|
||||||
}
|
}
|
||||||
if (options->disk != NULL && attachDisk(options->disk, options->writeProtect)) {
|
// Every drive named, in the order it was named. Write protection is the machine's rather
|
||||||
return MACHINE_ERROR;
|
// 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 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
|
// 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 opt;
|
||||||
int option_index = 0;
|
int option_index = 0;
|
||||||
|
|
||||||
options->debug = 0;
|
// ---- Everything off, in one line rather than nine ----
|
||||||
options->fast = 0;
|
//
|
||||||
options->cycles = 0;
|
// This was a list of assignments, one per field, and a list beside a struct drifts from
|
||||||
options->disk = NULL;
|
// the struct: adding `disks` and `diskCount` left them holding whatever was on the stack,
|
||||||
options->writeProtect = 0;
|
// so a machine given one disk was told it already had four drives. The same struct
|
||||||
options->diskCycles = 0;
|
// growing a field once before left Voyager linking against an object that disagreed
|
||||||
options->screen = NULL;
|
// about its size.
|
||||||
options->keyboard = NULL;
|
//
|
||||||
options->sound = NULL;
|
// 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
|
// Parse options
|
||||||
while ((opt = getopt_long(argc, argv, "dc:fhD:WL:S:K:N:", long_options, &option_index)) != -1) {
|
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;
|
options->fast = 1;
|
||||||
break;
|
break;
|
||||||
case 'D':
|
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;
|
break;
|
||||||
case 'W':
|
case 'W':
|
||||||
options->writeProtect = 1;
|
options->writeProtect = 1;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#define UTILITY_H
|
#define UTILITY_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
#include "io.h"
|
||||||
#include "cpu.h"
|
#include "cpu.h"
|
||||||
|
|
||||||
// Results of reading the command line.
|
// 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.
|
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 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.
|
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 *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.
|
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 *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.
|
const char *keyboard; // Feed the console from this file as a keyboard, not a terminal.
|
||||||
|
|||||||
@@ -546,7 +546,7 @@ If nothing is installed for the vector a device refused with, the machine stops
|
|||||||
| 0x00 - 0x05 | The console. See The Console. Writing to 0x00 sends a byte to standard output, reading takes one from standard input. It interrupts on 0x00, its base port, when asked to. | 0x02 |
|
| 0x00 - 0x05 | The console. See The Console. Writing to 0x00 sends a byte to standard output, reading takes one from standard input. It interrupts on 0x00, its base port, when asked to. | 0x02 |
|
||||||
| 0x10 | A test device. Writing anything to it puts its own line up, so that interrupt handling can be exercised without waiting on anything. The byte written is ignored. | 0x10 |
|
| 0x10 | A test device. Writing anything to it puts its own line up, so that interrupt handling can be exercised without waiting on anything. The byte written is ignored. | 0x10 |
|
||||||
| 0x11 | A device that refuses everything, in both directions, so that refusal can be exercised without the memory controller. | 0x11 |
|
| 0x11 | A device that refuses everything, in both directions, so that refusal can be exercised without the memory controller. | 0x11 |
|
||||||
| 0x20 - 0x23 | The disk. See Storage. It interrupts on 0x20, its base port. | 0x13 |
|
| 0x20 - 0x25 | The disk. See Storage. It interrupts on 0x20, its base port. | 0x13 |
|
||||||
| 0x13 | The machine itself. Writing 1 asks it to start over: whatever put the first instruction in memory does it again, and the CPU begins where the boot vector points. A port rather than a service, because a reset has to work when the system does not - and a program that owns the whole machine has no system to ask. The disk is not unplugged and keeps what was written to it; the vector table is cleared, because a handler left behind would aim an interrupt into a program that is no longer running. | 0x04 |
|
| 0x13 | The machine itself. Writing 1 asks it to start over: whatever put the first instruction in memory does it again, and the CPU begins where the boot vector points. A port rather than a service, because a reset has to work when the system does not - and a program that owns the whole machine has no system to ask. The disk is not unplugged and keeps what was written to it; the vector table is cleared, because a handler left behind would aim an interrupt into a program that is no longer running. | 0x04 |
|
||||||
| 0x12 | A device that owns 256 bytes of memory. Writing to its port fills that memory with the byte written, standing in for a disk controller reading a sector. Its memory is unreachable until it is registered as a bank. | 0x12 |
|
| 0x12 | A device that owns 256 bytes of memory. Writing to its port fills that memory with the byte written, standing in for a disk controller reading a sector. Its memory is unreachable until it is registered as a bank. | 0x12 |
|
||||||
| 0x30 - 0x3F | The screen. See The Screen. It brings video memory, which is unreachable until it is registered as a bank. | 0x14 |
|
| 0x30 - 0x3F | The screen. See The Screen. It brings video memory, which is unreachable until it is registered as a bank. | 0x14 |
|
||||||
@@ -1185,6 +1185,36 @@ The disk owns one block of memory, its buffer. Reading fills it and writing take
|
|||||||
|
|
||||||
A device that answers on more than one port raises its line on the first of them, so the disk interrupts on 0x20.
|
A device that answers on more than one port raises its line on the first of them, so the disk interrupts on 0x20.
|
||||||
|
|
||||||
|
### Several Disks:
|
||||||
|
|
||||||
|
**One controller with four drives, not four devices**, and the instruction set is the reason.
|
||||||
|
A port is an immediate byte inside the `OUT` that names it, so a program cannot compute one -
|
||||||
|
*the disk on port 0x20 plus drive times four* is not something this machine can say. Two disks
|
||||||
|
as two devices would mean a branch on the drive number in every place a program touches a
|
||||||
|
disk port. So the drive is a register, which is what a floppy controller has always been.
|
||||||
|
|
||||||
|
| Port | Register |
|
||||||
|
| --- | --- |
|
||||||
|
| 0x24 | Drive. Which one the block, command and status registers refer to. Reads back. |
|
||||||
|
| 0x25 | Drives, read only. How many are plugged in. |
|
||||||
|
|
||||||
|
The block, command and status registers, **and the single buffer**, all belong to whichever
|
||||||
|
drive is selected. A program that changes drives is holding a buffer that no longer contains
|
||||||
|
what it thought, and has to say so to itself - the controller cannot know what the program
|
||||||
|
believed.
|
||||||
|
|
||||||
|
A drive that is not there is **refused rather than wrapped**: writing 9 to the drive register
|
||||||
|
leaves the selection where it was, and reading the register says so. Wrapping would mean a
|
||||||
|
program asking for a drive this machine does not have quietly reading the one it does.
|
||||||
|
|
||||||
|
**Selecting an empty drive is allowed**, because a controller has its drives whether or not
|
||||||
|
there are disks in them. Reads from one fail with the error bit, which is what an empty drive
|
||||||
|
should do. `Drives` says how many have disks; the drive register accepts any of the four.
|
||||||
|
|
||||||
|
Changing drives finishes whatever the drive 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 instead of the one that asked for it would be a fault with no owner.
|
||||||
|
|
||||||
### Waiting:
|
### Waiting:
|
||||||
|
|
||||||
A command returns at once and the line goes up when the block has moved. Status bit 0 says the disk is still working.
|
A command returns at once and the line goes up when the block has moved. Status bit 0 says the disk is still working.
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ from `make`, not from here.
|
|||||||
### 1. Recorded output
|
### 1. Recorded output
|
||||||
|
|
||||||
`Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares
|
`Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares
|
||||||
everything it printed against a file in `Tests/expected`. 178 tests, of which 116 run, 35
|
everything it printed against a file in `Tests/expected`. 179 tests, of which 117 run, 35
|
||||||
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
|
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
|
||||||
given at all.
|
given at all.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
111N
|
||||||
|
Execution halted.
|
||||||
|
[exit 0]
|
||||||
@@ -802,6 +802,12 @@ cosmosStartupBad | CosmOS/Source/cosmos.asm | run | cosmosSta
|
|||||||
# So this runs a program by name, then Grid, then the same program again. The second one is
|
# So this runs a program by name, then Grid, then the same program again. The second one is
|
||||||
# the check. dir at the end says the disk is still there to be read.
|
# the check. dir at the end says the disk is still there to be read.
|
||||||
cosmosGrid | CosmOS/Source/cosmos.asm | run | cosmosGrid.in | 60000000 | disks/cosmos.img
|
cosmosGrid | CosmOS/Source/cosmos.asm | run | cosmosGrid.in | 60000000 | disks/cosmos.img
|
||||||
|
# Which disk the registers mean. Several disks are one controller with a drive register
|
||||||
|
# rather than several devices, because a port is an immediate byte inside the instruction
|
||||||
|
# that names it and a program cannot compute one. Run with a single disk, so drive 1 is a
|
||||||
|
# drive that exists with nothing in it - selectable, and failing to read, like an empty
|
||||||
|
# floppy drive.
|
||||||
|
driveSelectTest | testPrograms/driveSelectTest.asm | run | - | 200000 | disks/sbfs.img
|
||||||
printDecimalTest | testPrograms/printDecimalTest.asm | xfail | - | -
|
printDecimalTest | testPrograms/printDecimalTest.asm | xfail | - | -
|
||||||
printDigitTest | testPrograms/printDigitTest.asm | xfail | - | -
|
printDigitTest | testPrograms/printDigitTest.asm | xfail | - | -
|
||||||
printHexTest | testPrograms/printHexTest.asm | xfail | - | -
|
printHexTest | testPrograms/printHexTest.asm | xfail | - | -
|
||||||
|
|||||||
Reference in New Issue
Block a user