Fault injection

This example accesses three objects through their natural elaborated paths: a resolved net, a clocked variable, and a four-element memory. Slang infers all three from the RTL; there is no probe list or user-authored hierarchy file.

make cpp-dpi-fault-injection-run
make cpp-dpi-fault-injection-sv-run

Framework shape

The hand-written file imports the generated Dut, defines one coroutine, and registers it. Clock generation, reset stimulus, hierarchy operations, and checks are all visible in that user-facing sequence; generated DPI scheduling and transport stay under the target’s ignored build directory.

#include <cstdint>

#include "cpptb/cpptb.hpp"
#include "dut.hpp"

namespace cpptb::examples::fault_injection {
namespace {

using cpptb::Dut;
using coro::NextTimeStep;
using coro::ReadOnly;
using coro::RisingEdge;
using coro::Task;
using namespace coro;

Task<void> fault_injection_sequence(Dut dut, TestContext& test) {
    dut.clk.set_now(0);
    test.start_clock(dut.clk, 10_ns);

    dut.rst_n.set(0);
    dut.source_i.set(0);
    dut.memory_address.set(0);
    dut.memory_write.set(0);
    dut.memory_write_data.set(0);

    co_await clock_cycles(dut.clk, 2);
    dut.rst_n.set(1);

    // No sub-cycle settling delays: a check runs in ReadOnly -- the settled
    // region of the current timestep -- and the next mutation steps out of
    // it with NextTimeStep. The running clock provides the timesteps.
    co_await RisingEdge{dut.clk};
    co_await ReadOnly{};
    test.expect_eq("counter baseline", dut.counter_o.get(), 1u);
    co_await NextTimeStep{};

    dut.source_i.set(0x12);
    co_await ReadOnly{};
    test.expect_eq("resolved net baseline", dut.resolved_o.get(), 0x48u);
    co_await NextTimeStep{};

    dut.resolved_value.force(0xa5);
    test.expect_eq("force is immediately readable",
                   dut.resolved_value.get(), 0xa5u);
    co_await ReadOnly{};
    test.expect_eq("forced net reaches output", dut.resolved_o.get(), 0xa5u);
    co_await NextTimeStep{};

    dut.source_i.set(0x34);
    co_await ReadOnly{};
    test.expect_eq("force overrides changing driver", dut.resolved_o.get(),
                   0xa5u);
    co_await NextTimeStep{};

    dut.resolved_value.release();
    co_await ReadOnly{};
    test.expect_eq("release restores resolved driver", dut.resolved_o.get(),
                   0x6eu);
    co_await NextTimeStep{};

    dut.counter.force(0x55);
    co_await clock_cycles(dut.clk, 2);
    co_await ReadOnly{};
    test.expect_eq("RTL writes do not override force", dut.counter_o.get(),
                   0x55u);
    co_await NextTimeStep{};

    dut.counter.release();
    co_await RisingEdge{dut.clk};
    co_await ReadOnly{};
    test.expect_eq("RTL writes resume after release", dut.counter_o.get(),
                   0x56u);
    co_await NextTimeStep{};

    dut.memory[2].deposit(0xbeef);
    test.expect_eq("deposit is immediately readable",
                   dut.memory[2].get(), 0xbeefu);
    dut.memory_address.set(2);
    co_await ReadOnly{};
    test.expect_eq("deposit reaches memory output", dut.memory_read_data.get(),
                   0xbeefu);
    co_await NextTimeStep{};

    dut.memory[2].force(0xcafe);
    test.expect_eq("memory force is immediately readable",
                   dut.memory[2].get(), 0xcafeu);
    co_await ReadOnly{};
    test.expect_eq("memory force reaches output", dut.memory_read_data.get(),
                   0xcafeu);
    co_await NextTimeStep{};
    dut.memory[2].release();

    dut.memory_write_data.set(0x1234);
    dut.memory_write.set(1);
    co_await RisingEdge{dut.clk};
    co_await ReadOnly{};
    test.expect_eq("front-door write follows release",
                   dut.memory_read_data.get(), 0x1234u);
    co_await NextTimeStep{};
    dut.memory_write.set(0);
}

CPPTB_REGISTER_TEST(fault_injection_sequence);

}  // namespace
}  // namespace cpptb::examples::fault_injection

The complete sequence is kept in the repository as examples/fault_injection/testbench.cpp. The snippets below are copied from its hierarchy-focused portions.

Force a resolved net

force() and release() act immediately. Checks run in ReadOnly – the settled region of the current timestep – and each next mutation steps out of it with NextTimeStep; no settling delays are needed:

dut.source_i.set(0x12);
co_await ReadOnly{};
test.expect_eq("resolved net baseline", dut.resolved_o.get(), 0x48u);
co_await NextTimeStep{};

dut.resolved_value.force(0xa5);
test.expect_eq("force is immediately readable",
               dut.resolved_value.get(), 0xa5u);
co_await ReadOnly{};
test.expect_eq("forced net reaches output", dut.resolved_o.get(), 0xa5u);
co_await NextTimeStep{};

dut.source_i.set(0x34);
co_await ReadOnly{};
test.expect_eq("force overrides changing driver", dut.resolved_o.get(),
               0xa5u);
co_await NextTimeStep{};

dut.resolved_value.release();
co_await ReadOnly{};
test.expect_eq("release restores resolved driver", dut.resolved_o.get(),
               0x6eu);

The matching pure-SV operations are force i_dut.resolved_value = 8'ha5; and release i_dut.resolved_value;.

Clocked state and memory

A force on a variable remains effective while the RTL attempts to write it:

dut.counter.force(0x55);
co_await clock_cycles(dut.clk, 2);
co_await ReadOnly{};
test.expect_eq("RTL writes do not override force", dut.counter_o.get(),
               0x55u);
co_await NextTimeStep{};

dut.counter.release();
co_await RisingEdge{dut.clk};
co_await ReadOnly{};
test.expect_eq("RTL writes resume after release", dut.counter_o.get(),
               0x56u);

Memory elements use the same primitive interface. A deposit is a one-time blocking assignment; a force continues to override other writers:

dut.memory[2].deposit(0xbeef);
test.expect_eq("deposit is immediately readable",
               dut.memory[2].get(), 0xbeefu);

dut.memory[2].force(0xcafe);
co_await ReadOnly{};
test.expect_eq("memory force reaches output", dut.memory_read_data.get(),
               0xcafeu);
co_await NextTimeStep{};
dut.memory[2].release();

The complete C++ and pure-SV benches perform the same 13 checks over the same 7 rising clock edges.

Clock force limitation

Force is available on inferred hierarchical objects; ordinary port signals do not have .force(). Do not force a registered input clock as a workaround: start_clock() installs a generated simulator-side driver and scheduler edge source, so overriding only the RTL value would make observed scheduler edges disagree with the DUT clock.

Input clocks should be driven with start_clock(). DUT-produced output clocks are observed as ordinary signals. A coherent clock pause/override API would need to control both the simulator driver and scheduler registration together.

Attempting dut.clk.force(0) or dut.clk.release() fails at compile time with an explicit diagnostic explaining that ordinary DUT ports cannot be forced, that scheduler-owned clocks must remain under TestContext::start_clock(), and that coherent clock pause/override is not yet supported. The same diagnostic directs non-clock use cases to inferred hierarchy paths.