| name | build-rtl |
| description | Guides RTL frontend design discipline — module boundary rules, synthesizability constraints, observability instrumentation, lint cleanliness as a hard gate, reset strategy, and clock-domain crossing. Activate when the user explicitly invokes /chipdev-method:build-rtl, or asks "RTL 怎么分层", "should I use always_ff or always", "怎么做 lint clean", "reset strategy", "CDC how to handle", or starts writing SystemVerilog for a new module. |
Build RTL
Use this skill when the user is writing SystemVerilog (or another HDL) for
a chip frontend module. Do not autoload.
The point of this skill is to give the user a small set of non-negotiable
disciplines — module boundary rules, synthesizability constraints, lint
cleanliness, observability hooks. These disciplines pay back as the design
scales. Letting any of them slip in Phase 0–1 creates a debt that is
order-of-magnitude more expensive to repay in Phase 4.
How to use this skill in a response
When triggered:
- Identify what the user is about to write — a new module, a refactor, a
wrapper around legacy IP, an instrumentation pass.
- Surface the disciplines that apply to their situation.
- If they're writing the first RTL for a multi-artifact project, push
them to
define-contracts first — interfaces and hierarchy should
come from the same DSL the simulator uses.
- Recommend the lint configuration that fits their toolchain.
- Surface the failure mode most likely to bite them on what they're
building.
Module boundary discipline [abstract]
Five rules. Every rule has a long-term cost when violated.
- One clock domain per module. Modules that internally bridge clock
domains hide the bridge from synthesis tools and from human review.
Always wrap CDC in a dedicated synchronizer module that does only
the bridging.
- Direction is part of the port name or enforced by the DSL.
cmd_in, cmd_out, data_resp_out are unambiguous. cmd is not.
- No combinational paths through module boundaries. Every output is
either a register output or marked explicitly as combinational with a
reason. This protects against unexpected timing closure problems and
makes the simulator interface (
proc_* callbacks) trivially safe.
- Backpressure is bidirectional, decided at port-pair time.
valid + ready for any port pair where the receiver has bounded
capacity. Decide protocol with the contract, not at integration time.
- No hidden state across the boundary. No module remembers what the
previous transaction was unless the transaction itself encodes that.
Hidden state across module boundaries is the prime cause of
integration bugs.
If the project uses an interface DSL (see define-contracts), rules
2, 3, 4 are largely enforced by the generator.
Synthesizability constraints [abstract]
Hard rules. Never violate in synthesizable RTL.
- Sequential logic only in
always_ff with an explicit clock and
optional reset.
- Combinational logic only in
always_comb (which auto-detects
sensitivity).
- No
always @(*) — use always_comb. The latter catches latch
bugs at elaboration.
- No
real, time, realtime in synthesizable code.
- No
$display, $write, $fopen, $fclose in synthesizable
scopes (they're fine inside if (!synthesis) debug blocks if your
tools support it).
- No
wait, force, release outside testbenches.
- No event control inside functions (
@(...), wait(...) inside
function).
- No multi-driven nets. One driver per net, always.
- No initial values in registers intended for synthesis. Reset clears
them.
These should be enforced by lint as a hard gate (see "Lint clean as a hard
gate" below).
Coding style essentials — SystemVerilog 2017 [industry-pattern]
Use
logic instead of wire/reg.
always_ff / always_comb / always_latch.
unique case / priority case when the intent is documented.
- Packed structs for protocol payloads — they cross language boundaries
cleanly to C++.
- Parameterization via
parameter and (for elaboration-time) localparam.
- Hierarchical references only for testbenches and debug.
Avoid
wire reg style mixed declarations.
assign for anything beyond simple wire connections.
- Implicit conversions of vector widths — always be explicit
(
{32'h0, foo}, not foo extended silently).
- Vendor-specific pragmas in core RTL — keep them at integration boundaries.
- Generate blocks that span more than one declarative file — they hide
hierarchy from tools.
- Macros for control flow. Macros for repetition (e.g., per-lane
instantiation) are fine; macros for
if-chains are not.
Naming
- Module name:
PascalCase or snake_case, pick one project-wide.
- Signal name:
snake_case. Direction-suffixed (_in, _out).
- Parameter:
UPPER_SNAKE.
- Localparam:
UpperCamel or UPPER_SNAKE, pick one.
These are conventions. The point of conventions is that they are uniform.
Less important which you choose, more important that you choose one.
Observability instrumentation [abstract]
RTL must be observable to the difftest infrastructure (see
align-and-difftest). Plan instrumentation at module write time, not
afterwards.
State probes
For every architectural state element this module owns (a register, an
output of a state machine that implements an architectural commit),
expose a probe interface:
// At commit time, push the architectural value out to the difftest probe.
`ifdef DIFFTEST
always_ff @(posedge clk) begin
if (commit_valid) begin
$diff_commit_register(reg_id, reg_data);
end
end
`endif
Wrap probes in ifdef DIFFTEST so synthesis sees nothing. The DPI-C
function $diff_commit_register (or whatever name the project uses) is
defined by the difftest framework.
Coverage and assertions
- Cover every state-machine transition you care about.
- Assert every protocol invariant the contract requires (e.g.,
valid cannot drop without ready having been asserted).
- Use SVA (
assert property) so the same property runs in simulation
and formal.
These are not optional in a project targeting tape-out. Plan to write
them as you write the module, not afterwards.
Reset strategy [industry-pattern]
Three workable strategies. Pick one project-wide.
| Strategy | Pros | Cons | When to choose |
|---|
Synchronous active-low (if (!rst_n) ...) | Cleanest in flop libraries; simulator-friendly. | Needs reset to be deterministically sampled. | Default for most digital frontends. |
| Asynchronous assert, sync de-assert | Tolerates reset outside clock domain. | Requires careful CDC on reset. | Mixed clock domains, many resets. |
| Asynchronous active-high | Common in some legacy flows. | Synthesis quality varies; sim-RTL gap risk. | Only when forced by foundry / library. |
Document the choice once at project root, enforce in lint.
A separate concern: don't reset what doesn't need it. Datapath flops
(pipeline registers, FIFO entries) usually don't need reset; control flops
(state, enables, valid) almost always do. Resetting datapath flops increases
area and sometimes timing.
Clock-domain crossing [abstract]
CDC bugs are silent in simulation and lethal in silicon. Three rules:
- Every CDC goes through a dedicated synchronizer module. No CDC
inline in functional logic.
- Every CDC is documented at the project root (a CDC report). Lint
tools catch unsynchronized crossings; document the synchronized ones
with the chosen synchronization style (2-flop, async-FIFO, handshake).
- CDC tools run in CI, not just at hand-off. SpyGlass-CDC, VC-CDC,
or Conformal-CDC all work; the choice is policy.
If the project will have only one clock domain, this section is optional —
but get the policy down even before adding a second domain.
Lint clean as a hard gate [abstract]
This is the single highest-leverage discipline in RTL development. The
project should treat lint warnings as build failures from day one.
Retrofitting lint cleanliness in Phase 4 takes weeks; maintaining it
from Phase 0 takes minutes per module.
Recommended baseline rule set
Enable at minimum:
- No latch inference (
always_comb without complete assignments).
- No combinational loops.
- No mixed blocking / non-blocking assignments in the same
always_ff.
- No undriven or multi-driven nets.
- All vector widths matched in assignments.
- No implicit wires.
- No port direction mismatches at instantiation.
- No clock-domain crossing without synchronizer (separate CDC tool).
Tools: Verilator's --lint-only for free baseline; SpyGlass /
VC-SpyGlass / AscentLint / SLang for production-grade.
How to introduce lint mid-project
If the project is already past Phase 1 without lint:
- Run lint, count violations per file.
- Add a CI job that blocks regressions (no new violations allowed).
- Allocate cleanup work in priority order (CDC and latch first).
- Drive count to zero over 2–4 weeks of background work.
Common failure modes [abstract]
- Mixing
always and always_ff — synthesis treats them differently,
simulation may not. Pick always_ff everywhere for sequential.
- Forgetting
default in case — synthesizes a latch silently. Always
use unique case with a default branch.
- Resetting datapath flops "to be safe" — bloats area, sometimes
hurts timing. Only reset control flops.
- CDC inline in functional logic — silent metastability bug. Always
go through a synchronizer module.
- Probes added after first sim run — every probe addition causes a
sim regression. Pre-embed at module write time.
- Lint violations accepted "for now" — count drifts up; lint becomes
background noise; one real violation hides for weeks. Never accept new
violations.
- Generating SV by hand instead of from the DSL — the simulator and
RTL diverge. Always generate the interface and hierarchy from the
shared DSL.
See also
choose-artifact — when RTL is the right next step.
define-contracts — the interface and hierarchy DSL the RTL consumes.
align-and-difftest — how the RTL serves as the DUT for difftest.
references/lint-rule-set.md — long-form lint policy.
references/case-ibex.md — lowRISC Ibex SV style.
references/case-boom.md — BOOM processor design.
references/case-xiangshan-coding-style.md — Chisel idioms transferable
to SV.