One net drives sixteen loads, and the tool quietly changes your cells without anything looking logically wrong. This lesson shows which three rules the net is checked against, where each is written down, and which repair the mapper reaches for as the load grows: buffers, or stronger cells. Every comparison below is a real Yosys and ABC run.
Each rule lives in two places. The library describes what its cells can drive: every X1 output pin in the teaching library carries at most 1.0 pF, 2.0 on X2 and 4.0 on X4, with a 1.0 ns default for the slowest allowed edge. The constraints say what this design may use: a maximum transition, capacitance and fanout, set in a commercial flow with the three commands sketched below. The sketch is illustrative: Yosys has no such commands, and only the ABC runs here are recorded.
Rule | Library side (sized.lib, copied exactly) | Constraint side (commercial, illustrative) |
|---|---|---|
Max fanout | default fanout load 1 per input pin | set_max_fanout |
Max transition | default maximum transition 1.0 ns | set_max_transition |
Max capacitance | X1 pins 1.0 pF, X2 2.0 pF, X4 4.0 pF | set_max_capacitance |
Commercial max fanout counts load units, not sinks: each input pin contributes its fanout load attribute, defaulting to 1. Each load here is a NAND2 built as ~(n & c[i]), so only its n-side input touches the shared net; the c-side input is driven from outside. Sixteen loads present sixteen units, and a commercial limit of 16 would be exactly met. Counting sinks agrees here only by accident.
The design below is a driver NAND2 feeding sixteen load NAND2 cells, each with its own second input; every load survives mapping while contributing one pin to the shared net: the sixteen units the Python check replays. The script maps the same generic netlist four times: loosely and tightly under the typical output load, then loosely and tightly under the heavy one. The typical constraint file asks for a BUF driver and 0.05 pF on every output; the heavy file keeps the driver and asks for 0.2 pF. A four-load sibling at the end feeds the break-it, and an eight-load half feeds the fanout-cap comparison.
read_verilog <<EOV
module drv(input a, input b, input [15:0] c, output [15:0] y);
wire n = ~(a & b);
genvar i;
generate for (i = 0; i < 16; i = i + 1) begin : g
assign y[i] = ~(n & c[i]);
end
endgenerate
endmodule
EOV
synth -top drv -flatten
design -save generic
tee -o abc_loose.txt abc -liberty sized.lib -constr typical.constr -D 1000
tee -o stat_loose.txt stat -liberty sized.lib
write_json net_loose.json
select -assert-count 17 t:NAND2 t:NAND2_X2 t:NAND2_X4
select -assert-none t:*_X2 t:*_X4
design -load generic
tee -o abc_loose_heavy.txt abc -liberty sized.lib -constr heavy.constr -D 1000
tee -o stat_loose_heavy.txt stat -liberty sized.lib
write_json net_loose_heavy.json
select -assert-count 17 t:NAND2 t:NAND2_X2 t:NAND2_X4
select -assert-none t:*_X2 t:*_X4
design -load generic
tee -o abc_typ.txt abc -liberty sized.lib -constr typical.constr -D 250
tee -o stat_typ.txt stat -liberty sized.lib
write_json net_typ.json
select -assert-count 17 t:NAND2 t:NAND2_X2 t:NAND2_X4
select -assert-min 1 t:*_X2 t:*_X4 %u
design -load generic
tee -o abc_heavy.txt abc -liberty sized.lib -constr heavy.constr -D 250
tee -o stat_heavy.txt stat -liberty sized.lib
write_json net_heavy.json
select -assert-count 17 t:NAND2 t:NAND2_X2 t:NAND2_X4
select -assert-min 1 t:*_X2 t:*_X4 %u
design -reset
read_verilog <<EOV
module drv4(input a, input b, input [3:0] c, output [3:0] y);
wire n = ~(a & b);
genvar i;
generate for (i = 0; i < 4; i = i + 1) begin : g
assign y[i] = ~(n & c[i]);
end
endgenerate
endmodule
EOV
synth -top drv4 -flatten
design -save gen4
design -load gen4
tee -o abc_b4_typ.txt abc -liberty sized.lib -constr typical.constr -D 250
tee -o stat_b4_typ.txt stat -liberty sized.lib
select -assert-count 5 t:NAND2 t:NAND2_X2 t:NAND2_X4
select -assert-none t:*_X2 t:*_X4 t:BUF t:BUF_X2 t:BUF_X4
design -load gen4
tee -o abc_b4_heavy.txt abc -liberty sized.lib -constr heavy.constr -D 250
tee -o stat_b4_heavy.txt stat -liberty sized.lib
write_json net_b4_heavy.json
select -assert-count 5 t:NAND2 t:NAND2_X2 t:NAND2_X4
select -assert-min 1 t:*_X2 t:*_X4 %u
design -reset
read_verilog <<EOV
module drv8(input a, input b, input [7:0] c, output [7:0] y);
wire n = ~(a & b);
genvar i;
generate for (i = 0; i < 8; i = i + 1) begin : g
assign y[i] = ~(n & c[i]);
end
endgenerate
endmodule
EOV
synth -top drv8 -flatten
design -save gen8
design -load gen8
tee -o abc_cap8_heavy.txt abc -liberty sized.lib -constr heavy.constr -D 250
tee -o stat_cap8_heavy.txt stat -liberty sized.lib
write_json net_cap8_heavy.json
select -assert-count 9 t:NAND2 t:NAND2_X2 t:NAND2_X4
log PASS: load decides the inserted buffers and the target decides the sizes# illustrative sketch (not run): what the commercial tool calls this step
set_max_fanout 16 [current_design]
set_max_transition 1.0 [current_design]
set_max_capacitance 1.0 [current_design]With both, split by axis: heavy runs add buffers, tight runs add sizes. The loose heavy run inserts 17 buffers and upsizes nothing; the tight heavy run keeps them and upsizes too.
Run | Cells | Area | Delay reached | Buffers (outputs + shared net) | Logic upsized |
|---|---|---|---|---|---|
loose, typical | 18 | 36 | 335 ps | 1 (0+1) | 0 |
loose, heavy | 34 | 68 | 477 ps | 17 (16+1) | 0 |
tight, typical | 18 | 42.4 | 248 ps | 1 (0+1) | 1 |
tight, heavy | 34 | 114.8 | 336 ps | 17 (16+1) | 2 |
Read the table one axis at a time; each buffer count splits into outputs versus the shared net. Down the load axis at the loose target the count moves from 18 to 34 with every logic cell still size 1: the heavier output load alone orders 17 (16+1) buffers against 1 (0+1). Down the target axis the count stays at 18 while one NAND2 hardens to X4. No rule is violated anywhere: the shared net carries 0.16 pF against the 1.0 pF pin limit, so buffers-for-load plus sizes-for-target is this ABC script's split of duties, not a general law. Scorecard: the tight heavy run lands at about 336 ps, short by about 86 ps, and the tight typical run at about 248 ps, inside; every loose run and both break-it runs meet their targets.
Both netlists come from the write_json files; orange marks inserted and upsized cells.
The script ends with a new variant: the same driver feeding four loads instead of sixteen, mapped tightly twice. At the typical load it records 5 cells and no buffer at all; under the heavy load 4 (4+0) buffers with 1 upsized logic cell, every buffer on an output. The shared-net repair vanishes with the fanout while per-output buffers follow the output load at any fanout. Which threshold fires the shared-net buffer is not isolated by these runs: ABC's buffer step takes a fanout cap (-N, default 10) and a gain target (-G, default 300) in this build.
The cap is structural: ABC takes no numeric limit through this runner: abc -script, the only way to pass buffer -N, is rejected, so the cap is structural. The script maps an 8-load half of the same netlist under the same heavy, tight conditions; two such halves are what a cap of 8 allows:
Version | Buffers (outputs + shared net) | Area | Delay reached |
|---|---|---|---|
uncapped, 16 loads | 17 (16+1) | 114.8 | 336 ps |
one capped half, 8 loads | 8 (8+0) | 62.8 | 249 ps |
two halves (doubled) | 16 (16+0) | 125.6 | about 249 ps per half |
The cap removes the shared-net buffer (1 to 0) while the per-output buffers stand (16 against 16): fanout sets the shared-net repair, output load the per-output one, for about 125.6 area against 114.8 uncapped: a second driver, each half still paying its heavy-load buffers.
Sixteen units: only each load's n-side pin touches the shared net, one unit per pin, with each c-side pin driven from outside.
Per-output buffers carry the extra capacitance and the upsized pair holds the slow path: 0.16 pF on the shared net against the 1.0 pF limit, so the repair is delay-driven.
Every number and picture on this page comes from one Yosys 0.69 run of the script shown, recorded by the course checker (scripts/tools/run-rtl-testbenches.mjs with COURSE=synth): ABC maps a driver plus sixteen NAND2 loads against sized.lib under typical.constr (0.05 pF) and heavy.constr (0.2 pF) at loose and tight delay targets, with stat and netlists saved so each buffer count splits into outputs versus the shared net, plus a four-load sibling at the tight target for the break-it and an eight-load half of the same netlist for the cap comparison. The set_max_* sketch is a commercial-tool illustration and was not run. The libraries are fictional teaching files, so the shape of the trade, not the digits, is the point.