Files
SplitBit-Emulator/README.md
T
AnachronautandClaude Opus 5 3da020898c Write down what the machine costs, before it has to be worked out again
Three measurements that would otherwise be re-derived from scratch in three months, put
where a reader will meet them rather than where somebody has to know to look.

The README's cost model section says what a nominal 1 MHz actually means: about 270,000
instructions a second, since the average instruction costs 3.72 cycles. The same program
therefore takes 3.72 times the wall clock it did when a cycle was an instruction - nothing
got slower, the number got honest, and the number it replaced described no machine anybody
could build. Beside it, the two figures that put that in proportion: the emulator runs 195
million cycles a second in --fast, and CosmOS assembling CosmOS is 654 million of them,
which is eleven minutes at a megahertz, 3.3 seconds at --fast, and six and a half seconds at
a hypothetical 100 MHz. That last is pessimistic rather than hopeful: it assumes hardware
overlaps nothing, which is the same conservative reading the controller's cost model takes.

The CosmOS README gets what that means for its assembler, which is superlinear in what it
reads: 1,383 cycles a byte for a four kilobyte source, 6,290 for a hundred kilobyte one.
IT IS NOT THE DISK, and the number that says so is worth keeping - 654 million on a disk
carrying the whole source tree against 653 million on a flat one with a sixth as many files.
The suspected cause is walking a table of some nine hundred labels once per reference,
suspected rather than measured.

Nothing is being done about it, and the reason is written down too: development happens with
the host assembler. But faster hardware buys a constant factor and does not change the shape
of the curve, so the program that forces this is not CosmOS - it is the first one twice its
size.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-29 10:10:12 -04:00

615 lines
36 KiB
Markdown

# SplitBit
SplitBit is an 8 bit computer that does not exist: a CPU with its own instruction set, split Program and Data memories, an interrupt and vector system, a bus that programs can enumerate, a memory controller that can write code into memory, and a disk. This repository is a C implementation of the machine, an assembler for it, a tool for its disks, and the software that runs on it, which now includes an operating system and an assembler written in SplitBit's own assembly language.
![CosmOS assembling a program, running it, and disassembling what it built](Media/CosmOS.png)
*The whole loop in one screen. `Asm.sbx` assembles `hello.asm` into `hello.sbx`; the shell loads that and runs it; then the monitor disassembles what is at 0x2000, which is the program the machine wrote for itself at the top of the screen. No host is involved at any point.*
**SplitBit assembles SplitBit.** [`Programs/CosmOS/Assembler/`](Programs/CosmOS/Assembler) runs on the machine, reads source off a SplitBit disk, and writes a boot image or a loadable program back to it with no host involved. It builds the operating system it is running under, and it builds itself, both byte for byte identical to what the C assembler produces from the same source. The test suite then boots the CosmOS that CosmOS built and has *that* one assemble CosmOS again, and the second generation is identical to the first, so the machinery has been through itself. After that the host is a convenience rather than a necessity.
```
> load Asm.sbx
> run cosmos.asm
wrote cosmos.bin: program 7036, data 2448, labels 475
> run Asm.asm
wrote Asm.sbx: program 7533, data 4099, labels 555
```
## What Is In Here:
| Directory | What it holds |
| --- | --- |
| [`Source/Emulator`](Source/Emulator) | The machine: CPU, memory controller, devices, console, disk - and the two front ends that present it |
| [`Source/Assembler`](Source/Assembler) | The assembler that runs on a host |
| [`Source/DiskTool`](Source/DiskTool) | SplitDisk, which reads and writes SplitBit's filesystem |
| [`Source/Linter`](Source/Linter) | SplitLint, which points out needlessly long assembly forms |
| [`Programs/Examples`](Programs/Examples) | Programs to read: hello, a calculator, Fibonacci, a prime sieve, Life, the colours |
| [`Programs/Libraries`](Programs/Libraries) | Code included by name rather than linked, since there is no linker |
| [`Programs/Loader`](Programs/Loader) | The standalone loader CosmOS grew out of |
| [`Programs/CosmOS`](Programs/CosmOS) | The operating system, its applications, and the native assembler |
| [`Programs/testPrograms`](Programs/testPrograms) | What the test suite drives |
| [`Tests`](Tests) | The suite: the manifest, the recorded output, and the scripts that check it |
## Two Front Ends:
`make` builds **SplitBit**, which is the machine with a terminal attached, and where Raylib
is installed it also builds **Voyager**, which is the same machine with a screen and a
speaker. Everything that is actually the machine - every instruction, every device, every
cycle - is shared between them, and each brings one file of its own: a terminal or a window.
Voyager is deliberately not required. The machine, the assembler, the disk tool, the linter
and the whole test suite build and run on a host with no graphics library at all, because a
project about a small understandable CPU should not need OpenGL to run its tests. Where
Raylib is missing, `make` says so once and builds everything else.
The suite holds the two to being the same machine rather than taking it on trust: it runs
the entire manifest through Voyager as well, with `--headless`, and requires it to satisfy
every recorded result byte for byte.
**The screen belongs to the machine, not to the window.** The video device is a tile engine
on ports 0x30 to 0x3F that brings its own bank of video memory, and it renders into a buffer
that is a pure function of that memory - so the same program draws the same picture whether
or not anybody is watching. The console draws on it: it is a display controller as well as a
port, with a font, a cursor and scrollback, which is why CosmOS runs in a window without a
line of it being changed. Voyager puts that buffer on the glass and decides nothing about
it. Either binary will save a picture of the screen with `--screen`, which is how a test
suite on a host with no display checks what was drawn. See **The Screen** in the Programming
Manual.
## The Machine:
- **Harvard architecture.** Two 64K memories, one for instructions and one for data. An instruction can only read the second, which is why strings live there and why the memory controller exists.
- **Its own instruction set**, 72 instructions, four Data Pointers, and a Q register that holds what the ALU last worked out. Small enough that the table describing it fits in the machine's own memory, which is what lets it disassemble and assemble for itself.
- **Interrupts.** Software traps, hardware lines from devices, and faults, all arriving through one vector table with a full context save.
- **A bus programs can enumerate**, so a program can ask what a machine is made of rather than being told.
- **A memory controller** that reads and writes Program Memory, moves blocks between banks, reaches memory that devices bring with them, and guards a range against being written by accident. It is how a SplitBit machine loads a program.
- **Storage**: a block device with 256 byte blocks and up to 16 megabytes of them, backed by an image file. It knows blocks and not files, because a filesystem is meant to be software SplitBit runs.
## The Software:
- **SBFS**, a filesystem read and written by SplitBit itself and by a host tool that speaks the same format, so an image can be moved either way and each implementation checks the other.
- **CosmOS**, an operating system: it boots the machine, mounts a disk, lists what is on it, loads a program and runs it, and takes the machine back when the program finishes. It comes with a library of programs including a game, a line editor, and a monitor that examines memory, disassembles, assembles a line at a time, and sets breakpoints.
- **Loadable programs.** A program that was not booted from carries a header saying where it belongs, and may bring interrupt handlers of its own for the loader to install and take back again.
- **System services.** A loaded program reaches the console and the disk through numbered software interrupts rather than carrying a copy of the code that drives them. The numbers are written down in one file both sides include, so neither ever types one. It took the editor from 4941 bytes to 1983 without changing a line of what it does.
- **Streaming reads.** A file bigger than the machine's memory is read a block at a time, through services that keep nothing open between calls. CosmOS's own source is far larger than its 64K of Data Memory, and this is what the native assembler stands on.
## Getting Started:
Clone it and build the four tools. You need gcc and make, or similar:
```
git clone https://github.com/RealBusinessAccount/SplitBit-Emulator.git
cd SplitBit-Emulator
make
```
The sources are ISO C and build clean under `-std=c11 -pedantic` with `-Wall -Wextra`. Beyond ISO C they need POSIX.1-2008, which the makefile asks for by name, and `getopt_long` for the long form of the command line options.
Building needs nothing else. **Running the tests needs `bash` and Python 3**, because two of the checking scripts are Python and one of them opens a pseudo-terminal to ask questions a recorded file cannot answer. They also use `stty`, `timeout`, and the usual text utilities: `sed`, `awk`, `grep`, `cmp`, `diff`, `tr`, `sort`, `wc`.
Assemble something and run it:
```
./Assembler Programs/Examples/hello.asm
./SplitBit hello.bin
```
Or boot the operating system, with a disk of programs and all of its own source on it:
```
cd Programs
make run-cosmos
```
Then `dir` to see what is there, `load Snake.sbx` and `run` to play something, or `load Asm.sbx` and `run cosmos.asm` to watch the machine build itself.
Every source in `Programs/` is on that disk, under `/Source`, so anything not shipped as a
binary can still be assembled on the machine: `cd /Source/Examples` and `Asm colours.asm`.
`make run-voyager` boots the same disk on the machine with a screen instead of a terminal.
Both targets depend on the disk, so a disk built before a change to the machine is rebuilt
rather than booted as it stands: **what is on a disk is whatever was built when the disk was
made**, and a system whose console has changed will happily start an image full of programs
written for the old one.
## Running Programs: SplitBit
```
./SplitBit [options] [boot image]
```
| Option | What it does |
| --- | --- |
| `-d`, `--debug` | Single step through cycles. Each key press advances one instruction. |
| `-c`, `--cycles N` | Stop after N cycles rather than running until the program halts. Useful for programs that never halt, and for getting the same output from a run every time. |
| `-f`, `--fast` | Run as fast as the host allows, ignoring the emulated cycle rate. |
| `-D`, `--disk <file>` | Attach a disk image, creating a 128K one if the file is not there. |
| `-L`, `--disk-cycles N` | How many cycles a block read or write takes. Zero, the default, finishes before the next instruction starts. |
| `-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 -
there is no fetching the next instruction while this one finishes - so the count is simply
how many times the machine used the bus.
That makes the numbers describe something buildable. `RSTA` costs 1 and `SETD` costs 4,
because one is a byte and the other is four. `CALL` and `RET` together cost 24 and `RCAL`
and `RRET` cost 8, because the first pair moves twenty bytes of Stack and the second moves
four. Counting instructions said those were the same, which is not true of any machine
anybody could build - and it is the emulator's job to be the thing the hardware is designed
against.
The average SplitBit instruction costs 3.72 cycles, measured over the native assembler
assembling a program. **Which is what a nominal 1 MHz means here**: about 270,000
instructions a second, not a million. The same program takes 3.72 times the wall clock it
did when a cycle was an instruction - nothing got slower, the number got honest, and the
number it replaced described no machine anybody could build.
Two measurements that put that in proportion, taken on the machine assembling its own
operating system:
| | |
| --- | --- |
| The emulator, in `--fast` | 195 million cycles a second |
| CosmOS assembling CosmOS | 654 million cycles: 11 minutes at 1 MHz, 3.3 seconds at `--fast` |
At a hypothetical 100 MHz that build is six and a half seconds, and that is a pessimistic
figure rather than a hopeful one: it assumes hardware overlaps nothing, which is the same
conservative reading the memory controller's cost model takes and the same one that wants
measuring before it is designed.
**The memory controller is charged for what it moves**, on the same terms. Banks are
separate memories, and that is what sets the rate: a move between two of them can overlap
its read and its write, while a move within one bank cannot and costs twice as much. A fill
has nothing to read and goes at the between-banks rate. Against the ten cycles a transfer
used to cost - the five port writes that set it up, and nothing at all for the quarter of a
kilobyte that moved.
**And the controller's path to memory is sixteen bits wide.** A transfer whose source,
destination and length are all even moves two bytes a cycle between banks and one within a
bank; anything odd falls back to a byte a cycle, because lining bytes up across word
boundaries is a second design and this is not it. So a 256 byte block is 129 cycles between
banks and 257 within one when it is aligned, and 257 and 513 when it is not. The CPU still
sees eight bits and no instruction means anything different: this is a peripheral being
faster, not a new machine.
The transfer stalls the program that asked for it. Whether hardware would let the two run at
once is left open, the same way pipelining is: the memories are separate, so it plausibly
could, and the measurements say it would buy less than it sounds like.
**The disk can be given a latency** with `--disk-cycles`, and then it really does take that
long: it says busy, finishes when the machine has run that far, and a program that does not
wait reads the block *before* the one it asked for. That is not an error anywhere - just
quietly the wrong bytes - which is why the filesystem now watches the busy bit rather than
trusting the answer to be there. Zero is the default and is how the machine has always run.
The waiting is one small routine, and it is reached with `RCAL` rather than `CALL` because
what it hands back is the settled status in A, and an ordinary call would put A back the way
it found it.
**And waiting is not the same kind of cycle as working.** A machine stopped in a `WAIT` is
clocked but is not using the bus, so those cycles are counted apart from the rest and the
halt line says so when there are any:
```
Execution halted after 1042474 cycles, 119772 of them waiting.
```
Added together they are elapsed time, which is what `--cycles` measures. Told apart they
say whether a program was working or waiting - and that distinction is the only thing that
separates a machine which slept through a slow disk from one which spun on it. The two take
the same wall clock time and print the same characters. When the filesystem's wait was
first written, taking the line-clearing out of `WAIT` moved the total by a single cycle,
20,100 against 20,099, while the idle half halved.
Whether real hardware would overlap a fetch with the end of the previous instruction is
left open, and deliberately: this is the conservative model, and pipelining is a decision
to make while drawing the hardware rather than one to inherit from an emulator.
If the CPU reads a byte that is not an instruction, it goes to the fault handler the program installed. If it installed none, it raises the Fault Flag and halts, and the emulator reports the byte and the address it was found at and exits with a non zero status. The same happens if a program or a device asks for a handler that was never installed.
## Assembling: Assembler
```
./Assembler [options] [assembly file]
```
| Option | What it does |
| --- | --- |
| `-o <file>` | Write the output to this path. |
| `-I <dir>` | Look in this directory for included files. May be given more than once. |
| `-M <file>` | Write out which source files the output depends on, as a make rule. |
| `-S <file>` | Write every label and the address it was given, in address order. |
| `-h`, `--help` | Show help and usage information. |
`-S` is the only thing that knows what a program's addresses are called. A program on the
disk is bytes; the machine's own monitor can disassemble it but has no idea what any of it
is named. So a count of which addresses get called says a great deal and names nothing, and
this is what turns such a count into a list of routine names.
Without `-o` the output takes the source file's name, in the directory you called the assembler from, with the extension the format asks for: `.bin` for a boot image and `.sbx` for a loadable program. Included files are looked for beside the file that includes them, and then along the directories given with `-I`.
## Checking Assembly: SplitLint
```
./SplitLint [--fatal-warnings] [--machine] <sourcefile> [sourcefile ...]
```
**Every warning names the rule that produced it**, in brackets the way a compiler names a
flag, and `--machine` prints one tab-separated line per warning and nothing else - file,
line, rule, message, help - so that nothing downstream has to read prose. A run that finds
nothing says so rather than exiting silently, because a tool that says nothing has not told
you it found nothing; it has told you nothing at all, and from outside those look the same:
```
No style warnings: 121 files checked against 12 rules.
```
SplitLint reports valid assembly that has a shorter direct expression, beginning with
zero loads that can use `RSTA` or `RSTB`, Q-to-register transfers that need not pass
through the stack, self-push/pop pairs, register assignments overwritten by the next
instruction, instructions with no ordinary fallthrough path, and one-byte Data Pointer
changes that can use `INCD` or `DECD`, including direct branches whose target is already
the next labeled address. It also tracks symbolic Data Pointer bases and known offsets to
find a `SETD` that reloads an address the pointer already holds. Device input and stack
pops are excluded from dead-assignment checks because consuming their input is itself an effect.
**Nothing it knows survives a call.** `CALL` really does restore A, B and Data Pointers 0
to 2, so a pointer set before one is genuinely still set after it - and saying so produced
advice that was correct today and unsafe to take. 122 of the 178 redundant `SETD`s it first
found were redundant *only* because of that restore, and removing them would have become a
wrong-pointer bug the moment the callee was reached with `RCAL` instead, which is what
`RCAL` was added to this machine for. Worse, the linter would have gone quiet rather than
complained, because it forgets everything across an `RCAL`. Fifty four recommendations that
stay true are worth more than a hundred and seventy eight that are conditional on a change
the project intends to make.
SplitLint's control-flow knowledge is otherwise deliberately local: labels begin reachable regions, while an unparsed
directive or data item ends the current claim. It does not yet expand includes or build a
complete control-flow graph. Warnings normally leave a successful exit status, while
`--fatal-warnings` makes any warning fail the command for use in automated checks.
**A line whose comment says `splitlint: <reason>` is not reported on, and the reason is
required.** A suppression with no explanation is a way to make a tool quiet rather than a
way to say something, and a bare marker is refused rather than honoured. The corpus has
three of them, all in test programs: `branchTest.asm` exists to check that a branch whose
carry is known behaves correctly, so a diagnostic saying the outcome is known is exactly
right and exactly unwanted. `splitlint[rule]: <reason>` silences one rule and leaves the
line honest about the others.
Suppressions are counted and reported at the end of a run, and **a marker that no longer
silences anything is itself reported** - an exception that outlived its reason is the thing
the required reason was meant to prevent.
**The corpus is held to a baseline.** Sixty one warnings are left in it on purpose, and
`Tests/lint-baseline.txt` records how many of each rule each file is expected to produce,
so a sixty second fails `make test` while the sixty one stay quiet. It counts rather than
recording line numbers, because recording lines would churn the whole file whenever
anything was inserted above a warning. `./Tests/lint.sh --bless` records it again once
warnings have been deliberately fixed or deliberately accepted.
The same local model tracks whether carry is known set or clear. It reports a redundant
`CCF`, a `BRC` or `BNC` whose outcome is already determined, and computes carry through
increment, decrement, addition, and subtraction when their inputs are known.
## Managing Disks: SplitDisk
```
./SplitDisk <command> <image> [arguments]
```
| Command | What it does |
| --- | --- |
| `boot <image> <file> [slot]` | Write a file into a boot slot, padding the rest of it with zeroes. Slot 0 unless told otherwise. |
| `bootstate <image> [0\|1\|2]` | Show how the last start went, or set it. Setting it to 0 is how a disk that fell back is told to try again. |
| `bootslot <image> <slot>` | Choose which slot the machine starts from. One byte, on its own, so writing a slot and committing to it stay separate decisions. |
| `format <image> [blocks] [dirblocks] [bootblocks]` | Lay down a fresh filesystem. 512 blocks and 8 of directory by default, which is 128K and room for 64 entries. A fourth number reserves a boot area of two slots that size. |
| `list <image> [path]` | Show the whole disk, or one directory of it. |
| `put <image> <file> [path]` | Put a host file onto it. Without a path it uses the file's own name, which is often longer than the 22 characters a name may be. |
| `get <image> <path> [file]` | Take one off it. |
| `delete <image> <path>` | Remove a file. |
| `mkdir <image> <path>` | Make a directory. |
| `rmdir <image> <path>` | Remove an empty one. |
### The Boot Area:
Blocks between the superblock and the directory, which the filesystem never allocates and
never sees. **Nothing was added to reserve them.** Both implementations work out the first
usable block as `directoryStart + directoryBlocks`, and `directoryStart` has always been a
field in the superblock rather than a constant, so moving the directory up reserves
everything below it by arithmetic that was already there. Neither allocator changed.
A disk made before any of this has `directoryStart` of 1 and a boot area of zero, which
reads as *not bootable* - true, and the same shape as the version two parent field, where
the value an older disk already held turned out to be the right answer.
**There are always two slots**, because a boot slot is raw blocks. A file being rewritten
is protected by writing a temporary and renaming it, and there is no name here to rename -
so a machine interrupted while updating its only boot slot would not boot at all, which is
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
shell, because starting it means there is no shell; and pointing `boot.cfg` at it means a
machine that keeps starting it, which is a poor place to find a mistake.
```
> Once /System/Boot/mine.bin
next start: /System/Boot/mine.bin, once
```
That writes `/System/Boot/once.cfg`, in the same format as `boot.cfg` and read with the same
routines, because a second format for one setting would be a second format. The loader reads
it before `boot.cfg` and **deletes it before it jumps** - the only moment there is, since
after the jump the loader does not exist.
**Consumed by being read, not by working.** A one shot that hangs cannot hang twice: the
request is gone before the image ran, so the next start reads `boot.cfg` like any other.
And **the boot state is not touched** by a one shot, which the first version got wrong. A
program with the whole machine has no filesystem to clear a mark with, and is doing nothing
wrong by not having one - so marking it reported every successful bare metal boot as a start
that never arrived.
Which closes the loop on the machine itself: write a bare metal program in `Edit`, assemble
it with `Asm`, ask for it with `Once`, restart, watch it run, and the system comes back
without being asked.
### Knowing Whether The Last Start Arrived:
The loader marks the disk before it hands over, and the system clears the mark when it
reaches its prompt. **A system that crashes on the way there leaves the mark**, and the
loader finding it still set next time is how a machine that will not start says so to the
only thing in a position to do anything about it.
| State | Means |
| -- | -- |
| 0 | Settled. The last start arrived, so start what the configuration says. |
| 1 | Trying. The loader handed over and nothing came back to say it got there. |
| 2 | Fell back. A try failed and the fallback was used, and will be until this is settled. |
Three starts of a machine whose configuration names something broken:
```
stage two
a system that never reaches a prompt <- marks the disk, dies
stage two
the last start did not arrive <- finds the mark, uses the fallback
CosmOS
this is the fallback: what boot.cfg asks for did not start
stage two
still on the fallback: settle it to try again <- does NOT retry
```
That third one is the part worth having. A system known not to start is not tried every
other boot for ever; it waits to be told the situation has changed.
**Reaching the prompt is a deliberate choice of threshold.** It is not a claim that the
system works - a shell can be reached by something broken in every other way. It is the
point where somebody can type, which is exactly what the fallback exists to give back:
anything wrong past there can be fixed from the prompt, and nothing wrong before it can be
fixed at all.
`bootBlocks` and `directoryStart` describe the same fact from two sides, so a disk where
they disagree is refused rather than guessed at.
`Programs/Boot/stage1.asm` is what starts a machine from one, and is written to end up in
ROM: **330 bytes**, and everything it knows is a thing that will be true forever. Which
port the disk is on, that a SplitBit disk begins with its own name, and where two numbers
sit in that first block. It does not know what a file is, what a directory is, or that SBFS
has versions - all of that lives in the boot area, on the disk, where it can be replaced.
It reads the live slot into Program Memory, jumps to the first byte, and prints one
character and stops if there is nothing to start.
`Programs/Boot/stage2.asm` is what sits in the slot. It mounts the filesystem, reads
`/System/Boot/boot.cfg` to find out what to start, and takes the image apart: code into Program Memory, data into Data
Memory, and each vector written through the controller, which is the only thing that can
write Program Memory at all. The entry point is noticed on the boot vector's way past,
because Program Memory cannot be read back to look it up afterwards.
**What it loads is an ordinary boot image** - the same SPBT file the emulator has always
been handed directly. That is the whole trick: a second stage that loads the machine's
normal image format is not a boot-specific mechanism, so **bare metal SplitBit stops being
a special case.** A program that wants no operating system under it is just an image,
written under CosmOS like any other, and startable because it is a file.
### Configuration:
One setting to a line: a key, a space, and the rest of the line is the value. A semicolon
starts a comment and a blank line is nothing.
```
; /System/Boot/boot.cfg
system /System/Boot/cosmos.bin
fallback /System/Boot/cosmos-previous.bin
```
That format was noticed rather than designed. `textSplit` already cuts the first word off a
line and leaves the rest; `textSame` already compares two strings and insists they end
together, so `system` and `systemd` are different words. Reading a setting is those two
routines and a loop - and it is the shape the shell already reads, so **a configuration
line is a command line the machine reads instead of a person typing one.**
**Configuration is advice.** A missing file, a missing key, a value that makes no sense, a
line too long to read: all of them mean *use the default*, and none of them is a failure. A
program that cannot run without its configuration has turned its configuration into a
single point of failure, and for the thing that starts the machine that would mean a
mistyped file is a machine that will not start.
**But quiet is not silent.** A setting somebody meant, which did not take effect, says so:
```
a setting nothing asked for: fallbcak
a setting with no value: system
a line too long to read: ...
```
So `config.asm` has two routines rather than one. `cfgGet` reads and never says anything -
reading three settings should not report the same bad line three times. `cfgCheck` reads
the file once and reports what it did not understand, and is given the caller's list of
keys, because whether a key means anything is the only part of this the reader cannot
judge for itself.
Keys are matched exactly and case sensitively, the first line that matches wins, and
unknown keys are ignored rather than refused - so a file written by a newer system loses
the settings an older one never had and keeps the ones it did. A line is at most 128 bytes
and a key at most 22, which is what a name is everywhere else on this machine.
Deliberately absent: sections, nesting, types, includes, substitution, conditions. Each is
a step from reading a file toward running one, and the boot loader is the worst place on
this machine to put an interpreter. The test to apply when the pressure arrives is: **if
this file cannot be read, can the machine still start?**
Stage two arranges its own Data Segment. Stage one places Program Memory and nothing else,
since knowing where a payload's data ended would mean knowing a format, so the image is
written into the slot as code followed by data and stage two's first act is to blit its
data down from just past its own code.
SplitDisk speaks the same on disk format SplitBit does, so an image it makes is one the machine can read, and one the machine writes is one it can read back. It is a convenience rather than a necessity: SplitBit writes its own filesystem, and now assembles its own programs, so a disk can be filled without leaving the machine.
Files are laid down contiguously, so a disk can have free blocks without having them in one piece. When that happens `put` says so rather than putting part of a file on.
A path is names with `/` between them, always from the root, since a command line tool has nowhere to keep a working directory between one run and the next. `.` and `..` mean what they usually do, and `..` from the root is the root.
A disk has two ceilings and it is usually the less obvious one that bites: blocks, and **entries**. Every file and every directory costs one entry, and `list` says how many of them are gone as well as how many blocks are. On a disk of small files the entries run out long before the space does, which is a matter of how the disk was formatted rather than a limit of the format - `dirblocks` is carried per disk, and each one is 256 bytes and holds eight entries.
### Two Versions:
| Version | What it means |
| --- | --- |
| 1 | Flat. Every file is in the root, because there is nowhere else. |
| 2 | Directories. Each entry says which directory it is in. |
**A version one disk is already a valid version two disk.** The parent is stored as an entry index *plus one*, so the zeroes a version one disk has in those bytes read as "in the root" - which is exactly where all of its files are. There is nothing to convert.
A disk is at the lowest version that describes what is on it, so `format` makes a version one disk and `mkdir` is what raises it. That is deliberate: a disk stays readable by anything that has never heard of a directory right up until it actually has one. Compatibility runs one way, which is the ordinary shape of it - version one code reading a version two disk would list directories as strange empty files.
## Building Programs With Make:
The assembler is built to work with make. `-o` puts the output where the build system wants it, and `-M` writes out which libraries went into it, so that editing a library reassembles everything that includes it.
`Programs/makefile` does this for the programs in this repository:
```
cd Programs
make
```
The rule it uses is small enough to copy into your own projects:
```
$(BUILD)/%.bin: %.asm
@mkdir -p $(@D)
$(ASM) $(INCLUDES) -M $(@:.bin=.d) -o $@ $<
-include $(BINARIES:.bin=.d)
```
## Tests:
```
make test
```
Builds the four tools, checks they build clean under strict ISO C, and runs nine scripts.
`Tests/run.sh` assembles and runs every program in `Programs/` and compares the results
against recorded output; six more ask the questions a recorded file cannot answer. Between
them they check the two assemblers against each other byte for byte, the two SBFS
implementations against each other on the same disk, the disk tool against the format, the
linter against a fixture written to trip it, the terminal behaviour that a pipe makes
invisible, and the manuals against the code.
```
make bless
```
Records the current output as the expected result, after you have checked that it is
correct.
```
make sanitize
```
Rebuilds all four tools with the address and undefined behaviour sanitizers and runs the
whole suite under them. It catches reads and writes past the end of an array, use after
free, leaks, and undefined arithmetic, takes about twice as long, and puts the ordinary
binaries back when it finishes.
Everything is built inside `Tests/build`, so running the suite never overwrites anything in
`Programs/`. To run only some of the tests, call the runner directly with their names:
```
./Tests/run.sh hello 8bitFibonacci
```
What each script can and cannot answer, how to add a test, and where the suite is blind are
in the [SplitBit Test Manual](SplitBit%20Test%20Manual.md).
## Documentation:
Four documents, divided by what they are about rather than by who reads them.
**[SplitBit Programming Manual](SplitBit%20Programming%20Manual.md)** describes **the machine**: the instruction set, the registers, the vector table, interrupts, devices, the memory controller, the console, storage, and faults. Everything here is true of any SplitBit, whatever is running on it.
**[SplitBit Assembler Manual](SplitBit%20Assembler%20Manual.md)** describes **the language and the file formats**: literal values, labels, segments, the directives, the loadable program header, and the assembler that runs on SplitBit itself.
[Programs/CosmOS/README.md](Programs/CosmOS/README.md) describes **the operating system**: its shell, its applications, what a program may ask it for, and the libraries it owns. A different system on the same machine would answer all of that differently, which is why it is documented with the system rather than with the CPU.
**[SplitBit Test Manual](SplitBit%20Test%20Manual.md)** describes **the test suite**: what each of its scripts claims, which of those claims are worth the most, how to add to it, and what it is blind to. It is about this repository rather than about the machine, which is why it comes last.
## License:
Apache License, Version 2.0. You may obtain a copy at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0).