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.

Both ends call into the FIFOSolid arrows labelled with a call leave the caller. The FIFO's two exports are imps; the producer and the consumer each own a port, so both are initiators.
imp (implements)data: where the transaction goescall: who calls whom
Both ends call into the FIFOSolid arrows labelled with a call leave the caller. The FIFO's two exports are imps; the producer and the consumer each own a port, so both are initiators. component producer put_port: puts #1 to #5back to back imp fifo.put_export An imp: put, try_put,can_put queue uvm_tlm_fifo, size 2 used(), is_full(), flush() imp fifo.get_peek_export An imp: get, peek and theirtry_ forms component consumer get_port: one get every10 ns put(t) call, t goes instores tget(t) calloldest tt returns
  1. Fill0 ns (illustrative)

    At 0 ns the producer puts #1 and #2; each put returns at once. used() is 2 and is_full() is 1.

    More context

    Trace lines 1 to 4. Each packet travels with its put call into the FIFO's mailbox; nobody has taken anything yet.

    Follow the calls, values and waits
    1. data #1: producer → fifo.put_export
    2. data #1: fifo.put_export → uvm_tlm_fifo, size 2
    3. data #2: producer → fifo.put_export
    4. data #2: fifo.put_export → uvm_tlm_fifo, size 2
  2. Stall0 ns (illustrative)

    The producer calls put(#3). The FIFO is full, so the producer's thread waits inside the FIFO's put until space appears.

    More context

    Trace line 5. At 1 ns the test confirms used=2 is_full=1 can_put=0, and a try_put(#99) returns 0 at once instead of waiting.

    Follow the calls, values and waits
    1. data #3: producer → fifo.put_export
    2. Waiting: producer — blocked in put(#3): FIFO full
  3. Drain10 ns (illustrative)

    At 10 ns the consumer calls get and 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
    1. call get(t): consumer → fifo.get_peek_export
    2. data #1: uvm_tlm_fifo, size 2 → fifo.get_peek_export
    3. return #1: fifo.get_peek_export → consumer
  4. Resume10 ns (illustrative)

    In the same time step #3 is stored and the producer's put returns. 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 ns get frees a place.

    Follow the calls, values and waits
    1. data #3: fifo.put_export → uvm_tlm_fifo, size 2
    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 spacetry_put(t) returns 0 if fullcan_put()
get(t) waits for an itemtry_get(t) returns 0 if emptycan_get()
peek(t) waits for an itemtry_peek(t) returns 0 if emptycan_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.

Backpressure from a size-2 FIFO
012345time (ns)01020304050consumer took-#1#2#3#4#5used() after210producerwaits in put(#3)waits in put(#4)waits in put(#5)donefulllast put returns

The producer runs two packets ahead, then each get lets exactly one put return.

  1. producer: put(#3) at 0 ns

    The FIFO already holds #1 and #2, so the producer's thread waits inside fifo.put.

  2. consumer: get -> #1 at 10 ns

    The consumer's get removes #1. used() reads 1 at that moment.

  3. producer: put(#3) returned at 10 ns

    Space exists, so #3 is stored and the producer moves on to put(#4), which waits again.

  4. 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.

A request FIFO and a response FIFOArrows with a call name leave the caller; arrows ending in returns carry data back. The label in brackets is the channel export each port connects to.
call: who calls whomdata: where the transaction goes
A request FIFO and a response FIFOArrows with a call name leave the caller; arrows ending in returns carry data back. The label in brackets is the channel export each port connects to. component requester req_port, rsp_port queue request FIFO put_request_export,get_peek_request_export queue response FIFO put_response_export,get_peek_response_export component responder req_port, rsp_port put(q) call[put_request_export]get(q) call[get_peek_request_export]q returns to get(q)put(r) call[put_response_export]get(r) call[get_peek_response_export]r returns to get(r)
  1. 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 get on an empty FIFO waits inside the call. Nothing comes back until a request is stored.

    Follow the calls, values and waits
    1. call get(q): responder → request FIFO
    2. Waiting: responder — blocked in get(q)
  2. 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 put call; the size-1 request FIFO has room, so put returns at once.

    Follow the calls, values and waits
    1. call req #1: requester → request FIFO
    2. call get(r): requester → response FIFO
    3. Waiting: requester — blocked in get(r)
  3. Serve60 ns (illustrative)

    The responder's pending get returns #1 at 60 ns, and it works for 5 ns.

    More context

    Trace line 20. The get called at 0 ns returns now, carrying the request back as its output argument.

    Follow the calls, values and waits
    1. return req #1: request FIFO → responder
    2. Waiting: responder — working on #1 for 5 ns
    3. Released: responder
  4. 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 put call: the responder is an initiator too.

    Follow the calls, values and waits
    1. call rsp #101: responder → response FIFO
    2. Released: responder
  5. Resume65 ns (illustrative)

    The requester's pending get returns #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
    1. return rsp #101: response FIFO → requester
    2. call req #2: requester → request FIFO
    3. 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 needUseWho waits
The target to act during the callDirect port to impThe initiator, for the target's whole task
The two sides to run at different ratesuvm_tlm_fifo with a chosen sizeThe producer when full, the consumer when empty
A publisher that must never waituvm_tlm_analysis_fifo (unbounded), U3.5Only the consumer, when empty
A request and its answer, in orderuvm_tlm_req_rsp_channel or transportThe 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.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 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 : 0

Check yourself

Predict the outputThe producer puts 1 to 5 back to back into a uvm_tlm_fifo of size 3. The consumer waits 10 ns, then calls get, forever. What does the producer print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

class producer extends uvm_component;
  `uvm_component_utils(producer)
  uvm_blocking_put_port #(int) put_port;
  function new(string name, uvm_component parent); super.new(name, parent); put_port = new("put_port", this); endfunction
  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    for (int i = 1; i <= 5; i++) put_port.put(i);
    $display("producer finished at %0d ns", $time);
    phase.drop_objection(this);
  endtask
endclass

class consumer extends uvm_component;
  `uvm_component_utils(consumer)
  uvm_blocking_get_port #(int) get_port;
  function new(string name, uvm_component parent); super.new(name, parent); get_port = new("get_port", this); endfunction
  task run_phase(uvm_phase phase);
    int v;
    forever begin
      #10;
      get_port.get(v);
    end
  endtask
endclass

class depth_test extends uvm_test;
  `uvm_component_utils(depth_test)
  producer prod;
  consumer cons;
  uvm_tlm_fifo #(int) fifo;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    prod = producer::type_id::create("prod", this);
    cons = consumer::type_id::create("cons", this);
    fifo = new("fifo", this, 3);
  endfunction
  function void connect_phase(uvm_phase phase);
    prod.put_port.connect(fifo.put_export);
    cons.get_port.connect(fifo.get_export);
  endfunction
endclass

module tb;
  initial run_test("depth_test");
endmodule
Spot the bugThe producer puts #1 to #5 into a size-2 FIFO. Each option is the consumer's loop body, run every 10 ns, before it records t.id. The run passes when the consumer records 1 to 5 in order and the producer finishes. Which body fails?
ConceptA monitor's write function must hand each packet to a uvm_tlm_fifo of size 4 through a uvm_put_port. Which statement is right?
ConceptTwo consumers call get on the same uvm_tlm_fifo through two ports. A packet is put. What happens?

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_fifo has 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 before put blocks.
  • peek returns the same handle without removing it; flush discards 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_channel is two FIFOs that pair requests and responses by order; choose a connection by who may be made to wait.

Practice