Self-checking testbenches¶
A self-checking testbench converts the specification into code and raises a failure whenever actual behavior differs.
Exhaustive testing¶
When the input space is small, test every combination.
for a_value in 0 to 15 loop
for b_value in 0 to 15 loop
for cin_value in 0 to 1 loop
a_s <= to_unsigned(a_value, a_s'length);
b_s <= to_unsigned(b_value, b_s'length);
if cin_value = 0 then
cin_s <= '0';
else
cin_s <= '1';
end if;
wait for 1 ns;
expected := a_value + b_value + cin_value;
actual := to_integer(unsigned'(carry_s & sum_s));
assert actual = expected
report "adder mismatch"
severity failure;
end loop;
end loop;
end loop;
A 4-bit adder needs only 16 × 16 × 2 = 512 cases, so exhaustive testing is ideal.
Reference functions¶
Use a function that expresses the mathematical specification:
function model_saturating_add(
a_value : natural;
b_value : natural;
maximum : natural
) return natural is
begin
if a_value + b_value > maximum then
return maximum;
else
return a_value + b_value;
end if;
end function;
Keep the reference model simple, readable, and independent of the RTL structure.
Cycle-accurate scoreboard¶
For a pipelined design, store expected results until their due cycle.
Concept:
cycle N: drive input A; compute expected A; enqueue it
cycle N+1: drive input B; compute expected B; enqueue it
cycle N+L: dequeue expected A; compare DUT output A
For fixed latency, an array shift register is enough:
expected_pipe(0) := model(input_s);
for stage in 1 to LATENCY loop
expected_pipe(stage) := expected_pipe(stage - 1);
end loop;
if output_valid_s = '1' then
assert output_s = expected_pipe(LATENCY)
severity failure;
end if;
Order variable updates carefully; a protected queue or record-based transaction is clearer for variable-latency protocols.
Transaction records¶
Group related values:
Driver, monitor, and scoreboard can pass a transaction rather than loose values.
Boundary-first strategy¶
Always include:
- Zero.
- One.
- Minimum and maximum representable values.
- Maximum−1 and sign boundaries.
- Equal operands.
- Carry/borrow/overflow transitions.
- Reset during activity.
- Enable changes.
- Back-to-back transactions.
- Long idle gaps.
Most arithmetic and counter bugs live at boundaries.
Invariants¶
An invariant must always be true, not just at one test point:
invariant_check : process(clk_s)
begin
if rising_edge(clk_s) then
assert not (read_enable_s = '1' and empty_s = '1')
report "Read attempted while FIFO empty"
severity failure;
end if;
end process;
Some invariants describe the environment rather than the DUT. Label them as assumptions so a failure has the right meaning.
Reproducible pseudo-random tests¶
Random tests must be repeatable:
- Use an explicit seed.
- Print the seed at the beginning.
- Print the failing transaction index.
- Preserve a failing seed as a directed regression test.
Do not use randomness as a substitute for boundary analysis or coverage.
Coverage without a framework¶
Simple counters already add value:
if saw_overflow then
overflow_cases := overflow_cases + 1;
end if;
-- At test end
assert overflow_cases > 0
report "Coverage hole: overflow never exercised"
severity failure;
For an FSM, count state visits and transitions. For a protocol, count each command, response, error, and backpressure case.
Expected-failure discipline¶
A test of error handling should check the error response, not intentionally end with a failure assertion. Reserve simulator failure for a broken requirement.
Runnable example in this library¶
The folder examples/adder contains:
- A generic unsigned adder.
- An exhaustive test of all 512 four-bit combinations.
- A file-driven test.
- A PowerShell runner for XSim.
Run: