A memory compiler builds an SRAM macro for one exact depth, width, and port count: it emits the layout block plus a timing and power model for that shape, and a new shape means a new compiler run with a new model. A synthesis tool treats that compiled macro as a black box: it connects the pins and meets the setup and hold requirements from the model, and it never maps logic inside. Between the RTL and the macro sits the wrapper, which registers the boundary, absorbs the awkward cases, and isolates every macro-specific hook from the rest of the design. This lesson records that boundary in Yosys and selects macros from a stand-in list with a checked program.
Yosys cannot read a vendor macro, so the lesson demonstrates the flow with a stand-in: a module marked with the blackbox attribute, which tells the tool the inside is implemented elsewhere. The wrapper instantiates that macro exactly the way RTL instantiates a compiled block: address and control on the way in, data on the way out, and a pipeline register on the read data. Two variants run: the full wrapper with the output register, and a direct variant that exposes the macro data without registering it. Both assert the same boundary fact: exactly one macro cell survives the flow.
read_verilog <<VERILOG
(* blackbox *)
module sram_1024x32 (input clk, input cs, input we, input [9:0] addr, input [31:0] wdata, output [31:0] rdata);
endmodule
module wrapper (input clk, input we, input [9:0] waddr, input [9:0] raddr, input [31:0] wdata, output reg [31:0] rdata);
wire c_we = we;
wire [9:0] c_addr = we ? waddr : raddr;
wire [31:0] c_rdata;
sram_1024x32 macro (.clk(clk), .cs(1'b1), .we(c_we), .addr(c_addr), .wdata(wdata), .rdata(c_rdata));
always @(posedge clk) rdata <= c_rdata;
endmodule
VERILOG
hierarchy -check -top wrapper
proc
opt
techmap
opt
tee -o stat_wrap.txt stat
write_json net_wrap.json
select -assert-count 1 t:sram_1024x32
design -reset
read_verilog <<VERILOG
(* blackbox *)
module sram_1024x32 (input clk, input cs, input we, input [9:0] addr, input [31:0] wdata, output [31:0] rdata);
endmodule
module wrapper_direct (input clk, input we, input [9:0] waddr, input [9:0] raddr, input [31:0] wdata, output [31:0] rdata);
wire c_we = we;
wire [9:0] c_addr = we ? waddr : raddr;
sram_1024x32 macro (.clk(clk), .cs(1'b1), .we(c_we), .addr(c_addr), .wdata(wdata), .rdata(rdata));
endmodule
VERILOG
hierarchy -check -top wrapper_direct
proc
opt
techmap
opt
tee -o stat_direct.txt stat
write_json net_direct.json
select -assert-count 1 t:sram_1024x32
log PASS: wrapper keeps the macro as one blackbox cell with and without output registerThe stat files record the boundary the assertions pin. Each variant holds exactly one sram_1024x32 cell: the tool mapped the wrapper logic around it and never touched the inside. The wrapper variant records its output flops plus address muxing; the direct variant records 0 output flops with the same muxing. The macro count is identical in both: wrapper conveniences change the flops around the macro, never the macro itself.
Variant | Macro cells | Output flops | Address muxes |
|---|---|---|---|
Wrapper with output register | 1 | 32 | 10 |
Direct without output register | 1 | 0 | 10 |
The wrapper exists because the macro boundary is awkward in three predictable ways. First, timing: the macro model quotes a clock-to-data delay and setup requirements that the surrounding logic meets cleanly through a pipeline stage, so address, control, and write data register on the way in and the read data registers on the way out. Second, behaviour: the bypass mux and any byte-mask merging live here, forwarding write data to a same-cycle same-address read when the macro has no transparency, exactly the logic the last lesson priced. Third, macro specificity: power, test, and retention hooks vary by compiler and even by macro, so the wrapper absorbs them in one place instead of spreading macro knowledge through the RTL.
Wrapper piece | Job |
|---|---|
Input registers | Meet macro setup with a clean pipeline stage |
Bypass mux | Forward write data to a same-cycle same-address read |
Output register | Absorb macro clock-to-data delay |
Macro hooks | Power, test, and retention pins that vary by compiler |
The port count shapes the wrapper the same way it shapes the flop array: one write port plus one read port is the common single-port shape, and each extra port adds address, data, and enable wiring through the wrapper to the macro pins. The shared address mux is the single-port bargain made visible: on write cycles the macro serves the write address, so a same-cycle read to another address waits, and reads block on writes. The macro menu prices those shapes one by one, which is what the selection program below checks.
The compiler menu below is a hand-written stand-in, not a vendor catalogue: five macros with generic names, each one exact depth by exact width. Yosys cannot produce a vendor menu, so the list is labelled as an assumption and the selection rule is what the program checks. The rule is deliberately simple: take the smallest single macro that fits the request, waste and all; tile the largest same-width macro only when no single macro is deep enough.
Stand-in macro (assumption) | Depth | Width |
|---|---|---|
M512x16 | 512 | 16 |
M1024x16 | 1024 | 16 |
M1024x32 | 1024 | 32 |
M2048x8 | 2048 | 8 |
M4096x32 | 4096 | 32 |
import sys
# Hand-written stand-in macro list, not a vendor catalogue: (name, depth, width).
# A memory compiler emits one macro per shape; the list below stands in for its menu.
MACROS = [
("M512x16", 512, 16),
("M1024x16", 1024, 16),
("M1024x32", 1024, 32),
("M2048x8", 2048, 8),
("M4096x32", 4096, 32),
]
def choose(depth, width):
same = [m for m in MACROS if m[2] == width and m[1] >= depth]
if same:
same.sort(key=lambda m: (m[1], m[0]))
return (same[0][0], 1)
big = [m for m in MACROS if m[2] == width]
big.sort(key=lambda m: (m[1], m[0]), reverse=True)
name, mdepth, mwidth = big[0]
count = (depth + mdepth - 1) // mdepth
return (name, count)
cases = [
((1024, 32), ("M1024x32", 1)),
((512, 32), ("M1024x32", 1)),
((2048, 32), ("M4096x32", 1)),
((100, 8), ("M2048x8", 1)),
((3000, 16), ("M1024x16", 3)),
]
bad = 0
for i in range(len(cases)):
(depth, width), want = cases[i]
got = choose(depth, width)
if got != want:
print("FAIL: case " + str(i) + " got " + str(got) + " want " + str(want))
bad += 1
if bad != 0:
sys.exit(1)
print("PASS: macro selection holds on 5 depth-by-width requests")The program proves five selections. An exact request takes its macro whole. A 512-deep 32-bit request takes the 1024-deep macro and wastes half: there is no smaller 32-bit macro on the menu, and paying a whole macro for a small table is exactly why small tables stay flops. A 2048-deep 32-bit request takes the 4096-deep macro for the same reason. Only the 3000-deep 16-bit request tiles, three copies of the largest 16-bit macro, because tiling multiplies pins, power hooks, and wrapper wiring. Never invent a vendor name for a menu like this: the shapes are the point, and every real menu comes from the compiler, not from a lesson.
The second variant in the script is the break-it: the same macro, the same address muxing, but the read data leaves the macro unregistered. The recording shows what that convenience costs: the output flops fall from 32 to 0 while the macro count is unchanged and the address muxing matches at 10. A wrapper register is a timing choice, not a macro choice: it absorbs the clock-to-data delay into a clean pipeline stage, and removing it pushes that delay onto whatever consumes the data. The register also costs a cycle of read latency: the registered wrapper delivers macro data one cycle after the request, while the direct variant exposes it in the same cycle. Fix macro problems by rerunning the compiler or changing the wrapper, never by editing the netlist inside the macro boundary the black box protects.
One M1024x32: the smallest single macro that fits the request exactly, proved by the selection program. No tiling is needed because the menu holds the shape.
A whole M2048x8 macro for 800 bits of need: the smallest 8-bit macro on the menu, mostly wasted. That waste is why the first lesson keeps small tables in flops.
Every number and picture on this page comes from Yosys and Python runs of the scripts shown, recorded by the course checker (scripts/tools/run-rtl-testbenches.mjs with COURSE=synth): a (* blackbox *) stand-in macro with a registered and a direct wrapper runs hierarchy, proc, opt, and techmap with one macro cell asserted per variant, and the stat histograms are recorded; the macro menu is a hand-written stand-in list labelled on the page, and the stdlib-only Python program proves its five selections with asserts. No vendor name appears anywhere: Yosys cannot read a vendor macro.