A hook is a place in the RTL designed to be changed later: a parameter for pipeline depth, a switch between two implementations, or an attribute that tells the tool what to preserve. Timing closure then becomes a configuration change plus a test run.
Seven hooks cover depth, implementation choice, preservation and tool limits. The table names each form and its use.
Hook | Form | Used for |
|---|---|---|
Pipeline depth |
| Add or remove register ranks on a long path |
Implementation choice |
| Plain adder or prefix adder |
Output register |
| Registered outputs at block boundaries |
Replication count |
| Fanout control |
Preserve |
| Stop merging of duplicated registers or logic |
Fanout limit | A tool attribute such as | Ask the flow to buffer or duplicate |
Don't touch |
| Protect hand-built structures such as clock gates |
Attribute names are tool specific. keep and keep_hierarchy are Yosys spellings; other tools use syn_keep, dont_touch, max_fanout, KEEP.
Put the attributes in one wrapper or a single macro per tool, so a switch of flow changes one file.
STAGES = 0 implemented as a wire in a generate branch instead of a register with a zero-length chain?A zero-length array is illegal, and a one-register chain would add a cycle of latency. The generate branch removes the registers entirely, which is exactly what 0 stages means.
Legal parameters are STAGES >= 0 and W >= 1. STAGES counts register ranks. With input A stable before acceptance edge E0 and observation after NBA, stage 1 shows A after E0, stage 2 after E1, and stage 4 after E3: STAGES-1 elapsed periods to visibility for registered cases. A downstream flop consumes A at E1/E2/E4 respectively. Stage 0 is combinational, with no accepting edge or storage. This hook has no valid, reset or backpressure; the consumer owns fill masking and transaction alignment.
Input A sampled at E0 | Post-NBA visibility | Earliest downstream edge |
|---|---|---|
STAGES=0 | Combinational before E0 | E0 if setup is met |
STAGES=1 | E0 | E1 |
STAGES=2 | E1 | E2 |
STAGES=4 | E3 | E4 |
Each slot is one clock cycle, sampled just after its rising edge. Token 1 is driven in slot 0, so the edge that starts slot 1 is its accepting edge E0.
out0 is a wire: it shows each token in the same slot as in.
out1 shows token 1 in slot 1, straight after E0. out2 shows it in slot 2 (after E1).
out4 shows token 1 in slot 4, after E3, and token 5 in slot 8: four cycles of latency, one per rank.
Every lane carries the same sequence 1 to 5; only the delay differs, which is why a consumer must take its latency from the same parameter.
The finite checker below drives on falling edges, observes after NBA, checks only filled ranks, and drains four edges.
// An optional pipeline stage count, chosen at instantiation. STAGES = 0 is a plain wire.
module opt_pipe #(parameter int STAGES = 1, parameter int W = 16) (
input logic clk,
input logic [W-1:0] in,
output logic [W-1:0] out
);
if (STAGES < 0 || W < 1) begin : g_invalid
initial $fatal(1, "Require STAGES >= 0 and W >= 1");
end else if (STAGES == 0) begin : g_wire
assign out = in;
end else begin : g_pipe
logic [W-1:0] r [STAGES];
always_ff @(posedge clk) begin
r[0] <= in;
for (int i = 1; i < STAGES; i++) r[i] <= r[i-1];
end
assign out = r[STAGES-1];
end
endmodule
Latency bench, run with Icarus Verilog 13.0 (2026-10-16): prints PASS. It drives on falling edges, observes after NBA, checks only filled ranks and drains four edges. It checks latency in an ideal model, not a timing result.