A plain clocked block is the smallest sequential statement: on every rising edge, copy the input to the output. The question is what the tool builds from it, how that structure grows with width, and which spellings change the answer. This lesson infers a 12-bit plain register, maps it and a 24-bit twin to the teaching library, and breaks a chained blocking assignment. Every count and area below is read back from the recorded files the scripts saved.
A block clocked on one edge with no enable and no reset infers D flops: one bit of storage per bit of the register, updating on every edge. An if without an else still infers a flop here, because holding the old value is what a flop does. Combinational blocks with a skipped branch infer a latch instead; a clocked block never does, which is the first thing the assertions below pin down.
The first script elaborates a 12-bit plain register and pins its family: exactly one $dff cell, with no enabled, sync-reset, or async-reset variant beside it. Pre-mapping vectors stay together, so the 12 bits share one $dff cell carrying a width of 12. The stat report and the JSON netlist are saved so the table and the figure can read them back.
read_verilog <<EOV
module plain12(input clk, input [11:0] d, output reg [11:0] q);
always @(posedge clk) q <= d;
endmodule
EOV
hierarchy -check -top plain12
proc
opt
select -assert-count 1 t:$dff
select -assert-none t:$dffe
select -assert-none t:$adff
select -assert-none t:$sdff
tee -o stat_plain12_generic.txt stat
write_json net_plain12_generic.json
log PASS: 12-bit plain register infers one $dff cellOne flop per bit at 8 units each: the 12-bit register reports chip area 96, and the 24-bit twin reports 192, exactly double. Width scales the area linearly because every extra bit is one more identical DFF.
Register | Generic cells (recorded) | Reading |
|---|---|---|
12-bit plain | 1 $dff | One |
Generic cells have no area, so the price question needs mapping. The second script maps the 12-bit register and a 24-bit twin through synth, dfflegalize, dfflibmap against course.lib, then abc with -constr typical.constr and a loose -D 1000 target plus stat against course.lib. Each run asserts its DFF count: 12 DFFs for the narrow register and 24 DFFs for the wide one. With nothing but flops and no combinational logic, ABC has nothing to map and says so in its report; the price comes from the stat chip total, which includes the sequential cells. That split is worth remembering: the ABC summary line covers only mapped logic, so for any register the stat total is the number to quote.
read_verilog <<EOV
module plain12(input clk, input [11:0] d, output reg [11:0] q);
always @(posedge clk) q <= d;
endmodule
EOV
read_liberty -lib course.lib
hierarchy -check -top plain12
synth -top plain12 -flatten
dfflegalize -cell $_DFF_P_ 01 -cell $_DFF_PN0_ 01
dfflibmap -liberty course.lib
tee -o abc_plain12.txt abc -liberty course.lib -constr typical.constr -D 1000
tee -o stat_plain12.txt stat -liberty course.lib
write_json net_plain12.json
select -assert-count 12 t:DFF
select -assert-none t:DFFR
design -reset
read_verilog <<EOV
module wide24(input clk, input [23:0] d, output reg [23:0] q);
always @(posedge clk) q <= d;
endmodule
EOV
read_liberty -lib course.lib
hierarchy -check -top wide24
synth -top wide24 -flatten
dfflegalize -cell $_DFF_P_ 01 -cell $_DFF_PN0_ 01
dfflibmap -liberty course.lib
tee -o abc_wide24.txt abc -liberty course.lib -constr typical.constr -D 1000
tee -o stat_wide24.txt stat -liberty course.lib
write_json net_wide24.json
select -assert-count 24 t:DFF
log PASS: plain registers map to one DFF per bit at 12 and 24 bitsRegister | Mapped library cells (recorded) | Chip area (stat -liberty) |
|---|---|---|
12-bit plain | 12 DFF | 96 |
24-bit plain | 24 DFF | 192 |
Read the chip-area column as the per-bit price of storage in this library: 96 for 12 bits and 192 for 24 bits, which is 8 units per bit in both rows. Doubling the width doubles the area because every extra bit is one more identical DFF; there is no sharing, no control overhead, and no surprise in a plain register.
The third script respells the same 12-bit register with a blocking assignment, q = d, and asserts the identical family: one $dff cell and no latch. The recorded netlist reads 1 $dff, the same single cell as the nonblocking form. For one assignment the synthesis claim stops there: the tool sees one edge-triggered copy either way. Chaining is where the spellings diverge, and the fourth script records it: begin a = d; b = a; end in one clocked block builds two $dff cells at proc, then opt merges them into 1 $dff, because both sample d on the same edge and no shift stage survives. The testbench below pins the behaviour: beside a nonblocking shift register fed the same data, the blocking chain's second output is already correct after one edge while the true shift needs two.
read_verilog <<EOV
module plain12b(input clk, input [11:0] d, output reg [11:0] q);
always @(posedge clk) q = d;
endmodule
EOV
hierarchy -check -top plain12b
proc
opt
select -assert-count 1 t:$dff
select -assert-none t:$dlatch
tee -o stat_plain12b_generic.txt stat
write_json net_plain12b_generic.json
log PASS: blocking assignment in a clocked block infers the same $dffread_verilog <<EOV
module chain1b(input clk, input d, output reg a, output reg b);
always @(posedge clk) begin a = d; b = a; end
endmodule
EOV
hierarchy -check -top chain1b
proc
select -assert-count 2 t:$dff
select -clear
opt
select -assert-count 1 t:$dff
tee -o stat_chain1b_generic.txt stat
write_json net_chain1b_generic.json
log PASS: chained blocking assignments collapse to one flop// engine: icarus
module chain1b_tb(input logic clk, input logic d, output logic a, output logic b);
always @(posedge clk) begin a = d; b = a; end
endmodule
module chain1n_tb(input logic clk, input logic d, output logic a, output logic b);
always @(posedge clk) begin a <= d; b <= a; end
endmodule
module tb_chain;
logic clk = 0, d = 0, ab, bb, an, bn;
chain1b_tb blk(.clk(clk), .d(d), .a(ab), .b(bb));
chain1n_tb nblk(.clk(clk), .d(d), .a(an), .b(bn));
always #5 clk = ~clk;
int errors = 0;
initial begin
d = 0;
@(posedge clk); #1;
d = 1;
@(posedge clk); #1;
if (bb !== 1'b1) begin $display("FAIL: blocking chain did not collapse"); errors++; end
if (bn !== 1'b0) begin $display("FAIL: reference shift misbehaving"); errors++; end
@(posedge clk); #1;
d = 0;
@(posedge clk); #1;
if (bn !== 1'b1) begin $display("FAIL: nonblocking chain did not shift"); errors++; end
if (bb !== 1'b0) begin $display("FAIL: blocking chain held stale data"); errors++; end
if (errors == 0) $display("PASS: chained blocking collapses the shift stage");
$finish;
end
endmoduleReading the flop count off the generic netlist as the library cost. One $dff cell is one vector before mapping and 12 library flops after; only the mapped histogram tells you what the chip pays.
$dff for a 12-bit register. Why one cell and not twelve?Pre-mapping vectors stay together in one cell with a width parameter. The twelve library flops appear only after mapping, as the recorded DFF counts confirm.
It proves one flop per bit with no sharing or control overhead at this width. A bit tied to a constant, an undriven bit, or an enable folded in would break the exact doubling, and the mapped histogram would show it.
Every number and picture on this page comes from four Yosys 0.70 runs recorded by the course checker (scripts/tools/run-rtl-testbenches.mjs with COURSE=synth): a 12-bit plain register through proc and opt with family assertions, the 12-bit and 24-bit twins through synth, dfflegalize, dfflibmap -liberty course.lib, abc -liberty course.lib -constr typical.constr -D 1000 and stat -liberty course.lib, a single-assignment blocking respelling, and a chained-blocking collapse that builds two $dff at proc and merges them at opt. An Icarus sim proves the collapse against a nonblocking shift. The library is fictional, so absolute areas are teaching units; the linear scaling is the point.