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

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
    • How to Use This Course
    • The DMA You Will Verify
    • Running UVM on EDA Playground
    • UVM 1.2 vs IEEE 1800.2 at a Glance
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Start Here
  1. Topics
  2. UVM Verification
  3. Start Here
  4. The DMA You Will Verify

Start here · Intermediate

The DMA You Will Verify

The course's running design read as a verification engineer reads a spec: register map, stream rules, the interrupt and the corner cases a testplan must cover.

On this page

    A designer hands you a one-page spec and says the block is "simple, maybe two weeks of verification". Three months later the escape that reaches silicon is in a sentence you read on day one: writes to LEN are ignored while the engine is busy. Nobody wrote a test that wrote LEN mid-job, so nobody noticed that the scoreboard believed the new value while the hardware kept the old one.

    Every lesson in this course verifies one design, a small stream DMA. Before you write any UVM, this page reads its spec the way a verification engineer does: not as a description of what the block does, but as a list of promises, each of which needs a check and a way to prove it was exercised. Which promises would you have missed?

    Read a spec as a list of promises

    A designer reads a spec to know what to build. A verification engineer reads it to know what can go wrong, and the useful mental model is a contract with fine print. The headline ("moves LEN words and XORs them with KEY") is the part everyone tests. The fine print (what happens when the packet is short, when software writes mid-job, when the output stalls) is where bugs live, because the designer handled those cases last and the testbench author thought about them least.

    So for every sentence of the spec, ask three questions. What stimulus makes it happen? What check proves it happened correctly? What coverage proves the stimulus really occurred? Those three questions are the rows of a testplan, the document that defines when verification is done. The stimulus, check and coverage triangle turns them into UVM parts; this page produces the list.

    The picture: one block, three kinds of traffic

    Block diagram of the DMA: APB register block, input stream into an XOR with KEY, one output register, output stream with backpressure, and an interrupt derived from IRQ_EN, DONE and ERR.
    dma_dut. Software programs the registers over APB; words flow from the input stream through the XOR and a single output register to the output stream. Dotted arrows are backpressure. The interrupt is a level, not a pulse.

    The design has three interfaces, and each one is a different kind of traffic. APB is a request-response register bus with wait states. AXI-Stream is a valid/ready handshake where either side can stall. The interrupt is a level that stays high until software clears its cause. Almost every UVM topic, from reactive agents to the register layer, has a natural home in one of the three.

    The register map, with the fine print

    Offset

    Register

    Fields

    Access and fine print

    0x00

    CTRL

    bit 0 GO, bit 1 IRQ_EN

    GO starts a job and reads back 0. GO is ignored while BUSY, but IRQ_EN still takes the write.

    0x04

    STATUS

    bit 0 BUSY, bit 1 DONE, bit 2 ERR

    BUSY is read-only. DONE and ERR are write 1 to clear (W1C): writing 0 leaves them alone.

    0x08

    LEN

    bits 15:0

    Words per job. Writes are ignored while BUSY.

    0x0C

    KEY

    bits 31:0

    XOR key. Writes are ignored while BUSY.

    0x10

    COUNT

    bits 15:0

    Words moved in the current or last job. Read-only; GO resets it.

    other

    none

    Any other address completes with PSLVERR = 1 and reads 0.

    All registers reset to 0. Every APB access takes one wait state: the slave holds PREADY low for the first access cycle. A driver that assumes zero wait states, or a monitor that samples PRDATA on the first access cycle, fails on this design immediately, which is exactly why it has the wait state.

    How a job runs

    The stream side

    • After GO with LEN = N, the DMA takes N words from the input stream and sends each one out as word ^ KEY, with TLAST on the N-th output word. It does not need TLAST on the input to finish: it counts.

    • There is one output register. The input is ready only when the DMA is busy and that register is empty or being emptied this cycle (s_tready = busy && (!m_tvalid || m_tready)). Stall the output, and the input stalls as soon as that one register is full.

    • An input word with TLAST before the N-th word ends the job early: that word goes out with TLAST, BUSY drops, and ERR is set instead of DONE.

    • If the input packet is longer than LEN, the DMA takes N words, sets DONE and drops TREADY. The extra words are never accepted, so a stimulus driver that waits for TREADY on them waits forever.

    • LEN = 0 is legal: GO sets DONE at once, BUSY never rises, and no word moves.

    The interrupt

    irq = IRQ_EN && (DONE || ERR). It is a level: it rises when a job ends with IRQ_EN set and stays high until software clears DONE or ERR, or clears IRQ_EN. Two consequences catch people. GO clears DONE (for a non-zero LEN) but does not clear ERR, so an uncleared error from the previous job keeps the interrupt high the moment the next job enables it. And because the register block writes STATUS before it updates DONE in the same clock, a W1C of DONE in the very cycle a job finishes loses: set wins over clear.

    The corner cases your testplan must cover

    Promise

    Stimulus

    Check

    Data is word ^ KEY, in order

    Random KEY, LEN and data

    Scoreboard predicts from observed input and KEY

    LEN = 0 finishes at once

    GO with LEN = 0

    DONE set, no output word, IRQ if enabled

    Early TLAST ends the job with ERR

    Packet shorter than LEN

    ERR set, DONE clear, COUNT = words taken, last word has TLAST

    Long packets do not over-run

    Packet longer than LEN

    Exactly LEN words out; stimulus does not hang

    Backpressure loses nothing

    Random TREADY low on the output

    Same words, same order; input stalls

    DONE and ERR are W1C

    Write 0, then 1, to each bit

    0 leaves the bit, 1 clears it, BUSY ignores writes

    Set beats clear

    W1C DONE in the finishing cycle

    DONE stays set

    KEY and LEN ignored while busy

    Write them mid-job

    Job uses the old values; the scoreboard must too

    GO ignored while busy

    Second GO mid-job

    No restart; COUNT keeps counting

    ERR survives GO

    Error job, no clear, next job

    IRQ high immediately; test must clear first

    Unmapped addresses error

    Access 0x14 and up

    PSLVERR = 1, read data 0, no register changes

    Reset in the middle of a job abandons it cleanly

    Assert rst_n while words are moving, then run a full job

    BUSY, DONE, ERR, COUNT, LEN, KEY and IRQ_EN read 0; no output beat after reset; the abandoned words are never compared; the next job is correct

    Look at the "ignored while busy" row again, because it is the hook's bug. The course's base scoreboard learns KEY from every KEY write the APB monitor sees. That is correct for every test in the bench today, because they only write KEY between jobs. A test that writes KEY mid-job would expose a gap in the model, not in the DUT: the scoreboard needs to know BUSY, just as the hardware does. You will meet this again when the course builds a register model, which is the standard way to keep that knowledge in one place.

    The design source

    The whole design is about 130 lines of plain SystemVerilog. Read it once now with the table above beside you: each row maps to two or three lines, and finding them is the fastest way to learn the block.

    dma_dut.sv
    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
    132
    133
    134
    // dma_dut.sv - the course's running example: a small stream DMA.
    //
    // APB registers (32-bit, word addressed, byte offsets):
    //   0x00 CTRL    [0] GO (write 1 to start, reads 0)   [1] IRQ_EN
    //   0x04 STATUS  [0] BUSY (RO)  [1] DONE (W1C)  [2] ERR (W1C)
    //   0x08 LEN     [15:0] words to move (RW)
    //   0x0C KEY     [31:0] every word is XORed with KEY on the way through (RW)
    //   0x10 COUNT   [15:0] words moved in the current or last job (RO)
    // Any other address answers with PSLVERR = 1. Every access takes one wait state
    // when WAIT_STATES = 1 (PREADY low for one access cycle).
    //
    // Data: after GO, LEN words are taken from the s_axis input and sent to the
    // m_axis output as (word ^ KEY), with m_tlast on the last one. An input word
    // with s_tlast before the LEN-th word ends the job early with STATUS.ERR.
    // irq = IRQ_EN & (DONE | ERR).
    module dma_dut #(parameter bit WAIT_STATES = 1) (
      input  logic        clk,
      input  logic        rst_n,
      // APB
      input  logic [7:0]  paddr,
      input  logic        psel,
      input  logic        penable,
      input  logic        pwrite,
      input  logic [31:0] pwdata,
      output logic [31:0] prdata,
      output logic        pready,
      output logic        pslverr,
      // AXI-Stream in
      input  logic [31:0] s_tdata,
      input  logic        s_tvalid,
      input  logic        s_tlast,
      output logic        s_tready,
      // AXI-Stream out
      output logic [31:0] m_tdata,
      output logic        m_tvalid,
      output logic        m_tlast,
      input  logic        m_tready,
      // Interrupt
      output logic        irq
    );
      logic        irq_en, busy, done, err;
      logic [15:0] len, count;
      logic [31:0] key;
    
      // ---------------- APB ----------------
      logic waited;                       // one wait state per access
      wire  access = psel && penable;
      assign pready = !WAIT_STATES || waited;
      wire  xfer   = access && pready;    // the cycle the access completes
    
      wire mapped = (paddr == 8'h00) || (paddr == 8'h04) || (paddr == 8'h08) ||
                    (paddr == 8'h0C) || (paddr == 8'h10);
      assign pslverr = xfer && !mapped;
    
      always_comb begin
        unique case (paddr)
          8'h00:   prdata = {30'b0, irq_en, 1'b0};
          8'h04:   prdata = {29'b0, err, done, busy};
          8'h08:   prdata = {16'b0, len};
          8'h0C:   prdata = key;
          8'h10:   prdata = {16'b0, count};
          default: prdata = 32'h0;
        endcase
      end
    
      wire go = xfer && pwrite && (paddr == 8'h00) && pwdata[0] && !busy;
    
      // ---------------- data path ----------------
      // One output register: the input is accepted when the output slot is free
      // or is being emptied this cycle.
      wire out_free = !m_tvalid || m_tready;
      assign s_tready = busy && out_free;
      wire take = s_tvalid && s_tready;
      wire last_word = (count + 16'd1 == len);
    
      always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
          waited   <= 1'b0;
          irq_en   <= 1'b0;
          busy     <= 1'b0;
          done     <= 1'b0;
          err      <= 1'b0;
          len      <= 16'd0;
          key      <= 32'd0;
          count    <= 16'd0;
          m_tvalid <= 1'b0;
          m_tdata  <= 32'd0;
          m_tlast  <= 1'b0;
        end else begin
          waited <= access && !waited;
    
          if (xfer && pwrite) begin
            unique case (paddr)
              8'h00: irq_en <= pwdata[1];
              8'h04: begin                       // write 1 to clear
                       if (pwdata[1]) done <= 1'b0;
                       if (pwdata[2]) err  <= 1'b0;
                     end
              8'h08: if (!busy) len <= pwdata[15:0];
              8'h0C: if (!busy) key <= pwdata;
              default: ;
            endcase
          end
    
          if (go) begin
            busy  <= (len != 16'd0);
            done  <= (len == 16'd0);             // a zero-length job is done at once
            count <= 16'd0;
          end
    
          if (m_tvalid && m_tready) m_tvalid <= 1'b0;
    
          if (take) begin
    `ifdef DMA_BUG_WORD6
            m_tdata  <= s_tdata ^ (count == 16'd5 ? ~key : key);   // planted bug for the mismatch playbook
    `else
            m_tdata  <= s_tdata ^ key;
    `endif
            m_tvalid <= 1'b1;
            m_tlast  <= last_word || s_tlast;
            count    <= count + 16'd1;
            if (last_word) begin
              busy <= 1'b0;
              done <= 1'b1;
            end else if (s_tlast) begin           // input packet ended early
              busy <= 1'b0;
              err  <= 1'b1;
            end
          end
        end
      end
    
      assign irq = irq_en && (done || err);
    endmodule

    Watch it run: two promises, two logs

    The bench exercises the headline promise in dma_smoke_test and the early-TLAST promise in dma_truncate_test, which swaps in a driver that forces TLAST on beat 3 of an 8-word packet. From the real runs (Verilator with the open-source UVM library; times in ps):

    Diagram
    +UVM_TESTNAME=dma_smoke_test
    UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
    
    +UVM_TESTNAME=dma_truncate_test
    UVM_INFO dma_tb.sv(520) @ 275000: uvm_test_top.env.in_agt.drv [TRUNC] TLAST forced on beat 3
    UVM_INFO dma_tb.sv(761) @ 385000: uvm_test_top [TRUNC] DMA reported ERR for the short packet, as expected
    UVM_INFO dma_tb.sv(411) @ 485000: uvm_test_top.env.sb [SB] 4 words matched, 0 mismatched
    • 8 words matched in the smoke test: every output word was predicted and compared.

    • 4 words matched in the truncate test: beats 0 to 3 went in (beat 3 carrying TLAST), the DMA stopped there, and the scoreboard saw exactly those four come out. The other four beats were dropped by the driver, so they were never predicted either.

    • The ERR line comes from the test reading STATUS after the interrupt. The data check and the status check are separate, and both are needed.

    What breaks: the interrupt that fired too early

    This design invites a specific bug, and it is worth walking through before you meet it in a regression. Suppose a test runs the truncate scenario, reads STATUS, sees ERR, and moves straight on to a normal job without writing 1 to ERR. The interrupt line never went low: IRQ_EN is still set from the last CTRL write and ERR is still set. The next job writes KEY, LEN and GO, and the test's wait (irq) returns at once, it reads STATUS = BUSY | ERR, and it reports a failure on a job that has not even started. Worse, if the test only checks DONE, it may drop its objection while the job is still running, and the scoreboard's leftover check reports words that "never came out".

    The root cause is not in the DUT. It behaves exactly as specified: GO clears DONE and leaves ERR alone. The bug is a test that treated the interrupt as an edge ("something just finished") when the spec defines it as a level ("something is waiting to be cleared"). The bench's run_job task avoids it by always writing 0x6 to STATUS after each job, clearing both bits. When you write your own tests, clear before you start, and make your interrupt handling check why the line is high.

    Interview

    "Here is a one-page spec. What would you verify first?" (often asked with a block much like this one)

    A weak answer starts with infrastructure: "I'd build an APB agent, an AXI-Stream agent and a scoreboard." That is the how, not the what.

    A strong answer starts with risk: "The data path is easy to check with a predicting scoreboard, so I'd get that running first as a smoke test. Then I'd list the fine print: what happens on a short packet, on writes while busy, on backpressure, on W1C racing a set, and on an uncleared error. Each becomes a testplan row with a check and a coverage point. I'd prioritise the ones where the scoreboard's model could silently disagree with the hardware, like KEY written mid-job."

    Expect the follow-ups: How do you know your testplan is complete? (walk every spec sentence and every register field; review it with the designer) and Which of these would you leave to formal? (W1C and set-beats-clear are small, local properties that suit assertions or formal well).

    Check yourself A test sets LEN = 4, starts a job, and sends a 6-word packet with TLAST on the sixth word. What happens to words 5 and 6?

    The DMA takes words 1 to 4, puts TLAST on output word 4 (it counts, it does not wait for input TLAST), sets DONE and drops TREADY. Words 5 and 6 are never accepted. A driver that waits for TREADY on word 5 waits forever, so the test hangs unless something else ends it, such as a timeout.

    Check yourself Software writes STATUS = 0x2 in the same clock cycle that the last word of a job is taken. Is DONE set or clear afterwards?

    Set. In the RTL the STATUS write is handled first and the end-of-job update later in the same always_ff block, so the later assignment (DONE = 1) wins. Set beats clear. A test that wants a clean STATUS must clear after the job has finished, and a testplan should have a row that forces this collision on purpose.

    Check yourself Why does the testplan need both a scoreboard check and a STATUS read for the early-TLAST case?

    They check different promises. The scoreboard proves the words that did move were correct and that no extra words appeared. The STATUS read proves the DMA knew the job ended early (ERR set, DONE clear). A DUT that moved the right four words but reported DONE would pass the scoreboard and fail the spec.

    Next: you know what the design should do. Running UVM on EDA Playground shows how to run its testbench yourself, for free, and how to read what comes back.

    Related topics

    • Up: Start here

    • Running UVM on EDA Playground

    • Your first UVM testbench

    • Stimulus, check and coverage

    Previous

    2 / 280

    Next: Running UVM on EDA Playground

    Up next

    Up nextRunning UVM on EDA Playground

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • UVM 1.2 vs IEEE 1800.2 at a Glance