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

  • How to Use This Course
  • The NanoSoC Tour
  • Reading Path
  • SoC Glossary10
  • SoC Metrics Reference
  • SoC Cheatsheet
  • What SoC Integration Is12
  • Architecture and Budgets21
  • Memory Maps and Address Decode26
  • Interconnect and Bus Fabrics26
  • Clock and Reset Architecture24
  • Power Architecture21
  • Interrupts, DMA and Peripherals15
  • Memories, Boot and Security21
  • IP Integration and Handoff21
  • Top-Level Verification and Bring-up21
  • Package, Pads and I/O18
  • Chip Signoff and Tapeout18
  • SoC Interview Prep16
  • Capstone: Integrate NanoSoC14
    • Capstone Brief
    • Stage 1: Memory Map and Interconnect
      • Map Table, Overlap and Alignment Check
      • Decode, Response Codes and the Alias
      • Link Table, Bandwidth and the Bottleneck
    • Stage 2: Clocks, Resets, Power
    • Stage 3: Interrupts and Boot
    • Stage 4: Verification and Signoff
Loading lesson

←Stage 1: Memory Map and Interconnect
  1. Topics
  2. SoC Design
  3. Capstone: Integrate NanoSoC
  4. Stage 1: Memory Map and Interconnect
  5. Link Table, Bandwidth and the Bottleneck
On this page

    Part 14 of 14, lesson 5 of 14Part 14 of 14/Capstone: Integrate NanoSoC/Lesson 5 of 14All levels

    Link Table, Bandwidth and the Bottleneck

    Recompute every per-link rate, check the DMA job time, and find the same-bank bottleneck.

    Why it matters

    The stream runs at half rate with no error anywhere: 180.7 MB/s per direction instead of 361.4. The fabric is innocent, the DMA is innocent, the code is innocent. Source and destination sit in the same SRAM bank, and one single-port macro serves reads and writes together, so each direction gets half the port. Link sizing that stops at peak rates never sees it; only sustained rates per shared resource do.

    Goal, inputs and steps

    • Goal: the per-link table recomputed, and the bottleneck of the DMA stream and of CPU register traffic named from the table.

    • Inputs: 32-bit links, 96 MHz fabric, burst efficiency L/(L+1), the 64-word job formula N+8.

    • Steps: recompute peak and sustained per row; check the job time; compute the stream rate and its bottleneck for two banks and for one bank, and confirm the halving.

    Link

    Peak (MB/s)

    Sustained (MB/s)

    Fabric per direction, 96 MHz

    384.0

    361.4 at burst 16

    Both SRAM banks together

    768.0

    722.8

    Same bank, per stream

    384.0 shared

    180.7 each

    Fast APB segment with bridge

    128.0 bus

    76.8

    Slow APB segment through cdc

    48.0 bus

    19.2

    CPU line fill, 24 clk_cpu

    768.0

    128.0

    The checker

    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
    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
    MAP = [
        ("BOOTROM", 0x00000000, 0x4000),
        ("SRAM0", 0x20000000, 0x10000),
        ("SRAM1", 0x20010000, 0x10000),
        ("DMA", 0x40000000, 0x1000),
        ("SBR", 0x40001000, 0x1000),
        ("UART", 0x40002000, 0x1000),
        ("SPI", 0x40003000, 0x1000),
        ("GPIO", 0x40004000, 0x1000),
        ("INTC", 0x40005000, 0x1000),
        ("RSTPWR", 0x40006000, 0x1000),
    ]
    SECURE_ONLY = {"SRAM1", "RSTPWR"}
    SLOTS = ["DMA", "SBR", "UART", "SPI", "GPIO", "INTC", "RSTPWR"]
    T_CPU, T_SYS, T_PER = 1e9 / 192e6, 1e9 / 96e6, 1e9 / 24e6
    def end_of(base, size):
        return base + size - 1
    def overlaps(entries=MAP):
        bad = []
        xs = sorted(entries, key=lambda e: e[1])
        for a, b in zip(xs, xs[1:]):
            if b[1] < a[1] + a[2]:
                bad.append((a[0], b[0]))
        return bad
    def misaligned(entries=MAP):
        return [n for n, b, s in entries if b % s != 0]
    def decode(addr, slots=SLOTS):
        if addr >> 14 == 0:
            return "BOOTROM"
        if addr >> 16 == 0x2000:
            return "SRAM0"
        if addr >> 16 == 0x2001:
            return "SRAM1"
        if addr >> 16 == 0x4000:
            slot = (addr >> 12) & 0xF
            return slots[slot] if slot < 7 else "H3"
        if addr < 0x20000000:
            return "H1"
        if addr < 0x40000000:
            return "H2"
        return "H4"
    def response(addr, secure, slots=SLOTS):
        t = decode(addr, slots)
        if t in ("H1", "H2", "H4"):
            return "DECERR"
        if t == "H3":
            return "SLVERR"
        if t in SECURE_ONLY and not secure:
            return "SLVERR"
        return "OK"
    def sustained(peak, beats):
        return peak * beats / (beats + 1)
    
    # ---- this lesson starts here ----
    
    FABRIC = sustained(384.0, 16)
    LINKS = {"cpu_fill": 16 / (24 * T_CPU) * 1e3,        # one 16-byte line per 24 clk_cpu
             "fabric": FABRIC, "sram_bank": FABRIC,       # per direction; one bank port
             "apb_fast": 4 * 96 / 5,                      # DMA and SBR registers, 3-clock access plus 2 bridge clocks
             "apb_slow": 4 / 208.33e-9 / 1e6}             # through nano_apb_cdc: 208.33 ns per access
    def stream_rate(src_bank, dst_bank):
        bank = LINKS["sram_bank"] / (2 if src_bank == dst_bank else 1)     # one single-port macro serves both streams
        path = {"sbr_read": LINKS["fabric"], "src_bank": bank, "dma_in": LINKS["fabric"],
                "dma_out": LINKS["fabric"], "dst_bank": bank, "sbr_write": LINKS["fabric"]}
        low = min(path.values())
        return low, sorted(n for n, v in path.items() if v - low < 0.05)
    assert abs(FABRIC - 361.4) < 0.05 and abs(LINKS["cpu_fill"] - 128.0) < 0.05
    assert abs(LINKS["apb_fast"] - 76.8) < 0.01 and abs(LINKS["apb_slow"] - 19.2) < 0.05
    assert abs(2 * FABRIC - 722.8) < 0.05
    assert abs((64 + 8) * T_SYS - 750.0) < 0.5
    rate, tied = stream_rate("SRAM0", "SRAM1")
    assert abs(rate - 361.4) < 0.05 and len(tied) == 6            # six links tie: the 32-bit 96 MHz stream is the ceiling
    rate, tied = stream_rate("SRAM0", "SRAM0")
    assert abs(rate - 180.7) < 0.05 and tied == ["dst_bank", "src_bank"]    # the shared bank port is the bottleneck
    def placement_ok(src, dst):
        return stream_rate(src, dst)[0] > 0.99 * FABRIC
    assert placement_ok("SRAM0", "SRAM1") and not placement_ok("SRAM1", "SRAM1")
    assert min(("apb_slow", LINKS["apb_slow"]), ("apb_fast", LINKS["apb_fast"]), key=lambda x: x[1])[0] == "apb_slow"
    print("PASS: links agree: 361.4 per stream, 180.7 same-bank (bank port is the bottleneck), 64-word job 750 ns")

    Expected output

    One PASS line with the three numbers the lab quotes: full-rate stream, same-bank rate, job time. The two stream_rate calls are the failing-state demonstration: with the buffers in different banks six links tie at 361.4, so the stream itself is the ceiling; in one bank only the bank port drops, to 180.7, and the function names it. For CPU register traffic the slowest link is the 19.2 MB/s clk_per segment.

    What each failure means

    A 361.4 failure means the efficiency used the wrong burst length or forgot the one bubble per burst. A 19.2 failure means the cdc-plus-bridge total missed a handshake cycle: it is 2 plus 2 peripheral cycles and 2 plus 2 system cycles, 208.33 ns. A 750 ns failure means the 8-cycle fill and drain was dropped. A bottleneck list of more than the bank means the placement was already split across banks.

    Hint ladder

    Hint 1: sustained is peak times beats over beats-plus-one; name the plus-one before computing. Hint 2: one single-port macro serves both directions, so divide, do not re-derive. Hint 3: the signature is a rate that is exactly half with no error flag anywhere, which a refusal would never show. Answer: put source in SRAM0 and destination in SRAM1, guard it with a buffer-placement rule checked at allocation, and budget 180.7 wherever sharing is unavoidable.

    Sizing the buffer plan by peak rates: 384 MB/s per bank looks like room for both streams, but source and destination in one bank get 180.7 MB/s each. Check bank placement, not only link width.

    If asked where a bandwidth number comes from, derive it aloud: width times clock, times efficiency with the burst length named. Then give the trap: same bank halves it, and nothing reports an error, so only the rate counters show it.

    Check yourself The CPU fill row shows 128 sustained against a 768 peak. Where did the factor of six go?

    A 4-beat line costs 24 cpu clocks for 16 bytes: the issue, two crossings, fabric address, memory latency and response path dominate the four data beats.

    Related topics

    • Back: Decode, Response Codes and the Alias

    • Stage 2: Clocks, Resets, Power

    • Protocol First Principles: Peak and Sustained Bandwidth

    Previous

    270 / 289

    Next: Stage 2: Clocks, Resets, Power

    Up next

    Up nextStage 2: Clocks, Resets, Power

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Map Table, Overlap and Alignment Check