diff --git a/CONTEXT.md b/CONTEXT.md index dbfb560..98c2c76 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -32,7 +32,11 @@ every pipeline register, forwarding path and stall comes from a `dbg_*` port on - Phase 7 (views): DONE. L1 datapath SVG (`DatapathView`, values from a single-cycle run of the same program, `datapath.ts`), L3 hazard lab (`hazard-lab.ts`: 8 cases, each run on pipeline fwd/no-fwd and single-cycle), L4 programs comparison and data-memory heatmap (`programs-compare.ts`, `Heatmap`), light/dark theme toggle that restores its saved choice. 67 Vitest + 9 Playwright. -- Next: docs pages and launch (PLAN section 8, the non-app pages); Phase 8 per docs/PLAN.md. +- Phase 8 (landing and docs): BUILT on feat/phase-8-launch, not yet deployed. `/` has a live fib run in the hero; `/docs/*` has the + five pages (how it works, architecture, the pipeline, hazards, verification), each with a live `MiniRun` embed where it helps. + 72 Vitest and 17 Playwright tests pass. Still to do: Vercel deploy (needs the user's go-ahead), the hero GIF/MP4, and the + CodeMirror editor, pass-by-pass replay and default-branch items that the plan lists under Phase 8. +- Next: Phase 9 (FPGA synthesis), which can run alongside the Phase 8 launch work. ## Interfaces Phase 2+ must honour (defined by the ISS, `sim/golden/iss.py`) - Retire record fields, lane/mask conventions, forced alignment, `cycle_dependent` masking: see the module docstring and PLAN 2.4. @@ -120,3 +124,11 @@ every pipeline register, forwarding path and stall comes from a `dbg_*` port on - The datapath view needs operand values, which a pipeline run does not expose per instruction. It reads them from a single-cycle run of the same program; both cores compute the same architecture, so the values match. - `tabpanel` ids and `aria-controls` must match the tab ids exactly, or the Playwright role queries and screen readers miss them. + +## Gotchas found in Phase 8 +- The plan refers to tiny-tpu's `LandingEmbed` and `gen-og-image.mjs`, but that repo has no web app here, so the landing and + docs pages use their own `MiniRun` island instead. +- A branch switch whose tool call was interrupted did not happen; check `git branch --show-current` before building on it. +- A `.gitignore` rule with a trailing comment is not an exception: git reads the comment as part of the pattern. Keep comments on + their own lines (this hid the WASM binary from git until Phase 7's CI caught it). +- An embedded run starts at cycle 1, so "Halted" appears only after stepping to the end. Tests must click "Go to end" first. diff --git a/README.md b/README.md index 091bd28..85b6a8b 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,9 @@ Third in the series after [tiny-gpu](https://github.com/deaneeth/tiny-gpu) and [ -> **Status: Phase 7 done (views).** The app at `/app` has four views: the pipeline, the single-cycle datapath, a hazard lab that measures each hazard on the pipeline with and without forwarding, and a programs comparison with a data-memory heatmap. Next: the docs pages and launch. +> **Status: Phase 8 built, not yet deployed.** The app at `/app`, the landing page and five docs pages are built and +> tested on `feat/phase-8-launch`. The Vercel deploy and FPGA synthesis numbers (Phase 9) are still to come. > -> *Previous: Phase 6 done (web app, first slice); Phase 5 done (WASM harness).* Both cores run in one WebAssembly module (`wasm/build.sh`, 77 KB gzipped) and pass the golden fixtures in Node (`node wasm/test.mjs`). Next: the web app. -> -> *Previous: Phase 4 done (hazards).* Both RV32I cores retire instructions identically to the Python golden ISS on all 40 official `rv32ui-p-*` tests, 5 programs and hundreds of random programs. The 5-stage core has forwarding and load-use stalls (switchable at runtime) and matches an independent timing model cycle for cycle. Next: the WASM harness. > See [`docs/PLAN.md`](docs/PLAN.md) for the full roadmap. Every pipeline register, forwarding path and stall the UI will draw is a live `dbg_*` signal from the @@ -27,12 +25,29 @@ compiled RTL. Nothing on screen is faked. rtl/*.sv -> Verilator C++ -> Emscripten -> web/public/tiny_cpu.wasm -> React island -> SVG ``` -## Planned +## What it does + +- A single-cycle and a 5-stage pipelined RV32I core sharing one decoder, ALU and register file, with forwarding, + load-use stalls and taken-branch flushes. +- The same hardware in the browser: an assembler (byte-identical to GNU's), an editor, and a viewer that steps through + every clock edge with the pipeline, the single-cycle datapath, a hazard lab and a programs comparison. +- FPGA synthesis numbers for iCE40 and ECP5 (Phase 9, in progress). + +## Verification + +Each check runs in continuous integration on every change. + +| What | How much | Against | +|---|---|---| +| Official RISC-V instruction tests (`rv32ui`) | 40 of 42, both cores | Python golden ISS. `fence_i` and `ma_data` are excluded. | +| Hand-written and random programs | 5 and 260 (both forwarding modes) | Golden ISS for results; timing model for pipeline cycles | +| Fault cases | 13 | Golden ISS | +| Mutation checks | 7 (pipeline) and 11 (hazards) | Each one must make a test fail | +| WASM module | 534 checks | Golden fixtures, both cores, forwarding on and off | +| Web app | 72 unit tests, 17 browser tests | Assembler vs GNU bytes; presets on the WASM core | +| Size | About 77 KB gzipped | Build fails above 400 KB | -- Single-cycle and 5-stage pipelined RV32I cores sharing one ALU/decoder/regfile -- Verified against a Python golden ISS, the official `riscv-tests` and random hazard-heavy programs -- In-browser assembler, step/rewind, datapath / pipeline / hazard-lab / programs views -- FPGA synthesis numbers (iCE40 / ECP5) +Details, and what is not verified yet, are on the Verification docs page and in `docs/PLAN.md`. ## Layout diff --git a/web/e2e/docs.spec.ts b/web/e2e/docs.spec.ts new file mode 100644 index 0000000..5e20b6a --- /dev/null +++ b/web/e2e/docs.spec.ts @@ -0,0 +1,45 @@ +// Smoke tests for the landing page and the five docs pages, against the production build. The embedded runs +// start when they scroll into view (client:visible), so each test scrolls the embed first. +import { expect, test } from "@playwright/test"; + +test("the landing page runs fib live and reports PASS", async ({ page }) => { + await page.goto("/"); + await expect(page.getByRole("heading", { level: 1 })).toContainText("A real RISC-V CPU"); + const embed = page.locator("figure").first(); + await embed.scrollIntoViewIfNeeded(); + await expect(embed.getByText(/^Cycle 1 of \d+/)).toBeVisible({ timeout: 60_000 }); + await embed.getByRole("button", { name: "Go to end" }).click(); + await expect(embed.getByText(/Halted at cycle \d+/)).toBeVisible(); + await expect(embed.getByText(/PASS/)).toBeVisible(); +}); + +for (const [path, heading] of [ + ["/docs/how-it-works", "How it works"], + ["/docs/architecture", "Architecture"], + ["/docs/the-pipeline", "The pipeline"], + ["/docs/hazards", "Hazards"], + ["/docs/verification", "Verification"], +] as const) { + test(`${path} renders with navigation`, async ({ page }) => { + await page.goto(path); + await expect(page.getByRole("heading", { level: 1, name: heading })).toBeVisible(); + await expect(page.getByRole("link", { name: "Open the CPU" }).first()).toBeVisible(); + await expect(page.locator('a[aria-current="page"]')).toHaveText(heading); + }); +} + +test("the how-it-works embed runs on the single-cycle core", async ({ page }) => { + await page.goto("/docs/how-it-works"); + const embed = page.locator("figure").first(); + await embed.scrollIntoViewIfNeeded(); + await expect(embed.getByText(/^Cycle 1 of \d+/)).toBeVisible({ timeout: 60_000 }); + await embed.getByRole("button", { name: "Go to end" }).click(); + await expect(embed.getByText(/Halted at cycle \d+ — PASS/)).toBeVisible(); +}); + +test("docs pages have no horizontal scroll at phone width", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto("/docs/verification"); + const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); + expect(overflow).toBeLessThanOrEqual(1); +}); diff --git a/web/src/components/MiniRun.tsx b/web/src/components/MiniRun.tsx new file mode 100644 index 0000000..71c0926 --- /dev/null +++ b/web/src/components/MiniRun.tsx @@ -0,0 +1,118 @@ +// A small, self-contained run for the landing page and docs: assembles one program, runs it once on the +// chosen core, and lets the reader step through the cycles with the same pipeline view as /app. +import { useEffect, useMemo, useState } from "react"; +import { assemble, type AsmResult } from "../lib/assembler"; +import { runProgram, type RunResult } from "../lib/runner"; +import { buildRunModel, type RunModel } from "../lib/run-model"; +import { loadTinyCpu, type TinyCpuModule } from "../lib/wasm-loader"; +import type { CoreName } from "../lib/state-schema"; +import { PipelineView } from "./PipelineView"; + +interface Props { + source: string; + core: CoreName; + forwarding?: boolean; + /** what the reader is looking at, shown above the run */ + caption: string; + maxCycles?: number; +} + +interface Ready { + asm: AsmResult; + run: RunResult; + model: RunModel; +} + +const button = + "rounded-md border border-[var(--line)] px-3 py-1 font-mono text-sm hover:bg-[var(--active)] disabled:opacity-40"; + +export function MiniRun({ source, core, forwarding = true, caption, maxCycles = 20_000 }: Props) { + const [mod, setMod] = useState(null); + const [failure, setFailure] = useState(null); + const [index, setIndex] = useState(0); + const [playing, setPlaying] = useState(false); + + useEffect(() => { + let alive = true; + loadTinyCpu(`${import.meta.env.BASE_URL}tiny_cpu.mjs`) + .then((m) => alive && setMod(m)) + .catch((e: unknown) => alive && setFailure(e instanceof Error ? e.message : String(e))); + return () => { + alive = false; + }; + }, []); + + const result = useMemo(() => { + if (!mod) return null; + const asm = assemble(source); + if (!asm.ok) return { error: asm.errors[0]?.message ?? "does not assemble" }; + const run = runProgram(mod, asm, { core, forwarding, maxCycles }); + return { asm, run, model: buildRunModel(run.states, asm.dmem) }; + }, [mod, source, core, forwarding, maxCycles]); + + const ok: Ready | null = result && "run" in result ? result : null; + const last = ok ? ok.run.states.length - 1 : 0; + const i = Math.min(index, last); + + useEffect(() => { + if (!playing) return; + const id = window.setInterval(() => { + setIndex((k) => { + if (k >= last) { + setPlaying(false); + return k; + } + return k + 1; + }); + }, 250); + return () => window.clearInterval(id); + }, [playing, last]); + + const state = ok ? (ok.run.states[i] ?? null) : null; + const stats = ok ? ok.model.stats(i) : null; + const status = (() => { + if (failure) return `Could not load the core: ${failure}`; + if (!result) return "Loading the core…"; + if ("error" in result) return `Assembly error: ${result.error}`; + if (result.run.fault) return "Fault: the program stopped on an illegal access."; + if (state?.halted) return `Halted at cycle ${state.cycle}${state.tohost === 1 ? " — PASS" : ""}.`; + if (result.run.truncated) return `Stopped at ${maxCycles.toLocaleString()} cycles without halting.`; + return `Cycle ${state?.cycle ?? 0} of ${result.run.states.length}`; + })(); + + return ( +
+
+ {caption} + {core === "PIPE" ? (forwarding ? "5-stage, forwarding" : "5-stage, no forwarding") : "single-cycle"} +
+ + {ok && state ? ( + + ) : ( +

{status}

+ )} + + {ok && ( + <> +
+ + + + + + {status} +
+ {stats && ( +

+ {stats.instret} instructions · CPI {stats.instret ? stats.cpi.toFixed(2) : "—"} + {core === "PIPE" ? ` · ${stats.stalls} stalls · ${stats.flushes} flushes` : ""} +

+ )} + + )} +
+ ); +} diff --git a/web/src/layouts/DocsLayout.astro b/web/src/layouts/DocsLayout.astro new file mode 100644 index 0000000..7e5b5ad --- /dev/null +++ b/web/src/layouts/DocsLayout.astro @@ -0,0 +1,88 @@ +--- +import Layout from "@/layouts/Layout.astro"; + +interface Props { + title: string; + current: string; +} + +const { title, current } = Astro.props; +const pages = [ + { href: "/docs/how-it-works", label: "How it works" }, + { href: "/docs/architecture", label: "Architecture" }, + { href: "/docs/the-pipeline", label: "The pipeline" }, + { href: "/docs/hazards", label: "Hazards" }, + { href: "/docs/verification", label: "Verification" }, +]; +--- + + +
+ +
+
+

{title}

+ +
+ +
+ + diff --git a/web/src/lib/__tests__/phase8.test.ts b/web/src/lib/__tests__/phase8.test.ts new file mode 100644 index 0000000..1651394 --- /dev/null +++ b/web/src/lib/__tests__/phase8.test.ts @@ -0,0 +1,52 @@ +// Phase 8: every state a real run produces passes the schema validator, and the programs used on the +// landing and docs pages assemble and halt with PASS on the WASM core. +import { describe, expect, it } from "vitest"; +import { assemble } from "../assembler"; +import { HAZARD_CASES } from "../hazard-lab"; +import { runProgram } from "../runner"; +import { isCycleState } from "../state-schema"; +import { loadTinyCpu } from "../wasm-loader"; +import fibSource from "../presets/fib.s?raw"; +import helloSource from "../presets/hello.s?raw"; + +const moduleUrl = new URL("../../../public/tiny_cpu.mjs", import.meta.url).href; +const mod = await loadTinyCpu(moduleUrl); + +describe("states from a real run", () => { + it("every CycleState from the pipeline run passes the validator", () => { + const asm = assemble(fibSource); + const run = runProgram(mod, asm, { core: "PIPE", forwarding: true, maxCycles: 20_000 }); + expect(run.states.every((s) => isCycleState(s))).toBe(true); + }); + + it("the single-cycle run validates too, and the validator rejects a corrupted state", () => { + const asm = assemble(helloSource); + const run = runProgram(mod, asm, { core: "SINGLE", forwarding: true, maxCycles: 20_000 }); + expect(run.states.every((s) => isCycleState(s))).toBe(true); + const broken = { ...run.states[0], regs: run.states[0]?.regs.slice(0, 31) }; + expect(isCycleState(broken)).toBe(false); + }); +}); + +describe("programs on the landing and docs pages", () => { + it("fib halts with PASS on the pipeline with forwarding", () => { + const run = runProgram(mod, assemble(fibSource), { core: "PIPE", forwarding: true, maxCycles: 20_000 }); + expect(run.halted).toBe(true); + expect(run.tohost).toBe(1); + }); + + it("hello halts with PASS on the single-cycle core", () => { + const run = runProgram(mod, assemble(helloSource), { core: "SINGLE", forwarding: true, maxCycles: 20_000 }); + expect(run.halted).toBe(true); + expect(run.tohost).toBe(1); + }); + + it("the load-use hazard program used on the hazards page assembles and halts", () => { + const c = HAZARD_CASES.find((x) => x.id === "load-use"); + expect(c).toBeDefined(); + const asm = assemble(c?.source ?? ""); + expect(asm.errors).toEqual([]); + const run = runProgram(mod, asm, { core: "PIPE", forwarding: true, maxCycles: 2_000 }); + expect(run.halted).toBe(true); + }); +}); diff --git a/web/src/pages/docs/architecture.astro b/web/src/pages/docs/architecture.astro new file mode 100644 index 0000000..09c0412 --- /dev/null +++ b/web/src/pages/docs/architecture.astro @@ -0,0 +1,62 @@ +--- +import DocsLayout from "@/layouts/DocsLayout.astro"; +--- + + +

+ The design is split so that each piece can be checked on its own. The RTL is the hardware. A golden model + written in Python is the specification. The WebAssembly module is the same hardware, seen from the browser. +

+ +

Memory map

+ + + + + + + + + + + +
RegionAddressSizeNotes
IMEM0x0000_00004 KiBLoaded by the harness; read-only to the core.
DMEM0x0001_00004 KiBByte-addressable: LB, LH, LW, SB, SH, SW.
CONSOLE0x0002_00001 wordWriting a byte sends it to the console panel.
TOHOST0x0002_00041 wordWriting 1 reports PASS and halts the core.
CYCLE0x0002_00081 wordReads the cycle counter.
+

+ Instruction and data memory are separate ports (a Harvard design), but they share one address space, so a + program can read its own constants from DMEM. +

+ +

Two cores, one instruction set

+
    +
  • + Single-cycle: fetch, decode, execute, memory and write-back in one cycle. It is the reference for + what the program should do, and its cycle count is exactly its instruction count. +
  • +
  • + 5-stage pipeline: IF, ID, EX, MEM and WB, with pipeline registers between them. It takes the same + instructions and produces the same results, but may take more cycles, because of hazards. +
  • +
+

Both cores are compiled into the same WebAssembly module, and the app chooses one at run time.

+ +

The golden model

+

+ The reference is an instruction-set simulator written in Python (sim/golden/iss.py). It executes + every instruction and writes a retire record for each one: the program counter, the instruction, the register + and memory it changed, and the next program counter. The RTL is checked by comparing its retire records to the + model's, instruction by instruction. +

+ +

Assembler in the browser

+

+ The app assembles RV32I source itself. Its output is byte-for-byte what the GNU RISC-V toolchain produces for + the same source, which is tested against GNU's output. +

+ +

The contract between hardware and browser

+

+ The WebAssembly module exposes the state of each cycle as a plain object: the five stages, the hazard flags, + the registers, the retired instruction, console output and tohost. The field-by-field definition is in + docs/STATE_SCHEMA.md, and the browser checks every state it receives against it. +

+
diff --git a/web/src/pages/docs/hazards.astro b/web/src/pages/docs/hazards.astro new file mode 100644 index 0000000..d168f78 --- /dev/null +++ b/web/src/pages/docs/hazards.astro @@ -0,0 +1,55 @@ +--- +import DocsLayout from "@/layouts/DocsLayout.astro"; +import { MiniRun } from "@/components/MiniRun"; +import { HAZARD_CASES } from "@/lib/hazard-lab"; + +const loadUse = HAZARD_CASES.find((c) => c.id === "load-use")?.source ?? ""; +--- + + +

+ A hazard is a situation where the next instruction cannot run in the next cycle. The pipeline has three kinds, + and the core handles each one differently. The app's hazard lab runs each kind on the + pipeline with forwarding on and off, and on the single-cycle core, so you can see the cost. +

+ +

Data hazards: a value is not ready yet

+

+ If an instruction reads a register that the instruction just before it is still computing, the value does not + exist yet in the register file. The pipeline has two options. +

+
    +
  • + Forwarding: take the value from the EX/MEM or MEM/WB pipeline register, where it already is, and pass + it straight into EX. With forwarding, an ALU result can be used by the very next instruction with no delay. + Without it, the pipeline must wait. +
  • +
  • + Stalling: hold the dependent instruction in ID for a cycle. The hazard lab's "ALU to ALU, distance 1" + case shows the difference: forwarding removes the stall. +
  • +
+ +

Load-use: the one case forwarding cannot fix

+

+ A load returns its value at the end of the MEM stage, which is later than the EX stage that needs it. Even with + forwarding, the next instruction must wait one cycle. This is the only stall the core takes with forwarding on. + Step through this program and find the cycle where the dependent addi waits. +

+ + +

Control hazards: a branch changes the path

+

+ A branch decides whether to jump in EX, but two instructions behind it have already been fetched. The core + predicts that a branch is not taken, so it keeps fetching in order. If the branch is taken, those two + instructions are discarded: a taken branch costs two cycles, with or without forwarding. A branch that is not + taken costs nothing. +

+ +

Measured, not assumed

+

+ Each claim on this page is a test. The Vitest suite runs every hazard case on the WASM module and checks the + cycle counts: the distance-1 stall disappears with forwarding, the load-use case stalls exactly once, the taken + branch costs the same with and without forwarding, and the independent case is the same either way. +

+
diff --git a/web/src/pages/docs/how-it-works.astro b/web/src/pages/docs/how-it-works.astro new file mode 100644 index 0000000..b643660 --- /dev/null +++ b/web/src/pages/docs/how-it-works.astro @@ -0,0 +1,42 @@ +--- +import DocsLayout from "@/layouts/DocsLayout.astro"; +import { MiniRun } from "@/components/MiniRun"; +import hello from "@/lib/presets/hello.s?raw"; +--- + + +

+ TinyCPU is a 32-bit RISC-V (RV32I) processor written in SystemVerilog. It has two cores that share one + instruction set: a single-cycle reference core, which finishes one instruction per clock, and a 5-stage + pipelined core, which overlaps instructions and handles the hazards that creates. +

+

+ Everything on this site runs the real hardware. The SystemVerilog is compiled by Verilator into C++, the C++ + is compiled by Emscripten into WebAssembly, and the browser steps that WebAssembly one clock edge at a time. + Nothing is simulated in JavaScript. +

+ +

The chain, from source to screen

+
    +
  • RTL: rtl/*.sv, both cores and the memory-mapped I/O.
  • +
  • Verilator: turns the RTL into one C++ model per core, so both run in one WebAssembly module.
  • +
  • Emscripten: links the models into tiny_cpu.wasm, about 77 KB gzipped.
  • +
  • The app: an assembler and a viewer that show every cycle, every register and every byte of memory.
  • +
+ +

Try it: a program on the single-cycle core

+

+ The program below prints a greeting one byte at a time through the console register, then reports PASS. + On the single-cycle core every instruction takes exactly one cycle, so the cycle count equals the instruction + count. Step through it. +

+ + +

Where to go next

+
    +
  • Architecture: the memory map, the two cores and how they are checked.
  • +
  • The pipeline: the five stages, and what happens to an instruction in each.
  • +
  • Hazards: when the pipeline has to wait, and how forwarding avoids it.
  • +
  • Verification: what is tested, and against what.
  • +
+
diff --git a/web/src/pages/docs/the-pipeline.astro b/web/src/pages/docs/the-pipeline.astro new file mode 100644 index 0000000..da894e5 --- /dev/null +++ b/web/src/pages/docs/the-pipeline.astro @@ -0,0 +1,47 @@ +--- +import DocsLayout from "@/layouts/DocsLayout.astro"; +import { MiniRun } from "@/components/MiniRun"; +import fib from "@/lib/presets/fib.s?raw"; +--- + + +

+ The pipelined core works on five instructions at once, one per stage. A new instruction starts every cycle, so + in steady state the core finishes one instruction per cycle, as the single-cycle core does, when no hazard + interrupts the flow. +

+ +

The five stages

+ + + + + + + + + + + +
StageWhat happens
IFFetch the instruction at the program counter.
IDDecode it and read the source registers.
EXCompute the ALU result, or the branch decision, or a memory address.
MEMLoad from or store to data memory.
WBWrite the result to the destination register.
+ +

Watch it run

+

+ This is the Fibonacci program on the pipelined core, with forwarding on. The stage boxes show which instruction + is in each stage on the selected cycle. Step through a few cycles and watch the first instructions move + through the stages. +

+ + +

Two things that break the flow

+
    +
  • + A hazard is an instruction that needs a value the pipeline has not produced yet. The pipeline either + forwards the value from a later stage or stalls. See Hazards. +
  • +
  • + A taken branch changes the program counter after two instructions have already been fetched behind it. + The pipeline predicts that branches are not taken, and when one is taken it discards those two instructions. +
  • +
+
diff --git a/web/src/pages/docs/verification.astro b/web/src/pages/docs/verification.astro new file mode 100644 index 0000000..513388b --- /dev/null +++ b/web/src/pages/docs/verification.astro @@ -0,0 +1,43 @@ +--- +import DocsLayout from "@/layouts/DocsLayout.astro"; +--- + + +

+ A processor is only worth what its tests can show. Each row below is a check that runs in continuous integration + on every change, and each one can fail. +

+ + + + + + + + + + + + + + + + + + + +
WhatHow muchAgainst
Official RISC-V instruction tests (rv32ui)40 of 42, both coresThe golden model. fence_i and ma_data are excluded: they need features this core does not have.
Hand-written programs5, both coresThe golden model for results; the timing model for pipeline cycles.
Random programs260, both forwarding modesThe golden model for results; the timing model for pipeline cycles.
Fault cases13The golden model: illegal instructions, bad accesses, misaligned jumps.
Mutation checks7 on the pipeline (Phase 3), 11 on hazards (Phase 4)Each check must fail on a deliberately broken core. A check that passes on a broken core is itself a bug.
WASM module534 checksGolden fixtures, on both cores, with forwarding on and off.
Web app67 unit tests and 9 browser testsThe assembler against GNU's bytes, and the presets on the WASM core.
Size budgetAbout 77 KB gzippedThe build fails above 400 KB.
+ +

What "cycle-exact" means

+

+ The pipeline's cycle count is checked against an independent timing model written in Python + (sim/golden/pipemodel.py). The RTL and the model are two separate implementations of the same + timing rules, so agreement between them is evidence about both. +

+ +

What is not verified yet

+
    +
  • Clock speed. Cycles are measured; time to finish needs the FPGA synthesis numbers, which are still to come.
  • +
  • The two excluded riscv-tests, and anything the core does not implement (interrupts, privilege modes, unaligned access).
  • +
+
diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro index a45081f..e8e964f 100644 --- a/web/src/pages/index.astro +++ b/web/src/pages/index.astro @@ -1,19 +1,54 @@ --- import Layout from "@/layouts/Layout.astro"; +import { MiniRun } from "@/components/MiniRun"; +import fib from "@/lib/presets/fib.s?raw"; --- - -
-

TinyCPU

-

- A real, synthesizable RV32I RISC-V CPU in SystemVerilog, compiled to WebAssembly. Write assembly, - run it on the simulated pipeline, and step through every cycle. -

-

- Open the CPU → -

-

- WASM toolchain spike -

+ +
+ +
+ +
+
+

A real RISC-V CPU, running in your browser.

+

+ TinyCPU is a 32-bit RISC-V processor written in SystemVerilog, with a single-cycle core and a 5-stage + pipeline. The hardware is compiled to WebAssembly, so the clock you see below is the real design, stepping + one cycle at a time. +

+ +
+ +
+

Fibonacci on the pipeline

+

+ This runs live in your browser. Step through it to watch five instructions share the pipeline. +

+ +
+ +
+
+

Two cores

+

A single-cycle reference and a 5-stage pipeline with forwarding, stalls and branch flushes.

+
+
+

Checked, not assumed

+

40 official RISC-V tests and 260 random programs agree with a golden model, and pipeline cycle counts match an independent timing model.

+
+
+

Every cycle visible

+

Registers, memory, hazards and the datapath, one clock edge at a time.

+
+