A colleague adds a producer that puts three APB items into a TLM FIFO, logs each return, and pings you: the second put() returned 20 ns after the first, and the third 20 ns after that. Is the FIFO broken?
UVM_INFO tlm_tb.sv(83) @ 0: uvm_test_top.req [FIFO] put #0 returned
UVM_INFO tlm_tb.sv(83) @ 20000: uvm_test_top.req [FIFO] put #1 returned
UVM_INFO tlm_tb.sv(83) @ 40000: uvm_test_top.req [FIFO] put #2 returnedNothing is broken. The consumer takes one item every 20 ns, the FIFO holds one item, and a blocking put() waits until there is room. The receiver is setting the pace, which is exactly what these interfaces are for. When do you want that, and which call gives you which behaviour?
Analysis write() never waits: the monitor reports what happened and moves on. The flow-controlled families are for links where the receiver must be able to say not yet. put hands a transaction over; get takes one and removes it; peek looks at the next one without removing it; transport sends a request and returns its response in one call. Each comes in a blocking form, a task that may wait, and a non-blocking form, a function that returns at once with a status bit.
Think of a doorway with a doorman. A blocking put queues at the door until they wave you in. try_put asks once and walks away if the answer is no. can_put only asks whether there is room right now; someone else may take that room before you move. Peeking is looking through the window; getting is walking out with the item.
Family | Blocking (task) | Non-blocking (function) | Port class examples |
|---|---|---|---|
put |
|
|
|
get |
|
|
|
peek |
|
|
|
transport |
|
|
|
The provider must implement every method in the interface its imp declares: a uvm_put_imp needs put, try_put and can_put; a uvm_blocking_put_imp needs only put. A component that implements two imps of the same family uses the matching declaration macro, such as ` uvm_blocking_put_imp_decl(_cfg) `, exactly as the scoreboard uses uvm_analysis_imp_decl ``.
These excerpts are from the test that produced the log: a requester with a put port, a responder with a get port, and a uvm_tlm_fifo between them. The FIFO's put_export and get_export are imps, so both components simply connect across to them.
// snippet: from tlm_tb.sv in the course bench
// requester: uvm_blocking_put_port #(apb_item) pport;
for (int i = 0; i < 3; i++) begin
req = apb_item::type_id::create("req");
req.addr = 8'h10 + 4 * i;
pport.put(req); // waits while the fifo is full
`uvm_info("FIFO", $sformatf("put #%0d returned", i), UVM_LOW)
end
// responder: uvm_blocking_get_port #(apb_item) gport;
forever begin
apb_item t;
#20ns gport.get(t); // one item every 20 ns
end
// test connect_phase (fifo = new("fifo", this): default depth 1)
req.pport.connect(fifo.put_export);
rsp.gport.connect(fifo.get_export);The FIFO's default depth of 1 is a classic interview question; the FIFO classes and their other knobs are covered in TLM1 channels and port cardinality.
Non-blocking calls suit a component that must keep doing other work, such as a model that services several queues in one thread. The status bit is the whole contract: a try_put that returns 0 did nothing, and the item is still yours to retry or to count as dropped.
// snippet
uvm_nonblocking_put_port #(apb_item) nb_port;
apb_item backlog[$];
task run_phase(uvm_phase phase);
forever begin
@(posedge vif.clk); // one attempt per clock, never a zero-time loop
while (backlog.size() > 0 && nb_port.try_put(backlog[0]))
void'(backlog.pop_front()); // accepted: drop it from the backlog
// can_put() could be checked first, but only try_put's return is binding
end
endtaskpeek returns the next item and leaves it where it is; a second peek returns the same item. get returns it and removes it. A checker that peeks to decide whether an item is interesting must still get it eventually, or the queue never advances. And a peeked handle is the same object that is still queued, so changing it changes what the later get returns.
transport couples a request with its response, which suits a service such as a register model that answers each access. The caller blocks until the response comes back, so request and response can never be mispaired.
// snippet
class dma_reg_model extends uvm_component;
`uvm_component_utils(dma_reg_model)
uvm_blocking_transport_imp #(apb_item, apb_item, dma_reg_model) tr_imp;
bit [31:0] regs[bit [7:0]];
function new(string name, uvm_component parent);
super.new(name, parent);
tr_imp = new("tr_imp", this);
endfunction
task transport(input apb_item req, output apb_item rsp);
$cast(rsp, req.clone());
if (req.write) regs[req.addr] = req.data;
else rsp.rdata = regs.exists(req.addr) ? regs[req.addr] : 32'h0;
endtask
endclassseq_item_port.get_next_item() behaves like a blocking get with two additions: the sequencer arbitrates between sequences before it answers, and the driver must call item_done() to finish the handshake. The pull port also offers get() (take the item and finish at once) and peek(). The details belong to the driver handshake.
The opening log is the whole story. put #0 returns at time 0 because the empty FIFO had room for one item. put #1 waits until the consumer's first get at 20 ns (20000 ps), and put #2 until the second at 40 ns. With a depth of 3, all three puts in this test would return at time 0 and no backpressure would be visible at all; a fourth consecutive put would be the first to wait for a get. Depth decides when backpressure starts, never whether the consumer is the bottleneck.
A reference-model engineer wanted the model to service two input queues in one thread, so they wrote a polling loop with non-blocking gets:
// bad-example: polls forever without letting time advance
forever begin
if (cfg_port.try_get(cfg)) apply(cfg);
if (data_port.try_get(d)) predict(d);
endBoth queues were empty at the start of a test, so neither try_get succeeded and the loop went round again, at the same simulation time, forever. Nothing else could run: the producers that would have filled the queues never got a turn. The simulation sat at time 0 burning CPU. The UVM timeout did not fire, because +UVM_TIMEOUT and set_timeout count simulated time, and simulated time had stopped. The regression job was eventually killed by the farm's wall-clock limit with no UVM log at all.
The fix is to give every polling loop something that can block: a clock edge, an event, or a blocking call. Here the cleanest version uses two threads with blocking gets, one per queue. If one thread must poll, it waits a clock between attempts.
can_put() returned 1, and the very next try_put() returned 0. How is that possible?can_put reports the state at the moment you asked. Another producer, in another thread in the same timestep, filled the last slot before your try_put. Only the try_put return is binding, so code must handle 0 even after a positive can_put.
If the loop blocks between peeks it sees the same item forever; if it does not, it spins in zero time. Either way the queue never advances. The checker must get the item to consume it, or skip it explicitly.
try_get loop?The timeout is measured in simulated time. A loop that never blocks never lets simulated time advance, so the timeout's own delay never expires. Only a wall-clock limit outside the simulator ends it.
Next: the monitor links work the other way: no waiting, many listeners. Analysis broadcast shows how one write() reaches every subscriber, in an order you must not rely on.