Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • Logic Synthesis Glossary
  • Logic Synthesis Cheatsheet
  • Synthesis Foundations14
  • Logic Optimisation and Mapping20
  • Constraints and Timing15
  • Sequential, FSM and Memory19
    • Flops, Resets and Enables
    • FSM Synthesis
    • Memory and Macro Inference
      • Flop Arrays or RAM Macros
      • Read and Write Port Styles
      • Byte Enables and Bypass Wrappers
      • Compilers, Macros and Wrappers
    • Retiming and Pipelining
  • Datapath Synthesis14
  • Area, Power and Test14
  • Verification, ECO and Handoff15
  • Flows and Debugging15
  • Interview and Judgment11
Loading lesson

←Memory and Macro Inference
  1. Topics
  2. Logic Synthesis
  3. Sequential, FSM and Memory
  4. Memory and Macro Inference
  5. Read and Write Port Styles
On this page

    Part 4 of 9, lesson 12 of 19Part 4 of 9/Sequential, FSM and Memory/Lesson 12 of 19Intermediate

    Read and Write Port Styles

    Sync and async reads, write ports, and read-during-write behavior.

    Two arrays with identical depth and width infer different hardware, and the difference hides in port attributes, not in the RTL the designer rereads. Without the recorded parameters, sync and async look interchangeable until the macro will not bind.

    After this lesson you can: read RD_CLK_ENABLE, transparency, and port counts off a $mem_v2 dump, say what memory_dff absorbs, and price a second write port from a recorded run.

    The question

    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 pass sequence and what memory_dff moves

    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.

    Flowchart of the memory pass sequence with the memory_dff branch.
    From the always block through proc to the memory cell: memory_dff absorbs the read register into the port, and skipping it leaves an async port with the flop outside.
    yosys
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    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 parameters

    Each 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.

    What the four variants recorded

    Predict first The async variant saves one output flop, and the two-port variant adds a whole write path. Order the four mapped areas before reading the table: does the saved flop or the extra port move the total more?

    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.

    Recorded chip area for the four 16-entry port variants. The two-write-port bar stands apart at 3317 units; the other three cluster near 2472.

    Break it: add a second write port

    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.

    Read during write

    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

    Coding an async read and expecting a compiled macro is the classic miss. The macro supports sync read, so the tool builds flops and the area report surprises the team late. And reaching for a second write port to double throughput without pricing it first is the same mistake one port up: the port costs access logic on every entry, not just one more pin.

    When does an array infer a RAM, and what breaks if the RAM is not transparent?

    • Weak answer: big arrays become RAMs, and a non-transparent RAM just returns old data, which is usually fine.

    • Strong answer: an array earns a macro with few ports, a synchronous read, and size above about a kilobit against the assumed 20000-unit macro block; an async read forces flops at any size, and here it saved exactly one word flop while barely moving the recorded area, as the table shows. If the macro is not transparent, a same-cycle same-address read returns stale data, so every read-after-write to the same address needs the bypass the next lesson proves.

    • Follow-up: the read register sits outside the port with RD_CLK_ENABLE 0. What do you run? The memory_dff step of the memory flow, which absorbs the register into the port and clocks it.

    Check yourself The address changes and the data must be valid in the same cycle. Which read style, and what does it cost?

    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.

    Check yourself A second write port is added to serve two writes per cycle. What does the recording charge for it?

    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.

    • memory_dff absorbs the read register into the port: RD_CLK_ENABLE moves from 0 to 1 with the clock connected.

    • Async read saves one flop and costs macro fit: most compiled macros offer sync read only.

    • A second write port adds priority muxing and decode, not storage; the recorded price is access logic.

    Rule of thumb: read the port parameters before the area: the attributes explain the structure the area totals.

    How this was checked

    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.

    • Interview: run a mock interview on this topic

    Related topics

    • Next: Byte Enables and Bypass Wrappers

    • SRAM and ROM

    • Synthesizable RTL and Inference

    Previous

    63 / 139

    Next: Byte Enables and Bypass Wrappers

    Up next

    Up nextByte Enables and Bypass Wrappers

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Compilers, Macros and Wrappers