A single-board computer with a 7-bit data bus and 14-bit address bus.
  • Rust 60.2%
  • SystemVerilog 18.9%
  • Assembly 10.7%
  • Verilog 4.1%
  • Makefile 2.5%
  • Other 3.5%
Find a file
Ben Cox e6c3724c0c syn7: M5d-b design -- one clock to decide which voice
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>
2026-08-23 10:18:11 -04:00
.cargo Add emu7 GUI emulator with VGA display and UART0 serial I/O 2026-03-20 01:30:04 -04:00
.claude use American spellings in comments and documentation 2026-08-07 09:21:17 -04:00
.forgejo/workflows syn7: --syn7-trace, and the replay that makes M5e affordable 2026-08-16 18:30:57 -04:00
.vscode Switch builds, packaging, and tooling to PR7 as default output format 2026-04-12 23:04:49 -04:00
android docs, fdroid: the Forgejo instance is the only place the code lives 2026-08-04 11:49:28 -04:00
assets Switch builds, packaging, and tooling to PR7 as default output format 2026-04-12 23:04:49 -04:00
docs syn7: M5d-b design -- one clock to decide which voice 2026-08-23 10:18:11 -04:00
editors/vscode-sbc7 l7_lsp: surface /// doc comments and complete block members 2026-07-26 23:44:37 -04:00
keys Replace all non-ASCII characters with plain ASCII equivalents 2026-04-05 07:59:49 -04:00
mister syn7: M5d-a final fix wave -- a check whose premise was already false 2026-08-23 03:34:14 -04:00
pkg build: add cpal and hound behind the desktop feature 2026-08-12 17:31:43 -04:00
rtl syn7: M5d-a final fix wave -- a check whose premise was already false 2026-08-23 03:34:14 -04:00
scripts fix s7fs-fatcache-desync bug 2026-08-15 00:05:18 -04:00
src syn7: M5d-a final fix wave -- a check whose premise was already false 2026-08-23 03:34:14 -04:00
tools build: guard rom.s's PRINT_RTC literal against silent drift 2026-08-11 20:25:08 -04:00
web Switch builds, packaging, and tooling to PR7 as default output format 2026-04-12 23:04:49 -04:00
.editorconfig Add fmt7 assembly formatter, apply canonical formatting, and add conventions doc 2026-03-23 18:17:53 -04:00
.envrc Add clap CLI parsing, release automation, and Docker packaging 2026-03-22 16:36:18 -04:00
.gitignore syn7: the fourteen tables, in a form Quartus can read 2026-08-16 18:21:30 -04:00
.markdownlint.jsonc Add fmt7 assembly formatter, apply canonical formatting, and add conventions doc 2026-03-23 18:17:53 -04:00
CLAUDE.md separate the test fixtures from the examples 2026-08-07 14:41:38 -04:00
INSTALL.md packaging: ship what the tools now are, not what they were in 1.3.7 2026-08-07 17:54:35 -04:00
LICENSE Apply CC0-1.0 license, add INSTALL.md, update permissions 2026-03-23 20:29:23 -04:00
Makefile syn7: M5c final fix wave -- seventy-one, and nobody had measured it 2026-08-19 07:43:03 -04:00
README.md use American spellings in comments and documentation 2026-08-07 09:21:17 -04:00
RELEASE_NOTES.md use American spellings in comments and documentation 2026-08-07 09:21:17 -04:00

SBC7

SBC7 Logo

Most computers went from 4 to 8 bits and kept growing. SBC7 stopped off to smell the clover and decided 7 felt lucky.

SBC Screenshot

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.