You review a colleague's new checker. It declares uvm_analysis_port #(apb_item) in_port, defines a write() method, and the env connects a monitor's port to it. It compiles. Elaboration is silent. The test passes with zero errors. Then you read the checker's own report line: 0 item(s) received. Nothing in the library complained, because nothing was illegal. The checker simply owned the wrong kind of object. Which side of a TLM link owns which object, and why did no tool notice?
TLM (transaction-level modeling) is how UVM components exchange whole transaction objects: one component calls a method such as write(), put() or get(), and another component runs it. They never hold handles to each other. Instead, each end of a link is a small connection object, and connect() joins them before the run starts. That indirection is what lets you swap a scoreboard, add a coverage collector or move an agent to another env without editing the components themselves.
Think of a phone call. The port is the handset of the person who dials: it says I need someone to call. The imp (implementation) is the phone that rings, wired to the person who answers and actually does the work. An export is a company switchboard: it has no one behind it, it only forwards the call one level down to the right desk. Who dials says nothing about which way information flows. You can call to tell someone something (push: put, write) or call to ask for something (pull: get, peek). Either way, the caller owns the port and the answerer owns the imp.
Object | Owned by | What it does | Connects to |
|---|---|---|---|
Port | The component that makes the call | Declares which interface it needs and forwards each call along its connection | An export or imp, or its parent's port |
Export | A parent that hides a child | Forwards a call one level down; runs nothing itself | A child's export or imp |
Imp | The component that defines the method | Calls that component's method; ends the chain | Nothing: an imp's own |
The driver is the case that trips people up. It receives items, yet it owns the port, because it is the one that calls get_next_item. The item comes back as the call's output argument. Role follows the call, never the data.
The APB monitor creates its analysis port in the constructor and calls ap.write(it) once per completed transfer, with a freshly created item each time. The monitor does not know or care who is listening. That is the whole point: the same monitor works with one scoreboard, three subscribers, or none.
// snippet: from the course DMA bench
class apb_monitor extends uvm_monitor;
`uvm_component_utils(apb_monitor)
virtual apb_if vif;
uvm_analysis_port #(apb_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
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 monitor")
endfunction
// A transfer completes on the edge where PSEL, PENABLE and PREADY are all 1.
task run_phase(uvm_phase phase);
forever begin
@(vif.mon_cb);
if (vif.mon_cb.psel && vif.mon_cb.penable && vif.mon_cb.pready) begin
apb_item it = apb_item::type_id::create("it"); // a fresh object per transfer
it.addr = vif.mon_cb.paddr;
it.write = vif.mon_cb.pwrite;
it.data = vif.mon_cb.pwdata;
it.rdata = vif.mon_cb.prdata;
it.slverr = vif.mon_cb.pslverr;
`uvm_info("APB_MON", it.convert2string(), UVM_HIGH)
ap.write(it);
end
end
endtask
endclassAn imp takes two type parameters: the transaction type and the class that implements the method. The second one matters. Here is the library's analysis imp, trimmed to the lines that do the work:
// snippet: uvm_analysis_imp, trimmed from the UVM library source
class uvm_analysis_imp #(type T = int, type IMP = int)
extends uvm_port_base #(uvm_tlm_if_base #(T, T));
local IMP m_imp; // the component that implements write()
function new(string name, IMP imp);
super.new(name, imp, UVM_IMPLEMENTATION, 1, 1);
m_imp = imp;
endfunction
function void write(input T t);
m_imp.write(t); // the call lands in the component
endfunction
endclassYou construct an imp with this as its second argument, so the imp is both a child of your component and a pointer back to it. When a port calls the imp's write(), the imp calls m_imp.write() on your component. Two consequences follow. Your class must define the method the imp calls, with the exact signature. And the call goes through a handle typed as the class you named in the parameter, which is why war story 2 happened.
The driver's seq_item_port is a uvm_seq_item_pull_port, declared for you in uvm_driver. The sequencer's seq_item_export is, despite its name, a uvm_seq_item_pull_imp: the sequencer itself implements get_next_item, item_done, get and peek. The agent joins them with one line in its connect_phase: drv.seq_item_port.connect(sqr.seq_item_export). The handshake on that link is the subject of the driver handshake lesson.
The same role rule holds for the flow-controlled families. A component that hands DMA jobs to a job runner owns a put port; the runner owns a put imp and implements put(). If instead the runner asks for the next job when it is ready, the runner owns a get port and the job source implements get().
// snippet
class job_source extends uvm_component; // push side: calls put()
`uvm_component_utils(job_source)
uvm_blocking_put_port #(dma_job_cfg) job_port;
function new(string name, uvm_component parent);
super.new(name, parent);
job_port = new("job_port", this);
endfunction
endclass
class job_runner extends uvm_component; // implements put()
`uvm_component_utils(job_runner)
uvm_blocking_put_imp #(dma_job_cfg, job_runner) job_imp;
function new(string name, uvm_component parent);
super.new(name, parent);
job_imp = new("job_imp", this);
endfunction
task put(dma_job_cfg job); // blocks until the job is done
`uvm_info("JOB", job.convert2string(), UVM_LOW)
endtask
endclass
// in the parent's connect_phase: src.job_port.connect(runner.job_imp);Which family to use, and what blocking means for each call, is the subject of put, get, peek and transport.
The topology table printed by dma_base_test at end of elaboration lists every port, export and imp with its real class. These rows come from a dma_smoke_test run:
apb apb_agent - @257
drv apb_driver - @459
rsp_port uvm_analysis_port - @476
seq_item_port uvm_seq_item_pull_port - @467
mon apb_monitor - @319
ap uvm_analysis_port - @327
sqr uvm_sequencer - @336
rsp_export uvm_analysis_export - @344
seq_item_export uvm_seq_item_pull_imp - @450
...
sb dma_scoreboard - @281
apb_export uvm_analysis_imp_apb - @289
in_export uvm_analysis_imp_in - @299
out_export uvm_analysis_imp_out - @308seq_item_export prints as uvm_seq_item_pull_imp: the name says export, the class says imp. Trust the class column.
The scoreboard's three inputs are imps of three different classes, one per ` uvm_analysis_imp_decl `` suffix.
The agent declares uvm_analysis_port #(apb_item) ap, yet no ap row appears under apb. The next lesson explains why.
Back to the opening. The checker was written like this:
// bad-example: a "subscriber" that owns a port instead of an imp
class deaf_sb extends uvm_component;
`uvm_component_utils(deaf_sb)
uvm_analysis_port #(apb_item) in_port; // should be an imp
int unsigned n_seen;
function new(string name, uvm_component parent);
super.new(name, parent);
in_port = new("in_port", this);
endfunction
function void write(apb_item t); // never called
n_seen++;
endfunction
endclass
// parent connect_phase: tk.ap.connect(sb.in_port); // compiles, elaborates, delivers nothingA small test, tlm_deaf_test, wrote three items into the publisher's port. The real log (the test is in tlm_tb.sv, the TLM lessons' file in the course bench):
UVM_INFO tlm_tb.sv(30) @ 30000: uvm_test_top.tk [TALK] wrote 3 items
UVM_INFO tlm_tb.sv(48) @ 30000: uvm_test_top.sb [DEAF] 0 item(s) received
UVM_ERROR : 0
UVM_FATAL : 0Every step was legal. A port may connect to another port (that is how a port is passed up through a parent). An analysis port skips the hierarchy check on its connections. And an analysis port with no imp at the end of its chain is allowed, because a monitor with no listeners is a normal thing. So write() walked a list of zero imps and returned. The checker's write() was just an ordinary method that nobody called. The fix is one line: declare uvm_analysis_imp #(apb_item, deaf_sb) in_imp and connect the publisher to it. The lesson behind it is the one the bench's scoreboard already applies: a checker must prove it checked something, which is why dma_scoreboard fails the test when it compared zero words.
uvm_component. What does it declare, and which method must it define?It declares uvm_analysis_imp #(apb_item, apb_cov) analysis_imp, constructs it with this, and defines function void write(apb_item t). The monitor's port is the caller; the collector implements. Extending uvm_subscriber #(apb_item) gives you that imp ready-made, as shown in imp_decl vs FIFOs.
Because it makes the call. The driver decides when it is ready and calls get_next_item; the sequencer implements it and returns the item through the output argument. Ownership follows the call, and the data simply travels back along it.
uvm_analysis_imp members?Both imps would call the same method name, write(), so the class could not tell the two streams apart (and both would need the same transaction type). ` uvm_analysis_imp_decl(_apb) ` generates a new imp class whose write() calls write_apb()`, which is how the bench's scoreboard gets three distinct methods.
Next: the scoreboard sits in the env, two levels above the monitor's port. In connect() rules and pass-through you will see which way each link must point to cross that hierarchy, and why the agent's ap never appears in the topology.