A clock period is not a budget: a 10.0 ns period does not give the logic all of it. The outside world consumes a share before the signal reaches the input port and demands a share after it leaves the output port, and the clock itself arrives late and smeared. What is left after every claimant takes its share is the available time, and the four path classes split the period differently: input to register, register to register, register to output, and input to output straight through.
This lesson builds those four splits from one EXAMPLE period and hand-derived arithmetic, every figure and table labelled hand-derived. The recorded run proves the registered classes exist in real structure, two flops of it, before any budget is spent.
The design below is a two-flop chain: req captured twice, then driven out as ack. That gives the three registered paths in the smallest possible structure. The script synthesises the chain and asserts the two flops; the second half is the break-it preview, req wired straight to ack with no flop at all.
read_verilog <<EOV
module io_chain(input clk, input req, output ack);
reg a, b;
always @(posedge clk) begin a <= req; b <= a; end
assign ack = b;
endmodule
EOV
synth -top io_chain
tee -o stat_chain.txt stat
write_json net_chain.json
select -assert-count 2 t:$_DFF_P_
check
design -reset
read_verilog <<EOV
module io_direct(input clk, input req, output ack);
assign ack = req;
endmodule
EOV
synth -top io_direct
tee -o stat_direct.txt stat
write_json net_direct.json
select -assert-none t:$_DFF_P_
check
log PASS: two flops make the chain and a wire needs nonePort directions stay fixed for the rest of the topic: req is an input, ack is an output, so every input delay belongs to req and every output delay to ack.
The chain has the three registered classes: 2 cells, 2 flops. The direct version has the fourth class only (no cells), one feedthrough path the port delays bound to 4.0 ns.
Version (recorded) | Cells (recorded) | Flops (recorded) |
|---|---|---|
Chain | 2 | 2 flops |
Direct wire | no cells | no flops |
The chain netlist holds 2 cells, both flops: req in, two flops, ack out, each gap one registered path. The direct version is the fourth class: a feedthrough wire budgeted only from the port delays.
The constraint below bounds the same chain the run just proved: EXAMPLE period, uncertainty margin on every path, source latency shifting arrival, input delay before req, and output delay after ack, external setup included. The numbers live in the listing and the table; the prose names only who spends what.
# not-run: no SDC reader in the open tools
create_clock -name CLK -period 10.0 [get_ports clk]
set_clock_uncertainty -setup 0.5 [get_clocks CLK]
set_clock_latency -source 1.2 [get_clocks CLK]
set_input_delay -clock CLK 3.0 [get_ports req]
set_output_delay -clock CLK 2.5 [get_ports ack]Uncertainty and latency are routinely confused. Uncertainty is margin the tool must not spend. Latency shifts both edges together, so the period is unchanged. One shrinks the budget; the other moves the window.
Uncertainty exists because synthesis runs on the previous lesson's ideal clock: zero skew by assumption. Silicon has skew, so the constraint holds back 0.5 ns on every setup check, plus jitter. That margin comes off the usable period on all four path classes, which is why the budgets below subtract it first.
Latency is the delay from the clock source to the flop clock pin: source latency before the definition point, network latency after it. It does not shrink the period: if launch and capture both shift by the same 1.2 ns, the distance between them is still the full period. It changes absolute arrival against the outside world, which the input and output delays reconcile.
The program below spends the period four times in integer picoseconds, with an EXAMPLE clock-to-Q of 0.8 ns and an EXAMPLE setup of 0.5 ns. The input path leaves a 6.5 ns remainder and a 6.0 ns budget. The register path is period minus clock-to-Q, uncertainty, and setup: 8.2 ns. The output path is period minus clock-to-Q and the output delay: 6.2 ns, no capture setup subtracted. The feedthrough path is period minus both external delays and uncertainty: 10 minus 3 minus 2.5 minus 0.5, a 4.0 ns budget with no flop at either end. Ten self-checks pin every remainder, every budget, and every sum back to the period.
import sys
# IO budget arithmetic in integer picoseconds. EXAMPLE parameters, hand-derived.
PERIOD_PS = 10000
IN_DELAY_PS = 3000
OUT_DELAY_PS = 2500
UNC_PS = 500
SETUP_PS = 500
TCQ_PS = 800
# Input path: the outside world owns the input delay; uncertainty and the
# capturing flop's setup come off the remainder.
in_avail = PERIOD_PS - IN_DELAY_PS - UNC_PS
in_comb = in_avail - SETUP_PS
# Register-to-register path: launch clock-to-Q, uncertainty, capture setup.
reg_comb = PERIOD_PS - TCQ_PS - UNC_PS - SETUP_PS
# Output path: launch clock-to-Q plus the output delay, which already holds
# the external setup; no capture setup is subtracted here.
out_avail = PERIOD_PS - OUT_DELAY_PS - UNC_PS
out_comb = out_avail - TCQ_PS
# Input-to-output feedthrough: no flop at either end, so both external
# delays and the uncertainty come off the period directly.
inout_comb = PERIOD_PS - IN_DELAY_PS - OUT_DELAY_PS - UNC_PS
if in_avail != 6500:
print("FAIL: input remainder got " + str(in_avail) + " want 6500")
sys.exit(1)
if in_comb != 6000:
print("FAIL: input budget got " + str(in_comb) + " want 6000")
sys.exit(1)
if reg_comb != 8200:
print("FAIL: register budget got " + str(reg_comb) + " want 8200")
sys.exit(1)
if out_avail != 7000:
print("FAIL: output remainder got " + str(out_avail) + " want 7000")
sys.exit(1)
if out_comb != 6200:
print("FAIL: output budget got " + str(out_comb) + " want 6200")
sys.exit(1)
if inout_comb != 4000:
print("FAIL: feedthrough budget got " + str(inout_comb) + " want 4000")
sys.exit(1)
if in_comb + IN_DELAY_PS + UNC_PS + SETUP_PS != PERIOD_PS:
print("FAIL: input parts do not sum to period")
sys.exit(1)
if reg_comb + TCQ_PS + UNC_PS + SETUP_PS != PERIOD_PS:
print("FAIL: register parts do not sum to period")
sys.exit(1)
if out_comb + TCQ_PS + OUT_DELAY_PS + UNC_PS != PERIOD_PS:
print("FAIL: output parts do not sum to period")
sys.exit(1)
if inout_comb + IN_DELAY_PS + OUT_DELAY_PS + UNC_PS != PERIOD_PS:
print("FAIL: feedthrough parts do not sum to period")
sys.exit(1)
print("PASS: comb budgets 6.0 ns in and 8.2 ns through and 6.2 ns out and 4.0 ns feedthrough of 10.0 ns")Path (hand-derived) | Consumes (hand-derived) | Combinational budget (hand-derived) |
|---|---|---|
Input to register | Input delay 3.0 ns, uncertainty 0.5 ns, setup 0.5 ns | 6.0 ns |
Register to register | Clock-to-Q 0.8 ns, uncertainty 0.5 ns, setup 0.5 ns | 8.2 ns |
Register to output | Clock-to-Q 0.8 ns, output delay 2.5 ns, uncertainty 0.5 ns | 6.2 ns |
Input to output | Input delay 3.0 ns, output delay 2.5 ns, uncertainty 0.5 ns | 4.0 ns |
Each row's consumers plus its budget sum back to the period; the program proves all four sums. The output row is the one people get wrong: its capture flop sits outside the block, so its setup is part of the 2.5 ns output delay, never a separate subtraction. The feedthrough row has no clock-to-Q and no setup at all.
The second run in the script is the break-it: req tied straight to ack, no flop left in the path. The flop count falls from 2 flops to no flops, and the three registered paths collapse into the fourth class: registers removed, so one feedthrough path with a 4.0 ns budget, bounded only by the input delay, the output delay, and uncertainty.
Remainder 6.5 ns minus setup: 6.0 ns, because the capturing flop sits inside the block.
Output: 10.0 minus 2.5 minus 0.5 minus 0.8, which is 6.2 ns, with no capture setup subtracted. Feedthrough: 10.0 minus 3.0 minus 2.5 minus 0.5, which is 4.0 ns.
Cell and flop counts and the netlist picture on this page come from one Yosys 0.70 run of the script shown, recorded by the course checker (scripts/tools/run-rtl-testbenches.mjs with COURSE=synth): a two-flop chain and a direct-wire variant are synthesised, with write_json netlists and flop-count assertions. All budgets, remainders, and timing bars come from the stdlib-only Python program shown, which asserts every figure it prints, and are labelled hand-derived. The SDC listing is illustrative: the open tools have no SDC reader.