Clocking

The testbench owns clock timing. You declare a period once in C++ and the simulator drives the waveform from then on — there is nothing to configure at generation time, and no process of yours toggling the pin every half period.

This page covers registering input clocks, the reset sequence that usually travels with them, running several domains at once, and waiting on clocks the DUT produces.

Input clocks

Initialize each DUT input clock and register its full period before the first co_await:

Task<void> test_sequence(Dut dut, TestContext& test) {
    dut.core_clk.set_now(0);
    dut.bus_clk.set_now(0);

    test.start_clock(dut.core_clk, 4_ns);
    test.start_clock(dut.bus_clk, 10_ns, 1_ns);

    co_await clock_cycles(dut.core_clk, 2);
    co_await RisingEdge{dut.bus_clk};
}

The optional third argument is a phase: the first rising edge lands at phase + period / 2. Each clock has an independent period and phase, the period must be even and — like the phase — a whole multiple of the simulator precision, and the generated waveform is a fixed 50% duty cycle. Call start_clock() once per clock. The first registered clock is the primary clock used for the result’s cycle count; it does not restrict which clock a coroutine may await. The signature is in the TestContext reference.

start_clock() registers the clock with the runtime during test initialization; the generated SystemVerilog wrapper carries a driver task for every writable one-bit signal (including unpacked-array elements, which is how interface-member clocks arrive), asks the runtime after initialization which were registered, and those toggle in simulator time. C++ does not cross DPI merely to write each clock level, and nothing about the clocks is decided at build time. set_now() initializes the pin because it is cocotb’s setimmediatevalue() – correct under either write model, before the clock exists.

Registration is final: there is no supported way today to stop, pause, restart, or change the period or duty cycle of a scheduler-owned clock. Coherent runtime clock control is roadmap milestone 8; until it lands, a gated or reconfigurable clock is modeled in RTL and observed as a DUT-produced clock (below).

Reset

A clock and a reset usually travel together, and the standard write model makes the ordinary shape correct: initialize the clock pin with set_now(), start the clock, assert reset with a plain set() — the queued write flushes in the first timestep, well before the first rising edge at half a period — hold it for a couple of cycles, and release it:

dut.clk.set_now(0);
test.start_clock(dut.clk, 10_ns);

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

One caution for edge-sensitive resets: a design that resets on the reset transition itself (negedge rst_ni) must actually receive that edge. In a two-state simulator a pin held at 0 from time zero never transitions, the async reset never fires, and the design starts in an unreset state — so drive the pin to its inactive level first, or assert and then release, rather than holding it asserted from the start of time.

DUT-produced clocks

Do not call start_clock() for a divided, gated, recovered, or otherwise DUT-produced output clock. Await the generated signal directly:

co_await RisingEdge{dut.output_clk};
co_await FallingEdge{dut.output_clk};

One-bit DUT outputs use interest-gated observers. Their transitions call DPI only while C++ has a matching edge wait.

Delays and clock cycles

clock_cycles(clock, count) waits for rising edges of the selected signal. Delay{10_ns} waits for absolute simulator time and does not depend on any clock. Neither signal writes nor backdoor operations insert an implicit delay.

Clock generation and scheduler notification are separate concerns: the wrapper calls the C++ scheduler on every rising edge of a registered clock, and on falling edges only while a falling- or either-edge wait is active. Scheduling covers the dispatch machinery and trigger composition.