Blocking, non-blocking, FIFOs and channels
Choose between a blocking and a non-blocking call, put a bounded uvm_tlm_fifo between two components and predict when the producer stalls, and pair requests with responses through a uvm_tlm_req_rsp_channel.
- Lesson
- U3.4
- Time
- 30 min
- Runs on
- Verilator with UVM
- Check
- 4 questions
A generator puts packets straight into a driver model, so every put takes as long as the driver needs. Put a two-entry FIFO between them and the generator runs ahead until the FIFO fills, then waits. How far ahead, and when exactly it waits, is something you can read off a trace.
- Fill0 ns (illustrative)
At 0 ns the producer puts #1 and #2; each
putreturns at once.used()is 2 andis_full()is 1.More context
Trace lines 1 to 4. Each packet travels with its
putcall into the FIFO's mailbox; nobody has taken anything yet.Follow the calls, values and waits
- data #1: producer → fifo.put_export
- data #1: fifo.put_export → uvm_tlm_fifo, size 2
- data #2: producer → fifo.put_export
- data #2: fifo.put_export → uvm_tlm_fifo, size 2
- Stall0 ns (illustrative)
The producer calls
put(#3). The FIFO is full, so the producer's thread waits inside the FIFO'sputuntil space appears.More context
Trace line 5. At 1 ns the test confirms
used=2 is_full=1 can_put=0, and atry_put(#99)returns 0 at once instead of waiting.Follow the calls, values and waits
- data #3: producer → fifo.put_export
- Waiting: producer — blocked in put(#3): FIFO full
- Drain10 ns (illustrative)
At 10 ns the consumer calls
getand receives #1. That frees one place.More context
Trace line 9. The oldest packet leaves first; the call goes in, and #1 comes back as
get's output argument.Follow the calls, values and waits
- call get(t): consumer → fifo.get_peek_export
- data #1: uvm_tlm_fifo, size 2 → fifo.get_peek_export
- return #1: fifo.get_peek_export → consumer
- Resume10 ns (illustrative)
In the same time step #3 is stored and the producer's
putreturns. From now on the producer is paced by the consumer: one put per 10 ns.More context
Trace lines 10 and 11. The producer at once calls
put(#4), which waits again until the 20 nsgetfrees a place.Follow the calls, values and waits
- data #3: fifo.put_export → uvm_tlm_fifo, size 2
- Released: producer
Read the diagram as text
- producer: put_port: puts #1 to #5 back to back
- fifo.put_export: An imp: put, try_put, can_put
- uvm_tlm_fifo, size 2: used(), is_full(), flush()
- fifo.get_peek_export: An imp: get, peek and their try_ forms
- consumer: get_port: one get every 10 ns
- producer → fifo.put_export: put(t) call, t goes in
- fifo.put_export → uvm_tlm_fifo, size 2: stores t
- consumer → fifo.get_peek_export: get(t) call
- uvm_tlm_fifo, size 2 → fifo.get_peek_export: oldest t
- fifo.get_peek_export → consumer: t returns
A bounded FIFO lets the producer run ahead by its size, then applies backpressure: the full FIFO blocks put.
What blocking means in time
U3.3 named the flavours; here is what they mean in time against a FIFO. A blocking method is a task: the caller's thread may wait inside it, for any amount of simulated time. A non-blocking method is a function: it returns in the same time step, with 1 when it did the work and 0 when it could not.
| Blocking (task) | Non-blocking (function) | Ask first (function) |
|---|---|---|
put(t) waits for space | try_put(t) returns 0 if full | can_put() |
get(t) waits for an item | try_get(t) returns 0 if empty | can_get() |
peek(t) waits for an item | try_peek(t) returns 0 if empty | can_peek() |
transport(req, rsp) | nb_transport(req, rsp) |
What each flavour does when a uvm_tlm_fifo is full or empty.
The flavour decides where you may call it. A function cannot call a task, so code inside a write function or another function can use only the non-blocking forms.
Blocking call in a function
function void send(pkt t);
put_port.put(t); // put is a task
endfunction
// %Error-FUNCTIMECTL: Functions cannot
// invoke tasks
Rejected when it compiles: a function must finish without waiting.
Non-blocking call in a function
function bit send(pkt t);
if (put_port.try_put(t)) return 1;
dropped++; // decide what a full FIFO means
return 0;
endfunction
Compiles. The caller must handle the 0 return, or the packet is gone.
A try_put that returns 0 does nothing at all. In the example the test calls try_put(#99) on the full FIFO at 1 ns; it returns 0 and #99 never appears. Count or report those returns, or the loss is silent.
A FIFO between two initiators
uvm_tlm_fifo #(T) is a component with a storage queue and imps on both sides. Its put_export is a uvm_put_imp and its get_peek_export is a uvm_get_peek_imp. The producer pushes in through a port, the consumer pulls out through a port, and neither knows the other exists.
The third constructor argument is the size: new("fifo", this, 2) holds two packets, the default is 1, and 0 means unbounded. The trace below follows the example.
The producer runs two packets ahead, then each get lets exactly one put return.
producer: put(#3) at 0 ns
The FIFO already holds #1 and #2, so the producer's thread waits inside
fifo.put.consumer: get -> #1 at 10 ns
The consumer's
getremoves #1.used()reads 1 at that moment.producer: put(#3) returned at 10 ns
Space exists, so #3 is stored and the producer moves on to
put(#4), which waits again.producer: put(#5) returned at 30 ns
The producer finishes. The consumer drains #4 and #5 at 40 and 50 ns.
Size is a design decision. A size of 1 makes the producer wait for nearly every packet; size 0 never blocks but lets a runaway producer fill memory. Pick the depth the real hardware would have, or the depth you want to stress.
Looking inside and clearing out
The FIFO answers questions without changing anything: size() is its capacity, used() the packets held, and is_empty() and is_full() the two extremes. At 1 ns the example prints size=2 used=2 is_full=1 can_put=0.
peek and try_peek return the oldest packet without removing it. Two peeks give the same handle: the example prints 1 for a == b. An output argument of a class type copies the handle, never the object; a separate object would need clone(). Change a field through a peeked handle and the consumer's later get sees the change.
flush() discards what the FIFO holds. In a variant that flushes at 1 ns, #1 and #2 are never seen, and the producer waiting in put(#3) refills the FIFO at once: put(#3) returns at 1 ns. So flush alone is not a reset. A reset must also stop or restart the producer and the consumer, and count the discarded packets itself.
Two more limits apply. flush refuses, with the error flush failed, when packets are stored while a get is still pending. And the FIFO's two analysis ports, put_ap and get_ap, publish each successful put and get; packets removed by flush, and packets only peeked at, are never published.
Requests and responses through a channel
Some exchanges need an answer. uvm_tlm_req_rsp_channel #(REQ, RSP) is two FIFOs, one for requests and one for responses, each of size 1 by default. Both sides are initiators again, with two ports each.
- Waiting to serve0 ns (illustrative)
From 0 ns the responder sits in
req_port.get(q): its call has reached the empty request FIFO and waits there.More context
A blocking
geton an empty FIFO waits inside the call. Nothing comes back until a request is stored.Follow the calls, values and waits
- call get(q): responder → request FIFO
- Waiting: responder — blocked in get(q)
- Request60 ns (illustrative)
At 60 ns the requester puts request #1 and returns at once, then calls
rsp_port.get(r)and waits.More context
Trace line 19. Request #1 rides in the
putcall; the size-1 request FIFO has room, soputreturns at once.Follow the calls, values and waits
- call req #1: requester → request FIFO
- call get(r): requester → response FIFO
- Waiting: requester — blocked in get(r)
- Serve60 ns (illustrative)
The responder's pending
getreturns #1 at 60 ns, and it works for 5 ns.More context
Trace line 20. The
getcalled at 0 ns returns now, carrying the request back as its output argument.Follow the calls, values and waits
- return req #1: request FIFO → responder
- Waiting: responder — working on #1 for 5 ns
- Released: responder
- Answer65 ns (illustrative)
At 65 ns the responder puts #101 into the response FIFO.
More context
Trace line 21. The response is the argument of the responder's own
putcall: the responder is an initiator too.Follow the calls, values and waits
- call rsp #101: responder → response FIFO
- Released: responder
- Resume65 ns (illustrative)
The requester's pending
getreturns #101 at 65 ns, and it sends request #2.More context
Trace lines 22 and 23. Request #2 follows the same path and is answered with #102 at 70 ns (lines 24 to 26).
Follow the calls, values and waits
- return rsp #101: response FIFO → requester
- call req #2: requester → request FIFO
- Released: requester
Read the diagram as text
- requester: req_port, rsp_port
- request FIFO: put_request_export, get_peek_request_export
- response FIFO: put_response_export, get_peek_response_export
- responder: req_port, rsp_port
- requester → request FIFO: put(q) call [put_request_export]
- responder → request FIFO: get(q) call [get_peek_request_export]
- request FIFO → responder: q returns to get(q)
- responder → response FIFO: put(r) call [put_response_export]
- requester → response FIFO: get(r) call [get_peek_response_export]
- response FIFO → requester: r returns to get(r)
The channel pairs requests with responses only by order. A responder that answers out of order needs IDs in the packets.
A related component, uvm_tlm_transport_channel, adds a transport_export: one transport(req, rsp) call puts the request and then waits for the response from the same two FIFOs.
| You need | Use | Who waits |
|---|---|---|
| The target to act during the call | Direct port to imp | The initiator, for the target's whole task |
| The two sides to run at different rates | uvm_tlm_fifo with a chosen size | The producer when full, the consumer when empty |
| A publisher that must never wait | uvm_tlm_analysis_fifo (unbounded), U3.5 | Only the consumer, when empty |
| A request and its answer, in order | uvm_tlm_req_rsp_channel or transport | The requester until the response arrives |
The choice is about who may be made to wait, and for how long.
Try it
Lines 1 to 18 are the FIFO: two puts return at once, the third waits until the first get at 10 ns, and the producer finishes at 30 ns. Lines 19 to 26 are the channel: each request waits 5 ns for its response.
Try it: change the size to 0 (unbounded) and all five puts return at 0 ns. Change it to 1 and the producer finishes at 40 ns.
1 0 ns producer: put(#1) 2 0 ns producer: put(#1) returned 3 0 ns producer: put(#2) 4 0 ns producer: put(#2) returned 5 0 ns producer: put(#3) 6 1 ns test: size=2 used=2 is_full=1 can_put=0 7 1 ns test: try_put(#99) returned 0; nothing was stored 8 1 ns test: two peeks return the same handle: 1 (#1) 9 10 ns consumer: get -> #1 (used=1) 10 10 ns producer: put(#3) returned 11 10 ns producer: put(#4) 12 20 ns consumer: get -> #2 (used=1) 13 20 ns producer: put(#4) returned 14 20 ns producer: put(#5) 15 30 ns consumer: get -> #3 (used=1) 16 30 ns producer: put(#5) returned 17 40 ns consumer: get -> #4 (used=1) 18 50 ns consumer: get -> #5 (used=0) 19 60 ns requester: put request #1 20 60 ns responder: got request #1 21 65 ns responder: put response #101 22 65 ns requester: got response #101 23 65 ns requester: put request #2 24 65 ns responder: got request #2 25 70 ns responder: put response #102 26 70 ns requester: got response #102 UVM_ERROR : 0Check yourself
Remember
- Blocking methods are tasks and may wait; non-blocking
try_methods are functions that return 0 when they cannot act, and only they may be called from a function. uvm_tlm_fifohas imps on both sides, so producer and consumer are both initiators; its size (default 1, 0 = unbounded) sets how far the producer may run ahead beforeputblocks.peekreturns the same handle without removing it;flushdiscards the contents without publishing them, and a blocked producer refills the FIFO at once, so a reset must also control the producer.- A
uvm_tlm_req_rsp_channelis two FIFOs that pair requests and responses by order; choose a connection by who may be made to wait.
Practice
- The trace window that never drops quietly Implement · Medium