A synthesis tool turns each RTL array into storage plus access logic. A small array becomes one flop per bit with read muxes and write decode, all mapped with the surrounding logic. A large array becomes a macro instance instead: the tool connects a compiled block and never builds the storage from gates. This lesson records the flop side of that choice on five shapes that share one read style, a synchronous read with one write port, so that only size moves. Four depths at 8 bits wide trace the cost against entries; a fifth shape keeps 16 entries but widens the word to 32 bits, which tests whether the cost follows entries or total bits.
Every shape runs the same passes, and the script asserts each stage: exactly one $mem_v2 cell after collection, and none after mapping. Proc turns the always block into memory write cells plus a register for the read data. Next memory with -nomap, the usual flow of proc followed by memory collection, gathers the writes and the read into that single cell. Then memory_map converts it into word-wide $dff cells plus decoders and read muxes. Finally techmap, library mapping through dfflibmap and ABC against course.lib with typical.constr, and stat -liberty, which is where the recorded chip area comes from. Quote chip area from stat -liberty, never the ABC summary line: the summary covers only mapped logic while stat totals the module including flops.
read_verilog <<VERILOG
module r08 (input clk, input we, input [2:0] waddr, input [2:0] raddr, input [7:0] wdata, output reg [7:0] rdata);
reg [7:0] mem [0:7];
always @(posedge clk) begin
if (we) mem[waddr] <= wdata;
rdata <= mem[raddr];
end
endmodule
VERILOG
hierarchy -check -top r08
proc
memory -nomap
select -assert-count 1 t:$mem*
memory_map
select -assert-none t:$mem*
tee -o gen_08.txt stat
techmap
opt
dfflibmap -liberty course.lib
tee -o abc_08.txt abc -liberty course.lib -constr typical.constr -D 100000
tee -o stat_08.txt stat -liberty course.lib
design -reset
read_verilog <<VERILOG
module r16 (input clk, input we, input [3:0] waddr, input [3:0] raddr, input [7:0] wdata, output reg [7:0] rdata);
reg [7:0] mem [0:15];
always @(posedge clk) begin
if (we) mem[waddr] <= wdata;
rdata <= mem[raddr];
end
endmodule
VERILOG
hierarchy -check -top r16
proc
memory -nomap
select -assert-count 1 t:$mem*
tee -o dump_16x8.txt dump
memory_map
select -assert-none t:$mem*
tee -o gen_16.txt stat
techmap
opt
dfflibmap -liberty course.lib
tee -o abc_16.txt abc -liberty course.lib -constr typical.constr -D 100000
tee -o stat_16.txt stat -liberty course.lib
design -reset
read_verilog <<VERILOG
module r32 (input clk, input we, input [4:0] waddr, input [4:0] raddr, input [7:0] wdata, output reg [7:0] rdata);
reg [7:0] mem [0:31];
always @(posedge clk) begin
if (we) mem[waddr] <= wdata;
rdata <= mem[raddr];
end
endmodule
VERILOG
hierarchy -check -top r32
proc
memory -nomap
select -assert-count 1 t:$mem*
memory_map
select -assert-none t:$mem*
tee -o gen_32.txt stat
techmap
opt
dfflibmap -liberty course.lib
tee -o abc_32.txt abc -liberty course.lib -constr typical.constr -D 100000
tee -o stat_32.txt stat -liberty course.lib
design -reset
read_verilog <<VERILOG
module r64 (input clk, input we, input [5:0] waddr, input [5:0] raddr, input [7:0] wdata, output reg [7:0] rdata);
reg [7:0] mem [0:63];
always @(posedge clk) begin
if (we) mem[waddr] <= wdata;
rdata <= mem[raddr];
end
endmodule
VERILOG
hierarchy -check -top r64
proc
memory -nomap
select -assert-count 1 t:$mem*
memory_map
select -assert-none t:$mem*
tee -o gen_64.txt stat
techmap
opt
dfflibmap -liberty course.lib
tee -o abc_64.txt abc -liberty course.lib -constr typical.constr -D 100000
tee -o stat_64.txt stat -liberty course.lib
design -reset
read_verilog <<VERILOG
module rW32 (input clk, input we, input [3:0] waddr, input [3:0] raddr, input [31:0] wdata, output reg [31:0] rdata);
reg [31:0] mem [0:15];
always @(posedge clk) begin
if (we) mem[waddr] <= wdata;
rdata <= mem[raddr];
end
endmodule
VERILOG
hierarchy -check -top rW32
proc
memory -nomap
select -assert-count 1 t:$mem*
memory_map
select -assert-none t:$mem*
tee -o gen_w32.txt stat
techmap
opt
dfflibmap -liberty course.lib
tee -o abc_w32.txt abc -liberty course.lib -constr typical.constr -D 100000
tee -o stat_w32.txt stat -liberty course.lib
log PASS: five flop arrays collect to one memory each and map with recorded areasA commercial flow reaches the same fork one step later: it keeps the $mem_v2 cell and binds a compiled macro when one fits. Yosys cannot read a vendor macro, so everything here maps to flops; the macro side enters later as a labelled assumption.
Capacity is depth times width in bits, bytes are bits divided by eight, and the address width is the ceiling of log2 of the depth. The program below proves all three for the five shapes.
import sys
# Capacity arithmetic for the five sweep shapes: depth, width, bits, bytes, address bits.
cases = [
(8, 8, 64, 8, 3),
(16, 8, 128, 16, 4),
(32, 8, 256, 32, 5),
(64, 8, 512, 64, 6),
(16, 32, 512, 64, 4),
]
def addr_bits(depth):
n = 0
while (1 << n) < depth:
n = n + 1
return n
bad = 0
for i in range(len(cases)):
depth, width, want_bits, want_bytes, want_addr = cases[i]
bits = depth * width
total_bytes = bits // 8
abits = addr_bits(depth)
if bits != want_bits:
print("FAIL: case " + str(i) + " bits " + str(bits))
bad += 1
if total_bytes != want_bytes:
print("FAIL: case " + str(i) + " bytes " + str(total_bytes))
bad += 1
if abits != want_addr:
print("FAIL: case " + str(i) + " addr " + str(abits))
bad += 1
if bits % 8 != 0:
print("FAIL: case " + str(i) + " not a whole number of bytes")
bad += 1
if bad != 0:
sys.exit(1)
print("PASS: capacity arithmetic holds on 5 sweep shapes")Count the five shapes in bits from here on: 64, 128, 256, and 512 across the depth sweep, plus a second 512-bit shape built wide. If cost follows bits, the two 512-bit shapes land together.
It doubles cleanly. The area moves from 1244 at 8 entries to 9736 at 64 entries, tracking the bits at 19 area units per bit, with no fixed block anywhere: each entry pays its own flops and muxing.
Shape | Bits | Generic cells | Word flops | Read muxes | Chip area (library units) |
|---|---|---|---|---|---|
8 x 8 | 64 | 165 | 9 | 74 | 1244 |
16 x 8 | 128 | 323 | 17 | 146 | 2473 |
32 x 8 | 256 | 637 | 33 | 290 | 4890 |
64 x 8 | 512 | 1255 | 65 | 578 | 9736 |
16 x 32 | 512 | 1091 | 17 | 530 | 9745 |
Read the table top to bottom. The generic cells climb from 165 to 1255, the word flops from 9 to 65, and the read muxes from 74 to 578. The 16-entry shape records 17 word flops: sixteen words of storage plus the registered read data that memory_dff absorbed into the port, as the next lesson shows. No column holds a fixed block: every entry pays its own flops and muxing, which is the linear cost the chart draws.
The dump file for the 16-entry shape shows what collection saw: one $mem_v2 cell with WIDTH 8, SIZE 16, and ABITS 4, one read port and one write port. The mapping that follows builds exactly that many words of flops and no macro.
$mem_v2 parameter | Recorded value |
|---|---|
WIDTH | 8 |
SIZE | 16 |
Address bits (ABITS) | 4 |
Read ports | 1 |
Write ports | 1 |
The open tools cannot produce a macro area, so the macro side of this lesson is a labelled assumption, and everything built on it says so. A compiled macro is one fixed block: periphery, decoders, and a timing model around dense bit cells, barely moving while the shape stays inside one compiled size. Against it the flop line keeps climbing at the recorded slope, so the two must cross. The calc block below prices that crossing with the recorded slope on one side and a hand-written block on the other.
hand-written assumption, not a recording: smallest compiled macro block = 20000 area units (fixed block plus wrapper)
recorded flop slope = 19 area units per bit (course.lib teaching units, from the depth sweep above)
crossover bits = 20000 / 19 = about 1053 bits, roughly one kilobitState the rule plainly: while bits times 19 stays below the assumed block, flops win; here that is about 1053 bits, roughly one kilobit. Above it, price your own compiler's smallest macro, because the crossing moves with the macro list. A small table never reaches the crossing: at 256 bits the flop array costs a fraction of the assumed block and needs no wrapper, model, or compiler run. Port count, read style, and reset needs move the boundary further toward flops; the next three lessons record each effect.
The fifth shape in the script is the break-it: the same 16 entries with 32-bit words, so 512 bits built wide rather than deep. If cost follows entries it should resemble the 16-entry run; if it follows bits it should land near the 64-entry run, which also holds 512 bits. The recording settles it: 1091 generic cells with 530 read muxes and area 9745, against 1255 cells and area 9736 for the deep shape. Same bits, same cost within the noise of two mux trees. Cost follows bits, not shape, which is why the crossover is written in bits.
512 bits, which is 64 bytes, with 6 address bits. The run recorded area 9736, and 65 word flops: 64 words of storage plus the absorbed read register.
Flops. The crossover at roughly one kilobit against the assumed macro block already points at flops at 256 bits, and the async read plus per-bit reset each independently rule out most compiled macros, so no macro comparison is needed.
Every number and picture on this page comes from Yosys runs of the script shown, recorded by the course checker (scripts/tools/run-rtl-testbenches.mjs with COURSE=synth): five 8-bit and 32-bit shapes run proc, memory -nomap, memory_map, techmap, dfflibmap, and ABC against course.lib with typical.constr, with one $mem_v2 cell asserted per shape and none after mapping. The capacity checks come from the stdlib-only Python program shown, which asserts every figure it prints. The macro block is a hand-written assumption labelled on the page, not a recording.