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
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
    • Callbacks, Command Line, Seeds and Versions
    • Synchronisation and Reporting
      • Events, Barriers and Pools
      • Report Catchers and Waivers
      • Custom Report Servers and the Verdict Line
      • Heartbeats and Watchdogs
      • Transaction Recording
    • Testbenches at Scale
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Synchronisation and Reporting
  1. Topics
  2. UVM Verification
  3. Advanced UVM in Practice
  4. Synchronisation and Reporting
  5. Transaction Recording

Part 10 · Advanced Topics · Intermediate

Transaction Recording

begin_tr and end_tr, recorders, streams and do_record, enabling recording with recording_detail, automatic item recording on sequencers, how streams show in a waveform viewer, and what recording costs.

On this page

    A KEY write that the DMA ignored has to be found in a 40 µs waveform with 300 signals. You add PSEL, PENABLE, PADDR and PWDATA, zoom, scroll, and count transfers by eye. A colleague opens the same run, expands a stream called apb_bus under the APB monitor, and clicks the fourth bar: addr=0c data=ffff0000 write=1, at the exact time, aligned with the wires below it.

    That stream is transaction recording. UVM can write every transaction a component sees into a database that the waveform tool shows as bars with fields. This lesson shows how to turn it on, how the library records items automatically and where you add calls yourself, what it costs, and why it belongs in debug reruns rather than regressions.

    The idea: a flight recorder's event log next to the sensor traces

    Waveforms are raw sensor traces: every wire, every clock. A transaction stream is the flight recorder's event log: "write to KEY started here, ended there, with these values". Both are recorded against the same clock, so the viewer can show one above the other. You read the log to find the moment, then the traces to see why.

    In UVM terms, a component calls begin_tr() when a transaction starts and end_tr() when it ends. Between them the library opens a recorder on a stream, a named lane of transactions belonging to that component, inside a transaction database. At end_tr() it calls the transaction's do_record(), which writes the fields.

    The picture: from begin_tr to a bar in the viewer

    Flowchart: a component with recording enabled calls begin_tr, which opens a recorder on a stream; end_tr calls do_record to write the fields; the recorder writes to a transaction database, either the library's text database file or the simulator's own database shown in its waveform viewer.
    A transaction opens a recorder on its component's stream at begin_tr and gets its fields written at end_tr. The open-source library writes text; simulators plug in their own database for their viewers.

    Build it

    Turn recording on, before the component exists

    A component records only when its recording_detail is not UVM_NONE, and the default is UVM_NONE. The 1800.2 reference library reads recording_detail from the configuration in the component's constructor, so the setting must be made before the component is created:

    design.sv
    1
    2
    3
    4
    5
    6
    // snippet: in the test, before the env and its agents are created
    function void build_phase(uvm_phase phase);
      if ($test$plusargs("DMA_RECORD"))
        uvm_config_db#(int)::set(this, "env.*", "recording_detail", UVM_FULL);
      super.build_phase(phase);                 // dma_base_test creates env here
    endfunction

    Making it a plusarg is the point: the same compiled bench records only when someone asks. IEEE 1800.2 also offers set_recording_enabled(1) and set_recording_enabled_hier(1) on a component, which take effect at once. Most simulators additionally have their own switch to make their database the default one and to record UVM activity automatically; it differs per tool, so check your simulator's documentation for it.

    What the library records for you, and what you add

    A sequencer with recording on records items automatically: finish_item() calls begin_tr() on the sequencer and item_done() ends it, so every item a driver executes appears on a stream named after its root sequence, with parent links to the sequences that sent it. disable_auto_item_recording() on the sequencer turns that off when a driver wants to record its own timing. Monitors record nothing unless they call the API themselves, which is where the most useful streams come from, because they show what really happened on the bus:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // snippet: verified in a scratch run (sync_tb.sv)
    task run_phase(uvm_phase phase);
      for (int i = 0; i < 2; i++) begin
        bus_item t = bus_item::type_id::create("apb_tr");
        #10ns;
        void'(begin_tr(t, "apb_bus"));        // transaction starts on the bus
        t.addr = 8'h0C; t.data = 32'hCAFE_F000 + i; t.write = 1;
        #20ns;
        end_tr(t);                             // transaction completes: do_record runs now
      end
    endtask

    In a real monitor, begin_tr() goes where the transfer starts (APB: the setup phase) and end_tr() where it completes (PREADY in the access phase). The item says what to record in do_record(); explicit record_field() calls give each field a size and a radix:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    // snippet
    function void do_record(uvm_recorder recorder);
      super.do_record(recorder);
      recorder.record_field("addr",  addr,  $bits(addr),  UVM_HEX);
      recorder.record_field("data",  data,  $bits(data),  UVM_HEX);
      recorder.record_field("write", write, $bits(write), UVM_BIN);
    endfunction

    begin_tr() also takes a label, a description, a begin time and a parent handle, which links transactions into a tree: a burst as the parent, its beats as children. And the UVM_RM_RECORD action records report messages into the same database, on a MESSAGES stream per reporting component, so errors can be viewed on the same time axis as the transactions.

    Watch it run

    The open-source library's default database is uvm_text_tr_database, which writes tr_db.log. It is not meant for viewing, but it shows exactly what a viewer receives:

    Diagram
    scratch run: +UVM_TESTNAME=rec_test   (tr_db.log, first transaction)
      CREATE_STREAM @10000 {NAME:apb_bus T:TVM SCOPE:uvm_test_top.mon STREAM:329}
        OPEN_RECORDER @10000 {TXH:331 STREAM:329 NAME:apb_tr TIME:10000 TYPE="Begin_No_Parent, Link"}
          SET_ATTR @30000 {TXH:331 NAME:inst_id VALUE:245   RADIX:UVM_DEC BITS=32}
          SET_ATTR @30000 {TXH:331 NAME:addr VALUE:c   RADIX:UVM_HEX BITS=8}
          SET_ATTR @30000 {TXH:331 NAME:data VALUE:cafef000   RADIX:UVM_HEX BITS=32}
          SET_ATTR @30000 {TXH:331 NAME:write VALUE:1   RADIX:UVM_BIN BITS=1}
        CLOSE_RECORDER @30000 {TXH:331 TIME=30000}
        FREE_RECORDER @30000 {TXH:331}
    • One stream per component and stream name: apb_bus under uvm_test_top.mon. In a viewer this is the row under the monitor's scope.

    • The recorder opens at 10 ns and closes at 30 ns: that is the bar's extent.

    • The attributes are written at 30 ns, at end_tr(). Fields filled in during the transfer, such as read data, are recorded correctly because do_record() runs at the end.

    In a commercial simulator's waveform viewer, recorded streams appear in the design hierarchy under the component that recorded them. Each transaction is a bar spanning begin to end, labelled with its name; selecting it shows the recorded fields, and parent links let you expand a sequence into its items. Signals and streams share one time axis, so a cursor on a bar lands on the matching wires.

    What it costs, and when to turn it on

    Every recorded transaction opens a recorder, formats each field, writes to a database and frees the recorder; at UVM_FULL across a whole SoC env, that is per beat, on every interface. Recording can easily become one of the largest costs in a run, and the database can grow larger than the waveform. Measure it on your own bench with the simulator's profiler before deciding; the order of magnitude depends on the tool and the traffic. The working rule:

    Situation

    Recording

    Nightly regression, thousands of seeds

    Off. Nobody opens those databases, and the farm pays for every byte.

    Debug rerun of one failing seed

    On, with the plusarg, usually only on the env subtree around the failure.

    Bring-up of a new agent or VIP

    On for that agent, to check begin and end points against the waveform.

    Performance analysis of a protocol (latency, outstanding transactions)

    On for the monitors involved; the transaction database is the data source.

    What breaks: the recording that was never on

    An engineer added the recording_detail setting to the test's end_of_elaboration_phase, "after everything is built, so the paths exist". The run produced no streams at all. The setting was correct but too late: the components had read recording_detail in their constructors, during build, and nothing reads it again. Moving the config_db::set before super.build_phase() in the test fixed it. The same rule applies to every setting a component reads at construction: set it before the component exists.

    A second, quieter failure: a monitor that calls begin_tr() without a matching end_tr() on an error path (a reset in the middle of a transfer) leaves an open recorder per aborted transaction. The viewer shows bars that never end, and memory grows over a long test. End or abandon the transaction on every path, including reset.

    Interview

    "How do you debug at transaction level instead of signal level?"

    A weak answer: "I print the transactions with uvm_info and read the log."

    A strong answer: "For a failing seed I rerun with transaction recording on, through a plusarg that sets recording_detail before the env is built. Sequencers record items automatically, and our monitors call begin_tr and end_tr at the protocol's start and end points, with do_record writing the fields, so the viewer shows each bus as a stream of bars aligned with the waveform. I find the transaction there, then drop to the signals. Recording is off in regressions: it costs time and disk on every beat and nobody opens those databases."

    Expect the follow-ups: When are the fields written? (at end_tr(), through do_record()) and Why might recording silently not happen? (recording_detail set after the component was constructed).

    Check yourself A read transaction's data field shows the right value in the recorded stream although the monitor fills it in only at the end of the access phase. Why?

    The fields are written by do_record(), which the library calls from end_tr(). By then the monitor has filled in the read data, so the recorded value is the final one.

    Check yourself Sequencer streams show every item, but the bars are much longer than the bus transfers. What do they measure, and how do you get bus-accurate bars?

    Automatic item recording spans from finish_item() (the item handed to the driver) to item_done(), which includes the time the driver waits before driving. Bus-accurate bars come from a monitor that calls begin_tr() and end_tr() at the protocol's real start and end.

    Check yourself Why put the recording switch behind a plusarg instead of a separate test class?

    So any failing test can be rerun with recording on, with the same seed and the same compiled bench, without a code change or a new test name. A separate test class would only record that one scenario.

    Next: this topic is complete. Testbenches at Scale moves from one run to many: code quality, stimulus for many configurations, and VIP shared across projects.

    Related topics

    • Up: Synchronisation and Reporting

    • Inside the sequencer

    • Passive sampling in monitors

    • Performance and memory discipline

    Previous

    178 / 280

    Next: Testbenches at Scale

    Up next

    Up nextTestbenches at Scale

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Events, Barriers and Pools
    • Report Catchers and Waivers
    • Custom Report Servers and the Verdict Line