Mailboxes and backpressure
Pass items between processes through a mailbox, and give it a size that holds a fast producer back instead of letting it run ahead of the DUT.
- Lesson
- 3.5
- Time
- 25 min
- Runs on
- Verilator
- Check
- 5 questions
A generator puts 1,000 transactions into an unbounded mailbox in zero simulation time and reports that it is done. The test then allows 50 cycles for the driver to finish, but the driver needs one cycle per transaction, so only 50 transactions reach the DUT. The coverage report still shows all 1,000, because the generator sampled coverage as it created them.
One column per clock cycle. Fifty cycles later, 950 transactions are still waiting when the test ends.
A channel that makes processes wait
A mailbox is a first-in, first-out channel between processes. put() adds a message at the back, and get() removes the oldest one. If the mailbox is empty, get() blocks until a message arrives, so the consumer needs no polling loop.
Declare it with the type it carries, as in mailbox #(packet) gen2drv = new(4);, and the compiler checks every put and get against that type. A mailbox is an object, so the variable holds no mailbox until new() creates one.
| Method | Blocks while | What it does | Returns |
|---|---|---|---|
put(m) | the mailbox is full (bounded only) | adds m at the back | nothing |
get(m) | the mailbox is empty | removes the oldest message into m | nothing |
peek(m) | the mailbox is empty | copies the oldest message into m and leaves it there | nothing |
try_put(m) | never | adds m if there is room | nonzero if added, 0 if full |
try_get(m), try_peek(m) | never | as get or peek, if there is a message | nonzero on success, 0 if empty |
num() | never | counts the waiting messages | the count at this moment |
Mailbox methods: the first three block, the try_ forms never do.
Backpressure from a bounded mailbox
new() or new(0) creates an unbounded mailbox, where put() never blocks. That is the opening story: the generator can create every transaction at time 0, however slowly the driver works.
An unbounded mailbox also hides a driver that stalls. If the driver stops taking items because of a DUT hang, the generator keeps going, and the backlog grows without any message in the log.
new(N) with N greater than 0 creates a bounded mailbox that holds at most N messages. When it is full, put() blocks until the consumer takes one. This holding back of a producer by a slower consumer is called backpressure.
One column per clock cycle. The producer can never be more than two items ahead.
With a bound, the generator runs at the driver's pace. When it reports done, at most N items are still waiting, so the end of the test only has to wait for a short, known drain. Choose N from the design: 1 makes the generator hand over one item at a time, and a larger N models a master with a request queue of that depth.
Nonblocking calls return a status
try_get() and try_put() never wait, so they need another way to say that nothing happened. Each returns a nonzero value on success and 0 on failure. A failed try_get(t) delivers no message, so t still holds an old value, which looks exactly like a real message.
Status ignored
void'(mb.try_get(t));
check(t); // may check the old t again
When the mailbox is empty, the previous transaction is checked a second time.
Status tested
if (mb.try_get(t))
check(t);
Only a message that was really taken is checked.
Use the try_ forms when a process must not stall, for example a model that services several channels in one loop. Everywhere else, the blocking forms are simpler and harder to get wrong.
num() is a snapshot. Another process can put or get right after you read it, so do not use num() to decide whether a later get() will block. It is reliable at the end of the test, once every producer and consumer has stopped, for checking that nothing was left behind.
A mailbox carries handles
A mailbox of class objects stores handles, references to objects, not copies of the objects (Module 4 covers classes in depth). Suppose a generator creates one object, then changes its fields and puts it again for every transaction. All the messages then point at the same object, and the driver sees only the latest values.
Create a new object for every message with new(), and treat an object as the receiver's once you have put it. The producer must not change it afterwards.
Knowing when the stream has ended
A consumer that loops on get() forever never learns that the producer has finished. Give it a way to know. The simplest is a fixed count agreed with the producer, as in repeat (n) mb.get(t);. Another is a counter of items sent, which the end of the test can wait on with wait (n_taken == n_sent).
A special end message also works, such as a null handle in a mailbox of objects, provided it can never be confused with a real item. The consumer stops when it gets that message, and the producer sends it only after its last real item.
Then finish with a check that the mailbox is empty. A leftover message means an item was generated but never driven, which is exactly the bug in the opening story.
Try it
The first two puts return at once. From then on each put() returns only when a get() makes room, so the producer is held to the consumer's pace. Both processes only record times; the parent prints everything after join.
Change new(2) to new() and predict the put times before you run it. Then try new(1).
item 0: put returned at t=0, got at t=10 item 10: put returned at t=0, got at t=20 item 20: put returned at t=10, got at t=30 item 30: put returned at t=20, got at t=40 item 40: put returned at t=30, got at t=50 left in mailbox: 0Check yourself
Remember
- A mailbox is a FIFO channel:
get()blocks when it is empty, andput()blocks when a bounded mailbox is full. new(N)with N greater than 0 bounds the mailbox and holds a fast producer to the consumer's pace.try_put()andtry_get()never block; always test the status they return.- A mailbox of objects carries handles, so create a new object for every message.
- Give the consumer a count or another end signal, and check at the end that the mailbox is empty.
Practice
- Exact-count consumer Implement · Easy