The critical path is the slowest register-to-register route in the design. Only changes on that path improve the clock rate, and only up to the next-worst path. Fix every path with negative slack, worst first, and expect the critical path to move after each fix.
Speeding up anything else costs area for no gain. The workflow is always the same: read the timing report, find the worst path, see which RTL it came from, and change the structure.
An if / else if chain means priority. Each condition waits for all earlier ones to fail.
Built literally, it is as deep as the chain is long. If the conditions can never overlap, use a parallel AND-OR select instead.
A one-hot request is one example. The parallel form has shallow depth. It uses one AND stage plus one wide OR.
Built from bounded-fan-in gates, that OR is a tree, so the select costs roughly 1 + ceil(log_f N) levels for fan-in f in that idealized topology, plus routing and load. That still grows far more slowly than a chain N deep. It gives the same answer when the one-hot invariant holds.
Read each panel from the data inputs to y and count the gates on the highlighted path from d3.
Chain: d3 is the last choice, so it crosses all four muxes. Adding a fifth request adds a fifth level.
AND-OR: every input crosses one AND and two OR levels: 1 + ceil(log2 4) = 3 for 2-input gates.
Growth: doubling to 8 requests makes the chain 8 deep but the AND-OR tree only 4.
$onehot0(req) is true when zero or one bits of req are set. Every AND-OR select in this section needs that invariant.
If one input arrives much later than the rest, structure the logic so that signal passes through as few gates as possible. Compute both outcomes from the early inputs, then use the late signal as the final mux select.
Compute conditions in the previous cycle and register them, so the current cycle only consumes a flop output.
Share a sub-expression only if the shared node does not become the fan-out bottleneck.
Replace a wide comparison (a == b on 64 bits) with a registered partial result when the inputs are available earlier.
When no restructuring is enough, cut the path with a register. This trades latency (one more cycle) for clock rate, and is the topic of the pipelining section in the sequential chapter.
Symptom in the timing report | First thing to try |
|---|---|
Long chain of | Parallel select or |
Deep adder or comparator | Let the tool build a fast adder; check you did not write a bit-serial loop |
One very late input | Make it the last mux select |
High fan-out net | Duplicate or buffer the driver; register it |
Everything is slightly too slow | Pipeline |
if / else if chain that selects a data bus. The requests are one-hot by protocol. What do you change, and what do you add for safety?Replace the chain with a parallel AND-OR (or unique case) select. Add an assertion that $onehot0(req) always holds, because the faster structure is only correct under that assumption.
The 30-level path first. It is the critical one, and shortening the 5-level path does not raise the clock rate. Fixing the 30-level path helps only until the next-worst path becomes critical, so keep going until no path has negative slack.
The AND-OR ORs both data words together, a defined but wrong value. The chain would have picked the lowest index instead. That is why the one-hot invariant needs an assertion.
This testbench proves the chain and the parallel select agree whenever requests never overlap.
module prio_chain (
input logic [3:0] req,
input logic [7:0] d0, d1, d2, d3,
output logic [7:0] y
);
always_comb begin
if (req[0]) y = d0; // serial priority: req[3] waits for req[0..2]
else if (req[1]) y = d1;
else if (req[2]) y = d2;
else if (req[3]) y = d3;
else y = '0;
end
endmodule
module onehot_select (
input logic [3:0] req, // assumed one-hot or zero
input logic [7:0] d0, d1, d2, d3,
output logic [7:0] y
);
// parallel AND-OR: shallow depth. One AND stage plus a bounded-fan-in OR tree, about 1 + ceil(log_f N) levels.
assign y = ({8{req[0]}} & d0) | ({8{req[1]}} & d1)
| ({8{req[2]}} & d2) | ({8{req[3]}} & d3);
endmodule