The same depth and width can infer completely different hardware once the ports change. The read style decides whether the port carries a clock: a registered read absorbs its output flop into the memory port, while a combinational read leaves an asynchronous port behind. Each extra write port widens the bit cell and deepens the access muxing. This lesson records four combinations on 16-entry 8-bit shapes: a synchronous baseline with one write port, an async-read variant, a two-write-port variant, and the baseline collected without the memory_dff step, which isolates exactly what that pass does.
The baseline runs proc, then the full memory flow with -nomap: proc followed by memory collection with read-register absorption turned on. After proc the design holds memory write cells, memory read cells, and one plain $dff for the read data. The memory_dff pass merges that $dff into the port, so the port gains a clock and the external flop disappears. Then memory_collect builds the single $mem_v2 cell the assertions pin, and memory_map expands it into word-wide flops plus decode and muxes. The flowchart draws that path, including the branch the fourth variant takes when memory_dff never runs.
read_verilog <<VERILOG
module sync16 (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 sync16
proc
memory -nomap
select -assert-count 1 t:$mem*
tee -o dump_sync.txt dump
memory_map
select -assert-none t:$mem*
tee -o gen_sync.txt stat
techmap
opt
dfflibmap -liberty course.lib
tee -o abc_sync.txt abc -liberty course.lib -constr typical.constr -D 100000
tee -o stat_sync.txt stat -liberty course.lib
design -reset
read_verilog <<VERILOG
module async16 (input clk, input we, input [3:0] waddr, input [3:0] raddr, input [7:0] wdata, output [7:0] rdata);
reg [7:0] mem [0:15];
always @(posedge clk) if (we) mem[waddr] <= wdata;
assign rdata = mem[raddr];
endmodule
VERILOG
hierarchy -check -top async16
proc
memory -nomap
select -assert-count 1 t:$mem*
tee -o dump_async.txt dump
memory_map
select -assert-none t:$mem*
tee -o gen_async.txt stat
techmap
opt
dfflibmap -liberty course.lib
tee -o abc_async.txt abc -liberty course.lib -constr typical.constr -D 100000
tee -o stat_async.txt stat -liberty course.lib
design -reset
read_verilog <<VERILOG
module w2x16 (input clk, input we0, input we1, input [3:0] waddr0, input [3:0] waddr1, input [3:0] raddr, input [7:0] wdata0, input [7:0] wdata1, output reg [7:0] rdata);
reg [7:0] mem [0:15];
always @(posedge clk) begin
if (we0) mem[waddr0] <= wdata0;
if (we1) mem[waddr1] <= wdata1;
rdata <= mem[raddr];
end
endmodule
VERILOG
hierarchy -check -top w2x16
proc
memory -nomap
select -assert-count 1 t:$mem*
tee -o dump_w2.txt dump
memory_map
select -assert-none t:$mem*
tee -o gen_w2.txt stat
techmap
opt
dfflibmap -liberty course.lib
tee -o abc_w2.txt abc -liberty course.lib -constr typical.constr -D 100000
tee -o stat_w2.txt stat -liberty course.lib
design -reset
read_verilog <<VERILOG
module sync_nodff (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 sync_nodff
proc
memory_collect
select -assert-count 1 t:$mem*
tee -o dump_nodff.txt dump
memory_map
select -assert-none t:$mem*
tee -o gen_nodff.txt stat
techmap
opt
dfflibmap -liberty course.lib
tee -o abc_nodff.txt abc -liberty course.lib -constr typical.constr -D 100000
tee -o stat_nodff.txt stat -liberty course.lib
log PASS: four port variants collect to one memory each and map with recorded parametersEach variant asserts the same two facts: one $mem_v2 cell after collection, none after mapping. The differences sit in the port parameters: RD_CLK_ENABLE for the clock, RD_TRANSPARENCY_MASK for same-cycle reads, WR_PORTS for the port count, and WR_PRIORITY_MASK for the write order when two ports can collide.
The extra port moves it far more. The synchronous baseline records area 2472 and the async variant records area 2397: saving the read flop barely moves the total. Adding the second write port jumps to area 3317, because priority muxing and a second decode path cost far more than one flop. The collect-only variant is close to the baseline but not identical at 326 generic cells and 20 word flops against 323 and 17: without memory_dff the read $dff stays outside the port and the mapped netlist carries the extra flops, which is the memory_dff point made visible.
Variant | RD_CLK_ENABLE | Transparency | Write ports | Word flops | Generic cells | Chip area (library units) |
|---|---|---|---|---|---|---|
Sync, one write port | 1'1 | 1'0 | 1 | 17 | 323 | 2472 |
Async read | 1'0 | 1'0 | 1 | 16 | 322 | 2397 |
Sync, two write ports | 1'1 | 2'00 | 2 | 17 | 614 | 3317 |
Sync, collect only | 1'0 | 1'0 | 1 | 20 | 326 | 2463 |
Read the port columns first. The baseline shows RD_CLK_ENABLE 1'1 with the clock connected: the read register was absorbed, so the port is synchronous. The async variant shows 1'0: no clock reaches the port, and data follows the address in the same cycle. The two-port variant shows WR_PORTS 2 with WR_PRIORITY_MASK 4'0100, the inferred write order when both ports target one entry. The collect-only variant is the telling contrast: identical RTL to the baseline, yet RD_CLK_ENABLE 1'0 with the clock unconnected, because without memory_dff the read $dff stays outside and the port stays asynchronous.
Then read the cost columns. The async variant maps to 16 word flops against 17 for the baseline: exactly the missing read register, and the area barely moves between the two runs. The read style barely changes the flop cost, which is why the style decision is about macro fit, not about saving one flop. Most compiled SRAM macros offer synchronous read only, so an asynchronous read forces a flop implementation at any size: that row of the hub table is a technology rule, and here is its recorded price. The baseline records area 2472 here while the identical 16-entry shape on the flop-array page records 2473: the 1-unit gap between the two runs is ABC drift, not a design difference.
The third variant in the script is the break-it: the same synchronous 16-entry memory with a second write port, each port carrying its own address, data, and enable. Nothing else changed, so the whole gap comes from the port. The generic cell count moves from 323 to 614 and the read muxes from 146 to 277, while the word flops stay at 17: the storage did not change, the access logic did. The area moves from 2472 to 3317. Each added port widens the bit cell and the access muxing, so port count pushes the flop-versus-macro boundary toward flops while the memory is small: two ports double the accesses per cycle but here cost roughly a third more area for the same 128 bits.
A read and a write to the same address in the same cycle is read-during-write. The baseline, async, and collect-only variants record RD_TRANSPARENCY_MASK 1'0 with RD_COLLISION_X_MASK 1'0: a same-cycle same-address read returns the previous contents, a read-old port with no undefined bits. The two-port row instead records 2'00 with collision mask 2'00, one bit per write port. On the async variant the mask is carried but meaningless: the read is combinational, so data follows the address in the same cycle whatever the mask claims. Portable RTL assumes none of the three behaviours below, because the mask varies by macro and mode. Skipping the handling corrupts every read-after-write to the same address, and the next lesson records the bypass wrapper that fixes it against a macro with no transparency.
Behavior | Meaning | Safe handling |
|---|---|---|
Read-old | Read returns the previous contents | Add bypass when the new data is needed |
Read-new | Read returns the data being written | Still add bypass for macros that differ |
Undefined | Read returns neither reliably | Avoid the collision or register around it |
Async read, with RD_CLK_ENABLE 1'0 in the recording. It costs macro fit: most compiled macros cannot do it, so the table becomes flops at any size, here 322 generic cells against 323 for the sync form.
Access logic, not storage: the word flops stay at 17 while the generic cells move from 323 to 614, and the area column of the table shows the price. The write order between the ports is recorded in WR_PRIORITY_MASK 4'0100.
Every number and picture on this page comes from one Yosys 0.70+post run of the script shown, recorded by the course checker (scripts/tools/run-rtl-testbenches.mjs with COURSE=synth): sync, async, two-write-port, and collect-only 16-entry shapes run proc, memory collection, 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 flowchart is a drawing of that pass sequence, not tool output.