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
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
    • Agent Architecture
      • What an Agent Owns, and What It Shows the World
      • Agent Configuration: Knobs, Config Objects and Who Owns Them
      • Mode-Gated Build: One Agent Class, Active or Passive
      • The Driver: Owning the Pins, One Transfer at a Time
      • The Monitor: Watch the Pins, Publish a Fresh Transaction
      • The Sequencer: The Agent's Stimulus Socket
      • Monitor Implementation Detail: Sampling, Races and X
      • Interface Abstraction: Virtual Interfaces vs Abstract BFMs
    • Reusable VIP: Active, Passive and Protocol Agents
    • Error and Fault Injection
    • Agent Patterns: Slaves, Reset, Interrupts and Pipelines
  • 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

‹Agent Architecture
  1. Topics
  2. UVM Verification
  3. UVM Agents & Protocol VIP
  4. Agent Architecture
  5. The Driver: Owning the Pins, One Transfer at a Time

Part 6 · Agents & Protocol IP · Intermediate

The Driver: Owning the Pins, One Transfer at a Time

A protocol-correct APB driver edge by edge: setup and access phases, wait states, reset parking, the stream driver's hook, and the write that vanished on a zero-wait slave.

On this page

    An APB driver had passed every regression for a year, against a register block that always took one wait state. Then the design team removed the wait state. Overnight, a third of register writes stopped having any effect. The driver's log said every write was sent. The DUT's log said it never saw them. Nobody had changed the driver.

    What exactly does a driver owe the bus, and how can a driver that "sends" every write still lose some?

    The idea: an interpreter who speaks at the listener's pace

    A driver is the only component allowed to change the DUT's input pins. It pulls one transaction at a time from the sequencer and turns it into signal changes at the right clock edges, waiting for the DUT's handshakes. Think of a simultaneous interpreter: the sentence (the transaction) is fixed, but when each word is spoken depends on the listener, and a good interpreter waits until the listener has caught up before starting the next sentence.

    A driver owns timing, nothing else. It does not decide what to send (that is the sequence), does not check what comes back (that is the scoreboard) and does not report what happened on the bus (that is the monitor). Two timing rules from the bench keep it out of trouble. It waits on @(posedge vif.clk), the same event every other bench thread uses. And it drives with nonblocking assignments (<=), so its changes land after the edge, exactly like a flip-flop output, and a value it reads right after the edge is the value from before it.

    The picture: one APB write, edge by edge

    • Setup phase (edge 1 to 2): PSEL=1, PENABLE=0, with address, direction and write data stable. It always lasts exactly one clock.

    • Access phase (edge 2 onward): PENABLE=1, everything else held, until the slave answers PREADY=1. The DMA adds one wait state, so this access takes two clocks.

    • Completion (edge 4): the transfer happens on the edge where PSEL, PENABLE and PREADY are all 1. Read data (PRDATA) and PSLVERR are valid in that same cycle, so that is where the driver captures them and where the monitor samples.

    Build it: the bench's APB driver

    design.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
    // snippet: from the course DMA bench
    class apb_driver extends uvm_driver #(apb_item);
      `uvm_component_utils(apb_driver)
      virtual apb_if vif;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
          `uvm_fatal("NOVIF", "apb_if not set for this driver")
      endfunction
    
      task run_phase(uvm_phase phase);
        vif.psel    <= 1'b0;
        vif.penable <= 1'b0;
        @(posedge vif.rst_n);
        forever begin
          seq_item_port.get_next_item(req);
          drive(req);
          seq_item_port.item_done();     // rdata travels back inside req itself
        end
      endtask
    
      // Setup phase: PSEL=1, PENABLE=0 for one clock. Access phase: PENABLE=1, held
      // until the slave answers PREADY=1; read data is valid in that cycle.
      // Every wait is on the same event, @(posedge vif.clk), and every drive is a
      // nonblocking assignment, so a value read right after the edge is the value
      // from before it: the one the DUT sampled.
      task drive(apb_item it);
        @(posedge vif.clk);
        vif.paddr   <= it.addr;
        vif.pwrite  <= it.write;
        vif.pwdata  <= it.data;
        vif.psel    <= 1'b1;
        vif.penable <= 1'b0;
        @(posedge vif.clk);
        vif.penable <= 1'b1;
        do @(posedge vif.clk); while (!vif.pready);
        it.rdata  = vif.prdata;
        it.slverr = vif.pslverr;
        vif.psel    <= 1'b0;
        vif.penable <= 1'b0;
      endtask
    endclass

    Before the loop: park the bus and wait out reset

    The run_phase first drives PSEL and PENABLE low and waits for rst_n to rise. A driver that starts pulling items at time 0 would drive a transfer into a DUT still in reset, where it is silently dropped. Parking the strobes first also means the bus is never X when reset releases.

    The loop: one item, one transfer, one item_done

    get_next_item(req) blocks until a sequence offers an item; drive(req) performs the transfer; item_done() tells the sequence it is finished. There is no argument to item_done: read data goes back inside req, which the sequence still holds. The handshake itself, from both sides, is the subject of the driver handshake lesson; here the interest is what happens between the two calls.

    The protocol task: one wait per phase

    drive() maps line for line onto the waveform. @(posedge vif.clk) then the setup values. @(posedge vif.clk) then PENABLE. Then do @(posedge vif.clk); while (!vif.pready);: wait at least one edge, then keep waiting while PREADY was low before that edge. When the loop exits, the transfer has completed on the edge just passed, so the task captures PRDATA and PSLVERR and parks the strobes. Every assignment to a pin is nonblocking; the two captures into it are blocking because it is a class variable, not a pin.

    The stream driver: a handshake, an idle gap and a hook

    design.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
    // snippet: from the course DMA bench
    class axis_driver extends uvm_driver #(axis_item);
      `uvm_component_utils(axis_driver)
      virtual axis_if vif;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(virtual axis_if)::get(this, "", "vif", vif))
          `uvm_fatal("NOVIF", "axis_if not set for this driver")
      endfunction
    
      task run_phase(uvm_phase phase);
        vif.tvalid <= 1'b0;
        @(posedge vif.rst_n);
        forever begin
          axis_item beat;
          seq_item_port.get_next_item(req);
          beat = pre_drive(req);
          if (beat != null) begin
            repeat (beat.idle) @(posedge vif.clk);
            @(posedge vif.clk);
            vif.tdata  <= beat.data;
            vif.tlast  <= beat.last;
            vif.tvalid <= 1'b1;
            do @(posedge vif.clk); while (!vif.tready);   // the beat moves on TVALID && TREADY
            vif.tvalid <= 1'b0;
          end
          seq_item_port.item_done();
        end
      endtask
    
      // Hook for derived drivers: return the beat to drive (the sequence's item, or a
      // modified copy), or null to drop it. The base driver drives every item as is.
      virtual function axis_item pre_drive(axis_item it);
        return it;
      endfunction
    endclass

    Same shape, different protocol. The item carries an idle count, so the driver inserts that many clocks of TVALID=0 before the beat: randomized gaps are stimulus, so they come from the item, not from the driver. Then it holds TVALID with the data until it has seen TREADY on an edge, which is the AXI-Stream rule that a beat moves on TVALID and TREADY together.

    The pre_drive function is the driver's extension point. It is virtual and returns the item to drive, a modified copy, or null to drop the beat. The base version returns the item unchanged. dma_truncate_test swaps in axis_truncating_driver with a factory override, and that class's pre_drive clones the item and forces TLAST early, without editing a line of the base driver. Error injection through hooks like this is the subject of the error injection topic.

    What these drivers deliberately leave out

    Neither driver survives a reset in the middle of a transfer: if rst_n falls during the access phase, drive() keeps waiting for a PREADY that a reset DUT may never give. The bench's reset-aware variants (apb_driver_ra, axis_driver_ra) race the item loop against @(negedge rst_n) and call item_done for the item they hold; the agent patterns topic builds them, and Stopping sequences and reset covers the sequence side. Drivers that keep several transfers in flight are in Responses and pipelined drivers. And neither driver raises objections: end of test belongs to the test, or to a scoreboard that knows what is still owed.

    Watch it run

    dma_truncate_test shows both drivers and the hook at work. The APB driver programs the DMA for an 8-word job, the stream driver sends the packet, and the overriding driver's pre_drive cuts it short by forcing TLAST on beat 3 (counting from 0):

    Diagram
    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
    UVM_ERROR :    0
    • The reporter is `env.in_agt.drv` even though the class is now axis_truncating_driver: the factory replaced the object, not its place in the tree.

    • 4 words matched because the scoreboard predicts from what the input monitor saw (four beats, the last with TLAST), not from what the sequence intended. The monitor does not care which driver class produced the pins.

    What breaks: the write that vanished

    Back to the year-old driver. Its access phase was written like this:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // bad-example: drops the access phase when PREADY is already high
    task drive(apb_item it);
      @(posedge vif.clk);
      vif.paddr <= it.addr;  vif.pwrite <= it.write;  vif.pwdata <= it.data;
      vif.psel  <= 1'b1;     vif.penable <= 1'b0;
      @(posedge vif.clk);
      vif.penable <= 1'b1;
      while (!vif.pready) @(posedge vif.clk);   // tests PREADY before any edge has passed
      vif.psel    <= 1'b0;
      vif.penable <= 1'b0;
    endtask

    With a wait-state slave PREADY is low when the while first tests it, so the loop waits an edge and all is well. The new slave drove PREADY high whenever it was idle. Now the while test passed immediately, in the same timestep as vif.penable <= 1'b1. The next two lines scheduled PSEL and PENABLE low in that same timestep, and with nonblocking assignments the last one scheduled wins. PENABLE never rose on the bus. The DUT saw a setup phase followed by idle, which is no transfer at all, and the driver reported success.

    The fix is the bench's do @(posedge vif.clk); while (!vif.pready);. The body runs before the test, so the access phase is always on the bus for at least one edge, and the PREADY tested is the value from before that edge. It is the APB rule in code: the access phase lasts at least one clock, and the transfer completes on an edge where PREADY is high. Protocol assertions in the interface would have caught it on the first run: "PSEL in setup must be followed by PENABLE" fails immediately. That is one argument for the checks covered in Sampling, races and X.

    Interview

    "Write me an APB driver. What's the bug people usually make?"

    A weak answer writes the signals in the right order and stops. It usually tests PREADY before an edge, captures PRDATA in the wrong cycle, or forgets to park the bus.

    A strong answer narrates the phases: "Wait for reset with the strobes parked. Per item: on an edge drive setup, PSEL=1, PENABLE=0; on the next edge raise PENABLE; then do-wait-while: wait at least one edge and repeat while PREADY was low. The transfer completed on that edge, so capture PRDATA and PSLVERR into the request, park PSEL and PENABLE, and call item_done with no argument. Every pin assignment is nonblocking right after posedge clk, so what I read after the edge is what the DUT sampled. The classic bug is testing PREADY with a while loop before any edge, which drops the access phase on a zero-wait slave."

    Expect the follow-ups: How do you handle reset in the middle of the access phase? (race the transfer against the reset edge in an isolated fork, park the bus, and still call item_done) and Where does read data go back to the sequence? (inside req, read after finish_item; no response object needed).

    Check yourself The driver captures it.rdata = vif.prdata; right after the do-while loop exits. The DMA's PRDATA is combinational from the address. Is the captured value the one from the completing cycle?

    Yes. The loop exited right after the completing edge, in the same timestep, before any nonblocking update scheduled at that edge has taken effect. PSEL, PENABLE and the address are still the values from the access cycle, so PRDATA is still the read data for this transfer.

    Check yourself A colleague proposes moving the idle gap out of axis_item and into the driver as a fixed config knob, "because timing is the driver's job". What does the bench lose?

    Randomized, per-beat gaps that a sequence can constrain. Timing execution is the driver's job; timing choices are stimulus. With the gap in the item, a test can ask for back-to-back beats, long stalls or a mix through constraints, and the driver stays the same.

    Check yourself Should the APB driver raise an objection while it holds an item, so the test cannot end mid-transfer?

    Usually not. The test already holds an objection while its sequences run, and a sequence's finish_item does not return until item_done, so the transfer completes before the sequence can let the test end. Per-item driver objections add overhead to every transfer and hide the real owner of end of test. Some teams do use them; a scoreboard that holds the phase through phase_ready_to_end until outstanding work is done is the cleaner alternative.

    Next: the driver produced the transfer, and the monitor saw it at edge 4. In The monitor you will build that observer, and see why it must hand every subscriber a brand-new object.

    Related topics

    • The monitor

    • The driver handshake

    • Responses and pipelined drivers

    • Error and fault injection

    Previous

    97 / 280

    Next: The Monitor: Watch the Pins, Publish a Fresh Transaction

    Up next

    Up nextThe Monitor: Watch the Pins, Publish a Fresh Transaction

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • What an Agent Owns, and What It Shows the World
    • Agent Configuration: Knobs, Config Objects and Who Owns Them
    • The Sequencer: The Agent's Stimulus Socket
    APB write to KEY, one wait state0123456clkPSELPENABLEPREADYPADDR0C (KEY)PWRITEPWDATAkeyphasesetupaccess, waitaccess, donemonitorap.write
    One APB write to KEY with the DMA's single wait state. Edge 1: the driver drives the setup phase (PSEL=1, PENABLE=0). Edge 2: PENABLE=1 starts the access phase; PREADY is low, so this is a wait state. Edge 3: the driver still reads PREADY=0 (the value before the edge) and waits again. Edge 4: PSEL, PENABLE and PREADY are all 1 just before the edge, so the transfer completes there: the DUT takes the write, the driver parks the bus, and the monitor publishes the transaction.