← Design Verification Interviews

Interview Question · Concept

One C Model, Two Agents

L2 Mid-LevelL3 Seniorsystemverilogdpireference models

The Question

The interviewer asks

Your C reference model keeps its state in a static global. It worked with one agent; with two agent instances the predictions are wrong for both. Why, and how do you fix it?

Reveal the Model Answer

A static global is one variable for the whole simulation, so both agents' calls update the same state. Give each instance its own state. A C model_new() returns a pointer, SystemVerilog stores it in a chandle, and every call passes it back. Add reset and free entry points too, so a mid-test reset and the end of test leave nothing stale behind.

One global total, two checkers
01234callAPB +3AXI +2APB +4APB resetAXI +5shared total35905AXI expects-2-7APB resets the global

Columns are calls in order. The AXI side expects 7 after its second packet and reads 5: the APB reset wiped its count as well.

Two Handles, Two StructsArrows from the handles show which struct each one points at. Arrows from the API show the struct a call reads or writes; the handle argument picks it.
Two handles, two structsArrows from the handles show which struct each one points at. Arrows from the API show the struct a call reads or writes; the handle argument picks it. SystemVerilog chandle a an addressSystemVerilog neverreads SystemVerilog chandle b a second address C model_add / reset/ delete the handle comesfirst struct Model A int total struct Model B int total points atpoints atreads or writesreads or writes
  1. Allocate

    Two calls to model_new give two structs with separate totals. The testbench keeps the two addresses in a and b.

  2. Add through each handle

    A receives 3 and then 4, B receives 2. Each call lands on the struct its handle names, so A reports 7 and B reports 2.

    Follow the calls, values and waits
    1. call model_add(a, 3): model_add / reset / delete → Model A
    2. return total 7: Model A → model_add / reset / delete
    3. call model_add(b, 2): model_add / reset / delete → Model B
    4. return total 2: Model B → model_add / reset / delete
  3. Reset A

    Reset clears A's total and keeps the struct where it is. B still holds 2, and the handles still point where they did.

    Follow the calls, values and waits
    1. call model_reset(a): model_add / reset / delete → Model A
  4. Delete A

    Delete returns the struct's memory. The variable a still holds the old address, so the testbench sets it to null at once.

    Follow the calls, values and waits
    1. call model_delete(a): model_add / reset / delete → Model A
Read the diagram as text
  • chandle a: an address SystemVerilog never reads
  • chandle b: a second address
  • model_add / reset / delete: the handle comes first
  • Model A: int total
  • Model B: int total
  • chandle a → Model A: points at
  • chandle b → Model B: points at
  • model_add / reset / delete → Model A: reads or writes
  • model_add / reset / delete → Model B: reads or writes

A global would be the single struct in the middle, shared by every caller. A handle per instance keeps the two totals apart.

Self-Check

Tick each key point you covered in your answer.

What Interviewers Listen For (Mid-Level)

A good mid-level answer points at the static global as the cause. It proposes a handle for each instance, with create, reset and free calls.

Cross-Questions

Where the interviewer goes next. Open a node to practise that follow-up.

WhyWhat IfBreak ItCompareScale It

  1. One C Model, Two Agents
  2. WhyWhy a Chandle and Not an IntegerL2 Mid-Level

    Why store the C pointer in a chandle rather than in a longint?

    Reveal the Model Answer

    A chandle always has the size of a pointer on the machine that runs the simulation. The language only lets you assign it, compare it with null and pass it to C. That stops SystemVerilog code from doing arithmetic on it or inventing values. A longint would work by accident on a 64-bit host and offer none of those guarantees.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "You can read the struct fields through the chandle in SystemVerilog."

    It is opaque: only C code can dereference it, through accessor functions you import.

    Source IEEE 1800-2017 §6.14

  3. CompareImporting C vs Exporting SystemVerilogL2 Mid-Level

    Compare an import and an export. When would the C model need an export?

    Reveal the Model Answer

    An import lets SystemVerilog call C; an export lets C call a SystemVerilog function or task. The model needs an export when it must call into the testbench. For example, it can report a mismatch through the testbench's error counter, or read a configuration value. C may call an export only from inside a context import call.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "C can call an exported function from any thread at any time."

    Exports may be called only while a context import is executing, so the simulator knows the scope and the time.

    Source IEEE 1800-2017 §35.5.3 (context tasks and functions)

  4. What IfTime Inside an Exported TaskL2 Mid-LevelL3 Senior

    What if the C model needs to wait for ten clock cycles in the middle of its work?

    Reveal the Model Answer

    C itself cannot consume simulation time. Make the import a context task, and have C call an exported SystemVerilog task such as wait_cycles(10), which may contain @(posedge clk). The calling SystemVerilog thread blocks until the exported task returns, while the rest of the simulation runs on.

    Out to C, Back into the Same InstanceArrows are calls. The request travels from the testbench into C and back into SystemVerilog; each return travels the same arrow backwards.
    Out to C, back into the same instanceArrows are calls. The request travels from the testbench into C and back into SystemVerilog; each return travels the same arrow backwards. SystemVerilog u0.visit(3) a call from instanceu0 C c_visit(3) context import C svGetScope() answers: tb.u0 SystemVerilog sv_accept(3) exported function state u0.accepted 0, then 3 import callwhich instance?export callaccepted += 3
    1. Call out

      Instance u0 calls the import. The simulator notes which scope made the call.

      Follow the calls, values and waits
      1. call c_visit(3): u0.visit(3) → c_visit(3)
    2. Ask for the scope

      svGetScope returns the calling instance, tb.u0. Without context on the import, C has no such answer.

    3. Call back in

      C calls the export. It runs in tb.u0, so it updates that instance's accepted, on the same thread, right now.

      Follow the calls, values and waits
      1. call sv_accept(3): c_visit(3) → sv_accept(3)
      2. data 3: sv_accept(3) → u0.accepted
    4. Return twice

      The export returns 3 to C, and C returns 3 to the testbench. No simulation time has passed.

      Follow the calls, values and waits
      1. return 3: sv_accept(3) → c_visit(3)
      2. return 3: c_visit(3) → u0.visit(3)
    Read the diagram as text
    • u0.visit(3): a call from instance u0
    • c_visit(3): context import
    • svGetScope(): answers: tb.u0
    • sv_accept(3): exported function
    • u0.accepted: 0, then 3
    • u0.visit(3) → c_visit(3): import call
    • c_visit(3) → svGetScope(): which instance?
    • c_visit(3) → sv_accept(3): export call
    • sv_accept(3) → u0.accepted: accepted += 3

    One thread, one chain of calls. The C function is in the middle of it, and the testbench waits for it to return.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "Call usleep() in C to let ten cycles pass."

    That pauses the simulator process on the wall clock while simulation time stands still. Lesson D1.7 measured time=0 after such a sleep.

    Source Lesson D1.7 review claims (a sleep in C left the simulation at time 0 on Verilator 5.052)

  5. What IfA Python Model Behind DPIL2 Mid-LevelL3 Senior

    Asked if the model is written in Python

    What if the reference model is written in Python? How does it reach the testbench?

    Reveal the Model Answer

    The standard defines DPI only between SystemVerilog and C. So a small C bridge starts the Python interpreter, calls the model and converts arguments and results. The bridge must catch every Python exception and turn it into a status code, because nothing may unwind into the simulator. Pass a whole packet per call, not one byte, because each call into Python is much slower than a C call.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "Let the Python exception propagate; the simulator will report it."

    The standard gives imported routines no way to raise into SystemVerilog, so an exception crossing the boundary has undefined behaviour. The bridge must handle it.

    Source IEEE 1800-2017 §35.5.1 (semantic constraints on imported routines)

  6. Scale ItIntegrating an Architecture Model at Subsystem LevelL3 Senior

    The architecture team owns a C++ model of the whole subsystem. How do you use it as the scoreboard's reference without forking it?

    Reveal the Model Answer

    Wrap it behind a thin, versioned DPI layer owned by verification: transaction in, predicted transactions out, one handle per instance, plus reset. Feed it from monitors, never drivers, so it predicts from what the DUT actually received. Agree on where the model is untimed, such as ordering between independent streams, and compare there by key rather than by cycle.

    Self-Check

    Tick each key point you covered in your answer.

    What Interviewers Listen For (Senior)

    A good senior answer separates the shared model from the wrapper that verification owns. It says exactly where the comparison must allow differences in order.

    Wrong Altitude

    Copies the model into the verification repository and patches it to match the RTL.

    The model then stops being independent: it quietly learns the RTL's bugs, and the architecture fixes never arrive.

  7. Break ItCopying an Object That Holds a ChandleL2 Mid-Level

    Asked if the model object is copied

    What if a model class holds a chandle, and someone copies the object with new this or a copy() method?

    Reveal the Model Answer

    The copy gets the same address, so both objects drive one C model. An update through either is seen by both. If the copy needs its own state, call a C clone function that allocates a new struct. Then decide which object deletes which model.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "The copied object now has its own C model."

    A shallow copy copies field values, and a chandle's value is an address. The twin adds 100 through the copy and the original shows 105.

    Run It

    A model object with a C accumulator, copied after adding 5.

    RUNVerilator + C/C++ over DPI · simulator service
    Edit the code, then run it
    Expected: a=105 b=105 FAIL: the copy shares one C model, because copying a chandle copies only the address
    See the Fix
    module tb;
      import "DPI-C" function chandle acc_new();
      import "DPI-C" function chandle acc_clone(input chandle h);
      import "DPI-C" function int acc_add(input chandle h, input int v);
      class model;
        chandle h;
        function new(); h = acc_new(); endfunction
        function model copy();
          model c = new this;              // a shallow copy of every field
          c.h = acc_clone(h);              // the fix: the copy gets its own C state
          return c;
        endfunction
      endclass
      model a = new, b;
      initial begin
        void'(acc_add(a.h, 5));
        b = a.copy();
        void'(acc_add(b.h, 100));
        $display("a=%0d b=%0d", acc_add(a.h, 0), acc_add(b.h, 0));
        if (acc_add(a.h, 0) != 5) $display("FAIL: the copy shares one C model, because copying a chandle copies only the address");
        else $display("PASS: each object owns its own C state");
        $finish;
      end
    endmodule
    

    Prints: a=5 b=105 PASS: each object owns its own C state

  8. CompareWhy a Chandle and Not an IntegerL2 Mid-Level

    Why store the C pointer in a chandle rather than in a longint?

    Reveal the Model Answer

    A chandle always has the size of a pointer on the machine that runs the simulation. The language only lets you assign it, compare it with null and pass it to C. That stops SystemVerilog code from doing arithmetic on it or inventing values. A longint would work by accident on a 64-bit host and offer none of those guarantees.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "You can read the struct fields through the chandle in SystemVerilog."

    It is opaque: only C code can dereference it, through accessor functions you import.

    Source IEEE 1800-2017 §6.14

What People Get Wrong

Common wrong answers and why they fail. Run It shows the wrong belief failing on a real simulator.

Misconception

"Each SystemVerilog object that calls the model gets its own copy of the C code's state."

C knows nothing about the calling object. The twin's two models share one counter until each holds its own handle.

Run It

Two port_model objects call a C accumulator that keeps a static total.

RUNVerilator + C/C++ over DPI · simulator service
Edit the code, then run it
Expected: a=7 b=7 FAIL: the static C counter is one variable shared by every model instance
See the Fix
module tb;
  import "DPI-C" function chandle acc_new();
  import "DPI-C" function void acc_add(input chandle h, input int v);
  import "DPI-C" function int  acc_total(input chandle h);
  class port_model;
    chandle h;                                // the fix: each model owns its C state
    function new(); h = acc_new(); endfunction
    function void add(int v); acc_add(h, v); endfunction
    function int total(); return acc_total(h); endfunction
  endclass
  port_model a = new, b = new;
  initial begin
    a.add(3); b.add(4);
    $display("a=%0d b=%0d", a.total(), b.total());
    if (a.total() == 3 && b.total() == 4) $display("PASS: each model owns its state behind its own chandle");
    else $display("FAIL: the static C counter is one variable shared by every model instance");
    $finish;
  end
endmodule

Prints: a=3 b=4 PASS: each model owns its state behind its own chandle

Misconception

"Declare the model functions pure so the simulator knows they are safe."

A model with state is the opposite of pure. The simulator may then drop or reuse calls, and the model misses updates.

Source IEEE 1800-2017 §35.5.2 (pure functions)

Missed Corner

Resets the SystemVerilog agent on a mid-test reset, but not the C model.

The model still holds transactions from before the reset and predicts responses the DUT has correctly discarded.

Source Lesson D1.6 example (a reset entry point restores the model's state)

Wrong Altitude

"Rewrite the model in SystemVerilog and avoid DPI."

That throws away the architecture team's checked model and creates a second model that must always match the first. The fix is a handle per instance, not a rewrite.

Learn It, Then Prove It