From 54ff7196c96b78944b771c60938f05e369c680ad Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Thu, 27 Aug 2026 14:41:33 -0400 Subject: [PATCH] Stage 0: the emulator carries the ROM, so a disk is enough ./SplitBit --disk system.img stage two CosmOS > No boot image named. The emulator shadows its built in stage one into Program Memory - boot vector included - and the CPU then does exactly what it has always done: reads the boot vector and starts where it points. NOTHING ABOUT THE CPU CHANGED to make a machine that starts itself, which is what picking shadowing over a mapped ROM bought. The ROM is generated from Programs/Boot/stage1.asm by the makefile rather than committed beside it, because a copy of a program kept next to the program is a copy that goes stale. That makes the assembler a real dependency of the emulator, which it always sort of was and now says so. od and awk rather than xxd, which is not everywhere, or python, which the README does not ask anybody to install in order to build this. loadROM is loadFile given bytes instead of a path: both go through one reader over an fmemopen stream, because a ROM is a boot image and there is no reason for the machine to have two ways of understanding one. Naming an image still works and is what every other test here does. That path is not a shortcut to apologise for - placing memory from outside is a real thing real machines allow, and it is a debugger. The help says so now. No image and no disk is the one case with nothing to run, and it says that rather than printing a usage message about a missing file. run.sh gained a "rom" mode which hands the emulator a disk and nothing else. The source column still names stage1.asm, because that is what is IN the ROM: assembling it there says the thing the emulator carries is a thing that still assembles. --- .gitignore | 1 + README.md | 23 +++++++++++++++++++++ Source/Emulator/bootstrap.c | 41 +++++++++++++++++++++++++++++++------ Source/Emulator/bootstrap.h | 4 ++++ Source/Emulator/emulator.c | 29 ++++++++++++++++++++------ Source/Emulator/rom.h | 18 ++++++++++++++++ Source/Emulator/utility.c | 7 ++++++- Tests/expected/romBoot.out | 14 +++++++++++++ Tests/manifest | 12 +++++++++++ Tests/run.sh | 11 ++++++++-- makefile | 33 ++++++++++++++++++++++++++++- 11 files changed, 177 insertions(+), 16 deletions(-) create mode 100644 Source/Emulator/rom.h create mode 100644 Tests/expected/romBoot.out diff --git a/.gitignore b/.gitignore index a886581..a9d940e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ __pycache__/ # Kate leaves these beside a file it has open. .*.kate-swp +Source/Emulator/rom.c diff --git a/README.md b/README.md index 913ed33..d47fef4 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,29 @@ Then `dir` to see what is there, `load Snake.sbx` and `run` to play something, o | `-W`, `--write-protect` | Attach the disk read only. A disk whose image the host will not let you write is read only whether you ask for this or not. | | `-h`, `--help` | Show help and usage information. | +**The boot image is optional now.** Named one, the emulator places it into memory and +starts it, which is what a debugger does and how every test here runs - a real thing real +machines allow, not a shortcut to apologise for. Given only a disk, the machine starts the +way hardware would: + +``` +./SplitBit --disk system.img +``` + +The emulator carries `Programs/Boot/stage1.asm` as a **shadowed ROM**: at reset its bytes +are copied into Program Memory, boot vector included, and the CPU then does exactly what it +has always done - reads the boot vector and starts where it points. Nothing about the CPU +changed to make a machine that starts itself. + +Because it is a copy rather than a mapping, those bytes are ordinary Program Memory once +stage one has jumped away. The system may write over them, and a reset puts them back, +which is why rebooting has to be a reset rather than a jump - and `SWI SoftReset` is the +instruction for it. + +The ROM is generated from the assembly by the makefile rather than kept beside it, because +a copy of a program stored next to the program is a copy that goes stale. + + **A cycle is one access to memory**, not one instruction. Fetching an opcode is a cycle, fetching each byte after it is another, reading or writing Data Memory is one, every byte a CALL pushes or a RET pops is one, and reaching a device port is one. Nothing overlaps - diff --git a/Source/Emulator/bootstrap.c b/Source/Emulator/bootstrap.c index 5f7305d..7633a42 100644 --- a/Source/Emulator/bootstrap.c +++ b/Source/Emulator/bootstrap.c @@ -6,6 +6,7 @@ #include "bootstrap.h" #include "../Assembler/assembly.h" // For the boot image format, which both tools share. #include +#include #include // Reads a number of the given width, most significant byte first. @@ -144,18 +145,46 @@ static uint8_t readVectorSegment(FILE *file, uint8_t *Program) { return 0; } +// The Program Segment must come first, then the Data Segment. Short circuiting here means +// there is one exit, and so only one place that has to close whatever it was reading. +static uint8_t readImage(FILE *file, uint8_t *Program, uint8_t *Data) { + return readFileHeader(file) + || readSegment(file, "PRG", Program) + || readSegment(file, "DAT", Data) + || readVectorSegment(file, Program); +} + +// ---- Waking up in ROM ---- +// +// The same reader as a named file, given the bytes instead of a path, because a ROM is a +// boot image and there is no reason for the machine to have two ways of understanding one. +// +// THIS IS SHADOWING, which is a real technique rather than a convenience: reset copies the +// ROM into Program Memory, including its boot vector, and the CPU then does exactly what it +// has always done - reads the boot vector and starts where it points. Nothing about the CPU +// changes to make a machine that starts itself. +// +// And because it is a copy rather than a mapping, the bytes are ordinary Program Memory +// once stage one has jumped away. The system may write over them; a reset puts them back. +uint8_t loadROM(const unsigned char *bytes, unsigned long length, + uint8_t *Program, uint8_t *Data) { + FILE *file = fmemopen((void *)(uintptr_t)bytes, (size_t)length, "rb"); + if (file == NULL) { + fprintf(stderr, "Error: Couldn't open the boot ROM.\n"); + return 1; + } + uint8_t failed = readImage(file, Program, Data); + fclose(file); + return failed; +} + uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data) { FILE *file = fopen(path, "rb"); if (file == NULL) { fprintf(stderr, "Error: Couldn't open file: %s\n", path); return 1; } - // The Program Segment must come first, then the Data Segment. Short circuiting - // here means there is one exit, and so only one place that has to close the file. - uint8_t failed = readFileHeader(file) - || readSegment(file, "PRG", Program) - || readSegment(file, "DAT", Data) - || readVectorSegment(file, Program); + uint8_t failed = readImage(file, Program, Data); fclose(file); return failed; } diff --git a/Source/Emulator/bootstrap.h b/Source/Emulator/bootstrap.h index 107ce89..75b2e45 100644 --- a/Source/Emulator/bootstrap.h +++ b/Source/Emulator/bootstrap.h @@ -10,4 +10,8 @@ uint8_t loadFile(char *path, uint8_t *Program, uint8_t *Data); +// The same, from bytes the emulator carries rather than a file it opens. See loadROM. +uint8_t loadROM(const unsigned char *bytes, unsigned long length, + uint8_t *Program, uint8_t *Data); + #endif // BOOTSTRAP_H diff --git a/Source/Emulator/emulator.c b/Source/Emulator/emulator.c index 2313120..45f224f 100644 --- a/Source/Emulator/emulator.c +++ b/Source/Emulator/emulator.c @@ -5,6 +5,8 @@ // Written by Anachronaut // 10/15/2024 +#include "rom.h" +#include "bootstrap.h" #include #include #include @@ -94,17 +96,32 @@ int main (int argc, char *argv[]) { if (optind < argc) { programFile = argv[optind]; optind++; - } else { - fprintf(stderr, "Error: No boot image specified.\n"); - printHelp(argv[0]); - return 1; } if (optind < argc) { fprintf(stderr, "Error: Unexpected argument: %s\n", argv[optind]); return 1; } - if (loadFile(programFile, Program, Data)) { - fprintf(stderr, "Error: Couldn't read file: %s\n", programFile); + // ---- Where the machine's first instruction comes from ---- + // + // Named an image, it is placed into memory and started - which is what a debugger + // does, and is how every test here runs. That path is not a shortcut to apologise + // for: placing memory from outside is a real thing real machines allow. + // + // Named none, the machine starts the way hardware would: the ROM is shadowed into + // Program Memory and it reads the disk for the rest. There has to be a disk for that + // to mean anything, and no image and no disk is a machine with nothing to run. + if (programFile == NULL && options.disk == NULL) { + fprintf(stderr, "Error: No boot image and no disk, so there is nothing to run.\n"); + printHelp(argv[0]); + return 1; + } + if (programFile != NULL) { + if (loadFile(programFile, Program, Data)) { + fprintf(stderr, "Error: Couldn't read file: %s\n", programFile); + return 1; + } + } else if (loadROM(bootROM, bootROMBytes, Program, Data)) { + fprintf(stderr, "Error: The boot ROM is not a boot image.\n"); return 1; } if (options.disk != NULL && attachDisk(options.disk, options.writeProtect)) { diff --git a/Source/Emulator/rom.h b/Source/Emulator/rom.h new file mode 100644 index 0000000..399ad48 --- /dev/null +++ b/Source/Emulator/rom.h @@ -0,0 +1,18 @@ +// rom.h +// The bytes the machine wakes up in. +// +// Stage one, built from Programs/Boot/stage1.asm by the makefile rather than kept here as +// a copy, because a copy of a program stored beside the program is a copy that goes stale. +// +// It is an ordinary boot image, and that is the point: nothing about stage one changes +// when it moves from a file into a ROM except who puts it in memory. +// +// Written by Anachronaut + +#ifndef ROM_H +#define ROM_H + +extern const unsigned char bootROM[]; +extern const unsigned long bootROMBytes; + +#endif // ROM_H diff --git a/Source/Emulator/utility.c b/Source/Emulator/utility.c index e9face9..562da31 100644 --- a/Source/Emulator/utility.c +++ b/Source/Emulator/utility.c @@ -11,7 +11,12 @@ #include "../Assembler/assembly.h" void printHelp(const char *programName) { - printf("Usage: %s [OPTIONS] \n", programName); + printf("Usage: %s [OPTIONS] [boot image]\n", programName); + printf("\n"); + printf("Named an image, it is placed into memory and started, which is what a\n"); + printf("debugger does and how the test suite runs. Given only a disk, the machine\n"); + printf("starts the way hardware would: the built in ROM is shadowed into Program\n"); + printf("Memory, and it reads the disk for everything else.\n"); printf("\n"); printf("Options:\n"); printf(" -d, --debug Enable debug mode.\n"); diff --git a/Tests/expected/romBoot.out b/Tests/expected/romBoot.out new file mode 100644 index 0000000..402617e --- /dev/null +++ b/Tests/expected/romBoot.out @@ -0,0 +1,14 @@ +stage two +CosmOS +> saved it +read it back, 22 bytes: +a file kept by asking +renamed it +deleted it +and it is gone +finished +> cannot make that: check the path, the name, and whether it is taken +> /Notes> 0 files +/Notes> halted +Execution halted. +[exit 0] diff --git a/Tests/manifest b/Tests/manifest index 350fede..2d1caca 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -389,6 +389,18 @@ bootDataRuns | Boot/stage1.asm | run | - selfBoot | Boot/stage1.asm | run | selfBoot.in | 90000000 | disks/selfboot.img # And a disk with a boot slot but nothing to start, which says so rather than jumping. selfBootNoSystem | Boot/stage1.asm | run | - | 90000000 | disks/nosystem.img + +# ---- And with no image named at all ---- +# +# The mode is "rom": the emulator is handed a disk and nothing else, so it shadows its own +# built-in stage one into Program Memory and starts there, which is what a machine with no +# debugger attached does. Naming an image is the debugger, and every other test here is +# using it. +# +# The source column still names stage1.asm even though the binary is thrown away, because +# that is what is IN the ROM: the makefile builds rom.c from that file, so assembling it +# here says the thing the emulator carries is a thing that still assembles. +romBoot | Boot/stage1.asm | rom | selfBoot.in | 90000000 | disks/selfboot.img # Reading a disk that has directories on it. The machine can walk a path at this point but # cannot make a directory, so the disk is built by the host tool and read here - which is # the two implementations checking each other rather than either checking itself. diff --git a/Tests/run.sh b/Tests/run.sh index 188261b..a89dcfc 100755 --- a/Tests/run.sh +++ b/Tests/run.sh @@ -193,7 +193,7 @@ while IFS='|' read -r name src mode stdin limit disk; do OUT="$BUILD/.out" case "$mode" in - run) + run|rom) # --fast because there is nothing to learn from waiting out the emulated # clock, and --cycles for programs that never halt on their own, which # bounds them by cycle count rather than by wall clock. @@ -226,7 +226,14 @@ while IFS='|' read -r name src mode stdin limit disk; do [ -n "$DISKWAIT" ] && EMUARGS+=(--disk-cycles "$DISKWAIT") case "$disk" in *:ro) EMUARGS+=(--write-protect) ;; esac fi - timeout "$RUN_TIMEOUT" "$EMULATOR" "${EMUARGS[@]}" "$BIN" <"$IN" >"$OUT" 2>&1 + # A rom test names no image. The emulator then shadows its built in stage + # one into Program Memory and reads the disk for everything else, which is + # what a machine with no debugger attached does. + if [ "$mode" = "rom" ]; then + timeout "$RUN_TIMEOUT" "$EMULATOR" "${EMUARGS[@]}" <"$IN" >"$OUT" 2>&1 + else + timeout "$RUN_TIMEOUT" "$EMULATOR" "${EMUARGS[@]}" "$BIN" <"$IN" >"$OUT" 2>&1 + fi STATUS=$? if [ "$STATUS" -eq 124 ]; then FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name") diff --git a/makefile b/makefile index 99584f0..29ca3e7 100644 --- a/makefile +++ b/makefile @@ -33,7 +33,7 @@ SRC_DIR_LINT = Source/Linter OBJ_DIR = Object # Source files -EMU_SRCS = emulator.c io.c controller.c utility.c cpu.c bootstrap.c assembly.c +EMU_SRCS = emulator.c io.c controller.c utility.c cpu.c bootstrap.c assembly.c rom.c ASM_SRCS = Assembler.c assembly.c firstPass.c Assm-util.c secondPass.c DSK_SRCS = SplitDisk.c LINT_SRCS = Linter.c @@ -52,6 +52,36 @@ LINT_TARGET = SplitLint # Default target: build the emulator and its three host-side tools. all: $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET) +# ---- The ROM the machine wakes up in ---- +# +# Generated from the assembly rather than committed, because a copy of a program kept +# beside the program is a copy that goes stale. It needs the assembler, which is built +# first; that is a real dependency and saying so is better than hiding it. +# +# od and awk rather than xxd, which is not everywhere, and rather than python, which the +# README does not ask anybody to install to build this. +$(SRC_DIR_EMU)/rom.c: Programs/Boot/stage1.asm $(ASM_TARGET) + @mkdir -p $(OBJ_DIR) + @./$(ASM_TARGET) Programs/Boot/stage1.asm -o $(OBJ_DIR)/stage1.bin > /dev/null + @{ \ + echo '// rom.c'; \ + echo '// GENERATED from Programs/Boot/stage1.asm by the makefile. Do not edit.'; \ + echo '//'; \ + echo '// The first thing the machine runs, and the only part of it that is not on'; \ + echo '// the disk. On hardware this is a chip; here it is an array, placed into'; \ + echo '// Program Memory at reset the way a shadowed ROM is.'; \ + echo ''; \ + echo '#include "rom.h"'; \ + echo ''; \ + echo 'const unsigned char bootROM[] = {'; \ + od -v -An -tu1 $(OBJ_DIR)/stage1.bin | awk '{ printf " "; for (i = 1; i <= NF; i++) printf " %s,", $$i; print "" }'; \ + echo '};'; \ + echo ''; \ + echo 'const unsigned long bootROMBytes = sizeof(bootROM);'; \ + } > $@ + +$(OBJ_DIR)/rom.o: $(SRC_DIR_EMU)/rom.c $(SRC_DIR_EMU)/rom.h + # Emulator binary $(EMU_TARGET): $(EMU_OBJS) $(CC) $(CFLAGS) -o $(EMU_TARGET) $(EMU_OBJS) @@ -172,6 +202,7 @@ bless: $(EMU_TARGET) $(ASM_TARGET) clean: rm -rf $(OBJ_DIR) rm -rf Tests/build + rm -f $(SRC_DIR_EMU)/rom.c rm -f $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET) # Install compiled binaries