diff --git a/Programs/CosmOS/Apps/Reboot.asm b/Programs/CosmOS/Apps/Reboot.asm new file mode 100644 index 0000000..acf903f --- /dev/null +++ b/Programs/CosmOS/Apps/Reboot.asm @@ -0,0 +1,52 @@ +; Reboot.asm +; Starts the machine again. +; +; Whatever put the first instruction in memory does it again, and everything after that +; follows: the boot slot is read, the loader runs, and whatever the configuration names - +; or whatever Once asked for - is what starts. +; +; ---- Why this writes a port ---- +; +; A reset has to work when the system does not. Something that could only be asked for +; through SWI would be unavailable in exactly the case that wants it most, and a program +; that owns the whole machine has no system to ask. So the machine takes it directly, the +; way it takes everything else: a byte out of a port. +; +; It is still a program rather than a shell word, because it is not the shell's business +; and because a word built into the shell is not callable by anything else. +; +; ---- What survives ---- +; +; The disk, and everything written to it. That is what warm means: the machine starts +; again, the world it starts into does not. Nothing here flushes anything, because nothing +; on this machine is held back - a file is on the disk when the write returns. +; +; Written by Anachronaut + +#Include services.asm + +#Program + + #Base 0x4000 + +start: + SETD.0 Saying + SWI osPrintString + + INIA 0x01 + OUTA 0x13 + + ; Not reached. The machine has started over by the time the next instruction would run, + ; so anything here is a statement about a machine that no longer exists - but a program + ; whose last instruction is an output is one byte from running into whatever follows it, + ; and that is not a habit worth keeping. + RSTA + SWI osExit + +#Data + + #Base 0x2000 + +Saying: +"starting again +" diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index 8aeb5de..71b32c0 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -361,6 +361,7 @@ from every assembly file in it. Several are old programs written for the bare ma | Snake | A game. Draws a whole screen with cursor addressing and steers with single keys, asking the console once a frame and never waiting. | | Keys | The console interrupting rather than being asked. The only one that brings a vector of its own, which is what the version two format exists for. | | Say | Prints whatever it was told, which is the shortest thing that shows osArgument working. | +| Reboot | Starts the machine again, in 45 bytes. Writes a port rather than asking the system, because a reset has to work when the system does not. | | Once | Asks the loader to start something else on the next start, and only that one, in 569 bytes. | | Status | Says what the last program made of what it was asked to do, in 222 bytes. The shell keeps the number and does not print it; this is how a person looks. | | Settle | Says how the last start went and tells the machine to stop falling back, in 353 bytes. A program rather than a shell word, because the shell is for what cannot be done without it. | diff --git a/README.md b/README.md index 700b1c1..122142f 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,28 @@ so a machine interrupted while updating its only boot slot would not boot at all the one failure on this disk with no way back. Writing the slot that is *not* live and then moving one byte in the superblock turns that into a machine that boots what it had before. +### Starting Again: + +Writing `1` to port `0x13` asks the machine to start over, and `Reboot` is the program that +does it - forty five bytes, most of them the word it prints. + +**A port rather than a service**, because a reset has to work when the system does not. +Something that could only be asked for through `SWI` would be unavailable in exactly the +case that wants it most, and a program that owns the whole machine has no system to ask. + +**What a reset repeats is how the machine started.** Named an image, the emulator places it +again; named none, the ROM is shadowed again and reads the disk for the rest. Anything else +would mean a reset changed what the machine *is*, which is the one thing a reset must not +do. It is taken between instructions, because a device cannot restart the machine from +inside the instruction that asked. + +The disk is not unplugged and keeps everything written to it. That is what warm means: the +machine starts again, the world it starts into does not. **The vector table is cleared**, +which is the one deliberate departure from leaving memory alone - a vector points into +whatever installed it, and after a reset that program is not running, so a handler left +behind would aim an interrupt at an address belonging to something gone. It is the argument +CosmOS already makes when it takes a program's vectors back at exit. + ### Starting Something Else Just This Once: A program that owns the whole machine has nowhere to run. It cannot be started from the diff --git a/Source/Emulator/emulator.c b/Source/Emulator/emulator.c index 45f224f..5c148cb 100644 --- a/Source/Emulator/emulator.c +++ b/Source/Emulator/emulator.c @@ -7,6 +7,7 @@ #include "rom.h" #include "bootstrap.h" +#include "../Assembler/assembly.h" #include #include #include @@ -177,6 +178,38 @@ int main (int argc, char *argv[]) { cycleCount += took; // Time has passed, so anything waiting on it may be finished. deviceTick(cycleCount); + + // ---- Starting over ---- + // + // Between instructions, which is the only place it can happen: a device cannot + // restart the machine from inside the instruction that asked for it. + // + // WHAT A RESET REPEATS IS HOW THIS MACHINE STARTED. Named an image, it is + // placed again; named none, the ROM is shadowed again and reads the disk for + // the rest. Anything else would mean a reset changed what the machine is, + // which is the one thing a reset must not do. + // + // The disk is not unplugged and its image keeps everything written to it. That + // is what warm means: the machine starts again, the world it starts into does + // not. + if (takeResetRequest()) { + // The vector table goes, and that is a deliberate departure from leaving + // memory alone. A vector points into whatever installed it, and after this + // that program is not running - so a handler left behind would aim an + // interrupt at an address belonging to something gone. It is the argument + // CosmOS already makes when it takes a program's vectors back at exit. + memset(Program + SOFTWARE_VECTOR_BASE, 0, + (size_t)(0x10000 - SOFTWARE_VECTOR_BASE)); + uint8_t failed = (programFile != NULL) + ? loadFile(programFile, Program, Data) + : loadROM(bootROM, bootROMBytes, Program, Data); + if (failed) { + fprintf(stderr, "Error: The machine could not be started again.\n"); + return 1; + } + initializeCPU(&cpu, Program, Data); + break; // Out of this batch; the loop above carries on with a new CPU. + } if (cpu.Status & STATUS_HALT) { // We've halted. break; diff --git a/Source/Emulator/io.c b/Source/Emulator/io.c index ec98561..d3c2d41 100644 --- a/Source/Emulator/io.c +++ b/Source/Emulator/io.c @@ -324,6 +324,15 @@ static uint8_t consoleStatus(void) { static uint8_t pendingInterrupts[INTERRUPT_LINE_BYTES]; +// Whether somebody has asked the machine to start over, and taking that request away. +static int resetWanted = 0; + +int takeResetRequest(void) { + int wanted = resetWanted; + resetWanted = 0; + return wanted; +} + void raiseInterrupt(uint8_t port) { pendingInterrupts[port >> 3] |= (uint8_t)(1u << (port & 7)); } @@ -570,6 +579,7 @@ static const DeviceRecord deviceTable[] = { { PORT_CONSOLE, DEVICE_CONSOLE, 0 }, { PORT_TEST, DEVICE_TEST, 0 }, { PORT_REFUSE, DEVICE_REFUSE, 0 }, + { PORT_MACHINE, DEVICE_MACHINE, 0 }, { PORT_MEMORY, DEVICE_MEMORY, DEVICE_FLAG_HAS_MEMORY }, { PORT_DISK, DEVICE_DISK, DEVICE_FLAG_HAS_MEMORY }, { PORT_REGISTRY, DEVICE_REGISTRY, 0 }, @@ -645,6 +655,15 @@ 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 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 + // way through a step and its state is not yet anything a reset could leave + // consistently behind. + if (DataByte == MACHINE_RESET) { + resetWanted = 1; + } + break; case PORT_MEMORY: // Fills the memory this device owns with the byte written. Nothing is // reachable from here: to get at it, register it as a bank and go through diff --git a/Source/Emulator/io.h b/Source/Emulator/io.h index bb69696..c491256 100644 --- a/Source/Emulator/io.h +++ b/Source/Emulator/io.h @@ -27,6 +27,21 @@ #define PORT_REFUSE 0x11 #define PORT_MEMORY 0x12 +// ---- Starting again ---- +// +// Writing 1 here asks the machine 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. +// Something that could only be asked for through SWI would be unavailable in exactly the +// case that most wants it, and a program that owns the whole machine has no system to ask. +// +// What it does NOT do is unplug anything. The disk stays attached and its image keeps +// whatever was written to it, which is what a warm restart means: the machine starts +// again, the world it starts into does not. +#define PORT_MACHINE 0x13 +#define MACHINE_RESET 0x01 + // The disk answers on a block of four ports and interrupts on the first of them. A device // 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. @@ -121,6 +136,10 @@ uint8_t consoleReadByte(void); #define DEVICE_REGISTRY 0x01 #define DEVICE_CONSOLE 0x02 #define DEVICE_CONTROLLER 0x03 +// The machine itself, which is what a reset is asking. In the range kept for the machine +// rather than among the peripherals, because it is not one: it is not attached to +// anything and cannot be unplugged. +#define DEVICE_MACHINE 0x04 #define DEVICE_TEST 0x10 #define DEVICE_REFUSE 0x11 #define DEVICE_MEMORY 0x12 @@ -213,6 +232,11 @@ uint8_t InputHandler(uint8_t Address); // must cost nothing when it does not. void serviceDevices(void); +// Set when something has written MACHINE_RESET, and taken by the loop that acts on it. A +// request rather than an action, because a device cannot restart the machine from inside +// the instruction that asked - the CPU is mid-step and its state is not yet consistent. +int takeResetRequest(void); + void raiseInterrupt(uint8_t port); void clearInterrupt(uint8_t port); diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index 6a1a395..7048a83 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -507,6 +507,7 @@ If nothing is installed for the vector a device refused with, the machine stops | 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 | +| 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 | | 0xE0 - 0xEF | The memory controller. See The Memory Controller. | 0x03 | | 0xFF | The bus registry. See Asking What Is There. | 0x01 | @@ -546,7 +547,8 @@ One thing to be careful of: the registry remembers which port it was asked about | 0x01 | Bus registry. | | 0x02 | Console. | | 0x03 | Memory controller. | -| 0x04 - 0x0F | Reserved for the machine itself. | +| 0x04 | The machine itself. Writing 1 to its port asks it to start over. | +| 0x05 - 0x0F | Reserved for the machine itself. | | 0x10 | Test device, which raises its own line. | | 0x11 | Test device, which refuses everything. | | 0x12 | Test device, which owns memory. | diff --git a/Tests/expected/rebootDirect.out b/Tests/expected/rebootDirect.out new file mode 100644 index 0000000..f5b9084 --- /dev/null +++ b/Tests/expected/rebootDirect.out @@ -0,0 +1,6 @@ +CosmOS +> starting again +CosmOS +> halted +Execution halted. +[exit 0] diff --git a/Tests/expected/rebootOnce.out b/Tests/expected/rebootOnce.out new file mode 100644 index 0000000..cbf6447 --- /dev/null +++ b/Tests/expected/rebootOnce.out @@ -0,0 +1,10 @@ +stage two +CosmOS +> next start: /System/Boot/bare.bin, once +finished +> starting again +stage two +just this once: /System/Boot/bare.bin +bare metal: no system, just this +Execution halted. +[exit 0] diff --git a/Tests/input/reboot.in b/Tests/input/reboot.in new file mode 100644 index 0000000..55c1b3f --- /dev/null +++ b/Tests/input/reboot.in @@ -0,0 +1,2 @@ +Once /System/Boot/bare.bin +Reboot diff --git a/Tests/input/rebootDirect.in b/Tests/input/rebootDirect.in new file mode 100644 index 0000000..95ba186 --- /dev/null +++ b/Tests/input/rebootDirect.in @@ -0,0 +1,2 @@ +Reboot +exit diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index bf47c11..957e516 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -550,3 +550,13 @@ cp "$DISKS/onceasked.img" "$DISKS/oncedue.img" "$TOOL" put "$DISKS/oncedue.img" "$WORK/once.cfg" /System/Boot/once.cfg >/dev/null # And after it has been taken: the request is gone, and nothing else changed. cp "$DISKS/onceasked.img" "$DISKS/onceafter.img" + +# ---- Starting again without leaving the machine ---- +# +# The whole loop in one session: ask for a one shot, restart, watch it own the machine. +# Before Reboot the only way to restart was to stop the emulator and run it again, which +# meant the one thing the machine could not do was the thing Once was written for. +"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ + "$ROOT/Programs/CosmOS/Apps/Reboot.asm" -o "$WORK/Reboot.sbx" >/dev/null +cp "$DISKS/onceasked.img" "$DISKS/reboot.img" +"$TOOL" put "$DISKS/reboot.img" "$WORK/Reboot.sbx" /Apps/Reboot.sbx >/dev/null diff --git a/Tests/manifest b/Tests/manifest index 9a3677a..3325838 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -483,6 +483,21 @@ onceDue | Boot/stage1.asm | rom | - onceAfter | Boot/stage1.asm | rom | - | 200000000 | disks/onceafter.img onceApp | CosmOS/Apps/Once.asm | assemble | - | - +# ---- Starting again ---- +# +# The loop end to end and inside one session: ask for a one shot, restart, and watch an +# image with no operating system in it take the machine. Before this the only way to +# restart was to stop the emulator, which meant the one thing the machine could not do was +# the thing Once exists for. +# +# A reset repeats HOW THIS MACHINE STARTED. Here no image is named, so the ROM is shadowed +# again and reads the disk. +rebootOnce | Boot/stage1.asm | rom | reboot.in | 300000000 | disks/reboot.img +# And here one IS named, so the reset places it again rather than reading the disk - a +# reset that changed what the machine is would be the one thing a reset must not do. +rebootDirect | CosmOS/Source/cosmos.asm | run | rebootDirect.in | 200000000 | disks/reboot.img +rebootApp | CosmOS/Apps/Reboot.asm | assemble | - | - + # A failed start with NOTHING to fall back to. The mark must not become a reason to refuse # to start at all - a failure that was passing recovers here, and one that is not leaves # the machine exactly where it would have been without any of this.