Triggers and phase waits

Everything a testbench can co_await to move simulation time, and the time type behind it. All of it lives in namespace cpptb::coro; testbenches conventionally write using namespace coro;. Scheduling is the authoritative reference for when each wait resumes and what state it observes; this page is the API surface.

Two rules apply to every awaitable here: co_await yields void unless a return type is stated, and awaiting outside a scheduler-owned coroutine is a runtime error that names the wait.

Edge triggers

RisingEdge

FallingEdge

Edge

explicit RisingEdge(Signal signal);
explicit FallingEdge(Signal signal);
explicit Edge(Signal signal);        // either edge

co_await RisingEdge{dut.clk};

Resume on the named edge of a one-bit signal. Every constructor is explicit, so the brace form above is the intended spelling. co_await RisingEdge{} resumes before the design evaluates the edge — the monitor instant; a driver that reads DUT outputs follows it with ReadWrite{} (see Coming from cocotb).

Phase waits

ReadWrite

ReadOnly

NextTimeStep

co_await ReadWrite{};      // evaluation settled; queued writes flush here
co_await ReadOnly{};       // end of the timestep; writing is illegal
co_await NextTimeStep{};   // start of the next timestep

The settle points of the current timestep, supplied by the project’s timing backend. One ordering rule is enforced at run time: after ReadOnly has been reached in a timestep, awaiting ReadWrite{} or ReadOnly{} again in that same timestep aborts with a diagnostic — await NextTimeStep{}, a Delay, or an edge first. set() from inside ReadOnly fails at the offending call site, exactly as in cocotb.

Delays and time

Delay

explicit constexpr Delay(SimTime duration);

co_await Delay{10_ns};

SimTime

struct SimTime {
    uint64_t femtoseconds = 0;
    constexpr uint64_t in_femtoseconds() const;
    constexpr uint64_t in_picoseconds() const;   // truncating
    constexpr uint64_t in_nanoseconds() const;   // truncating
};
// ==, <=> ordering, and operator+ are provided; there is no operator-.

Femtosecond-resolution value type. The literals are _fs, _ps, _ns, _us, and _ms — integer-valued only (1.5_ns does not compile), and there is no _s.

clock_cycles

Task<void> clock_cycles(Signal clock, uint64_t count);

Awaits RisingEdge{clock} exactly count times; count == 0 completes without suspending.

wait_until

Task<void> wait_until(SignalType signal, Predicate predicate, Signal clock);

Resumes when predicate(signal.get()) holds, sampling on rising edges of clock. The predicate is tested before the first wait, so an already-true condition returns without suspending.

Composition and timeouts

First

size_t winner = co_await First{RisingEdge{dut.clk}, Delay{100_ns}};

Races two or more triggers (edge, delay, or phase waits — not tasks) and yields the zero-based index of the one that fired first, in argument order. At least two triggers are required; the losing registrations are removed when the winner fires.

with_timeout

Two overloads with different result types. The trigger form accepts an edge trigger only and yields a plain enum:

Task<TimeoutOutcome> with_timeout(Trigger trigger, SimTime timeout);
// Trigger is RisingEdge, FallingEdge, or Edge

const auto outcome = co_await with_timeout(RisingEdge{dut.clk}, 3_ns);
if (outcome == TimeoutOutcome::TimedOut) { ... }

The task form wraps any Task<T> and yields a result object; on timeout the child task and any waits nested inside it are cancelled recursively:

Task<TimeoutResult<T>> with_timeout(Task<T> task, SimTime timeout);

const auto result = co_await with_timeout(read_response(dut), 100_ns);
if (result.timed_out()) { ... }
else use(result.value());

TimeoutOutcome

enum class TimeoutOutcome { Triggered, TimedOut };

A bare scoped enum — compare it, it has no member functions.

TimeoutResult

bool triggered() const;    // the task completed in time
bool timed_out() const;    // its negation
explicit operator bool() const;   // same as triggered()
T& value();                // the task's result; aborts after a timeout

TimeoutResult<void> carries the same predicates without a value. An exception thrown by the child is rethrown at the co_await when the child actually completed.

See also