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