diff --git a/Programs/testPrograms/driveSelectTest.asm b/Programs/testPrograms/driveSelectTest.asm new file mode 100644 index 0000000..a0f5605 --- /dev/null +++ b/Programs/testPrograms/driveSelectTest.asm @@ -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 diff --git a/Source/Emulator/io.c b/Source/Emulator/io.c index 19e0d7f..88f35ea 100644 --- a/Source/Emulator/io.c +++ b/Source/Emulator/io.c @@ -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 ---- // diff --git a/Source/Emulator/io.h b/Source/Emulator/io.h index c55a19e..9932044 100644 --- a/Source/Emulator/io.h +++ b/Source/Emulator/io.h @@ -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 diff --git a/Source/Emulator/machine.c b/Source/Emulator/machine.c index 8227049..db97a40 100644 --- a/Source/Emulator/machine.c +++ b/Source/Emulator/machine.c @@ -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 diff --git a/Source/Emulator/utility.c b/Source/Emulator/utility.c index 4b1bd44..8ff2255 100644 --- a/Source/Emulator/utility.c +++ b/Source/Emulator/utility.c @@ -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; diff --git a/Source/Emulator/utility.h b/Source/Emulator/utility.h index cffcb30..79ed1d7 100644 --- a/Source/Emulator/utility.h +++ b/Source/Emulator/utility.h @@ -8,6 +8,7 @@ #define UTILITY_H #include +#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. diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index 6fd765f..bfa811b 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -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 | | 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 | -| 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 | | 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 | @@ -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. +### 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: A command returns at once and the line goes up when the block has moved. Status bit 0 says the disk is still working. diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index ad2d5b6..c26e761 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -79,7 +79,7 @@ from `make`, not from here. ### 1. Recorded output `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 given at all. diff --git a/Tests/expected/driveSelectTest.out b/Tests/expected/driveSelectTest.out new file mode 100644 index 0000000..62fb2ad --- /dev/null +++ b/Tests/expected/driveSelectTest.out @@ -0,0 +1,3 @@ +111N +Execution halted. +[exit 0] diff --git a/Tests/manifest b/Tests/manifest index e17e0b6..fe60e9e 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -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 # 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 +# 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 | - | - printDigitTest | testPrograms/printDigitTest.asm | xfail | - | - printHexTest | testPrograms/printHexTest.asm | xfail | - | -