Files
SplitBit-Emulator/Source/Emulator/bootstrap.c
T
Anachronaut 54ff7196c9 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.
2026-08-27 14:41:33 -04:00

191 lines
7.3 KiB
C

// boostrap.c
// Boostrapping Functions for the SplitBit CPU Emulator
// Written by Anachronaut
// 10/16/2024
#include "bootstrap.h"
#include "../Assembler/assembly.h" // For the boot image format, which both tools share.
#include <stdio.h>
#include <stdint.h>
#include <string.h>
// Reads a number of the given width, most significant byte first.
// Returns 1 if the file runs out before the number does.
static uint8_t readNumber(FILE *file, int width, const char *what, uint32_t *result) {
uint32_t value = 0;
for (int i = 0; i < width; i++) {
int byte = fgetc(file);
if (byte == EOF) {
fprintf(stderr, "Error: Unexpected end of file while reading %s.\n", what);
return 1;
}
value = (value << 8) | (uint8_t)byte;
}
*result = value;
return 0;
}
// Reads a fixed length marker and checks it against the one expected. The caller's
// buffer must have room for length + 1 characters; it always comes back with a null
// on the end, so it is safe to print in an error message either way.
static uint8_t readMarker(FILE *file, const char *expected, int length, char *found) {
memset(found, 0, length + 1);
for (int i = 0; i < length; i++) {
int byte = fgetc(file);
if (byte == EOF) {
return 1;
}
found[i] = (char)byte;
}
return strncmp(found, expected, length) != 0;
}
// Reads the file header: the magic, the format version, and the features the boot image
// says it needs from the machine.
static uint8_t readFileHeader(FILE *file) {
char magic[SPLITBIT_MAGIC_LENGTH + 1];
if (readMarker(file, SPLITBIT_MAGIC, SPLITBIT_MAGIC_LENGTH, magic)) {
fprintf(stderr, "Error: This is not a SplitBit boot image.\n");
if (strncmp(magic, "PRG", 3) == 0) {
fprintf(stderr, " It looks like a boot image from before the format carried a version.\n Reassemble it and try again.\n");
} else {
fprintf(stderr, " Expected the file to begin with \"%s\", found \"%s\".\n", SPLITBIT_MAGIC, magic);
}
return 1;
}
uint32_t version;
if (readNumber(file, 1, "the format version", &version)) {
return 1;
}
if (version != SPLITBIT_FORMAT_VERSION) {
fprintf(stderr, "Error: This boot image is in format version %u, and this emulator reads version %u.\n", version, SPLITBIT_FORMAT_VERSION);
return 1;
}
uint32_t required;
if (readNumber(file, SPLITBIT_FLAGS_LENGTH, "the feature flags", &required)) {
return 1;
}
uint32_t missing = required & ~(uint32_t)SPLITBIT_FEATURES_SUPPORTED;
if (missing) {
fprintf(stderr, "Error: This boot image was built for a machine this emulator cannot provide.\n");
fprintf(stderr, " It asks for feature bits 0x%08X, which are not implemented here.\n", missing);
return 1;
}
return 0;
}
static uint8_t loadSegment(FILE *file, uint8_t *Memory, uint32_t length) {
for (uint32_t i = 0; i < length; i++) {
int byte = fgetc(file);
if (byte == EOF) {
fprintf(stderr, "Error: Unexpected end of file while reading a segment.\n");
return 1;
}
Memory[i] = (uint8_t)byte;
}
return 0;
}
// Reads one segment: its marker, its length, and then its contents.
static uint8_t readSegment(FILE *file, const char *marker, uint8_t *Memory) {
char found[SEGMENT_MARKER_LENGTH + 1];
if (readMarker(file, marker, SEGMENT_MARKER_LENGTH, found)) {
fprintf(stderr, "Error: Expected a \"%s\" segment here, found \"%s\".\n", marker, found);
return 1;
}
uint32_t length;
if (readNumber(file, SEGMENT_LENGTH_BYTES, "a segment length", &length)) {
return 1;
}
return loadSegment(file, Memory, length);
}
// Reads the Vector Segment, which is optional and last. A file that simply ends here
// was written before vectors existed, and an empty table is exactly right for it: every
// entry reads as zero, which means no handler, and the boot vector reading zero means
// the program starts at 0x0000 the way it always did.
//
// Each entry says where in Program Memory the vector sits and where its handler is, so
// installing one is a write straight into the vector table.
static uint8_t readVectorSegment(FILE *file, uint8_t *Program) {
int first = fgetc(file);
if (first == EOF) {
return 0;
}
ungetc(first, file);
char found[SEGMENT_MARKER_LENGTH + 1];
if (readMarker(file, "VEC", SEGMENT_MARKER_LENGTH, found)) {
fprintf(stderr, "Error: Expected a \"VEC\" segment here, found \"%s\".\n", found);
return 1;
}
uint32_t length;
if (readNumber(file, SEGMENT_LENGTH_BYTES, "the vector segment length", &length)) {
return 1;
}
if (length % VECTOR_ENTRY_FILE_BYTES != 0) {
fprintf(stderr, "Error: The vector segment is %u bytes, which is not a whole number of vectors.\n", length);
return 1;
}
for (uint32_t i = 0; i < length / VECTOR_ENTRY_FILE_BYTES; i++) {
uint32_t slot, handler;
if (readNumber(file, 2, "a vector address", &slot)
|| readNumber(file, 2, "a handler address", &handler)) {
return 1;
}
if (slot < SOFTWARE_VECTOR_BASE) {
fprintf(stderr, "Error: This boot image puts a vector at 0x%04X, which is below the vector table.\n", slot);
return 1;
}
Program[slot] = (handler >> 8) & 0xFF;
Program[(uint16_t)(slot + 1)] = handler & 0xFF;
}
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;
}
uint8_t failed = readImage(file, Program, Data);
fclose(file);
return failed;
}