Getting started

In about five minutes you will run a real cpptb test against real RTL, read the testbench that drives it, and break it on purpose to see what a failure looks like. Everything here uses examples/counter, the smallest complete cpptb project.

Prerequisites

Install Verilator, a C++20 compiler, CMake, Python 3.11 or newer, and uv. Then, from a checkout of this repository:

uv sync --frozen

1. Run your first test

uv run --frozen cpptb test --project examples/counter --build-dir build

The first run generates the typed DUT, compiles the simulator, and runs every registered test. It takes a minute or two; later runs reuse the cached build.

cpptb: counter built: .../build/cpptb/counter/obj/Vdpi_counter
PASS  counter_sequence checks=9 seed=1 wall_ms=0.062
PASS  counter_reset_defaults checks=1 seed=1 wall_ms=0.025
cpptb: 2 tests: 2 passed, 0 failed, 0 errors

Two tests, ten checks, no configuration written by hand. cpptb list shows the catalog without running it:

counter_sequence
counter_reset_defaults

Each test runs in its own fresh simulator process, so no test can inherit another’s DUT state.

2. Read the testbench

The design under test is an ordinary 8-bit counter — examples/counter/counter.sv:

module counter (
    input  logic       clk,
    input  logic       rst_n,
    input  logic       enable,
    output logic [7:0] count
);
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) count <= '0;
    else if (enable) count <= count + 1'b1;
  end
endmodule

You never write a wrapper, a probe list, or a DPI manifest for it. cpptb elaborates the RTL and generates a typed Dut whose members are the ports — so dut.count exists, is 8 bits wide, and a typo is a compile error.

Here is the heart of examples/counter/testbench.cpp:

Task<void> counter_sequence(Dut dut, TestContext& test) {
    dut.clk.set_now(0);                  // seed the pin before the clock starts
    test.start_clock(dut.clk, 10_ns);    // the testbench owns clock timing

    dut.rst_n.set(0);
    dut.enable.set(0);
    co_await clock_cycles(dut.clk, 2);   // hold reset for two cycles

    dut.rst_n.set(1);
    dut.enable.set(1);

    for (uint32_t expected = 1; expected <= kCountCycles; ++expected) {
        co_await RisingEdge{dut.clk};    // advance to the edge
        co_await ReadOnly{};             // let the design settle, then sample
        test.expect_eq("enabled count", dut.count.get(), expected);
    }
}

CPPTB_REGISTER_TEST(counter_sequence);

Four ideas carry most of cpptb:

  • CPPTB_REGISTER_TEST makes a coroutine a test. The registered name is the function name, and it is what cpptb list prints.

  • Signal access is explicit and never moves time. set() and get() are ordinary calls. Only co_await advances the simulation, so you can always see where time passes.

  • The testbench owns the clock. start_clock() registers a period; it does not suspend. Call it once per input clock before the first co_await.

  • Checks are values, not exceptions. expect_eq records a failure and keeps going; require_eq ends the test at once.

Note

Those two lines are the standard cpptb project setup, and this example’s cpptb.toml has them:

[build]
timing_backend = "verilator-direct"   # or "vpi"
deferred_writes = true

Both are also the defaults, so a new project gets them without a cpptb.toml at all; the examples state them so the configuration is visible. timing_backend supplies the phase waits — it is what makes co_await ReadOnly{} work — and deferred_writes = true selects cocotb’s write model, so a set() right after an awaited edge lands for the next edge rather than the one just awaited. See The write model.

3. Break it on purpose

A test you have never seen fail is a test you should not trust yet. In examples/counter/testbench.cpp, find the expect_eq inside the counting loop and ask for a count that is one too high:

test.expect_eq("enabled count", dut.count.get(), expected + 1);

Run it again. cpptb rebuilds automatically because the source changed:

FAIL  counter_sequence checks=9 seed=1 wall_ms=0.572
PASS  counter_reset_defaults checks=1 seed=1 wall_ms=0.029
cpptb: 2 tests: 1 passed, 1 failed, 0 errors

The console stays compact; the diagnostics land in the per-test log under build/cpptb/counter/results/:

cpptb: examples/counter/testbench.cpp:35: enabled count: actual=1 expected=2
cpptb: examples/counter/testbench.cpp:35: enabled count: actual=2 expected=3
cpptb: examples/counter/testbench.cpp:35: enabled count: actual=3 expected=4
...

Every failure carries its source location, its label, and both values. Because expect_eq is nonfatal, all eight mismatches are reported in one run instead of only the first. The same records are available as structured JSON next to the log, in counter_sequence.json — that file is the contract a CI system or a custom harness consumes.

Undo the change before moving on.

4. Start your own project

A cpptb project needs two files you write yourself:

my-project/
├── design.sv        # your RTL
└── testbench.cpp    # your registered coroutines

Everything generated goes under an ignored build/ directory. From an installed cpptb package, run these in the project directory:

cpptb build     # generate the typed DUT and compile the simulator
cpptb list      # show the registered tests
cpptb test      # run them all
cpptb test my_test_name    # run just one

From a checkout of this repository, prefix each with uv run --frozen.

Discovery is zero-configuration: cpptb accepts either root-level .sv/.v files plus testbench.cpp, or RTL under rtl/ with C++ under tests/. Add --top module_name if more than one module could be the root. Copy examples/counter as a starting skeleton, or examples/apb_regfile for a transaction-based bench.

Next steps

To learn

Read

The model behind every cpptb test

Core ideas

Concurrency, timeouts, events, queues, process control

Tasks and concurrency

Exactly when each co_await resumes

Scheduling

The test workflow and structured results

Running tests

Every common operation on one page

API reference

Every command-line option and cpptb.toml key

cpptb command line · cpptb.toml

Reading and driving internal signals, not just ports

Hierarchical DUT access

Working code for a DUT shaped like yours

Examples

Already write cocotb testbenches? Coming from cocotb maps the trigger vocabulary and covers three translation traps worth knowing before you convert anything.