C Part II · Module 25 · Debugging DPI-C and Simulator Integration
Wrong Assumptions About Simulator Execution
The C is correct and the Verilog is correct, and the defect is in an assumption that appears in neither file — so reading either one cannot falsify it. One C function measured four ways: ten calls where the author expected five, eleven for the same model on a level, one call where four writes happened, and none at all for a write that changed nothing. The error runs in both directions, so no correction factor exists, and the only instrument that finds any of it is a count.
Expert24 min readCPart IIVerificationDPI-C
Part II · Module 25 · Chapter 25.10 · Wrong Assumptions About Simulator Execution
1. A Defect That Is in Neither File
Every other failure in this module lives in a file. A signature mismatch is in a declaration, a layout bug is in an index expression, a lifetime bug is in a pointer's scope. You can open the file and point at it.
This one is different, and the difference is why it survives review.
The C is correct. The Verilog is correct. The defective object is an assumption — "my model runs once per transaction" — and an assumption is not written anywhere. It is not in the C, so reading the C cannot falsify it. It is not in the testbench, so reading the testbench cannot falsify it either.
Only counting can. This chapter is four counts.
2. Four Experiments on One Function
One C function, called from one testbench, with a per-call-site counter in C. Every number below is a measured delta, not a total.
A — Five clock cycles
site 0 calls: 10 (the author expected 5)The source contains one obvious call site, in a block that runs on posedge clk. Ten calls. There is a second always @(negedge clk) block, added for a reason nobody now remembers, that calls the same function.
Nothing is wrong with either block. The assumption "one call site in the source means one call per cycle" is what failed.
B — The same model, level-sensitive
site 2 calls: 11 (the same model, level-sensitive)Identical function, identical five-cycle window, sensitivity list @(clk or data) instead of an edge. Eleven calls — both clock edges and every data change.
The rate was set by the sensitivity list, which is in the testbench, not by the model and not by the clock.
C — Four writes in zero time
new calls: 1 model saw 0xf (four writes happened)Four single-bit writes to the same byte, in the same simulation instant, building 0x0F. One call, and the value the model received was the final one.
Three intermediate values were produced by executing statements and never crossed the boundary at all. A C-side counter incrementing once per call reports 1 where four writes occurred.
D — The same writes, spaced out
new calls: 2 model saw 0x3
new calls: 0 (the write happened; nothing changed)
new calls: 1 (a real change)The same two writes with a delay between them produce two calls instead of one. The model was not edited. The writes were not edited. A delay was moved, and the call count changed.
And writing a signal the value it already holds produces no call, because nothing changed; a real change produces one.
3. The Error Runs Both Ways
Put the four together and the useful property appears.
A and B are too many — 10 and 11 against an expected 5. C is too few — 1 against 4. D is neither, twice: the same writes give 1 or 2 depending on where a delay sits, and a write that changes nothing gives 0.
So there is no correction factor. You cannot rescue "once per transaction" by halving it, because sometimes it needs doubling and sometimes it needs quadrupling and sometimes the right answer is zero. The assumption is not approximately right. It is unrelated to the measurement.
Which gives the rule:
The call count is a property of the simulation's event structure, not of the C you wrote.
4. What This Breaks
The rule sounds abstract until you list what depends on it, and the list is longer than people expect.
Anything the C side counts. Chapter 23.6 established that a count is what distinguishes a check that is working from one that quietly stopped running, and Chapter 24.8 built reports on counts. Experiment A doubles every one of them; experiment C divides one by four. A report saying "4,096 transactions checked" is reporting calls.
Anything averaged over calls. A rate, a mean, a utilisation figure — the denominator is a call count, so it inherits every error above.
State keyed on "the previous call." Chapter 22.7's state-in-C machinery is correct and still produces nonsense if "the previous call" is not the previous transaction. Experiment C's model never saw three of the four values, so a model computing a delta against the last call computed it against the wrong baseline.
Any history the model builds. A sequence of received values is a sequence of calls, and experiment C measured that it is not a sequence of writes.
And the one that bites hardest: nothing above reports an error. Each of these produces a number that is the right type, the right magnitude, and completely wrong — which is Chapter 25.1's sixth symptom and the reason this class is filed under Expert.
5. The Instrument
The chapter's own method is the one that produced every number in §2, and it is three lines.
static int calls; /* one counter per call site */
calls++;
/* and, at the end of the run: */
printf("[C] site %d was called %d times\n", site, calls);Three properties make this worth doing rather than reasoning about.
It is cheap enough to leave in. An increment per call is noise against a simulation.
It falsifies, which reading cannot. The assumption "five calls" becomes a number printed beside the expectation, and 10 against 5 ends the discussion in one line.
It is per-site. A single global counter would have conflated §2's four experiments into one meaningless total; separating them is what made the two directions of error visible.
The strengthened form, when the count alone is not enough: have the design say which event this is. Chapter 25.4 used a self-describing payload — one whose every byte names its own position — to turn four indistinguishable array faults into four readable signatures, and the same idea applies to time rather than to layout: put a sequence number in the payload, generated where the transaction is generated, and carry it across the boundary. Then the model does not infer "this is the fourth transaction" from the fact that it is the fourth call; it is told, and experiment C's missing three announce themselves as a jump from 1 to 5.
6. Reading the Difference
Given a model that is producing numbers nobody trusts, these three questions separate this class from the rest of the module in a few minutes.
Does the C-side count match the testbench's count of the same thing? If the model says 4,096 and the testbench says 2,048, the answer is in this chapter and nowhere else. Print both.
Is every call site accounted for? Experiment A's second caller was in a block whose name was in the file all along. Search for the function name across the whole testbench, not the block you have open.
Does the model's history have gaps? If transactions carry a sequence number, a gap is experiment C and the count alone would never have shown it — 1 and 1 look identical whether or not three values vanished.
7. The Misconception: You Can Read It Off
"I do not need to count. I can read the testbench and work out how often my model runs."
Measured false twice, for two different reasons.
Experiment A's second caller is in a different block. It is in the file, it is perfectly readable, and it was not read — because the person checking opened the block they were thinking about and confirmed what they expected there. Reading is a search, and a search needs to be exhaustive to be a proof.
Experiment C's three lost values are in no file at all. This is the stronger reason. Four writes are visible in the source; four calls were assumed and one occurred; and the three that did not occur are an absence. There is no line to find, no block to overlook, nothing to read more carefully. The only evidence that they did not happen is a number that came out as 1.
The accurate position:
The frequency is not a property of any text, so no amount of reading text can establish it. It is a property of the run, and the only instrument that reports it is a count taken during the run.
This is the module's running theme in its last and cleanest form: the check that confirms a thing is usually the check that cannot distinguish it from its most likely failure. Reading the call site confirms that the model is called. It cannot distinguish being called once from being called twice, or four writes from one — and those are exactly the two failures.
8. What You Should Be Able to Say Now
- The defect is in an assumption, which is in neither file — so reading either file cannot falsify it.
- Measured, A: five clock cycles, one apparent call site → 10 calls; a second
alwaysblock calls the same function. - Measured, B: the same model, level-sensitive, same window → 11 calls; the sensitivity list sets the rate.
- Measured, C: four writes in zero time → 1 call, and the model saw only the final
0x0F. - Measured, D: the same writes with a delay → 2 calls; a write of the value already held → 0.
- The error runs both ways — too many and too few — so no correction factor exists.
- The rule: the call count is a property of the simulation's event structure, not of the C.
- What breaks: every C-side count, every average over calls, state keyed on "the previous call", and any history the model builds.
- Nothing reports an error — the numbers are the right type and the right magnitude.
- Time, cited from 23.4/23.5: simulation time does not advance inside a call (200M iterations, delta 0); a callback registered at
t=10for+20fired att=30. - The instrument is a per-site counter printed beside the expectation — cheap, and it falsifies.
- The strengthened form: a self-describing payload, so the model is told which transaction this is rather than inferring it from the call number.
- You cannot read the frequency off the source — a second caller is easy to miss, and a call that never happened is an absence with no line to find.
9. Exercises
- Add a per-call counter to a model and print it beside the number of transactions the testbench thinks it sent. Do they agree?
- Search your whole testbench for the model's name. How many call sites are there, and how many did you expect?
- Write four single-bit assignments to one byte with no delay between them, with a counting model on the signal. How many calls?
- Add a delay between two of those writes. Does the count change? Did you edit the model?
- Write a signal the value it already holds. How many calls, and why?
- §3 says no correction factor exists. Construct the argument from the four measurements.
- Add a sequence number to a payload and have the model report gaps. Which of §2's experiments does that detect, and which does it not?
- §4 lists four things that break. For a model you have, which of the four does it rely on?
- A model reports a per-transaction average. Name two of §2's experiments that would corrupt it, in opposite directions.
- §7 claims the frequency is not a property of any text. Explain why experiment C in particular cannot be read off the source.
10. Summary
- The defect is an assumption, present in neither file, so review cannot find it.
- Measured: 10 against 5 · 11 against 5 · 1 against 4 · 0 for a write that changed nothing.
- Both directions — too many and too few — so there is no correction factor.
- The call count belongs to the simulation's event structure, not to your C.
- Moving a delay changed 1 call into 2 with no edit to the model or the writes.
- It breaks every C-side count, every average over calls, "the previous call" state, and every history the model builds — silently.
- Time (23.4/23.5): no advance inside a call — 200M iterations, delta 0; a callback for
+20att=10fired att=30. - The instrument: a per-site counter printed beside the expectation; then a self-describing payload so the model is told, not inferring.
- You cannot read it off the source — a second caller is overlooked, and a call that never happened leaves no line to find.
That is the last of the failures. The module has nine of them now, each with a measurement and a method, and the final chapter is about the other direction entirely: the handful of habits that make most of these impossible to introduce in the first place.
Where this fits
Part of the C Programming curriculum.
