sim: document.

This commit includes additional non-documentation changes, related to
issues found while documenting it:
- `Simulator.run_until()` no longer accepts a `run_passive=` argument.
  Passive no longer exist and in any case defaulting to `False` does not
  make a lot of sense from an API perspective.
- `add_clock()`'s `phase=` argument, when specified, no longer has
  `period/2` added to it. This wasn't the documented behavior in first
  place and it makes no sense to do that.
- `add_clock()` raises a `NameError` if a clock domain does not exist,
  instead of `ValueError`.
- `add_clock()` raises a `DriverConflict` if a clock domain is already
  being driven by a clock, instead of `ValueError`.
- GTKWave is no longer a part of the installation instructions, and both
  Surfer and GTKWave are recommended (in this order).
This commit is contained in:
Catherine 2024-05-29 15:57:16 +00:00
parent 3c1060f7c7
commit 7870eb344b
16 changed files with 1181 additions and 224 deletions

View file

@ -47,29 +47,29 @@ from amaranth.sim import Simulator
dut = UpCounter(25)
def bench():
async def bench(ctx):
# Disabled counter should not overflow.
yield dut.en.eq(0)
ctx.set(dut.en, 0)
for _ in range(30):
yield
assert not (yield dut.ovf)
await ctx.tick()
assert not ctx.get(dut.ovf)
# Once enabled, the counter should overflow in 25 cycles.
yield dut.en.eq(1)
for _ in range(25):
yield
assert not (yield dut.ovf)
yield
assert (yield dut.ovf)
ctx.set(dut.en, 1)
for _ in range(24):
await ctx.tick()
assert not ctx.get(dut.ovf)
await ctx.tick()
assert ctx.get(dut.ovf)
# The overflow should clear in one cycle.
yield
assert not (yield dut.ovf)
await ctx.tick()
assert not ctx.get(dut.ovf)
sim = Simulator(dut)
sim.add_clock(1e-6) # 1 MHz
sim.add_sync_process(bench)
sim.add_testbench(bench)
with sim.write_vcd("up_counter.vcd"):
sim.run()
# --- CONVERT ---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View file

@ -29,20 +29,23 @@ Migrating from version 0.4
Apply the following changes to code written against Amaranth 0.4 to migrate it to version 0.5:
* Replace uses of ``m.Case()`` with no patterns with ``m.Default()``
* Replace uses of ``Value.matches()`` with no patterns with ``Const(1)``
* Update uses of ``amaranth.utils.log2_int(need_pow2=False)`` to :func:`amaranth.utils.ceil_log2`
* Update uses of ``amaranth.utils.log2_int(need_pow2=True)`` to :func:`amaranth.utils.exact_log2`
* Update uses of ``reset=`` keyword argument to ``init=``
* Convert uses of ``Simulator.add_sync_process`` used as testbenches to ``Simulator.add_testbench``
* Convert other uses of ``Simulator.add_sync_process`` to ``Simulator.add_process``
* Replace uses of ``amaranth.hdl.Memory`` with ``amaranth.lib.memory.Memory``
* Replace imports of ``amaranth.asserts.{Assert, Assume, Cover}`` with imports from ``amaranth.hdl``
* Remove any usage of ``name=`` with assertions, possibly replacing them with custom messages
* Ensure all elaboratables are subclasses of :class:`Elaboratable`
* Replace uses of :py:`m.Case()` with no patterns with :py:`m.Default()`.
* Replace uses of :py:`Value.matches()` with no patterns with :py:`Const(1)`.
* Update uses of :py:`amaranth.utils.log2_int(need_pow2=False)` to :func:`amaranth.utils.ceil_log2`.
* Update uses of :py:`amaranth.utils.log2_int(need_pow2=True)` to :func:`amaranth.utils.exact_log2`.
* Update uses of :py:`reset=` keyword argument to :py:`init=`.
* Convert uses of :py:`Simulator.add_sync_process` used as testbenches to :meth:`Simulator.add_testbench <amaranth.sim.Simulator.add_testbench>`.
* Convert other uses of :py:`Simulator.add_sync_process` to :meth:`Simulator.add_process <amaranth.sim.Simulator.add_process>`.
* Convert simulator processes and testbenches to use the new async API.
* Replace uses of :py:`amaranth.hdl.Memory` with :class:`amaranth.lib.memory.Memory`.
* Replace imports of :py:`amaranth.asserts.Assert`, :py:`Assume`, and :py:`Cover` with imports from :py:`amaranth.hdl`.
* Remove uses of :py:`name=` keyword argument of :py:`Assert`, :py:`Assume`, and :py:`Cover`; a message can be used instead.
* Ensure all elaboratables are subclasses of :class:`Elaboratable`.
* Ensure clock domains aren't used outside the module that defines them, or its submodules; move clock domain definitions upwards in the hierarchy as necessary
* Remove uses of ``amaranth.lib.coding.*`` by inlining or copying the implementation of the modules
* Update uses of ``platform.request`` to pass ``dir="-"`` and use :mod:`amaranth.lib.io` buffers
* Remove uses of :py:`amaranth.lib.coding.*` by inlining or copying the implementation of the modules.
* Update uses of :py:`platform.request` to pass :py:`dir="-"` and use :mod:`amaranth.lib.io` buffers.
* Update uses of :meth:`Simulator.add_clock <amaranth.sim.Simulator.add_clock>` with explicit :py:`phase` to take into account simulator no longer adding implicit :py:`period / 2`. (Previously, :meth:`Simulator.add_clock <amaranth.sim.Simulator.add_clock>` was documented to first toggle the clock at the time :py:`phase`, but actually first toggled the clock at :py:`period / 2 + phase`.)
* Update uses of :meth:`Simulator.run_until <amaranth.sim.Simulator.run_until>` to remove the :py:`run_passive=True` argument. If the code uses :py:`run_passive=False`, ensure it still works with the new behavior.
Implemented RFCs
@ -51,6 +54,7 @@ Implemented RFCs
.. _RFC 17: https://amaranth-lang.org/rfcs/0017-remove-log2-int.html
.. _RFC 27: https://amaranth-lang.org/rfcs/0027-simulator-testbenches.html
.. _RFC 30: https://amaranth-lang.org/rfcs/0030-component-metadata.html
.. _RFC 36: https://amaranth-lang.org/rfcs/0036-async-testbench-functions.html
.. _RFC 39: https://amaranth-lang.org/rfcs/0039-empty-case.html
.. _RFC 43: https://amaranth-lang.org/rfcs/0043-rename-reset-to-init.html
.. _RFC 45: https://amaranth-lang.org/rfcs/0045-lib-memory.html
@ -68,6 +72,7 @@ Implemented RFCs
* `RFC 17`_: Remove ``log2_int``
* `RFC 27`_: Testbench processes for the simulator
* `RFC 30`_: Component metadata
* `RFC 36`_: Async testbench functions
* `RFC 39`_: Change semantics of no-argument ``m.Case()``
* `RFC 43`_: Rename ``reset=`` to ``init=``
* `RFC 45`_: Move ``hdl.Memory`` to ``lib.Memory``
@ -94,12 +99,12 @@ Language changes
* Added: :meth:`ShapeCastable.from_bits` method. (`RFC 51`_)
* Added: IO values, :class:`IOPort` objects, :class:`IOBufferInstance` objects. (`RFC 53`_)
* Added: :class:`MemoryData` objects. (`RFC 62`_)
* Changed: ``m.Case()`` with no patterns is never active instead of always active. (`RFC 39`_)
* Changed: ``Value.matches()`` with no patterns is ``Const(0)`` instead of ``Const(1)``. (`RFC 39`_)
* Changed: ``Signal(range(stop), init=stop)`` warning has been changed into a hard error and made to trigger on any out-of range value.
* Changed: ``Signal(range(0))`` is now valid without a warning.
* Changed: ``Shape.cast(range(1))`` is now ``unsigned(0)``. (`RFC 46`_)
* Changed: the ``reset=`` argument of :class:`Signal`, :meth:`Signal.like`, :class:`amaranth.lib.wiring.Member`, :class:`amaranth.lib.cdc.FFSynchronizer`, and ``m.FSM()`` has been renamed to ``init=``. (`RFC 43`_)
* Changed: :py:`m.Case()` with no patterns is never active instead of always active. (`RFC 39`_)
* Changed: :py:`Value.matches()` with no patterns is :py:`Const(0)` instead of :py:`Const(1)`. (`RFC 39`_)
* Changed: :py:`Signal(range(stop), init=stop)` warning has been changed into a hard error and made to trigger on any out-of range value.
* Changed: :py:`Signal(range(0))` is now valid without a warning.
* Changed: :py:`Shape.cast(range(1))` is now :py:`unsigned(0)`. (`RFC 46`_)
* Changed: the :py:`reset=` argument of :class:`Signal`, :meth:`Signal.like`, :class:`amaranth.lib.wiring.Member`, :class:`amaranth.lib.cdc.FFSynchronizer`, and :py:`m.FSM()` has been renamed to :py:`init=`. (`RFC 43`_)
* Changed: :class:`Shape` has been made immutable and hashable.
* Changed: :class:`Assert`, :class:`Assume`, :class:`Cover` have been moved to :mod:`amaranth.hdl` from :mod:`amaranth.asserts`. (`RFC 50`_)
* Changed: :class:`Instance` IO ports now accept only IO values, not plain values. (`RFC 53`_)
@ -127,17 +132,21 @@ Standard library changes
* Added: :mod:`amaranth.lib.meta`, :class:`amaranth.lib.wiring.ComponentMetadata`. (`RFC 30`_)
* Deprecated: :mod:`amaranth.lib.coding`. (`RFC 63`_)
* Removed: (deprecated in 0.4) :mod:`amaranth.lib.scheduler`. (`RFC 19`_)
* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.FIFOInterface` with ``fwft=False``. (`RFC 20`_)
* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.SyncFIFO` with ``fwft=False``. (`RFC 20`_)
* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.FIFOInterface` with :py:`fwft=False`. (`RFC 20`_)
* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.SyncFIFO` with :py:`fwft=False`. (`RFC 20`_)
Toolchain changes
-----------------
* Added: ``Simulator.add_testbench``. (`RFC 27`_)
* Added: :meth:`Simulator.add_testbench <amaranth.sim.Simulator.add_testbench>`. (`RFC 27`_)
* Added: async function support in :meth:`Simulator.add_testbench <amaranth.sim.Simulator.add_testbench>` and :meth:`Simulator.add_process <amaranth.sim.Simulator.add_process>`. (`RFC 36`_)
* Added: support for :class:`amaranth.hdl.Assert` in simulation. (`RFC 50`_)
* Deprecated: ``Settle`` simulation command. (`RFC 27`_)
* Deprecated: ``Simulator.add_sync_process``. (`RFC 27`_)
* Changed: :meth:`Simulator.add_clock <amaranth.sim.Simulator.add_clock>` no longer implicitly adds :py:`period / 2` when :py:`phase` is specified, actually matching the documentation.
* Changed: :meth:`Simulator.run_until <amaranth.sim.Simulator.run_until>` always runs the simulation until the given deadline, even when no critical processes or testbenches are present.
* Deprecated: :py:`Settle` simulation command. (`RFC 27`_)
* Deprecated: :py:`Simulator.add_sync_process`. (`RFC 27`_)
* Deprecated: the :py:`run_passive` argument to :meth:`Simulator.run_until <amaranth.sim.Simulator.run_until>` has been deprecated, and does nothing.
* Removed: (deprecated in 0.4) use of mixed-case toolchain environment variable names, such as ``NMIGEN_ENV_Diamond`` or ``AMARANTH_ENV_Diamond``; use upper-case environment variable names, such as ``AMARANTH_ENV_DIAMOND``.
@ -150,7 +159,7 @@ Platform integration changes
* Added: :meth:`BuildPlan.extract`.
* Added: ``build.sh`` begins with ``#!/bin/sh``.
* Changed: ``IntelPlatform`` renamed to ``AlteraPlatform``.
* Deprecated: argument ``run_script=`` in :meth:`BuildPlan.execute_local`.
* Deprecated: argument :py:`run_script=` in :meth:`BuildPlan.execute_local`.
* Removed: (deprecated in 0.4) :mod:`vendor.intel`, :mod:`vendor.lattice_ecp5`, :mod:`vendor.lattice_ice40`, :mod:`vendor.lattice_machxo2_3l`, :mod:`vendor.quicklogic`, :mod:`vendor.xilinx`. (`RFC 18`_)

View file

@ -39,6 +39,10 @@ autodoc_default_options = {
autodoc_preserve_defaults = True
autodoc_inherit_docstrings = False
# Amaranth mostly does not include typehints, and showing them in some places but not others is
# worse than not showing them at all.
autodoc_typehints = "none"
napoleon_google_docstring = False
napoleon_numpy_docstring = True
napoleon_use_ivar = True

View file

@ -15,6 +15,7 @@ Language & toolchain
guide
reference
stdlib
simulator
platform
changes
contrib

View file

@ -27,7 +27,7 @@ Amaranth HDL requires Python 3.8; it works on CPython_ 3.8 (or newer), and works
For most workflows, Amaranth requires Yosys_ |yosys-version|. A `compatible version of Yosys <amaranth-yosys_>`_ is distributed via PyPI_ for most popular platforms, so it is usually not necessary to install Yosys separately.
Simulating Amaranth code requires no additional software. However, a waveform viewer like GTKWave_ is invaluable for debugging. As an alternative to GTKWave, the `Amaranth Playground`_ can be used to display waveforms for simple designs.
Simulating Amaranth code requires no additional software. However, a waveform viewer like Surfer_ or GTKWave_ is invaluable for debugging. As an alternative, the `Amaranth Playground`_ can be used to display waveforms for simple designs.
Synthesizing, placing and routing an Amaranth design for an FPGA requires the FPGA family specific toolchain. The open source iCE40, ECP5, MachXO2/3, Nexus, and Gowin toolchains are distributed via PyPI_ for most popular platforms by the YoWASP_ project.
@ -39,6 +39,7 @@ Synthesizing, placing and routing an Amaranth design for an FPGA requires the FP
.. _Yosys: https://yosyshq.net/yosys/
.. _amaranth-yosys: https://pypi.org/project/amaranth-yosys/
.. _PyPI: https://pypi.org/
.. _Surfer: https://surfer-project.org/
.. _GTKWave: https://gtkwave.sourceforge.net/
.. _YoWASP: https://yowasp.org/
@ -58,10 +59,6 @@ Installing prerequisites
:ref:`Install Python <python:using-on-windows>`, either from Windows Store or using the full installer. If using the full installer, make sure to install a 64-bit version of Python.
`Download GTKWave`_, either win32 or win64 binaries. GTKWave does not need to be installed; it can be unpacked to any convenient location and run from there.
.. _Download GTKWave: https://sourceforge.net/projects/gtkwave/files/
|upgrade-pip|
.. code-block:: doscon
@ -71,11 +68,11 @@ Installing prerequisites
.. platform-choice:: macos
:title: macOS
Install Homebrew_. Then, install Python and GTKWave by running:
Install Homebrew_. Then, install Python by running:
.. code-block:: console
$ brew install python gtkwave
$ brew install python
.. _Homebrew: https://brew.sh
@ -89,11 +86,11 @@ Installing prerequisites
:altname: linux
:title: Debian
Install Python and GTKWave by running:
Install Python by running:
.. code-block:: console
$ sudo apt-get install python3-pip gtkwave
$ sudo apt-get install python3-pip
On architectures other than |builtin-yosys-architectures|, install Yosys by running:
@ -113,16 +110,16 @@ Installing prerequisites
:altname: linux
:title: Arch Linux
Install Python, pip, GTKWave and Yosys by running:
Install Python and pip by running:
.. code-block:: console
$ sudo pacman -S python python-pip gtkwave yosys
$ sudo pacman -S python python-pip
.. platform-choice:: linux
:title: Other Linux
Install Python and GTKWave from the package repository of your distribution.
Install Python from the package repository of your distribution.
On architectures other than |builtin-yosys-architectures|, install Yosys from the package repository of your distribution.

288
docs/simulator.rst Normal file
View file

@ -0,0 +1,288 @@
Simulator
#########
.. py:module:: amaranth.sim
The :mod:`amaranth.sim` module, also known as the simulator, makes it possible to evaluate a design's functionality in a virtual environment before it is implemented in hardware.
Simulating circuits
-------------------
.. testsetup::
from amaranth import *
The following examples simulate one of the two designs below: synchronous counter running in the ``sync`` clock domain, and combinational adder. They assume familiarity with the :doc:`language guide <guide>`.
.. testcode::
from amaranth.lib import wiring
from amaranth.lib.wiring import In, Out
class Counter(wiring.Component):
en: In(1, init=1)
count: Out(4)
def elaborate(self, platform):
m = Module()
with m.If(self.en):
m.d.sync += self.count.eq(self.count + 1)
return m
class Adder(wiring.Component):
a: In(16)
b: In(16)
o: Out(17)
def elaborate(self, platform):
m = Module()
m.d.comb += self.o.eq(self.a + self.b)
return m
Running a simulation
++++++++++++++++++++
Simulating a design always requires the three basic steps: constructing the :abbr:`DUT (Design Under Test)`, constructing a :class:`Simulator` for it, and running the simulation with the :meth:`Simulator.run` or :meth:`Simulator.run_until` method:
.. testcode::
from amaranth.sim import Simulator
dut = Counter()
sim = Simulator(dut)
sim.run()
However, the code above neither stimulates the DUT's inputs nor measures the DUT's outputs; the :meth:`Simulator.run` method also immediately returns if no stimulus is added to the simulation. To make it useful, several changes are necessary:
* The :meth:`Simulator.add_clock` method adds a *stimulus*: a process external to the DUT that manipulates its inputs (in this case, toggles the clock of the ``sync`` domain).
* The :meth:`Simulator.run_until` method runs the simulation until a specific deadline is reached.
* The :meth:`Simulator.write_vcd` method captures the DUT's inputs, state, and outputs, and writes it to a :abbr:`VCD (Value Change Dump)` file.
.. _Surfer: https://surfer-project.org/
.. _GTKWave: https://gtkwave.sourceforge.net/
The following code simulates a design and capture the values of all the signals used in the design for each moment of simulation time:
.. testcode::
dut = Counter()
sim = Simulator(dut)
sim.add_clock(1e-6) # 1 µs period, or 1 MHz
with sim.write_vcd("example1.vcd"):
sim.run_until(1e-6 * 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_:
.. wavedrom:: simulator/example1
{
"head": {"tock": 0},
"signal": [
{"name": "clk", "wave": "lp.............."},
{"name": "rst", "wave": "l..............."},
{"name": "en", "wave": "h..............."},
{"name": "count", "wave": "================",
"data": ["0","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15"]}
]
}
Testing synchronous circuits
++++++++++++++++++++++++++++
To verify that the DUT works as intended during a simulation, known values are provided as the inputs, and the outputs are compared with the expected results.
This is done by adding a different type of stimulus to the simulator, a *testbench*: an :py:`async` Python function that runs concurrently with the DUT and can manipulate the signals used in the simulation. A testbench is added using the :meth:`Simulator.add_testbench` method, and receives a :class:`SimulatorContext` object through which it can interact with the simulator: inspect the value of signals using the :meth:`ctx.get() <SimulatorContext.get>` method, change the value of signals using the :meth:`ctx.set() <SimulatorContext.set>` method, or wait for an active edge of a :ref:`clock domain <lang-clockdomains>` using the :meth:`ctx.tick() <SimulatorContext.tick>` method.
The following example simulates a counter and verifies that it can be stopped using its :py:`en` input:
.. testcode::
dut = Counter()
async def testbench_example2(ctx):
await ctx.tick().repeat(5) # wait until after the 5th edge of the `sync` domain clock
assert ctx.get(dut.count) == 5 # verify that the counter has the expected value
ctx.set(dut.en, False) # deassert `dut.en`, disabling the counter
await ctx.tick().repeat(5) # wait until after the 10th edge of clock
assert ctx.get(dut.count) == 5 # verify that the counter has not been incrementing
ctx.set(dut.en, True) # assert `dut.en`, enabling the counter again
sim = Simulator(dut)
sim.add_clock(1e-6)
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)
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:
.. wavedrom:: simulator/example2
{
"head": {"tock": 0},
"signal": [
{"name": "clk", "wave": "lp.............."},
{"name": "rst", "wave": "l..............."},
{"name": "en", "wave": "h....0....1....."},
{"name": "count", "wave": "======.....=====",
"data": ["0","1","2","3","4","5","6","7","8","9","10"]}
]
}
Testing combinational circuits
++++++++++++++++++++++++++++++
A testbench that tests a combinational circuit advances simulation time using the :meth:`ctx.delay() <SimulatorContext.delay>` method instead of the :meth:`ctx.tick() <SimulatorContext.tick>` method, since the simulation does not contain a clock in this case. The :meth:`Simulator.run` method stops the simulation and returns once all testbenches finish executing.
The following example simulates an adder:
.. testcode::
dut = Adder()
async def testbench_example3(ctx):
await ctx.delay(1e-6)
ctx.set(dut.a, 2)
ctx.set(dut.b, 2)
assert ctx.get(dut.o) == 4
await ctx.delay(1e-6)
ctx.set(dut.a, 1717)
ctx.set(dut.b, 420)
assert ctx.get(dut.o) == 2137
await ctx.delay(2e-6)
sim = Simulator(dut)
sim.add_testbench(testbench_example3)
with sim.write_vcd("example3.vcd"):
sim.run()
Since this circuit is entirely combinational, and the Amaranth simulator uses a *zero-delay model* of combinational circuits, the outputs change in the same instant as the inputs do:
.. wavedrom:: simulator/example3
{
"signal": [
{"name": "a", "wave": "===.", "data": [0, 2, 1717]},
{"name": "b", "wave": "===.", "data": [0, 2, 420]},
{"name": "o", "wave": "===.", "data": [0, 4, 2137]}
]
}
Replacing circuits with code
----------------------------
.. note::
This section describes an advanced technique that is not commonly used. If you are first learning how to use the simulator, you can skip it.
During simulation, it is possible to replace an Amaranth circuit with the equivalent Python code. This can be used to improve simulation performance, or to avoid reimplementing complex Python algorithms in Amaranth if they do not need to be synthesized.
This is done by adding a *process* to the simulator: an :py:`async` Python function that runs as an integral part of the simulation, simultaneously with the DUT. A process is added using the :meth:`Simulator.add_process` method, and receives a :class:`SimulatorContext` object through which it can interact with the simulator. A process is conceptually similar to a testbench but differs from it in two important ways:
* Testbenches run in a well-defined order (from first to last in the order they were added, yielding control only at :py:`await` points) and cannot observe inconsistent intermediate states of a design, but processes run in an undefined order while the design is converging after a change to its inputs.
* In a process, it is not possible to inspect the value of a signal using the :meth:`ctx.get() <SimulatorContext.get>` method, which guarantees that inconsistent intermediate states of a design cannot be observed by a process either.
A process communicates with the rest of the design in the same way an elaboratable would: through :class:`Signal`\ s.
Replacing synchronous circuits
++++++++++++++++++++++++++++++
Processes cannot inspect values of signals using the :meth:`ctx.get() <SimulatorContext.get>` method. Instead, values of signals in a synchronous process are sampled at each active edge of the clock domain (or, for domains with asynchronous reset, at the assertion of the reset signal) using the :meth:`ctx.tick() <SimulatorContext.tick>` method.
The following code replaces the :py:`Counter` elaboratable with the equivalent Python code in a process, and uses a testbench to verify its correct operation:
.. testcode::
m = Module()
m.domains.sync = cd_sync = ClockDomain()
en = Signal(init=1)
count = Signal(4)
async def process_example4(ctx):
count_value = 0 # initialize counter to 0
async for clk_edge, rst_value, en_value in ctx.tick().sample(en):
if rst_value: # can be asserted with or without clk_edge
count_value = 0 # re-initialize counter
elif clk_edge and en_value:
count_value += 1 # advance the counter
ctx.set(count, count_value) # publish its value to the simulation
async def testbench_example4(ctx):
await ctx.tick().repeat(5)
assert ctx.get(count) == 5
ctx.set(en, False)
await ctx.tick().repeat(5)
assert ctx.get(count) == 5
ctx.set(en, True)
sim = Simulator(m)
sim.add_clock(1e-6)
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)):
sim.run()
Unless it is instructed otherwise, the :meth:`Simulator.write_vcd` method only captures values of signals that appear in the circuit provided to the simulator when it is created. The :py:`en` and :py:`count` signals do not, and are added explicitly using the :py:`traces` argument so that they will appear in the VCD file.
Replacing combinational circuits
++++++++++++++++++++++++++++++++
Values of signals in a combinational process are sampled anytime they change using the :meth:`ctx.changed() <SimulatorContext.changed>` method.
The following code replaces the :py:`Adder` elaboratable with the equivalent Python code in a process, and uses a testbench to verify its correct operation:
.. testcode::
m = Module()
a = Signal(16)
b = Signal(16)
o = Signal(17)
async def process_example5(ctx):
async for a_value, b_value in ctx.changed(a, b):
ctx.set(o, a_value + b_value)
async def testbench_example5(ctx):
await ctx.delay(1e-6)
ctx.set(a, 2)
ctx.set(b, 2)
assert ctx.get(o) == 4
await ctx.delay(1e-6)
ctx.set(a, 1717)
ctx.set(b, 420)
assert ctx.get(o) == 2137
await ctx.delay(2e-6)
sim = Simulator(m)
sim.add_process(process_example5)
sim.add_testbench(testbench_example5)
with sim.write_vcd("example5.vcd", traces=[a, b, o]):
sim.run()
Reference
---------
.. autoclass:: Simulator
.. autoclass:: SimulatorContext
.. autoexception:: BrokenTrigger
.. autoexception:: DomainReset
.. _sim-tick-trigger:
.. autoclass:: TickTrigger
.. autoclass:: TriggerCombination

View file

@ -47,14 +47,23 @@ To verify its functionality, the counter can be simulated for a small amount of
:start-after: # --- TEST ---
:end-before: # --- CONVERT ---
The test bench is implemented as a Python generator function that is co-simulated with the counter itself. The test bench can inspect the simulated signals with ``yield sig``, update them with ``yield sig.eq(val)``, and advance the simulation by one clock cycle with ``yield``.
The testbench is implemented as a Python :py:`async` function that is simulated concurrently with the counter itself. The testbench can inspect the simulated signals using :py:`ctx.get(sig)`, update them using :py:`ctx.set(sig, val)`, and advance the simulation by one clock cycle with :py:`await ctx.tick()`. See the :doc:`simulator documentation <simulator>` for details.
.. TODO: link to simulator reference
When run, the testbench finishes successfully, since all of the assertions hold, and produces a VCD file with waveforms recorded for every :class:`Signal` as well as the clock of the ``sync`` domain:
When run, the test bench finishes successfully, since all of the assertions hold, and produces a VCD file with waveforms recorded for every ``Signal`` as well as the clock of the ``sync`` domain:
.. wavedrom:: start/up_counter
.. image:: _images/up_counter_gtkwave.png
:alt: A screenshot of GTKWave displaying waveforms near the clock cycle where the counter overflows.
{
"signal": [
{"name": "clk", "wave": "p.........."},
{"name": "count", "wave": "===========", "data": ["17", "18", "19", "20", "21", "22", "23", "24", "25", "0", "1"]},
{"name": "en", "wave": "1.........."},
{"name": "ovf", "wave": "0.......10."},
],
"head": {
"tock": 48
}
}
Converting a counter

View file

@ -171,21 +171,35 @@ However, the memory read port is also configured to be *transparent* relative to
Simulation
==========
++++++++++
.. todo::
There are two ways to interact with a memory array in a simulator: requesting a read and/or write port that is used only in a testbench, or directly reading and writing memory contents. In most cases, directly accessing memory contents using :meth:`MemoryData.__getitem__ <amaranth.hdl.MemoryData.__getitem__>` is more convenient.
This section will be written once the simulator itself is documented.
For example, this :doc:`testbench </simulator>` will clear the least significant bit of every memory row:
.. testcode::
async def testbench(ctx):
for index in len(memory.data):
ctx.set(memory.data[index], ctx.get(memory.data[index]) & ~1)
Memory description
==================
.. autoexception:: amaranth.hdl.AlreadyElaborated
:noindex:
:noindex:
..
:canonical: amaranth.hdl.AlreadyElaborated
(not available until `amaranth.hdl` documents it)
.. autoclass:: amaranth.hdl.MemoryData
..
:canonical: amaranth.hdl.MemoryData
(not available until `amaranth.hdl` documents it)
Memory component
================