Associative arrays and transaction IDs
Track outstanding reads by ID in an associative array, test for presence with exists, and use the entries left at the end to find responses that never came.
- Lesson
- 2.3
- Time
- 20 min
- Runs on
- Verilator
- Check
- 5 questions
The DUT returns a read response with ID 9, an ID the testbench never issued, because one bit flipped in the ID path. The scoreboard reads exp_data[9], which does not exist, and gets 0. The response data happens to be 0 as well, from memory that nobody had written, so the check passes. Nothing in the log mentions ID 9.
Reading a key that was never written returns the default value, which can look like a match.
Storage only for the keys you use
An associative array stores elements under keys of a type you choose, and it only holds the keys you have written. int exp_data [int]; maps an int key, here a transaction ID, to the expected data.
This suits sparse key spaces. A scoreboard that tags each read with a 32-bit number has 4 billion possible keys but perhaps 40 outstanding reads, and the array holds just those 40 entries.
| Method | What it does |
|---|---|
m[k] = v; | creates key k, or overwrites its value |
m.exists(k) | 1 if key k is present, 0 if not |
m.num() or m.size() | number of keys present |
m.delete(k) | removes key k |
m.delete() | removes every key |
m.first(k), m.next(k) | step through the keys in order; return 0 when none is left |
foreach (m[k]) | visits every key present, in key order |
Associative array methods for int m [int].
Reading a missing key
Reading a key that is not present does not create it and does not stop the simulation. It returns the element type's default value, 0 for an int, and the simulator may print a warning; Verilator 5.052 prints none.
So the value tells you nothing about presence. An ID whose expected data is 0 and an ID that was never issued both read as 0. Call exists first, every time, and treat a missing key as its own kind of failure.
Writing behaves differently from reading. An assignment to a missing key creates it, so a store that uses the wrong ID quietly creates a new entry. The end-of-test check then reports that entry as a read that never completed.
Read and compare
if (exp_data[rid] != rdata)
$error("ID %0d mismatch", rid);
An unknown ID with data 0 passes, and the stale entry for a real ID is never removed.
Check presence, compare, remove
if (!exp_data.exists(rid))
$error("unexpected response, ID %0d", rid);
else begin
if (exp_data[rid] != rdata)
$error("ID %0d mismatch", rid);
exp_data.delete(rid);
end
Reports the unknown ID, and removes each entry once it has been answered.
The life of one transaction
Request
If
exists(id)is already true, report a duplicate ID instead of overwriting the first expectation. Otherwise store the expected data underid.Response
If the ID is absent, report an unexpected response. Otherwise compare the data and then delete the key, so that a second response with the same ID is caught.
End of test
Every request should have been answered, so
num()must be 0. If it is not, list the leftover IDs withforeach: each one is a read that never completed.
The last step catches the quietest bug of all, a response that never arrives. A test that simply stops after its last request cannot see it, because nothing ever fails.
Several reads with the same ID
An AXI-style bus allows several reads with the same ID to be outstanding at once, answered in request order. A single value per key cannot hold them, so make the value a queue: int exp_q [int][$];.
This combines the previous lesson with this one. The key finds the ID, and the queue keeps the order within that ID. When a queue becomes empty, delete its key, so that num() still counts the IDs with work outstanding.
On a response, check exp_q.exists(rid) first, pop the front of exp_q[rid], and delete the key if that queue is now empty.
A sparse memory model
The same structure models a large memory cheaply. logic [31:0] mem [bit [31:0]]; covers a 4 GB address space, but it stores only the addresses the test has written.
Reading an address that was never written returns the element type's default value. For a logic element that is x, which flags a read of uninitialised memory, just as Module 1 recommends. With a two-state element, the same read returns a plausible 0.
A reference model built this way can also answer a question that a plain array cannot. mem.exists(addr) tells you whether the address was ever written, which is useful when a test must not read memory it never filled.
Choosing the key type
Use a key type that matches the data. Bus IDs and addresses suit an unsigned type such as int unsigned or bit [15:0], and names suit string. The standard visits integer keys in numerical order, using the key type to decide whether a value is negative.
Try it
Reading the missing key 9 returned 0 and left the count at 3. The checker then caught the unknown ID 9, the wrong data for ID 3, and the second response for ID 5, which had already been answered.
The final loop lists ID 1000, a read that never completed. Remove the return after the unexpected-response message and predict what changes.
outstanding=3 missing key reads 0, outstanding still 3 ID 5: match ID 9: unexpected response ID 3: expected a3 got a4 ID 5: unexpected response never answered: ID 1000Check yourself
Remember
- An associative array stores only the keys you write, so a huge ID space costs nothing until it is used.
- Reading a missing key returns a default value; use
existsto test presence. - Check for duplicates on request, check presence on response, and delete each key once it is answered.
- Keys left at the end of the test are responses that never arrived; check that
num()is 0. - Use a queue as the value when several requests can share one ID.
Practice
- Outstanding request lookup Implement · Easy
- Duplicate-ID protection Implement · Medium
- Out-of-order response matcher Implement · Hard