diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index 7901ea4..c2e5b83 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -853,6 +853,23 @@ lives, and a disk that `make clean` deletes is not a disk of your own. It sits o Delete it by hand if you ever want a fresh one. +### Disks Made Of Memory: + +A drive the machine calls **volatile** loses everything when the machine stops. CosmOS formats +one it cannot read, because a drive whose contents do not survive never had anything to lose, +and mounts it like any other - so `--ram-disk 2048` gives you a working disk with nothing on +it, brought up before you reach a prompt. + +**It leaves every other unreadable drive alone.** An unformatted floppy is not an invitation. +That distinction is the machine's to state and the system's to act on: the hardware says what +a drive *is*, and says nothing about filesystems, which is what leaves room for a system that +would rather have its own. + +The size to format comes from the drive, not from a superblock - a superblock states a size +too, and that is no use on a disk which has not got one. The directory is sixteen blocks, 128 +names, chosen rather than worked out: a scratch disk runs out of names long before it runs out +of room, and this machine cannot divide. + ### Where A Program Is Looked For: Three places, tried in order: diff --git a/Programs/CosmOS/Source/sbfs.asm b/Programs/CosmOS/Source/sbfs.asm index 35d5d99..b728492 100644 --- a/Programs/CosmOS/Source/sbfs.asm +++ b/Programs/CosmOS/Source/sbfs.asm @@ -136,6 +136,138 @@ sbfsMountTooBig: ADD ; Q is not zero: not a disk this will mount. RET +; ---- Making a disk into a filesystem ---- +; +; The machine hands out blocks and says how many; what they mean is the system's business. +; That is the whole reason this exists here rather than only in the host tool: a machine with +; a drive made of memory comes up with a drive full of zeroes, and zeroes are not a +; filesystem. Somebody has to write the first one, and it should be whoever is going to read +; it - which is also what leaves room for a system that would rather have its own. +; +; Q is zero if it worked. Everything on the drive is lost, which is why nothing calls this +; except on a drive the machine has said is volatile. +sbfsFormat: + ; How big it is, which only the drive can say. A superblock would say too, and a disk with + ; no superblock is exactly the case this is for. + INA 0x27 + SETD.1 SbfsScratch + STA.1 + INA 0x28 + INCD.1 + STA.1 + + ; The buffer, cleared, because everything not written below has to be nought and the + ; controller's buffer holds whatever was last read. + SETD.0 SbfsBuffer + RSTB +sbfsFormatClear: + RSTA + STA.0 + INCD.0 + DECB + BNB sbfsFormatClear ; 256 of them: B wraps from nought to 255 and back to nought. + + ; "SBFS", and the version. Two, because a disk made now has directories. + SETD.0 SbfsMagic + SETD.1 SbfsBuffer + INIB 0d4 +sbfsFormatMagic: + LDA.0 + STA.1 + INCD.0 + INCD.1 + DECB + BNB sbfsFormatMagic + INIA 0d2 + STA.1 ; Offset 4: the version. + + ; Offset 6, how many blocks the disk has, as the drive reported it. + SETD.1 SbfsBuffer + DPUP.1 0d06 + SETD.0 SbfsScratch + LDA.0 + STA.1 + INCD.0 + INCD.1 + LDA.0 + STA.1 + + ; Offset 8, where the directory starts: block one, straight after this one. + SETD.1 SbfsBuffer + DPUP.1 0d08 + RSTA + STA.1 + INCD.1 + INIA 0d1 + STA.1 + + ; Offset 10, how many blocks of directory. SIXTEEN, WHICH IS 128 NAMES, and chosen rather + ; than worked out: a scratch disk runs out of names long before it runs out of room, and + ; this machine cannot divide, so a number that fits every size this is used for is worth + ; more than arithmetic to find a better one. + SETD.1 SbfsBuffer + DPUP.1 0d10 + RSTA + STA.1 + INCD.1 + INIA 0d16 + STA.1 + + ; Block nought, written. + SETD.0 SbfsBlock + RSTA + STA.0 + INCD.0 + STA.0 + SETD.1 SbfsBuffer + CALL sbfsBufferIn + CALL sbfsWriteBlock + BNQ sbfsFormatFailed + + ; And the directory cleared, so that no entry is in use. The buffer is already nought + ; everywhere the superblock did not reach, so it is cleared once more and written sixteen + ; times rather than built again each time. + SETD.0 SbfsBuffer + RSTB +sbfsFormatBlank: + RSTA + STA.0 + INCD.0 + DECB + BNB sbfsFormatBlank + + INIA 0d16 + SETD.1 SbfsCount + STA.1 +sbfsFormatDirectory: + SETD.0 SbfsBlock + INCD.0 + LDA.0 + INCA + STA.0 ; Blocks one to sixteen. A directory never crosses 255 here. + SETD.1 SbfsBuffer + CALL sbfsBufferIn + CALL sbfsWriteBlock + BNQ sbfsFormatFailed + SETD.1 SbfsCount + LDA.1 + DECA + STA.1 + BNA sbfsFormatDirectory + + RSTA + RSTB + CCF + ADD + RET + +sbfsFormatFailed: + RSTA + INIB 0d1 + CCF + ADD + RET + ; ---- Every drive the machine has ---- ; ; Asked for rather than assumed: the controller says how many are plugged in, and each is @@ -192,7 +324,29 @@ sbfsMountEach: STA.1 CALL sbfsMount - BNQ sbfsMountNext ; Nothing readable in that drive, and its slot stays empty. + BRQ sbfsMountGot + + ; ---- Nothing readable, and whether that is an invitation ---- + ; + ; A drive the machine calls VOLATILE loses everything when the machine stops, so a volatile + ; drive with no filesystem on it never had one to lose and bringing it up is the system's + ; job. Anything else is somebody's disk: an unformatted floppy is not an invitation, it is + ; a blank floppy, and a system that formatted it on sight would be a system you could not + ; safely put a disk into. + ; + ; THE MACHINE SAYS WHAT THE DRIVE IS AND NOTHING ABOUT FILESYSTEMS. A system that would + ; rather have its own reads the same bit and writes whatever it likes. + INA 0x26 + INIB 0x01 ; VOLATILE + AND + BRQ sbfsMountNext ; Not ours to touch. + + CALL sbfsFormat + BNQ sbfsMountNext + CALL sbfsMount + BNQ sbfsMountNext + +sbfsMountGot: ; Mounted: remember which, and put the live record where it belongs. SETD.1 SbfsDriveAt diff --git a/Source/Emulator/io.c b/Source/Emulator/io.c index 317cef8..0395233 100644 --- a/Source/Emulator/io.c +++ b/Source/Emulator/io.c @@ -774,6 +774,11 @@ static FILE *diskImage[DISK_DRIVE_COUNT]; static uint32_t diskBlockCount[DISK_DRIVE_COUNT]; static uint8_t diskProtected[DISK_DRIVE_COUNT]; +// A drive whose blocks are memory. Everything else about it is a drive: it selects, it reads +// and writes, it has a size, and a filesystem on it is a filesystem. What it does not have is +// a file behind it, so it comes up as zeroes and goes away when the machine does. +static uint8_t *diskMemory[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; @@ -852,12 +857,35 @@ uint8_t attachDisk(const char *path, uint8_t writeProtect) { return 0; } +uint8_t attachRamDisk(uint32_t blocks) { + if (diskDrives >= DISK_DRIVE_COUNT) { + fprintf(stderr, "Error: This machine has %d drives.\n", DISK_DRIVE_COUNT); + return 1; + } + if (blocks == 0) { + fprintf(stderr, "Error: A disk of no blocks is not a disk.\n"); + return 1; + } + const uint8_t at = diskDrives; + diskMemory[at] = calloc(blocks, DISK_BLOCK_BYTES); + if (diskMemory[at] == NULL) { + fprintf(stderr, "Error: Couldn't make a %u block disk in memory.\n", blocks); + return 1; + } + diskBlockCount[at] = blocks; + diskProtected[at] = 0; + diskDrives++; + return 0; +} + void detachDisk(void) { for (int at = 0; at < DISK_DRIVE_COUNT; at++) { if (diskImage[at] != NULL) { fclose(diskImage[at]); diskImage[at] = NULL; } + free(diskMemory[at]); + diskMemory[at] = NULL; } diskDrives = 0; diskDrive = 0; @@ -873,6 +901,23 @@ static void diskCommand(uint8_t command) { raiseInterrupt(PORT_DISK); return; } + if (diskMemory[diskDrive] != NULL) { + // A drive made of memory. The same block, the same 256 bytes, and no seek: what + // makes this worth having is that a program cannot tell except by how fast it was. + if (diskBlock >= diskBlockCount[diskDrive]) { + diskStatus |= DISK_STATUS_ERROR; + raiseInterrupt(PORT_DISK); + return; + } + uint8_t *at = diskMemory[diskDrive] + (size_t)diskBlock * DISK_BLOCK_BYTES; + if (command == DISK_COMMAND_WRITE) { + memcpy(at, diskBuffer, DISK_BLOCK_BYTES); + } else { + memcpy(diskBuffer, at, DISK_BLOCK_BYTES); + } + raiseInterrupt(PORT_DISK); + return; + } if (diskImage[diskDrive] == NULL || diskBlock >= diskBlockCount[diskDrive]) { diskStatus |= DISK_STATUS_ERROR; raiseInterrupt(PORT_DISK); @@ -1276,7 +1321,11 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) { } break; case DISK_DRIVES: - // Read only: how many drives there are is a fact about the machine. + case DISK_FLAGS: + case DISK_SIZE_HIGH: + case DISK_SIZE_LOW: + // Read only: how many drives there are, and what kind each one is, are facts + // about the machine rather than instructions to it. break; case PORT_MACHINE: // Asked for here and acted on between instructions, because a device cannot @@ -1359,6 +1408,12 @@ uint8_t InputHandler(uint8_t Address) { case DISK_BLOCK_LOW: return (uint8_t)(diskBlock & 0xFF); case DISK_DRIVE: return diskDrive; case DISK_DRIVES: return diskDrives; + case DISK_FLAGS: + // What the selected drive IS. Volatile means its contents do not survive the + // machine stopping, which is the one thing a system cannot find out by looking. + return (uint8_t)(diskMemory[diskDrive] != NULL ? DISK_FLAG_VOLATILE : 0); + case DISK_SIZE_HIGH: return (uint8_t)(diskBlockCount[diskDrive] >> 8); + case DISK_SIZE_LOW: return (uint8_t)(diskBlockCount[diskDrive] & 0xFF); case DISK_STATUS: // ---- Looking is what answers it ---- // diff --git a/Source/Emulator/io.h b/Source/Emulator/io.h index 1ddb595..976f8f7 100644 --- a/Source/Emulator/io.h +++ b/Source/Emulator/io.h @@ -66,7 +66,7 @@ // 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 0x25 +#define PORT_DISK_TOP 0x28 #define DISK_BLOCK_HIGH 0x20 #define DISK_BLOCK_LOW 0x21 #define DISK_COMMAND 0x22 @@ -87,6 +87,26 @@ #define DISK_DRIVE 0x24 #define DISK_DRIVES 0x25 +// ---- What a drive IS, which is the machine's business ---- +// +// Not what is on it, which is the system's. A drive backed by memory loses everything when +// the machine stops, and that is the one fact about it a system cannot work out for itself - +// an empty disk and a volatile disk look identical from the outside. +// +// It matters because it is the difference between a drive a system may FORMAT on sight and +// one it must not. An unformatted floppy somebody put in deliberately is not an invitation; +// an unformatted RAM disk is, because there was never anything there to lose. The machine +// says which kind it is and says nothing about filesystems, so a system that would rather +// have its own reads the same bit and does as it likes. +#define DISK_FLAGS 0x26 +#define DISK_FLAG_VOLATILE 0x01 + +// How many blocks the selected drive has. A fact about the drive rather than about what is +// on it - and the one a system needs before it can put anything on it, since the size a +// superblock states is no use on a disk that has not got one yet. +#define DISK_SIZE_HIGH 0x27 +#define DISK_SIZE_LOW 0x28 + // Four is a floppy controller's worth. The cost of another is a file handle. #define DISK_DRIVE_COUNT 4 // ---- The screen ---- @@ -317,6 +337,10 @@ void setDiskLatency(unsigned long cycles); // equivalent of the tab on the side of a floppy. Returns 1 if it could not attach. uint8_t attachDisk(const char *path, uint8_t writeProtect); +// A drive of that many blocks, backed by memory rather than by a file. It comes up as +// zeroes, which is not a filesystem - bringing it up is the system's job. +uint8_t attachRamDisk(uint32_t blocks); + void detachDisk(void); // How many bytes a device's entry in the registry runs to. Reading past the end gives diff --git a/Source/Emulator/machine.c b/Source/Emulator/machine.c index 97839a9..6f6f833 100644 --- a/Source/Emulator/machine.c +++ b/Source/Emulator/machine.c @@ -197,6 +197,12 @@ uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *pro return MACHINE_ERROR; } } + // After the images, so the drive numbers a command line asks for are the order it asks + // in. A disk made of memory is still a drive and still has to be brought up by whatever + // system is running; the machine only supplies the blocks. + if (options->ramDisk > 0 && attachRamDisk((uint32_t)options->ramDisk)) { + 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 // did not happen. diff --git a/Source/Emulator/utility.c b/Source/Emulator/utility.c index 8ff2255..7ddbb1e 100644 --- a/Source/Emulator/utility.c +++ b/Source/Emulator/utility.c @@ -46,6 +46,7 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) { {"cycles", required_argument, 0, 'c'}, {"fast", no_argument, 0, 'f'}, {"disk", required_argument, 0, 'D'}, + {"ram-disk", required_argument, 0, 'R'}, {"write-protect", no_argument, 0, 'W'}, {"disk-cycles", required_argument, 0, 'L'}, {"screen", required_argument, 0, 'S'}, @@ -70,7 +71,7 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) { *options = (EmulatorOptions){0}; // 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:R:", long_options, &option_index)) != -1) { switch (opt) { case 'd': options->debug = 1; @@ -105,6 +106,16 @@ uint8_t parseOptions(int argc, char *argv[], EmulatorOptions *options) { options->disk = optarg; } break; + case 'R': { + char *end; + const unsigned long blocks = strtoul(optarg, &end, 10); + if (*optarg == '\0' || *end != '\0' || blocks == 0) { + fprintf(stderr, "Error: --ram-disk wants a number of blocks.\n"); + return 1; + } + options->ramDisk = blocks; + } + break; case 'W': options->writeProtect = 1; break; diff --git a/Source/Emulator/utility.h b/Source/Emulator/utility.h index 79ed1d7..eb809ce 100644 --- a/Source/Emulator/utility.h +++ b/Source/Emulator/utility.h @@ -29,6 +29,9 @@ typedef struct { const char *disk; // Disk image to attach, or NULL for a machine with no disk. const char *disks[DISK_DRIVE_COUNT]; int diskCount; + // A drive of this many blocks made of memory, taking the next drive number after the + // images above. Zero for a machine without one. + unsigned long ramDisk; 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 26461c6..28b44fb 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 - 0x25 | The disk. See Storage. It interrupts on 0x20, its base port. | 0x13 | +| 0x20 - 0x28 | 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 | @@ -1249,6 +1249,29 @@ disk port. So the drive is a register, which is what a floppy controller has alw | --- | --- | | 0x24 | Drive. Which one the block, command and status registers refer to. Reads back. | | 0x25 | Drives, read only. How many are plugged in. | +| 0x26 | What the selected drive is, read only. Bit 0: its contents do not survive the machine stopping. | +| 0x27, 0x28 | How many blocks the selected drive has, read only. | + +### A Drive Made Of Memory: + +A drive may have memory behind it instead of a file. It selects, reads, writes and has a size +like any other, and a filesystem on it is a filesystem - **a program cannot tell the +difference except by how fast it was.** What it has not got is anything that survives the +machine stopping. + +That difference is the one thing a system cannot work out for itself, because an empty disk +and a volatile disk look identical from outside. So the machine says it, in bit 0 of 0x26, and +says nothing whatever about filesystems. + +**Which is the whole point of saying it that way.** The bit is what separates a drive a system +may format on sight from one it must not: an unformatted floppy somebody put in deliberately +is not an invitation, while an unformatted drive made of memory never had anything to lose. A +system reads the bit and draws its own conclusion - and a system that would rather have a +different filesystem entirely reads the same bit and writes whatever it likes. **The machine +supplies blocks. Bringing them up is the system's job.** + +The size registers exist for the same reason. A superblock states a disk's size too, and that +is no use at all on a disk which has not got one yet. 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 diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index edf7715..f0eb2b6 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`. 184 tests, of which 122 run, 35 +everything it printed against a file in `Tests/expected`. 186 tests, of which 124 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/cosmosBlankDisk.out b/Tests/expected/cosmosBlankDisk.out new file mode 100644 index 0000000..b529edb --- /dev/null +++ b/Tests/expected/cosmosBlankDisk.out @@ -0,0 +1,6 @@ +CosmOS +> drive: nothing this can read is in that drive +> 0 +> halted +Execution halted. +[exit 0] diff --git a/Tests/expected/cosmosRamDisk.out b/Tests/expected/cosmosRamDisk.out new file mode 100644 index 0000000..3a5d02d --- /dev/null +++ b/Tests/expected/cosmosRamDisk.out @@ -0,0 +1,10 @@ +CosmOS +> > 0 files +> copied +finished +> Say.sbx 156 +1 file +> 1 +> halted +Execution halted. +[exit 0] diff --git a/Tests/input/cosmosBlankDisk.in b/Tests/input/cosmosBlankDisk.in new file mode 100644 index 0000000..8aacc30 --- /dev/null +++ b/Tests/input/cosmosBlankDisk.in @@ -0,0 +1,3 @@ +drive 1 +drive +exit diff --git a/Tests/input/cosmosRamDisk.in b/Tests/input/cosmosRamDisk.in new file mode 100644 index 0000000..6ae1169 --- /dev/null +++ b/Tests/input/cosmosRamDisk.in @@ -0,0 +1,6 @@ +drive 1 +dir +Copy 0:/Say.sbx 1:/Say.sbx +dir +drive +exit diff --git a/Tests/manifest b/Tests/manifest index 5f2a1c1..9b46a5d 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -849,6 +849,25 @@ timerTest | testPrograms/timerTest.asm | run | - # at 120 beats a minute - seven and a half frames, so the screen cannot express it at all. # Eight of them is a second, and nearly all of that second is spent asleep. timerBeatTest | testPrograms/timerBeatTest.asm | run | - | 5000000 | - +# ---- A disk made of memory ---- +# +# The machine supplies blocks and says the drive is volatile; the SYSTEM decides what that +# means. CosmOS formats a volatile drive it cannot read, because a drive whose contents do +# not survive the machine never had anything to lose - and leaves any other one alone, which +# is why an unformatted floppy is safe from it. +# +# So this boots with a drive of nothing, and the first dir on it is a listing rather than an +# error: the system brought it up. Then a copy onto it, to show it is a disk like any other. +cosmosRamDisk | CosmOS/Source/cosmos.asm | run | cosmosRamDisk.in | 90000000 | disks/cosmos.img+ram:2048 +# ---- And the other half of that rule, which is the dangerous half ---- +# +# The same blank disk, on a drive the machine has NOT called volatile. It must be refused +# rather than formatted: an unformatted floppy is not an invitation, it is a blank floppy, +# and a system that formatted one on sight is a system you could not safely put a disk into. +# +# blank.img has no directory in its name, so run.sh removes it before every run and the +# emulator makes a fresh one of zeroes - which is exactly the disk this is about. +cosmosBlankDisk | CosmOS/Source/cosmos.asm | run | cosmosBlankDisk.in | 90000000 | disks/cosmos.img+blank.img printDecimalTest | testPrograms/printDecimalTest.asm | xfail | - | - printDigitTest | testPrograms/printDigitTest.asm | xfail | - | - printHexTest | testPrograms/printHexTest.asm | xfail | - | - diff --git a/Tests/run.sh b/Tests/run.sh index d5f8524..493ce74 100755 --- a/Tests/run.sh +++ b/Tests/run.sh @@ -257,6 +257,14 @@ while IFS='|' read -r name src mode stdin limit disk keys; do # become drives 0 upwards in the order written. Each keeps its own :ro and @N, # because those are properties of a disk rather than of the machine. for onedisk in ${disk//+/ }; do + # ---- A drive made of memory ---- + # + # "ram:2048" asks for one of that many blocks instead of naming an image. It has + # no file behind it, so there is nothing to remove between runs and nothing to + # go stale: it is zeroes every time by construction. + case "$onedisk" in + ram:*) EMUARGS+=(--ram-disk "${onedisk#ram:}"); onedisk="-" ;; + esac if [ "$onedisk" != "-" ]; then # A trailing :ro attaches the image write protected, so that a test can # check the device bars writes rather than the filesystem asking nicely.