Crash1.json, made in soundThing and converted by SoundPatch. Two noise oscillators fifteen cents apart, a filter opened 11.6 kHz by the modulation envelope, and the LEVEL shaped by that same envelope rather than by the amplitude one - which is exactly the freedom the routed level was for, and not a thing the engine could express a week ago. It peaks at 10,501 and is gone in about six tenths of a second, inside the eight tenths the pieces are in the air. The guessed one wandered between 1600 and 8200 and sounded like a drain. The patch predates the gate field, so the program's own trigger write is what makes it a one-shot - which is the argument for having put it there rather than leaving it to the patch. LowFuel.json is here too, saved from the newer soundThing with the gate and LFO mode in it, and is not wired to anything yet. Both JSONs live beside the tables they produce. The build has never needed soundThing and still does not, but a sound can now be regenerated, or simply read, without going to find the editor first.
692 lines
42 KiB
Markdown
692 lines
42 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.
|
|
|
|

|
|
|
|
*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 |
|
|
| [`Source/Patch`](Source/Patch) | SoundPatch, which turns a soundThing patch into a table the sound device takes |
|
|
| [`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/Sounds`](Programs/Sounds) | Patches as soundThing saved them, and the tables SoundPatch made from them, both checked in so the build never needs soundThing |
|
|
| [`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.
|
|
|
|
**Control, Shift and R is the reset button.** On real hardware it is not a key at all - a
|
|
Voyager has a button on the case, and what a window has instead of a case is a gesture. Three
|
|
things follow from that. It must not be a key software might want, because a machine with a
|
|
keyboard has function keys and something will eventually have a use for them. It must not be
|
|
reachable by accident, because restarting throws away everything in memory and a single key
|
|
that does that sits one mistake away from losing work. And **it must be a gesture the host
|
|
has no opinion about**.
|
|
|
|
That last one rules out the obvious answer. Control, Alt and Delete has meant this since 1981
|
|
and cannot be used: it is a secure attention key, reserved by every serious operating system
|
|
so that it always reaches the system and never an application - precisely so a program cannot
|
|
imitate a login screen. On Windows an application cannot see it without a kernel driver, and
|
|
on Linux the desktop takes it. It is unavailable for the same reason it seemed right.
|
|
|
|
It does exactly what writing to the machine port does: the machine starts the way it started,
|
|
so the boot chain runs again and finds whatever the disk now says to run. **And it works on a
|
|
machine that has stopped** - one that halted, or faulted, or is a bare metal demo that ended.
|
|
That is the whole point of a button: a machine which is not going anywhere is exactly the one
|
|
worth restarting, and it is the one that cannot notice a request by itself, because a reset is
|
|
otherwise seen between instructions and a halted machine runs none.
|
|
|
|
This part is **emulator magic and known to be**. There is no reset line on this machine yet
|
|
and no keyboard controller to assert one; the window reaches in and sets the same flag the
|
|
machine port sets. When those are designed, a keyboard controller will have to see the gesture
|
|
and pull reset regardless of what the CPU is doing - which is the property that matters, and
|
|
the one a port write can never have, since a port write needs a program willing and able to
|
|
make it.
|
|
|
|
That is what makes a bare metal program escapable. `Once` puts one in front of the next start
|
|
and deletes the request before jumping, so a demo that has taken the whole machine is one
|
|
gesture away from the system coming back, without closing the window and opening it again. It
|
|
works on a machine that is stuck waiting for a key, too, which is when a reset button earns
|
|
its keep.
|
|
|
|
**Escape reaches the machine.** Raylib closes a window on Escape unless it is told not to,
|
|
and this machine sends Escape to the console like any other key - so a program reading keys
|
|
could be ended by one of them, taking whatever was in memory with it.
|
|
|
|
**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 five 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:
|
|
|
|
```
|
|
make run-voyager
|
|
```
|
|
|
|
`make` builds a disk as well as the tools, so there is one to boot. `make run-cosmos` is the
|
|
same system in the terminal, for a machine with no graphics library.
|
|
|
|
Both put a second disk in drive 1, at `Disks/personal.img`. It is made once and then never
|
|
rebuilt, cleaned or committed: everything else here can be thrown away and made again from
|
|
source, and that one is where anything made ON the machine lives.
|
|
|
|
Starting the machine copies it first, three starts back, as `personal.img.1` and so on. Not
|
|
one copy: the way a disk is lost is that something goes wrong and the very next thing anybody
|
|
does is start the machine again to see how bad it is, which is exactly when a single backup
|
|
would be overwritten by the wreckage.
|
|
|
|
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`.
|
|
|
|
## Making A Patch: SoundPatch
|
|
|
|
```
|
|
./SoundPatch <patch.json> <label> [output.asm]
|
|
```
|
|
|
|
The sound device is soundThing's voice engine with the editor taken off, so a patch designed
|
|
in soundThing makes the same sound here. What differs is how it arrives: soundThing writes
|
|
seconds and hertz, and the device takes bytes through a selector. SoundPatch converts one into
|
|
the other and writes a table a program can hand to the device.
|
|
|
|
A byte is not a number of seconds, and that is the whole reason this exists. Times are squared
|
|
into four seconds, because the difference between five and fifty milliseconds is the character
|
|
of a percussive sound and the difference between three and four seconds is nothing anybody can
|
|
hear. Cutoff and LFO rate are exponential, because hearing is logarithmic. Depths and detune
|
|
are centred on 128, so half of nothing is no change. The first sound written for a game here
|
|
was guessed at directly in bytes: a cutoff of 40 looks small and is 57 Hz, and the bang came
|
|
out as a low gurgle.
|
|
|
|
A field the tool does not recognise stops it rather than being skipped, because a patch format
|
|
that has moved on would otherwise produce a table that quietly means something else.
|
|
|
|
**The build does not depend on it.** soundThing lives in its own repository and is not
|
|
required to build anything here. `Programs/Sounds` holds both halves - the patch as soundThing
|
|
saved it and the table made from it - so a sound can be regenerated, or read, without going
|
|
and finding the editor first. The tool is for when a sound is being changed.
|
|
|
|
## 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.
|
|
|
|
The makefile does this for the programs in this repository:
|
|
|
|
```
|
|
make programs
|
|
```
|
|
|
|
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 five 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 five 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).
|