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
    • Stage 2: Clocks, Resets, Power
    • Stage 3: Interrupts and Boot
      • Interrupt Map and Latency Budget
      • DMA Programming Sequence and Contract Traps
      • Boot Flow and Secure Partition
    • Stage 4: Verification and Signoff
Loading lesson

←Stage 3: Interrupts and Boot
  1. Topics
  2. SoC Design
  3. Capstone: Integrate NanoSoC
  4. Stage 3: Interrupts and Boot
  5. DMA Programming Sequence and Contract Traps
On this page

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

    DMA Programming Sequence and Contract Traps

    Write the driver sequence that survives length mismatch and every status-register trap, catching bugs 8 and 9.

    Why it matters

    Two jobs fail with no error flag to blame. With SBR LEN at 32 and DMA LEN at 64 the DMA sets ERR at word 32 with COUNT at 32; reversed, the DMA finishes at 32 words while the SBR waits forever. And a second job silently reuses the old KEY, or irq stays high across a good job, or enabling the interrupt disables it. The DMA and SBR share no length register, ignore writes while BUSY, keep a sticky ERR, and rewrite IRQ_EN on every CTRL write. The driver must carry what the hardware does not.

    Goal, inputs and steps

    • Goal: an ordered driver sequence, DMA first and the SBR START last, plus the post-job check that proves both LENs agreed.

    • Inputs: DMA offsets 0x00 to 0x10, SBR SRC/DST/LEN/CTRL, the BUSY and W1C rules.

    • Steps: write the sequence; run both mismatch cases on the DMA model and confirm the driver check names them; run a naive driver and a careful driver against the three traps and confirm only the careful one is clean; file the sequence.

    The core idea

    The sequence: wait for BUSY = 0 on both blocks; write KEY, then LEN, then CTRL = 0x3 (GO with IRQ_EN) to the DMA, so it is BUSY and ready to accept; then write SRC, DST and the same n_words to the SBR and set CTRL.START last; on the interrupt read STATUS, demand COUNT == SBR LEN, then clear both DONE and ERR with 0x6. That is seven writes in the order the boot self-test uses. If the SBR started first, its first word would meet a DMA whose s_tready is still low, and the stream would wait for a GO that software might never send. Case A of bug 8, SBR shorter, ends the job early with ERR set by the real early-tlast behaviour; case B, DMA shorter, leaves the SBR BUSY because the DMA stops accepting. Bug 9 has three traps: LEN and KEY writes while BUSY are silently ignored, GO clears DONE but not ERR so a stale ERR holds irq high, and any CTRL write copies bit 1 into IRQ_EN so 0x1 disables the interrupt. One variable, n_words, is written to both LENs, and the driver never touches LEN or KEY while BUSY.

    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
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    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 ----
    
    class Dma:                                  # the register behaviour of the DMA, one rule per line
        def __init__(self):
            self.busy = self.done = self.err = self.irq_en = False
            self.key = self.len = self.count = self.stalls = 0
        def irq(self):
            return self.irq_en and (self.done or self.err)             # a level, held until software clears it
        def write_key(self, v):
            self.key = self.key if self.busy else v                    # ignored while BUSY, no PSLVERR
        def write_len(self, v):
            self.len = self.len if self.busy else v & 0xFFFF           # ignored while BUSY
        def ctrl_write(self, v):
            self.irq_en = bool(v & 2)                                  # every CTRL write copies bit 1 into IRQ_EN
            if v & 1 and not self.busy:                                # GO while BUSY is ignored
                if self.len == 0:
                    self.done = True
                else:
                    self.busy, self.done, self.count = True, False, 0  # GO clears DONE but not ERR
        def status_write(self, v):
            self.done = self.done and not v & 2                        # W1C
            self.err = self.err and not v & 4
        def accept(self, last):                                        # one stream word offered
            if not self.busy:
                self.stalls += 1                                       # s_tready is low when not BUSY
                return False
            self.count += 1
            if last and self.count < self.len:
                self.err, self.busy = True, False                      # early tlast: ERR, never DONE
            elif self.count == self.len:
                self.done, self.busy = True, False
            return True
    def stream(dma, n):                                                # the SBR read engine; True means it is stuck BUSY
        return not all(dma.accept(i == n - 1) for i in range(n))
    def job(n_dma, n_sbr, key=0x5A):
        d = Dma()
        d.write_key(key)
        d.write_len(n_dma)
        d.ctrl_write(0x3)                                              # DMA first
        return d, stream(d, n_sbr)                                     # SBR START last
    def driver_ok(count, sbr_len, dma_len):
        return count == sbr_len == dma_len and dma_len > 0
    ok, stuck = job(64, 64)
    assert ok.done and not ok.err and not stuck and ok.stalls == 0 and driver_ok(ok.count, 64, 64)
    a, a_stuck = job(64, 32)                                           # case A: SBR LEN 32, DMA LEN 64
    assert a.err and not a.done and a.count == 32 and not a_stuck and not driver_ok(a.count, 32, 64)
    b, b_stuck = job(32, 64)                                           # case B: SBR LEN 64, DMA LEN 32
    assert b.done and not b.err and b.count == 32 and b_stuck and not driver_ok(b.count, 64, 32)
    early = Dma()
    assert early.accept(False) is False and early.stalls == 1          # a word offered before GO is refused
    class Good:
        def start(self, d): d.ctrl_write(0x3)
        def rekey(self, d, key, finish):
            if d.busy:
                finish(d)                                              # wait for BUSY = 0 first
            d.write_key(key)
        def clear(self, d): d.status_write(0x6)                        # DONE and ERR together
    class Naive:
        def start(self, d): d.ctrl_write(0x1)
        def rekey(self, d, key, finish): d.write_key(key)
        def clear(self, d): d.status_write(0x2)
    def contract_problems(drv):
        found = []
        a = Dma(); a.write_key(0xAA); a.write_len(4); a.ctrl_write(0x3); a.accept(False)     # a job is in flight
        drv.rekey(a, 0x55, lambda x: [x.accept(i == 2) for i in range(3)])
        if a.key != 0x55:
            found.append("stale_key")
        b = Dma(); b.write_len(8); b.ctrl_write(0x3); b.accept(True)                         # a failed job leaves ERR set
        drv.clear(b)
        if b.irq():
            found.append("irq_stuck")
        c = Dma(); c.write_len(1); drv.start(c)
        if not c.irq_en:
            found.append("irq_disabled")
        return found
    assert contract_problems(Good()) == []
    assert contract_problems(Naive()) == ["stale_key", "irq_stuck", "irq_disabled"]
    print("PASS: length mismatch named both ways; careful driver clean, naive driver trips all 3 traps")

    Expected output

    One PASS line. The two job calls with unequal lengths are the bug-8 demonstration, and driver_ok is the post-job check that refuses both. The naive driver is the bug-9 demonstration: it rewrites a key while BUSY, clears only DONE after a failed job, and starts with 0x1, and the scenario reports exactly the three symptoms.

    What each failure means

    A case-A failure means the early-tlast ERR behaviour was modelled as DONE: the real RTL sets ERR, never DONE, on an early last word. A case-B failure means the model let the SBR finish without the DMA accepting: s_tready is 0 when not BUSY, so the read engine stalls. A clean report from the naive driver means a trap was coded as a refusal that the RTL does not make: the writes are ignored silently, not rejected.

    Hint ladder

    Hint 1: the two LEN registers are separate fields in separate blocks; nothing ties them. Hint 2: read the STATUS table row for what GO clears and what it does not. Hint 3: bit 1 of every CTRL write becomes the new IRQ_EN, including pure GO writes. Answer: one n_words variable to both LENs, wait BUSY = 0 before LEN or KEY, clear DONE and ERR together with 0x6, always start with 0x3, and check COUNT against SBR LEN after DONE.

    GO while BUSY is ignored with no error, so a double-start looks like a lost job rather than a refused one. The driver gates every start on BUSY = 0 instead of retrying blindly.

    If asked why the driver checks COUNT after DONE, answer with case B: DONE from the DMA alone proves the DMA's length, not the transfer's length, and only COUNT against SBR LEN proves both.

    Check yourself A zero-length GO sets DONE at once with BUSY never rising. Which driver step does that threaten?

    The COUNT check: COUNT stays 0, which equals a zero SBR LEN, so the check passes vacuously and only an explicit non-zero-length rule keeps the job meaningful.

    Related topics

    • Back: Interrupt Map and Latency Budget

    • Next: Boot Flow and Secure Partition

    • UVM: The DMA You Will Verify

    Previous

    275 / 289

    Next: Boot Flow and Secure Partition

    Up next

    Up nextBoot Flow and Secure Partition

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error