Verilog → draw.io: Architecture / Structure / State Machine / Control Structure
Four perspectives, choose based on the question: What is the design idea? (For explaining to people, for papers/reports) → arch2drawio.py (default); What does the gate level look like? (Unit-by-unit datapath) → rtl2drawio.py; How does it step? (Single FSM) → fsm2drawio.py fsm; Who commands whom? (Overall control structure) → fsm2drawio.py ctrl. For control-dominated modules (FSM, decoder, arbiter), block diagrams become messy; use the latter two instead.
Dependencies: PyYAML; export to PNG/SVG requires drawio (lookup order: $DRAWIO_BIN → PATH → macOS app → ~/.local/opt/drawio; on Linux without $DISPLAY, xvfb-run is used automatically; on servers without root, use tools/install-drawio-linux.sh from the repository). Without an exporter, all commands still produce .drawio files, only missing PNG, with a single stderr hint. Block and control diagrams additionally require yosys, sv2v (for SystemVerilog), dot (Graphviz), and an installed drawio-skill (default ~/.claude/skills/drawio-skill, overridable by env var DRAWIO_SKILL_DIR; only uses its autolayout.py, does not modify it). All three generating scripts accept --png / --svg / --scale N (PNG default scale is 3, approximately 1 model unit = 3 px; $DRAWIO_SCALE can change the default). For separate export: python3 $S/scripts/drawio_export.py X.drawio --png --svg. Prefer SVG for Markdown/Obsidian (no blur). $S refers to this skill's directory. examples/ contains two comparison samples: wrr_arbiter.v (single-stage + state register) and load_pipe.v (multi-stage pipeline with replay).
0. Architecture Block Diagram (arch2drawio.py) – Default
Input is code (or a paper/patent/user description). Yosys netlists are too detailed for architecture diagrams, so we do not synthesize here: the script produces a source outline, you (the agent) read the code, extract the core, and write a diagram description, which the script then deterministically lays out. The user does not need to write any description file; only show the diagram to the user, unless the user wants to modify the description themselves.
python3 $S/scripts/arch2drawio.py outline src/*.sv --top gpu [--module core] # Source outline (no synthesis)
python3 $S/scripts/arch2drawio.py render fig.yaml -o diagrams/arch_core.drawio --png
outline lists: the hierarchy tree (generate replicates ×N), ports, storage arrays (depth × width, packed struct fields, same-depth arrays hint "possibly same table"), sequential registers named by stage (s1_ / stage2_ / if_id_ …), state machines, suspected feedback signals (replay/kill/flush/stall/forward/refill…), inter-instance data flow (via shared signals, with clk/reset/loop variables removed). This step is skipped when input is a paper/patent/description.
Drawing Rules (follow when writing descriptions)
- Draw only the core: First describe the core design idea in one or two sentences; retain only components that embody it. Omit glue logic, clk/reset, debug ports, and scattered control. Generally 8–20 boxes; if more, split into two diagrams (one top-level + one for the key unit).
- Highlight emphases: Mark 1–3 key design points with
hl: true(contrast color: dark background, white text) orbold: true(bold). Key path lines can also use| hl. Usegroups+hl: true(light background box) to emphasize a region. - Do not number every component: Only number core components with
num: Nwhen text references are needed, and include explanations incallouts. - Keep unit boxes concise: Box content is only the name (≤ 2 lines). Meaning is conveyed by shape: use
adder/mult/cmp/opcircular symbols oralufor operations;muxfor selection (select inputx.selenters from bottom);tablefor storage;fifofor queues;regfor registers;and/or/xor/notfor gates. - Write individual entry fields in SRAM tables: For
table, write each field's meaning infields(e.g.[V, Tag (20b), Data (32b)]). Usestack: 4for multi-way set-associative (stacked + ×4). Merge parallel arrays of the same depth and address into one table, with each array as a field. - Pipelines:
stagesdefines pipeline stages (id+ functionsub, e.g.,{id: S1, sub: hit check}). The script draws dashed stage dividers and stage names. Inter-stage registers/FIFOs usebetween: [S0, S1], and the script places them on the stage boundary. Feedback (target is an earlier stage) automatically loops back along the bottom corridor, or use| fb,| fb via=top(loop from above). Multiple pipelines uselanes, sharing column positions by stage; corresponding stages across lanes are automatically aligned. - For non-pipelined block-level diagrams (top-level, SoC), omit
stages. Usestack: Nfor replicated units, andgroupsto enclose per-thread/per-core resources.
Description Format (YAML)
title: dcache_pipe: Direct-mapped D-Cache Lookup Pipeline
stages: [{id: S0, sub: Select / Read Array}, {id: S1, sub: Hit Check}, {id: S2, sub: Miss / Replay}]
lanes: [{id: ld, name: Load Pipeline}, {id: st, name: Store Pipeline}] # Optional, for multiple pipelines
nodes:
- {id: arr, type: table, label: "Cache Array · 64 Entries", fields: [V, "Tag (20b)", "Data (32b)"], stage: S0}
- {id: r1, type: reg, label: addr, between: [S0, S1]} # Falls on the S0|S1 boundary
- {id: tcmp, type: cmp, label: Tag Compare, stage: S1, hl: true} # Circle "=", description below
- {id: rmux, type: mux, stage: S0, ports: 2}
edges:
- "rmux => arr : idx | w=6" # -> data => bus (thick) ~> control (dashed); w=N bit width
- "rpl -> rmux.sel : replay | fb hl" # .sel/.en enters from bottom, .top from top; fb feedback, hl highlight, via=top, pos=src|dst
groups: [{label: "×4 per thread", members: [rf, alu, lsu], hl: true}]
legend: {reg: Pipeline register, fb: Feedback, ctrl: Control, hl: Key design} # Usable keys: data bus ctrl fb hl hl_edge hl_region reg
callouts: {1: "Number only when reference is needed"}
Node types: block reg fifo table(fields, rows, stack) array(dims, cell) mux(ports) adder mult cmp/op(sym) alu and or xor nand nor xnor not cloud mem xbar in out text dot.
Common attributes: stage / lane / between, hl bold num stack, fine-tuning with row / col (row/column within a stage, can be negative), dx / dy, w / h.
Misc: array supports cell_labels (per-cell labels, e.g., T0..T3) and cell_h (rectangular cells for thread channels); groups supports stack: N (stack the whole region, e.g., "Core ×2" with internals exposed) and label_align: right; leaving lanes' name empty = only splits, no label; edges with | both draw bidirectional arrows (request/response); a feedback between two units in the same column is drawn as a tight vertical short line (e.g., Dispatcher ⇅ Core).
Procedure
outline+ read key source code (or read paper/patent), write down the core idea → decide which layer to draw and what are the highlights.- Write the description to a scratchpad directory. Give each node a
stage(if stages exist). Automatically break loops: nodes whose names resemble controllers (sched / ctrl / fsm / arbiter / dispatch / scheduler / controller …, followed by high-fanout control wires) start first; their outputs go right, and edges returning to them become feedback. If heuristic is wrong, explicitly mark that edge with| fb. Optional: Composition reference (gpt-image-bridge, requires user confirmation per image) — The grid layout ensures correct connections but does not "compose." When modules ≥ 10 or there are clear partitions (control plane / data plane / memory hierarchy), you can first output a draw.io baseline, then write the module list and connections derived from code into the prompt (paper illustration style, white background, single accent color, three-section layout, minimal text, short English labels). Use gpt-image-bridge to generate 1 sketch and Read it. Only borrow layout and visual hierarchy (partitioning, what goes above what, arrangement, whitespace, key positions). Convert tolane/row/col/groups. Modules, connections, and names must all be based on code — common sketch mistakes: connecting to the wrong source, missing/extra modules, dark backgrounds. Before generation, list the images (purpose, path, size, prompt summary) per this skill and wait for explicit user consent. Save sketches asdiagrams/sketch_*.png, with a comment at the top of the description file referencing it. render --png→validate.py(error must be 0; overlap warnings for stage divider lines crossing stage headers/registers are by design) → Read PNG for visual inspection. Fine-tune withrow/colfor at most two rounds (common: make the main datapath occupy row 0 for a straight line; put bypass/control on the next row).- Deliver
.drawio+.png. Leave the description file in the scratchpad; only give it to the user if they want to modify it themselves.
Examples: tiny-gpu top-level ~/hardware-project/tiny-gpu/diagrams/arch_gpu.* (composition guided by sketch sketch_gpu_top.png, compared to unsketched baseline arch_gpu_baseline.png); examples/dcache_pipe.v → dcache_pipe.arch.yaml (description written from code) → .png; arch_dual_issue.yaml (dual pipeline alignment + forwarding + numbered annotations), arch_mac.yaml (circuit level: multiply / add symbols, accumulate feedback, MUX select input). Block diagram of tiny-gpu core: ~/hardware-project/tiny-gpu/diagrams/arch_core.*.
1. Structural Block Diagram (rtl2drawio.py, Gate-Level Details)
python3 $S/scripts/rtl2drawio.py src/*.sv --top gpu --list # List modules and their unit composition
python3 $S/scripts/rtl2drawio.py src/*.sv --top gpu --module alu -o diagrams/alu.drawio --png
Procedure: sv2v (automatic for .sv files) → Yosys proc; opt; opt_dff; wreduce; memory -nomap → JSON netlist → unit-level pipeline analysis → simplification → column-constrained layout. After generation, run python3 ~/.claude/skills/drawio-skill/scripts/validate.py X.drawio (error must be 0; through-vertex / overlap warnings for stage dividers crossing registers on boundaries are by design), then Read PNG for visual inspection.
Layout Rules (--layout pipeline; default auto: for hierarchical modules dominated by submodule instances, use free layout; otherwise pipeline layout)
- Inputs on the left, outputs on the right, logic arranged left to right by combinational depth. Multi-stage pipelines go S0 → Sn left to right, with dashed dividers and stage labels in the column header.
- Two register types:
- Pipeline registers (narrow vertical bars) placed on stage boundaries. A row of registers only connected to output ports at the end is an output register (not a stage, no divider drawn).
- State registers (FSM / counter / pointers: outputs only go through combinational logic back to their own input) drawn inside the stage, with outputs as feedback.
- When register names include stage numbers (e.g.,
s2_x,x_s2,stage2_x,if_id_/id_ex_/ex_mem_/mem_wb_, at least two types) the name takes precedence — valid bits with kill are still pipeline registers even with combinational loops. For designs not named by stage, inference is structural; input arrival times are unknown, so stage numbers may be too early.
- Stage number inference: Start from registers with stage numbers. A unit's stage = max stage of its register operands. Logic driven only by input ports is assigned to the stage of the register that captures it (ALAP); corresponding input ports are also placed at the beginning of that stage (e.g., TLB response placed in S1).
- Feedback: Signals that enter the input cone of a stage-numbered register but can only come from a later stage (replay→S0, kill→S1), along with state register loops, are drawn as red lines looping back along the outer bottom edge, with wider-spanning lines on the outside. When there are more than 3 feedbacks between the same pair of stages, draw one thick line representing the bundle (label it
Feedback ×N: …). - Bundling: When there are more than 4 pipeline registers on the same boundary, merge them into one register group (vertical bar labeled
S1→S2 Registers ×Nand total bit width). - Simplification: Merge slices of same-named registers. Combine MUX chains from nested if/case into a single
N:1MUX. Inline small decode logic that only drives one MUX select/enable, drawing it as a select/enable line. Draw up to 3 gates as AND/OR/NOT/XOR gate symbols; larger gates fold into a "combinational logic" cloud (which is cut at arithmetic units to avoid false loops). Fold generate loop-expanded units of the same origin (same source location) andthreads[i].xinstances into "×N" (with shadow). All merging is done only within the same stage. clk/reset are hidden by default. - Wires: solid = data (thick = multi-bit bus,
/N= bit width); purple dashed = MUX select; blue dotted = enable; red = feedback.
Parameters
| Parameter | Function |
|---|---|
--list | List modules and unit composition, then exit |
--module M | Draw a specific module (parameterized variant draws the first; will prompt) |
--layout auto|pipeline|free | Default auto: if there are submodule instances and combinational logic nodes are fewer than instances (top-level glue), use free (bidirectional inter-block communication is not pipeline feedback); otherwise pipeline |
--show-clock | Draw clk/reset |
--no-fold | Do not fold generate instances / same-origin units |
--no-collapse | Do not collapse control/combinational logic |
--sv2v auto|always|never | SV front-end (syntaxes like unpacked array ports require sv2v) |
--keep-json PATH | Keep the Yosys JSON netlist |
2. State Machine Diagram (fsm2drawio.py fsm)
python3 $S/scripts/fsm2drawio.py list src/*.sv
python3 $S/scripts/fsm2drawio.py fsm src/*.sv [--module M] -d diagrams --png [--no-actions]
- Direct source code parsing, transition conditions preserve original expressions. Recognizes assignments to
S(or the next-state variableNviaS <= N) insidecase (S)branches. Uses localparam names for states. Constants comparing other FSM state variables are translated to the other's state name (e.g.,core_state == 3'b001→core_state == FETCH). - Transition conditions = the if/case/for directly enclosing the state assignment;
else ifonly writes its own condition; conditions insideforare written as∃j: …; an outerfor iwrapping the entire case indicates replication by i. Guards (enable, read/write branches) outside the case go into a comment box. Local boolean variables set to 1 in a loop are defined in the comment box. - Drawing method: Each state is an equal-sized circle (name wraps at
_, below in small text are encoding and ⟳), final state double circle, black dot = reset entry. When the main flow covers most states, arrange in a U-shaped loop (top row left→right, turn down, bottom row right→left). For branch types, arrange evenly in columns by step distance from the initial state. Adjacent states are directly connected. Transitions skipping other states arch over from above/below, label at the arch top. A↔B round trips each curve to one side. Explicit self-loops are drawn above the circle. Column spacing auto-adjusts to the longest label. Assignments on transition (after/) show at most 2. - Not recognized: controls without
case(state)structure (counter-based, purely combinational decode) — these will not appear inlist, which is expected.
3. Overall Control Structure (fsm2drawio.py ctrl)
python3 $S/scripts/fsm2drawio.py ctrl src/*.sv --top gpu --module core -o diagrams/ctrl_core.drawio --png
- Draws submodule instances within module L (generate instances folded ×N), retaining only control signals: Orange thick lines = FSM state broadcast; Blue lines = valid/ready handshake; Purple dashed = start/done/enable/select/reset, etc. Data buses are omitted. Instance boxes containing FSMs list the state sequence; sub-hierarchies with FSMs are marked "Contains FSM."
- Control signals are traced through at most 3 layers of basic units (comparators, gates, registers) to find the source. Classification relies on signal names; non-standard naming may result in missing/extra signals.
- Suggested top-down order: Start with top-level (who starts whom, who handshakes with memory) → next level (how the main controller FSM schedules) → the state machine diagram of key FSMs.
Limitations
- Layout is automatically done by Graphviz with column constraints; crossings cannot be fully eliminated. Fine-tune by dragging in draw.io (all objects are native and editable).
- Block diagrams for large control-oriented modules (hundreds of units) will still be messy. Use the state machine / control structure diagram methods instead, or draw each submodule separately.
- The circles and arcs in state machine diagrams are drawn directly based on coordinates. Draging a state will not automatically follow the arc's control points; you will need to adjust them manually.