Repository: amaranth-lang/amaranth
Pull request #1461 (2024-07-23 22:15:01)

Implement RFC 66: Simulation time.

whitequark: You should add a changelog entry.

Looks good to me on the first pass.

@wanda-phi, could you take a look as well?
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) Report
Attention: Patch coverage is `92.01681%` with `19 lines` in your changes missing coverage. Please review.
> Project coverage is 91.25%. Comparing base [(`74a4096`)](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/74a4096830c22df42cfdeb2af8e6cf75bc01809b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) to head [(`54ed9d1`)](https://app.codecov.io/gh/amaranth-lang/amaranth/commit/54ed9d18a30b4105cf1720fcb2299eed97ec45c4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).
> Report is 9 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) | Patch % | Lines |
|---|---|---|
| [amaranth/build/plat.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?src=pr&el=tree&filepath=amaranth%2Fbuild%2Fplat.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvYnVpbGQvcGxhdC5weQ==) | 33.33% | [6 Missing :warning: ](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) |
| [amaranth/build/dsl.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?src=pr&el=tree&filepath=amaranth%2Fbuild%2Fdsl.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvYnVpbGQvZHNsLnB5) | 77.77% | [3 Missing and 1 partial :warning: ](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) |
| [amaranth/sim/core.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?src=pr&el=tree&filepath=amaranth%2Fsim%2Fcore.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvc2ltL2NvcmUucHk=) | 78.57% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) |
| [amaranth/build/res.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?src=pr&el=tree&filepath=amaranth%2Fbuild%2Fres.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvYnVpbGQvcmVzLnB5) | 83.33% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) |
| [amaranth/hdl/\_time.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?src=pr&el=tree&filepath=amaranth%2Fhdl%2F_time.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvaGRsL190aW1lLnB5) | 98.83% | [0 Missing and 2 partials :warning: ](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) |
| [amaranth/sim/\_async.py](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?src=pr&el=tree&filepath=amaranth%2Fsim%2F_async.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang#diff-YW1hcmFudGgvc2ltL19hc3luYy5weQ==) | 75.00% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1461      +/-   ##
==========================================
+ Coverage   91.15%   91.25%   +0.10%     
==========================================
  Files          43       44       +1     
  Lines       11153    11446     +293     
  Branches     2715     2800      +85     
==========================================
+ Hits        10166    10445     +279     
- Misses        818      827       +9     
- Partials      169      174       +5     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/amaranth-lang/amaranth/pull/1461?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=amaranth-lang).


Patch:
diff --git a/amaranth/build/dsl.py b/amaranth/build/dsl.py
index 3581c47..a83952d 100644
--- a/amaranth/build/dsl.py
+++ b/amaranth/build/dsl.py
@@ -1,8 +1,11 @@
+import warnings
+
 from collections import OrderedDict
+from ..hdl import Period
 
 
 __all__ = ["Pins", "PinsN", "DiffPairs", "DiffPairsN",
-           "Attrs", "Clock", "Subsignal", "Resource", "Connector"]
+           "Attrs", "Clock", "Subsignal", "Resource", "Connector", "Period"]
 
 
 class Pins:
@@ -107,18 +110,39 @@ class Attrs(OrderedDict):
 
 
 class Clock:
-    def __init__(self, frequency):
-        if not isinstance(frequency, (float, int)):
-            raise TypeError("Clock frequency must be a number")
-
-        self.frequency = float(frequency)
+    def __init__(self, period=None, frequency=None):
+        # TODO(amaranth-0.7): remove
+        if (period is None) == (frequency is None):
+            raise TypeError("Exactly one of the `period` or `frequency` arguments must be specified.")
+        
+        if frequency is not None:
+            warnings.warn(
+                f"`frequency=` is deprecated, use `period=` instead",
+                DeprecationWarning, stacklevel=1)
+            period = Period(Hz=frequency)
+
+        if not isinstance(period, Period):
+            warnings.warn(
+                f"Per RFC 66, `Clock()` will only accept a `Period` in the future.",
+                DeprecationWarning, stacklevel=1)
+            period = Period(Hz=period)
+
+        self._period = period
 
     @property
     def period(self):
-        return 1 / self.frequency
+        return self._period
+
+    # TODO(amaranth-0.7): remove
+    @property
+    def frequency(self):
+        warnings.warn(
+            f"Per RFC 66, `Clock.frequency` is deprecated.  Use `Clock.period` instead.",
+            DeprecationWarning, stacklevel=1)
+        return self._period.hertz
 
     def __repr__(self):
-        return f"(clock {self.frequency})"
+        return f"(clock {self._period.hertz})"
 
 
 class Subsignal:
diff --git a/amaranth/build/plat.py b/amaranth/build/plat.py
index 02dbbaa..0baa834 100644
--- a/amaranth/build/plat.py
+++ b/amaranth/build/plat.py
@@ -5,6 +5,7 @@ import os
 import textwrap
 import re
 import jinja2
+import warnings
 
 from .. import __version__
 from .._toolchain import *
@@ -45,11 +46,25 @@ class Platform(ResourceManager, metaclass=ABCMeta):
 
     @property
     def default_clk_frequency(self):
+        # TODO(amaranth-0.7): remove
+        warnings.warn(
+            f"Per RFC 66, `default_clk_frequency` is deprecated. Use `default_clk_period` instead."
+            f" instead.",
+            DeprecationWarning, stacklevel=1)
+
+        constraint = self.default_clk_constraint
+        if constraint is None:
+            raise AttributeError("Platform '{}' does not constrain its default clock"
+                                 .format(type(self).__qualname__))
+        return constraint.period.hertz
+
+    @property
+    def default_clk_period(self):
         constraint = self.default_clk_constraint
         if constraint is None:
             raise AttributeError("Platform '{}' does not constrain its default clock"
                                  .format(type(self).__qualname__))
-        return constraint.frequency
+        return constraint.period
 
     def add_file(self, filename, content):
         if not isinstance(filename, str):
diff --git a/amaranth/build/res.py b/amaranth/build/res.py
index effe44e..c3c90fe 100644
--- a/amaranth/build/res.py
+++ b/amaranth/build/res.py
@@ -222,7 +222,7 @@ class ResourceManager:
                     ])
                     port = io.SingleEndedPort(iop, invert=phys.invert, direction=direction)
                     if resource.clock is not None:
-                        self.add_clock_constraint(iop, resource.clock.frequency)
+                        self.add_clock_constraint(iop, resource.clock.period)
                 if isinstance(phys, DiffPairs):
                     phys_names_p = phys.p.map_names(self._conn_pins, resource)
                     phys_names_n = phys.n.map_names(self._conn_pins, resource)
@@ -237,7 +237,7 @@ class ResourceManager:
                     ])
                     port = io.DifferentialPort(p, n, invert=phys.invert, direction=direction)
                     if resource.clock is not None:
-                        self.add_clock_constraint(p, resource.clock.frequency)
+                        self.add_clock_constraint(p, resource.clock.period)
                 for phys_name in phys_names:
                     if phys_name in self._phys_reqd:
                         raise ResourceError("Resource component {} uses physical pin {}, but it "
@@ -273,22 +273,36 @@ class ResourceManager:
     def iter_pins(self):
         yield from self._pins
 
-    def add_clock_constraint(self, clock, frequency):
+    def add_clock_constraint(self, clock, period=None, frequency=None):
+        # TODO(amaranth-0.7): remove
+        if (period is None) == (frequency is None):
+            raise TypeError("Exactly one of the `period` or `frequency` arguments must be specified.")
+        
+        if frequency is not None:
+            warnings.warn(
+                f"`frequency=` is deprecated, use `period=` instead",
+                DeprecationWarning, stacklevel=1)
+            period = Period(Hz=frequency)
+
+        if not isinstance(period, Period):
+            warnings.warn(
+                f"Per RFC 66, `add_clock_constraint()` will only accept a `Period` in the future.",
+                DeprecationWarning, stacklevel=1)
+            period = Period(Hz=period)
+
         if isinstance(clock, ClockSignal):
             raise TypeError(f"A clock constraint can only be applied to a Signal, but a "
                             f"ClockSignal is provided; assign the ClockSignal to an "
                             f"intermediate signal and constrain the latter instead.")
         elif not isinstance(clock, (Signal, IOPort)):
             raise TypeError(f"Object {clock!r} is not a Signal or IOPort")
-        if not isinstance(frequency, (int, float)):
-            raise TypeError(f"Frequency must be a number, not {frequency!r}")
 
         if isinstance(clock, IOPort):
             clocks = self._io_clocks
         else:
             clocks = self._clocks
 
-        frequency = float(frequency)
+        frequency = period.hertz
         if clock in clocks and clocks[clock] != frequency:
             raise ValueError("Cannot add clock constraint on {!r}, which is already constrained "
                              "to {} Hz"
diff --git a/amaranth/hdl/__init__.py b/amaranth/hdl/__init__.py
index 0a1f753..8130ec6 100644
--- a/amaranth/hdl/__init__.py
+++ b/amaranth/hdl/__init__.py
@@ -10,6 +10,7 @@ from ._ir import AlreadyElaborated, UnusedElaboratable, Elaboratable, DriverConf
 from ._ir import Instance, IOBufferInstance
 from ._mem import MemoryData, MemoryInstance
 from ._nir import CombinationalCycle
+from ._time import Period
 from ._xfrm import DomainRenamer, ResetInserter, EnableInserter
 
 
@@ -32,6 +33,8 @@ __all__ = [
     "CombinationalCycle",
     # _mem
     "MemoryData", "MemoryInstance",
+    # _time
+    "Period",
     # _xfrm
     "DomainRenamer", "ResetInserter", "EnableInserter",
 ]
diff --git a/amaranth/hdl/_time.py b/amaranth/hdl/_time.py
new file mode 100644
index 0000000..85e502b
--- /dev/null
+++ b/amaranth/hdl/_time.py
@@ -0,0 +1,252 @@
+import numbers
+import re
+
+
+__all__ = ["Period"]
+
+
+_TIME_UNITS = {
+    "s":  1_000_000_000_000_000,
+    "ms": 1_000_000_000_000,
+    "us": 1_000_000_000,
+    "ns": 1_000_000,
+    "ps": 1_000,
+    "fs": 1,
+}
+
+
+_FREQUENCY_UNITS = {
+    "Hz":  1_000_000_000_000_000,
+    "kHz": 1_000_000_000_000,
+    "MHz": 1_000_000_000,
+    "GHz": 1_000_000,
+}
+
+
+class Period:
+    def __init__(self, **kwargs):
+        if not kwargs:
+            self._femtoseconds = 0
+            return
+
+        if len(kwargs) > 1:
+            raise TypeError("Period accepts at most one argument")
+
+        (unit, value), = kwargs.items()
+
+        if not isinstance(value, numbers.Real):
+            raise TypeError(f"{unit} value must be a real number")
+
+        if unit in _TIME_UNITS:
+            self._femtoseconds = round(value * _TIME_UNITS[unit])
+
+        elif unit in _FREQUENCY_UNITS:
+            if value == 0:
+                raise ZeroDivisionError("Frequency can't be zero")
+            elif value < 0:
+                raise ValueError("Frequency can't be negative")
+
+            self._femtoseconds = round(_FREQUENCY_UNITS[unit] / value)
+
+        else:
+            raise TypeError(f"{unit} is not a valid unit")
+
+    @property
+    def seconds(self):
+        return self._femtoseconds / 1_000_000_000_000_000
+
+    @property
+    def milliseconds(self):
+        return self._femtoseconds / 1_000_000_000_000
+
+    @property
+    def microseconds(self):
+        return self._femtoseconds / 1_000_000_000
+
+    @property
+    def nanoseconds(self):
+        return self._femtoseconds / 1_000_000
+
+    @property
+    def picoseconds(self):
+        return self._femtoseconds / 1_000
+
+    @property
+    def femtoseconds(self):
+        return self._femtoseconds
+
+    def _check_reciprocal(self):
+        if self._femtoseconds == 0:
+            raise ZeroDivisionError("Can't calculate the frequency of a zero period")
+        elif self._femtoseconds < 0:
+            raise ValueError("Can't calculate the frequency of a negative period")
+
+    @property
+    def hertz(self):
+        self._check_reciprocal()
+        return 1_000_000_000_000_000 / self._femtoseconds
+
+    @property
+    def kilohertz(self):
+        self._check_reciprocal()
+        return 1_000_000_000_000 / self._femtoseconds
+
+    @property
+    def megahertz(self):
+        self._check_reciprocal()
+        return 1_000_000_000 / self._femtoseconds
+
+    @property
+    def gigahertz(self):
+        self._check_reciprocal()
+        return 1_000_000 / self._femtoseconds
+
+    def __lt__(self, other):
+        if not isinstance(other, Period):
+            return NotImplemented
+        return self._femtoseconds < other._femtoseconds
+
+    def __le__(self, other):
+        if not isinstance(other, Period):
+            return NotImplemented
+        return self._femtoseconds <= other._femtoseconds
+
+    def __eq__(self, other):
+        if not isinstance(other, Period):
+            return NotImplemented
+        return self._femtoseconds == other._femtoseconds
+
+    def __ne__(self, other):
+        if not isinstance(other, Period):
+            return NotImplemented
+        return self._femtoseconds != other._femtoseconds
+
+    def __gt__(self, other):
+        if not isinstance(other, Period):
+            return NotImplemented
+        return self._femtoseconds > other._femtoseconds
+
+    def __ge__(self, other):
+        if not isinstance(other, Period):
+            return NotImplemented
+        return self._femtoseconds >= other._femtoseconds
+
+    def __hash__(self):
+        return hash(self._femtoseconds)
+
+    def __bool__(self):
+        return bool(self._femtoseconds)
+
+    def __neg__(self):
+        return Period(fs=-self._femtoseconds)
+
+    def __pos__(self):
+        return self
+
+    def __abs__(self):
+        return Period(fs=abs(self._femtoseconds))
+
+    def __add__(self, other):
+        if not isinstance(other, Period):
+            return NotImplemented
+        return Period(fs=self._femtoseconds + other._femtoseconds)
+
+    def __sub__(self, other):
+        if not isinstance(other, Period):
+            return NotImplemented
+        return Period(fs=self._femtoseconds - other._femtoseconds)
+
+    def __mul__(self, other):
+        if not isinstance(other, numbers.Real):
+            return NotImplemented
+        return Period(fs=self._femtoseconds * other)
+
+    __rmul__ = __mul__
+
+    def __truediv__(self, other):
+        if isinstance(other, Period):
+            return self._femtoseconds / other._femtoseconds
+        elif isinstance(other, numbers.Real):
+            return Period(fs=self._femtoseconds / other)
+        else:
+            return NotImplemented
+
+    def __floordiv__(self, other):
+        if not isinstance(other, Period):
+            return NotImplemented
+        return self._femtoseconds // other._femtoseconds
+
+    def __mod__(self, other):
+        if not isinstance(other, Period):
+            return NotImplemented
+        return Period(fs=self._femtoseconds % other._femtoseconds)
+
+    def __str__(self):
+        return self.__format__("")
+
+    def __format__(self, format_spec):
+        m = re.match(r"^([1-9]\d*)?(\.\d+)?( ?)(([munpf]?)s|([kMG]?)Hz)?$", format_spec)
+
+        if m is None:
+            raise ValueError(f"Invalid format specifier '{format_spec}' for object of type 'Period'")
+
+        width, precision, space, unit, s_unit, hz_unit = m.groups()
+
+        if unit is None:
+            if abs(self._femtoseconds) >= 1_000_000_000_000_000:
+                s_unit = ""
+            elif abs(self._femtoseconds) >= 1_000_000_000_000:
+                s_unit = "m"
+            elif abs(self._femtoseconds) >= 1_000_000_000:
+                s_unit = "u"
+            elif abs(self._femtoseconds) >= 1_000_000:
+                s_unit = "n"
+            elif abs(self._femtoseconds) >= 1_000:
+                s_unit = "p"
+            else:
+                s_unit = "f"
+
+            unit = f"{s_unit}s"
+
+        if s_unit is not None:
+            div, digits = {
+                "":  (1_000_000_000_000_000, 15),
+                "m": (1_000_000_000_000, 12),
+                "u": (1_000_000_000, 9),
+                "n": (1_000_000, 6),
+                "p": (1_000, 3),
+                "f": (1, 0),
+            }[s_unit]
+            integer, decimal = divmod(self._femtoseconds, div)
+
+            if precision:
+                precision = int(precision[1:])
+                decimal = round(decimal * 10**(precision - digits))
+                digits = precision
+
+            value = f"{integer}.{decimal:0{digits}}"
+
+            if not precision:
+                value = value.rstrip('0')
+            value = value.rstrip('.')
+
+        else:
+            if hz_unit == "":
+                value = f"{self.hertz:{precision or ''}f}"
+            elif hz_unit == "k":
+                value = f"{self.kilohertz:{precision or ''}f}"
+            elif hz_unit == "M":
+                value = f"{self.megahertz:{precision or ''}f}"
+            elif hz_unit == "G":
+                value = f"{self.gigahertz:{precision or ''}f}"
+
+        str = f"{value}{space}{unit}"
+        if width:
+            str = f"{str:>{width}}"
+
+        return str
+
+    def __repr__(self):
+        for unit, div in _TIME_UNITS.items():
+            if self._femtoseconds % div == 0:
+                return f"Period({unit}={self._femtoseconds // div})"
diff --git a/amaranth/sim/__init__.py b/amaranth/sim/__init__.py
index de9ccfb..fa7cc13 100644
--- a/amaranth/sim/__init__.py
+++ b/amaranth/sim/__init__.py
@@ -1,11 +1,13 @@
 from .core import Simulator
 from ._async import DomainReset, BrokenTrigger, SimulatorContext, TickTrigger, TriggerCombination
 from ._pycoro import Settle, Delay, Tick, Passive, Active
+from ..hdl import Period
 
 
 __all__ = [
     "DomainReset", "BrokenTrigger",
     "SimulatorContext", "Simulator", "TickTrigger", "TriggerCombination",
+    "Period",
     # deprecated
     "Settle", "Delay", "Tick", "Passive", "Active",
 ]
diff --git a/amaranth/sim/_async.py b/amaranth/sim/_async.py
index 5651ddd..3bf7021 100644
--- a/amaranth/sim/_async.py
+++ b/amaranth/sim/_async.py
@@ -2,6 +2,7 @@
 # Annoyingly, this means that the `sphinxcontrib.napoleon` style docstrings cannot be used, and
 # the Sphinx style docstrings must be used instead. I'm sorry.
 
+import warnings
 import typing
 import operator
 from contextlib import contextmanager
@@ -72,9 +73,18 @@ class DelayTrigger:
         # Note: even though it is likely to be a bad idea, ``await ctx.delay(0)`` is accepted.
         # This is because, if disallowed, people are likely to do even worse things, such as
         # `await ctx.delay(1e-15)` instead.
-        if interval < 0:
+
+        if not isinstance(interval, Period):
+            # TODO(amaranth-0.7): remove
+            warnings.warn(
+                f"Per RFC 66, the `interval` argument of `DelayTrigger()` will only accept a `Period`"
+                f" in the future.",
+                DeprecationWarning, stacklevel=1)
+            interval = Period(s=interval)
+
+        if interval < Period():
             raise ValueError(f"Delay cannot be negative")
-        self.interval_fs = round(float(interval) * 1e15)
+        self.interval = interval
 
 
 class TriggerCombination:
@@ -681,6 +691,11 @@ class SimulatorContext:
         """
         return TriggerCombination(self._engine, self._process).negedge(signal)
 
+    def elapsed_time(self) -> Period:
+        """Return the currently elapsed simulation time."""
+
+        return Period(fs = self._engine.now)
+
 
 class ProcessContext(SimulatorContext):
     def get(self, expr: ValueLike) -> 'typing.Never':
diff --git a/amaranth/sim/_pycoro.py b/amaranth/sim/_pycoro.py
index 181435f..364ce0f 100644
--- a/amaranth/sim/_pycoro.py
+++ b/amaranth/sim/_pycoro.py
@@ -102,10 +102,10 @@ def coro_wrapper(process, *, testbench, default_cmd=None):
                     raise TypeError(f"Command {command!r} is not allowed in testbenches")
 
                 elif type(command) is Settle:
-                    await context.delay(0)
+                    await context.delay(Period())
 
                 elif type(command) is Delay:
-                    await context.delay(command.interval or 0)
+                    await context.delay(Period(s=command.interval or 0))
 
                 elif type(command) is Passive:
                     context._process.critical = False
diff --git a/amaranth/sim/core.py b/amaranth/sim/core.py
index 2c9bc57..c963359 100644
--- a/amaranth/sim/core.py
+++ b/amaranth/sim/core.py
@@ -2,7 +2,7 @@ import inspect
 import warnings
 
 from .._utils import deprecated
-from ..hdl import Value, ValueLike, MemoryData, ClockDomain, Fragment
+from ..hdl import Value, ValueLike, MemoryData, ClockDomain, Fragment, Period
 from ..hdl._ir import DriverConflict
 from ._base import BaseEngine
 from ._async import DomainReset, BrokenTrigger
@@ -17,10 +17,6 @@ __all__ = [
 ]
 
 
-def _seconds_to_femtos(delay: float):
-    return int(delay * 1e15) # seconds to femtoseconds
-
-
 class Simulator:
     # Simulator engines aren't yet a part of the public API.
     """Simulator(toplevel)
@@ -39,8 +35,8 @@ class Simulator:
 
     2. Processes, clocks, and testbenches are added as necessary: ::
 
-        sim.add_clock(1e-6)
-        sim.add_clock(1e-7, domain="fast")
+        sim.add_clock(Period(MHz=1))
+        sim.add_clock(Period(MHz=10), domain="fast")
         sim.add_process(process_instr_decoder)
         sim.add_testbench(testbench_cpu_execute)
 
@@ -122,12 +118,25 @@ class Simulator:
         if domain in self._clocked:
             raise DriverConflict(f"Domain {domain.name!r} already has a clock driving it")
 
-        period_fs = _seconds_to_femtos(period)
+        if not isinstance(period, Period):
+            # TODO(amaranth-0.7): remove
+            warnings.warn(
+                f"Per RFC 66, the `period` argument of `add_clock()` will only accept a `Period`"
+                f" in the future.",
+                DeprecationWarning, stacklevel=1)
+            period = Period(s=period)
+
         if phase is None:
-            phase_fs = _seconds_to_femtos(period / 2)
-        else:
-            phase_fs = _seconds_to_femtos(phase)
-        self._engine.add_clock_process(domain.clk, phase=phase_fs, period=period_fs)
+            phase = period / 2
+        elif not isinstance(phase, Period):
+            # TODO(amaranth-0.7): remove
+            warnings.warn(
+                f"Per RFC 66, the `phase` argument of `add_clock()` will only accept a `Period`"
+                f" (or `None`) in the future.",
+                DeprecationWarning, stacklevel=1)
+            phase = Period(s=phase)
+
+        self._engine.add_clock_process(domain.clk, phase=phase.femtoseconds, period=period.femtoseconds)
         self._clocked.add(domain)
 
     @staticmethod
@@ -319,9 +328,17 @@ class Simulator:
                 f"The `run_passive` argument of `run_until()` has been removed as a part of "
                 f"transition to RFC 36.",
                 DeprecationWarning, stacklevel=1)
-        deadline_fs = _seconds_to_femtos(deadline)
-        assert self._engine.now <= deadline_fs
-        while self._engine.now < deadline_fs:
+
+        if not isinstance(deadline, Period):
+            # TODO(amaranth-0.7): remove
+            warnings.warn(
+                f"Per RFC 66, the `deadline` argument of `run_until()` will only accept a `Period`"
+                f" in the future.",
+                DeprecationWarning, stacklevel=1)
+            deadline = Period(s=deadline)
+
+        assert self._engine.now <= deadline.femtoseconds
+        while self._engine.now < deadline.femtoseconds:
             self.advance()
 
     def advance(self):
diff --git a/amaranth/sim/pysim.py b/amaranth/sim/pysim.py
index ddfb8ad..d98ef77 100644
--- a/amaranth/sim/pysim.py
+++ b/amaranth/sim/pysim.py
@@ -551,8 +551,8 @@ class _PyTriggerState:
                 return
             self._triggers_hit.add(trigger)
             self.activate()
-        self._engine.state.set_delay_waker(trigger.interval_fs, waker)
-        self._delay_wakers[waker] = trigger.interval_fs
+        self._engine.state.set_delay_waker(trigger.interval.femtoseconds, waker)
+        self._delay_wakers[waker] = trigger.interval.femtoseconds
 
     def activate(self):
         if self._combination._process.waits_on is self:
diff --git a/amaranth/vendor/_altera.py b/amaranth/vendor/_altera.py
index 21e66e8..5389a13 100644
--- a/amaranth/vendor/_altera.py
+++ b/amaranth/vendor/_altera.py
@@ -444,8 +444,9 @@ class AlteraPlatform(TemplatedPlatform):
             return self.mistral_command_templates
         assert False
 
-    def add_clock_constraint(self, clock, frequency):
-        super().add_clock_constraint(clock, frequency)
+    def add_clock_constraint(self, clock, period=None, frequency=None):
+        # TODO(amaranth-0.7): remove frequency argument
+        super().add_clock_constraint(clock, period, frequency)
         clock.attrs["keep"] = "true"
 
     @property
@@ -456,7 +457,7 @@ class AlteraPlatform(TemplatedPlatform):
         # of 78 to 84 MHz have been observed.
         if self.default_clk == "cyclonev_oscillator":
             assert self.device.startswith("5C")
-            return Clock(100e6)
+            return Clock(Period(MHz=100))
         # Otherwise, use the defined Clock resource.
         return super().default_clk_constraint
 
diff --git a/amaranth/vendor/_gowin.py b/amaranth/vendor/_gowin.py
index d20ed3d..3b72d98 100644
--- a/amaranth/vendor/_gowin.py
+++ b/amaranth/vendor/_gowin.py
@@ -490,8 +490,9 @@ class GowinPlatform(TemplatedPlatform):
             return self._gowin_command_templates
         assert False
 
-    def add_clock_constraint(self, clock, frequency):
-        super().add_clock_constraint(clock, frequency)
+    def add_clock_constraint(self, clock, period=None, frequency=None):
+        # TODO(amaranth-0.7): remove frequency argument
+        super().add_clock_constraint(clock, period, frequency)
         clock.attrs["keep"] = "true"
 
     @property
@@ -501,7 +502,7 @@ class GowinPlatform(TemplatedPlatform):
                 raise AttributeError(
                     "Using the on-chip oscillator as the default clock source requires "
                     "the platform.osc_frequency attribute to be set")
-            return Clock(self.osc_frequency)
+            return Clock(Period(Hz=self.osc_frequency))
 
         # Use the defined Clock resource.
         return super().default_clk_constraint
diff --git a/amaranth/vendor/_lattice.py b/amaranth/vendor/_lattice.py
index a0cea69..eb303b9 100644
--- a/amaranth/vendor/_lattice.py
+++ b/amaranth/vendor/_lattice.py
@@ -894,15 +894,15 @@ class LatticePlatform(TemplatedPlatform):
     def default_clk_constraint(self):
         if self.default_clk == "OSCG":
             # Internal high-speed oscillator on ECP5 devices.
-            return Clock(310e6 / self.oscg_div)
+            return Clock(Period(MHz=310 / self.oscg_div))
         if self.default_clk == "OSCH":
             # Internal high-speed oscillator on MachXO2/MachXO3L devices.
             # It can have a range of frequencies.
             assert self.osch_frequency in self._supported_osch_freqs
-            return Clock(int(self.osch_frequency * 1e6))
+            return Clock(Period(MHz=self.osch_frequency))
         if self.default_clk == "OSCA":
             # Internal high-speed oscillator on Nexus devices.
-            return Clock(450e6 / self.osca_div)
+            return Clock(Period(MHz=450 / self.osca_div))
         # Otherwise, use the defined Clock resource.
         return super().default_clk_constraint
 
diff --git a/amaranth/vendor/_quicklogic.py b/amaranth/vendor/_quicklogic.py
index c7809b9..afa7248 100644
--- a/amaranth/vendor/_quicklogic.py
+++ b/amaranth/vendor/_quicklogic.py
@@ -140,11 +140,12 @@ class QuicklogicPlatform(TemplatedPlatform):
     @property
     def default_clk_constraint(self):
         if self.default_clk == "sys_clk0":
-            return Clock(self.osc_freq / self.osc_div)
+            return Clock(Period(Hz=self.osc_freq / self.osc_div))
         return super().default_clk_constraint
 
-    def add_clock_constraint(self, clock, frequency):
-        super().add_clock_constraint(clock, frequency)
+    def add_clock_constraint(self, clock, period=None, frequency=None):
+        # TODO(amaranth-0.7): remove frequency argument
+        super().add_clock_constraint(clock, period, frequency)
         clock.attrs["keep"] = "TRUE"
 
     def create_missing_domain(self, name):
diff --git a/amaranth/vendor/_siliconblue.py b/amaranth/vendor/_siliconblue.py
index bd5f998..9ebf939 100644
--- a/amaranth/vendor/_siliconblue.py
+++ b/amaranth/vendor/_siliconblue.py
@@ -1,6 +1,7 @@
 # Currently owned by Lattice, originally designed and built by a startup called SiliconBlue, which
 # was acquired by Lattice. The primitives are prefixed with `SB_` for that reason.
 
+import math
 from abc import abstractmethod
 
 from ..hdl import *
@@ -339,10 +340,10 @@ class SiliconBluePlatform(TemplatedPlatform):
     def default_clk_constraint(self):
         # Internal high-speed oscillator: 48 MHz / (2 ^ div)
         if self.default_clk == "SB_HFOSC":
-            return Clock(48e6 / 2 ** self.hfosc_div)
+            return Clock(Period(MHz=48 / 2 ** self.hfosc_div))
         # Internal low-speed oscillator: 10 KHz
         elif self.default_clk == "SB_LFOSC":
-            return Clock(10e3)
+            return Clock(Period(kHz=10))
         # Otherwise, use the defined Clock resource.
         return super().default_clk_constraint
 
@@ -382,7 +383,7 @@ class SiliconBluePlatform(TemplatedPlatform):
                                          i_CLKHFPU=1,
                                          p_CLKHF_DIV=f"0b{self.hfosc_div:02b}",
                                          o_CLKHF=clk_i)
-                delay = int(100e-6 * self.default_clk_frequency)
+                delay = 1 + math.ceil(Period(us=100) / self.default_clk_period)
             # Internal low-speed clock: 10 KHz.
             elif self.default_clk == "SB_LFOSC":
                 clk_i = Signal()
@@ -390,13 +391,13 @@ class SiliconBluePlatform(TemplatedPlatform):
                                          i_CLKLFEN=1,
                                          i_CLKLFPU=1,
                                          o_CLKLF=clk_i)
-                delay = int(100e-6 * self.default_clk_frequency)
+                delay = 1 + math.ceil(Period(us=100) / self.default_clk_period)
             # User-defined clock signal.
             else:
                 clk_io = self.request(self.default_clk, dir="-")
                 m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io)
                 clk_i = clk_buf.i
-                delay = int(15e-6 * self.default_clk_frequency)
+                delay = 1 + math.ceil(Period(us=15) / self.default_clk_period)
 
             if self.default_rst is not None:
                 rst_io = self.request(self.default_rst, dir="-")
diff --git a/amaranth/vendor/_xilinx.py b/amaranth/vendor/_xilinx.py
index 02adfff..c28e111 100644
--- a/amaranth/vendor/_xilinx.py
+++ b/amaranth/vendor/_xilinx.py
@@ -1180,8 +1180,9 @@ class XilinxPlatform(TemplatedPlatform):
                 m.submodules.reset_sync = ResetSynchronizer(rst_i, domain="sync")
             return m
 
-    def add_clock_constraint(self, clock, frequency):
-        super().add_clock_constraint(clock, frequency)
+    def add_clock_constraint(self, clock, period=None, frequency=None):
+        # TODO(amaranth-0.7): remove frequency argument
+        super().add_clock_constraint(clock, period, frequency)
         if not isinstance(clock, IOPort):
             clock.attrs["keep"] = "TRUE"
 
diff --git a/docs/_code/led_blinker.py b/docs/_code/led_blinker.py
index a450c1f..6c27595 100644
--- a/docs/_code/led_blinker.py
+++ b/docs/_code/led_blinker.py
@@ -7,7 +7,7 @@ class LEDBlinker(Elaboratable):
 
         led = platform.request("led")
 
-        half_freq = int(platform.default_clk_frequency // 2)
+        half_freq = int(platform.default_clk_period.hertz // 2)
         timer = Signal(range(half_freq + 1))
 
         with m.If(timer == half_freq):
diff --git a/docs/_code/up_counter.py b/docs/_code/up_counter.py
index 69b3115..5a4783a 100644
--- a/docs/_code/up_counter.py
+++ b/docs/_code/up_counter.py
@@ -43,7 +43,7 @@ class UpCounter(wiring.Component):
 
         return m
 # --- TEST ---
-from amaranth.sim import Simulator
+from amaranth.sim import Simulator, Period
 
 
 dut = UpCounter(25)
@@ -68,7 +68,7 @@ async def bench(ctx):
 
 
 sim = Simulator(dut)
-sim.add_clock(1e-6) # 1 MHz
+sim.add_clock(Period(MHz=1))
 sim.add_testbench(bench)
 with sim.write_vcd("up_counter.vcd"):
     sim.run()
diff --git a/docs/changes.rst b/docs/changes.rst
index 81c7ab2..5cea418 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -26,8 +26,10 @@ Version 0.6.0 (unreleased)
 Implemented RFCs
 ----------------
 
+.. _RFC 66: https://amaranth-lang.org/rfcs/0066-simulation-time.html
 .. _RFC 71: https://amaranth-lang.org/rfcs/0071-enumview-matches.html
 
+* `RFC 66`_: Simulation time
 * `RFC 71`_: ``EnumView.matches``
 
 
@@ -36,6 +38,7 @@ Language changes
 
 .. currentmodule:: amaranth.hdl
 
+* Added: :class:`Period` for representing time periods. (`RFC 66`_)
 * Changed: overriding :meth:`ValueCastable.from_bits` is now mandatory. (`RFC 51`_)
 * Deprecated: the :py:`local=` argument to :class:`ClockDomain`. (`RFC 59`_)
 * Removed: (deprecated in 0.4.0) :class:`Record`.
@@ -58,6 +61,24 @@ Standard library changes
 * Removed: (deprecated in 0.5.0) :mod:`amaranth.lib.coding`. (`RFC 63`_)
 
 
+Toolchain changes
+-----------------
+
+* Added: :meth:`SimulatorContext.elapsed_time <amaranth.sim._async.SimulatorContext.elapsed_time>` for getting elapsed simulation time. (`RFC 66`_)
+* Added: :meth:`Platform.default_clk_period <amaranth.build.plat.Platform.default_clk_period>`. (`RFC 66`_)
+* Changed: :meth:`Simulator.add_clock <amaranth.sim.Simulator.add_clock>` now accepts a :class:`Period <amaranth.hdl.Period>` for :py:`period` and :py:`phase`. (`RFC 66`_)
+* Changed: :meth:`Simulator.run_until <amaranth.sim.Simulator.run_until>` now accepts a :class:`Period <amaranth.hdl.Period>` for :py:`deadline`. (`RFC 66`_)
+* Changed: :meth:`SimulatorContext.delay <amaranth.sim._async.SimulatorContext.delay>` now accepts a :class:`Period <amaranth.hdl.Period>` for :py:`interval`. (`RFC 66`_)
+* Changed: :meth:`ResourceManager.add_clock_constraint <amaranth.build.res.ResourceManager.add_clock_constraint>` now accepts a :class:`Period <amaranth.hdl.Period>` for :py:`period`. (`RFC 66`_)
+* Changed: :class:`Clock <amaranth.build.dsl.Clock>` now accepts a :class:`Period <amaranth.hdl.Period>` for :py:`period`. (`RFC 66`_)
+* Changed: :attr:`Clock.period <amaranth.build.dsl.Clock.period>` now returns a :class:`Period <amaranth.hdl.Period>`. (`RFC 66`_)
+* Deprecated: Passing a :class:`float` of seconds or hertz to any of the methods/arguments now accepting a :class:`Period <amaranth.hdl.Period>`. (`RFC 66`_)
+* Deprecated: Passing :py:`frequency=` to :meth:`ResourceManager.add_clock_constraint <amaranth.build.res.ResourceManager.add_clock_constraint>`. (`RFC 66`_)
+* Deprecated: Passing :py:`frequency=` to :class:`Clock <amaranth.build.dsl.Clock>`. (`RFC 66`_)
+* Deprecated: :attr:`Clock.frequency <amaranth.build.dsl.Clock.frequency>`. (`RFC 66`_)
+* Deprecated: :meth:`Platform.default_clk_frequency <amaranth.build.plat.Platform.default_clk_frequency>`. (`RFC 66`_)
+
+
 Platform integration changes
 ----------------------------
 
diff --git a/docs/simulator.rst b/docs/simulator.rst
index b34a463..be621fb 100644
--- a/docs/simulator.rst
+++ b/docs/simulator.rst
@@ -48,7 +48,7 @@ Simulating a design always requires the three basic steps: constructing the :abb
 
 .. testcode::
 
-    from amaranth.sim import Simulator
+    from amaranth.sim import Simulator, Period
 
     dut = Counter()
     sim = Simulator(dut)
@@ -69,9 +69,9 @@ The following code simulates a design and capture the values of all the signals
 
     dut = Counter()
     sim = Simulator(dut)
-    sim.add_clock(1e-6) # 1 µs period, or 1 MHz
+    sim.add_clock(Period(MHz=1)) # 1 µs period, or 1 MHz
     with sim.write_vcd("example1.vcd"):
-        sim.run_until(1e-6 * 15) # 15 periods of the clock
+        sim.run_until(Period(MHz=1) * 15) # 15 periods of the clock
 
 The captured data is saved to a :abbr:`VCD` file :file:`example1.vcd`, which can be displayed with a *waveform viewer* such as Surfer_ or GTKWave_:
 
@@ -122,10 +122,10 @@ The following example simulates a counter and verifies that it can be stopped us
         ctx.set(dut.en, True)          # assert `dut.en`, enabling the counter again
 
     sim = Simulator(dut)
-    sim.add_clock(1e-6)
+    sim.add_clock(Period(MHz=1))
     sim.add_testbench(testbench_example2) # add the testbench; run_until() calls the function
     with sim.write_vcd("example2.vcd"):
-        sim.run_until(1e-6 * 15)
+        sim.run_until(Period(MHz=1) * 15)
 
 Since this circuit is synchronous, and the :meth:`ctx.tick() <SimulatorContext.tick>` method waits until after the circuit has reacted to the clock edge, the change to the :py:`en` input affects the behavior of the circuit on the next clock cycle after the change:
 
@@ -155,17 +155,17 @@ The following example simulates an adder:
     dut = Adder()
 
     async def testbench_example3(ctx):
-        await ctx.delay(1e-6)
+        await ctx.delay(Period(us=1))
         ctx.set(dut.a, 2)
         ctx.set(dut.b, 2)
         assert ctx.get(dut.o) == 4
 
-        await ctx.delay(1e-6)
+        await ctx.delay(Period(us=1))
         ctx.set(dut.a, 1717)
         ctx.set(dut.b, 420)
         assert ctx.get(dut.o) == 2137
 
-        await ctx.delay(2e-6)
+        await ctx.delay(Period(us=2))
 
     sim = Simulator(dut)
     sim.add_testbench(testbench_example3)
@@ -234,7 +234,7 @@ The following code replaces the :py:`Counter` elaboratable with the equivalent P
         ctx.set(en, True)
 
     sim = Simulator(m)
-    sim.add_clock(1e-6)
+    sim.add_clock(Period(MHz=1))
     sim.add_process(process_example4)
     sim.add_testbench(testbench_example4)
     with sim.write_vcd("example4.vcd", traces=(cd_sync.clk, cd_sync.rst, en, count)):
@@ -262,17 +262,17 @@ The following code replaces the :py:`Adder` elaboratable with the equivalent Pyt
             ctx.set(o, a_value + b_value)
 
     async def testbench_example5(ctx):
-        await ctx.delay(1e-6)
+        await ctx.delay(Period(us=1))
         ctx.set(a, 2)
         ctx.set(b, 2)
         assert ctx.get(o) == 4
 
-        await ctx.delay(1e-6)
+        await ctx.delay(Period(us=1))
         ctx.set(a, 1717)
         ctx.set(b, 420)
         assert ctx.get(o) == 2137
 
-        await ctx.delay(2e-6)
+        await ctx.delay(Period(us=2))
 
     sim = Simulator(m)
     sim.add_process(process_example5)
diff --git a/docs/stdlib/io.rst b/docs/stdlib/io.rst
index 2028a01..b7d3223 100644
--- a/docs/stdlib/io.rst
+++ b/docs/stdlib/io.rst
@@ -60,7 +60,7 @@ All of the following examples assume that one of the built-in FPGA platforms is
 
 .. testcode::
 
-    from amaranth.sim import Simulator
+    from amaranth.sim import Simulator, Period
     from amaranth.lib import io, wiring, stream
     from amaranth.lib.wiring import In, Out
 
@@ -249,7 +249,7 @@ For example, consider a simple serializer that accepts a stream of multi-bit dat
                 assert dout_value == bit, "DUT drives the wrong value on data output"
 
         sim = Simulator(dut)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         sim.add_testbench(testbench_write_data)
         sim.add_testbench(testbench_sample_output)
         sim.run()
diff --git a/docs/stdlib/stream.rst b/docs/stdlib/stream.rst
index acdcf9a..391a638 100644
--- a/docs/stdlib/stream.rst
+++ b/docs/stdlib/stream.rst
@@ -59,7 +59,7 @@ The pipeline is tested using the :doc:`built-in simulator </simulator>` and the
 
 .. testcode::
 
-    from amaranth.sim import Simulator
+    from amaranth.sim import Simulator, Period
 
     async def stream_get(ctx, stream):
         ctx.set(stream.ready, 1)
@@ -150,7 +150,7 @@ In this example, the external device does not provide a way to pause data transm
                 f"{payload & 0xff:08b} != {expected_word & 0xff:08b} (expected)"
 
         sim = Simulator(dut)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         sim.add_testbench(testbench_input)
         sim.add_testbench(testbench_output)
         with sim.write_vcd("stream_serial_receiver.vcd"):
@@ -244,7 +244,7 @@ The serial transmitter accepts a stream of words and provides it to the serial i
             await ctx.tick()
 
         sim = Simulator(dut)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         sim.add_testbench(testbench_input)
         sim.add_testbench(testbench_output)
         with sim.write_vcd("stream_serial_transmitter.vcd"):
@@ -293,7 +293,7 @@ The value negator accepts a stream of words, negates the 2's complement value of
             assert await stream_get(ctx, dut.o_stream) == -17
 
         sim = Simulator(dut)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         sim.add_testbench(testbench_input)
         sim.add_testbench(testbench_output)
         with sim.write_vcd("stream_value_negator.vcd"):
@@ -388,7 +388,7 @@ The complete pipeline consists of a serial receiver, a value negator, a FIFO que
             ctx.set(dut.o_ssel, 0)
 
         sim = Simulator(dut)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         sim.add_testbench(testbench_input)
         sim.add_testbench(testbench_output)
         with sim.write_vcd("stream_example_pipeline.vcd"):
diff --git a/examples/basic/ctr_en.py b/examples/basic/ctr_en.py
index da15244..d8c20d5 100644
--- a/examples/basic/ctr_en.py
+++ b/examples/basic/ctr_en.py
@@ -25,7 +25,7 @@ ctr = Counter(width=16)
 print(verilog.convert(ctr))
 
 sim = Simulator(ctr)
-sim.add_clock(1e-6)
+sim.add_clock(Period(MHz=1))
 async def testbench_ce(ctx):
     await ctx.tick().repeat(3)
     ctx.set(ctr.en, 1)
@@ -35,4 +35,4 @@ async def testbench_ce(ctx):
     ctx.set(ctr.en,1)
 sim.add_testbench(testbench_ce)
 with sim.write_vcd("ctrl.vcd", "ctrl.gtkw", traces=[ctr.en, ctr.v, ctr.o]):
-    sim.run_until(100e-6)
+    sim.run_until(Period(us=100))
diff --git a/examples/basic/uart.py b/examples/basic/uart.py
index da27fcf..4a60148 100644
--- a/examples/basic/uart.py
+++ b/examples/basic/uart.py
@@ -104,10 +104,10 @@ if __name__ == "__main__":
 
     args = parser.parse_args()
     if args.action == "simulate":
-        from amaranth.sim import Simulator, Passive
+        from amaranth.sim import Simulator, Passive, Period
 
         sim = Simulator(uart)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
 
         async def testbench_loopback(ctx):
             async for val in ctx.changed(uart.tx_o):


Test patch:
diff --git a/tests/test_build_dsl.py b/tests/test_build_dsl.py
index b6002ea..53a449c 100644
--- a/tests/test_build_dsl.py
+++ b/tests/test_build_dsl.py
@@ -149,11 +149,19 @@ class AttrsTestCase(FHDLTestCase):
 
 class ClockTestCase(FHDLTestCase):
     def test_basic(self):
-        c = Clock(1_000_000)
-        self.assertEqual(c.frequency, 1e6)
-        self.assertEqual(c.period, 1e-6)
+        c = Clock(Period(MHz=1))
+        self.assertEqual(c.period, Period(MHz=1))
         self.assertEqual(repr(c), "(clock 1000000.0)")
 
+    def test_deprecated(self):
+        with self.assertWarnsRegex(DeprecationWarning,
+                r"Per RFC 66, `Clock\(\)` will only accept a `Period` in the future."):
+            Clock(1e6)
+
+        with self.assertWarnsRegex(DeprecationWarning,
+                r"`frequency=` is deprecated, use `period=` instead"):
+            Clock(frequency=1e6)
+
 
 class SubsignalTestCase(FHDLTestCase):
     def test_basic_pins(self):
@@ -188,8 +196,8 @@ class SubsignalTestCase(FHDLTestCase):
         self.assertEqual(s.attrs, {"SLEW": "FAST", "PULLUP": "1"})
 
     def test_clock(self):
-        s = Subsignal("a", Pins("A0"), Clock(1e6))
-        self.assertEqual(s.clock.frequency, 1e6)
+        s = Subsignal("a", Pins("A0"), Clock(Period(MHz=1)))
+        self.assertEqual(s.clock.period, Period(MHz=1))
 
     def test_wrong_empty_io(self):
         with self.assertRaisesRegex(ValueError, r"^Missing I\/O constraints$"):
@@ -223,12 +231,12 @@ class SubsignalTestCase(FHDLTestCase):
         with self.assertRaisesRegex(TypeError,
                 (r"^Clock constraint can only be applied to Pins or DiffPairs, not "
                     r"\(subsignal b \(pins io A0\)\)$")):
-            s = Subsignal("a", Subsignal("b", Pins("A0")), Clock(1e6))
+            s = Subsignal("a", Subsignal("b", Pins("A0")), Clock(Period(MHz=1)))
 
     def test_wrong_clock_many(self):
         with self.assertRaisesRegex(ValueError,
                 r"^Clock constraint can be applied only once$"):
-            s = Subsignal("a", Pins("A0"), Clock(1e6), Clock(1e7))
+            s = Subsignal("a", Pins("A0"), Clock(Period(MHz=1)), Clock(Period(MHz=10)))
 
 
 class ResourceTestCase(FHDLTestCase):
diff --git a/tests/test_build_res.py b/tests/test_build_res.py
index 758cdce..7df2b17 100644
--- a/tests/test_build_res.py
+++ b/tests/test_build_res.py
@@ -13,8 +13,8 @@ from .utils import *
 class ResourceManagerTestCase(FHDLTestCase):
     def setUp(self):
         self.resources = [
-            Resource("clk100", 0, DiffPairs("H1", "H2", dir="i"), Clock(100e6)),
-            Resource("clk50", 0, Pins("K1"), Clock(50e6)),
+            Resource("clk100", 0, DiffPairs("H1", "H2", dir="i"), Clock(Period(MHz=100))),
+            Resource("clk50", 0, Pins("K1"), Clock(Period(MHz=50))),
             Resource("user_led", 0, Pins("A0", dir="o")),
             Resource("i2c", 0,
                 Subsignal("scl", Pins("N10", dir="o")),
@@ -240,7 +240,7 @@ class ResourceManagerTestCase(FHDLTestCase):
     def test_add_clock(self):
         with _ignore_deprecated():
             i2c = self.cm.request("i2c")
-        self.cm.add_clock_constraint(i2c.scl.o, 100e3)
+        self.cm.add_clock_constraint(i2c.scl.o, Period(kHz=100))
         self.assertEqual(list(self.cm.iter_signal_clock_constraints()), [
             (i2c.scl.o, 100e3)
         ])
@@ -276,12 +276,16 @@ class ResourceManagerTestCase(FHDLTestCase):
     def test_wrong_clock_signal(self):
         with self.assertRaisesRegex(TypeError,
                 r"^Object None is not a Signal or IOPort$"):
-            self.cm.add_clock_constraint(None, 10e6)
+            self.cm.add_clock_constraint(None, Period(MHz=10))
 
-    def test_wrong_clock_frequency(self):
-        with self.assertRaisesRegex(TypeError,
-                r"^Frequency must be a number, not None$"):
-            self.cm.add_clock_constraint(Signal(), None)
+    def test_deprecated_clock_frequency(self):
+        with self.assertWarnsRegex(DeprecationWarning,
+                r"Per RFC 66, `add_clock_constraint\(\)` will only accept a `Period` in the future."):
+            self.cm.add_clock_constraint(Signal(), 1e6)
+
+        with self.assertWarnsRegex(DeprecationWarning,
+                r"`frequency=` is deprecated, use `period=` instead"):
+            self.cm.add_clock_constraint(Signal(), frequency=1e6)
 
     def test_wrong_request_duplicate(self):
         with _ignore_deprecated():
@@ -345,4 +349,4 @@ class ResourceManagerTestCase(FHDLTestCase):
         with self.assertRaisesRegex(ValueError,
                 (r"^Cannot add clock constraint on \(io-port clk100_0__p\), which is already "
                     r"constrained to 100000000\.0 Hz$")):
-            self.cm.add_clock_constraint(clk100.p, 1e6)
+            self.cm.add_clock_constraint(clk100.p, Period(MHz=1))
diff --git a/tests/test_hdl_time.py b/tests/test_hdl_time.py
new file mode 100644
index 0000000..4b189d7
--- /dev/null
+++ b/tests/test_hdl_time.py
@@ -0,0 +1,143 @@
+from amaranth.hdl._time import *
+
+from .utils import *
+
+
+class PeriodTestCase(FHDLTestCase):
+    def test_constructor(self):
+        self.assertEqual(Period().femtoseconds, 0)
+
+        self.assertEqual(Period(fs=5).femtoseconds, 5)
+        self.assertEqual(Period(ps=5).femtoseconds, 5_000)
+        self.assertEqual(Period(ns=5).femtoseconds, 5_000_000)
+        self.assertEqual(Period(us=5).femtoseconds, 5_000_000_000)
+        self.assertEqual(Period(ms=5).femtoseconds, 5_000_000_000_000)
+        self.assertEqual(Period( s=5).femtoseconds, 5_000_000_000_000_000)
+
+        self.assertEqual(Period(GHz=5).femtoseconds, 200_000)
+        self.assertEqual(Period(MHz=5).femtoseconds, 200_000_000)
+        self.assertEqual(Period(kHz=5).femtoseconds, 200_000_000_000)
+        self.assertEqual(Period( Hz=5).femtoseconds, 200_000_000_000_000)
+
+    def test_constructor_exceptions(self):
+        with self.assertRaisesRegex(TypeError,
+                r"^Period accepts at most one argument$"):
+            Period(s=5, ms = 3)
+
+        with self.assertRaisesRegex(TypeError,
+                r"^foo is not a valid unit$"):
+            Period(foo=5)
+
+        with self.assertRaisesRegex(TypeError,
+                r"^s value must be a real number$"):
+            Period(s="five")
+
+        with self.assertRaisesRegex(ZeroDivisionError,
+                r"^Frequency can't be zero$"):
+            Period(Hz=0)
+
+        with self.assertRaisesRegex(ValueError,
+                r"^Frequency can't be negative$"):
+            Period(Hz=-1)
+
+    def test_accessors(self):
+        self.assertEqual(Period(s=5).seconds,      5.0)
+        self.assertEqual(Period(s=5).milliseconds, 5_000.0)
+        self.assertEqual(Period(s=5).microseconds, 5_000_000.0)
+        self.assertEqual(Period(s=5).nanoseconds,  5_000_000_000.0)
+        self.assertEqual(Period(s=5).picoseconds,  5_000_000_000_000.0)
+        self.assertEqual(Period(s=5).femtoseconds, 5_000_000_000_000_000)
+
+        self.assertEqual(Period(GHz=5).gigahertz, 5.0)
+        self.assertEqual(Period(GHz=5).megahertz, 5_000.0)
+        self.assertEqual(Period(GHz=5).kilohertz, 5_000_000.0)
+        self.assertEqual(Period(GHz=5).hertz,     5_000_000_000.0)
+
+    def test_accessor_exceptions(self):
+        with self.assertRaisesRegex(ZeroDivisionError,
+                r"^Can't calculate the frequency of a zero period$"):
+            Period(s=0).hertz
+
+        with self.assertRaisesRegex(ValueError,
+                r"^Can't calculate the frequency of a negative period$"):
+            Period(s=-1).hertz
+
+    def test_operators(self):
+        for a, b in [(3, 5), (3, 3), (5, 3)]:
+            self.assertEqual(Period(s=a) <  Period(s=b), a <  b)
+            self.assertEqual(Period(s=a) <= Period(s=b), a <= b)
+            self.assertEqual(Period(s=a) == Period(s=b), a == b)
+            self.assertEqual(Period(s=a) != Period(s=b), a != b)
+            self.assertEqual(Period(s=a) >  Period(s=b), a >  b)
+            self.assertEqual(Period(s=a) >= Period(s=b), a >= b)
+
+        self.assertEqual(hash(Period(fs=5)), hash(5))
+
+        self.assertFalse(Period())
+        self.assertTrue(Period(s=5))
+
+        self.assertEqual(-Period(s=5), Period(s=-5))
+        self.assertEqual(+Period(s=5), Period(s=5))
+        self.assertEqual(abs(Period(s=-5)), Period(s=5))
+
+        self.assertEqual(Period(s=3) + Period(ms=5), Period(ms=3005))
+        self.assertEqual(Period(s=3) - Period(ms=5), Period(ms=2995))
+        self.assertEqual(Period(s=3) * 5, Period(s=15))
+        self.assertEqual(3 * Period(s=5), Period(s=15))
+        self.assertEqual(Period(s=15) / 3, Period(s=5))
+        self.assertEqual(Period(s=15) / Period(s=3), 5.0)
+        self.assertEqual(Period(s=8) // Period(s=3), 2)
+        self.assertEqual(Period(s=8) % Period(s=3), Period(s=2))
+
+    def test_invalid_operands(self):
+        with self.assertRaises(TypeError):
+            Period(s=5) > 3
+        with self.assertRaises(TypeError):
+            Period(s=5) >= 3
+        with self.assertRaises(TypeError):
+            Period(s=5) < 3
+        with self.assertRaises(TypeError):
+            Period(s=5) <= 3
+        self.assertFalse(Period(s=5) == 3)
+        self.assertTrue(Period(s=5) != 3)
+
+        with self.assertRaises(TypeError):
+            Period(s=5) + 3
+        with self.assertRaises(TypeError):
+            Period(s=5) - 3
+        with self.assertRaises(TypeError):
+            Period(s=5) * Period(s=3)
+        with self.assertRaises(TypeError):
+            Period(s=5) / "three"
+        with self.assertRaises(TypeError):
+            Period(s=5) // 3
+        with self.assertRaises(TypeError):
+            Period(s=5) % 3
+
+    def test_str(self):
+        self.assertEqual(str(Period( s=5)), "5s")
+        self.assertEqual(str(Period(ms=5)), "5ms")
+        self.assertEqual(str(Period(us=5)), "5us")
+        self.assertEqual(str(Period(ns=5)), "5ns")
+        self.assertEqual(str(Period(ps=5)), "5ps")
+        self.assertEqual(str(Period(fs=5)), "5fs")
+
+    def test_repr(self):
+        self.assertRepr(Period( s=5), "Period(s=5)")
+        self.assertRepr(Period(ms=5), "Period(ms=5)")
+
+    def test_format(self):
+        with self.assertRaisesRegex(ValueError,
+                r"^Invalid format specifier 'foo' for object of type 'Period'"):
+            f"{Period(s=5):foo}"
+
+        self.assertEqual(f"{Period(ms=1234):}", "1.234s")
+        self.assertEqual(f"{Period(ms=1234):ms}", "1234ms")
+        self.assertEqual(f"{Period(ms=1234):.1}", "1.2s")
+        self.assertEqual(f"{Period(ms=1234): }", "1.234 s")
+        self.assertEqual(f"{Period(ms=1234):10}", "    1.234s")
+
+        self.assertEqual(f"{Period(MHz=1250):.0Hz}", "1250000000Hz")
+        self.assertEqual(f"{Period(MHz=1250):.0kHz}", "1250000kHz")
+        self.assertEqual(f"{Period(MHz=1250):.0MHz}", "1250MHz")
+        self.assertEqual(f"{Period(MHz=1250):.0GHz}", "1GHz")
diff --git a/tests/test_lib_cdc.py b/tests/test_lib_cdc.py
index 86264be..40dcd0f 100644
--- a/tests/test_lib_cdc.py
+++ b/tests/test_lib_cdc.py
@@ -22,7 +22,7 @@ class FFSynchronizerTestCase(FHDLTestCase):
         frag = FFSynchronizer(i, o)
 
         sim = Simulator(frag)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         async def testbench(ctx):
             self.assertEqual(ctx.get(o), 0)
             ctx.set(i, 1)
@@ -39,7 +39,7 @@ class FFSynchronizerTestCase(FHDLTestCase):
         frag = FFSynchronizer(i, o, init=1)
 
         sim = Simulator(frag)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         async def testbench(ctx):
             self.assertEqual(ctx.get(o), 1)
             ctx.set(i, 0)
@@ -58,7 +58,7 @@ class FFSynchronizerTestCase(FHDLTestCase):
             frag = FFSynchronizer(i, o, reset=1)
 
         sim = Simulator(frag)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         async def testbench(ctx):
             self.assertEqual(ctx.get(o), 1)
             ctx.set(i, 0)
@@ -107,7 +107,7 @@ class AsyncFFSynchronizerTestCase(FHDLTestCase):
         m.submodules += AsyncFFSynchronizer(i, o)
 
         sim = Simulator(m)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         async def testbench(ctx):
             # initial reset
             self.assertEqual(ctx.get(i), 0)
@@ -144,7 +144,7 @@ class AsyncFFSynchronizerTestCase(FHDLTestCase):
         m.submodules += AsyncFFSynchronizer(i, o, async_edge="neg")
 
         sim = Simulator(m)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         async def testbench(ctx):
             # initial reset
             self.assertEqual(ctx.get(i), 1)
@@ -192,7 +192,7 @@ class ResetSynchronizerTestCase(FHDLTestCase):
         m.d.sync += s.eq(0)
 
         sim = Simulator(m)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         async def testbench(ctx):
             # initial reset
             self.assertEqual(ctx.get(s), 1)
@@ -237,7 +237,7 @@ class PulseSynchronizerTestCase(FHDLTestCase):
         ps = m.submodules.dut = PulseSynchronizer("sync", "sync")
 
         sim = Simulator(m)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         async def testbench(ctx):
             ctx.set(ps.i, 0)
             # TODO: think about reset
diff --git a/tests/test_lib_crc.py b/tests/test_lib_crc.py
index bf7a851..ff1441c 100644
--- a/tests/test_lib_crc.py
+++ b/tests/test_lib_crc.py
@@ -253,7 +253,7 @@ class CRCTestCase(unittest.TestCase):
 
         sim = Simulator(crc)
         sim.add_testbench(testbench)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         sim.run()
 
     def test_crc_bytes(self):
@@ -301,7 +301,7 @@ class CRCTestCase(unittest.TestCase):
 
             sim = Simulator(crc)
             sim.add_testbench(testbench)
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             sim.run()
 
     def test_crc_words(self):
@@ -358,7 +358,7 @@ class CRCTestCase(unittest.TestCase):
 
         sim = Simulator(crc)
         sim.add_testbench(testbench)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         sim.run()
 
     def test_crc_match(self):
diff --git a/tests/test_lib_fifo.py b/tests/test_lib_fifo.py
index d7afd0e..6bbfb7a 100644
--- a/tests/test_lib_fifo.py
+++ b/tests/test_lib_fifo.py
@@ -350,7 +350,7 @@ class AsyncFIFOSimCase(FHDLTestCase):
                     self.assertEqual(r_level, 0)
 
         simulator = Simulator(fifo)
-        simulator.add_clock(100e-6)
+        simulator.add_clock(Period(us=100))
         simulator.add_testbench(testbench)
         simulator.run()
 
@@ -375,9 +375,9 @@ class AsyncFIFOSimCase(FHDLTestCase):
             self.assertEqual(ctx.get(fifo.r_level), expected_level)
 
         simulator = Simulator(fifo)
-        simulator.add_clock(100e-6, domain="write")
+        simulator.add_clock(Period(us=100), domain="write")
         simulator.add_testbench(testbench_write)
-        simulator.add_clock(50e-6, domain="read")
+        simulator.add_clock(Period(us=50), domain="read")
         simulator.add_testbench(testbench_read)
         with simulator.write_vcd("test.vcd"):
             simulator.run()
diff --git a/tests/test_sim.py b/tests/test_sim.py
index 45e84d1..3745e05 100644
--- a/tests/test_sim.py
+++ b/tests/test_sim.py
@@ -506,7 +506,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
     def test_counter_clock_and_sync_process(self):
         self.setUp_counter()
         with self.assertSimulation(self.m) as sim:
-            sim.add_clock(1e-6, domain="sync")
+            sim.add_clock(Period(MHz=1), domain="sync")
             def process():
                 self.assertEqual((yield self.count), 4)
                 self.assertEqual((yield self.sync.clk), 1)
@@ -522,7 +522,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
     def test_reset(self):
         self.setUp_counter()
         sim = Simulator(self.m)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         times = 0
         async def testbench(ctx):
             nonlocal times
@@ -564,7 +564,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
     def test_alu(self):
         self.setUp_alu()
         with self.assertSimulation(self.m) as sim:
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             def process():
                 yield self.a.eq(5)
                 yield self.b.eq(1)
@@ -586,7 +586,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
     def test_alu_bench(self):
         self.setUp_alu()
         with self.assertSimulation(self.m) as sim:
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             async def testbench(ctx):
                 ctx.set(self.a, 5)
                 ctx.set(self.b, 1)
@@ -619,8 +619,8 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
     def test_clock_phase(self):
         self.setUp_clock_phase()
         with self.assertSimulation(self.m) as sim:
-            period=1
-            sim.add_clock(period/8, phase=0,          domain="check")
+            period=Period(s=1)
+            sim.add_clock(period/8, phase=Period(),   domain="check")
             sim.add_clock(period,   phase=0*period/4, domain="phase0")
             sim.add_clock(period,   phase=1*period/4, domain="phase90")
             sim.add_clock(period,   phase=2*period/4, domain="phase180")
@@ -650,8 +650,8 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
     def test_multiclock(self):
         self.setUp_multiclock()
         with self.assertSimulation(self.m) as sim:
-            sim.add_clock(1e-6, domain="sys")
-            sim.add_clock(0.3e-6, domain="pix")
+            sim.add_clock(Period(us=1), domain="sys")
+            sim.add_clock(Period(us=0.3), domain="pix")
 
             async def sys_process(ctx):
                 await ctx.tick("sys")
@@ -685,11 +685,11 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         m = Module()
         s = Signal()
         m.d.sync += s.eq(0)
-        with self.assertSimulation(m, deadline=100e-6) as sim:
-            sim.add_clock(1e-6)
+        with self.assertSimulation(m, deadline=Period(us=100)) as sim:
+            sim.add_clock(Period(us=1))
             async def process(ctx):
                 for _ in range(101):
-                    await ctx.delay(1e-6)
+                    await ctx.delay(Period(us=1))
                 self.fail()
             sim.add_testbench(process)
 
@@ -698,11 +698,25 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         s = Signal()
         m.d.sync += s.eq(0)
         with self.assertRaises(AssertionError):
-            with self.assertSimulation(m, deadline=100e-6) as sim:
-                sim.add_clock(1e-6)
+            with self.assertSimulation(m, deadline=Period(us=100)) as sim:
+                sim.add_clock(Period(us=1))
                 async def process(ctx):
                     for _ in range(99):
-                        await ctx.delay(1e-6)
+                        await ctx.delay(Period(us=1))
+                    self.fail()
+                sim.add_testbench(process)
+
+    def test_run_until_deprecated(self):
+        m = Module()
+        s = Signal()
+        m.d.sync += s.eq(0)
+        with self.assertWarnsRegex(DeprecationWarning,
+                r"Per RFC 66, the `deadline` argument of `run_until\(\)` will only accept a `Period` in the future."):
+            with self.assertSimulation(m, deadline=100e-6) as sim:
+                sim.add_clock(Period(us=1))
+                async def process(ctx):
+                    for _ in range(101):
+                        await ctx.delay(Period(us=1))
                     self.fail()
                 sim.add_testbench(process)
 
@@ -770,22 +784,45 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         s = Signal()
         m.d.sync += s.eq(0)
         with self.assertSimulation(m) as sim:
-            sim.add_clock(1)
+            sim.add_clock(Period(s=1))
             with self.assertRaisesRegex(DriverConflict,
                     r"^Domain 'sync' already has a clock driving it$"):
-                sim.add_clock(1)
+                sim.add_clock(Period(s=1))
 
     def test_add_clock_wrong_missing(self):
         m = Module()
         with self.assertSimulation(m) as sim:
             with self.assertRaisesRegex(NameError,
                     r"^Domain 'sync' is not present in simulation$"):
-                sim.add_clock(1)
+                sim.add_clock(Period(s=1))
 
     def test_add_clock_if_exists(self):
         m = Module()
         with self.assertSimulation(m) as sim:
-            sim.add_clock(1, if_exists=True)
+            sim.add_clock(Period(s=1), if_exists=True)
+
+    def test_add_clock_deprecated(self):
+        m = Module()
+        s = Signal()
+        m.d.sync += s.eq(0)
+        with self.assertSimulation(m) as sim:
+            with self.assertWarnsRegex(DeprecationWarning,
+                    r"Per RFC 66, the `period` argument of `add_clock\(\)` will only accept a `Period` in the future."):
+                sim.add_clock(1)
+
+    def test_elapsed_time(self):
+        m = Module()
+        s = Signal()
+        m.d.sync += s.eq(0)
+        with self.assertSimulation(m) as sim:
+            sim.add_clock(Period(us=1))
+            async def process(ctx):
+                self.assertEqual(ctx.elapsed_time(), Period(ns=0))
+                await ctx.tick()
+                self.assertEqual(ctx.elapsed_time(), Period(ns=500))
+                await ctx.tick()
+                self.assertEqual(ctx.elapsed_time(), Period(ns=1500))
+            sim.add_testbench(process)
 
     def test_command_wrong(self):
         survived = False
@@ -814,7 +851,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 survived = True
             with _ignore_deprecated():
                 sim.add_sync_process(process)
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
         self.assertTrue(survived)
 
     def test_value_castable(self):
@@ -861,7 +898,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 ctx.set(self.rdport.addr, 2)
                 await ctx.tick()
                 self.assertEqual(ctx.get(self.rdport.data), 0x00)
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             sim.add_testbench(testbench)
 
     def test_memory_write(self):
@@ -876,7 +913,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 ctx.set(self.rdport.addr, 4)
                 await ctx.tick()
                 self.assertEqual(ctx.get(self.rdport.data), 0x33)
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             sim.add_testbench(testbench)
 
     def test_memory_write_granularity(self):
@@ -900,7 +937,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 ctx.set(self.wrport.en, 0)
                 await ctx.tick()
                 self.assertEqual(ctx.get(self.rdport.data), 0x53)
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             sim.add_testbench(testbench)
 
     def test_memory_read_before_write(self):
@@ -913,7 +950,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 self.assertEqual(ctx.get(self.rdport.data), 0xaa)
                 await ctx.tick()
                 self.assertEqual(ctx.get(self.rdport.data), 0x33)
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             sim.add_testbench(testbench)
 
     def test_memory_write_through(self):
@@ -929,7 +966,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 await ctx.tick()
                 ctx.set(self.rdport.addr, 1)
                 self.assertEqual(ctx.get(self.rdport.data), 0x33)
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             sim.add_testbench(testbench)
 
     def test_memory_async_read_write(self):
@@ -947,7 +984,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 self.assertEqual(ctx.get(self.rdport.data), 0xaa)
                 await ctx.tick("sync")
                 self.assertEqual(ctx.get(self.rdport.data), 0x33)
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             sim.add_testbench(testbench)
 
     def test_memory_read_only(self):
@@ -961,7 +998,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 ctx.set(self.rdport.addr, 1)
                 await ctx.tick()
                 self.assertEqual(ctx.get(self.rdport.data), 0x55)
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             sim.add_testbench(testbench)
 
     def test_comb_bench_process(self):
@@ -1004,7 +1041,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 await ctx.tick()
                 self.assertEqual(ctx.get(a), 0)
                 self.assertEqual(ctx.get(b), 0)
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             sim.add_testbench(testbench)
 
     def test_memory_transparency_simple(self):
@@ -1043,7 +1080,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 ctx.set(rdport.en, 1)
                 await ctx.tick()
                 self.assertEqual(ctx.get(rdport.data), 0x66)
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             sim.add_testbench(testbench)
 
     def test_memory_transparency_multibit(self):
@@ -1082,7 +1119,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 ctx.set(rdport.en, 1)
                 await ctx.tick()
                 self.assertEqual(ctx.get(rdport.data), 0x22776655)
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             sim.add_testbench(testbench)
 
     def test_memory_access(self):
@@ -1103,7 +1140,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 await ctx.tick()
                 self.assertEqual(ctx.get(self.memory.data[3]), 0x22)
 
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             sim.add_testbench(testbench)
 
     def test_memory_access_sync(self):
@@ -1118,7 +1155,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
                 yield Tick()
                 self.assertEqual((yield self.memory.data[1]), 0x33)
 
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
             with _ignore_deprecated():
                 sim.add_process(process)
 
@@ -1127,8 +1164,8 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         m = Module()
         m.d.sync += s.eq(s)
         sim = Simulator(m)
-        sim.add_clock(1e-6)
-        sim.run_until(1e-5)
+        sim.add_clock(Period(MHz=1))
+        sim.run_until(Period(us=10))
         with self.assertRaisesRegex(ValueError,
                 r"^Cannot start writing waveforms after advancing simulation time$"):
             with open(os.path.devnull, "w") as f:
@@ -1193,9 +1230,9 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         output = StringIO()
         with redirect_stdout(output):
             with self.assertSimulation(m) as sim:
-                sim.add_clock(1e-6, domain="sync")
+                sim.add_clock(Period(MHz=1), domain="sync")
                 async def testbench(ctx):
-                    await ctx.delay(1e-5)
+                    await ctx.delay(Period(us=10))
                 sim.add_testbench(testbench)
         self.assertEqual(output.getvalue(), dedent("""\
             Counter: 000
@@ -1224,9 +1261,9 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         output = StringIO()
         with redirect_stdout(output):
             with self.assertSimulation(m) as sim:
-                sim.add_clock(1e-6, domain="sync")
+                sim.add_clock(Period(MHz=1), domain="sync")
                 async def testbench(ctx):
-                    await ctx.delay(1e-5)
+                    await ctx.delay(Period(us=10))
                 sim.add_testbench(testbench)
         self.assertEqual(output.getvalue(), dedent("""\
             Counter:     zero
@@ -1252,7 +1289,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         output = StringIO()
         with redirect_stdout(output):
             with self.assertSimulation(m) as sim:
-                sim.add_clock(1e-6, domain="sync")
+                sim.add_clock(Period(MHz=1), domain="sync")
                 async def testbench(ctx):
                     await ctx.tick().repeat(4)
                 sim.add_testbench(testbench)
@@ -1280,7 +1317,7 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         output = StringIO()
         with redirect_stdout(output):
             with self.assertSimulation(m) as sim:
-                sim.add_clock(1e-6, domain="sync")
+                sim.add_clock(Period(MHz=1), domain="sync")
                 async def testbench(ctx):
                     await ctx.tick().repeat(4)
                 sim.add_testbench(testbench)
@@ -1299,9 +1336,9 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         with self.assertRaisesRegex(AssertionError,
                 r"^Assertion violated: Counter too large: 4$"):
             with self.assertSimulation(m) as sim:
-                sim.add_clock(1e-6, domain="sync")
+                sim.add_clock(Period(MHz=1), domain="sync")
                 async def testbench(ctx):
-                    await ctx.delay(1e-5)
+                    await ctx.delay(Period(us=10))
                 sim.add_testbench(testbench)
 
     def test_assume(self):
@@ -1312,9 +1349,9 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         with self.assertRaisesRegex(AssertionError,
                 r"^Assumption violated$"):
             with self.assertSimulation(m) as sim:
-                sim.add_clock(1e-6, domain="sync")
+                sim.add_clock(Period(MHz=1), domain="sync")
                 async def testbench(ctx):
-                    await ctx.delay(1e-5)
+                    await ctx.delay(Period(us=10))
                 sim.add_testbench(testbench)
 
     def test_cover(self):
@@ -1327,9 +1364,9 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         output = StringIO()
         with redirect_stdout(output):
             with self.assertSimulation(m) as sim:
-                sim.add_clock(1e-6, domain="sync")
+                sim.add_clock(Period(MHz=1), domain="sync")
                 async def testbench(ctx):
-                    await ctx.delay(1e-5)
+                    await ctx.delay(Period(us=10))
                 sim.add_testbench(testbench)
         self.assertRegex(output.getvalue(), dedent(r"""
             Coverage hit at .*test_sim\.py:\d+: Counter: 000
@@ -1400,11 +1437,11 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         sig = Signal(2, decoder=decoder)
 
         async def testbench(ctx):
-            await ctx.delay(1e-6)
+            await ctx.delay(Period(us=1))
             ctx.set(sig, 1)
-            await ctx.delay(1e-6)
+            await ctx.delay(Period(us=1))
             ctx.set(sig, 2)
-            await ctx.delay(1e-6)
+            await ctx.delay(Period(us=1))
 
         with self.assertSimulation(Module(), traces=[sig]) as sim:
             sim.add_testbench(testbench)
@@ -1421,13 +1458,13 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
         mem4 = MemoryData(shape=signed(8), depth=4, init=[1, -2, 3])
 
         async def testbench(ctx):
-            await ctx.delay(1e-6)
+            await ctx.delay(Period(us=1))
             ctx.set(mem1[0], 4)
             ctx.set(mem2[3], MyEnum.C)
             ctx.set(mem3[2], {"a": -1, "b": 2})
             ctx.set(mem4[1][4:], 0)
             ctx.set(mem4[3][7], 1)
-            await ctx.delay(1e-6)
+            await ctx.delay(Period(us=1))
             self.assertEqual(ctx.get(mem4[1]), 0xe)
             self.assertEqual(ctx.get(mem4[3]), -128)
 
@@ -1462,7 +1499,7 @@ class SimulatorTracesTestCase(FHDLTestCase):
         frag = Fragment()
 
         async def testbench(ctx):
-            await ctx.delay(1e-6)
+            await ctx.delay(Period(us=1))
 
         sim = Simulator(frag)
         sim.add_testbench(testbench)
@@ -1548,7 +1585,7 @@ class SimulatorRegressionTestCase(FHDLTestCase):
         with self.assertWarnsRegex(UserWarning,
                 r"^Adding a clock that drives a clock domain object named 'sync', which is "
                 r"distinct from an identically named domain in the simulated design$"):
-            sim.add_clock(1e-6, domain=ClockDomain("sync"))
+            sim.add_clock(Period(MHz=1), domain=ClockDomain("sync"))
 
     def test_bug_826(self):
         sim = Simulator(Module())
@@ -1578,7 +1615,7 @@ class SimulatorRegressionTestCase(FHDLTestCase):
         sim = Simulator(m)
         with self.assertRaisesRegex(DriverConflict,
                 r"^Clock signal is already driven by combinational logic$"):
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
 
     def test_initial(self):
         a = Signal(4, init=3)
@@ -1696,11 +1733,11 @@ class SimulatorRegressionTestCase(FHDLTestCase):
         log = []
 
         async def monitor(ctx):
-            async for res in ctx.delay(1).delay(2).delay(1):
+            async for res in ctx.delay(Period(s=1)).delay(Period(s=2)).delay(Period(s=1)):
                 log.append(res)
 
         async def testbench(ctx):
-            await ctx.delay(4)
+            await ctx.delay(Period(s=4))
 
         sim = Simulator(Module())
         sim.add_process(monitor)
@@ -1721,19 +1758,19 @@ class SimulatorRegressionTestCase(FHDLTestCase):
         log = []
 
         async def monitor(ctx):
-            async for res in ctx.posedge(a).delay(1.5):
+            async for res in ctx.posedge(a).delay(Period(s=1.5)):
                 log.append(res)
 
         async def testbench(ctx):
-            await ctx.delay(0.5)
+            await ctx.delay(Period(s=0.5))
             ctx.set(a, 1)
-            await ctx.delay(0.5)
+            await ctx.delay(Period(s=0.5))
             ctx.set(a, 0)
-            await ctx.delay(0.5)
+            await ctx.delay(Period(s=0.5))
             ctx.set(a, 1)
-            await ctx.delay(1)
+            await ctx.delay(Period(s=1))
             ctx.set(a, 0)
-            await ctx.delay(1)
+            await ctx.delay(Period(s=1))
             ctx.set(a, 1)
 
         sim = Simulator(Module())
@@ -1786,7 +1823,7 @@ class SimulatorRegressionTestCase(FHDLTestCase):
         sim.add_process(adder)
         sim.add_process(monitor)
         sim.add_testbench(testbench)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         sim.run()
 
         self.assertEqual(log, [
@@ -1837,7 +1874,7 @@ class SimulatorRegressionTestCase(FHDLTestCase):
 
         sim = Simulator(m)
         sim.add_testbench(testbench)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         sim.run()
 
     def test_critical(self):
@@ -1862,7 +1899,7 @@ class SimulatorRegressionTestCase(FHDLTestCase):
         sim = Simulator(m)
         sim.add_testbench(testbench)
         sim.add_testbench(bgbench, background=True)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         sim.run()
 
         self.assertEqual(last_ctr, 9)
@@ -1902,7 +1939,7 @@ class SimulatorRegressionTestCase(FHDLTestCase):
         sim.add_testbench(testbench)
         sim.add_testbench(repeat_bench)
         sim.add_testbench(until_bench)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         sim.run()
 
         self.assertEqual(log, [
@@ -1939,7 +1976,7 @@ class SimulatorRegressionTestCase(FHDLTestCase):
         sim = Simulator(m)
         sim.add_process(monitor)
         sim.add_testbench(testbench)
-        sim.add_clock(1e-6)
+        sim.add_clock(Period(MHz=1))
         sim.run()
 
         self.assertEqual(log, [
@@ -1956,12 +1993,12 @@ class SimulatorRegressionTestCase(FHDLTestCase):
         broken_trigger_hit = False
 
         async def testbench(ctx):
-            await ctx.delay(1)
+            await ctx.delay(Period(s=1))
             ctx.set(a, 1)
             ctx.set(a, 0)
             ctx.set(a, 1)
             ctx.set(a, 0)
-            await ctx.delay(1)
+            await ctx.delay(Period(s=1))
 
         async def monitor(ctx):
             nonlocal broken_trigger_hit
@@ -1985,11 +2022,11 @@ class SimulatorRegressionTestCase(FHDLTestCase):
         async def testbench(ctx):
             with self.assertRaises(BrokenTrigger):
                 async for _ in ctx.tick():
-                    await ctx.delay(2)
+                    await ctx.delay(Period(s=2))
 
         sim = Simulator(m)
         sim.add_testbench(testbench)
-        sim.add_clock(1)
+        sim.add_clock(Period(s=1))
         sim.run()
 
     def test_abandon_delay(self):
@@ -1999,7 +2036,7 @@ class SimulatorRegressionTestCase(FHDLTestCase):
         m.d.sync += ctr.eq(ctr + 1)
 
         async def testbench(ctx):
-            async for _ in ctx.delay(1).delay(1):
+            async for _ in ctx.delay(Period(s=1)).delay(Period(s=1)):
                 break
 
             await ctx.tick()
@@ -2008,7 +2045,7 @@ class SimulatorRegressionTestCase(FHDLTestCase):
 
         sim = Simulator(m)
         sim.add_testbench(testbench)
-        sim.add_clock(4)
+        sim.add_clock(Period(s=4))
         sim.run()
 
     def test_abandon_changed(self):
@@ -2027,17 +2064,17 @@ class SimulatorRegressionTestCase(FHDLTestCase):
             self.assertEqual(ctx.get(ctr), 2)
 
         async def change(ctx):
-            await ctx.delay(1)
+            await ctx.delay(Period(s=1))
             ctx.set(a, 1)
-            await ctx.delay(1)
+            await ctx.delay(Period(s=1))
             ctx.set(a, 0)
-            await ctx.delay(1)
+            await ctx.delay(Period(s=1))
             ctx.set(a, 1)
 
         sim = Simulator(m)
         sim.add_testbench(testbench)
         sim.add_testbench(change)
-        sim.add_clock(4)
+        sim.add_clock(Period(s=4))
         sim.run()
 
     def test_trigger_wrong(self):
@@ -2085,7 +2122,7 @@ class SimulatorRegressionTestCase(FHDLTestCase):
                 await ctx.tick(cd_sync, context=m)
             with self.assertRaisesRegex(ValueError,
                     r"^Delay cannot be negative$"):
-                await ctx.delay(-1)
+                await ctx.delay(Period(s=-1))
             s = Signal(data.StructLayout({"a": unsigned(1)}))
             with self.assertRaisesRegex(TypeError,
                     r"^The shape of a condition may only be `signed` or `unsigned`, not StructLayout.*$"):
@@ -2103,7 +2140,7 @@ class SimulatorRegressionTestCase(FHDLTestCase):
     def test_issue_1368(self):
         sim = Simulator(Module())
         async def testbench(ctx):
-            sim.add_clock(1e-6)
+            sim.add_clock(Period(MHz=1))
         sim.add_testbench(testbench)
         with self.assertRaisesRegex(RuntimeError,
                 r"^Cannot add a clock to a running simulation$"):