RouteRTL FPGA-Friendly RTL Rules
RouteRTL enforces "write HDL that maps well to silicon" as a layered system, not a single linter. Each layer has a distinct job; together they take a rule from human rationale down to measured silicon.
The enforcement stack
| Layer | What it is | Where |
|---|---|---|
| 1. Knowledge / judgment | The fpga-rtl-design skill — human-readable rationale for every rule (the why: the silent failure or wasted resource), with a pointer to its enforcement and its evidence (Haz.N / H.N). Read before writing. | skill |
| 2. Mechanical lint | rr lint --profile <profile> — the RR-* checkers (regex + AST, per-language). Bundled into profiles; runs in the pre-commit hook and at rr sim coverage-map time. | routertl_core/lint_checkers/, smart_linter.py, lint_profile.py |
| 3. Structural (CDC/RDC) | SV-Gap / Yosys — a manifest.toml generated from ip.yml, run in the background; reports coverage (source counts, clocks/crossings) so a green run is not mistaken for full CDC sign-off. | profile rr-cdc-structural-provisional |
| 4. Formal | rr formal — PSL (VHDL) / SVA property proofs via GHDL→yosys→SymbiYosys (k-induction). | rr formal run --contract |
| 5. Empirical ground truth | OOC synth / impl utilization + timing reports — the ultimate check. The rules predict ("this should be a BRAM"); synth confirms (RAMB count). | rr synth/impl run --ooc |
Design principle — severity is calibrated to false-positive risk. A mechanical rule
that would nag correct code ships as info / review (suppressible, Spyglass-style),
not a hard error. Only rules that detect an unambiguous silicon bug are promoted to
error (build-gating). This is why the same family spans info → error.
Opt-out: a line pragma -- rr-lint-disable-line RR-XXX-NNN suppresses a specific
finding where the author has reviewed and accepts it.
The RR-* catalog
Memory inference — RR-MEM-*
RR-MEM-001 · info (advisory) · FF-matrix anti-pattern. A large registered array that should infer as Block RAM but is coded so it maps to a flip-flop matrix + LUT mux instead. Stays quiet when you follow the inferrable-RAM template: one write port, a synchronous registered read, no asynchronous read, no array-wide reset. Fix: register the read (present the address one cycle early), drop the array-wide reset (use an epoch/generation tag if you need a fast clear). Why info: the FF form is legal and sometimes intended (small arrays); this is a nudge, not a defect.
RR-MEM-002 · warning · >2 runtime read ports on a large array. Structural complement to RR-MEM-001: a Block RAM has at most 2 ports, so a large array read under more than 2 distinct runtime index expressions cannot map to a single BRAM — it replicates into extra BRAMs or LUT-heavy distributed RAM, even when every read is registered. Fix: time-multiplex the reads onto ≤2 ports, or accept the replication deliberately. (Constant-indexed taps do not count — only runtime indices.)
Arithmetic cost — RR-ARITH-*
RR-ARITH-001 · warning · Runtime or non-power-of-2 division.
A / or mod by a runtime value, or by a constant that is not a power of two, infers a
multi-cycle divider or a wide combinational network. Fix: a lookup ROM for a small bounded
operand, a reciprocal-multiply, or constant-fold; power-of-2 divides are free shifts.
RR-ARITH-002 · warning · runtime × runtime multiply. A multiply where both operands are runtime signals consumes a DSP (or a large LUT network) — flagged so it is a deliberate choice, not an accident. Fix: confirm a DSP is intended and sized to pack; if one operand is constant, it folds much cheaper.
Clocking — RR-CLK-*
RR-CLK-001 · error (build-gating) · Combinational clock gating.
A clock net driven by combinational logic (gated <= clk and en; ... rising_edge(gated))
is a hard silicon bug: glitches on the enable become spurious clock edges, and the path
is unconstrained. Promoted to error (RTL-T2.79) so it gates the build. Fix: use a
clock-enable on the FF, or a proper BUFGCE/clock-gating cell. Opt out only with an
explicit -- rr-lint-disable-line RR-CLK-001.
Known limits (RTL-T3.57): catches the direct 1-hop gate; does not yet flag a 2-hop
aliased gate or a procedural if/else clock mux.
Clock-domain crossing — RR-CDC-*
RR-CDC-001 · warning · ASYNC_REG must be the string "TRUE", not VHDL boolean.
Vivado exposes only the string form of ASYNC_REG as a queryable cell property. A
boolean-typed async_reg keeps the placement/MTBF effect but is invisible to
get_cells -hier -filter {ASYNC_REG == "TRUE"} — so the design-wide
set_max_delay -datapath_only CDC idiom silently never reaches those synchroniser FFs,
leaving the crossing unconstrained (a false-negative WNS that only surfaces at P&R). Fix:
attribute async_reg of sync : signal is "TRUE";.
RR-CDC-002 · info (review) · Multi-bit bus through a 2-FF synchroniser. A per-bit 2-FF synchroniser does not keep the bits of a coherent bus together: each bit resolves metastability independently and can settle on a different cycle, so the receiver can sample a transiently wrong multi-bit value. 2-FF sync is safe only for a single bit, a gray-coded pointer (one bit changes per step), or genuinely independent bits. A review item (not pass/fail) — confirm coherency; use a proper handshake / async FIFO / gray code for a real multi-bit crossing.
(Structural CDC/RDC beyond regex is handled by the SV-Gap backend — profile
rr-cdc-structural-provisional — which now also reports coverage: RTL-P2.814.)
RR-AXIS-001: AXI-Stream / AXI4 backpressure needs a skid buffer
live · rr sim lint-axis-skid · severity warning
Rule. When a module exposes an AXI4-Stream (or AXI4 full) interface and needs to
be pipelined / registered across a timing or module boundary, the backpressure
(tready / ready) must be handled with a skid buffer (a 2-deep register slice), not
a combinational ready passthrough and not a naive single register.
Why. AXI tready backpressure is combinational by spec. Two failure modes:
- Combinational
readychain across a pipeline → a longreadypath that kills fmax (thereadyof the last stage ripples back to the first). - Naively registering
ready→ you either drop the beat in flight when the consumer deassertsreadyone cycle late, or you inject a bubble every other cycle (≤50% throughput).
A skid buffer registers both the forward (tvalid/tdata) and backward (tready)
paths while sustaining 1 transfer/cycle (100% throughput) — it "skids" to absorb the
single in-flight beat when the consumer stalls. It is exactly Xilinx axis_register_slice
in behaviour; the minimal correct AXI pipeline register.
Default action for an agent. When emitting or registering an AXI-Stream/AXI4
interface at a boundary, instantiate the canonical skid-buffer / register-slice primitive
(don't hand-roll a bare register on ready). A thin combinational leaf adapter that does
not register the interface is exempt — this rule is about pipelined boundaries.
Mechanically flaggable anti-patterns (the lint check target): a combinational
tready → tvalid/tdata path across a module boundary, plus direct tready chains
with no visible buffer state. Semantic correctness ("backpressure handled right") is
review-level; the clear anti-patterns are pass/fail.
The canonical primitive ships in both languages:
src/units/axis_skid_buffer.vhd and src/units/axis_skid_buffer.sv.
Run the staged-file lint directly with:
rr sim lint-axis-skid src
Running the lint
rr lint --profile esa-vhdl-strict-provisional --src src # ESA style + RR-* FPGA rules
rr lint --profile rr-cdc-vhdl-provisional --src src # the RR-CDC/MEM/ARITH/CLK set
rr lint --profile rr-cdc-structural-provisional --src src # SV-Gap structural CDC/RDC
The FPGA rules also ship inside the broader profiles; rr-cdc-vhdl-provisional maps
each RR-* rule to its severity (the table above). Coding-style families live alongside
them: ESA-V-* (ESA-VHDL audit — naming, magic numbers, _q/_r convention) and
bsg-aligned-* (Basejump STL alignment) — style, not silicon-mapping, so separate
from the RR-* FPGA family.
The through-line
Rationale (skill) → mechanical lint (RR-*, profile-bundled, FP-calibrated) → structural
(SV-Gap) → formal (PSL/SVA) → measured (OOC synth/impl). The layers are complementary:
the structural audit predicts the mapping; synthesis confirms it. On the h264-dec CABAC
decoder this exact chain caught an async-read grid that would have exploded into a
flip-flop matrix (RR-MEM territory), the fix landed as BRAM line-buffers, and the KV260
OOC synth then measured 109 RAMB36E2 and 0 FF-matrix — prediction confirmed.