Skip to content

Instantly share code, notes, and snippets.

@stbenjam
Created April 15, 2026 01:16
Show Gist options
  • Select an option

  • Save stbenjam/46fd716e5dc04e2d26fe5a9ecebe6912 to your computer and use it in GitHub Desktop.

Select an option

Save stbenjam/46fd716e5dc04e2d26fe5a9ecebe6912 to your computer and use it in GitHub Desktop.
SKB CPU — interactive FSM navigator (Main + Memory FSMs from skb_cpu.vhd)
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>SKB CPU — FSM Navigator</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<style>
:root {
--bg-0: #05060f;
--bg-1: #0b0f1f;
--bg-2: #121733;
--ink: #e7ecff;
--ink-dim: #8892c1;
--line: #2a335f;
--glow: 0 0 22px;
/* Main FSM palette */
--c-fetch: #31d0ff;
--c-load-ir: #6fe3a1;
--c-exec: #ffb347;
--c-wr-reg: #ff6fb5;
--c-load-pc: #b580ff;
--c-load-sp: #ffd166;
--c-wret: #ff8787;
/* Memory FSM palette */
--m-idle: #5dc8ff;
--m-wd-high: #7afcb8;
--m-set: #ffd166;
--m-wm-low: #ffa0e5;
--m-wwc: #ff8a5c;
--m-done: #c4ff6d;
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0; min-height: 100%;
background:
radial-gradient(1200px 700px at 85% -10%, #1a2260 0%, transparent 60%),
radial-gradient(900px 600px at -10% 120%, #3a1560 0%, transparent 55%),
radial-gradient(700px 500px at 50% 50%, #0d1333 0%, var(--bg-0) 70%);
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Inter", "SF Pro Text", "Segoe UI", Roboto, sans-serif;
overflow-x: hidden;
}
header {
padding: 28px 40px 10px;
display: flex;
align-items: baseline;
gap: 18px;
flex-wrap: wrap;
}
h1 {
font-size: 28px;
font-weight: 800;
letter-spacing: .3px;
margin: 0;
background: linear-gradient(90deg, #7af7ff, #b580ff 40%, #ff6fb5 70%, #ffd166);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
header .sub {
color: var(--ink-dim);
font-size: 13px;
letter-spacing: .4px;
}
header .file {
font-family: "SF Mono", ui-monospace, Menlo, monospace;
color: #8fb9ff;
background: rgba(143,185,255,.08);
border: 1px solid rgba(143,185,255,.2);
padding: 3px 8px;
border-radius: 6px;
font-size: 12px;
}
.tabs {
display: inline-flex;
gap: 6px;
margin: 10px 40px 0;
padding: 5px;
background: rgba(255,255,255,.04);
border: 1px solid rgba(255,255,255,.08);
border-radius: 12px;
backdrop-filter: blur(8px);
}
.tab {
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
color: var(--ink-dim);
letter-spacing: .3px;
transition: all .18s ease;
user-select: none;
}
.tab:hover { color: var(--ink); background: rgba(255,255,255,.05); }
.tab.active {
color: #0a0c1e;
background: linear-gradient(90deg, #7af7ff, #b580ff);
box-shadow: 0 0 24px rgba(122,247,255,.35);
}
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 340px;
gap: 20px;
padding: 18px 40px 50px;
}
@media (max-width: 1050px) {
.layout { grid-template-columns: 1fr; }
}
.stage {
position: relative;
background:
radial-gradient(600px 300px at 20% 0%, rgba(122,247,255,.08), transparent 55%),
radial-gradient(500px 300px at 100% 100%, rgba(255,111,181,.08), transparent 60%),
linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.015));
border: 1px solid rgba(255,255,255,.08);
border-radius: 18px;
overflow: hidden;
min-height: 640px;
box-shadow: 0 30px 60px -30px rgba(0,0,0,.6), inset 0 1px 0 rgba(255,255,255,.04);
}
.stage svg { display: block; width: 100%; height: auto; }
.fsm-panel { display: none; }
.fsm-panel.active { display: block; }
/* Node styles */
.node { cursor: pointer; }
.node .halo {
opacity: 0;
transition: opacity .25s ease;
filter: blur(12px);
}
.node:hover .halo,
.node.selected .halo,
.node.active .halo { opacity: .9; }
.node.active .shell {
animation: pulse 1.2s ease-in-out infinite;
}
@keyframes pulse {
0%,100% { filter: drop-shadow(0 0 8px currentColor); }
50% { filter: drop-shadow(0 0 22px currentColor); }
}
.node .shell {
transition: transform .2s ease, filter .25s ease;
transform-origin: center;
transform-box: fill-box;
}
.node:hover .shell { transform: scale(1.04); }
.node.selected .shell { filter: drop-shadow(0 0 14px currentColor); }
.node text {
pointer-events: none;
font-family: -apple-system, "Inter", "Segoe UI", sans-serif;
font-weight: 700;
fill: #0a0c1e;
text-anchor: middle;
}
.node .enc {
font-family: "SF Mono", ui-monospace, Menlo, monospace;
font-size: 10px;
fill: rgba(10,12,30,.75);
font-weight: 600;
}
/* Edges */
.edge { pointer-events: stroke; cursor: pointer; }
.edge path.line {
fill: none;
stroke: rgba(180,200,255,.28);
stroke-width: 1.8;
transition: stroke .2s ease, stroke-width .2s ease, filter .2s ease;
}
.edge:hover path.line,
.edge.highlighted path.line {
stroke: #ffffff;
stroke-width: 2.5;
filter: drop-shadow(0 0 6px rgba(255,255,255,.8));
}
.edge.active path.line {
stroke: #ffe15c;
stroke-width: 3;
filter: drop-shadow(0 0 10px #ffd166);
}
.edge text {
font-family: "SF Mono", ui-monospace, Menlo, monospace;
font-size: 10px;
fill: var(--ink-dim);
pointer-events: none;
}
.edge.active text, .edge.highlighted text { fill: #fff; }
/* Moving dot on active transition */
.dot-active {
fill: #fff7b1;
filter: drop-shadow(0 0 6px #ffd166);
}
/* Sidebar */
.side {
background: linear-gradient(180deg, rgba(255,255,255,.04), rgba(255,255,255,.015));
border: 1px solid rgba(255,255,255,.08);
border-radius: 18px;
padding: 20px 20px 22px;
min-height: 300px;
position: sticky;
top: 16px;
max-height: calc(100vh - 32px);
overflow-y: auto;
box-shadow: 0 30px 60px -30px rgba(0,0,0,.6);
}
.side h2 {
margin: 0 0 6px;
font-size: 18px;
letter-spacing: .2px;
}
.side .tag {
display: inline-block;
font-family: "SF Mono", ui-monospace, monospace;
font-size: 11px;
padding: 2px 8px;
border-radius: 6px;
margin-right: 8px;
background: rgba(255,255,255,.06);
color: var(--ink-dim);
border: 1px solid rgba(255,255,255,.08);
}
.side section { margin-top: 18px; }
.side h3 {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--ink-dim);
margin: 0 0 8px;
}
.side p, .side li { color: var(--ink); font-size: 13px; line-height: 1.5; }
.side ul { margin: 0; padding-left: 16px; }
.side code {
font-family: "SF Mono", ui-monospace, Menlo, monospace;
background: rgba(143,185,255,.08);
border: 1px solid rgba(143,185,255,.15);
color: #b7d2ff;
padding: 1px 6px;
border-radius: 4px;
font-size: 12px;
}
.side .empty { color: var(--ink-dim); font-size: 13px; font-style: italic; }
.pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 10px;
border-radius: 999px;
font-size: 12px; font-weight: 600;
background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.1);
color: var(--ink);
}
.pill .dot {
width: 8px; height: 8px; border-radius: 50%;
background: currentColor;
box-shadow: 0 0 10px currentColor;
}
/* Controls */
.controls {
display: flex; gap: 8px;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid rgba(255,255,255,.06);
background: rgba(10,12,30,.4);
flex-wrap: wrap;
}
.controls .spacer { flex: 1; }
.btn {
background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.12);
color: var(--ink);
padding: 7px 12px;
border-radius: 8px;
font-size: 12px;
cursor: pointer;
font-weight: 600;
transition: all .15s ease;
backdrop-filter: blur(6px);
}
.btn:hover { background: rgba(255,255,255,.12); }
.btn.primary {
background: linear-gradient(90deg, #31d0ff, #b580ff);
color: #0a0c1e;
border-color: transparent;
}
.btn.primary:hover { filter: brightness(1.1); box-shadow: 0 0 18px rgba(122,247,255,.5); }
select.btn { appearance: none; padding-right: 24px; background-image: linear-gradient(45deg, transparent 50%, var(--ink) 50%), linear-gradient(-45deg, transparent 50%, var(--ink) 50%); background-position: calc(100% - 12px) 50%, calc(100% - 7px) 50%; background-size: 5px 5px; background-repeat: no-repeat; }
.legend {
position: absolute;
left: 16px; bottom: 14px;
background: rgba(10,12,30,.65);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,.08);
border-radius: 12px;
padding: 10px 14px;
font-size: 11px;
color: var(--ink-dim);
display: flex; gap: 16px;
z-index: 3;
}
.legend .row { display: flex; align-items: center; gap: 6px; }
.legend .sw { width: 10px; height: 10px; border-radius: 3px; }
.status-strip {
font-family: "SF Mono", ui-monospace, monospace;
font-size: 11px;
color: var(--ink-dim);
padding: 7px 12px;
border-radius: 8px;
background: rgba(255,255,255,.04);
border: 1px solid rgba(255,255,255,.08);
}
.status-strip b { color: var(--ink); font-weight: 700; }
footer {
padding: 10px 40px 32px;
color: var(--ink-dim);
font-size: 12px;
text-align: center;
}
</style>
</head>
<body>
<header>
<h1>SKB CPU — FSM Navigator</h1>
<span class="sub">Main &amp; Memory finite state machines</span>
<span class="file">skb_cpu.vhd</span>
</header>
<div class="tabs" role="tablist">
<div class="tab active" data-target="main" role="tab">Main FSM</div>
<div class="tab" data-target="memory" role="tab">Memory FSM</div>
</div>
<div class="layout">
<div class="stage">
<!-- MAIN FSM -->
<div class="fsm-panel active" id="panel-main">
<div class="controls">
<select id="instr-select" class="btn" title="Simulate an instruction">
<option value="LoadWord">LoadWord</option>
<option value="StoreWord">StoreWord</option>
<option value="Push">Push</option>
<option value="Pop">Pop</option>
<option value="ALUInstruction">ALU</option>
<option value="AddImmediate">AddImmediate</option>
<option value="OrImmediate">OrImmediate</option>
<option value="LoadImmediate">LoadImmediate</option>
<option value="LoadUpperImmediate">LoadUpperImmediate</option>
<option value="SkipOnEqual">SkipOnEqual</option>
<option value="SkipOnNotEqual">SkipOnNotEqual</option>
<option value="SkipOnLessThan">SkipOnLessThan</option>
<option value="Jump">Jump</option>
<option value="JumpRegister">JumpRegister</option>
<option value="Rand">Rand</option>
</select>
<button id="play-main" class="btn primary">▶ Simulate</button>
<button id="reset-main" class="btn">Reset</button>
<span class="spacer"></span>
<div class="status-strip" id="status-main">idle — pick an instruction and hit Simulate</div>
</div>
<div class="legend">
<div class="row"><span class="sw" style="background: var(--c-fetch)"></span> Fetch / mem-read</div>
<div class="row"><span class="sw" style="background: var(--c-exec)"></span> Execute</div>
<div class="row"><span class="sw" style="background: var(--c-wr-reg)"></span> Write-back</div>
<div class="row"><span class="sw" style="background: var(--c-load-pc)"></span> PC / control</div>
</div>
<svg id="svg-main" viewBox="0 0 1100 680" preserveAspectRatio="xMidYMid meet">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="rgba(210,220,255,.55)"/>
</marker>
<marker id="arrow-active" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#ffe15c"/>
</marker>
<radialGradient id="g-fetch" cx="35%" cy="30%"><stop offset="0%" stop-color="#a7efff"/><stop offset="100%" stop-color="#31d0ff"/></radialGradient>
<radialGradient id="g-load-ir" cx="35%" cy="30%"><stop offset="0%" stop-color="#c4f9d6"/><stop offset="100%" stop-color="#6fe3a1"/></radialGradient>
<radialGradient id="g-exec" cx="35%" cy="30%"><stop offset="0%" stop-color="#ffe0b3"/><stop offset="100%" stop-color="#ffb347"/></radialGradient>
<radialGradient id="g-wr-reg" cx="35%" cy="30%"><stop offset="0%" stop-color="#ffc4dd"/><stop offset="100%" stop-color="#ff6fb5"/></radialGradient>
<radialGradient id="g-load-pc" cx="35%" cy="30%"><stop offset="0%" stop-color="#dcc2ff"/><stop offset="100%" stop-color="#b580ff"/></radialGradient>
<radialGradient id="g-load-sp" cx="35%" cy="30%"><stop offset="0%" stop-color="#ffe8a8"/><stop offset="100%" stop-color="#ffd166"/></radialGradient>
<radialGradient id="g-wret" cx="35%" cy="30%"><stop offset="0%" stop-color="#ffc0c0"/><stop offset="100%" stop-color="#ff8787"/></radialGradient>
</defs>
<!-- edges drawn first so nodes overlay them -->
<g id="edges-main"></g>
<g id="nodes-main"></g>
</svg>
</div>
<!-- MEMORY FSM -->
<div class="fsm-panel" id="panel-memory">
<div class="controls">
<select id="mem-mode" class="btn" title="Simulate read or write">
<option value="read">Read cycle</option>
<option value="write">Write cycle</option>
</select>
<button id="play-mem" class="btn primary">▶ Simulate</button>
<button id="reset-mem" class="btn">Reset</button>
<span class="spacer"></span>
<div class="status-strip" id="status-mem">idle — pick a mode and hit Simulate</div>
</div>
<div class="legend">
<div class="row"><span class="sw" style="background: var(--m-idle)"></span> Waiting</div>
<div class="row"><span class="sw" style="background: var(--m-set)"></span> Addr assert</div>
<div class="row"><span class="sw" style="background: var(--m-wwc)"></span> Write phase</div>
<div class="row"><span class="sw" style="background: var(--m-done)"></span> Complete</div>
</div>
<svg id="svg-mem" viewBox="0 0 1100 600" preserveAspectRatio="xMidYMid meet">
<defs>
<marker id="arrow2" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="rgba(210,220,255,.55)"/>
</marker>
<marker id="arrow2-active" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#ffe15c"/>
</marker>
<radialGradient id="g-m-idle" cx="35%" cy="30%"><stop offset="0%" stop-color="#bce8ff"/><stop offset="100%" stop-color="#5dc8ff"/></radialGradient>
<radialGradient id="g-m-wd-high" cx="35%" cy="30%"><stop offset="0%" stop-color="#c6fde0"/><stop offset="100%" stop-color="#7afcb8"/></radialGradient>
<radialGradient id="g-m-set" cx="35%" cy="30%"><stop offset="0%" stop-color="#ffe8a8"/><stop offset="100%" stop-color="#ffd166"/></radialGradient>
<radialGradient id="g-m-wm-low" cx="35%" cy="30%"><stop offset="0%" stop-color="#ffd0ef"/><stop offset="100%" stop-color="#ffa0e5"/></radialGradient>
<radialGradient id="g-m-wwc" cx="35%" cy="30%"><stop offset="0%" stop-color="#ffcfb5"/><stop offset="100%" stop-color="#ff8a5c"/></radialGradient>
<radialGradient id="g-m-done" cx="35%" cy="30%"><stop offset="0%" stop-color="#e3ffad"/><stop offset="100%" stop-color="#c4ff6d"/></radialGradient>
</defs>
<g id="edges-mem"></g>
<g id="nodes-mem"></g>
</svg>
</div>
</div>
<!-- Sidebar -->
<aside class="side" id="sidebar">
<h2 id="side-title">Welcome</h2>
<div id="side-tags"></div>
<section>
<h3>About</h3>
<p id="side-about">
Click any <strong>state bubble</strong> to see what it does, what signals it drives,
and where it can go next. Click an <strong>edge</strong> to see its condition.
Hit <strong>Simulate</strong> to watch an instruction flow through the FSM.
</p>
</section>
<section>
<h3>Outputs asserted</h3>
<ul id="side-outputs"><li class="empty">Select a state</li></ul>
</section>
<section>
<h3>Transitions</h3>
<ul id="side-trans"><li class="empty">Select a state</li></ul>
</section>
<section id="side-extras"></section>
</aside>
</div>
<footer>SKB instruction set — soft processor FSMs, rendered from <code style="font-family:'SF Mono',monospace">skb_cpu.vhd</code></footer>
<script>
/* ============================================================
* State + transition data (derived from skb_cpu.vhd)
* ============================================================ */
const MAIN_STATES = {
FetchNextInstruction: {
label: "Fetch",
full: "FetchNextInstruction",
enc: "0000",
color: "var(--c-fetch)",
grad: "url(#g-fetch)",
pos: { x: 160, y: 130 },
r: 62,
about: "Request the next instruction word. PC drives the memory address bus; the memory FSM is told to start a read. We save the current PC into pcORIG so we can still capture it for jump-and-link later.",
outputs: [
["processor_mem_ready", "1"],
["processor_mem_addr", "pcOUT"],
["processor_mem_rw", "0 (read)"],
["pcORIG", "← pcOUT"],
],
trans: [
{ to: "FetchNextInstruction", cond: "processor_mem_done = 0", note: "memory not finished yet — stall" },
{ to: "LoadInstructionRegister", cond: "processor_mem_done = 1", note: "word arrived — clock it into IR" },
],
},
LoadInstructionRegister: {
label: "Load IR",
full: "LoadInstructionRegister",
enc: "0001",
color: "var(--c-load-ir)",
grad: "url(#g-load-ir)",
pos: { x: 440, y: 130 },
r: 62,
about: "The memory-read register has the word; enable the IR and latch it. Now the decoder sees the real opcode in irOUT(15..12).",
outputs: [
["loadIR", "1 — IR latches processor_mem_data_read"],
],
trans: [
{ to: "ExecuteInstruction", cond: "(unconditional)", note: "decoder now stable — execute" },
],
},
ExecuteInstruction: {
label: "Execute",
full: "ExecuteInstruction",
enc: "0010",
color: "var(--c-exec)",
grad: "url(#g-exec)",
pos: { x: 750, y: 130 },
r: 72,
about: "The combinational core runs: ALU computes, muxes pick operands, memory address is driven from register/stack depending on the instruction. Memory-touching ops (LW/SW/Push/Pop) stall here until processor_mem_done goes high. Also decides the next PC via OffsetOrNextOrSkip.",
outputs: [
["processor_mem_ready", "1"],
["processor_mem_addr", "from rs/rt/sp (per instruction)"],
["processor_mem_rw", "1 for StoreWord/Push, else 0"],
["OffsetOrNextOrSkip", "offset/next/skip (set combinationally on exit)"],
],
trans: [
{ to: "ExecuteInstruction", cond: "mem op & processor_mem_done = 0", note: "waiting on SRAM" },
{ to: "LoadProgramCounter", cond: "Push / StoreWord / Skip* / Jump / JumpRegister", note: "no register write needed" },
{ to: "WriteRegisters", cond: "anything else (ALU, LW, Pop, LI, LUI, Rand)", note: "result goes to rd" },
],
},
WriteRegisters: {
label: "Write Regs",
full: "WriteRegisters",
enc: "0011",
color: "var(--c-wr-reg)",
grad: "url(#g-wr-reg)",
pos: { x: 940, y: 340 },
r: 60,
about: "Commit the result to the register file. The regArrayDataIn mux chooses between the ALU, the memory-read register (LW/Pop), immediate, upper-immediate, LFSR output (Rand), or pcINCREMENT (jump link).",
outputs: [
["regWrite", "1"],
["regW", "irOUT(11..9) // rd"],
],
trans: [
{ to: "LoadProgramCounter", cond: "(unconditional)", note: "advance PC next" },
],
},
LoadProgramCounter: {
label: "Load PC",
full: "LoadProgramCounter",
enc: "0100",
color: "var(--c-load-pc)",
grad: "url(#g-load-pc)",
pos: { x: 560, y: 500 },
r: 62,
about: "Clock the program counter. pcIN is either outA (JumpRegister) or pcNEXT = pcOUT + {offset, 1, 2} per the Skip/Jump decoding done in Execute.",
outputs: [
["loadPC", "1"],
["pcIN", "outA if JumpRegister, else pcNEXT"],
],
trans: [
{ to: "LoadStackPointer", cond: "instruction ∈ {Push, Pop}", note: "sp needs to move too" },
{ to: "WriteReturnAddress", cond: "instruction ∈ {Jump, JumpRegister}", note: "store link in r7" },
{ to: "FetchNextInstruction", cond: "otherwise", note: "start next cycle" },
],
},
LoadStackPointer: {
label: "Load SP",
full: "LoadStackPointer",
enc: "0101",
color: "var(--c-load-sp)",
grad: "url(#g-load-sp)",
pos: { x: 250, y: 500 },
r: 60,
about: "Push/Pop need the stack pointer updated. spADDEND is +1 for Push and −1 (0xFFFF) for Pop; the SP register latches the new value.",
outputs: [
["loadSP", "1"],
["spADDEND", "+1 / −1 (push / pop)"],
],
trans: [
{ to: "FetchNextInstruction", cond: "(unconditional)" },
],
},
WriteReturnAddress: {
label: "Write Return",
full: "WriteReturnAddress",
enc: "0110",
color: "var(--c-wret)",
grad: "url(#g-wret)",
pos: { x: 900, y: 540 },
r: 62,
about: "Link register write-back for Jump / JumpRegister. regW forces 111 (r7), and regArrayDataIn is pcINCREMENT — the address of the instruction right after the jump.",
outputs: [
["regWrite", "1"],
["regW", "111 (r7 / link)"],
["regArrayDataIn", "pcINCREMENT = pcORIG + 1"],
],
trans: [
{ to: "FetchNextInstruction", cond: "(unconditional)" },
],
},
};
const MEM_STATES = {
Idle: {
label: ["Idle"],
full: "Idle",
enc: "000",
color: "var(--m-idle)",
grad: "url(#g-m-idle)",
pos: { x: 160, y: 300 },
r: 60,
about: "Nothing happening. Memory FSM sits here until the processor raises processor_mem_ready, which the main FSM does during Fetch and Execute.",
outputs: [
["mem_addressready", "0"],
["processor_mem_done", "0"],
],
trans: [
{ to: "Idle", cond: "processor_mem_ready = 0" },
{ to: "WaitDataReadyHigh", cond: "processor_mem_ready = 1" },
],
},
WaitDataReadyHigh: {
label: ["WaitData", "ReadyHigh"],
full: "WaitDataReadyHigh",
enc: "001",
color: "var(--m-wd-high)",
grad: "url(#g-m-wd-high)",
pos: { x: 370, y: 120 },
r: 66,
about: "Wait for the SRAM's mem_dataready_inv line to go high — the external memory controller's handshake signal indicating we're allowed to start a new transaction.",
outputs: [
["mem_addressready", "0"],
],
trans: [
{ to: "WaitDataReadyHigh", cond: "mem_dataready_inv = 0" },
{ to: "SetAddrLines", cond: "mem_dataready_inv = 1" },
],
},
SetAddrLines: {
label: ["SetAddr", "Lines"],
full: "SetAddrLines",
enc: "010",
color: "var(--m-set)",
grad: "url(#g-m-set)",
pos: { x: 620, y: 120 },
r: 60,
about: "Assert mem_addressready high — tell the memory controller the address lines are valid and a transaction is requested.",
outputs: [
["mem_addressready", "1"],
],
trans: [
{ to: "WaitMemReadyLow", cond: "(unconditional)" },
],
},
WaitMemReadyLow: {
label: ["WaitMem", "ReadyLow"],
full: "WaitMemReadyLow",
enc: "011",
color: "var(--m-wm-low)",
grad: "url(#g-m-wm-low)",
pos: { x: 880, y: 230 },
r: 66,
about: "Keep mem_addressready asserted and wait for mem_dataready_inv to fall — memory has acknowledged. If this is a write we still need to watch it complete; reads are done as soon as we see the ack.",
outputs: [
["mem_addressready", "1"],
],
trans: [
{ to: "WaitMemReadyLow", cond: "mem_dataready_inv = 1" },
{ to: "WaitWriteComplete", cond: "mem_dataready_inv = 0 ∧ processor_mem_rw = 1" },
{ to: "Done", cond: "mem_dataready_inv = 0 ∧ processor_mem_rw = 0" },
],
},
WaitWriteComplete: {
label: ["WaitWrite", "Complete"],
full: "WaitWriteComplete",
enc: "100",
color: "var(--m-wwc)",
grad: "url(#g-m-wwc)",
pos: { x: 760, y: 470 },
r: 66,
about: "Write cycles take an extra beat — wait for mem_dataready_inv to go back high, signalling the write is committed.",
outputs: [],
trans: [
{ to: "WaitWriteComplete", cond: "mem_dataready_inv = 0" },
{ to: "Done", cond: "mem_dataready_inv = 1" },
],
},
Done: {
label: ["Done"],
full: "Done",
enc: "101",
color: "var(--m-done)",
grad: "url(#g-m-done)",
pos: { x: 380, y: 470 },
r: 60,
about: "One-cycle pulse telling the processor FSM we're finished. memRegEn=1 clocks the memory-read word into the memOut register so the instruction word / loaded data is stable on the next edge.",
outputs: [
["processor_mem_done", "1"],
["memRegEn", "1 — latch read data"],
],
trans: [
{ to: "Idle", cond: "(unconditional)" },
],
},
};
/* edges: explicit list with optional curve hints for pretty routing */
const MAIN_EDGES = [
{ from:"FetchNextInstruction", to:"FetchNextInstruction", label:"¬done", loop:"top" },
{ from:"FetchNextInstruction", to:"LoadInstructionRegister", label:"done" },
{ from:"LoadInstructionRegister", to:"ExecuteInstruction", label:"" },
{ from:"ExecuteInstruction", to:"ExecuteInstruction", label:"mem op ∧ ¬done", loop:"top" },
{ from:"ExecuteInstruction", to:"WriteRegisters", label:"ALU / LW / Pop / LI / LUI / Rand", curve: 0.4 },
{ from:"ExecuteInstruction", to:"LoadProgramCounter", label:"Push / SW / Skip / Jump / JR", curve: -0.3 },
{ from:"WriteRegisters", to:"LoadProgramCounter", label:"", curve: 0.2 },
{ from:"LoadProgramCounter", to:"FetchNextInstruction", label:"default", curve: -0.55 },
{ from:"LoadProgramCounter", to:"LoadStackPointer", label:"Push/Pop", curve: 0.2 },
{ from:"LoadProgramCounter", to:"WriteReturnAddress", label:"Jump/JR", curve: -0.2 },
{ from:"LoadStackPointer", to:"FetchNextInstruction", label:"", curve: -0.5 },
{ from:"WriteReturnAddress", to:"FetchNextInstruction", label:"", curve: -0.65 },
];
const MEM_EDGES = [
{ from:"Idle", to:"Idle", label:"¬ready", loop:"left" },
{ from:"Idle", to:"WaitDataReadyHigh", label:"ready" },
{ from:"WaitDataReadyHigh", to:"WaitDataReadyHigh", label:"dr_inv=0", loop:"top" },
{ from:"WaitDataReadyHigh", to:"SetAddrLines", label:"dr_inv=1" },
{ from:"SetAddrLines", to:"WaitMemReadyLow", label:"" },
{ from:"WaitMemReadyLow", to:"WaitMemReadyLow", label:"dr_inv=1", loop:"right" },
{ from:"WaitMemReadyLow", to:"Done", label:"dr_inv=0 ∧ read", curve: -0.4 },
{ from:"WaitMemReadyLow", to:"WaitWriteComplete", label:"dr_inv=0 ∧ write", curve: 0.2 },
{ from:"WaitWriteComplete", to:"WaitWriteComplete", label:"dr_inv=0", loop:"bottom" },
{ from:"WaitWriteComplete", to:"Done", label:"dr_inv=1" },
{ from:"Done", to:"Idle", label:"", curve: -0.3 },
];
/* ============================================================
* Renderer
* ============================================================ */
function renderFSM(svgId, nodesGroupId, edgesGroupId, states, edges, opts={}) {
const svg = document.getElementById(svgId);
const edgesG = document.getElementById(edgesGroupId);
const nodesG = document.getElementById(nodesGroupId);
const SVGNS = "http://www.w3.org/2000/svg";
const arrowId = opts.arrow || "arrow";
const arrowActiveId = opts.arrowActive || "arrow-active";
// build edge paths first
const edgeEls = {};
edges.forEach(e => {
const a = states[e.from], b = states[e.to];
const g = document.createElementNS(SVGNS, "g");
g.classList.add("edge");
g.setAttribute("data-from", e.from);
g.setAttribute("data-to", e.to);
const path = document.createElementNS(SVGNS, "path");
path.classList.add("line");
path.setAttribute("marker-end", `url(#${arrowId})`);
let d, labelPos;
if (e.from === e.to) {
// self-loop
const dir = e.loop || "top";
const r = a.r;
let cx = a.pos.x, cy = a.pos.y;
let dx1, dy1, dx2, dy2, lx, ly;
if (dir === "top") {
dx1 = cx - 20; dy1 = cy - r;
dx2 = cx + 20; dy2 = cy - r;
const ctrlY = cy - r - 55;
d = `M ${dx1} ${dy1} C ${cx-60} ${ctrlY}, ${cx+60} ${ctrlY}, ${dx2} ${dy2}`;
lx = cx; ly = cy - r - 42;
} else if (dir === "bottom") {
dx1 = cx - 20; dy1 = cy + r;
dx2 = cx + 20; dy2 = cy + r;
const ctrlY = cy + r + 55;
d = `M ${dx1} ${dy1} C ${cx-60} ${ctrlY}, ${cx+60} ${ctrlY}, ${dx2} ${dy2}`;
lx = cx; ly = cy + r + 48;
} else if (dir === "left") {
dx1 = cx - r; dy1 = cy - 20;
dx2 = cx - r; dy2 = cy + 20;
const ctrlX = cx - r - 55;
d = `M ${dx1} ${dy1} C ${ctrlX} ${cy-60}, ${ctrlX} ${cy+60}, ${dx2} ${dy2}`;
lx = cx - r - 66; ly = cy + 4;
} else {
dx1 = cx + r; dy1 = cy - 20;
dx2 = cx + r; dy2 = cy + 20;
const ctrlX = cx + r + 55;
d = `M ${dx1} ${dy1} C ${ctrlX} ${cy-60}, ${ctrlX} ${cy+60}, ${dx2} ${dy2}`;
lx = cx + r + 66; ly = cy + 4;
}
labelPos = { x: lx, y: ly, anchor: "middle" };
} else {
const ax = a.pos.x, ay = a.pos.y, bx = b.pos.x, by = b.pos.y;
const dx = bx - ax, dy = by - ay;
const L = Math.sqrt(dx*dx + dy*dy);
const ux = dx/L, uy = dy/L;
// shrink endpoints to circle borders
const sx = ax + ux*a.r, sy = ay + uy*a.r;
const ex = bx - ux*b.r, ey = by - uy*b.r;
const curve = e.curve || 0;
// perpendicular offset for control point
const px = -uy, py = ux;
const mx = (sx+ex)/2, my = (sy+ey)/2;
const cx = mx + px * L * curve;
const cy = my + py * L * curve;
d = `M ${sx} ${sy} Q ${cx} ${cy}, ${ex} ${ey}`;
// label at the control-ish point
const lx = mx + px * L * curve * 0.9;
const ly = my + py * L * curve * 0.9;
labelPos = { x: lx, y: ly, anchor: "middle" };
}
path.setAttribute("d", d);
path.setAttribute("id", `edge-${svgId}-${e.from}-${e.to}`);
g.appendChild(path);
if (e.label) {
// Label background
const text = document.createElementNS(SVGNS, "text");
text.setAttribute("x", labelPos.x);
text.setAttribute("y", labelPos.y);
text.setAttribute("text-anchor", labelPos.anchor);
text.setAttribute("dominant-baseline", "middle");
text.textContent = e.label;
// put a subtle dark background
const bg = document.createElementNS(SVGNS, "rect");
const approxW = e.label.length * 6 + 10;
bg.setAttribute("x", labelPos.x - approxW/2);
bg.setAttribute("y", labelPos.y - 8);
bg.setAttribute("width", approxW);
bg.setAttribute("height", 16);
bg.setAttribute("rx", 4);
bg.setAttribute("fill", "rgba(10,12,30,.75)");
bg.setAttribute("stroke", "rgba(255,255,255,.08)");
g.appendChild(bg);
g.appendChild(text);
}
g.addEventListener("click", () => {
highlightEdge(svgId, e);
});
edgesG.appendChild(g);
edgeEls[`${e.from}→${e.to}`] = { g, path, edge: e };
});
// draw nodes on top
Object.entries(states).forEach(([key, s]) => {
const g = document.createElementNS(SVGNS, "g");
g.classList.add("node");
g.setAttribute("data-state", key);
g.style.color = s.color;
// glow halo
const halo = document.createElementNS(SVGNS, "circle");
halo.classList.add("halo");
halo.setAttribute("cx", s.pos.x);
halo.setAttribute("cy", s.pos.y);
halo.setAttribute("r", s.r + 10);
halo.setAttribute("fill", s.grad);
g.appendChild(halo);
const shell = document.createElementNS(SVGNS, "circle");
shell.classList.add("shell");
shell.setAttribute("cx", s.pos.x);
shell.setAttribute("cy", s.pos.y);
shell.setAttribute("r", s.r);
shell.setAttribute("fill", s.grad);
shell.setAttribute("stroke", "rgba(255,255,255,.5)");
shell.setAttribute("stroke-width", "1.2");
g.appendChild(shell);
const lines = Array.isArray(s.label) ? s.label : [s.label];
const fontSize = lines.length > 1 ? 13 : 15;
const lineH = fontSize + 1;
// stack lines vertically, centered; leave room for encoding below
const totalH = lines.length * lineH;
const topY = s.pos.y - totalH/2 - 4;
lines.forEach((ln, i) => {
const t = document.createElementNS(SVGNS, "text");
t.setAttribute("x", s.pos.x);
t.setAttribute("y", topY + i*lineH + lineH/2);
t.setAttribute("font-size", fontSize);
t.setAttribute("dominant-baseline", "middle");
t.textContent = ln;
g.appendChild(t);
});
const enc = document.createElementNS(SVGNS, "text");
enc.classList.add("enc");
enc.setAttribute("x", s.pos.x);
enc.setAttribute("y", topY + totalH + 8);
enc.setAttribute("dominant-baseline", "middle");
enc.textContent = s.enc;
g.appendChild(enc);
g.addEventListener("click", () => selectState(svgId, key, states));
nodesG.appendChild(g);
});
return { edgeEls };
}
/* ============================================================
* Interaction — sidebar
* ============================================================ */
function selectState(svgId, key, states) {
const svg = document.getElementById(svgId);
svg.querySelectorAll(".node.selected").forEach(n => n.classList.remove("selected"));
const target = svg.querySelector(`.node[data-state="${key}"]`);
if (target) target.classList.add("selected");
const s = states[key];
document.getElementById("side-title").textContent = s.full;
document.getElementById("side-tags").innerHTML =
`<span class="pill" style="color:${getComputedStyle(target).color}"><span class="dot"></span>${svgId === "svg-main" ? "Main FSM" : "Memory FSM"}</span>
<span class="tag">encoding ${s.enc}</span>`;
document.getElementById("side-about").textContent = s.about;
const outs = document.getElementById("side-outputs");
if (s.outputs && s.outputs.length) {
outs.innerHTML = s.outputs.map(o => `<li><code>${o[0]}</code> ← ${o[1]}</li>`).join("");
} else {
outs.innerHTML = `<li class="empty">(no side-effect outputs asserted)</li>`;
}
const trs = document.getElementById("side-trans");
trs.innerHTML = s.trans.map(t =>
`<li>→ <code>${t.to}</code><br><span style="color:var(--ink-dim);font-size:12px">${t.cond}${t.note ? " — " + t.note : ""}</span></li>`
).join("");
document.getElementById("side-extras").innerHTML = "";
// highlight outgoing edges subtly
svg.querySelectorAll(".edge").forEach(g => g.classList.remove("highlighted"));
svg.querySelectorAll(`.edge[data-from="${key}"]`).forEach(g => g.classList.add("highlighted"));
}
function highlightEdge(svgId, e) {
const svg = document.getElementById(svgId);
svg.querySelectorAll(".edge").forEach(g => g.classList.remove("highlighted"));
svg.querySelectorAll(`.edge[data-from="${e.from}"][data-to="${e.to}"]`).forEach(g => g.classList.add("highlighted"));
document.getElementById("side-title").textContent = `${e.from} → ${e.to}`;
document.getElementById("side-tags").innerHTML =
`<span class="tag">transition</span><span class="tag">${svgId === "svg-main" ? "Main FSM" : "Memory FSM"}</span>`;
document.getElementById("side-about").textContent = "Transition taken when the guard condition below evaluates true on the falling edge of sysclk1.";
document.getElementById("side-outputs").innerHTML = `<li class="empty">(transitions are guarded; outputs belong to states)</li>`;
document.getElementById("side-trans").innerHTML = `<li><code>${e.label || "(unconditional)"}</code></li>`;
document.getElementById("side-extras").innerHTML = "";
}
/* ============================================================
* Renderers — kick off
* ============================================================ */
const mainRender = renderFSM("svg-main", "nodes-main", "edges-main", MAIN_STATES, MAIN_EDGES, { arrow:"arrow", arrowActive:"arrow-active" });
const memRender = renderFSM("svg-mem", "nodes-mem", "edges-mem", MEM_STATES, MEM_EDGES, { arrow:"arrow2", arrowActive:"arrow2-active" });
/* ============================================================
* Tabs
* ============================================================ */
document.querySelectorAll(".tab").forEach(t => {
t.addEventListener("click", () => {
document.querySelectorAll(".tab").forEach(x => x.classList.remove("active"));
t.classList.add("active");
document.querySelectorAll(".fsm-panel").forEach(p => p.classList.remove("active"));
document.getElementById("panel-" + t.dataset.target).classList.add("active");
});
});
/* ============================================================
* Simulation — Main FSM
* ============================================================ */
function mainSequence(instr) {
// Always start with: Fetch (stall) → Fetch (done) → LoadIR → Execute
const seq = [
{ state: "FetchNextInstruction", note: "issue read @ PC" },
{ state: "FetchNextInstruction", note: "memory busy — stall" },
{ state: "LoadInstructionRegister", note: "IR ← mem" },
{ state: "ExecuteInstruction", note: "decode + operate" },
];
const memOps = ["LoadWord","StoreWord","Push","Pop"];
if (memOps.includes(instr)) {
seq.push({ state: "ExecuteInstruction", note: "waiting on memory…" });
}
const skipsPcOnly = ["Push","StoreWord","SkipOnEqual","SkipOnNotEqual","SkipOnLessThan","Jump","JumpRegister"];
if (skipsPcOnly.includes(instr)) {
seq.push({ state: "LoadProgramCounter", note: "update PC" });
} else {
seq.push({ state: "WriteRegisters", note: "rd ← result" });
seq.push({ state: "LoadProgramCounter", note: "PC ← PC+1" });
}
if (instr === "Push" || instr === "Pop") {
seq.push({ state: "LoadStackPointer", note: "SP ← SP ± 1" });
} else if (instr === "Jump" || instr === "JumpRegister") {
seq.push({ state: "WriteReturnAddress", note: "r7 ← PC_orig + 1" });
}
seq.push({ state: "FetchNextInstruction", note: "next cycle begins" });
return seq;
}
let mainPlaying = false;
async function playMain() {
if (mainPlaying) return;
mainPlaying = true;
const instr = document.getElementById("instr-select").value;
const seq = mainSequence(instr);
const svg = document.getElementById("svg-main");
const status = document.getElementById("status-main");
// reset classes
svg.querySelectorAll(".node").forEach(n => n.classList.remove("active", "selected"));
svg.querySelectorAll(".edge").forEach(g => g.classList.remove("active"));
for (let i = 0; i < seq.length; i++) {
const cur = seq[i];
svg.querySelectorAll(".node").forEach(n => n.classList.remove("active"));
const target = svg.querySelector(`.node[data-state="${cur.state}"]`);
if (target) target.classList.add("active");
selectState("svg-main", cur.state, MAIN_STATES);
status.innerHTML = `<b>${instr}</b> — step ${i+1}/${seq.length}: <b>${MAIN_STATES[cur.state].full}</b> &middot; ${cur.note}`;
if (i < seq.length - 1) {
const next = seq[i+1].state;
const edgeEl = document.querySelector(`#svg-main .edge[data-from="${cur.state}"][data-to="${next}"]`);
svg.querySelectorAll(".edge.active").forEach(g => g.classList.remove("active"));
if (edgeEl) {
edgeEl.classList.add("active");
await animateDot(edgeEl.querySelector("path"), 650);
} else {
await sleep(650);
}
} else {
await sleep(900);
}
}
status.innerHTML = `<b>${instr}</b> — cycle complete. ready for another.`;
svg.querySelectorAll(".edge.active").forEach(g => g.classList.remove("active"));
mainPlaying = false;
}
function resetMain() {
const svg = document.getElementById("svg-main");
svg.querySelectorAll(".node").forEach(n => n.classList.remove("active", "selected"));
svg.querySelectorAll(".edge").forEach(g => g.classList.remove("active","highlighted"));
document.getElementById("status-main").textContent = "idle — pick an instruction and hit Simulate";
document.getElementById("side-title").textContent = "Welcome";
document.getElementById("side-tags").innerHTML = "";
document.getElementById("side-about").textContent =
"Click any state bubble to see what it does. Click an edge for its condition. Hit Simulate to watch an instruction flow.";
document.getElementById("side-outputs").innerHTML = `<li class="empty">Select a state</li>`;
document.getElementById("side-trans").innerHTML = `<li class="empty">Select a state</li>`;
}
/* ============================================================
* Simulation — Memory FSM
* ============================================================ */
function memSequence(mode) {
// Read: Idle→WDRH→SetAddr→WMRL→Done→Idle
// Write: Idle→WDRH→SetAddr→WMRL→WWC→Done→Idle
const seq = [
{ state: "Idle", note: "waiting for processor_mem_ready" },
{ state: "WaitDataReadyHigh", note: "wait for mem_dataready_inv↑" },
{ state: "SetAddrLines", note: "assert mem_addressready" },
{ state: "WaitMemReadyLow", note: "wait for mem_dataready_inv↓" },
];
if (mode === "write") {
seq.push({ state: "WaitWriteComplete", note: "wait for write to commit" });
}
seq.push({ state: "Done", note: "pulse processor_mem_done" });
seq.push({ state: "Idle", note: "back to idle" });
return seq;
}
let memPlaying = false;
async function playMem() {
if (memPlaying) return;
memPlaying = true;
const mode = document.getElementById("mem-mode").value;
const seq = memSequence(mode);
const svg = document.getElementById("svg-mem");
const status = document.getElementById("status-mem");
svg.querySelectorAll(".node").forEach(n => n.classList.remove("active", "selected"));
svg.querySelectorAll(".edge").forEach(g => g.classList.remove("active"));
for (let i = 0; i < seq.length; i++) {
const cur = seq[i];
svg.querySelectorAll(".node").forEach(n => n.classList.remove("active"));
const target = svg.querySelector(`.node[data-state="${cur.state}"]`);
if (target) target.classList.add("active");
selectState("svg-mem", cur.state, MEM_STATES);
status.innerHTML = `<b>${mode === "write" ? "Write" : "Read"} cycle</b> — step ${i+1}/${seq.length}: <b>${MEM_STATES[cur.state].full}</b> &middot; ${cur.note}`;
if (i < seq.length - 1) {
const next = seq[i+1].state;
const edgeEl = document.querySelector(`#svg-mem .edge[data-from="${cur.state}"][data-to="${next}"]`);
svg.querySelectorAll(".edge.active").forEach(g => g.classList.remove("active"));
if (edgeEl) {
edgeEl.classList.add("active");
await animateDot(edgeEl.querySelector("path"), 650);
} else {
await sleep(650);
}
} else {
await sleep(900);
}
}
status.innerHTML = `<b>${mode === "write" ? "Write" : "Read"} cycle</b> complete.`;
svg.querySelectorAll(".edge.active").forEach(g => g.classList.remove("active"));
memPlaying = false;
}
function resetMem() {
const svg = document.getElementById("svg-mem");
svg.querySelectorAll(".node").forEach(n => n.classList.remove("active", "selected"));
svg.querySelectorAll(".edge").forEach(g => g.classList.remove("active","highlighted"));
document.getElementById("status-mem").textContent = "idle — pick a mode and hit Simulate";
}
/* ============================================================
* animation helpers
* ============================================================ */
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
function animateDot(pathEl, duration) {
return new Promise(resolve => {
if (!pathEl) return resolve();
const SVGNS = "http://www.w3.org/2000/svg";
const parent = pathEl.parentNode;
const dot = document.createElementNS(SVGNS, "circle");
dot.setAttribute("r", "5");
dot.classList.add("dot-active");
parent.appendChild(dot);
const len = pathEl.getTotalLength();
const start = performance.now();
function step(now) {
const t = Math.min(1, (now - start) / duration);
const p = pathEl.getPointAtLength(t * len);
dot.setAttribute("cx", p.x);
dot.setAttribute("cy", p.y);
if (t < 1) requestAnimationFrame(step);
else { parent.removeChild(dot); resolve(); }
}
requestAnimationFrame(step);
});
}
/* ============================================================
* wiring
* ============================================================ */
document.getElementById("play-main").addEventListener("click", playMain);
document.getElementById("reset-main").addEventListener("click", resetMain);
document.getElementById("play-mem").addEventListener("click", playMem);
document.getElementById("reset-mem").addEventListener("click", resetMem);
// preselect Fetch state for context on load
selectState("svg-main", "FetchNextInstruction", MAIN_STATES);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment