TLM-2.0 sockets for a reference model

Call a reference model through TLM-2.0 sockets with a generic payload, read its response status and annotated time, run two nonblocking transactions through their phases, and put the model behind a scoreboard's predictor.

Lesson
U9.4
Time
35 min
Runs on
Verilator with UVM
Check
6 questions

The platform team's memory model answers every read the scoreboard sends it, and the scoreboard copies the data out of each reply. A read to an unmapped address comes back with data 00, the design also returns 00 there, and the check passes for weeks. The model had said address error, and the design should have flagged one too. Nobody looked at the reply's status, only at its data.

TimeCallResponse statusDataAnnotated
0 nsWRITE 01 = 11OK1120 ns
20 nsWRITE 40 = 01 (inv on)OK0120 ns
40 nsREAD 01OKee20 ns
60 nsREAD 12OK2d40 ns
100 nsREAD 80ADDRESS_ERROR00 (untouched)0 ns

The example's blocking calls to the memory model. The last reply's data is whatever the initiator left in the payload.

A model behind a socket

U9.3 called its model through a function with arguments the two sides agreed on. TLM-2.0 is a standard for that agreement between models: one transaction class, the generic payload, and a few calls to move it. Models written to it plug into each other without glue, which is how the virtual platforms of U9.5 are assembled.

UVM ships the SystemVerilog side of the standard. The example turns U9.1's memory into a TLM-2.0 target that implements b_transport. The initiator starts transactions; in a bench it is the scoreboard's predictor. ini.b_sock.connect(tgt.b_sock) joins them in connect_phase.

Initiator, target and one payloadSolid arrows are calls on the forward and backward paths; the payload is one object both sides touch at agreed times.
Initiator, target and one payloadSolid arrows are calls on the forward and backward paths; the payload is one object both sides touch at agreed times. predictor initiator b_sock, nb_sock,nb_transport_bw object generic payload command, address,data, length, status target mem_model b_transport,nb_transport_fw process responder thread one per nonblockingrequest creates, fillsforward callsets status, dataforknb_transport_bw
  1. Blocking0 ns (illustrative)

    b_transport runs in the target and returns with the status set: one call, one transaction.

    More context

    Log line 0 ns b_transport WRITE 01 -> OK, data 11, annotated 20 ns. The target adds its 20 ns latency to delay instead of waiting; the initiator waits it out, so its next call is at 20 ns.

    Follow the calls, values and waits
    1. data WRITE 01 = 11: initiator → generic payload
    2. call b_transport(gp, delay): initiator → mem_model
    3. data status OK: mem_model → generic payload
    4. return returns; delay 20 ns: mem_model → initiator
  2. Nonblocking: the request100 ns (illustrative)

    The forward call returns at once with END_REQ. The target has already read the memory and starts a responder.

    More context

    Log lines 100 ns target fw BEGIN_REQ 12, returns UVM_TLM_UPDATED with END_REQ and the initiator's matching line. nb_transport_fw is a function, so it cannot wait for the response.

    Follow the calls, values and waits
    1. data READ 12: initiator → generic payload
    2. call nb_transport_fw: BEGIN_REQ: initiator → mem_model
    3. data data 2d, status OK: mem_model → generic payload
    4. call start a responder: mem_model → responder thread
    5. return UPDATED, END_REQ: mem_model → initiator
    6. Waiting: responder thread — waits 40 ns for bank 1
  3. Nonblocking: the response140 ns (illustrative)

    A responder answers later on the backward path, with the same payload.

    More context

    Log line 140 ns initiator bw BEGIN_RESP 12, data 2d, returns UVM_TLM_COMPLETED. The responder calls from its own thread, after the forward call has returned.

    Follow the calls, values and waits
    1. call nb_transport_bw: BEGIN_RESP: responder thread → initiator
    2. return COMPLETED: initiator → responder thread
    3. Released: responder thread
Read the diagram as text
  • initiator: b_sock, nb_sock, nb_transport_bw
  • generic payload: command, address, data, length, status
  • mem_model: b_transport, nb_transport_fw
  • responder thread: one per nonblocking request
  • initiator → generic payload: creates, fills
  • initiator → mem_model: forward call
  • mem_model → generic payload: sets status, data
  • mem_model → responder thread: fork
  • responder thread → initiator: nb_transport_bw

Initiator sockets connect to target sockets; connect is called on the initiator side.

Generic payload fieldSet byIn the example
commandInitiatorUVM_TLM_WRITE_COMMAND or UVM_TLM_READ_COMMAND
addressInitiator00-1f two banks, 40 the inv register
data, data length, streaming widthInitiator; target fills data on a readOne byte; length and width both 1
response statusTargetStarts UVM_TLM_INCOMPLETE_RESPONSE; target sets OK or an error

The fields this target supports. Byte enables and extensions exist too; the UVM library carries a DMI flag but has no DMI interface.

The response status is part of the answer. A new payload starts as INCOMPLETE, and a target that forgets to set it leaves it there, so an initiator can tell "never handled" from "handled, OK". The data length is a separate field from the data array, and the target reads get_data_length().

This target supports a restricted subset: one-byte reads and writes on the map shown, no byte enables. Anything else gets an error status, not a guess: BURST_ERROR for another length, BYTE_ENABLE_ERROR for byte enables, COMMAND_ERROR for an ignore command and ADDRESS_ERROR off the map.

Blocking transport and annotated time

b_transport(t, delay) is a task. When it returns, the target has finished the transaction and written the status into t. The second argument is a uvm_tlm_time, an object that carries a time offset with its own resolution, so models compiled under different timescales agree on what "20 ns" means.

This target never waits. It adds its latency to delay and returns at the same simulation time: 20 ns for bank 0, 40 for bank 1, the memory's minimum latencies at 10 ns a cycle. It leaves out the arbitration that can make a bank 1 answer later (U9.1): a timing abstraction, good enough for values, not for checking latency. That style is called loosely timed: the target annotates, and the initiator decides when to let simulation time catch up. The example's initiator waits out each annotation before its next call, so the calls happen at 0, 20, 40, 60 and 100 ns.

Reads the data only

b_sock.b_transport(t, dly);
t.get_data(d);
expected = d[0];

An address error leaves the initiator's 00 in the payload, and it becomes the prediction.

Reads the status first

b_sock.b_transport(t, dly);
if (!t.is_response_ok())
  expect_error(t.get_response_status());
else begin t.get_data(d); expected = d[0]; end

The model's error becomes an expected error response from the design.

The usage rules are short. The initiator creates the payload and may reuse it once a call returns, as the example does. The target may change the payload only during the call and must not keep the handle afterwards. So the observation point is the return: once b_transport is back, the status and data are final and the initiator may read them.

Temporal decoupling and its price

An initiator does not have to wait after every call. It can keep one uvm_tlm_time as its local time offset, let each call add to it, and synchronise with simulation time only now and then. The quiz's initiator makes three calls at simulation time 0, collects 80 ns of annotations, waits once and resets the offset.

Three calls, one synchronisation
0123stepcall bank 0call bank 1call bank 0syncsim time (ns)080local offset (ns)2060800one wait of 80 ns

Columns are steps of the initiator. The model runs ahead of simulation time and catches up once.

This is temporal decoupling, and it is why a model of a whole chip runs fast: it does not hand control back to the simulator for every transaction. Platforms bound the run-ahead with a quantum, a maximum offset after which the initiator must synchronise.

The price is causality. An event that happens in simulation time while the initiator runs ahead, such as an interrupt raised at 30 ns, is not seen until it synchronises at 80. If that interrupt should have changed what the model did between 30 and 80, the values are now wrong too, not just late. Run-ahead is correct only while nothing outside the initiator can influence what it computes, so synchronise before such an event can matter. A quantum bounds how far the initiator runs ahead; it does not by itself make the results right.

Nonblocking transport with two reads in flight

A model of a pipelined bus must accept a second request before the first is answered. The nonblocking interface allows this by splitting a transaction into phases: BEGIN_REQ, END_REQ, BEGIN_RESP, END_RESP. The initiator calls nb_transport_fw on the forward path; the target answers later with nb_transport_bw on the backward path. Both are functions: neither side may wait inside them.

Two nonblocking reads, answered out of order
012345time (ns)100120140callfw 12returnsfw 01returnsbw 01bw 12phaseBEGIN_REQEND_REQBEGIN_REQEND_REQBEGIN_RESPsync result-UPDATED-UPDATEDCOMPLETEDbank 0 answers first

Columns are calls, not clock edges. Both requests are accepted at 100 ns; the bank 0 read, started second, completes first.

Each call returns a uvm_tlm_sync_e. UVM_TLM_ACCEPTED means nothing changed; UVM_TLM_UPDATED means the callee moved the phase, here from BEGIN_REQ to END_REQ; UVM_TLM_COMPLETED ends the transaction. Because nb_transport_fw cannot wait, the target queues each request and starts a responder thread per request.

Two reads, two payloadsEach payload travels to the target and comes back on the backward call that completes it.
Two reads, two payloadsEach payload travels to the target and comes back on the backward call that completes it. object payload #1 READ 12, bank 1 object payload #2 READ 01, bank 0 target nb_transport_fw access now, END_REQ process responder 1 waits 40 ns process responder 2 waits 20 ns initiator nb_transport_bw returns COMPLETED 100 ns100 nsforkfork120 ns, #2140 ns, #1
  1. Both requests accepted100 ns (illustrative)

    Each forward call returns UVM_TLM_UPDATED with END_REQ at 100 ns, and the target starts one responder per read.

    More context

    Read 12 goes first, then read 01, both at 100 ns. Each read has its own payload object, so neither overwrites the other's address.

    Follow the calls, values and waits
    1. data payload #1: READ 12: payload #1 → nb_transport_fw
    2. call responder 1, 40 ns: nb_transport_fw → responder 1
    3. data payload #2: READ 01: payload #2 → nb_transport_fw
    4. call responder 2, 20 ns: nb_transport_fw → responder 2
    5. Waiting: responder 1 — bank 1: until 140 ns
    6. Waiting: responder 2 — bank 0: until 120 ns
  2. Bank 0 first120 ns (illustrative)

    The read started second completes first, at 120 ns, carrying payload #2.

    More context

    Log line 120 ns initiator bw BEGIN_RESP 01, data ee, returns UVM_TLM_COMPLETED. Payload #1 is still in flight.

    Follow the calls, values and waits
    1. data payload #2: data ee: responder 2 → nb_transport_bw
    2. Released: responder 2
  3. Bank 1 later140 ns (illustrative)

    Payload #1 completes at 140 ns. The handle tells the initiator which read it is.

    More context

    Log line 140 ns initiator bw BEGIN_RESP 12, data 2d, returns UVM_TLM_COMPLETED. No tag is needed: the payload handle is the identity.

    Follow the calls, values and waits
    1. data payload #1: data 2d: responder 1 → nb_transport_bw
    2. Released: responder 1
Read the diagram as text
  • payload #1: READ 12, bank 1
  • payload #2: READ 01, bank 0
  • nb_transport_fw: access now, END_REQ
  • responder 1: waits 40 ns
  • responder 2: waits 20 ns
  • nb_transport_bw: returns COMPLETED
  • payload #1 → nb_transport_fw: 100 ns
  • payload #2 → nb_transport_fw: 100 ns
  • nb_transport_fw → responder 1: fork
  • nb_transport_fw → responder 2: fork
  • responder 2 → nb_transport_bw: 120 ns, #2
  • responder 1 → nb_transport_bw: 140 ns, #1

A payload is one transaction until its final COMPLETED; refilling it early would corrupt the read in flight.

Two reads in flight need two payload objects. The backward call passes the same object the forward call carried, which is how the initiator knows which read is answered. Reusing one payload for the second read would overwrite the first one's address while the target still holds it, so nb_read creates a payload per read.

One more rule keeps the paths apart: a forward call must never call the backward path, directly or through other code, and the reverse holds too. The example's target answers from its own threads, after the forward call has returned.

The model as a scoreboard's predictor

In a bench, the initiator is the predictor of U8.1. It receives a monitored request, calls the model and turns the reply into an expected response. One rule shapes it: an analysis write is a function and b_transport is a task, and a function may not call a task, whatever the task does inside. So the predictor stores requests in an analysis FIFO, which keeps their order, and calls the socket from its own run_phase.

A model behind a predictorWrites are function calls; the FIFO hands requests to a task, which calls the model through the socket and publishes the prediction with its status.
A model behind a predictorWrites are function calls; the FIFO hands requests to a task, which calls the model through the socket and publishes the prediction with its status. monitor request monitor bus_req queue req_fifo Keeps request order task run_phase get, fill payload,b_transport target mem_model Sets status and data monitor response monitor bus_rsp from thedesign compare comparator Status first, thendata write (function)get (task)b_transport (task)expected: status,dataactual
  1. Buffer

    The analysis write only stores the request; it cannot call the socket.

    More context

    The quiz's unmapped read at 80. write returns in the same time step; a function may not call the task b_transport.

    Follow the calls, values and waits
    1. data bus_req: READ 80: request monitor → req_fifo
    2. Waiting: run_phase — blocked in req_fifo.get
  2. Call the model

    A task takes requests in order and calls b_transport; the reply carries the status and, for a read, the data.

    More context

    The task may wait, so it is the right place for the socket call. Here the model answers ADDRESS_ERROR: nothing is mapped at 80.

    Follow the calls, values and waits
    1. data bus_req: READ 80: req_fifo → run_phase
    2. call b_transport(gp, delay): run_phase → mem_model
    3. return ADDRESS_ERROR: mem_model → run_phase
    4. Released: run_phase
  3. Compare

    The comparator checks the status before the data, so an unmapped read answered with OK is caught.

    More context

    The report is design UVM_TLM_OK_RESPONSE, model UVM_TLM_ADDRESS_ERROR_RESPONSE. Comparing the data alone, 00 against 00, would have passed.

    Follow the calls, values and waits
    1. data expected: ADDRESS_ERROR: run_phase → comparator
    2. data actual: OK, 00: response monitor → comparator
Read the diagram as text
  • request monitor: bus_req
  • req_fifo: Keeps request order
  • run_phase: get, fill payload, b_transport
  • mem_model: Sets status and data
  • response monitor: bus_rsp from the design
  • comparator: Status first, then data
  • request monitor → req_fifo: write (function)
  • req_fifo → run_phase: get (task)
  • run_phase → mem_model: b_transport (task)
  • run_phase → comparator: expected: status, data
  • response monitor → comparator: actual

Function to FIFO to task: the socket call happens where waiting is allowed.

The predictor the quiz runs: request in through a FIFO, payload through the socket, prediction out with its status.
// Requests arrive by analysis write, a function; b_transport is a task. So the predictor
// buffers requests in an analysis FIFO, which keeps their order, and calls the model from run_phase.
class tlm_predictor extends uvm_component;
  `uvm_component_utils(tlm_predictor)
  uvm_tlm_analysis_fifo #(bus_req) req_fifo;     // the monitor connects to req_fifo.analysis_export
  uvm_tlm_b_initiator_socket #(gp) b_sock;       // to the model
  uvm_analysis_port #(bus_rsp)     ap;           // predictions, in request order
  function new(string name, uvm_component parent);
    super.new(name, parent);
    req_fifo = new("req_fifo", this);
    b_sock   = new("b_sock", this);
    ap       = new("ap", this);
  endfunction
  task run_phase(uvm_phase phase);
    gp           t   = gp::type_id::create("t");
    uvm_tlm_time dly = new("dly", 1e-9);
    forever begin
      bus_req       r;
      bus_rsp       e;
      byte unsigned d[] = new[1];
      req_fifo.get(r);
      d[0] = r.data;
      if (r.is_write) t.set_command(UVM_TLM_WRITE_COMMAND); else t.set_command(UVM_TLM_READ_COMMAND);
      t.set_address(r.addr);
      t.set_data(d);
      t.set_data_length(1);
      t.set_streaming_width(1);
      t.set_response_status(UVM_TLM_INCOMPLETE_RESPONSE);
      dly.reset();
      b_sock.b_transport(t, dly);                // a task, called from a task
      e = bus_rsp::type_id::create("e");
      e.addr    = r.addr;
      e.is_read = !r.is_write;
      e.status  = t.get_response_status();       // the status is part of the prediction
      t.get_data(d);
      e.data = d[0];
      ap.write(e);
    end
  endtask
endclass

The second rule is the hook's: the status is part of the prediction. The expected response carries the model's status, and the comparator checks status first and data only for a read the model completed with OK. A reply still INCOMPLETE means the model did not handle the request, and is reported as a model fault. Comparing a status needs a bus whose responses carry one; U9.1's memory returns data only, so the quiz program uses that memory with a status added.

The quiz's design answers the unmapped read at 80 with OK and 00, as in the hook. This comparator reports design UVM_TLM_OK_RESPONSE, model UVM_TLM_ADDRESS_ERROR_RESPONSE, the error the hook's bench never saw. U9.5 takes the same structure to the scale of a virtual platform.

Try it

Five blocking calls, each followed by a wait for its annotated time, then two nonblocking reads started at 100 ns. The read at 80 returns ADDRESS_ERROR and leaves the initiator's 00 in the data. The bank 0 read completes at 120 ns, before the bank 1 read started ahead of it.

Delete the #(dly.get_realtime(1ns) * 1ns); line in b_call and rerun: every blocking call now happens at 0 ns, and the nonblocking reads start at 0 too. The values are the same; only time has stopped advancing.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 0 ns b_transport WRITE 01 -> OK, data 11, annotated 20 ns 20 ns b_transport WRITE 40 -> OK, data 01, annotated 20 ns 40 ns b_transport READ 01 -> OK, data ee, annotated 20 ns 60 ns b_transport READ 12 -> OK, data 2d, annotated 40 ns 100 ns b_transport READ 80 -> ADDRESS_ERROR, data 00, annotated 0 ns 100 ns target fw BEGIN_REQ 12, returns UVM_TLM_UPDATED with END_REQ 100 ns initiator fw 12 returned UVM_TLM_UPDATED, phase END_REQ 100 ns target fw BEGIN_REQ 01, returns UVM_TLM_UPDATED with END_REQ 100 ns initiator fw 01 returned UVM_TLM_UPDATED, phase END_REQ 120 ns initiator bw BEGIN_RESP 01, data ee, returns UVM_TLM_COMPLETED 120 ns target bw 01 returned UVM_TLM_COMPLETED 140 ns initiator bw BEGIN_RESP 12, data 2d, returns UVM_TLM_COMPLETED 140 ns target bw 12 returned UVM_TLM_COMPLETED UVM_ERROR : 0

Check yourself

Predict the outputThis initiator makes three blocking calls to the same memory model (bank 0, bank 1, bank 0) without waiting between them, adding every annotation to one uvm_tlm_time, then waits once. What line does it print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

typedef uvm_tlm_generic_payload gp;

// U9.1's memory as a TLM-2.0 target: bank 0 at 00-0f, bank 1 at 10-1f, the inv register at 40.
// Loosely timed: b_transport annotates the latency on the delay argument and returns at once.
class mem_model extends uvm_component;
  `uvm_component_utils(mem_model)
  uvm_tlm_b_target_socket  #(mem_model, gp) b_sock;
  uvm_tlm_nb_target_socket #(mem_model, gp) nb_sock;
  byte unsigned mem [32];
  bit           inv;
  gp            in_flight[$];          // nb requests waiting for their response
  int           lat[$];
  function new(string name, uvm_component parent);
    super.new(name, parent);
    b_sock  = new("b_sock", this);
    nb_sock = new("nb_sock", this);
    foreach (mem[i]) mem[i] = (i < 16) ? (8'hC0 | i[3:0]) : (8'hD0 | i[3:0]);
  endfunction
  // Decode, access, set the status. Returns the latency in ns: bank 0 20, bank 1 40.
  // Restricted contract: one-byte reads and writes on the map above, no byte enables.
  function int access(gp t);
    byte unsigned d[];
    bit [63:0]    a;
    a = t.get_address();
    t.get_data(d);
    if (!t.is_read() && !t.is_write()) begin
      t.set_response_status(UVM_TLM_COMMAND_ERROR_RESPONSE);
      return 0;
    end
    if (t.get_data_length() != 1 || d.size() < 1) begin
      t.set_response_status(UVM_TLM_BURST_ERROR_RESPONSE);
      return 0;
    end
    if (t.get_byte_enable_length() != 0) begin
      t.set_response_status(UVM_TLM_BYTE_ENABLE_ERROR_RESPONSE);
      return 0;
    end
    if (a == 'h40) begin
      if (t.is_write()) inv = d[0][0];
      else begin d[0] = inv; t.set_data(d); end
    end else if (a < 32) begin
      if (t.is_write()) mem[a] = d[0];
      else begin d[0] = mem[a] ^ {8{inv}}; t.set_data(d); end
    end else begin
      t.set_response_status(UVM_TLM_ADDRESS_ERROR_RESPONSE);
      return 0;
    end
    t.set_response_status(UVM_TLM_OK_RESPONSE);
    if (a >= 16 && a < 32) return 40;
    return 20;
  endfunction
  task b_transport(gp t, uvm_tlm_time delay);
    delay.incr(access(t), 1ns);          // annotate; the initiator decides when to wait
  endtask
  // Nonblocking: take the request now, answer on the backward path later.
  function uvm_tlm_sync_e nb_transport_fw(gp t, ref uvm_tlm_phase_e p, input uvm_tlm_time delay);
    if (p != BEGIN_REQ) return UVM_TLM_ACCEPTED;
    lat.push_back(access(t));
    in_flight.push_back(t);
    $display("%3d ns  target    fw %s %02h, returns UVM_TLM_UPDATED with END_REQ", $time, p.name(), 8'(t.get_address()));
    p = END_REQ;
    return UVM_TLM_UPDATED;
  endfunction
  task run_phase(uvm_phase phase);
    forever begin
      wait (in_flight.size() != 0);
      fork                                 // one responder per request: they may overlap
        automatic gp  t = in_flight.pop_front();
        automatic int l = lat.pop_front();
        respond(t, l);
      join_none
    end
  endtask
  task respond(gp t, int l);
    uvm_tlm_phase_e p = BEGIN_RESP;
    uvm_tlm_time    dly = new("bw_dly", 1e-9);
    uvm_tlm_sync_e  st;
    #(l * 1ns);
    st = nb_sock.nb_transport_bw(t, p, dly);
    $display("%3d ns  target    bw %02h returned %s", $time, 8'(t.get_address()), st.name());
  endtask
endclass

class initiator extends uvm_component;
  `uvm_component_utils(initiator)
  uvm_tlm_b_initiator_socket  #(gp)            b_sock;
  uvm_tlm_nb_initiator_socket #(initiator, gp) nb_sock;
  gp           t;
  uvm_tlm_time dly;
  int          done;                   // nonblocking transactions completed
  function new(string name, uvm_component parent);
    super.new(name, parent);
    b_sock  = new("b_sock", this);
    nb_sock = new("nb_sock", this);
  endfunction
  // Fill the one payload this initiator reuses: a single byte at addr.
  function void fill(bit is_write, bit [63:0] addr, byte unsigned value = 0);
    byte unsigned d[] = new[1];
    d[0] = value;
    if (is_write) t.set_command(UVM_TLM_WRITE_COMMAND); else t.set_command(UVM_TLM_READ_COMMAND);
    t.set_address(addr);
    t.set_data(d);
    t.set_data_length(1);
    t.set_streaming_width(1);
    t.set_response_status(UVM_TLM_INCOMPLETE_RESPONSE);
  endfunction
  function byte unsigned data0();
    byte unsigned d[];
    t.get_data(d);
    return d[0];
  endfunction
  // The backward call names its payload, so responses may come back in any order.
  function uvm_tlm_sync_e nb_transport_bw(gp t, ref uvm_tlm_phase_e p, input uvm_tlm_time delay);
    byte unsigned d[];
    t.get_data(d);
    $display("%3d ns  initiator bw %s %02h, data %02h, returns UVM_TLM_COMPLETED", $time, p.name(), 8'(t.get_address()), d[0]);
    done++;
    return UVM_TLM_COMPLETED;
  endfunction

  task run_phase(uvm_phase phase);
    time t0;
    real off;
    phase.raise_objection(this);
    t   = gp::type_id::create("t");
    dly = new("dly", 1e-9);
    fill(1, 'h03, 'h5a); b_sock.b_transport(t, dly);   // keep adding to one delay
    fill(0, 'h13);       b_sock.b_transport(t, dly);
    fill(0, 'h03);       b_sock.b_transport(t, dly);
    t0  = $time;
    off = dly.get_realtime(1ns);
    #(off * 1ns);                                       // synchronise once
    dly.reset();                                        // the offset is now simulation time
    $display("returned at %0t with %0.0f ns annotated; synced at %0t", t0, off, $time);
    phase.drop_objection(this);
  endtask
endclass

class tlm_test extends uvm_test;
  `uvm_component_utils(tlm_test)
  initiator ini;
  mem_model tgt;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    ini = initiator::type_id::create("ini", this);
    tgt = mem_model::type_id::create("tgt", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    ini.b_sock.connect(tgt.b_sock);
    ini.nb_sock.connect(tgt.nb_sock);
  endfunction
endclass

module tb;
  initial run_test("tlm_test");
endmodule
Predict the outputThis program runs the predictor above against monitor stand-ins. The design writes 11, reads it back, answers an unmapped read at 80 with OK and 00, and reads the power-up value d2 at 12. What line does report_phase print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

typedef uvm_tlm_generic_payload gp;

// U9.1's memory as a TLM-2.0 target: bank 0 at 00-0f, bank 1 at 10-1f, the inv register at 40.
// Loosely timed: b_transport annotates the latency on the delay argument and returns at once.
class mem_model extends uvm_component;
  `uvm_component_utils(mem_model)
  uvm_tlm_b_target_socket  #(mem_model, gp) b_sock;
  byte unsigned mem [32];
  bit           inv;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    b_sock  = new("b_sock", this);
    foreach (mem[i]) mem[i] = (i < 16) ? (8'hC0 | i[3:0]) : (8'hD0 | i[3:0]);
  endfunction
  // Decode, access, set the status. Returns the latency in ns: bank 0 20, bank 1 40.
  // Restricted contract: one-byte reads and writes on the map above, no byte enables.
  function int access(gp t);
    byte unsigned d[];
    bit [63:0]    a;
    a = t.get_address();
    t.get_data(d);
    if (!t.is_read() && !t.is_write()) begin
      t.set_response_status(UVM_TLM_COMMAND_ERROR_RESPONSE);
      return 0;
    end
    if (t.get_data_length() != 1 || d.size() < 1) begin
      t.set_response_status(UVM_TLM_BURST_ERROR_RESPONSE);
      return 0;
    end
    if (t.get_byte_enable_length() != 0) begin
      t.set_response_status(UVM_TLM_BYTE_ENABLE_ERROR_RESPONSE);
      return 0;
    end
    if (a == 'h40) begin
      if (t.is_write()) inv = d[0][0];
      else begin d[0] = inv; t.set_data(d); end
    end else if (a < 32) begin
      if (t.is_write()) mem[a] = d[0];
      else begin d[0] = mem[a] ^ {8{inv}}; t.set_data(d); end
    end else begin
      t.set_response_status(UVM_TLM_ADDRESS_ERROR_RESPONSE);
      return 0;
    end
    t.set_response_status(UVM_TLM_OK_RESPONSE);
    if (a >= 16 && a < 32) return 40;
    return 20;
  endfunction
  task b_transport(gp t, uvm_tlm_time delay);
    delay.incr(access(t), 1ns);          // annotate; the initiator decides when to wait
  endtask
endclass

class bus_req extends uvm_sequence_item;
  `uvm_object_utils(bus_req)
  bit       is_write;
  bit [7:0] addr, data;
  function new(string name = "bus_req"); super.new(name); endfunction
endclass

class bus_rsp extends uvm_sequence_item;
  `uvm_object_utils(bus_rsp)
  uvm_tlm_response_status_e status;
  bit                       is_read;
  bit [7:0]                 addr, data;
  function new(string name = "bus_rsp"); super.new(name); endfunction
endclass

// Requests arrive by analysis write, a function; b_transport is a task. So the predictor
// buffers requests in an analysis FIFO, which keeps their order, and calls the model from run_phase.
class tlm_predictor extends uvm_component;
  `uvm_component_utils(tlm_predictor)
  uvm_tlm_analysis_fifo #(bus_req) req_fifo;     // the monitor connects to req_fifo.analysis_export
  uvm_tlm_b_initiator_socket #(gp) b_sock;       // to the model
  uvm_analysis_port #(bus_rsp)     ap;           // predictions, in request order
  function new(string name, uvm_component parent);
    super.new(name, parent);
    req_fifo = new("req_fifo", this);
    b_sock   = new("b_sock", this);
    ap       = new("ap", this);
  endfunction
  task run_phase(uvm_phase phase);
    gp           t   = gp::type_id::create("t");
    uvm_tlm_time dly = new("dly", 1e-9);
    forever begin
      bus_req       r;
      bus_rsp       e;
      byte unsigned d[] = new[1];
      req_fifo.get(r);
      d[0] = r.data;
      if (r.is_write) t.set_command(UVM_TLM_WRITE_COMMAND); else t.set_command(UVM_TLM_READ_COMMAND);
      t.set_address(r.addr);
      t.set_data(d);
      t.set_data_length(1);
      t.set_streaming_width(1);
      t.set_response_status(UVM_TLM_INCOMPLETE_RESPONSE);
      dly.reset();
      b_sock.b_transport(t, dly);                // a task, called from a task
      e = bus_rsp::type_id::create("e");
      e.addr    = r.addr;
      e.is_read = !r.is_write;
      e.status  = t.get_response_status();       // the status is part of the prediction
      t.get_data(d);
      e.data = d[0];
      ap.write(e);
    end
  endtask
endclass

// Status first; data only for a read the model completed with OK.
class status_comparator extends uvm_component;
  `uvm_component_utils(status_comparator)
  uvm_tlm_analysis_fifo #(bus_rsp) exp_fifo, act_fifo;
  int compared, status_mismatches, data_mismatches;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    exp_fifo = new("exp_fifo", this);
    act_fifo = new("act_fifo", this);
  endfunction
  task run_phase(uvm_phase phase);
    bus_rsp e, a;
    forever begin
      exp_fifo.get(e);
      act_fifo.get(a);
      compared++;
      if (e.status == UVM_TLM_INCOMPLETE_RESPONSE)
        `uvm_error("MODEL", $sformatf("addr %02h: the model did not handle the request", e.addr))
      else if (a.status != e.status) begin
        status_mismatches++;
        `uvm_error("CMP", $sformatf("addr %02h: design %s, model %s", a.addr, a.status.name(), e.status.name()))
      end else if (e.is_read && e.status == UVM_TLM_OK_RESPONSE && a.data != e.data) begin
        data_mismatches++;
        `uvm_error("CMP", $sformatf("addr %02h: design data %02h, model %02h", a.addr, a.data, e.data))
      end
    end
  endtask
endclass

class predictor_test extends uvm_test;
  `uvm_component_utils(predictor_test)
  uvm_analysis_port #(bus_req) req_ap;    // stand-ins for the request and response monitors
  uvm_analysis_port #(bus_rsp) rsp_ap;
  tlm_predictor     pred;
  mem_model         model;
  status_comparator cmp;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    req_ap = new("req_ap", this);
    rsp_ap = new("rsp_ap", this);
    pred   = tlm_predictor::type_id::create("pred", this);
    model  = mem_model::type_id::create("model", this);
    cmp    = status_comparator::type_id::create("cmp", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    req_ap.connect(pred.req_fifo.analysis_export);
    pred.b_sock.connect(model.b_sock);
    pred.ap.connect(cmp.exp_fifo.analysis_export);
    rsp_ap.connect(cmp.act_fifo.analysis_export);
  endfunction
  // One transfer as the monitors would report it: the request, then the design's response.
  task transfer(bit is_write, bit [7:0] addr, bit [7:0] data, uvm_tlm_response_status_e st, bit [7:0] rdata);
    bus_req r = bus_req::type_id::create("r");
    bus_rsp a = bus_rsp::type_id::create("a");
    r.is_write = is_write; r.addr = addr; r.data = data;
    a.is_read = !is_write; a.addr = addr; a.status = st; a.data = rdata;
    req_ap.write(r);
    #10;
    rsp_ap.write(a);
    #10;
  endtask
  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    transfer(1, 'h01, 'h11, UVM_TLM_OK_RESPONSE, 0);       // write 11
    transfer(0, 'h01, 0,    UVM_TLM_OK_RESPONSE, 'h11);    // read it back
    transfer(0, 'h80, 0,    UVM_TLM_OK_RESPONSE, 'h00);    // the design answers an unmapped read
    transfer(0, 'h12, 0,    UVM_TLM_OK_RESPONSE, 'hD2);    // power-up contents of bank 1, addr 2
    phase.drop_objection(this);
  endtask
  function void report_phase(uvm_phase phase);
    $display("compared %0d: status mismatches %0d, data mismatches %0d", cmp.compared, cmp.status_mismatches, cmp.data_mismatches);
  endfunction
endclass

module tb;
  initial run_test("predictor_test");
endmodule
Spot the bugThe initiator writes 5a to address 05, reads it back, then reads the unmapped address 50 and records each reply's status and data. Each option is a target's b_transport body. Which target gives the initiator the wrong replies?
ConceptWhy does nb_transport_fw in the example queue the request and answer from the target's run_phase, instead of waiting 20 ns and returning BEGIN_RESP?
ConceptAn initiator starts a nonblocking read of bank 1, then refills the same payload object and starts a read of bank 0 before the first has completed. What goes wrong?
ConceptThe example's initiator reuses one generic payload for every call. When is that allowed?

Remember

  • TLM-2.0 connects models through sockets: an initiator calls b_transport or nb_transport_fw, a target implements them, all on one generic payload.
  • The response status is part of the answer: it starts as INCOMPLETE, the target sets OK or an error, and the initiator checks it before using the data.
  • A loosely timed target annotates latency on a uvm_tlm_time and returns at once; the initiator chooses when simulation time catches up, which allows temporal decoupling.
  • Nonblocking transport splits a transaction into phases on a forward and a backward path; both calls are functions, so a target answers later from its own threads, possibly out of order, and each transaction in flight needs its own payload.
  • A predictor buffers requests in an analysis FIFO and calls the socket from run_phase (a function cannot call a task); the model's status is part of the prediction.