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. Compilers, Macros and Wrappers
On this page

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

    Compilers, Macros and Wrappers

    Memory compilers, black-box macros, wrappers, and port arithmetic.

    A macro arrives as a black box with a timing model, and the first design that treats it as mappable logic fails at the boundary. The wrapper is where timing, behaviour, and macro-specific hooks are absorbed, and the menu decides the shape before any RTL is written.

    After this lesson you can: show the black-box boundary in hierarchy and stat, list what the wrapper holds, and select macros from a list with a checked rule.

    The question

    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.

    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
    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 register

    The 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

    Recorded wrapper logic for the two variants. The output-register bar disappears in the direct variant while the address muxing is unchanged; the macro itself is one black-box cell in both.

    What the wrapper holds

    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.

    Choosing from the compiler list

    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

    example.py
    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
    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.

    Break it: drop the output register

    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.

    Common mistakes

    Editing the netlist inside a macro boundary throws away the model the flow signs off against. The inside has no gates to edit, only a black box with a timing model: rerun the compiler for a new shape, or change the wrapper, and re-check the boundary assertion after either edit.

    What does synthesis do with a compiled macro, and what lives in the wrapper?

    • Weak answer: the tool synthesizes the macro with the rest of the logic and the wrapper is just the instantiation.

    • Strong answer: the tool treats the macro as a black box: the recording shows exactly one macro cell with the wrapper mapped around it, 32 output flops plus address muxing, and no logic inside. The wrapper holds the pipeline registers, the bypass mux, and the macro-specific power and test hooks, so the rest of the RTL never learns which compiler built the block.

    • Follow-up: the request is 3000 deep and the deepest macro is 1024. What do you instantiate? Three copies of the largest same-width macro, because tiling is the fallback when no single macro fits, and each copy multiplies pins and hooks.

    Check yourself The RTL needs 1024 rows of 32 bits from compiled macros. Which stand-in macro, and how many?

    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.

    Check yourself A 100-entry table of 8-bit values requests a macro instead of flops. What does the menu charge?

    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.

    • The compiler emits one block plus models per exact shape; synthesis connects the pins and never maps inside.

    • The wrapper holds pipeline registers, the bypass mux, and macro-specific hooks; registers change the wrapper, never the macro.

    • Take the smallest single macro that fits; tile the largest only when nothing fits.

    Rule of thumb: change the wrapper or rerun the compiler; never edit inside the black-box boundary.

    How this was checked

    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.

    • Interview: run a mock interview on this topic

    Related topics

    • Previous: Byte Enables and Bypass Wrappers

    • Caches

    • Retiming and Pipelining

    Previous

    65 / 139

    Next: Retiming and Pipelining

    Up next

    Up nextRetiming and Pipelining

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Flop Arrays or RAM Macros
    • Read and Write Port Styles