Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
     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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
     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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
     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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
     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.

Four measured experiments on how often one C function is actually called, each with what the author assumed, what was measured, and what the gap costs. First, over five clock cycles a single apparent call site ran ten times, because a second always block was also calling it, and the same model wired level-sensitively ran eleven times over the same window. Second, four writes to one byte all in zero simulation time produced one call, and the model saw only the final value 0x0F. Third, the same two writes separated by a delay produced two calls. Fourth, writing the value the signal already held produced no call at all, while a real change produced one. The conclusion is that the call count is a property of the simulation's event structure rather than of the C code, so a count kept in C is a count of calls and not of anything in the design.The experimentWhat the authorassumedWhat was measuredWhat the gap costsFive clock cyclesone apparent call site inthe source5 callsone per cycle10 callsmeasured: a second alwaysblock calls it tooevery per-callcount is doubledand the rate looks like aperformance problemLevel-sensitiveinstead of edgethe same model, same fivecycles5 callsthe window did not change11 callsmeasured: both clockedges plus every datachangethe sensitivitylist sets the ratenot the model, not theclockFour writes to onebyteall in zero simulationtime4 callsone per write1 callmeasured: the model sawonly the final 0x0Fthree values nevercrossed at alla per-write counter in Cundercounts by threeThe same writes,spaced outplus a write of the valuealready heldthe same count asbeforethe writes did not change2 calls, then 0measured: a delay splitsone call in two; aredundant write, nonemoving a delaychanges the countwith no edit to the modelor the writesThe call count is a property of the simulation's event structure, not of the C you wrotemeasured four ways: 10 against 5, 11 against 5, 1 against 4, and 0 against 1 — so a count kept in C is a count ofcalls, and of nothing in the design12
Figure 1 — one C function, four experiments, each with the author's assumption beside the measurement. The first two rows measure too many calls, the third measures too few, and the fourth shows the count changing when a delay moves with no edit to the model or the writes. Because the error runs in both directions there is no correction factor to apply, which is what makes the assumption unsalvageable rather than merely inaccurate.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

  1. The defect is in an assumption, which is in neither file — so reading either file cannot falsify it.
  2. Measured, A: five clock cycles, one apparent call site → 10 calls; a second always block calls the same function.
  3. Measured, B: the same model, level-sensitive, same window → 11 calls; the sensitivity list sets the rate.
  4. Measured, C: four writes in zero time → 1 call, and the model saw only the final 0x0F.
  5. Measured, D: the same writes with a delay → 2 calls; a write of the value already held → 0.
  6. The error runs both ways — too many and too few — so no correction factor exists.
  7. The rule: the call count is a property of the simulation's event structure, not of the C.
  8. What breaks: every C-side count, every average over calls, state keyed on "the previous call", and any history the model builds.
  9. Nothing reports an error — the numbers are the right type and the right magnitude.
  10. Time, cited from 23.4/23.5: simulation time does not advance inside a call (200M iterations, delta 0); a callback registered at t=10 for +20 fired at t=30.
  11. The instrument is a per-site counter printed beside the expectation — cheap, and it falsifies.
  12. The strengthened form: a self-describing payload, so the model is told which transaction this is rather than inferring it from the call number.
  13. 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

  1. Add a per-call counter to a model and print it beside the number of transactions the testbench thinks it sent. Do they agree?
  2. Search your whole testbench for the model's name. How many call sites are there, and how many did you expect?
  3. Write four single-bit assignments to one byte with no delay between them, with a counting model on the signal. How many calls?
  4. Add a delay between two of those writes. Does the count change? Did you edit the model?
  5. Write a signal the value it already holds. How many calls, and why?
  6. §3 says no correction factor exists. Construct the argument from the four measurements.
  7. 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?
  8. §4 lists four things that break. For a model you have, which of the four does it rely on?
  9. A model reports a per-transaction average. Name two of §2's experiments that would corrupt it, in opposite directions.
  10. §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 +20 at t=10 fired at t=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.