- Rust 60.2%
- SystemVerilog 18.9%
- Assembly 10.7%
- Verilog 4.1%
- Makefile 2.5%
- Other 3.5%
M5d-a left six things and this answers them. The drain window has one clock of thirty-one spare -- twelve posts plus two nine-clock gate handshakes is thirty -- so the allocator's three rules are combinational and resolve on the clock the note event drains. A sequential scan needs forty-six of thirty-one and would reopen the window's timing argument, which every scenario in the vector set depends on. The pipelined candidate register that would take find_free and pick_victim off that clock's path is specified, with its settling argument, and deliberately not built: it is a remedy for a timing problem no fit has measured. `age` is 32 bits, transcribed literally, because then the bit-exactness argument is "the same arithmetic on the same width" and needs no proof. A 3-bit LRU rank is exactly equivalent until the reference's own counter wraps, and saves about 120 ALMs of the 9,401 free -- rejected at that price for an equivalence argument to write and defend. Retirement runs inside the engine, at the end of the slot it just rendered, because every field `retire_finished` resets is per-slot state in syn7_voice's own file. That costs zero drain-window clocks where a top-level pass would need up to eight routed ops and has no budget for one. Section 7.2 is the three-part argument that per-slot and per-frame retirement are observationally identical. A note event stays exactly one gate op, and section 4 is why: at most one active voice holds any given (channel, note), as a consequence of find_matching running before find_free and before the steal. The invariant is asserted in syn7_tb so a later reordering fails there rather than in an audio mismatch a sample late. SOLO_ENGINE is deleted and the third rtl-test pass with it -- once voice v goes to engine v mod N there is nothing left for it to steer, and a polyphonic scenario exercises its property harder than moving one voice to lane 2 did. In its place the scenarios declare a minimum N_ENGINE, because an engine's deadline caps it at three voices and a scenario above three cannot run at N_ENGINE = 1 at all. Two corrections to M5d-a section 7.3. Its warning that `v_vel` becomes an 8:1 selection is wrong: engine e renders only voices e, e+N, e+2N, so every per-voice read is SLOTS:1 -- three at the shipped N_ENGINE = 3, and eight only in the simulation-only pass no bitstream contains. And this slice needs no throwaway fit: allocation makes the other two engines reachable for real, so one shipped fit measures the real machine. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .cargo | ||
| .claude | ||
| .forgejo/workflows | ||
| .vscode | ||
| android | ||
| assets | ||
| docs | ||
| editors/vscode-sbc7 | ||
| keys | ||
| mister | ||
| pkg | ||
| rtl | ||
| scripts | ||
| src | ||
| tools | ||
| web | ||
| .editorconfig | ||
| .envrc | ||
| .gitignore | ||
| .markdownlint.jsonc | ||
| CLAUDE.md | ||
| INSTALL.md | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| RELEASE_NOTES.md | ||
SBC7
Most computers went from 4 to 8 bits and kept growing. SBC7 stopped off to smell the clover and decided 7 felt lucky.
Every data path in this machine -- the bus, the registers, the instruction
words, the memory cells -- is exactly 7 bits wide. Not because it's practical
(it mostly isn't), but because interesting constraints produce interesting
designs. The address bus is 14 bits (two 7-bit halves, notated &HH:LL),
giving the CPU a 16 KB logical address space. Bank switching extends this to
80 KB of physical memory (64 KB RAM + 16 KB ROM) by paging 2 KB banks into
eight independently controlled slots.
The ISA fills all 128 possible opcodes with zero waste: 14 general registers arranged as 7 named pairs (AB, CD, EF, GH, PC, FL, SP), an accumulator-centric ALU with 8 operations x 4 source modes, a 14-bit pair ALU, conditional branches (relative and absolute), stack operations, block copy (LDIR), and a VGA text display. It's a complete little computer -- just one bit short of conventional.
Documentation
See the docs directory.
Collaboration
See here for details. tl;dr: This is a hobby project that I host here. Ask me if you want an account, and you can file issues and PRs like anyone else — or just email me a patch.
Block Diagram
flowchart TD
clk([clk])
rst_n([rst_n])
io(["I/O bus\nio_addr . io_req\nio_we . io_din . io_dout"])
subgraph top["sbc7_top.sv"]
cpu["cpu.sv\n14-bit addr . 7-bit data . I/O bus"]
bankregs["bank_regs.sv\nI/O ports 0-7 . 8 x 7-bit"]
ram["ram.sv\nPort A: CPU . Port B: VGA\n32 banks x 2 KB (64 KB)"]
rom["rom.sv\n8 banks x 2 KB (16 KB)"]
vga["vga.sv\n640x480 @ 60 Hz . 64x24 chars"]
end
hsync([hsync])
vsync([vsync])
pixel([pixel])
clk & rst_n --> cpu
cpu <-->|io_*| bankregs
bankregs -->|"bank[0:7]"| ram & rom
cpu <-->|"addr / data / we"| ram
cpu -->|addr| rom
ram -->|"vram (port B)"| vga
rom -->|"char data (port B)"| vga
vga --> hsync & vsync & pixel
cpu <-->|io_*| io
Memory Map
The 14-bit logical address space is divided into eight 2 KB slots. Bank registers on I/O ports 0-7 select which physical bank appears in each slot. Default mapping (after reset) reproduces a flat layout:
| Range | Slot | Contents |
|---|---|---|
| &00:00-&00:3F | 0 | RST/interrupt vectors (slot 0 is fixed to RAM bank 0) |
| &00:40-&00:7F | 0 | ROM working variables |
| &01:00-&5F:7E | 0-5 | General-purpose RAM; default program load address |
| &5F:7F | 5 | Initial stack pointer (SP grows downward) |
| &60:00-&63:7F | 6 | Memory-mapped I/O (MMIO bypass -- always accessible) |
| &64:00-&6F:7F | 6 | Screen RAM (1536 bytes, 64x24 characters; VGA display bank set by port 37) |
| &70:00-&7F:7F | 7 | ROM (2 KB, reset vector at &70:00) |
Physical memory: 64 KB RAM (32 banks) + 16 KB ROM (8 banks) = 80 KB total. See docs/banking.md for the full specification.
Toolkit
asm7 is a two-pass assembler. Its default output format is H7X, a
checksummed ASCII object format designed for the SBC7; flat binary output is
available via --bin.
disasm7 is a disassembler that auto-detects H7X or binary input and produces annotated assembly listings with hex offsets.
emu7 is a GUI emulator with VGA character display, UART0 wired to
stdin/stdout, and disk, RTC, gamepad and timer emulation. Pass --no-gui for
terminal-only operation.
l7 compiles Lucky 7, the machine's own systems language -- a small
statically typed language that knows about 7-bit cells and bank switching, so
paging a bank in is a with block rather than a hand-written OUT. The command
shell, the edit text editor and the standard library are all written in it.
See the language spec.
roll is Lucky 7's build and dependency tool -- cargo for dice, the
language's unit of compilation. It reads a .dice manifest, resolves
dependencies, and drives l7, link7 and emu7 to build, run and test.
link7 links OB7 relocatable objects into a loadable PR7 program, packing dice into physical banks and dropping code nothing reaches.
s7fs creates and manages S7FS disk images from the host -- format, ls,
put, get, mkdir, rm, and a consistency check.
od7 dumps the structure of an S7B file (.pr7 or .ob7): chunks, symbols
and relocations.
TinyFORTH is a minimal Forth interpreter written in SBC7 assembly, inspired
by jonesforth. It uses 14-bit cells (two 7-bit halves) and provides 30+ built-in
words covering stack manipulation, arithmetic, memory access, and I/O. Run it
with make forth.
TinyBASIC is a line-numbered integer BASIC interpreter inspired by Li-Chen
Wang's Palo Alto Tiny BASIC (1976). Run it with make basic.
TinyLisp is a minimal Lisp interpreter with tagged 14-bit values, a
mark-and-sweep garbage collector, and a built-in standard library. Run it with
make lisp.
fmt7 is an opinionated assembly formatter for SBC7 source files.
lsp7 is a language server providing editor support (completions, diagnostics, hover) for SBC7 assembly.
dap7 is a Debug Adapter Protocol server for step-debugging SBC7 programs.
For the full story -- ISA reference, assembler manual, ROM walkthrough,
language interpreter guides, and more -- see the docs/
directory.
Building
SBC7 uses make as its build tool.
| Command | What it does |
|---|---|
make |
Build ROM image (default) |
make run |
Build and launch the emulator |
make forth |
Build and run TinyFORTH |
make basic |
Build and run TinyBASIC |
make lisp |
Build and run TinyLisp |
make example NAME=typewriter |
Assemble an example program |
make run-example NAME=typewriter |
Build and run an example in the emulator |
Process
SBC7 is a collaboration between a human designer and Claude Code. The ISA was shaped by cross-analysis of the 6502 and Z80 instruction sets, with the goal of maximizing code density within the 7-bit constraint -- squeezing every last microjoule of utility out of 128 opcodes. The working method is straightforward: agree on a design decision in plain language, plan the changes, implement and commit. All decisions -- memory map layout, mnemonic naming, notation conventions -- are recorded in commit history and the documentation, because future-you deserves to know what past-you was thinking.
At boot, a custom glyph at position $20 in the character ROM greets you on
screen -- the Lucky 7 glyph, a stylized 7 that serves as the machine's quiet
little signature.
Font
The SBC7 character ROM is available as a webfont in
tools/sbc7.woff2 (TTF also available). It's a monospace
bitmap font covering printable ASCII, generated from the VGA character ROM
glyphs.
Acknowledgments
The MiSTer FPGA core was built with the help of Alan Steremberg's MiSTer tutorials.
