Source code for cocotb.clock

# Copyright cocotb contributors
# Copyright (c) 2013 Potential Ventures Ltd
# Copyright (c) 2013 SolarFlare Communications Inc
# Licensed under the Revised BSD License, see LICENSE for details.
# SPDX-License-Identifier: BSD-3-Clause

"""A clock class."""

import logging
import warnings
from decimal import Decimal
from fractions import Fraction
from logging import Logger
from typing import ClassVar, Type, Union

import cocotb
from cocotb._py_compat import (
    Literal,
    TypeAlias,
    cached_property,
)
from cocotb._typing import TimeUnit
from cocotb.handle import (
    Deposit,
    Force,
    Immediate,
    LogicObject,
    _GPISetAction,
    _trust_inertial,
)
from cocotb.simulator import clock_create
from cocotb.task import Task
from cocotb.triggers import (
    ClockCycles,
    Event,
    FallingEdge,
    RisingEdge,
    Timer,
    ValueChange,
)
from cocotb.utils import get_sim_steps, get_time_from_sim_steps

__all__ = ("Clock",)

Impl: TypeAlias = Literal["gpi", "py"]


_valid_impls = ("gpi", "py")


[docs] class Clock: r"""Simple 50:50 duty cycle clock driver. .. code-block:: python c = Clock(dut.clk, 10, "ns") c.start() Args: signal: The clock pin/signal to be driven. period: The clock period. .. note:: Must convert to an even number of timesteps. unit: One of ``'step'``, ``'fs'``, ``'ps'``, ``'ns'``, ``'us'``, ``'ms'``, ``'sec'``. When *unit* is ``'step'``, the timestep is determined by the simulator (see :make:var:`COCOTB_HDL_TIMEPRECISION`). .. versionchanged:: 2.0 Renamed from ``units``. impl: One of ``'auto'``, ``'gpi'``, ``'py'``. Specify whether the clock is implemented with a :class:`~cocotb.simulator.GpiClock` (faster), or with a Python coroutine. When ``'auto'`` is used (default), the fastest implementation that supports your environment and use case is picked. .. versionadded:: 2.0 set_action: One of :class:`.Immediate`, :class:`.Deposit`, or :class:`.Force`. Specify the action to use when setting the clock signal value. Defaults to the value of :attr:`default_set_action`. .. versionadded:: 2.0 When *impl* is ``'auto'``, if :envvar:`COCOTB_TRUST_INERTIAL_WRITES` is defined, the :class:`~cocotb.simulator.GpiClock` implementation will be used. Otherwise, the Python coroutine implementation will be used. See the environment variable's documentation for more information on the consequences of using the simulator's inertial write mechanism. If you need more features like a phase shift and an asymmetric duty cycle, it is simple to create your own clock generator (that you then :func:`cocotb.start_soon`): .. code-block:: python async def custom_clock(): # pre-construct triggers for performance high_time = Timer(high_delay, unit="ns") low_time = Timer(low_delay, unit="ns") await Timer(initial_delay, unit="ns") while True: dut.clk.value = 1 await high_time dut.clk.value = 0 await low_time If you also want to change the timing during simulation, use this slightly more inefficient example instead where the :class:`Timer`\ s inside the while loop are created with current delay values: .. code-block:: python async def custom_clock(): while True: dut.clk.value = 1 await Timer(high_delay, unit="ns") dut.clk.value = 0 await Timer(low_delay, unit="ns") high_delay = low_delay = 100 cocotb.start_soon(custom_clock()) await Timer(1000, unit="ns") high_delay = low_delay = 10 # change the clock speed await Timer(1000, unit="ns") .. versionadded:: 1.5 Support ``'step'`` as the *unit* argument to mean "simulator time step". .. versionremoved:: 2.0 Passing ``None`` as the *unit* argument was removed, use ``'step'`` instead. .. versionchanged:: 2.0 :meth:`start` now automatically calls :func:`cocotb.start_soon` and stores the Task on the Clock object, so that it may later be :meth:`stop`\ ped. """ _impl: Impl default_set_action: ClassVar[Union[Type[Immediate], Type[Deposit], Type[Force]]] = ( Deposit ) """The default action used to set the clock signal value. One of :class:`.Immediate`, :class:`.Deposit`, or :class:`.Force`. .. versionadded:: 2.0 """ def __init__( self, signal: LogicObject, period: Union[float, Fraction, Decimal], unit: TimeUnit = "step", impl: Union[Impl, None] = None, *, units: None = None, set_action: Union[Type[Immediate], Type[Deposit], Type[Force], None] = None, ) -> None: self._signal = signal self._period = period if units is not None: warnings.warn( "The 'units' argument has been renamed to 'unit'.", DeprecationWarning, stacklevel=2, ) unit = units self._unit: TimeUnit = unit if set_action is None: set_action = type(self).default_set_action if set_action not in (Immediate, Deposit, Force): raise TypeError( "Invalid value for *set_action*. *set_action* must be one of Immediate, Deposit, or Force" ) self._set_action = set_action if impl is None: self._impl = "gpi" if _trust_inertial else "py" elif impl in _valid_impls: self._impl = impl else: valid_impls_str = ", ".join([repr(i) for i in _valid_impls]) raise ValueError( f"Invalid clock impl {impl!r}, must be one of: {valid_impls_str}" ) self._task: Union[Task[None], None] = None @property def signal(self) -> LogicObject: """The clock signal being driven.""" return self._signal @property def period(self) -> Union[float, Fraction, Decimal]: """The clock period (unit-less).""" return self._period @property def unit(self) -> TimeUnit: """The unit of the clock period. .. versionadded:: 2.0 """ return self._unit @property def impl(self) -> Impl: """The concrete implementation of the clock used. ``"gpi"`` if the clock is implemented in C in the GPI layer, or ``"py"`` if the clock is implemented in Python using cocotb Tasks. .. versionadded:: 2.0 """ return self._impl @property def set_action(self) -> Union[Type[Immediate], Type[Deposit], Type[Force]]: """The value setting action used to set the clock signal value. .. versionadded:: 2.0 """ return self._set_action
[docs] def start(self, start_high: bool = True) -> Task[None]: r"""Start driving the clock signal. You can later stop the clock by calling :meth:`stop`. Args: start_high: Whether to start the clock with a ``1`` for the first half of the period. Default is ``True``. .. versionadded:: 1.3 Raises: RuntimeError: If attempting to start a clock that has already been started. Returns: Object which can be passed to :func:`cocotb.start_soon` or ignored. .. versionremoved:: 2.0 Removed ``cycles`` arguments for toggling for a finite amount of cycles. Use :meth:`stop` to stop a clock from running. .. versionchanged:: 2.0 Previously, this method returned a :term:`coroutine` which needed to be passed to :func:`cocotb.start_soon`. Now the Clock object keeps track of its own driver Task, so this is no longer necessary. Simply call ``clock.start()`` to start running the clock. """ if self._task is not None: raise RuntimeError("Starting clock that has already been started.") period = get_sim_steps(self._period, self._unit) t_high = period // 2 if self._impl == "gpi": clkobj = clock_create(self._signal._handle) set_action = { Deposit: _GPISetAction.DEPOSIT, Immediate: _GPISetAction.NO_DELAY, Force: _GPISetAction.FORCE, }[self._set_action] clkobj.start(period, t_high, start_high, set_action) async def drive() -> None: # The clock is meant to toggle forever, so awaiting this should # never return by awaiting on Event that's never set. e = Event() try: await e.wait() finally: clkobj.stop() else: async def drive() -> None: timer_high = Timer(t_high) timer_low = Timer(period - t_high) if start_high: self._signal.set(self._set_action(1)) await timer_high while True: self._signal.set(self._set_action(0)) await timer_low self._signal.set(self._set_action(1)) await timer_high self._task = cocotb.start_soon(drive()) return self._task
[docs] def stop(self) -> None: """Stop driving the clock signal. You can later start the clock again by calling :meth:`start`. Raises: RuntimeError: If attempting to stop a clock that has never been started. .. versionadded:: 2.0 """ if self._task is None: raise RuntimeError("Stopping a clock that was never started.") self._task.cancel() self._task = None
[docs] async def cycles( self, num_cycles: int, edge_type: Union[ Type[RisingEdge], Type[FallingEdge], Type[ValueChange] ] = RisingEdge, ) -> None: """Wait for a number of clock cycles.""" # TODO Improve implementation to use a Timer to skip most of the cycles await ClockCycles(self._signal, num_cycles, edge_type)
def __repr__(self) -> str: return self._repr @cached_property def _repr(self) -> str: freq_mhz = 1 / get_time_from_sim_steps( get_sim_steps(self._period, self._unit), "us" ) return f"<{type(self).__qualname__}, {self._signal._path} @ {freq_mhz} MHz>" @cached_property def _log(self) -> Logger: return logging.getLogger( f"cocotb.{type(self).__qualname__}.{self._signal._name}" )