The Timing Model#

cocotb’s timing model is a simplification of the VPI‘s, VHPI‘s, and FLI‘s timing model, made by choosing the common subset of the most important aspects of those timing models.

Like the aforementioned timing models, cocotb’s is organized around time steps. A time step is a single point in simulated time, comprised of one or more evaluation cycles. For example, there is a time step that occurs on a rising clock edge, and the next time step is typically on the falling clock edge. Moving between time steps will advance simulated time.

Evaluation cycles occur when HDL or cocotb code is executed in reaction to events, such as simulated time advancing or a signal or variable changing value. The executed code tends to create more events, leading to the next evaluation cycle. Evaluation cycles don’t advance simulated time.

cocotb provides Python timing triggers allowing users to move through time steps and evaluation cycles. awaiting these triggers will cause the current cocotb coroutine to block until simulation reaches the specific point in time.

The Time Step#

Time steps are split into five phases in the cocotb timing model, some of which are repeated many times.

The time step starts in the Beginning of Time Step phase and ends in the End of Time Step phase. Evaluation cycles occur between these two points, with the simulator running in the HDL Evaluation phases, and cocotb code reacting in the Values Change and Values Settle phases.

_images/timing_model.svg

The phases of a time step#

Beginning of Time Step#

This is the phase where the NextTimeStep and Timer triggers will return. This phase begins the time step before any HDL code has executed or any signal or variable have changed value. In this phase, users are allowed to read and write any signal or variable. Once control returns to the simulator, it will enter the HDL Evaluation phase.

Users can await the following triggers in this phase:

HDL Evaluation#

This phase represents the time spent in the simulator evaluating always or process blocks, continuous assignments, or other HDL code. If a signal or variable passed to a ValueChange, RisingEdge, or FallingEdge trigger changes value accordingly, the simulator will enter the Values Change phase. Alternatively, after all values have changed and all HDL has finished executing, it will enter the Values Settle phase.

Note

cocotb is not executing during this phase.

Values Change#

This is the phase where the ValueChange, RisingEdge, or FallingEdge triggers will return. The signal or variable given to the trigger will have changed value, but no HDL that reacts to that value change will have executed; meaning “downstream” signals and variables will not have updated values. In this phase, users can read and write values on any signal or variable. After control returns to the simulator, it will re-enter the HDL Evaluation phase.

There are 0 or more of these phases in a time step and they are not distinguishable from cocotb. There is no way to jump to any particular one of these phases in a time step.

Users can await the following triggers in this phase:

Values Settle#

This is the phase where the ReadWrite trigger will return. All signals and variables will have their final values and all HDL will have executed for the time step. In this phase, users can read and write values on any signal or variable. If they do write, the simulator will re-enter the HDL Evaluation phase. Alternatively, the simulator will enter the End of Time Step phase.

There are 0 or more of these phases in a time step and they are not distinguishable from cocotb. There is no way to jump to any particular one of these phases in a time step.

Users can await the following triggers in this phase:

End of Time Step#

This is the phase where the ReadOnly trigger will return. All signals and variables will have their final values and all HDL will have executed for the time step. However, unlike the Values Settle phase, no writes are allowed in this phase; meaning no new evaluation cycles can occur. Users can still freely read in this phase. Once control returns to the simulator, it will move to the beginning of the next time step.

Users can await the following triggers in this phase:

Note

await ReadWrite() or await ReadOnly() in this phase are not well defined behaviors and will result in a RuntimeError being raised.

Triggers#

Timer#

The Timer trigger allows users to jump forward in simulated time arbitrarily. It will always return at the beginning of time step. Simulated time cannot move backwards, meaning negative and 0 time values are not valid. Timer cannot be used to move between evaluation cycles, only between time steps.

NextTimeStep#

NextTimeStep is like Timer, except that it always returns at the beginning of the next time step. The next time step could be at any simulated time thereafter, or never. It is only safe to use if there is scheduled behavior that will cause another time step to occur. Using NextTimeStep in other situations will result in undefined behavior.

ValueChange / RisingEdge / FallingEdge#

The edge triggers (ValueChange, RisingEdge, and FallingEdge) allow users to block a cocotb coroutine until a signal or variable changes value at some point in the future. That point in the future may be in a different evaluation cycle in the same time step, in a different time step, or never. Using an edge trigger on a signal or variable that will never change value will result in undefined behavior.

After returning, an edge trigger returns at the point where the signal or variable given to the trigger will have changed value, but no HDL that reacts to that value change will have executed; meaning “downstream” signals and variables will not have updated values.

Using a flip-flop for example, after an await RisingEdge(dut.clk), dut.clk will be 1, but the output of the flip-flop will remain the previous value. Wait until ReadWrite or ReadOnly to see the output change.

ReadWrite#

ReadWrite allows users to synchronize with the end of the current evaluation cycle. At the end of the evaluation cycle, all signals and variables will have their final values and all HDL will have executed for the time step. However, users are still allowed to write. This can be useful when trying to react combinationally to a registered signal.

For example, to set dut.valid high in reaction to dut.ready going high as a combinational circuit would, users could write the following.

while True:
    await RisingEdge(dut.clk)
    await ReadWrite()
    dut.valid.value = 0
    if dut.ready.value == 1:
        dut.valid.value = 1

ReadOnly#

ReadOnly allows users to jump to the end of the time step; allowing them to read the final values of signals or variables before more simulated time is consumed. This may be necessary if they wish to sample a signal or variable whose value glitches (changes value in multiple evaluation cycles).

Note

await ReadWrite() or await ReadOnly() after an await ReadOnly() is not well defined and will result in a RuntimeError being raised.

State Transitions#

N := time step
M := evaluation cycle

BEGIN{N} ->
    BEGIN{>N} : Timer
    BEGIN{N+1} : NextTimeStep
    CHANGE{N,>=0} : ValueChange/RisingEdge/FallingEdge
    CHANGE{>N,>=0} : ValueChange/RisingEdge/FallingEdge
    SETTLE{N,0} : ReadWrite
    END{N} : ReadOnly

CHANGE{N,M} ->
    BEGIN{>N} : Timer
    BEGIN{N+1} : NextTimeStep
    CHANGE{N,>M} : ValueChange/RisingEdge/FallingEdge
    CHANGE{>N,>=0} : ValueChange/RisingEdge/FallingEdge
    SETTLE{N,M} : ReadWrite
    END{N} : ReadOnly

SETTLE{N,M} ->
    BEGIN{>N} : Timer
    BEGIN{N+1} : NextTimeStep
    CHANGE{N,>M} : ValueChange/RisingEdge/FallingEdge
    CHANGE{>N,>=0} : ValueChange/RisingEdge/FallingEdge
    SETTLE{N,M+1} : ReadWrite
    END{N} : ReadOnly

END{N} ->
    BEGIN{>N} : Timer
    BEGIN{N+1} : NextTimeStep

Differences in Verilator#

Verilator is a cycle-based simulator, meaning it does not have discrete events like “value changed.” Instead it has “cycles”, meaning it evaluates all HDL code in a time step iteratively until quiescence, without stopping. This frees the simulator to evaluate the HDL however it sees fit, as long as it can maintain correctness, allowing for optimizations.

In Verilator, the timing triggers (Timer, NextTimeStep, ReadWrite, and ReadOnly) work as intended, as these map to “cycles” well. However, the value change triggers (ValueChange, RisingEdge, and FallingEdge) can not be handled in the middle of a cycle, so they are handled after the cycle has ended (equivalent to the Values Settle phase). The easiest way to think of the behavior is as if the value change triggers all have an implicit await ReadWrite() after them.