Created
April 15, 2026 01:16
-
-
Save stbenjam/46fd716e5dc04e2d26fe5a9ecebe6912 to your computer and use it in GitHub Desktop.
SKB CPU — interactive FSM navigator (Main + Memory FSMs from skb_cpu.vhd)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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 & 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> · ${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> · ${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