Technical Documentation: amaranth-lang/amaranth
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
Amaranth HDL (previously nMigen)
The Amaranth project provides an open-source toolchain for developing hardware based on synchronous digital logic using the Python programming language, as well as evaluation board definitions, a System on Chip toolkit, and more. It aims to be easy to learn and use, reduce or eliminate common coding mistakes, and simplify the design of complex hardware with reusable components.
The Amaranth toolchain consists of the Amaranth hardware definition language, the standard library, the simulator, and the build system, covering all steps of a typical FPGA development workflow. At the same time, it does not restrict the designer’s choice of tools: existing industry-standard (System)Verilog or VHDL code can be integrated into an Amaranth-based design flow, or, conversely, Amaranth code can be integrated into an existing Verilog-based design flow.
The development of Amaranth has been supported by LambdaConcept, ChipEleven, and Chipflow.
Introduction
See the Introduction section of the documentation.
Installation
See the Installation section of the documentation.
Usage
See the Language guide section of the documentation.
Platform support
Amaranth can be used to target any FPGA or ASIC process that accepts behavioral Verilog-2001 as input. It also offers extended support for many FPGA families, providing toolchain integration, abstractions for device-specific primitives, and more. Specifically:
- Lattice iCE40 (toolchains: Yosys+nextpnr, LSE-iCECube2, Synplify-iCECube2);
- Lattice MachXO2, MachXO3L (toolchains: Yosys+nextpnr, Diamond);
- Lattice ECP5 (toolchains: Yosys+nextpnr, Diamond);
- Lattice Nexus (toolchains: Yosys+nextpnr, Radiant);
- AMD Virtex, Virtex E, Spartan 2, Spartan 2E (toolchains: ISE);
- AMD Virtex II, Virtex II Pro (toolchains: ISE);
- AMD Spartan 3, Spartan 3E, Spartan 3A, Spartan 3AN, Spartan 3A DSP (toolchains: ISE);
- AMD Virtex 4, Virtex 5, Virtex 6 (toolchains: ISE);
- AMD Spartan 6 (toolchains: ISE);
- AMD 7-series (toolchains: Vivado, ISE);
- AMD UltraScale, UltraScale+ (toolchains: Vivado);
- Altera (toolchains: Quartus);
- Quicklogic EOS S3 (toolchains: Yosys+VPR).
FOSS toolchains are listed in bold.
Community
Amaranth has a dedicated IRC channel, #amaranth-lang at libera.chat, which is bridged[^1] to Matrix at #amaranth-lang:matrix.org. Feel free to join to ask questions about using Amaranth or discuss ongoing development of Amaranth and its related projects.
[^1]: The same messages appear on IRC and on Matrix, and one can participate in the discussion equally using either communication system.
License
Amaranth is released under the two-clause BSD license. You are permitted to use Amaranth for open-source and proprietary designs provided that the copyright notice in the license file is reproduced.
Changes (`docs/changes.rst`)
Changelog
#########
This document describes changes to the public interfaces in the Amaranth language and standard library. It does not include most bug fixes or implementation changes; versions which do not include notable changes are not listed here.
Documentation for past releases
Documentation for past releases of the Amaranth language and toolchain is available online:
Amaranth 0.5.9 <https://amaranth-lang.org/docs/amaranth/v0.5.9/>_Amaranth 0.5.8 <https://amaranth-lang.org/docs/amaranth/v0.5.8/>_Amaranth 0.5.7 <https://amaranth-lang.org/docs/amaranth/v0.5.7/>_Amaranth 0.5.6 <https://amaranth-lang.org/docs/amaranth/v0.5.6/>_Amaranth 0.5.5 <https://amaranth-lang.org/docs/amaranth/v0.5.5/>_Amaranth 0.5.4 <https://amaranth-lang.org/docs/amaranth/v0.5.4/>_Amaranth 0.5.3 <https://amaranth-lang.org/docs/amaranth/v0.5.3/>_Amaranth 0.5.2 <https://amaranth-lang.org/docs/amaranth/v0.5.2/>_Amaranth 0.5.1 <https://amaranth-lang.org/docs/amaranth/v0.5.1/>_Amaranth 0.5.0 <https://amaranth-lang.org/docs/amaranth/v0.5.0/>_Amaranth 0.4.5 <https://amaranth-lang.org/docs/amaranth/v0.4.5/>_Amaranth 0.4.4 <https://amaranth-lang.org/docs/amaranth/v0.4.4/>_Amaranth 0.4.3 <https://amaranth-lang.org/docs/amaranth/v0.4.3/>_Amaranth 0.4.2 <https://amaranth-lang.org/docs/amaranth/v0.4.2/>_Amaranth 0.4.1 <https://amaranth-lang.org/docs/amaranth/v0.4.1/>_Amaranth 0.4.0 <https://amaranth-lang.org/docs/amaranth/v0.4.0/>_Amaranth 0.3 <https://amaranth-lang.org/docs/amaranth/v0.3/>_
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 73: https://amaranth-lang.org/rfcs/0073-stricter-connections.html
RFC 66_: Simulation timeRFC 71_:EnumView.matchesRFC 73_: Stricter connections
Language changes
.. currentmodule:: amaranth.hdl
- Added: :class:
Periodfor representing time periods. (RFC 66_) - Changed: overriding :meth:
ValueCastable.from_bitsis now mandatory. (RFC 51_) - Deprecated: the :py:
local=argument to :class:ClockDomain. (RFC 59_) - Removed: (deprecated in 0.4.0) :class:
Record. - Removed: (deprecated in 0.5.0) :class:
Memory(RFC 45_) - Removed: (deprecated in 0.5.0) public submodules of :mod:
amaranth.hdl. - Removed: (deprecated in 0.5.0) :meth:
Value.implies. - Removed: (deprecated in 0.5.0) :meth:
Const.width, :meth:Const.signed, :meth:Signal.width, :meth:Signal.signed. - Removed: (deprecated in 0.5.0) upwards propagation of clock domains. (
RFC 59_) - Removed: (deprecated in 0.5.0) :func:
amaranth.utils.log2_int. (RFC 17_)
Standard library changes
.. currentmodule:: amaranth.lib
- Added: :py:
payload_init=argument in :class:amaranth.lib.stream.Signature. - Added: :meth:
enum.EnumView.matches. (RFC 71_) - Added: :class:
data.Field, :class:data.Layoutand :class:data.Constare hashable. - Changed: (deprecated in 0.5.1) providing :meth:
io.PortLike.__add__is now mandatory. (RFC 69_) - Deprecated: :meth:
data.View.eq, :meth:enum.EnumView.eqand :meth:wiring.connectcoercing :class:ValueCastablearguments to plain values. (RFC 73_) - 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:periodand :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_) - Changed: Anonymous sub-:class:
Module\ s/sub-:class:Fragment\ s are now assigned a name like<type_name>$<number>based on their type (outermost :class:Elaboratable, :attr:Instance.type, falling back toU$<number>) - Deprecated: Passing a :class:
floatof 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
.. currentmodule:: amaranth.vendor
- Changed: the Gowin platform now uses
nextpnr-himbaechelrather thannextpnr-gowin.
Version 0.5.9
Platform integration changes
- Fixed: ECP5 DDR buffer output enable latency to match output latency.
Version 0.5.8
Updated to address compatibility with Python 3.14.
Version 0.5.7
Platform integration changes
.. currentmodule:: amaranth.vendor
- Added: build commands are also serialized to
build_{{name}}.json.
Version 0.5.6
Updated to address compatibility with PyPy 3.11.
Version 0.5.5
Standard library changes
.. currentmodule:: amaranth.lib
- Added: :class:
amaranth.lib.wiring.FlippedInterfaceis hashable.
Version 0.5.4
Updated to address deprecations in Yosys 0.48.
Version 0.5.3
Language changes
- Added: individual bits of the same signal can now be assigned from different modules or domains.
Version 0.5.2
Standard library changes
.. currentmodule:: amaranth.lib
- Added: constants of :class:
amaranth.lib.data.ArrayLayoutcan be indexed with negative integers or slices. - Added: :py:
len()works on constants of :class:amaranth.lib.data.ArrayLayout. - Added: constants of :class:
amaranth.lib.data.ArrayLayoutare iterable.
Platform integration changes
.. currentmodule:: amaranth.vendor
- Added: :meth:
Platform.requestaccepts :py:dir="-"for resources with subsignals.
Version 0.5.1
Implemented RFCs
.. _RFC 69: https://amaranth-lang.org/rfcs/0069-simulation-port.html
RFC 69_: Add alib.io.PortLikeobject usable in simulation
Standard library changes
.. currentmodule:: amaranth.lib
- Added: views of :class:
amaranth.lib.data.ArrayLayoutcan be indexed with negative integers or slices. - Added: :py:
len()works on views of :class:amaranth.lib.data.ArrayLayout. - Added: views of :class:
amaranth.lib.data.ArrayLayoutare iterable. - Added: :class:
io.SimulationPort. (RFC 69_)
Version 0.5.0
The Migen compatibility layer has been removed.
Migrating from version 0.4
Apply the following changes to code written against Amaranth 0.4 to migrate it to version 0.5:
- Update uses of :py:
reset=keyword argument to :py:init=. - 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). - 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
- Replace imports of :py:
amaranth.asserts.Assert, :py:Assume, and :py:Coverwith 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. - Replace uses of :py:
amaranth.hdl.Memorywith :class:amaranth.lib.memory.Memory. - Update uses of :py:
platform.requestto pass :py:dir="-"and use :mod:amaranth.lib.iobuffers. - Remove uses of :py:
amaranth.lib.coding.*by inlining or copying the implementation of the modules. - Convert uses of :py:
Simulator.add_sync_processused as testbenches to :meth:Simulator.add_testbench <amaranth.sim.Simulator.add_testbench>. - Convert other uses of :py:
Simulator.add_sync_processto :meth:Simulator.add_process <amaranth.sim.Simulator.add_process>. - Convert simulator processes and testbenches to use the new async API.
- Update uses of :meth:
Simulator.add_clock <amaranth.sim.Simulator.add_clock>with explicit :py:phaseto 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=Trueargument. If the code uses :py:run_passive=False, ensure it still works with the new behavior. - 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. - Replace uses of :py:
a.implies(b)with~a | b.
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 42: https://amaranth-lang.org/rfcs/0042-const-from-shape-castable.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
.. _RFC 46: https://amaranth-lang.org/rfcs/0046-shape-range-1.html
.. _RFC 50: https://amaranth-lang.org/rfcs/0050-print.html
.. _RFC 51: https://amaranth-lang.org/rfcs/0051-const-from-bits.html
.. _RFC 53: https://amaranth-lang.org/rfcs/0053-ioport.html
.. _RFC 55: https://amaranth-lang.org/rfcs/0055-lib-io.html
.. _RFC 58: https://amaranth-lang.org/rfcs/0058-valuecastable-format.html
.. _RFC 59: https://amaranth-lang.org/rfcs/0059-no-domain-upwards-propagation.html
.. _RFC 61: https://amaranth-lang.org/rfcs/0061-minimal-streams.html
.. _RFC 62: https://amaranth-lang.org/rfcs/0062-memory-data.html
.. _RFC 63: https://amaranth-lang.org/rfcs/0063-remove-lib-coding.html
.. _RFC 65: https://amaranth-lang.org/rfcs/0065-format-struct-enum.html
RFC 17_: Removelog2_intRFC 27_: Testbench processes for the simulatorRFC 30_: Component metadataRFC 36_: Async testbench functionsRFC 39_: Change semantics of no-argumentm.Case()RFC 42_:Constfrom shape-castableRFC 43_: Renamereset=toinit=RFC 45_: Movehdl.Memorytolib.MemoryRFC 46_: ChangeShape.cast(range(1))tounsigned(0)RFC 50_:Printstatement and string formattingRFC 51_: AddShapeCastable.from_bitsandamaranth.lib.data.ConstRFC 53_: Low-level I/O primitivesRFC 55_: Newlib.iocomponentsRFC 58_: Core support forValueCastableformattingRFC 59_: Get rid of upwards propagation of clock domainsRFC 61_: Minimal streamsRFC 62_: TheMemoryDataclassRFC 63_: Removeamaranth.lib.codingRFC 65_: Special formatting for structures and enums
Language changes
.. currentmodule:: amaranth.hdl
- Added: :class:
Sliceobjects have been made const-castable. - Added: :func:
amaranth.utils.ceil_log2, :func:amaranth.utils.exact_log2. (RFC 17_) - Added: :class:
Formatobjects, :class:Printstatements, messages in :class:Assert, :class:Assumeand :class:Cover. (RFC 50_) - Added: :meth:
ShapeCastable.from_bitsmethod. (RFC 51_) - Added: IO values, :class:
IOPortobjects, :class:IOBufferInstanceobjects. (RFC 53_) - Added: :class:
MemoryDataobjects. (RFC 62_) - 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:
Const(value, shape)now accepts shape-castable objects as :py:shape. (RFC 42_) - 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:
Shapehas been made immutable and hashable. - Changed: :class:
Assert, :class:Assume, :class:Coverhave been moved to :mod:amaranth.hdlfrom :mod:amaranth.asserts. (RFC 50_) - Changed: :class:
InstanceIO ports now accept only IO values, not plain values. (RFC 53_) - Deprecated: :func:
amaranth.utils.log2_int. (RFC 17_) - Deprecated: :class:
amaranth.hdl.Memory. (RFC 45_) - Deprecated: upwards propagation of clock domains. (
RFC 59_) - Deprecated: :meth:
Value.implies. - Removed: (deprecated in 0.4.0) :meth:
Const.normalize. (RFC 5_) - Removed: (deprecated in 0.4.0) :class:
Repl. (RFC 10_) - Removed: (deprecated in 0.4.0) :class:
ast.Sample, :class:ast.Past, :class:ast.Stable, :class:ast.Rose, :class:ast.Fell. - Removed: assertion names in :class:
Assert, :class:Assumeand :class:Cover. (RFC 50_) - Removed: accepting non-subclasses of :class:
Elaboratableas elaboratables.
Standard library changes
.. currentmodule:: amaranth.lib
- Added: :mod:
amaranth.lib.memory. (RFC 45_) - Added: :class:
amaranth.lib.data.Constclass. (RFC 51_) - Changed: :meth:
amaranth.lib.data.Layout.constreturns a :class:amaranth.lib.data.Const, not a view (RFC 51_) - Changed: :meth:
amaranth.lib.wiring.Signature.is_compliantno longer rejects reset-less signals. - Added: :class:
amaranth.lib.io.SingleEndedPort, :class:amaranth.lib.io.DifferentialPort. (RFC 55_) - Added: :class:
amaranth.lib.io.Buffer, :class:amaranth.lib.io.FFBuffer, :class:amaranth.lib.io.DDRBuffer. (RFC 55_) - Added: :mod:
amaranth.lib.meta, :class:amaranth.lib.wiring.ComponentMetadata. (RFC 30_) - Added: :mod:
amaranth.lib.stream. (RFC 61_) - Deprecated: :mod:
amaranth.lib.coding. (RFC 63_) - Removed: (deprecated in 0.4.0) :mod:
amaranth.lib.scheduler. (RFC 19_) - Removed: (deprecated in 0.4.0) :class:
amaranth.lib.fifo.FIFOInterfacewith :py:fwft=False. (RFC 20_) - Removed: (deprecated in 0.4.0) :class:
amaranth.lib.fifo.SyncFIFOwith :py:fwft=False. (RFC 20_)
Toolchain changes
- 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.Assertin simulation. (RFC 50_) - Changed: :meth:
Simulator.add_clock <amaranth.sim.Simulator.add_clock>no longer implicitly adds :py:period / 2when :py:phaseis 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:
Settlesimulation command. (RFC 27_) - Deprecated: :py:
Simulator.add_sync_process. (RFC 27_) - Deprecated: generator-based simulation processes and testbenches. (
RFC 36_) - Deprecated: the :py:
run_passiveargument to :meth:Simulator.run_until <amaranth.sim.Simulator.run_until>has been deprecated, and does nothing. - Removed: (deprecated in 0.4.0) use of mixed-case toolchain environment variable names, such as
NMIGEN_ENV_DiamondorAMARANTH_ENV_Diamond; use upper-case environment variable names, such asAMARANTH_ENV_DIAMOND.
Platform integration changes
.. currentmodule:: amaranth.vendor
- Added: :meth:
BuildPlan.execute_local_docker. - Added: :meth:
BuildPlan.extract. - Added:
build.shbegins with#!/bin/sh. - Changed:
IntelPlatformrenamed toAlteraPlatform. - Deprecated: argument :py:
run_script=in :meth:BuildPlan.execute_local. - Removed: (deprecated in 0.4.0) :mod:
vendor.intel, :mod:vendor.lattice_ecp5, :mod:vendor.lattice_ice40, :mod:vendor.lattice_machxo2_3l, :mod:vendor.quicklogic, :mod:vendor.xilinx. (RFC 18_)
Version 0.4.0
Support has been added for a new and improved way of defining data structures in :mod:amaranth.lib.data and component interfaces in :mod:amaranth.lib.wiring, as defined in RFC 1_ and RFC 2_. :class:Record has been deprecated. In a departure from the usual policy, to give designers additional time to migrate, :class:Record will be removed in Amaranth 0.6 (one release later than normal).
Support for enumerations has been extended. A shape for enumeration members can be provided for an enumeration class, as defined in RFC 3_.
The language includes several new extension points for integration with :class:Value based data structures defined outside of the core language. In particular, Signal(shape) may now return a :class:Signal object wrapped in another if shape implements the call protocol, as defined in RFC 15_.
Several issues with shape inference have been resolved. Notably, a - b where both a and b are unsigned now returns a signed value.
Support for Python 3.6 and 3.7 has been removed, and support for Python 3.11 and 3.12 has been added.
Features deprecated in version 0.3 have been removed. In particular, the nmigen.* namespace is not provided, # nmigen: annotations are not recognized, and NMIGEN_* envronment variables are not used.
The Migen compatibility layer remains deprecated (as it had been since Amaranth 0.1), and is now scheduled to be removed in version 0.5.
Migrating from version 0.3
Apply the following changes to code written against Amaranth 0.3 to migrate it to version 0.4:
- Update shell environment to use
AMARANTH_*environment variables instead ofNMIGEN_*environment variables. - Update shell environment to use
AMARANTH_ENV_<TOOLCHAIN>(with all-uppercase<TOOLCHAIN>name) environment variable names instead ofAMARANTH_ENV_<Toolchain>orNMIGEN_ENV_<Toolchain>(with mixed-case<Toolchain>name). - Update imports of the form
from amaranth.vendor.some_vendor import SomeVendorPlatformtofrom amaranth.vendor import SomeVendorPlatform. This change will reduce future churn. - Replace uses of
Const.normalize(value, shape)withConst(value, shape).value. - Replace uses of
Repl(value, count)withvalue.replicate(count). - Replace uses of
Recordwith :mod:amaranth.lib.dataand :mod:amaranth.lib.wiring. The appropriate replacement depends on the use case. IfRecordwas being used for data storage and accessing the bit-level representation, use :mod:amaranth.lib.data. IfRecordwas being used for connecting design components together, use :mod:amaranth.lib.wiring. - Replace uses of
Sample,Past,Stable,Rose,Fellwith a manually instantiated register, e.g.past_x = Signal.like(x); m.d.sync += past_x.eq(x). - Remove uses of
amaranth.compatby migrating to native Amaranth syntax. - Ensure the
Pininstance returned byplatform.requestis not cast to value directly, but used for its fields. Replace code likeleds = Cat(platform.request(led, n) for n in range(4))withleds = Cat(platform.request(led, n).o for n in range(4))(note the.o). - Remove uses of
amaranth.lib.scheduler.RoundRobinby inlining or copying the implementation of that class. - Remove uses of
amaranth.lib.fifo.SyncFIFO(fwft=False)andamaranth.lib.fifo.FIFOInterface(fwft=False)by converting code to usefwft=TrueFIFOs or copying the implementation of those classes.
While code that uses the features listed as deprecated below will work in Amaranth 0.4, they will be removed in the next version.
Implemented RFCs
.. _RFC 1: https://amaranth-lang.org/rfcs/0001-aggregate-data-structures.html
.. _RFC 2: https://amaranth-lang.org/rfcs/0002-interfaces.html
.. _RFC 3: https://amaranth-lang.org/rfcs/0003-enumeration-shapes.html
.. _RFC 4: https://amaranth-lang.org/rfcs/0004-const-castable-exprs.html
.. _RFC 5: https://amaranth-lang.org/rfcs/0005-remove-const-normalize.html
.. _RFC 6: https://amaranth-lang.org/rfcs/0006-stdlib-crc.html
.. _RFC 8: https://amaranth-lang.org/rfcs/0008-aggregate-extensibility.html
.. _RFC 9: https://amaranth-lang.org/rfcs/0009-const-init-shape-castable.html
.. _RFC 10: https://amaranth-lang.org/rfcs/0010-move-repl-to-value.html
.. _RFC 15: https://amaranth-lang.org/rfcs/0015-lifting-shape-castables.html
.. _RFC 18: https://amaranth-lang.org/rfcs/0018-reorganize-vendor-platforms.html
.. _RFC 19: https://amaranth-lang.org/rfcs/0019-remove-scheduler.html
.. _RFC 20: https://amaranth-lang.org/rfcs/0020-deprecate-non-fwft-fifos.html
.. _RFC 22: https://amaranth-lang.org/rfcs/0022-valuecastable-shape.html
.. _RFC 28: https://amaranth-lang.org/rfcs/0028-override-value-operators.html
.. _RFC 31: https://amaranth-lang.org/rfcs/0031-enumeration-type-safety.html
.. _RFC 34: https://amaranth-lang.org/rfcs/0034-interface-rename.html
.. _RFC 35: https://amaranth-lang.org/rfcs/0035-shapelike-valuelike.html
.. _RFC 37: https://amaranth-lang.org/rfcs/0037-make-signature-immutable.html
.. _RFC 38: https://amaranth-lang.org/rfcs/0038-component-signature-immutability.html
RFC 1_: Aggregate data structure libraryRFC 2_: Interface definition libraryRFC 3_: Enumeration shapesRFC 4_: Constant-castable expressionsRFC 5_: RemoveConst.normalizeRFC 6_: CRC generatorRFC 8_: Aggregate extensibilityRFC 9_: Constant initialization for shape-castable objectsRFC 10_: MoveRepltoValue.replicateRFC 18_: Reorganize vendor platformsRFC 19_: Removeamaranth.lib.schedulerRFC 15_: Lifting shape-castable objectsRFC 20_: Deprecate non-FWFT FIFOsRFC 22_: DefineValueCastable.shape()RFC 28_: Allow overridingValueoperatorsRFC 31_: Enumeration type safetyRFC 34_: Renameamaranth.lib.wiring.InterfacetoPureInterfaceRFC 35_: AddShapeLike,ValueLikeRFC 37_: MakeSignatureimmutableRFC 38_:Component.signatureimmutability
Language changes
.. currentmodule:: amaranth.hdl
- Added: :class:
ShapeCastable, similar to :class:ValueCastable. - Added: :class:
ShapeLikeand :class:ValueLike. (RFC 35_) - Added: :meth:
Value.as_signedand :meth:Value.as_unsignedcan be used on left-hand side of assignment (with no difference in behavior). - Added: :meth:
Const.cast. (RFC 4_) - Added:
Signal(reset=), :meth:Value.matches,with m.Case():accept any constant-castable objects. (RFC 4_) - Added: :meth:
Value.replicate, superseding :class:Repl. (RFC 10_) - Added: :class:
Memorysupports transparent read ports with read enable. - Changed: creating a :class:
Signalwith a shape that is a :class:ShapeCastableimplementing :meth:ShapeCastable.__call__wraps the returned object using that method. (RFC 15_) - Changed: :meth:
Value.castcasts :class:ValueCastableobjects recursively. - Changed: :meth:
Value.casttreats instances of classes derived from both :class:enum.Enumand :class:int(including :class:enum.IntEnum) as enumerations rather than integers. - Changed: :meth:
Value.matcheswith an empty list of patterns returnsConst(1)rather thanConst(0), to match the behavior ofwith m.Case():. - Changed: :func:
Catwarns if an enumeration without an explicitly specified shape is used. (RFC 3_) - Changed:
signed(0)is no longer constructible. (The semantics of this shape were never defined.) - Changed: :meth:
Value.__abs__returns an unsigned value. - Deprecated: :class:
ast.Sample, :class:ast.Past, :class:ast.Stable, :class:ast.Rose, :class:ast.Fell. (Predating the RFC process.) - Deprecated: :meth:
Const.normalize; useConst(value, shape).valueinstead ofConst.normalize(value, shape). (RFC 5_) - Deprecated: :class:
Repl; use :meth:Value.replicateinstead. (RFC 10_) - Deprecated: :class:
Record; use :mod:amaranth.lib.dataand :mod:amaranth.lib.wiringinstead. (RFC 1,RFC 2) - Removed: (deprecated in 0.1) casting of :class:
Shapeto and from a(width, signed)tuple. - Removed: (deprecated in 0.3) :class:
ast.UserValue. - Removed: (deprecated in 0.3) support for
# nmigen:linter instructions at the beginning of file.
Standard library changes
.. currentmodule:: amaranth.lib
- Added: :mod:
amaranth.lib.enum. (RFC 3_) - Added: :mod:
amaranth.lib.data. (RFC 1_) - Added: :mod:
amaranth.lib.wiring. (RFC 2_) - Added: :mod:
amaranth.lib.crc. (RFC 6_) - Deprecated: :mod:
amaranth.lib.scheduler. (RFC 19_) - Deprecated: :class:
amaranth.lib.fifo.FIFOInterfacewithfwft=False. (RFC 20_) - Deprecated: :class:
amaranth.lib.fifo.SyncFIFOwithfwft=False. (RFC 20_)
Toolchain changes
.. currentmodule:: amaranth
- Changed: text files are written with LF line endings on Windows, like on other platforms.
- Added:
debug_verilogoverride in :class:build.TemplatedPlatform. - Added:
env=argument to :meth:build.run.BuildPlan.execute_local. - Changed: :meth:
build.run.BuildPlan.add_filerejects absolute paths. - Deprecated: use of mixed-case toolchain environment variable names, such as
NMIGEN_ENV_DiamondorAMARANTH_ENV_Diamond; use upper-case environment variable names, such asAMARANTH_ENV_DIAMOND. - Removed: (deprecated in 0.3) :meth:
sim.Simulator.step. - Removed: (deprecated in 0.3) :mod:
back.pysim. - Removed: (deprecated in 0.3) support for invoking :func:
back.rtlil.convert()and :func:back.verilog.convert()without an explicitports=argument. - Removed: (deprecated in 0.3) :mod:
test.
Platform integration changes
.. currentmodule:: amaranth.vendor
- Added:
icepack_optsoverride in :class:vendor.LatticeICE40Platform. - Added:
OSCHasdefault_clkclock source in :class:vendor.LatticeMachXO2Platform, :class:vendor.LatticeMachXO3LPlatform. - Added: Xray toolchain support in :class:
vendor.XilinxPlatform. - Added: Artix UltraScale+ part support in :class:
vendor.XilinxPlatform. - Added: :class:
vendor.GowinPlatform. - Deprecated: :mod:
vendor.intel, :mod:vendor.lattice_ecp5, :mod:vendor.lattice_ice40, :mod:vendor.lattice_machxo2_3l, :mod:vendor.quicklogic, :mod:vendor.xilinx; import platforms directly from :mod:vendorinstead. (RFC 18_) - Removed: (deprecated in 0.3) :mod:
lattice_machxo2 - Removed: (deprecated in 0.3) :class:
lattice_machxo_2_3l.LatticeMachXO2Or3LPlatformSVF programming vector{{name}}.svf. - Removed: (deprecated in 0.3) :class:
xilinx_spartan_3_6.XilinxSpartan3APlatform, :class:xilinx_spartan_3_6.XilinxSpartan6Platform, :class:xilinx_7series.Xilinx7SeriesPlatform, :class:xilinx_ultrascale.XilinxUltrascalePlatform.
Version 0.3
The project has been renamed from nMigen to Amaranth.
Features deprecated in version 0.2 have been removed.
Migrating from version 0.2
.. currentmodule:: amaranth
Apply the following changes to code written against nMigen 0.2 to migrate it to Amaranth 0.3:
- Update
import nmigen as nm:ref:explicit prelude imports <lang-prelude>to beimport amaranth as am, and adjust the code to use theam.*namespace. - Update
import nmigen.*imports to beimport amaranth.*. - Update
import nmigen_boards.*imports to beimport amaranth_boards.*. - Update board definitions using :class:
vendor.lattice_machxo2.LatticeMachXO2Platformto use :class:vendor.lattice_machxo_2_3l.LatticeMachXO2Platform. - Update board definitions using :class:
vendor.xilinx_spartan_3_6.XilinxSpartan3APlatform, :class:vendor.xilinx_spartan_3_6.XilinxSpartan6Platform, :class:vendor.xilinx_7series.Xilinx7SeriesPlatform, :class:vendor.xilinx_ultrascale.XilinxUltrascalePlatformto use :class:vendor.xilinx.XilinxPlatform. - Switch uses of :class:
hdl.ast.UserValueto :class:ValueCastable; note that :class:ValueCastabledoes not inherit from :class:Value, and inheriting from :class:Valueis not supported. - Switch uses of :mod:
back.pysimto :mod:sim. - Add an explicit
ports=argument to uses of :func:back.rtlil.convertand :func:back.verilog.convertif missing. - Remove uses of :class:
test.utils.FHDLTestCaseand vendor the implementation of :class:test.utils.FHDLTestCase.assertFormalif necessary.
While code that uses the features listed as deprecated below will work in Amaranth 0.3, they will be removed in the next version.
Language changes
.. currentmodule:: amaranth.hdl
- Added: :class:
Valuecan be used with :func:abs. - Added: :meth:
Value.rotate_leftand :meth:Value.rotate_right. - Added: :meth:
Value.shift_leftand :meth:Value.shift_right. - Added: :class:
ValueCastable. - Deprecated: :class:
ast.UserValue; use :class:ValueCastableinstead. - Added: Division and modulo operators can be used with a negative divisor.
- Deprecated:
# nmigen:linter instructions at the beginning of file; use# amaranth:instead.
Standard library changes
.. currentmodule:: amaranth.lib
- Added: :class:
cdc.PulseSynchronizer. - Added: :class:
cdc.AsyncFFSynchronizer. - Changed: :class:
fifo.AsyncFIFOis reset when the write domain is reset. - Added: :attr:
fifo.AsyncFIFO.r_rstis asserted when the write domain is reset. - Added: :attr:
fifo.FIFOInterface.r_leveland :attr:fifo.FIFOInterface.w_level.
Toolchain changes
.. currentmodule:: amaranth
- Changed: Backend and simulator reject wires larger than 65536 bits.
- Added: Backend emits Yosys enumeration attributes for :ref:
enumeration-shaped <lang-shapeenum>signals. - Added: If a compatible Yosys version is not installed, :mod:
back.verilogwill fall back to theamaranth-yosys <https://github.com/amaranth-lang/amaranth-yosys>_ PyPI package. The package can be :ref:installedasamaranth[builtin-yosys]to ensure this dependency is available. - Added: :mod:
back.cxxrtl. - Added: :mod:
sim, a simulator interface with support for multiple simulation backends. - Deprecated: :mod:
back.pysim; use :mod:siminstead. - Removed: The
with Simulator(fragment, ...) as sim:form. - Removed: :meth:
sim.Simulator.add_processwith a generator argument. - Deprecated: :meth:
sim.Simulator.step; use :meth:sim.Simulator.advanceinstead. - Added: :meth:
build.BuildPlan.execute_remote_ssh. - Deprecated: :class:
test.utils.FHDLTestCase, with no replacement. - Deprecated: :func:
back.rtlil.convert()and :func:back.verilog.convert()without an explicitports=argument. - Changed: VCD output now uses a top-level "bench" module that contains testbench only signals.
- Deprecated:
NMIGEN_*environment variables; useAMARANTH_*environment variables instead.
Platform integration changes
.. currentmodule:: amaranth.vendor
- Added:
SB_LFOSCandSB_HFOSCasdefault_clkclock sources in :class:lattice_ice40.LatticeICE40Platform. - Added: :class:
lattice_machxo2.LatticeMachXO2Platformgenerates binary (.bit) bitstreams. - Added: :class:
lattice_machxo_2_3l.LatticeMachXO3LPlatform. - Deprecated: :mod:
lattice_machxo2; use :class:lattice_machxo_2_3l.LatticeMachXO2Platforminstead. - Removed: :class:
xilinx_7series.Xilinx7SeriesPlatform.grade; this family has no temperature grades. - Removed: :class:
xilinx_ultrascale.XilinxUltrascalePlatform.grade; this family has temperature grade as part of speed grade. - Added: Symbiflow toolchain support for :class:
xilinx_7series.Xilinx7SeriesPlatform. - Added: :class:
lattice_machxo_2_3l.LatticeMachXO2Or3LPlatformgenerates separate Flash and SRAM SVF programming vectors,{{name}}_flash.svfand{{name}}_sram.svf. - Deprecated: :class:
lattice_machxo_2_3l.LatticeMachXO2Or3LPlatformSVF programming vector{{name}}.svf; use{{name}}_flash.svfinstead. - Added: :class:
quicklogic.QuicklogicPlatform. - Added:
cyclonev_oscillatorasdefault_clkclock source in :class:intel.IntelPlatform. - Added:
add_settingsandadd_constraintsoverrides in :class:intel.IntelPlatform. - Added: :class:
xilinx.XilinxPlatform. - Deprecated: :class:
xilinx_spartan_3_6.XilinxSpartan3APlatform, :class:xilinx_spartan_3_6.XilinxSpartan6Platform, :class:xilinx_7series.Xilinx7SeriesPlatform, :class:xilinx_ultrascale.XilinxUltrascalePlatform; use :class:xilinx.XilinxPlatforminstead. - Added: Mistral toolchain support for :class:
intel.IntelPlatform. - Added:
synth_design_optsoverride in :class:xilinx.XilinxPlatform.
Versions 0.1, 0.2
No changelog is provided for these versions.
The PyPI packages were published under the nmigen namespace, rather than amaranth.
Contrib (`docs/contrib.rst`)
Contributing
############
The Amaranth project is the collective work of many people collaborating over the years, and it would not be the same without everyone's unique perspectives and contributions. We're glad that you are considering joining us! This page will guide you through some of the ways to contribute to the project.
Filing problem reports
We would like Amaranth to be a best-in-class design tool, and hearing about issues people encounter while using it is crucial for improving it. While we do care a lot about correctness of the results, we care about the experience of using the tool just as much. Amaranth is meant to be a tool that is comfortable to use: with fewer sharp edges (no matter how much technological appeal they might have) and more signs and guardrails.
Please report _ any problems you encounter using Amaranth. To go beyond that: If, while you are using Amaranth, you see an error message that is hard to understand or is misleading, please report it as a bug. Even (especially!) if you think you did something wrong.
.. _issues: https://github.com/amaranth-lang/amaranth/issues
When filing problem reports, please include the following information:
- The exact version of Amaranth, which you can find by running
python -c "import amaranth; print(amaranth.__version__)"; - A complete, self-contained, and minimal program that demonstrates the problem you are reporting (if minimizing it is not feasible, include the exact sequence of steps that reproduces the problem);
- What you expected to happen, and what actually happened (where possible, including a verbatim copy of the log file or the terminal output);
- For usability issues: your reason for filing the report (i.e. why did you expect a different behavior).
There is no expectation that a person who is filing a problem report should work on fixing it. Submitting an issue is a valuable contribution in its own right.
Fixing problems
We appreciate that many in the open source community tend to see problems they encounter as opportunities to collaborate, and we enjoy seeing an issue being filed together with a pull request. However, unless you've contributed a few times before or the fix is truly trivial, please discuss it with one of the maintainers first. It doesn't take much time and it can sometimes save everyone a lot of unnecessary work and frustration.
Proposing new features
Amaranth is a programming language and a toolchain, which is different from many other kinds of open source projects in that just about every part of it is, unavoidably, tightly coupled to every other one, the result being that seemingly obvious and apparently minor decisions can have dramatic consequences years later.
To make sure that new features undergo the scrutiny necessary for commitment to many years of support, and to make sure that everyone in the community who will be impacted by the changes has a chance to make their voice heard, all substantial changes, including feature proposals, must go through a formal Request for Comments process. The process, as well as the accepted proposals, are described here <rfcs_>_. Typically, substantial changes are accepted after one to several rounds of community review achieve near-unanimous consensus.
.. _rfcs: https://amaranth-lang.org/rfcs/
Working with the codebase
Preparing the environment
The Amaranth codebase uses the PDM_ package and dependency manager to structure the development workflow. Please install PDM_ first and make sure you have downloaded the latest changes to the source files. Once you have done so, run:
.. _PDM: https://pdm-project.org/
.. _install PDM: https://pdm-project.org/latest/#recommended-installation-method
.. code-block:: console
$ pdm install --dev
This command creates a :ref:virtual environment located at ./.venv/ and installs the runtime dependencies of Amaranth as well as the necessary development tools in it.
Amaranth itself is installed in the editable mode, meaning that the changes to its source files are immediately reflected in running the tests and documentation. However, other changes (addition or removal of source files, or changes to dependencies) will not be picked up, and it is a good habit to run pdm install each time after updating the source tree.
Running the testsuite
Some of the tests make use of formal methods, and to run the complete testsuite, it is necessary to install the Yosys frontend and the yices2_ SMT solver. These are distributed as a part of the OSS CAD Suite_. Without the tools being installed, the tests that rely on formal verification will be skipped.
.. _formal methods: https://symbiyosys.readthedocs.io/en/latest/
.. _Yosys: https://github.com/YosysHQ/yosys
.. _yices2: https://github.com/SRI-CSL/yices2
.. _OSS CAD Suite: https://github.com/YosysHQ/oss-cad-suite-build
To run the testsuite, use:
.. code-block:: console
$ pdm run test
Building the documentation
To build the documentation once, use:
.. code-block:: console
$ pdm run document
The documentation index is located at ./docs/_build/index.html.
Working on documentation usually involves making many small, iterative changes, and it is laborous to rebuild it manually each time. To start a process that rebuilds documentation automatically on change, use:
.. code-block:: console
$ pdm run document-live
While it is running you can browse the documentation at http://127.0.0.1:8000 <http://127.0.0.1:8000>_. The edits you make are reflected on the document open in the browser after a short delay. It is useful to keep an eye on the terminal where this process is running, as the information about syntactic errors, missing references, and other issues will be printed there.
Occasionally, the documentation builder will persist in rendering an incorrect or outdated version of the contents of a Python source file. To fix this, run:
.. code-block:: console
$ pdm run document-live -a
Documentation style guide
.. warning::
Our documentation style guidelines are evolving, and this section is incomplete.
Some of the fundamental guidelines are:
Document the contract and the affordances, not the implementation. This is especially important because the Amaranth documentation is the source of truth for its semantics; the tests and the implementation source code are secondary to it, and the RFCs exist to record the process rather than document the outcome.
Shape the code to be documentable. This is a corollary to the previous guideline. If an interface is difficult to describe in a way that is readily understood, then it may need to be changed. Many breaking changes in Amaranth were done to make the language and libraries easier to understand.
Be consistent. Take inspiration from existing documentation for similar modules. However, don't be consistent at the expense of clarity.
Be concise. It is easy to write boilerplate, and difficult to navigate through it.
- In particular, if the
Parameterssection of the class docstring describes a parameter, it is expected that the same parameter will be available as a class attribute (usually, but not always, read-only), and there is no need to additionally document this fact. If there isn't a corresponding attribute it should likely be added. - There is no need to individually document every argument and every return value of every method. This mainly creates clutter. The goal in writing documentation is transferring knowledge, not ticking boxes.
- In particular, if the
Some of the formatting guidelines are:
Limit code (including docstrings, where possible--some of the Sphinx syntax does not allow wrapping) to 100 columns in
.pyfiles, but do not break paragraphs in.rstfiles.Use
###...#for first-level headings,===...=for second-level headings,---...-for third-level headings.Use the
:py:`...``` syntax for inline Python code references (even trivial ones, e.g.:py:`var_name```),.. testcode::for most Python code blocks (use.. code::where the code cannot or should not be tested),.. doctest::for doctests.Use admonitions sparingly, and only of the following kinds:
.. warning::for text which MUST be paid attention to, or else unexpected bad things may happen. This is the most noticeable kind, rendered in yellow at the moment... tip::for text which SHOULD be paid attention to, otherwise annoyance may happen. This is the second most noticeable kind, rendered in bright blue-green at the moment... note::for text which MAY be paid attention to, but which is not key for understanding of the topic as a whole. This is the least noticeable kind, rendered in faint blue at the moment... todo::may also be used for incomplete sections.
For methods, phrase the short description (first line of docstring) like
Do the thing., i.e. as an imperative sentence.For properties, phrase the short description (first line of docstring) like
Value of thing., i.e. as a declarative sentence.When documenting signatures of interfaces, as well as components, use the (non-standard)
Memberssection to document their interface members, and only that section; do not document them in anAttributessection.If an anchor for a section is needed, namespace it, e.g. the
.. _lang-assignable:anchor is a part of thelangnamespace. Anchor names are global.To refer to non-sequential logic, use the term "combinational" over "combinatorial".
Contributing your changes
.. warning::
Our code style guidelines are evolving, and we do not yet have a formal document listing them.
We ask that you do your best effort to keep the code that you add or modify similar in style as well as in spirit to the code surrounding it, and we may ask you to change it during review. When in doubt, submit your code as-is.
Weekly meetings
Every Monday at 17:00 UTC on our IRC channel #amaranth-lang at libera.chat_ or Matrix channel #amaranth-lang:matrix.org_ (the channels are bridged together: the same messages appear on both), Amaranth maintainers meet with users and contributors to discuss newly submitted Requests for Comments and any other issues that warrant broad attention. These public meetings are the primary avenue of decision making.
.. _#amaranth-lang at libera.chat: https://web.libera.chat/#amaranth-lang
.. _#amaranth-lang:matrix.org: https://matrix.to/#/#amaranth-lang:matrix.org
If you want to contribute, have interest in language evolution, or simply want to voice your view on proposed features, feel free to join these meetings; there is no formal attendance. If you are not able to make the time, the meetings are publicly recorded and the summaries are posted in the relevant GitHub thread after the meeting.
Cover (`docs/cover.rst`)
.. This page is loaded if you click on the Amaranth logo.
Amaranth project documentation
##############################
.. toctree::
:maxdepth: 1
index
Standard I/O components https://amaranth-lang.org/docs/amaranth-stdio/latest/
System on Chip toolkit https://amaranth-lang.org/docs/amaranth-soc/latest/
Guide (`docs/guide.rst`)
.. testsetup::
Required to capture warnings in doctests.
import sys; sys.stderr = sys.stdout
Language guide
##############
.. py:currentmodule:: amaranth.hdl
This guide introduces the Amaranth language in depth. It assumes familiarity with synchronous digital logic and the Python programming language, but does not require prior experience with any hardware description language. See the :doc:tutorial <tutorial> for a step-by-step introduction to the language, and the :doc:reference <reference> for a detailed description of the Python classes that underlie the language's syntax.
.. TODO: link to a good synchronous logic tutorial and a Python tutorial?
.. _lang-prelude:
The prelude
Because Amaranth is a regular Python library, it needs to be imported before use. The root amaranth module, called the prelude, is carefully curated to export a small amount of the most essential names, useful in nearly every design. In source files dedicated to Amaranth code, it is a good practice to use a :ref:glob import for readability:
.. code-block::
from amaranth import *
However, if a source file uses Amaranth together with other libraries, or if glob imports are frowned upon, it is conventional to use a short alias instead:
.. code-block::
import amaranth as am
All of the examples below assume that a glob import is used.
.. testsetup::
from amaranth import *
.. _lang-shapes:
Shapes
A :class:Shape describes the bit width and signedness of an Amaranth value. It can be constructed directly:
.. doctest::
Shape(width=5, signed=False)
unsigned(5)
Shape(width=12, signed=True)
signed(12)
However, in most cases, the signedness of a shape is known upfront, and the convenient aliases :func:signed and :func:unsigned can be used:
.. doctest::
unsigned(5) == Shape(width=5, signed=False)
True
signed(12) == Shape(width=12, signed=True)
True
Shapes of values
All values have a .shape() method that computes their shape. The width of a value v, v.shape().width, can also be retrieved with len(v).
.. doctest::
Const(5).shape()
unsigned(3)
len(Const(5))
3
.. _lang-values:
Values
The basic building block of the Amaranth language is a value, which is a term for a binary number that is computed or stored anywhere in the design. Each value has a width---the amount of bits used to represent the value---and a signedness---the interpretation of the value by arithmetic operations---collectively called its shape. Signed values always use two's complement_ representation.
.. _two's complement: https://en.wikipedia.org/wiki/Two's_complement
.. _lang-constants:
Constants
The simplest Amaranth value is a constant, representing a fixed number, and introduced using Const(...) or its short alias C(...):
.. doctest::
ten = Const(10)
minus_two = C(-2)
The code above does not specify any shape for the constants. If the shape is omitted, Amaranth uses unsigned shape for positive numbers and signed shape for negative numbers, with the width inferred from the smallest amount of bits necessary to represent the number. As a special case, in order to get the same inferred shape for True and False, 0 is considered to be 1-bit unsigned.
.. doctest::
ten.shape()
unsigned(4)
minus_two.shape()
signed(2)
C(0).shape()
unsigned(1)
The shape of the constant can be specified explicitly, in which case the number's binary representation will be truncated or extended to fit the shape. Although rarely useful, 0-bit constants are permitted.
.. doctest::
Const(360, unsigned(8)).value
104
Const(129, signed(8)).value
-127
Const(1, unsigned(0)).value
0
.. _lang-shapelike:
Shape casting
Shapes can be cast from other objects, which are called shape-like. Casting is a convenient way to specify a shape indirectly, for example, by a range of numbers representable by values with that shape. Shapes are shape-like objects as well.
Casting to a shape can be done explicitly with :meth:Shape.cast, but is usually implicit, since shape-like objects are accepted anywhere shapes are.
.. _lang-shapeint:
Shapes from integers
Casting a shape from an integer i is a shorthand for constructing a shape with :func:unsigned(i) :
.. doctest::
Shape.cast(5)
unsigned(5)
C(0, 3).shape()
unsigned(3)
.. _lang-shaperange:
Shapes from ranges
Casting a shape from a :class:range r produces a shape that:
- has a width large enough to represent both
min(r)andmax(r), but not larger, and - is signed if
rcontains any negative values, unsigned otherwise.
Specifying a shape with a range is convenient for counters, indexes, and all other values whose width is derived from a set of numbers they must be able to fit:
.. doctest::
Const(0, range(100)).shape()
unsigned(7)
items = [1, 2, 3]
C(1, range(len(items))).shape()
unsigned(2)
.. _lang-exclrange:
.. note::
Python ranges are exclusive or half-open, meaning they do not contain their .stop element. Because of this, values with shapes cast from a range(stop) where stop is a power of 2 are not wide enough to represent stop itself:
.. doctest::
:hide:
>>> import warnings
>>> _warning_filters_backup = warnings.catch_warnings()
>>> _warning_filters_backup.__enter__() # have to do this horrific hack to make it work with `PYTHONWARNINGS=error` :(
>>> warnings.simplefilter("default", amaranth.hdl._ast.SyntaxWarning).. doctest::
>>> fencepost = C(256, range(256))
<...>:1: SyntaxWarning: Value 256 equals the non-inclusive end of the constant shape range(0, 256); this is likely an off-by-one error
fencepost = C(256, range(256))
>>> fencepost.shape()
unsigned(8)
>>> fencepost.value
0 .. doctest::
:hide:
>>> _warning_filters_backup.__exit__() Amaranth detects uses of :class:Const and :class:Signal that invoke such an off-by-one error, and emits a diagnostic message.
.. note::
An empty range always casts to an :py:unsigned(0), even if both of its bounds are negative.
This happens because, being empty, it does not contain any negative values.
.. doctest::
>>> Shape.cast(range(-1, -1))
unsigned(0).. _lang-shapeenum:
Shapes from enumerations
Casting a shape from an :class:enum.Enum subclass requires all of the enumeration members to have :ref:constant-castable <lang-constcasting> values. The shape has a width large enough to represent the value of every member, and is signed only if there is a member with a negative value.
Specifying a shape with an enumeration is convenient for finite state machines, multiplexers, complex control signals, and all other values whose width is derived from a few distinct choices they must be able to fit:
.. testsetup::
import enum
.. testcode::
class Direction(enum.Enum):
TOP = 0
LEFT = 1
BOTTOM = 2
RIGHT = 3
.. doctest::
Shape.cast(Direction)
unsigned(2)
The :mod:amaranth.lib.enum module extends the standard enumerations such that their shape can be specified explicitly when they are defined:
.. testsetup::
import amaranth.lib.enum
.. testcode::
class Funct4(amaranth.lib.enum.Enum, shape=unsigned(4)):
ADD = 0
SUB = 1
MUL = 2
.. doctest::
Shape.cast(Funct4)
unsigned(4)
.. note::
The enumeration does not have to subclass :class:enum.IntEnum or have :class:int as one of its base classes; it only needs to have integers as values of every member. Using enumerations based on :class:enum.Enum rather than :class:enum.IntEnum prevents unwanted implicit conversion of enum members to integers.
.. _lang-shapecustom:
Custom shapes
Any Python value that implements the :class:ShapeCastable interface can extend the language with a custom shape-like object. For example, the standard library module :mod:amaranth.lib.data uses this facility to add support for aggregate data types to the language.
.. _lang-valuelike:
Value casting
Like shapes, values may be cast from other objects, which are called value-like. Casting to values allows objects that are not provided by Amaranth, such as integers or enumeration members, to be used in Amaranth expressions directly. Custom value-like objects can be defined by implementing the :class:~amaranth.hdl.ValueCastable interface. Values are value-like objects as well.
Casting to a value can be done explicitly with :meth:Value.cast, but is usually implicit, since value-like objects are accepted anywhere values are.
Values from integers
Casting a value from an integer i is equivalent to :class:Const(i) <Const>:
.. doctest::
Value.cast(5)
(const 3'd5)
.. note::
If a value subclasses :class:enum.IntEnum or its class otherwise inherits from both :class:int and :class:Enum, it is treated as an enumeration.
Values from enumeration members
Casting a value from an enumeration member m is equivalent to Const(m.value, type(m)):
.. doctest::
Value.cast(Direction.LEFT)
(const 2'd1)
.. note::
If a value subclasses :class:enum.IntEnum or its class otherwise inherits from both :class:int and :class:Enum, it is treated as an enumeration.
.. _lang-constcasting:
Constant casting
A subset of :ref:values <lang-values> are constant-castable. If a value is constant-castable and all of its operands are also constant-castable, it can be converted to a :class:Const, the numeric value of which can then be read by Python code. This provides a way to perform computation on Amaranth values while constructing the design.
Constant-castable objects are accepted anywhere a constant integer is accepted. Casting to a constant can also be done explicitly with :meth:Const.cast:
.. doctest::
Const.cast(Cat(C(10, 4), C(1, 2)))
(const 6'd26)
They may be used in enumeration members, provided the enumeration inherits from :class:amaranth.lib.enum.Enum:
.. testcode::
class Funct(amaranth.lib.enum.Enum, shape=4):
ADD = 0
...
class Op(amaranth.lib.enum.Enum, shape=1):
REG = 0
IMM = 1
class Instr(amaranth.lib.enum.Enum, shape=5):
ADD = Cat(Funct.ADD, Op.REG)
ADDI = Cat(Funct.ADD, Op.IMM)
...
They may also be provided as a pattern to the :ref:match operator <lang-matchop> and the :ref:Case block <lang-switch>.
.. note::
At the moment, only the following expressions are constant-castable:
- :class:
Const - :func:
Cat - :class:
Slice
This list will be expanded in the future.
.. _lang-signals:
Signals
.. |emph:assigned| replace:: assigned
.. _emph:assigned: #lang-assigns
A signal is a value representing a (potentially) varying number. Signals can be |emph:assigned|_ in a :ref:combinational <lang-comb> or :ref:synchronous <lang-sync> domain, in which case they are generated as wires or registers, respectively. Signals always have a well-defined value; they cannot be uninitialized or undefined.
Signal shapes
A signal can be created with an explicitly specified shape (any :ref:shape-like <lang-shapelike> object); if omitted, the shape defaults to :func:unsigned(1) . Although rarely useful, 0-bit signals are permitted.
.. doctest::
Signal().shape()
unsigned(1)
Signal(4).shape()
unsigned(4)
Signal(range(-8, 7)).shape()
signed(4)
Signal(Direction).shape()
unsigned(2)
Signal(0).shape()
unsigned(0)
.. _lang-signalname:
Signal names
Each signal has a name, which is used in the waveform viewer, diagnostic messages, Verilog output, and so on. In most cases, the name is omitted and inferred from the name of the variable or attribute the signal is placed into:
.. testsetup::
class dummy(object): pass
self = dummy()
.. doctest::
foo = Signal()
foo.name
'foo'
self.bar = Signal()
self.bar.name
'bar'
However, the name can also be specified explicitly with the name= parameter:
.. doctest::
foo2 = Signal(name="second_foo")
foo2.name
'second_foo'
The names do not need to be unique; if two signals with the same name end up in the same namespace while preparing for simulation or synthesis, one of them will be renamed to remove the ambiguity.
.. _lang-initial:
Initial signal values
Each signal has an initial value, specified with the init= parameter. If the initial value is not specified explicitly, zero is used by default. An initial value can be specified with an integer or an enumeration member.
Signals :ref:assigned <lang-assigns> in a :ref:combinational <lang-comb> domain assume their initial value when none of the assignments are :ref:active <lang-active>. Signals assigned in a :ref:synchronous <lang-sync> domain assume their initial value after power-on reset and, unless the signal is :ref:reset-less <lang-resetless>, explicit reset. Signals that are used but never assigned are equivalent to constants of their initial value.
.. doctest::
Signal(4).init
0
Signal(4, init=5).init
5
Signal(Direction, init=Direction.LEFT).init
1
.. _lang-resetless:
Reset-less signals
Signals assigned in a :ref:synchronous <lang-sync> domain can be resettable or reset-less, specified with the reset_less= parameter. If the parameter is not specified, signals are resettable by default. Resettable signals assume their :ref:initial value <lang-initial> on explicit reset, which can be asserted via the :ref:clock domain <lang-clockdomains> or by :ref:modifying control flow <lang-controlinserter> with :class:ResetInserter. Reset-less signals are not affected by explicit reset.
Signals assigned in a :ref:combinational <lang-comb> domain are not affected by the reset_less parameter.
.. doctest::
Signal().reset_less
False
Signal(reset_less=True).reset_less
True
.. _lang-operators:
Operators
To describe computations, Amaranth values can be combined with each other or with :ref:value-like <lang-valuelike> objects using a rich set of arithmetic, bitwise, logical, bit sequence, and other operators to form expressions, which are themselves values.
.. _lang-abstractexpr:
Performing or describing computations?
Code written in the Python language performs computations on concrete objects, like integers, with the goal of calculating a concrete result:
.. doctest::
a = 5
a + 1
6
In contrast, code written in the Amaranth language describes computations on abstract objects, like :ref:signals <lang-signals>, with the goal of generating a hardware circuit that can be simulated, synthesized, and so on. Amaranth expressions are ordinary Python objects that represent parts of this circuit:
.. doctest::
a = Signal(8, init=5)
a + 1
(+ (sig a) (const 1'd1))
Although the syntax is similar, it is important to remember that Amaranth values exist on a higher level of abstraction than Python values. For example, expressions that include Amaranth values cannot be used in Python control flow structures:
.. doctest::
if a == 0:
... print("Zero!")
Traceback (most recent call last):
...
TypeError: Attempted to convert Amaranth value to Python boolean
Because the value of a, and therefore a == 0, is not known at the time when the if statement is executed, there is no way to decide whether the body of the statement should be executed---in fact, if the design is synthesized, by the time a has any concrete value, the Python program has long finished! To solve this problem, Amaranth provides its own :ref:control flow syntax <lang-control> that, also, manipulates circuits.
.. _lang-widthext:
Width extension
Many of the operations described below (for example, addition, equality, bitwise OR, and part select) extend the width of one or both operands to match the width of the expression. When this happens, unsigned values are always zero-extended and signed values are always sign-extended regardless of the operation or signedness of the result.
.. _lang-arithops:
Arithmetic operators
Most arithmetic operations on integers provided by Python can be used on Amaranth values, too.
Although Python integers have unlimited precision and Amaranth values are represented with a :ref:finite amount of bits <lang-values>, arithmetics on Amaranth values never overflows because the width of the arithmetic expression is always sufficient to represent all possible results.
.. doctest::
a = Signal(8)
(a + 1).shape() # needs to represent 1 to 256
unsigned(9)
Similarly, although Python integers are always signed and Amaranth values can be either :ref:signed or unsigned <lang-values>, if any of the operands of an Amaranth arithmetic expression is signed, the expression itself is also signed, matching the behavior of Python.
.. doctest::
a = Signal(unsigned(8))
b = Signal(signed(8))
(a + b).shape() # needs to represent -128 to 382
signed(10)
While arithmetic computations never result in an overflow, :ref:assigning <lang-assigns> their results to signals may truncate the most significant bits.
The following table lists the arithmetic operations provided by Amaranth:
============ ==========================
Operation Description
============ ==========================a + b addition-a negationa - b subtractiona * b multiplicationa // b floor divisiona % b moduloabs(a) absolute value
============ ==========================
.. _lang-cmpops:
Comparison operators
All comparison operations on integers provided by Python can be used on Amaranth values. However, due to a limitation of Python, chained comparisons (e.g. a < b < c) cannot be used.
Similar to arithmetic operations, if any operand of a comparison expression is signed, a signed comparison is performed. The result of a comparison is a 1-bit unsigned value.
The following table lists the comparison operations provided by Amaranth:
============ ==========================
Operation Description
============ ==========================a == b equalitya != b inequalitya < b less thana <= b less than or equala > b greater thana >= b greater than or equal
============ ==========================
.. _lang-bitops:
Bitwise, shift, and rotate operators
All bitwise and shift operations on integers provided by Python can be used on Amaranth values as well.
Similar to arithmetic operations, if any operand of a bitwise expression is signed, the expression itself is signed as well. A shift expression is signed if the shifted value is signed. A rotate expression is always unsigned.
Rotate operations with variable rotate amounts cannot be efficiently synthesized for non-power-of-2 widths of the rotated value. Because of that, the rotate operations are only provided for constant rotate amounts, specified as Python :class:int\ s.
The following table lists the bitwise and shift operations provided by Amaranth:
===================== ========================================== ======
Operation Description Notes
===================== ========================================== ======~a bitwise NOT; complementa & b bitwise ANDa | b bitwise ORa ^ b bitwise XORa >> b arithmetic right shift by variable amount [#opB1], [#opB2]a << b left shift by variable amount [#opB2]_a.rotate_left(i) left rotate by constant amount [#opB3]_a.rotate_right(i) right rotate by constant amount [#opB3]_a.shift_left(i) left shift by constant amount [#opB3]_a.shift_right(i) right shift by constant amount [#opB3]_
===================== ========================================== ======
.. [#opB1] Logical and arithmetic right shift of an unsigned value are equivalent. Logical right shift of a signed value can be expressed by :ref:converting it to unsigned <lang-convops> first.
.. [#opB2] Shift amount must be unsigned; integer shifts in Python require the amount to be positive.
.. [#opB3] Shift and rotate amounts can be negative, in which case the direction is reversed.
.. _lang-hugeshift:
.. note::
Because Amaranth ensures that the width of a variable left shift expression is wide enough to represent any possible result, variable left shift by a wide amount produces exponentially wider intermediate values, stressing the synthesis tools:
.. doctest::
>>> (1 << C(0, 32)).shape()
unsigned(4294967296)Although Amaranth will detect and reject expressions wide enough to break other tools, it is a good practice to explicitly limit the width of a shift amount in a variable left shift.
.. _lang-reduceops:
.. _lang-bool:
Reduction operators
Bitwise reduction operations on integers are not provided by Python, but are very useful for hardware. They are similar to bitwise operations applied "sideways"; for example, if bitwise AND is a binary operator that applies AND to each pair of bits between its two operands, then reduction AND is an unary operator that applies AND to all of the bits in its sole operand.
The result of a reduction is a 1-bit unsigned value.
The following table lists the reduction operations provided by Amaranth:
============ ============================================= ======
Operation Description Notes
============ ============================================= ======a.all() reduction AND; are all bits set? [#opR1]_a.any() reduction OR; is any bit set? [#opR1]_ [#opR3]_a.xor() reduction XOR; is an odd number of bits set?a.bool() conversion to boolean; is non-zero? [#opR2]_ [#opR3]_
============ ============================================= ======
.. [#opR1] Conceptually the same as applying the Python :func:all or :func:any function to the value viewed as a collection of bits.
.. [#opR2] Conceptually the same as applying the Python :class:bool function to the value viewed as an integer.
.. [#opR3] While the :meth:Value.any() and :meth:Value.bool operators return the same value, the use of a.any() implies that a is semantically a bit sequence, and the use of a.bool() implies that a is semantically a number.
.. _lang-logicops:
Logical operators
Unlike the arithmetic or bitwise operators, it is not possible to change the behavior of the Python logical operators not, and, and or. Due to that, logical expressions in Amaranth are written using bitwise operations on boolean (1-bit unsigned) values, with explicit boolean conversions added where necessary.
The following table lists the Python logical expressions and their Amaranth equivalents:
================= ====================================
Python expression Amaranth expression (any operands)
================= ====================================not a ~(a).bool()a and b (a).bool() & (b).bool()a or b (a).bool() | (b).bool()
================= ====================================
When the operands are known to be boolean values, such as comparisons, reductions, or boolean signals, the .bool() conversion may be omitted for clarity:
================= ====================================
Python expression Amaranth expression (boolean operands)
================= ====================================not p ~(p)p and q (p) & (q)p or q (p) | (q)
================= ====================================
.. _lang-logicprecedence:
.. warning::
Because of Python :ref:operator precedence , logical operators bind less tightly than comparison operators whereas bitwise operators bind more tightly than comparison operators. As a result, all logical expressions in Amaranth must have parenthesized operands.
Omitting parentheses around operands in an Amaranth a logical expression is likely to introduce a subtle bug:
.. doctest::
>>> en = Signal()
>>> addr = Signal(8)
>>> en & (addr == 0) # correct
(& (sig en) (== (sig addr) (const 1'd0)))
>>> en & addr == 0 # WRONG! addr is truncated to 1 bit
(== (& (sig en) (sig addr)) (const 1'd0)).. TODO: can we detect this footgun automatically? #380
.. _lang-negatebool:
.. warning::
When applied to Amaranth boolean values, the ~ operator computes negation, and when applied to Python boolean values, the not operator also computes negation. However, the ~ operator applied to Python boolean values produces an unexpected result:
.. doctest::
:hide:
>>> import warnings
>>> _warning_filters_backup = warnings.catch_warnings()
>>> _warning_filters_backup.__enter__() # have to do this horrific hack to make it work with `PYTHONWARNINGS=error` :(
>>> warnings.simplefilter("ignore", DeprecationWarning).. doctest::
>>> ~False
-1
>>> ~True
-2 Because of this, Python booleans used in Amaranth logical expressions must be negated with the not operator, not the ~ operator. Negating a Python boolean with the ~ operator in an Amaranth logical expression is likely to introduce a subtle bug:
.. doctest::
>>> stb = Signal()
>>> use_stb = True
>>> (not use_stb) | stb # correct
(| (const 1'd0) (sig stb))
>>> ~use_stb | stb # WRONG! MSB of 2-bit wide OR expression is always 1
(| (const 2'sd-2) (sig stb)) .. doctest::
:hide:
>>> _warning_filters_backup.__exit__() Amaranth automatically detects some cases of misuse of ~ and emits a detailed diagnostic message.
.. TODO: this isn't quite reliable, #380
.. _lang-seqops:
Bit sequence operators
Apart from acting as numbers, Amaranth values can also be treated as bit :ref:sequences , supporting slicing, concatenation, replication, and other sequence operations. Since some of the operators Python defines for sequences clash with the operators it defines for numbers, Amaranth gives these operators a different name. Except for the names, Amaranth values follow Python sequence semantics, with the least significant bit at index 0.
Because every Amaranth value has a single fixed width, bit slicing and replication operations require the subscripts and count to be constant, specified as Python :class:int\ s. It is often useful to slice a value with a constant width and variable offset, but this cannot be expressed with the Python slice notation. To solve this problem, Amaranth provides additional part select operations with the necessary semantics.
The result of any bit sequence operation is an unsigned value.
The following table lists the bit sequence operations provided by Amaranth:
========================= ================================================ ========
Operation Description Notes
========================= ================================================ ========
:py:len(a) bit length; value width [#opS1]_
:py:a[i:j:k] bit slicing by constant subscripts [#opS2]_
:py:iter(a) bit iteration
:py:a.bit_select(b, w) overlapping part select with variable offset
:py:a.word_select(b, w) non-overlapping part select with variable offset
:py:Cat(a, b) concatenation [#opS3]_
:py:a.replicate(n) replication
========================= ================================================ ========
.. [#opS1] Words "length" and "width" have the same meaning when talking about Amaranth values. Conventionally, "width" is used.
.. [#opS2] All variations of the Python slice notation are supported, including "extended slicing". E.g. all of :py:a[0], :py:a[1:9], :py:a[2:], :py:a[:-2], :py:a[::-1], :py:a[0:8:2] select bits in the same way as other Python sequence types select their elements.
.. [#opS3] In the concatenated value, :py:a occupies the least significant bits, and :py:b the most significant bits. Any number of arguments (zero, one, two, or more) are supported.
For the operators introduced by Amaranth, the following table explains them in terms of Python code operating on tuples of bits rather than Amaranth values:
======================= ======================
Amaranth operation Equivalent Python code
======================= ======================Cat(a, b) a + ba.replicate(n) a * na.bit_select(b, w) a[b:b+w]a.word_select(b, w) a[b*w:b*w+w]
======================= ======================
.. warning::
In Python, the digits of a number are written right-to-left (0th exponent at the right), and the elements of a sequence are written left-to-right (0th element at the left). This mismatch can cause confusion when numeric operations (like shifts) are mixed with bit sequence operations (like concatenations). For example, Cat(C(0b1001), C(0b1010)) has the same value as C(0b1010_1001), val[4:] is equivalent to val >> 4, and val[-1] refers to the most significant bit.
Such confusion can often be avoided by not using numeric and bit sequence operations in the same expression. For example, although it may seem natural to describe a shift register with a numeric shift and a sequence slice operations, using sequence operations alone would make it easier to understand.
.. note::
Could Amaranth have used a different indexing or iteration order for values? Yes, but it would be necessary to either place the most significant bit at index 0, or deliberately break the Python sequence type interface. Both of these options would cause more issues than using different iteration orders for numeric and sequence operations.
.. _lang-matchop:
Match operator
The :py:val.matches(*patterns) operator examines a value against a set of patterns. It evaluates to :py:Const(1) if the value matches any of the patterns, and to :py:Const(0) otherwise. What it means for a value to match a pattern depends on the type of the pattern.
If the pattern is a :class:str, it is treated as a bit mask with "don't care" bits. After removing whitespace, each character of the pattern is compared to the corresponding bit of the value, where the leftmost character of the pattern (with the lowest index) corresponds to the most significant bit of the value. If the pattern character is '0' or '1', the comparison succeeds if the bit equals 0 or 1 correspondingly. If the pattern character is '-', the comparison always succeeds. Aside from spaces and tabs, which are ignored, no other characters are accepted.
Otherwise, the pattern is :ref:cast to a constant <lang-constcasting> and compared to :py:val using the :ref:equality operator <lang-cmpops>.
For example, given a 8-bit value :py:val, :py:val.matches(1, '---- -01-') is equivalent to :py:(val == 1) | ((val & 0b0000_0110) == 0b0000_0010). Bit patterns in this operator are treated similarly to :ref:bit sequence operators <lang-bitops>.
The :ref:Case <lang-switch> control flow block accepts the same patterns, with the same meaning, as the match operator.
.. _lang-convops:
Conversion operators
The .as_signed() and .as_unsigned() conversion operators reinterpret the bits of a value with the requested signedness. This is useful when the same value is sometimes treated as signed and sometimes as unsigned, or when a signed value is constructed using slices or concatenations.
For example, (pc + imm[:7].as_signed()).as_unsigned() sign-extends the 7 least significant bits of imm to the width of pc, performs the addition, and produces an unsigned result.
.. _lang-muxop:
Choice operator
The Mux(sel, val1, val0) choice expression (similar to the :ref:conditional expression in Python) is equal to the operand val1 if sel is non-zero, and to the other operand val0 otherwise. If any of val1 or val0 are signed, the expression itself is signed as well.
.. _lang-array:
Arrays
An array is a mutable collection that can be indexed with an :class:int or with a :ref:value-like <lang-valuelike> object. When indexed with an :class:int, it behaves like a :class:list. When indexed with a value-like object, it returns a proxy object containing the elements of the array that has three useful properties:
- The result of accessing an attribute of the proxy object or indexing it is another proxy object that contains the elements transformed in the same way.
- When the proxy object is :ref:
cast to a value <lang-valuelike>, all of its elements are also cast to a value, and an element is selected using the index originally used with the array. - The proxy object can be used both in an expression and :ref:
as the target of an assignment <lang-assigns>.
Crucially, this means that any Python object can be added to an array; the only requirement is that the final result of any computation involving it is a value-like object. For example:
.. testcode::
pixels = Array([
{"r": 180, "g": 92, "b": 230},
{"r": 74, "g": 130, "b": 128},
{"r": 115, "g": 58, "b": 31},
]).. doctest::
>>> index = Signal(range(len(pixels)))
>>> pixels[index]["r"]
(proxy (array [180, 74, 115]) (sig index)).. note::
An array becomes immutable after it is indexed for the first time. The elements of the array do not themselves become immutable, but it is not recommended to mutate them as the behavior can become unpredictable... note::
Arrays, :class:`amaranth.hdl.Array`, are distinct from and serve a different function than :class:`amaranth.lib.data.ArrayLayout`... _lang-data:
Data structures
Amaranth provides aggregate data structures in the standard library module :mod:amaranth.lib.data.
.. _lang-modules:
Modules
A module is a unit of the Amaranth design hierarchy: the smallest collection of logic that can be independently simulated, synthesized, or otherwise processed. Modules associate signals with :ref:control domains <lang-domains>, provide :ref:control flow syntax <lang-control>, manage :ref:clock domains <lang-clockdomains>, and aggregate :ref:submodules <lang-submodules>.
Every Amaranth design starts with a fresh module:
.. doctest::
m = Module()
.. _lang-domains:
Control domains
A control domain is a named group of :ref:signals <lang-signals> that change their value in identical conditions.
All designs have a single predefined combinational domain, containing all signals that change immediately when any value used to compute them changes. The name comb is reserved for the combinational domain, and refers to the same domain in all modules.
A design can also have any amount of user-defined synchronous domains, also called :ref:clock domains <lang-clockdomains>, containing signals that change when a specific edge occurs on the domain's clock signal or, for domains with asynchronous reset, on the domain's reset signal. Most modules only use a single synchronous domain, conventionally called sync, but the name sync does not have to be used, and lacks any special meaning beyond being the default.
The behavior of assignments differs for signals in :ref:combinational <lang-comb> and :ref:synchronous <lang-sync> domains. Collectively, signals in synchronous domains contain the state of a design, whereas signals in the combinational domain cannot form feedback loops or hold state.
.. _lang-assigns:
Assigning to signals
Assignments are used to change the values of signals. An assignment statement can be introduced with the .eq(...) syntax:
.. doctest::
s = Signal()
s.eq(1)
(eq (sig s) (const 1'd1))
Similar to :ref:how Amaranth operators work <lang-abstractexpr>, an Amaranth assignment is an ordinary Python object used to describe a part of a circuit. An assignment does not have any effect on the signal it changes until it is added to a control domain in a module. Once added, it introduces logic into the circuit generated from that module.
.. _lang-assignable:
Assignable values
An assignment can affect a value that is more complex than just a signal. It is possible to assign to any combination of :ref:signals <lang-signals>, :ref:bit slices <lang-seqops>, :ref:concatenations <lang-seqops>, :ref:part selects <lang-seqops>, and :ref:array proxy objects <lang-array> as long as it includes no other values:
.. doctest::
a = Signal(8)
b = Signal(4)
Cat(a, b).eq(0)
(eq (cat (sig a) (sig b)) (const 1'd0))
a[:4].eq(b)
(eq (slice (sig a) 0:4) (sig b))
Cat(a, a).bit_select(b, 2).eq(0b11)
(eq (part (cat (sig a) (sig a)) (sig b) 2 1) (const 2'd3))
.. _lang-assigndomains:
Assignment domains
The m.d.<domain> += ... syntax is used to add assignments to a specific control domain in a module. It can add just a single assignment, or an entire sequence of them:
.. testcode::
a = Signal()
b = Signal()
c = Signal()
m.d.comb += a.eq(1)
m.d.sync += [
b.eq(c),
c.eq(b),
]
If the name of a domain is not known upfront, the m.d["<domain>"] += ... syntax can be used instead:
.. testcode::
def add_toggle(num):
t = Signal()
m.d[f"sync_{num}"] += t.eq(~t)
add_toggle(2)
.. _lang-signalgranularity:
Every signal bit included in the target of an assignment becomes a part of the domain, or equivalently, driven by that domain. A signal bit can be either undriven or driven by exactly one domain; it is an error to add two assignments to the same signal bit to two different domains:
.. doctest::
d = Signal()
m.d.comb += d.eq(1)
m.d.sync += d.eq(0)
Traceback (most recent call last):
...
amaranth.hdl.dsl.SyntaxError: Driver-driver conflict: trying to drive (sig d) bit 0 from d.sync, but it is already driven from d.comb
However, two different bits of a signal can be driven from two different domains without an issue:
.. testcode::
e = Signal(2)
m.d.comb += e[0].eq(1)
m.d.sync += e[1].eq(0)
In addition to assignments, :ref:assertions <lang-assert> and :ref:debug prints <lang-print> can be added using the same syntax.
.. _lang-assignorder:
Assignment order
Unlike with two different domains, adding multiple assignments to the same signal to the same domain is well-defined.
Assignments to different signal bits apply independently. For example, the following two snippets are equivalent:
.. testcode::
a = Signal(8)
m.d.comb += [
a[0:4].eq(C(1, 4)),
a[4:8].eq(C(2, 4)),
]
.. testcode::
a = Signal(8)
m.d.comb += a.eq(Cat(C(1, 4), C(2, 4)))
If multiple assignments change the value of the same signal bits, the assignment that is added last determines the final value. For example, the following two snippets are equivalent:
.. testcode::
b = Signal(9)
m.d.comb += [
b[0:9].eq(Cat(C(1, 3), C(2, 3), C(3, 3))),
b[0:6].eq(Cat(C(4, 3), C(5, 3))),
b[3:6].eq(C(6, 3)),
]
.. testcode::
b = Signal(9)
m.d.comb += b.eq(Cat(C(4, 3), C(6, 3), C(3, 3)))
Multiple assignments to the same signal bits are more useful when combined with control structures, which can make some of the assignments :ref:active or inactive <lang-active>. If all assignments to some signal bits are :ref:inactive <lang-active>, their final values are determined by the signal's domain, :ref:combinational <lang-comb> or :ref:synchronous <lang-sync>.
.. _lang-control:
Control flow
Although it is possible to write any decision tree as a combination of :ref:assignments <lang-assigns> and :ref:choice expressions <lang-muxop>, Amaranth provides control flow syntax tailored for this task: :ref:If/Elif/Else <lang-if>, :ref:Switch/Case <lang-switch>, and :ref:FSM/State <lang-fsm>. The control flow syntax uses :py:with blocks (it is implemented using :ref:context managers ), for example:
.. testcode::
timer = Signal(8)
with m.If(timer == 0):
m.d.sync += timer.eq(10)
with m.Else():
m.d.sync += timer.eq(timer - 1)
While some Amaranth control structures are superficially similar to imperative control flow statements (such as Python's :py:if), their function---together with :ref:expressions <lang-abstractexpr> and :ref:assignments <lang-assigns>---is to describe circuits. The code above is equivalent to:
.. testcode::
timer = Signal(8)
m.d.sync += timer.eq(Mux(timer == 0, 10, timer - 1))
Because all branches of a decision tree affect the generated circuit, all of the Python code inside Amaranth control structures is always evaluated in the order in which it appears in the program. This can be observed through Python code with side effects, such as :py:print():
.. testcode::
timer = Signal(8)
with m.If(timer == 0):
print("inside If")
m.d.sync += timer.eq(10)
with m.Else():
print("inside Else")
m.d.sync += timer.eq(timer - 1)
.. testoutput::
inside If
inside Else
.. _lang-active:
Active and inactive assignments
An assignment added inside an Amaranth control structure, i.e. with m.<...>: block, is active if the condition of the control structure is satisfied, and inactive otherwise. For any given set of conditions, the final value of every signal assigned in a module is the same as if the inactive assignments were removed and the active assignments were performed unconditionally, taking into account the :ref:assignment order <lang-assignorder>.
For example, there are two possible cases in the circuit generated from the following code:
.. testcode::
timer = Signal(8)
m.d.sync += timer.eq(timer - 1)
with m.If(timer == 0):
m.d.sync += timer.eq(10)
When timer == 0 is true, the code reduces to:
.. code-block::
m.d.sync += timer.eq(timer - 1)
m.d.sync += timer.eq(10)
Due to the :ref:assignment order <lang-assignorder>, it further reduces to:
.. code-block::
m.d.sync += timer.eq(10)
When timer == 0 is false, the code reduces to:
.. code-block::
m.d.sync += timer.eq(timer - 1)
Combining these cases together, the code above is equivalent to:
.. testcode::
timer = Signal(8)
m.d.sync += timer.eq(Mux(timer == 0, 10, timer - 1))
.. _lang-if:
:py:`If`/:py:`Elif`/:py:`Else` control blocks
Conditional control flow is described using a :py:with m.If(cond1): block, which may be followed by one or more :py:with m.Elif(cond2): blocks, and optionally a final :py:with m.Else(): block. This structure parallels Python's own :ref:if/elif/else control flow syntax. For example:
.. testcode::
:hide:
x_coord = Signal(8)
is_fporch = Signal()
is_active = Signal()
is_bporch = Signal().. testcode::
with m.If(x_coord < 4):
m.d.comb += is_bporch.eq(1)
m.d.sync += x_coord.eq(x_coord + 1)
with m.Elif((x_coord >= 4) & (x_coord < 364)):
m.d.comb += is_active.eq(1)
m.d.sync += x_coord.eq(x_coord + 1)
with m.Elif((x_coord >= 364) & (x_coord < 374)):
m.d.comb += is_fporch.eq(1)
m.d.sync += x_coord.eq(x_coord + 1)
with m.Else():
m.d.sync += x_coord.eq(0)Within a single :py:If/:py:Elif/:py:Else sequence of blocks, the statements within at most one block will be active at any time. This will be the first block in the order of definition whose condition, :ref:converted to boolean <lang-bool>, is true.
If an :py:Else block is present, then the statements within exactly one block will be active at any time, and the sequence as a whole is called a full condition.
.. _lang-switch:
:py:`Switch`/:py:`Case` control blocks
Case comparison, where a single value is examined against several different patterns, is described using a :py:with m.Switch(value): block. This block can contain any amount of :py:with m.Case(*patterns) and :py:with m.Default(): blocks. This structure parallels Python's own :ref:match/case control flow syntax. For example:
.. TODO: rename Switch to Match, to mirror Value.matches()?
.. testcode::
:hide:
is_even = Signal()
is_odd = Signal()
too_big = Signal().. testcode::
value = Signal(4)
with m.Switch(value):
with m.Case(0, 2, 4):
m.d.comb += is_even.eq(1)
with m.Case(1, 3, 5):
m.d.comb += is_odd.eq(1)
with m.Default():
m.d.comb += too_big.eq(1)Within a single :py:Switch block, the statements within at most one block will be active at any time. This will be the first :py:Case block in the order of definition whose pattern :ref:matches <lang-matchop> the value, or the first :py:Default block, whichever is earlier.
If a :py:Default block is present, or the patterns in the :py:Case blocks cover every possible :py:Switch value, then the statements within exactly one block will be active at any time, and the sequence as a whole is called a full condition.
.. tip::
While all Amaranth control flow syntax can be generated programmatically, the :py:`Switch` control block is particularly easy to use in this way:
.. testcode::
length = Signal(4)
squared = Signal.like(length * length)
with m.Switch(length):
for value in range(length.shape().width):
with m.Case(value):
m.d.comb += squared.eq(value * value).. _lang-fsm:
:py:`FSM`/:py:`State` control blocks
Simple finite state machines <https://en.wikipedia.org/wiki/Finite-state_machine>_ are described using a :py:with m.FSM(): block. This block can contain one or more :py:with m.State("Name") blocks. In addition to these blocks, the :py:m.next = "Name" syntax chooses which state the FSM enters on the next clock cycle. For example, this FSM performs a bus read transaction once after reset:
.. testcode::
bus_addr = Signal(16)
r_data = Signal(8)
r_en = Signal()
latched = Signal.like(r_data)
with m.FSM():
with m.State("Set Address"):
m.d.sync += bus_addr.eq(0x1234)
m.next = "Strobe Read Enable"
with m.State("Strobe Read Enable"):
m.d.comb += r_en.eq(1)
m.next = "Sample Data"
with m.State("Sample Data"):
m.d.sync += latched.eq(r_data)
with m.If(r_data == 0):
m.next = "Set Address" # try again.. TODO: FSM() should require keyword arguments, for good measure
The initial (and reset) state of the FSM can be provided when defining it using the :py:with m.FSM(init="Name"): argument. If not provided, it is the first state in the order of definition. For example, this definition is equivalent to the one at the beginning of this section:
.. testcode::
with m.FSM(init="Set Address"):
...The FSM belongs to a :ref:clock domain <lang-domains>, which is specified using the :py:with m.FSM(domain="dom") argument. If not specified, it is the sync domain. For example, this definition is equivalent to the one at the beginning of this section:
.. testcode::
with m.FSM(domain="sync"):
...To determine (from code that is outside the FSM definition) whether it is currently in a particular state, the FSM can be captured; its :py:.ongoing("Name") method returns a value that is true whenever the FSM is in the corresponding state. For example:
.. testcode::
with m.FSM() as fsm:
...
with m.If(fsm.ongoing("Set Address")):
...Note that in Python, assignments made using :py:with x() as y: syntax persist past the end of the block.
.. warning::
If a non-string object is provided as a state name to :py:`with m.State(...):`, it is cast to a string first, which may lead to surprising behavior. :py:`with m.State(...):` **does not** treat an enumeration value specially; if one is provided, it is cast to a string, and its numeric value will have no correspondence to the numeric value of the generated state signal.
This hazard will be eliminated in the future... TODO: we should probably have fsm.next = "Name" or fsm.next("Name") instead
.. note::
If you are nesting two state machines within each other, the :py:`m.next = ...` syntax always refers to the innermost one. To change the state of the outer state machine from within the inner one, use an intermediate signal... _lang-comb:
Combinational evaluation
Signals in the combinational :ref:control domain <lang-domains> change whenever any value used to compute them changes. The final value of a combinational signal is equal to its :ref:initial value <lang-initial> updated by the :ref:active assignments <lang-active> in the :ref:assignment order <lang-assignorder>. Combinational signals cannot hold any state.
Consider the following code:
.. testcode::
:hide:
en = Signal()
b = Signal(8).. testcode::
a = Signal(8, init=1)
with m.If(en):
m.d.comb += a.eq(b + 1)Whenever the signals en or b change, the signal a changes as well. If en is false, the final value of a is its initial value, 1. If en is true, the final value of a is equal to b + 1.
A combinational signal that is computed directly or indirectly based on its own value is a part of a combinational feedback loop, sometimes shortened to just feedback loop. Combinational feedback loops can be stable (e.g. implement a constant driver or a transparent latch), or unstable (e.g. implement a ring oscillator). Amaranth prohibits using assignments to describe any kind of a combinational feedback loop, including transparent latches.
.. note::
In the exceedingly rare case when a combinational feedback loop is desirable, it is possible to implement it by directly instantiating technology primitives (e.g. device-specific LUTs or latches). This is also the only way to introduce a combinational feedback loop with well-defined behavior in simulation and synthesis, regardless of the HDL being used.
.. _lang-sync:
Synchronous evaluation
Signals in synchronous :ref:control domains <lang-domains> change whenever the active edge (a 0-to-1 or 1-to-0 transition, configured when :ref:creating the domain <lang-clockdomains>) occurs on the clock of the synchronous domain. In addition, the signals in :ref:clock domains <lang-clockdomains> with an asynchronous reset change when such a reset is asserted. The final value of a synchronous signal is equal to its :ref:initial value <lang-initial> if the reset (of any type) is asserted, or to its current value updated by the :ref:active assignments <lang-active> in the :ref:assignment order <lang-assignorder> otherwise. Synchronous signals always hold state.
Consider the following code:
.. testcode::
:hide:
up = Signal()
down = Signal().. testcode::
timer = Signal(8)
with m.If(up):
m.d.sync += timer.eq(timer + 1)
with m.Elif(down):
m.d.sync += timer.eq(timer - 1)Whenever there is a transition on the clock of the sync domain, the :py:timer signal is incremented by one if :py:up is true, decremented by one if :py:down is true, and retains its value otherwise.
.. _lang-assert:
Assertions
Some properties are so important that if they are violated, the computations described by the design become meaningless. These properties should be guarded with an :class:Assert statement that immediately terminates the simulation if its condition is false. Assertions should generally be added to a :ref:synchronous domain <lang-sync>, and may have an optional message printed when it is violated:
.. testcode::
ip = Signal(16)
m.d.sync += Assert(ip < 128, "instruction pointer past the end of program code!")Assertions may be nested within a :ref:control block <lang-control>:
.. testcode::
:hide:
booting = Signal().. testcode::
with m.If(~booting):
m.d.sync += Assert(ip < 128).. warning::
While is is also possible to add assertions to the :ref:`combinational domain <lang-comb>`, simulations of combinational circuits may have *glitches*: instantaneous, transient changes in the values of expressions that are being computed which do not affect the result of the computation (and are not visible in most waveform viewers for that reason). Depending on the tools used for simulation, a glitch in the condition of an assertion or of a :ref:`control block <lang-control>` that contains it may cause the simulation to be terminated, even if the glitch would have been instantaneously resolved afterwards.
If the condition of an assertion is assigned in a synchronous domain, then it is safe to add that assertion in the combinational domain. For example, neither of the assertions in the example below will be violated due to glitches, regardless of which domain the :py:`ip` and :py:`booting` signals are driven by:
.. testcode::
ip_sync = Signal.like(ip)
m.d.sync += ip_sync.eq(ip)
m.d.comb += Assert(ip_sync < 128)
with m.If(booting):
m.d.comb += Assert(ip_sync < 128)
Assertions should be added in a :ref:`synchronous domain <lang-sync>` when possible. In cases where it is not, such as if the condition is a signal that is assigned in a synchronous domain elsewhere, care should be taken while adding the assertion to the combinational domain... _lang-print:
Debug printing
The value of any expression, or of several of them, can be printed to the terminal during simulation using the :class:Print statement. When added to the :ref:combinational domain <lang-comb>, the value of an expression is printed whenever it changes:
.. testcode::
state = Signal()
m.d.comb += Print(state)When added to a :ref:synchronous domain <lang-sync>, the value of an expression is printed whenever the active edge occurs on the clock of that domain:
.. testcode::
m.d.sync += Print("on tick: ", state)The :class:Print statement, regardless of the domain, may be nested within a :ref:control block <lang-control>:
.. testcode::
old_state = Signal.like(state)
m.d.sync += old_state.eq(state)
with m.If(state != old_state):
m.d.sync += Print("was: ", old_state, "now: ", state)The arguments to the :class:Print statement have the same meaning as the arguments to the Python :func:print function, with the exception that only :py:sep and :py:end keyword arguments are supported. In addition, the :class:Format helper can be used to apply formatting to the values, similar to the Python :meth:str.format method:
.. testcode::
addr = Signal(32)
m.d.sync += Print(Format("address: {:08x}", addr))In both :class:Print and :class:Format, arguments that are not Amaranth :ref:values <lang-values> are formatted using the usual Python rules. The optional second :py:message argument to :class:Assert (described :ref:above <lang-assert>) also accepts a string or the :class:Format helper:
.. testcode::
m.d.sync += Assert((addr & 0b111) == 0, message=Format("unaligned address {:08x}!", addr)).. _lang-clockdomains:
Clock domains
A new synchronous :ref:control domain <lang-domains>, which is more often called a clock domain, can be defined in a design by creating a :class:ClockDomain object and adding it to the :py:m.domains collection:
.. testcode::
m.domains.video = cd_video = ClockDomain()If the name of the domain is not known upfront, another, less concise, syntax can be used instead:
.. testcode::
def add_video_domain(n):
cd = ClockDomain(f"video_{n}")
m.domains += cd
return cd
add_video_domain(2).. note::
Whenever the created :class:`ClockDomain` object is immediately assigned using the :py:`domain_name = ClockDomain(...)` or :py:`m.domains.domain_name = ClockDomain(...)` syntax, the name of the domain may be omitted from the :py:`ClockDomain()` invocation. In other cases, it must be provided as the first argument.A clock domain always has a clock signal, which can be accessed through the :attr:cd.clk <ClockDomain.clk> attribute. By default, the active edge of the clock domain is positive; this means that the signals in the domain change when the clock signal transitions from 0 to 1. A clock domain can be configured to have a negative active edge so that signals in it change when the clock signal transitions from 1 to 0:
.. testcode::
m.domains.jtag = ClockDomain(clk_edge="neg")A clock domain also has a reset signal, which can be accessed through the :attr:cd.rst <ClockDomain.rst> attribute. The reset signal is always active-high: the signals in the clock domain are reset if the value of the reset signal is 1. The :ref:initial value <lang-initial> of this signal is 0, so if the reset signal is never assigned, the signals in the clock domain are never explicitly reset (they are still :ref:reset at power-on <lang-initial>). Nevertheless, if its existence is undesirable, the clock domain can be configured to omit it:
.. testcode::
m.domains.startup = ClockDomain(reset_less=True)Signals in a reset-less clock domain can still be explicitly reset using the :class:ResetInserter :ref:control flow modifier <lang-controlinserter>.
If a clock domain is defined in a module, all of its :ref:submodules <lang-submodules> can refer to that domain under the same name.
.. warning::
Clock domains use synchronous reset unless otherwise specified. Clock domains with asynchronous reset are implemented, but their behavior is subject to change in near future, and is intentionally left undocumented... tip::
Unless you need to introduce a new asynchronous control set in the design, consider :ref:`using ResetInserter or EnableInserter <lang-controlinserter>` instead of defining a new clock domain. Designs with fewer clock domains are easier to reason about.
A new asynchronous control set is necessary when some signals must change on a different active edge of a clock, at a different frequency, with a different phase, or when a different asynchronous reset signal is asserted... _lang-latesignals:
Late binding of clock and reset signals
Clock domains are late bound, which means that their signals and properties can be referred to using the domain's name before the :class:ClockDomain object with that name is created and added to the design. This happens whenever :ref:an assignment is added <lang-assigns> to a domain. In some cases, it is necessary to refer to the domain's clock or reset signal using only the domain's name. The :class:ClockSignal and :class:ResetSignal values make this possible:
.. testcode::
:hide:
m = Module()
bus_clk = Signal()
bus_rstn = Signal().. testcode::
m.d.comb += [
ClockSignal().eq(bus_clk),
ResetSignal().eq(~bus_rstn),
]In this example, once the design is processed, the clock signal of the clock domain sync found in this module or one of its containing modules will be equal to :py:bus_clk. The reset signal of the same clock domain will be equal to the negated :py:bus_rstn. With the sync domain created in the same module, these statements become equivalent to:
.. TODO: explain the difference (or lack thereof, eventually) between m.d, m.domain, and m.domains
.. testcode::
m.domains.sync = cd_sync = ClockDomain()
m.d.comb += [
cd_sync.clk.eq(bus_clk),
cd_sync.rst.eq(~bus_rstn),
]The :class:ClockSignal and :class:ResetSignal values may also be assigned to other signals and used in expressions. They take a single argument, which is the name of the domain; if not specified, it defaults to :py:"sync".
.. warning::
Be especially careful when using :class:`ClockSignal` or :attr:`cd.clk <ClockDomain.clk>` in expressions. Assigning to and from a clock signal is usually safe; any other operations may have unpredictable results. Consult the documentation for your synthesis toolchain and platform to understand which operations with a clock signal are permitted.
FPGAs usually have dedicated clocking facilities that can be used to disable, divide, or multiplex clock signals. When targeting an FPGA, these facilities should be used if at all possible, and expressions like :py:`ClockSignal() & en` or :py:`Mux(sel, ClockSignal("a"), ClockSignal("b"))` should be avoided... _lang-elaboration:
Elaboration
Amaranth designs are built from a hierarchy of smaller subdivisions, which are called elaboratables. The process of creating a data structure representing the behavior of a complete design by composing such subdivisions together is called elaboration.
An elaboratable is any Python object that inherits from the :class:Elaboratable base class and implements the :meth:~Elaboratable.elaborate method:
.. testcode::
class Counter(Elaboratable):
def elaborate(self, platform):
m = Module()
...
return mThe :meth:~Elaboratable.elaborate method must either return an instance of :class:Module or :class:Instance to describe the behavior of the elaboratable, or delegate it by returning another elaboratable object.
.. note::
Instances of :class:`Module` also implement the :meth:`~Elaboratable.elaborate` method, which returns a special object that represents a fragment of a netlist. Such an object cannot be constructed without using :class:`Module`.The :py:platform argument received by the :meth:~Elaboratable.elaborate method can be :py:None, an instance of :ref:a built-in platform , or a custom object. It is used for dependency injection <https://en.wikipedia.org/wiki/Dependency_injection>_ and to contain the state of a design while it is being elaborated.
.. warning::
The :meth:`~Elaboratable.elaborate` method should not modify the ``self`` object it receives other than for debugging and experimentation. Elaborating the same design twice with two identical platform objects should produce two identical netlists. If the design needs to be modified after construction, this should happen before elaboration.
It is not possible to ensure that a design which modifies itself during elaboration is correctly converted to a netlist because the relative order in which the :meth:`~Elaboratable.elaborate` methods are called within a single design is not guaranteed.The Amaranth standard library provides components: elaboratable objects that also include a description of their interface. Unless otherwise necessary, an elaboratable should inherit from :class:amaranth.lib.wiring.Component rather than plain :class:Elaboratable. See the :ref:introduction to interfaces and components <wiring-introduction> for details.
.. _lang-submodules:
Submodules
An elaboratable can be included within another elaboratable, which is called its containing elaboratable, by adding it as a submodule:
.. testcode::
m.submodules.counter = counter = Counter()If the name of a submodule is not known upfront, a different syntax should be used:
.. testcode::
for n in range(3):
m.submodules[f"counter_{n}"] = Counter()A submodule can also be added without specifying a name:
.. testcode::
counter = Counter()
m.submodules += counter.. tip::
If a name is not explicitly specified for a submodule, one will be generated and assigned automatically. Designs with many autogenerated names can be difficult to debug, so a name should usually be supplied.A non-Amaranth design unit can be added as a submodule using an :ref:instance <lang-instance>.
.. _lang-controlinserter:
Modifying control flow
Control flow within an elaboratable can be altered without introducing a new clock domain by using control flow modifiers that affect :ref:synchronous evaluation <lang-sync> of signals in a specified domain (or domains). They never affect :ref:combinational evaluation <lang-comb>. There are two control flow modifiers:
- :class:
ResetInserterintroduces a synchronous reset input (or inputs), updating all of the signals in the specified domains to their :ref:initial value <lang-initial>whenever the active edge occurs on the clock of the domain if the synchronous reset input is asserted. - :class:
EnableInserterintroduces a synchronous enable input (or inputs), preventing any of the signals in the specified domains from changing value whenever the active edge occurs on the clock of the domain unless the synchronous enable input is asserted.
Control flow modifiers use the syntax :py:Modifier(controls)(elaboratable), where :py:controls is a mapping from :ref:clock domain <lang-clockdomains> names to 1-wide :ref:values <lang-values> and :py:elaboratable is any :ref:elaboratable <lang-elaboration> object. When only the sync domain is involved, instead of writing :py:Modifier({"sync": input})(elaboratable), the equivalent but shorter :py:Modifier(input)(elaboratable) syntax can be used.
The result of applying a control flow modifier to an elaboratable is, itself, an elaboratable object. A common way to use a control flow modifier is to apply it to another elaboratable while adding it as a submodule:
.. testcode::
:hide:
m = Module().. testcode::
rst = Signal()
m.submodules.counter = counter = ResetInserter(rst)(Counter())A control flow modifier affects all logic within a given elaboratable and clock domain, which includes the submodules of that elaboratable.
.. note::
Applying a control flow modifier to an elaboratable does not mutate it; a new proxy object is returned that forwards attribute accesses and method calls to the original elaboratable. Whenever this proxy object is elaborated, it manipulates the circuit defined by the original elaboratable to include the requested control inputs... note::
It is possible to apply several control flow modifiers to the same elaboratable, even if the same domain is used. For :class:`ResetInserter`, the signals in a domain are held at their initial value whenever any of the reset inputs for that domain are asserted (logical OR), and for :class:`EnableInserter`, the signals in a domain are allowed to update whenever all of the enable signals for that domain are asserted (logical AND).Consider the following code:
.. testcode::
:hide:
z = Signal()
n = Signal(8)
en = Signal()
rst = Signal().. testcode::
m = Module()
m.d.sync += n.eq(n + 1)
m.d.comb += z.eq(n == 0)
m = ResetInserter({"sync": rst})(m)
m = EnableInserter({"sync": en})(m)The application of control flow modifiers in it causes the behavior of the final :py:m to be identical to that of this module:
.. testcode::
m = Module()
with m.If(en):
m.d.sync += n.eq(n + 1)
with m.If(rst):
m.d.sync += n.eq(n.init)
m.d.comb += z.eq(n == 0).. tip::
The control input provided to :class:`ResetInserter` must be synchronous to the domain that is being reset by it. If you need to reset another domain, use :class:`amaranth.lib.cdc.ResetSynchronizer` instead... TODO: link to a clock gating primitive if/when we ever get one, from a tip about EnableInserter similar to the tip about ResetInserter above
.. _lang-domainrenamer:
Renaming domains
A reusable :ref:elaboratable <lang-elaboration> usually specifies the use of one or more :ref:clock domains <lang-clockdomains> while leaving the details of clocking and initialization to a later phase in the design process. :class:DomainRenamer can be used to alter a reusable elaboratable for integration in a specific design. Most elaboratables use a single clock domain named sync, and :class:DomainRenamer makes it easy to place such elaboratables in any clock domain of a design.
Clock domains can be renamed using the syntax :py:DomainRenamer(domains)(elaboratable), where :py:domains is a mapping from clock domain names to clock domain names and :py:elaboratable is any :ref:elaboratable <lang-elaboration> object. The keys of :py:domains correspond to existing clock domain names specified by :py:elaboratable, and the values of :py:domains correspond to the clock domain names from the containing elaboratable that will be used instead. When only the sync domain is being renamed, instead of writing :py:DomainRenamer({"sync": name})(elaboratable), the equivalent but shorter :py:DomainRenamer(name)(elaboratable) syntax can be used.
The result of renaming clock domains in an elaboratable is, itself, an elaboratable object. A common way to rename domains is to apply :class:DomainRenamer to another elaboratable while adding it as a submodule:
.. testcode::
:hide:
m = Module().. testcode::
m.submodules.counter = counter = DomainRenamer("video")(counter)Renaming a clock domain affects all logic within a given elaboratable and clock domain, which includes the submodules of that elaboratable. It does not affect any logic outside of that elaboratable.
.. note::
Renaming domains in an elaboratable does not mutate it; a new proxy object is returned that forwards attribute accesses and method calls to the original elaboratable. Whenever this proxy object is elaborated, it manipulates the circuit defined by the original elaboratable to use the requested clock domain... note::
It is possible to rename domains in an elaboratable and also apply :ref:`control flow modifiers <lang-controlinserter>`.Consider the following code:
.. testcode::
:hide:
count = Signal(8)
zero = Signal().. testcode::
m = Module()
m.d.sync += count.eq(count + 1)
m.d.comb += zero.eq(count == 0)
m = DomainRenamer({"sync": "video"})(m)The renaming of the sync clock domain in it causes the behavior of the final :py:m to be identical to that of this module:
.. testcode::
m = Module()
m.d.video += count.eq(count + 1)
m.d.comb += zero.eq(count == 0).. warning::
A combinational signal can change synchronously to a clock domain, as in the example above, in which case it may only be sampled from the same clock domain unless explicitly synchronized. Renaming a clock domain must be assumed to potentially affect any output of an elaboratable... _lang-memory:
Memories
Amaranth provides support for memories in the standard library module :mod:amaranth.lib.memory.
.. _lang-iovalues:
I/O values
To interoperate with external circuitry, Amaranth provides core I/O values, which represent bundles of wires carrying uninterpreted signals. Unlike regular :ref:values <lang-values>, which represent binary numbers and can be :ref:assigned <lang-assigns> to create a unidirectional connection or used in computations, core I/O values represent electrical signals that may be digital or analog and have no :ref:shape <lang-shapes>, cannot be assigned, used in computations, or simulated.
Core I/O values are only used to define connections between non-Amaranth building blocks that traverse an Amaranth design, including :ref:instances <lang-instance> and :ref:I/O buffer instances <lang-iobufferinstance>.
.. _lang-ioports:
I/O ports
A core I/O port is a core I/O value representing a connection to a port of the topmost module in the :ref:design hierarchy <lang-submodules>. It can be created with an explicitly specified width.
.. testcode::
from amaranth.hdl import IOPort.. doctest::
>>> port = IOPort(4)
>>> port.width
4Core I/O ports can be named in the same way as :ref:signals <lang-signalname>:
.. doctest::
>>> clk_port = IOPort(1, name="clk")
>>> clk_port.name
'clk'If two core I/O ports with the same name exist in a design, one of them will be renamed to remove the ambiguity. Because the name of a core I/O port is significant, they should be named unambiguously.
.. _lang-ioops:
I/O operators
Core I/O values support only a limited set of :ref:sequence operators, all of which return another core I/O value. The following table lists the operators provided by Amaranth for core I/O values:
=============== ============================== ===================
Operation Description Notes
=============== ============================== ===================
:py:len(a) length; width [#iopS1]_
:py:a[i:j:k] slicing by constant subscripts [#iopS2]_
:py:iter(a) iteration
:py:Cat(a, b) concatenation [#iopS3]_ [#iopS4]_
=============== ============================== ===================
.. [#iopS1] Words "length" and "width" have the same meaning when talking about Amaranth I/O values. Conventionally, "width" is used.
.. [#iopS2] All variations of the Python slice notation are supported, including "extended slicing". E.g. all of :py:a[0], :py:a[1:9], :py:a[2:], :py:a[:-2], :py:a[::-1], :py:a[0:8:2] select wires in the same way as other Python sequence types select their elements.
.. [#iopS3] In the concatenated value, :py:a occupies the lower indices and :py:b the higher indices. Any number of arguments (zero, one, two, or more) are supported.
.. [#iopS4] Concatenation of zero arguments, :py:Cat(), returns a 0-bit regular value, however any such value is accepted (and ignored) anywhere an I/O value is expected.
.. _lang-instance:
Instances
.. attributes are not documented because they can be easily used to break soundness and we don't document them for signals either; they are rarely necessary for interoperability
A submodule written in a non-Amaranth language is called an instance. An instance can be written in any language supported by the synthesis toolchain; usually, that is (System)Verilog, VHDL, or a language that is translated to one of those two. Adding an instance as a submodule corresponds to "module instantiation" in (System)Verilog and "component instantiation" in VHDL, and is done by specifying the following:
- The type of an instance is the name of a (System)Verilog module, VHDL entity or component, or another HDL design unit that is being instantiated.
- The name of an instance is the name of the submodule within the containing elaboratable.
- The attributes of an instance correspond to attributes of a (System)Verilog module instance, or a custom attribute of a VHDL entity or component instance. Attributes applied to instances are interpreted by the synthesis toolchain rather than the HDL.
- The parameters of an instance correspond to parameters of a (System)Verilog module instance, or a generic constant of a VHDL entity or component instance. Not all HDLs allow their design units to be parameterized during instantiation.
- The inputs, outputs, and inouts of an instance correspond to input ports, output ports, and bidirectional ports of the external design unit.
An instance can be added as a submodule using the :py:m.submodules.name = Instance("type", ...) syntax, where :py:"type" is the type of the instance as a string (which is passed to the synthesis toolchain uninterpreted), and :py:... is a list of parameters, inputs, and outputs. Depending on whether the name of an attribute, parameter, input, or output can be written as a part of a Python identifier or not, one of two possible syntaxes is used to specify them:
- An attribute is specified using the :py:
a_ANAME=attror :py:("a", "ANAME", attr)syntaxes. The :py:attrmust be an :class:int, a :class:str, or a :class:Const. - A parameter is specified using the :py:
p_PNAME=paramor :py:("p", "PNAME", param)syntaxes. The :py:parammust be an :class:int, a :class:str, or a :class:Const. - An input is specified using the :py:
i_INAME=in_valor :py:("i", "INAME", in_val)syntaxes. The :py:in_valmust be a :ref:core I/O value <lang-iovalues>or a :ref:value-like <lang-valuelike>object. - An output is specified using the :py:
o_ONAME=out_valor :py:("o", "ONAME", out_val)syntaxes. The :py:out_valmust be a :ref:core I/O value <lang-iovalues>or a :ref:value-like <lang-valuelike>object that casts to a :ref:signal <lang-signals>, a concatenation of signals, or a slice of a signal. - An inout is specified using the :py:
io_IONAME=inout_valor :py:("io", "IONAME", inout_val)syntaxes. The :py:inout_valmust be a :ref:core I/O value <lang-iovalues>.
The two following examples use both syntaxes to add the same instance of type external as a submodule named processor:
.. testcode::
:hide:
i_data = Signal(8)
o_data = Signal(8)
io_pin = IOPort(1)
m = Module().. testcode::
m.submodules.processor = Instance("external",
p_width=8,
i_clk=ClockSignal(),
i_rst=ResetSignal(),
i_en=1,
i_mode=Const(3, unsigned(4)),
i_data_in=i_data,
o_data_out=o_data,
io_pin=io_pin,
).. testcode::
:hide:
m = Module().. testcode::
m.submodules.processor = Instance("external",
("p", "width", 8),
("i", "clk", ClockSignal()),
("i", "rst", ResetSignal()),
("i", "en", 1),
("i", "mode", Const(3, unsigned(4))),
("i", "data_in", i_data),
("o", "data_out", o_data),
("io", "pin", io_pin),
)Like a regular submodule, an instance can also be added without specifying a name:
.. testcode::
m.submodules += Instance("external",
# ...
).. tip::
If a name is not explicitly specified for a submodule, one will be generated and assigned automatically. Designs with many autogenerated names can be difficult to debug, so a name should usually be supplied.Although an :class:Instance is not an elaboratable, as a special case, it can be returned from the :py:elaborate() method. This is convenient for implementing an elaboratable that adorns an instance with an Amaranth interface:
.. testcode::
from amaranth import vendor
class FlipFlop(Elaboratable):
def __init__(self):
self.d = Signal()
self.q = Signal()
def elaborate(self, platform):
# Decide on the instance to use based on the platform we are elaborating for.
if isinstance(platform, vendor.LatticeICE40Platform):
return Instance("SB_DFF",
i_C=ClockSignal(),
i_D=self.d,
o_Q=self.q
)
else:
raise NotImplementedError.. _lang-iobufferinstance:
I/O buffer instances
.. note::
I/O buffer instances are a low-level primitive which is documented to ensure that the standard library does not rely on private interfaces in the core language. Most designers should use the :mod:`amaranth.lib.io` module instead.An I/O buffer instance is a submodule that allows connecting :ref:core I/O values <lang-iovalues> and regular :ref:values <lang-values> without the use of an external, toolchain- and technology-dependent :ref:instance <lang-instance>. It can be created in four configurations: input, output, tristatable output, and bidirectional (input/output).
.. testcode::
from amaranth.hdl import IOBufferInstance
m = Module()In the input configuration, the buffer instance combinationally drives a signal :py:i by the port:
.. testcode::
port = IOPort(4)
port_i = Signal(4)
m.submodules += IOBufferInstance(port, i=port_i)In the output configuration, the buffer instance combinationally drives the port by a value :py:o:
.. testcode::
port = IOPort(4)
port_o = Signal(4)
m.submodules += IOBufferInstance(port, o=port_o)In the tristatable output configuration, the buffer instance combinationally drives the port by a value :py:o if :py:oe is asserted, and does not drive (leaves in a high-impedance state, or tristates) the port otherwise:
.. testcode::
port = IOPort(4)
port_o = Signal(4)
port_oe = Signal()
m.submodules += IOBufferInstance(port, o=port_o, oe=port_oe)In the bidirectional (input/output) configuration, the buffer instance combinationally drives a signal :py:i by the port, combinationally drives the port by a value :py:o if :py:oe is asserted, and does not drive (leaves in a high-impedance state, or tristates) the port otherwise:
.. testcode::
port = IOPort(4)
port_i = Signal(4)
port_o = Signal(4)
port_oe = Signal()
m.submodules += IOBufferInstance(port, i=port_i, o=port_o, oe=port_oe)The width of the :py:i and :py:o values (when present) must be the same as the width of the port, and the width of the :py:oe value must be 1.
Index (`docs/index.rst`)
Language & toolchain
####################
.. warning::
This manual is a work in progress and is seriously incomplete!
.. toctree::
:maxdepth: 2
intro
install
start
tutorial
guide
reference
stdlib
simulator
platform
changes
contrib
Install (`docs/install.rst`)
Installation
############
.. _install-playground:
In-browser playground
You can try Amaranth out without installing anything by visiting the Amaranth Playground. The playground webpage contains a fully functional Python interpreter and an Amaranth toolchain that can simulate a design, display waveforms, and generate Verilog code. It works on all modern browsers that support WebAssembly_, including Firefox, Chrome, and Edge.
.. _Amaranth Playground: https://amaranth-lang.org/play/
.. _Pyodide: https://pyodide.org/en/stable/
.. _WebAssembly: https://webassembly.org/
.. _install-sysreqs:
System requirements
..
This version requirement needs to be synchronized with the one in pyproject.toml!
.. |yosys-version| replace:: 0.40 (or newer)
Amaranth HDL requires Python 3.9; it works on CPython_ 3.9 (or newer), and works faster on PyPy3.9_ 7.3.7 (or newer). Installation requires pip_ 23.0 (or newer).
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 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.
.. TODO: Link to FPGA family docs here
.. _CPython: https://www.python.org/
.. _PyPy3.9: https://www.pypy.org/
.. _pip: https://pip.pypa.io/en/stable/
.. _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/
.. _install-deps:
Installing prerequisites
.. |builtin-yosys-architectures| replace:: x86_64 and AArch64
.. |upgrade-pip| replace:: Before continuing, make sure you have the latest version of pip_ installed by running:
.. platform-picker::
.. platform-choice:: windows
:title: Windows
:ref:`Install Python `, either from Windows Store or using the full installer. If using the full installer, make sure to install a 64-bit version of Python.
|upgrade-pip|
.. code-block:: doscon
> pip install --upgrade pip .. platform-choice:: macos
:title: macOS
Install Homebrew_. Then, install Python by running:
.. code-block:: console
$ brew install python
.. _Homebrew: https://brew.sh
|upgrade-pip|
.. code-block:: console
$ pip install --upgrade pip .. platform-choice:: debian
:altname: linux
:title: Debian
Install Python by running:
.. code-block:: console
$ sudo apt-get install python3-pip
On architectures other than |builtin-yosys-architectures|, install Yosys by running:
.. code-block:: console
$ sudo apt-get install yosys
If Yosys |yosys-version| is not available, `build Yosys from source`_.
|upgrade-pip|
.. code-block:: console
$ pip3 install --user --upgrade pip .. platform-choice:: linux
:title: Other Linux
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.
If Yosys |yosys-version| is not available, `build Yosys from source`_.
.. _build Yosys from source: https://github.com/YosysHQ/yosys/#building-from-source
|upgrade-pip|
.. code-block:: console
$ pip3 install --user --upgrade pip.. _install:
Installing Amaranth
The latest release of Amaranth should work well for most applications. A development snapshot---any commit from the main branch of Amaranth---should be similarly reliable, but is likely to include experimental API changes that will be in flux until the next release. With that in mind, development snapshots can be used to try out new functionality or to avoid bugs fixed since the last release.
.. _install-release:
Latest release
.. |release:install| replace:: To install the latest release of Amaranth, run:
.. platform-picker::
.. platform-choice:: windows
:title: Windows
|release:install|
.. code-block:: doscon
> pip install --upgrade amaranth[builtin-yosys] .. platform-choice:: macos
:title: macOS
|release:install|
.. code-block:: console
$ pip install --user --upgrade 'amaranth[builtin-yosys]' .. platform-choice:: linux
:title: Linux
If you **did not** install Yosys manually in the :ref:`previous step `, to install the latest release of Amaranth, run:
.. code-block:: console
$ pip3 install --user --upgrade 'amaranth[builtin-yosys]'
If you **did** install Yosys manually in the previous step, run:
.. code-block:: console
$ pip3 install --user --upgrade amaranth.. _install-snapshot:
Development snapshot
.. |snapshot:install| replace:: To install the latest development snapshot of Amaranth, run:
.. platform-picker::
.. platform-choice:: windows
:title: Windows
|snapshot:install|
.. code-block:: doscon
> pip install "amaranth[builtin-yosys] @ git+https://github.com/amaranth-lang/amaranth.git" .. platform-choice:: macos
:title: macOS
|snapshot:install|
.. code-block:: console
$ pip install --user 'amaranth[builtin-yosys] @ git+https://github.com/amaranth-lang/amaranth.git' .. platform-choice:: linux
:title: Linux
If you **did not** install Yosys manually in the :ref:`previous step `, to install the latest release of Amaranth, run:
.. code-block:: console
$ pip3 install --user 'amaranth[builtin-yosys] @ git+https://github.com/amaranth-lang/amaranth.git'
If you **did** install Yosys manually in the previous step, run:
.. code-block:: console
$ pip3 install --user 'amaranth @ git+https://github.com/amaranth-lang/amaranth.git'.. _install-develop:
Editable development snapshot
.. |develop:first-time| replace:: To install an editable development snapshot of Amaranth for the first time, run:
.. |develop:update| replace:: Any changes made to the amaranth directory will immediately affect any code that uses Amaranth. To update the snapshot, run:
.. |develop:reinstall| replace:: any time package dependencies may have been added or changed (notably after updating the snapshot with git). Otherwise, code using Amaranth may crash because of a dependency version mismatch.
.. platform-picker::
.. platform-choice:: windows
:title: Windows
|develop:first-time|
.. code-block:: doscon
> git clone https://github.com/amaranth-lang/amaranth
> cd amaranth
> pip install --editable .[builtin-yosys]
|develop:update|
.. code-block:: doscon
> cd amaranth
> git pull --ff-only origin main
> pip install --editable .[builtin-yosys]
Run the ``pip install --editable .[builtin-yosys]`` command |develop:reinstall| .. platform-choice:: macos
:title: macOS
|develop:first-time|
.. code-block:: console
$ git clone https://github.com/amaranth-lang/amaranth
$ cd amaranth
$ pip install --user --editable '.[builtin-yosys]'
|develop:update|
.. code-block:: console
$ cd amaranth
$ git pull --ff-only origin main
$ pip install --user --editable '.[builtin-yosys]'
Run the ``pip install --editable .[builtin-yosys]`` command |develop:reinstall| .. platform-choice:: linux
:title: Linux
If you **did** install Yosys manually in the :ref:`previous step `, omit ``[builtin-yosys]`` from the following commands.
|develop:first-time|
.. code-block:: console
$ git clone https://github.com/amaranth-lang/amaranth
$ cd amaranth
$ pip3 install --user --editable '.[builtin-yosys]'
|develop:update|
.. code-block:: console
$ cd amaranth
$ git pull --ff-only origin main
$ pip3 install --user --editable '.[builtin-yosys]'
Run the ``pip3 install --editable .[builtin-yosys]`` command |develop:reinstall|Installing board definitions
.. todo::
Explain how to install `<https://github.com/amaranth-lang/amaranth-boards>`_.Intro (`docs/intro.rst`)
.. TODO: this introduction is written for people well familiar with HDLs; we likely need
another one for people who will use Amaranth as their first HDL
Introduction
############
The Amaranth project provides an open-source toolchain for developing hardware based on synchronous digital logic using the Python programming language. It aims to be easy to learn and use, reduce or eliminate common coding mistakes, and simplify the design of complex hardware with reusable components.
The Amaranth toolchain consists of the :ref:Amaranth language , the :ref:standard library , the :ref:simulator , and the :ref:build system , covering all steps of a typical FPGA development workflow. At the same time, it does not restrict the designer's choice of tools: existing industry-standard (System)Verilog or VHDL code can be integrated into an Amaranth-based design flow, or, conversely, Amaranth code can be integrated into an existing Verilog-based design flow.
.. TODO: add links to connect_rpc docs once they exist
.. _intro-lang:
The Amaranth language
The :doc:Amaranth hardware description language <guide> is a Python library for register transfer level modeling of synchronous logic. Ordinary Python code is used to construct a netlist of a digital circuit, which can be simulated, directly synthesized via Yosys_, or converted to human-readable Verilog code for use with industry-standard toolchains.
By relying on the flexibility, rich functionality and widespread adoption of the Python language, the Amaranth language is focused on a single task: modeling digital logic well. It has first-class support for building blocks like clock domains and finite state machines, and uses simple rules for arithmetic operations that closely match the Python semantics. Python classes, functions, loops and conditionals can be used to build organized and flexible designs; Python libraries can be seamlessly used with Amaranth during design or verification; and Python development tools can process Amaranth code.
A core design principle of the Amaranth language is to be not only easy to use, but also hard to accidentally misuse. Some HDLs provide functionality that has unexpected and undesirable behavior in synthesis, often with expensive consequences, and require a significant effort in learning a "safe" coding style and adopting third-party linting tools. Amaranth lacks non-synthesizable constructs and avoids error-prone inference in favor of explicit instantiation. It has many diagnostics (and regularly adds new ones) highlighting potential design issues. Most importantly, all usability issues are considered reportable bugs_.
.. _Yosys: https://yosyshq.net/yosys/
.. _reportable bugs: https://github.com/amaranth-lang/amaranth/issues
.. _intro-stdlib:
The Amaranth standard library
The Amaranth language comes with a standard library---a collection of essential digital design components and interfaces. It includes clock domain crossing primitives, synchronous and asynchronous FIFOs, a flexible I/O buffer interface, and more. By providing reliable building blocks out of the box, Amaranth allows the designer to focus on their application and avoids subtle differences in behavior between different designs.
.. TODO: link to stdlib here
Clock domain crossing often requires special treatment, such as using vendor-defined attributes or instantiating device-specific primitives. The CDC primitives in the Amaranth standard library can be overridden by the platform integration, and every platform integration included with Amaranth follows the vendor recommendations for CDC.
High-speed designs usually require the use of registered (and sometimes, geared) I/O buffers. The Amaranth standard library provides a common interface to be used between I/O buffers and peripheral implementations. The Amaranth build system, if used, can instantiate I/O buffers for every platform integration included with Amaranth.
While many designs will use at least some vendor-specific functionality, the components provided by the Amaranth standard library reduce the amount of code that needs to be changed when migrating between FPGA families, and the common interfaces simplify peripherals, test benches and simulations.
The Amaranth standard library is optional: the Amaranth language can be used without it. Conversely, it is possible to use the Amaranth standard library components in Verilog or VHDL code, with some limitations.
.. TODO: link to connect_rpc docs here again
.. _intro-sim:
The Amaranth simulator
The Amaranth project includes an advanced simulator for Amaranth code implemented in Python with no system dependencies; in this simulator, test benches are written as Python generator functions. Of course, it is always possible to convert an Amaranth design to Verilog for use with well-known tool like Icarus Verilog_ or Verilator_.
The Amaranth simulator is event-driven and can simulate designs with multiple clocks or asynchronous resets. Although it is slower than Icarus Verilog, it compiles the netlist to Python code ahead of time, achieving remarkably high performance for a pure Python implementation---especially when running on PyPy.
Although Amaranth does not support native code simulation or co-simulation at the moment, such support will be added in near future.
.. _Icarus Verilog: https://steveicarus.github.io/iverilog/
.. _Verilator: https://www.veripool.org/verilator/
.. _GTKWave: http://gtkwave.sourceforge.net/
.. _PyPy: https://www.pypy.org/
.. _intro-build:
The Amaranth build system
To achieve an end-to-end FPGA development workflow, the Amaranth project integrates with all major FPGA toolchains and provides definitions for many common development boards.
.. TODO: link to vendor docs and board docs here
FPGA toolchain integration
Each FPGA family requires the use of synthesis and place & route tools specific for that device family. The Amaranth build system directly integrates with every major open-source and commercial FPGA toolchain, and can be easily extended to cover others.
Through this integration, Amaranth can specialize the CDC primitives and I/O buffers for a particular device and toolchain; generate I/O and clock constraints from board definition files; synchronize the power-on reset in single-clock designs; include (System)Verilog and VHDL files in the design (if supported by the toolchain); and finally, generate a script running synthesis, placement, routing, and timing analysis. The generated code can be customized to insert additional options, commands, constraints, and so on.
The Amaranth build system produces self-contained, portable build trees that require only the toolchain to be present in the environment. This makes builds easier to reproduce, or to run on a remote machine. The generated build scripts are always provided for both *nix and Windows.
Development board definitions
Getting started with a new FPGA development board often requires going through a laborous and error-prone process of deriving toolchain configuration and constraint files from the supplied documentation. The Amaranth project includes a community-maintained repository of definitions for many open-source and commercial FPGA development boards.
These board definitions contain everything that is necessary to start using the board: FPGA family and model, clocks and resets, descriptions of on-board peripherals (including pin direction and attributes such as I/O standard), connector pinouts, and for boards with a built-in debug probe, the steps required to program the board. It takes a single Python invocation to generate, build, and download a test design that shows whether the board, toolchain, and programmer are working correctly.
Amaranth establishes a pin naming convention for many common peripherals (such as 7-segment displays, SPI flashes and SDRAM memories), enabling the reuse of unmodified interface code with many different boards. Further, the polarity of all control signals is unified to be active high, eliminating accidental polarity inversions and making simulation traces easier to follow; active low signals are inverted during I/O buffer instantiation.
Platform (`docs/platform.rst`)
.. _platform:
Platform integration
####################
.. todo::
Write this section.
.. toctree::
:maxdepth: 2
platform/altera
platform/gowin
platform/lattice
platform/quicklogic
platform/siliconblue
platform/xilinx
Reference (`docs/reference.rst`)
Language reference
##################
.. py:module:: amaranth.hdl
.. warning::
This reference is a work in progress and is seriously incomplete!
While the wording below states that anything not described in this document isn't covered by the backwards compatibility guarantee, this should be ignored until the document is complete and this warning is removed.This reference describes the Python classes that underlie the Amaranth language's syntax. It assumes familiarity with the :doc:language guide <guide>.
.. _lang-stability:
Backwards compatibility
As part of the Amaranth backwards compatibility guarantee, any behaviors described in this document will not change from a version to another without at least one version including a warning about the impending change. Any nontrivial change to these behaviors must also go through the public review as a part of the Amaranth Request for Comments process <https://amaranth-lang.org/rfcs/>_.
Conversely, any behavior not documented here is subject to change at any time with or without notice, and any names under the :mod:amaranth.hdl module that are not explicitly included in this document, even if they do not begin with an underscore, are internal to the implementation of the language.
.. _lang-importing:
Importing syntax
There are two ways to import the Amaranth syntax into a Python file: by importing the :ref:prelude <lang-prelude> or by importing individual names from the :mod:amaranth.hdl module. Since the prelude is kept small and rarely extended to avoid breaking downstream code that uses a glob import, there are some names that are only exported from the :mod:amaranth.hdl module. The following three snippets are equivalent:
.. testcode::
from amaranth import *
m = Module().. testcode::
import amaranth as am
m = am.Module().. testcode::
from amaranth.hdl import Module
m = Module()The prelude exports exactly the following names:
.. must be kept in sync with amaranth/init.py!
- :class:
Shape - :func:
unsigned - :func:
signed - :class:
Value - :class:
Const - :func:
C - :func:
Mux - :func:
Cat - :class:
Array - :class:
Signal - :class:
ClockSignal - :class:
ResetSignal - :class:
Format - :class:
Print - :func:
Assert - :class:
Module - :class:
ClockDomain - :class:
Elaboratable - :class:
Fragment - :class:
Instance - :class:
Memory - :class:
DomainRenamer - :class:
ResetInserter - :class:
EnableInserter
.. _lang-srcloc:
Source locations
Many functions and methods in Amaranth take the :py:src_loc_at=0 keyword argument. These language constructs may inspect the call stack to determine the file and line of its call site, which will be used to annotate generated code when a netlist is generated or to improve diagnostic messages.
Some call sites are not relevant for an Amaranth designer; e.g. when an Amaranth language construct is called from a user-defined utility function, the source location of the call site within this utility function is usually not interesting to the designer. In these cases, one or more levels of function calls can be removed from consideration using the :py:src_loc_at argument as follows (using :meth:Shape.cast to demonstrate the concept):
.. testcode::
def my_shape_cast(obj, *, src_loc_at=0):
... # additionally process `obj`...
return Shape.cast(obj, src_loc_at=1 + src_loc_at)The number :py:1 corresponds to the number of call stack frames that should be skipped.
Shapes
See also the introduction to :ref:shapes <lang-shapes> and :ref:casting from shape-like objects <lang-shapelike> in the language guide.
.. autoclass:: Shape
.. autofunction:: unsigned
.. autofunction:: signed
.. autoclass:: ShapeCastable()
.. autoclass:: ShapeLike()
Values
See also the introduction to :ref:values <lang-values> and :ref:casting from value-like objects <lang-valuelike> in the language guide.
.. autoclass:: Value
:special-members: bool, pos, neg, add, radd, sub, rsub, mul, rmul, mod, rmod, floordiv, rfloordiv, eq, ne, lt, le, gt, ge, abs, invert, and, rand, or, ror, xor, rxor, lshift, rlshift, rshift, rrshift, len, getitem, contains, hash
.. autoclass:: ValueCastable()
.. autoclass:: ValueLike()
Simulator (`docs/simulator.rst`)
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 mRunning 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, Period
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_clockmethod adds a stimulus: a process external to the DUT that manipulates its inputs (in this case, toggles the clock of thesyncdomain). - The :meth:
Simulator.run_untilmethod runs the simulation until a specific deadline is reached. - The :meth:
Simulator.write_vcdmethod 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(Period(MHz=1)) # 1 µs period, or 1 MHz
with sim.write_vcd("example1.vcd"):
sim.run_until(Period(MHz=1) * 15) # 15 periods of the clockThe 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"]}
]
}The :meth:Simulator.reset method reverts the simulation to its initial state. It can be used to speed up tests by capturing the waveforms only when the simulation is known to encounter an error:
.. testcode::
try:
sim.run()
except:
sim.reset()
with sim.write_vcd("example1_error.vcd"):
sim.run()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(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(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:
.. 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(Period(us=1))
ctx.set(dut.a, 2)
ctx.set(dut.b, 2)
assert ctx.get(dut.o) == 4
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(Period(us=2))
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:
awaitpoints) 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(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)):
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(Period(us=1))
ctx.set(a, 2)
ctx.set(b, 2)
assert ctx.get(o) == 4
await ctx.delay(Period(us=1))
ctx.set(a, 1717)
ctx.set(b, 420)
assert ctx.get(o) == 2137
await ctx.delay(Period(us=2))
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
Start (`docs/start.rst`)
Getting started
###############
This section demonstrates the basic Amaranth workflow to provide a cursory overview of the language and the toolchain. See the :doc:tutorial <tutorial> for a step-by-step introduction to the language, and the :doc:language guide <guide> for a detailed explanation of every language construct.
.. TODO: add link to build system doc
.. TODO: add link to more complex examples?
A counter
As a first example, consider a counter with a fixed limit, enable, and overflow. The code for this example is shown below. :download:Download <_code/up_counter.py> and run it:
.. code-block:: shell
$ python3 up_counter.py
Implementing a counter
A 16-bit up counter with enable input, overflow output, and a limit fixed at design time can be implemented in Amaranth as follows:
.. literalinclude:: _code/up_counter.py
:linenos:
:lineno-match:
:end-before: # --- TEST ---
The reusable building block of Amaranth designs is a Component: a Python class declares its interface (en and ovf, in this case) and implements the elaborate method that defines its behavior.
.. TODO: link to Elaboratable reference
Most elaborate implementations use a Module helper to describe combinational (m.d.comb) and synchronous (m.d.sync) logic controlled with conditional syntax (m.If, m.Elif, m.Else) similar to Python's. They can also instantiate vendor-defined black boxes or modules written in other HDLs.
.. TODO: link to DSL reference
Testing a counter
To verify its functionality, the counter can be simulated for a small amount of time, with a test bench driving it and checking a few simple conditions:
.. literalinclude:: _code/up_counter.py
:linenos:
:lineno-match:
:start-after: # --- TEST ---
:end-before: # --- CONVERT ---
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.
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:
.. wavedrom:: start/up_counter
{
"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
Although some Amaranth workflows do not include Verilog at all, it is still the de facto standard for HDL interoperability. Any Amaranth design can be converted to synthesizable Verilog using the corresponding backend:
.. literalinclude:: _code/up_counter.py
:linenos:
:lineno-match:
:start-after: # --- CONVERT ---
The signals that will be connected to the ports of the top-level Verilog module should be specified explicitly. The rising edge clock and synchronous reset signals of the sync domain are added automatically; if necessary, the control signals can be configured explicitly. The result is the following Verilog code (lightly edited for clarity):
.. TODO: link to clock domain section of language reference
.. literalinclude:: _code/up_counter.v
:language: verilog
:linenos:
To aid debugging, the generated Verilog code has the same general structure as the Amaranth source code (although more verbose), and contains extensive source location information.
.. note::
Unfortunately, at the moment none of the supported toolchains will use the source location information in diagnostic messages.
A blinking LED
Although Amaranth works well as a standalone HDL, it also includes a build system that integrates with FPGA toolchains, and many board definition files for common developer boards that include pinouts and programming adapter invocations. The following code will blink a LED with a frequency of 1 Hz on any board that has a LED and an oscillator:
.. literalinclude:: _code/led_blinker.py
:linenos:
:lineno-match:
:end-before: # --- BUILD ---
The LEDBlinker module will use the first LED available on the board, and derive the clock divisor from the oscillator frequency specified in the clock constraint. It can be used, for example, with the Lattice iCEStick evaluation board <https://www.latticesemi.com/icestick>_, one of the many boards already supported by Amaranth:
.. TODO: link to list of supported boards
.. todo::
Link to the installation instructions for the FOSS iCE40 toolchain, probably as a part of board documentation... literalinclude:: _code/led_blinker.py
:linenos:
:lineno-match:
:start-after: # --- BUILD ---
With only a single line of code, the design is synthesized, placed, routed, and programmed to the on-board Flash memory. Although not all applications will use the Amaranth build system, the designs that choose it can benefit from the "turnkey" built-in workflows; if necessary, the built-in workflows can be customized to include user-specified options, commands, and files.
.. TODO: link to build system reference
.. note::
The ability to check with minimal effort whether the entire toolchain functions correctly is so important that it is built into every board definition file. To use it with the iCEStick board, run:
.. code-block:: shell
$ python3 -m amaranth_boards.icestickThis command will build and program a test bitstream similar to the example above.
Stdlib (`docs/stdlib.rst`)
Standard library
################
The :mod:amaranth.lib module, also known as the standard library, provides modules that falls into one of the three categories:
- Modules that will used by essentially all idiomatic Amaranth code, or which are necessary for interoperability. This includes :mod:
amaranth.lib.enum(enumerations), :mod:amaranth.lib.data(data structures), :mod:amaranth.lib.wiring(interfaces and components), :mod:amaranth.lib.meta(interface metadata), and :mod:amaranth.lib.stream(data streams). - Modules that abstract common functionality whose implementation differs between hardware platforms. This includes :mod:
amaranth.lib.memoryand :mod:amaranth.lib.cdc. - Modules that have essentially one correct implementation and are of broad utility in digital designs. This includes :mod:
amaranth.lib.fifo, and :mod:amaranth.lib.crc.
As part of the Amaranth backwards compatibility guarantee, any behaviors described in these documents will not change from a version to another without at least one version including a warning about the impending change. Any nontrivial change to these behaviors must also go through the public review as a part of the Amaranth Request for Comments process <https://amaranth-lang.org/rfcs/>_.
The Amaranth standard library is separate from the Amaranth language: everything provided in it could have been implemented in a third-party library.
.. toctree::
:maxdepth: 2
stdlib/enum
stdlib/data
stdlib/wiring
stdlib/meta
stdlib/stream
stdlib/memory
stdlib/io
stdlib/cdc
stdlib/fifo
stdlib/crc
Tutorial (`docs/tutorial.rst`)
Tutorial
.. todo::
The official tutorial is still being written. Until it's ready, consider following one of the tutorials written by the Amaranth community:
* `Learning FPGA Design with nMigen <https://vivonomicon.com/2020/04/14/learning-fpga-design-with-nmigen/>`_ by Vivonomicon;
* `"I want to learn nMigen" <https://github.com/kbob/nmigen-examples>`_ by kbob;
* `A tutorial for using Amaranth HDL <https://github.com/robertbaruch/amaranth-tutorial>`_ by Robert Baruch.
* `Graded exercises for Amaranth HDL <https://github.com/robertbaruch/amaranth-exercises>`_ by Robert Baruch.
* `My journey with the Amaranth HDL <https://medium.com/@sporniket.studio/my-journey-with-the-amaranth-hdl-226b38d0b023>`_ by David Sporn, focussed on setting up the workstation, using formal verification and setting up continuous integration.
People with electrical engineering background may find the `DigitalJS <https://digitaljs.tilk.eu/>`_ simulator by Marek Materzok helpful, as it lets you explore the circuits generated from Amaranth code as circuit diagrams. It offers more features than `the official playground <https://amaranth-lang.org/play/>`_, such as interactive I/O.Platform Altera (`docs/platform/altera.rst`)
Altera
.. currentmodule:: amaranth.vendor
The :class:AlteraPlatform class provides a base platform to support Altera toolchains.
The Quartus and Mistral toolchains are supported.
.. autoclass:: AlteraPlatform
Platform Gowin (`docs/platform/gowin.rst`)
Gowin
.. currentmodule:: amaranth.vendor
The :class:GowinPlatform class provides a base platform to support Gowin toolchains.
The Apicula and Gowin toolchains are supported.
.. autoclass:: GowinPlatform
Platform Lattice (`docs/platform/lattice.rst`)
Lattice
#######
.. currentmodule:: amaranth.vendor
The :class:LatticePlatform class provides a base platform to support Lattice toolchains (not including iCE40 devices, which are supported by :class:SiliconBluePlatform). Currently supported devices include ECP5, MachXO2, MachXO3L, and Nexus.
The Trellis and Diamond toolchains are supported.
.. autoclass:: LatticePlatform
Platform Quicklogic (`docs/platform/quicklogic.rst`)
Quicklogic
##########
.. currentmodule:: amaranth.vendor
The :class:QuicklogicPlatform class provides a base platform to support Quicklogic toolchains.
The Symbiflow toolchain is supported.
.. autoclass:: QuicklogicPlatform
Platform Siliconblue (`docs/platform/siliconblue.rst`)
SiliconBlue
###########
.. currentmodule:: amaranth.vendor
The :class:SiliconBluePlatform class provides a base platform to support Lattice (earlier SiliconBlue) iCE40 devices.
The IceStorm and iCECube2 toolchains are supported.
.. autoclass:: SiliconBluePlatform
Platform Xilinx (`docs/platform/xilinx.rst`)
Xilinx
.. currentmodule:: amaranth.vendor
The :class:XilinxPlatform class provides a base platform to support Xilinx toolchains.
The ISE, Vivado, and Symbiflow toolchains are supported.
.. autoclass:: XilinxPlatform
Stdlib Cdc (`docs/stdlib/cdc.rst`)
Clock domain crossing
#####################
.. py:module:: amaranth.lib.cdc
The :mod:amaranth.lib.cdc module provides building blocks for transferring data between clock domains.
.. autoclass:: FFSynchronizer()
.. autoclass:: AsyncFFSynchronizer()
.. autoclass:: ResetSynchronizer()
.. autoclass:: PulseSynchronizer()
Stdlib Crc (`docs/stdlib/crc.rst`)
Cyclic redundancy checks
########################
.. py:module:: amaranth.lib.crc
The :mod:amaranth.lib.crc module provides facilities for computing cyclic redundancy checks (CRCs)
in software and in hardware.
Introduction
The essentials of a CRC computation are specified with an :class:Algorithm object, which defines
CRC width, polynomial, initial value, input/output reflection, and output XOR. Many commonly used
CRC algorithms are available in the :py:mod:~amaranth.lib.crc.catalog module, while most other
CRC designs can be accommodated by manually constructing an :class:Algorithm.
An :class:Algorithm is specialized for a particular data width to obtain :class:Parameters,
which fully define a CRC computation. :meth:Parameters.compute computes a CRC in software, while
:meth:Parameters.create creates a :class:Processor that computes a CRC in hardware.
Examples
.. testsetup::
from amaranth import *
m = Module().. testcode::
from amaranth.lib.crc import Algorithm
from amaranth.lib.crc.catalog import CRC16_CCITT, CRC16_USB
# Compute a CRC in hardware using the predefined CRC16-CCITT algorithm and a data word
# width of 8 bits (in other words, computing it over bytes).
m.submodules.crc16_ccitt = crc16_ccitt = CRC16_CCITT().create()
# Compute a CRC in hardware using the predefined CRC16-USB algorithm and a data word
# width of 32 bits.
m.submodules.crc16_usb = crc16_usb = CRC16_USB(32).create()
# Compute a CRC in software using a custom CRC algorithm and explicitly specified data word
# width.
algo = Algorithm(crc_width=16, polynomial=0x1021, initial_crc=0xffff,
reflect_input=False, reflect_output=False, xor_output=0x0000)
assert algo(data_width=8).compute(b"123456789") == 0x29b1Algorithms and parameters
.. autoclass:: Algorithm
:special-members: call
.. autoclass:: Parameters
CRC computation
.. autoclass:: Processor()
Predefined algorithms
The following predefined CRC algorithms are available:
.. toctree::
crc/catalog
Stdlib Data (`docs/stdlib/data.rst`)
Data structures
###############
.. py:module:: amaranth.lib.data
The :mod:amaranth.lib.data module provides a way to describe the bitwise layout of values and a proxy class for accessing fields of values using the attribute access and indexing syntax.
Introduction
Overview
++++++++
This module provides four related facilities:
- Low-level bitwise layout description via :class:
Fieldand :class:Layout. These classes are rarely used directly, but are the foundation on which all other functionality is built. They are also useful for introspection. - High-level bitwise layout description via :class:
StructLayout, :class:UnionLayout, :class:ArrayLayout, and :class:FlexibleLayout. These classes are the ones most often used directly, in particular :class:StructLayoutand :class:ArrayLayout. - Data views via :class:
Viewor its user-defined subclasses. This class is used to apply a layout description to a plain :class:Value, enabling structured access to its bits. - Data classes :class:
Structand :class:Union. These classes are data views with a layout that is defined using Python :term:variable annotations(also known as type annotations).
To use this module, add the following imports to the beginning of the file:
.. testcode::
from amaranth.lib import data
Motivation
++++++++++
The fundamental Amaranth type is a :class:Value: a sequence of bits that can also be used as a number. Manipulating values directly is sufficient for simple applications, but in more complex ones, values are often more than just a sequence of bits; they have well-defined internal structure.
.. testsetup::
from amaranth import *
m = Module()For example, consider a module that processes pixels, converting them from RGB to grayscale. The color pixel format is RGB565:
.. wavedrom:: data/rgb565_layout
{
"reg": [
{"name": ".red", "bits": 5, "type": 2},
{"name": ".green", "bits": 6, "type": 3},
{"name": ".blue", "bits": 5, "type": 4}
],
"config": {
"lanes": 1,
"compact": true,
"vflip": true,
"hspace": 650
}
}This module could be implemented (using a fast but very approximate method) as follows:
.. testcode::
i_color = Signal(16)
o_gray = Signal(8)
m.d.comb += o_gray.eq((i_color[0:5] + i_color[5:11] + i_color[11:16]) << 1)While this implementation works, it is repetitive, error-prone, hard to read, and laborous to change; all because the color components are referenced using bit offsets. To improve it, the structure can be described with a :class:Layout so that the components can be referenced by name:
.. testcode::
from amaranth.lib import data, enum
rgb565_layout = data.StructLayout({
"red": 5,
"green": 6,
"blue": 5
})
i_color = Signal(rgb565_layout)
o_gray = Signal(8)
m.d.comb += o_gray.eq((i_color.red + i_color.green + i_color.blue) << 1)The :class:View is :ref:value-like <lang-valuelike> and can be used anywhere a plain value can be used. For example, it can be assigned to in the usual way:
.. testcode::
m.d.comb += i_color.eq(0) # everything is blackComposing layouts
+++++++++++++++++
Layouts are composable: a :class:Layout is a :ref:shape <lang-shapes> and can be used as a part of another layout. In this case, an attribute access through a view returns a view as well.
For example, consider a module that processes RGB pixels in groups of up to four at a time, provided by another module, and accumulates their average intensity:
.. testcode::
input_layout = data.StructLayout({
"pixels": data.ArrayLayout(rgb565_layout, 4),
"valid": 4
})
i_stream = Signal(input_layout)
r_accum = Signal(32)
m.d.sync += r_accum.eq(
r_accum + sum((i_stream.pixels[n].red +
i_stream.pixels[n].green +
i_stream.pixels[n].blue)
* i_stream.valid[n]
for n in range(len(i_stream.valid))))Note how the width of :py:i_stream is never defined explicitly; it is instead inferred from the shapes of its fields.
In the previous section, the precise bitwise layout was important, since RGB565 is an interchange format. In this section however the exact bit positions do not matter, since the layout is only used internally to communicate between two modules in the same design. It is sufficient that both of them use the same layout.
Defining layouts
++++++++++++++++
Data layouts can be defined in a few different ways depending on the use case.
In case the data format is defined using a family of layouts instead of a single specific one, a function can be used:
.. testcode::
def rgb_layout(r_bits, g_bits, b_bits):
return data.StructLayout({
"red": unsigned(r_bits),
"green": unsigned(g_bits),
"blue": unsigned(b_bits)
})
rgb565_layout = rgb_layout(5, 6, 5)
rgb24_layout = rgb_layout(8, 8, 8)In case the data has related operations or transformations, :class:View can be subclassed to define methods implementing them:
.. testcode::
class RGBLayout(data.StructLayout):
def __init__(self, r_bits, g_bits, b_bits):
super().__init__({
"red": unsigned(r_bits),
"green": unsigned(g_bits),
"blue": unsigned(b_bits)
})
def __call__(self, value):
return RGBView(self, value)
class RGBView(data.View):
def brightness(self):
return (self.red + self.green + self.blue)[-8:]Here, an instance of the :py:RGBLayout class itself is :ref:shape-like <lang-shapelike> and can be used anywhere a shape is accepted. When a :class:Signal is constructed with this layout, the returned value is wrapped in an :py:RGBView:
.. doctest::
pixel = Signal(RGBLayout(5, 6, 5))
len(pixel.as_value())
16
pixel.red
(slice (sig pixel) 0:5)
In case the data format is static, :class:Struct (or :class:Union) can be subclassed instead of :class:View, to reduce the amount of boilerplate needed:
.. testcode::
class IEEE754Single(data.Struct):
fraction: 23
exponent: 8 = 0x7f
sign: 1
def is_subnormal(self):
return self.exponent == 0Discriminated unions
++++++++++++++++++++
This module provides a :class:UnionLayout, which is rarely needed by itself, but is very useful in combination with a discriminant: a enumeration indicating which field of the union contains valid data.
For example, consider a module that can direct another module to perform one of a few operations, each of which requires its own parameters. The two modules could communicate through a channel with a layout like this:
.. testcode::
class Command(data.Struct):
class Kind(enum.Enum):
SET_ADDR = 0
SEND_DATA = 1
valid : 1
kind : Kind
params : data.UnionLayout({
"set_addr": data.StructLayout({
"addr": unsigned(32)
}),
"send_data": data.StructLayout({
"byte": unsigned(8)
})
})Here, the shape of the :py:Command is inferred, being large enough to accommodate the biggest of all defined parameter structures, and it is not necessary to manage it manually.
One module could submit a command with:
.. testcode::
cmd = Signal(Command)
m.d.comb += [
cmd.valid.eq(1),
cmd.kind.eq(Command.Kind.SET_ADDR),
cmd.params.set_addr.addr.eq(0x00001234)
]The other would react to commands as follows:
.. testcode::
addr = Signal(32)
with m.If(cmd.valid):
with m.Switch(cmd.kind):
with m.Case(Command.Kind.SET_ADDR):
m.d.sync += addr.eq(cmd.params.set_addr.addr)
with m.Case(Command.Kind.SEND_DATA):
...Modeling structured data
.. autoclass:: Field
.. autoclass:: Layout()
Common data layouts
.. autoclass:: StructLayout
.. autoclass:: UnionLayout
.. autoclass:: ArrayLayout
.. autoclass:: FlexibleLayout
Data views
.. autoclass:: View
.. autoclass:: Const
Data classes
.. autoclass:: Struct
.. autoclass:: Union
Stdlib Enum (`docs/stdlib/enum.rst`)
Enumerations
############
.. py:module:: amaranth.lib.enum
The :mod:amaranth.lib.enum module is a drop-in replacement for the standard :mod:enum module that provides extended :class:Enum, :class:IntEnum, :class:Flag, and :class:IntFlag classes with the ability to specify a shape explicitly.
A shape can be specified for an enumeration with the shape= keyword argument:
.. testsetup::
from amaranth import *
.. testcode::
from amaranth.lib import enum
class Funct(enum.Enum, shape=4):
ADD = 0
SUB = 1
MUL = 2
.. doctest::
Shape.cast(Funct)
unsigned(4)
Value.cast(Funct.ADD)
(const 4'd0)
Any :ref:constant-castable <lang-constcasting> expression can be used as the value of a member:
.. testcode::
class Op(enum.Enum, shape=1):
REG = 0
IMM = 1
class Instr(enum.Enum, shape=5):
ADD = Cat(Funct.ADD, Op.REG)
ADDI = Cat(Funct.ADD, Op.IMM)
SUB = Cat(Funct.SUB, Op.REG)
SUBI = Cat(Funct.SUB, Op.IMM)
...
.. doctest::
Instr.SUBI
The shape= argument is optional. If not specified, classes from this module behave exactly the same as classes from the standard :mod:enum module, and likewise, this module re-exports everything exported by the standard :mod:enum module.
.. testcode::
import amaranth.lib.enum
class NormalEnum(amaranth.lib.enum.Enum):
SPAM = 0
HAM = 1
In this way, this module is a drop-in replacement for the standard :mod:enum module, and in an Amaranth project, all import enum statements may be replaced with from amaranth.lib import enum.
Signals with :class:Enum or :class:Flag based shape are automatically wrapped in the :class:EnumView or :class:FlagView value-like wrappers, which ensure type safety. Any :ref:value-like <lang-valuelike> can also be explicitly wrapped in a view class by casting it to the enum type:
.. doctest::
a = Signal(Funct)
b = Signal(Op)
type(a)
<class 'amaranth.lib.enum.EnumView'>
a == b
Traceback (most recent call last):
File "", line 1, in
TypeError: an EnumView can only be compared to value or other EnumView of the same enum type
c = Signal(4)
type(Funct(c))
<class 'amaranth.lib.enum.EnumView'>
Like the standard Python :class:enum.IntEnum and :class:enum.IntFlag classes, the Amaranth :class:IntEnum and :class:IntFlag classes are loosely typed and will not be subject to wrapping in view classes:
.. testcode::
class TransparentEnum(enum.IntEnum, shape=unsigned(4)):
FOO = 0
BAR = 1
.. doctest::
a = Signal(TransparentEnum)
type(a) is Signal
True
It is also possible to define a custom view class for a given enum:
.. testcode::
class InstrView(enum.EnumView):
def has_immediate(self):
return (self == Instr.ADDI) | (self == Instr.SUBI)
class Instr(enum.Enum, shape=5, view_class=InstrView):
ADD = Cat(Funct.ADD, Op.REG)
ADDI = Cat(Funct.ADD, Op.IMM)
SUB = Cat(Funct.SUB, Op.REG)
SUBI = Cat(Funct.SUB, Op.IMM)
.. doctest::
a = Signal(Instr)
type(a)
<class 'InstrView'>
a.has_immediate()
(| (== (sig a) (const 5'd16)) (== (sig a) (const 5'd17)))
Metaclass
.. autoclass:: EnumType()
Base classes
.. autoclass:: Enum()
.. autoclass:: IntEnum()
.. autoclass:: Flag()
.. autoclass:: IntFlag()
View classes
.. autoclass:: EnumView()
.. autoclass:: FlagView()
Stdlib Fifo (`docs/stdlib/fifo.rst`)
First-in first-out queues
#########################
.. py:module:: amaranth.lib.fifo
The amaranth.lib.fifo module provides building blocks for first-in, first-out queues.
.. autoclass:: FIFOInterface
.. note::
The :class:`FIFOInterface` class can be used directly to substitute a FIFO in tests, or inherited from in a custom FIFO implementation... autoclass:: SyncFIFO(, width, depth)
.. autoclass:: SyncFIFOBuffered(, width, depth)
.. autoclass:: AsyncFIFO(, width, depth, r_domain="read", w_domain="write", exact_depth=False)
.. autoclass:: AsyncFIFOBuffered(, width, depth, r_domain="read", w_domain="write", exact_depth=False)
Stdlib Io (`docs/stdlib/io.rst`)
Input/output buffers
.. py:module:: amaranth.lib.io
The :mod:amaranth.lib.io module provides a platform-independent way to instantiate platform-specific input/output buffers: combinational, synchronous, and double data rate (DDR).
Introduction
The Amaranth language provides :ref:core I/O values <lang-iovalues> that designate connections to external devices, and :ref:I/O buffer instances <lang-iobufferinstance> that implement platform-independent combinational I/O buffers. This low-level mechanism is foundational to all I/O in Amaranth and must be used whenever a device-specific platform is unavailable, but is limited in its capabilities. The :mod:amaranth.lib.io module builds on top of it to provide library I/O ports that specialize and annotate I/O values, and buffer components that connect ports to logic.
.. note::
Unfortunately, the terminology related to I/O has several ambiguities:
* A "port" could refer to an *interface port* (:class:`.Signal` objects created by the :mod:`amaranth.lib.wiring` module), a *core I/O port* (:class:`amaranth.hdl.IOPort` object), or a *library I/O port* (:class:`amaranth.lib.io.PortLike` object).
* A "I/O buffer" could refer to an *I/O buffer instance* (:class:`amaranth.hdl.IOBufferInstance`) or a *I/O buffer component* (:class:`amaranth.lib.io.Buffer`, :class:`.FFBuffer`, or :class:`.DDRBuffer` objects).
Amaranth documentation always uses the least ambiguous form of these terms.Examples
.. testsetup::
from amaranth import *
class MockPlatform:
def request(self, name, *, dir):
from amaranth.hdl import IOPort
from amaranth.lib import io
if name == "led":
return io.SingleEndedPort(IOPort(1, name=name), direction="o")
if name == "clk24":
return io.SingleEndedPort(IOPort(1, name=name), direction="i")
if name == "d":
return io.SingleEndedPort(IOPort(8, name=name), direction="io")
if name == "re":
return io.SingleEndedPort(IOPort(1, name=name), direction="i")
if name == "we":
return io.SingleEndedPort(IOPort(1, name=name), direction="i")
if name == "dclk":
return io.SingleEndedPort(IOPort(1, name=name), direction="o")
if name == "dout":
return io.SingleEndedPort(IOPort(8, name=name), direction="o")
raise NameError
def get_io_buffer(self, buffer):
return Fragment()
def build(self, top):
from amaranth.back import rtlil
return rtlil.convert(Fragment.get(top, self), ports=[])All of the following examples assume that one of the built-in FPGA platforms is used.
.. testcode::
from amaranth.sim import Simulator, Period
from amaranth.lib import io, wiring, stream
from amaranth.lib.wiring import In, OutLED output
++++++++++
In this example, a library I/O port for a LED is requested from the platform and driven to blink the LED:
.. testcode::
class Toplevel(Elaboratable):
def elaborate(self, platform):
m = Module()
delay = Signal(24)
state = Signal()
with m.If(delay == 0):
m.d.sync += delay.eq(~0)
m.d.sync += state.eq(~state)
with m.Else():
m.d.sync += delay.eq(delay - 1)
m.submodules.led = led = io.Buffer("o", platform.request("led", dir="-"))
m.d.comb += led.o.eq(state)
return m.. testcode::
:hide:
MockPlatform().build(Toplevel())Clock input
+++++++++++
In this example, a clock domain is created and driven from an external clock source:
.. testcode::
class Toplevel(Elaboratable):
def elaborate(self, platform):
m = Module()
m.domains.sync = cd_sync = ClockDomain()
m.submodules.clk24 = clk24 = io.Buffer("i", platform.request("clk24", dir="-"))
m.d.comb += cd_sync.clk.eq(clk24.i)
...
return m.. testcode::
:hide:
MockPlatform().build(Toplevel())Bidirectional bus
+++++++++++++++++
This example implements a peripheral for a clocked parallel bus. This peripheral can store and recall one byte of data. The data is stored with a write enable pulse, and recalled with a read enable pulse:
.. testcode::
class Toplevel(Elaboratable):
def elaborate(self, platform):
m = Module()
m.submodules.bus_d = bus_d = io.FFBuffer("io", platform.request("d", dir="-"))
m.submodules.bus_re = bus_re = io.Buffer("i", platform.request("re", dir="-"))
m.submodules.bus_we = bus_we = io.Buffer("i", platform.request("we", dir="-"))
data = Signal.like(bus_d.i)
with m.If(bus_re.i):
m.d.comb += bus_d.oe.eq(1)
m.d.comb += bus_d.o.eq(data)
with m.Elif(bus_we.i):
m.d.sync += data.eq(bus_d.i)
return m.. testcode::
:hide:
MockPlatform().build(Toplevel())This bus requires a turn-around time of at least 1 cycle to avoid electrical contention.
Note that data appears on the bus one cycle after the read enable input is asserted, and that the write enable input stores the data present on the bus in the previous cycle. This is called pipelining and is typical for clocked buses; see :class:.FFBuffer for a waveform diagram. Although it increases the maximum clock frequency at which the bus can run, it also makes the bus signaling more complicated.
Clock forwarding
++++++++++++++++
In this example of a source-synchronous interface <https://en.wikipedia.org/wiki/Source-synchronous>__, a clock signal is generated with the same phase as the DDR data signals associated with it:
.. testcode::
class SourceSynchronousOutput(wiring.Component):
dout: In(16)
def elaborate(self, platform):
m = Module()
m.submodules.bus_dclk = bus_dclk = \
io.DDRBuffer("o", platform.request("dclk", dir="-"))
m.d.comb += [
bus_dclk.o[0].eq(1),
bus_dclk.o[1].eq(0),
]
m.submodules.bus_dout = bus_dout = \
io.DDRBuffer("o", platform.request("dout", dir="-"))
m.d.comb += [
bus_dout.o[0].eq(self.dout[:8]),
bus_dout.o[1].eq(self.dout[8:]),
]
return m.. testcode::
:hide:
MockPlatform().build(SourceSynchronousOutput())This component transmits :py:dout on each cycle as two halves: the low 8 bits on the rising edge of the data clock, and the high 8 bits on the falling edge of the data clock. The transmission is edge-aligned, meaning that the data edges exactly coincide with the clock edges.
Simulation
The Amaranth simulator, :mod:amaranth.sim, cannot simulate :ref:core I/O values <lang-iovalues> or :ref:I/O buffer instances <lang-iobufferinstance> as it only operates on unidirectionally driven two-state wires. This module provides a simulation-only library I/O port, :class:SimulationPort, so that components that use library I/O buffers can be tested.
A component that is designed for testing should accept the library I/O ports it will drive as constructor parameters rather than requesting them from the platform directly. Synthesizable designs will instantiate the component with a :class:SingleEndedPort, :class:DifferentialPort, or a platform-specific library I/O port, while tests will instantiate the component with a :class:SimulationPort. Tests are able to inject inputs into the component using :py:sim_port.i, capture the outputs of the component via :py:sim_port.o, and ensure that the component is driving the outputs at the appropriate times using :py:sim_port.oe.
For example, consider a simple serializer that accepts a stream of multi-bit data words and outputs them bit by bit. It can be tested as follows:
.. testcode::
class OutputSerializer(wiring.Component):
data: In(stream.Signature(8))
def __init__(self, dclk_port, dout_port):
self.dclk_port = dclk_port
self.dout_port = dout_port
super().__init__()
def elaborate(self, platform):
m = Module()
m.submodules.dclk = dclk = io.Buffer("o", self.dclk_port)
m.submodules.dout = dout = io.Buffer("o", self.dout_port)
index = Signal(range(8))
m.d.comb += dout.o.eq(self.data.payload.bit_select(index, 1))
with m.If(self.data.valid):
m.d.sync += dclk.o.eq(~dclk.o)
with m.If(dclk.o):
m.d.sync += index.eq(index + 1)
with m.If(index == 7):
m.d.comb += self.data.ready.eq(1)
return m
def test_output_serializer():
dclk_port = io.SimulationPort("o", 1)
dout_port = io.SimulationPort("o", 1)
dut = OutputSerializer(dclk_port, dout_port)
async def testbench_write_data(ctx):
ctx.set(dut.data.payload, 0xA1)
ctx.set(dut.data.valid, 1)
await ctx.tick().until(dut.data.ready)
ctx.set(dut.data.valid, 0)
async def testbench_sample_output(ctx):
for bit in [1,0,0,0,0,1,0,1]:
_, dout_value = await ctx.posedge(dut.dclk_port.o).sample(dut.dout_port.o)
assert ctx.get(dut.dout_port.oe) == 1, "DUT is not driving the data output"
assert dout_value == bit, "DUT drives the wrong value on data output"
sim = Simulator(dut)
sim.add_clock(Period(MHz=1))
sim.add_testbench(testbench_write_data)
sim.add_testbench(testbench_sample_output)
sim.run().. testcode::
:hide:
test_output_serializer()Ports
.. autoclass:: Direction()
.. autoclass:: PortLike
.. autoclass:: SingleEndedPort
.. autoclass:: DifferentialPort
.. autoclass:: SimulationPort
Buffers
.. autoclass:: Buffer(direction, port)
.. autoclass:: FFBuffer(direction, port, *, i_domain=None, o_domain=None)
.. autoclass:: DDRBuffer(direction, port, *, i_domain=None, o_domain=None)
Stdlib Memory (`docs/stdlib/memory.rst`)
Memory arrays
.. py:module:: amaranth.lib.memory
The :mod:amaranth.lib.memory module provides a way to efficiently store data organized as an array of identically shaped rows, which may be addressed (read and/or written) one at a time. This organization is well suited for an efficient implementation in hardware.
Introduction
A memory :ref:component <wiring> is accessed through read and write memory ports, which are :ref:interface objects <wiring> with address, data, and enable ports. The address input selects the memory row to be accessed, the enable input determines whether an access will be made, and the data output (for read ports) or data input (for write ports) transfers data between the memory row and the rest of the design. Read ports can be synchronous (where the memory row access is triggered by the :ref:active edge <lang-sync> of a clock) or asynchronous (where the memory row is accessed continuously); write ports are always synchronous.
.. note::
Unfortunately, the terminology related to memories has an ambiguity: a "port" could refer to either an *interface port* (:class:`.Signal` objects created by the :mod:`amaranth.lib.wiring` module) or to a *memory port* (:class:`ReadPort` or :class:`WritePort` object created by :class:`amaranth.lib.memory.Memory`).
Amaranth documentation always uses the term "memory port" when referring to the latter.To use a memory, first create a :class:Memory object, providing a shape, depth (the number of rows), and initial contents. Then, request as many memory ports as the number of concurrent accesses you need to perform by using the :meth:Memory.read_port and :meth:Memory.write_port methods.
.. warning::
While :class:`Memory` will provide virtually any memory configuration on request and all will simulate correctly, only a subset of configurations can implemented in hardware efficiently or `at all`. Exactly what any given hardware platform supports is specific to both the device and the toolchain used.
However, the following two configurations are well-supported on most platforms:
1. Zero or one write ports and any amount of read ports. Almost all devices include one or two read ports in a hardware memory block, but the toolchain will replicate memory blocks as needed to meet the requested amount of read ports, using more resources.
2. Two write ports and any amount of read ports whose address input always matches that of the either write port. Most devices include two combined read/write ports in a hardware memory block (known as "true dual-port", or "TDP", block RAM), and the toolchain will replicate memory blocks to meet the requested amount of read ports. However, some devices include one read-only and one write-only port in a hardware memory block (known as "simple dual-port", or "SDP", block RAM), making this configuration unavailable. Also, the combined (synchronous) read/write port of a TDP block RAM usually does not have independent read enable and write enable inputs; in this configuration, the read enable input should usually be left in the (default if not driven) asserted state.
Most devices include hardware primitives that can efficiently implement memories with asynchronous read ports (known as "LUT RAM", "distributed RAM", or "DRAM"; not to be confused with "dynamic RAM", also abbreviated as "DRAM"). On devices without these hardware primitives, memories with asynchronous read ports are implemented using logic resources, which are consumed at an extremely high rate. Synchronous read ports should be always preferred over asynchronous ones.
Additionally, some memory configurations (that seem like they should be supported by the device and the toolchain) may fail to be recognized, or may use much more resources than they should. This can happen due to device and/or toolchain errata (including defects in Amaranth). Unfortunately, such issues can only be handled on a case-by-case basis; in general, simpler memory configurations are better and more widely supported.Examples
.. testsetup::
from amaranth import *First, import the :class:Memory class.
.. testcode::
from amaranth.lib.memory import MemoryRead-only memory
++++++++++++++++
In the following example, a read-only memory is used to output a fixed message in a loop:
.. testcode::
:hide:
m = Module().. testcode::
message = b"Hello world\n"
m.submodules.memory = memory = \
Memory(shape=unsigned(8), depth=len(message), init=message)
rd_port = memory.read_port(domain="comb")
with m.If(rd_port.addr == memory.depth - 1):
m.d.sync += rd_port.addr.eq(0)
with m.Else():
m.d.sync += rd_port.addr.eq(rd_port.addr + 1)
character = Signal(8)
m.d.comb += character.eq(rd_port.data)In this example, the memory read port is asynchronous, and a change of the address input (labelled a on the diagram below) results in an immediate change of the data output (labelled d).
.. wavedrom:: memory/example_hello
{
"signal": [
{"name": "clk",
"wave": "0P............"},
{"name": "rd_port.addr",
"wave": "==============",
"data": [0,1,2,3,4,5,6,7,8,9,10,11,0,1],
"node": ".a"},
{"name": "rd_port.data",
"wave": "==============",
"data": ["H","e","l","l","o"," ","w","o","r","l","d","\\n","H","e"],
"node": ".d"}
],
"edge": [
"a-|d"
]
}First-in, first-out queue
+++++++++++++++++++++++++
In a more complex example, a power-of-2 sized writable memory is used to implement a first-in, first-out queue:
.. testcode::
:hide:
m = Module().. testcode::
push = Signal()
pop = Signal()
m.submodules.memory = memory = \
Memory(shape=unsigned(8), depth=16, init=[])
wr_port = memory.write_port()
m.d.comb += wr_port.en.eq(push)
with m.If(push):
m.d.sync += wr_port.addr.eq(wr_port.addr + 1)
rd_port = memory.read_port(transparent_for=(wr_port,))
m.d.comb += rd_port.en.eq(pop)
with m.If(pop):
m.d.sync += rd_port.addr.eq(rd_port.addr + 1)
# Data can be shifted in via `wr_port.data` and out via `rd_port.data`.
# This example assumes that empty queue will be never popped from.In this example, the memory read and write ports are synchronous. A write operation (labelled x, w) updates the addressed row 0 on the next clock cycle, and a read operation (labelled y, r) retrieves the contents of the same addressed row 0 on the next clock cycle as well.
However, the memory read port is also configured to be transparent relative to the memory write port. This means that if a write and a read operation (labelled t, u respectively) access the same row with address 3, the new contents will be read out, reducing the minimum push-to-pop latency to one cycle, down from two cycles that would be required without the use of transparency.
.. wavedrom:: memory/example_fifo
{
"signal": [
{"name": "clk",
"wave": "P........"},
{"name": "push",
"wave": "01..0.10.",
"node": ".x"},
{"name": "wr_port.addr",
"wave": "=.===..=.",
"data": ["0", "1", "2", "3", "4", "5"]},
{"name": "wr_port.data",
"wave": "====..=..",
"data": ["00", "AA", "BB", "CC", "DD"],
"node": ".w....t"},
{"name": "memory[0]",
"wave": "=.=......",
"data": ["00", "AA"],
"node": "..G"},
{"name": "memory[3]",
"wave": "=......=.",
"data": ["00", "DD"],
"node": ".......H"},
{"name": "pop",
"wave": "0..1...0.",
"node": "...y"},
{"name": "rd_port.addr",
"wave": "=...====.",
"data": ["0", "1", "2", "3", "4", "5"]},
{"name": "rd_port.data",
"wave": "=...====.",
"data": ["00", "AA", "BB", "CC", "DD"],
"node": "....r..u"}
], "edge": [
"x-~>G", "w->G", "y-~>r", "t->H", "t->u"
]
}Simulation
++++++++++
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.
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:
..
: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
..
attributes are not documented because they can be easily used to break soundness and we don't
document them for signals either; they are rarely necessary for interoperability
..
the following two directives document a pair of overloads of :class:Memory; this is a little
weird and not really how rst/sphinx are supposed to work but it results in a comprehensible
generated document. be careful to not break this!
.. class:: Memory(data, *, src_loc_at=0)
.. autoclass:: Memory(*, shape, depth, init, src_loc_at=0)
:noindex:
:no-members:
.. automethod:: read_port
.. automethod:: write_port
.. autoproperty:: read_ports
.. autoproperty:: write_portsMemory ports
.. autoclass:: ReadPort(...)
.. autoclass:: WritePort(...)
Stdlib Meta (`docs/stdlib/meta.rst`)
.. _meta:
Interface metadata
##################
.. py:module:: amaranth.lib.meta
The :mod:amaranth.lib.meta module provides a way to annotate objects in an Amaranth design and exchange these annotations with external tools in a standardized format.
.. _JSON Schema: https://json-schema.org
.. _"$id" keyword: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#name-the-id-keyword
.. testsetup::
from amaranth import *
from amaranth.lib import wiring, meta
from amaranth.lib.wiring import In, OutIntroduction
Many Amaranth designs stay entirely within the Amaranth ecosystem, using the facilities it provides to define, test, and build hardware. In this case, the design is available for exploration using Python code, and metadata is not necessary. However, if an Amaranth design needs to fit into an existing ecosystem, or, conversely, to integrate components developed for another ecosystem, metadata can be used to exchange structured information about the design.
Consider a simple :ref:component <wiring>:
.. testcode::
class Adder(wiring.Component):
a: In(unsigned(32))
b: In(unsigned(32))
o: Out(unsigned(33))
def elaborate(self, platform):
m = Module()
m.d.comb += self.o.eq(self.a + self.b)
return m..
TODO: link to Verilog backend doc when we have it
While it can be easily converted to Verilog, external tools will find the interface of the resulting module opaque unless they parse its Verilog source (a difficult and unrewarding task), or are provided with a description of it. Components can describe their signature with JSON-based metadata:
.. doctest::
>>> adder = Adder()
>>> adder.metadata # doctest: +ELLIPSIS
<amaranth.lib.wiring.ComponentMetadata for ...Adder object at ...>
>>> adder.metadata.as_json() # doctest: +SKIP
{
'interface': {
'members': {
'a': {
'type': 'port',
'name': 'a',
'dir': 'in',
'width': 32,
'signed': False,
'init': '0'
},
'b': {
'type': 'port',
'name': 'b',
'dir': 'in',
'width': 32,
'signed': False,
'init': '0'
},
'o': {
'type': 'port',
'name': 'o',
'dir': 'out',
'width': 33,
'signed': False,
'init': '0'
}
},
'annotations': {}
}
}.. testcode::
:hide:
# The way doctest requires this object to be formatted is truly hideous, even with +NORMALIZE_WHITESPACE.
assert adder.metadata.as_json() == {'interface': {'members': {'a': {'type': 'port', 'name': 'a', 'dir': 'in', 'width': 32, 'signed': False, 'init': '0'}, 'b': {'type': 'port', 'name': 'b', 'dir': 'in', 'width': 32, 'signed': False, 'init': '0'}, 'o': {'type': 'port', 'name': 'o', 'dir': 'out', 'width': 33, 'signed': False, 'init': '0'}}, 'annotations': {}}}All metadata in Amaranth must adhere to a schema in the JSON Schema_ language, which is integral to its definition, and can be used to validate the generated JSON:
.. doctest::
>>> wiring.ComponentMetadata.validate(adder.metadata.as_json())The built-in component metadata can be extended to provide arbitrary information about an interface through user-defined annotations. For example, a memory bus interface could provide the layout of any memory-mapped peripherals accessible through that bus.
Defining annotations
Consider a simple control and status register (CSR) bus that provides the memory layout of the accessible registers via an annotation:
.. testcode::
class CSRLayoutAnnotation(meta.Annotation):
schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://amaranth-lang.org/schema/example/0/csr-layout.json",
"type": "object",
"properties": {
"registers": {
"type": "object",
"patternProperties": {
"^.+$": {
"type": "integer",
"minimum": 0,
},
},
},
},
"requiredProperties": [
"registers",
],
}
def __init__(self, origin):
self._origin = origin
@property
def origin(self):
return self._origin
def as_json(self):
instance = {
"registers": self.origin.registers,
}
# Validating the value returned by `as_json()` ensures its conformance.
self.validate(instance)
return instance
class CSRSignature(wiring.Signature):
def __init__(self):
super().__init__({
"addr": Out(16),
"w_en": Out(1),
"w_data": Out(32),
"r_en": Out(1),
"r_data": In(32),
})
def annotations(self, obj, /):
# Unfortunately `super()` cannot be used in `wiring.Signature` subclasses;
# instead, use a direct call to a superclass method. In this case that is
# `wiring.Signature` itself, but in a more complex class hierarchy it could
# be different.
return wiring.Signature.annotations(self, obj) + (CSRLayoutAnnotation(obj),)A component that embeds a few CSR registers would define their addresses:
.. testcode::
class MyPeripheral(wiring.Component):
csr_bus: In(CSRSignature())
def __init__(self):
super().__init__()
self.csr_bus.registers = {
"control": 0x0000,
"status": 0x0004,
"data": 0x0008,
}.. doctest::
>>> peripheral = MyPeripheral()
>>> peripheral.metadata.as_json() # doctest: +SKIP
{
'interface': {
'members': {
'csr_bus': {
'type': 'interface',
'members': {
'addr': {
'type': 'port',
'name': 'csr_bus__addr',
'dir': 'in',
'width': 16,
'signed': False,
'init': '0'
},
'w_en': {
'type': 'port',
'name': 'csr_bus__w_en',
'dir': 'in',
'width': 1,
'signed': False,
'init': '0'
},
'w_data': {
'type': 'port',
'name': 'csr_bus__w_data',
'dir': 'in',
'width': 32,
'signed': False,
'init': '0'
},
'r_en': {
'type': 'port',
'name': 'csr_bus__r_en',
'dir': 'in',
'width': 1,
'signed': False,
'init': '0'
},
'r_data': {
'type': 'port',
'name': 'csr_bus__r_data',
'dir': 'out',
'width': 32,
'signed': False,
'init': '0'
},
},
'annotations': {
'https://amaranth-lang.org/schema/example/0/csr-layout.json': {
'registers': {
'control': 0,
'status': 4,
'data': 8
}
}
}
}
},
'annotations': {}
}
}.. testcode::
:hide:
# The way doctest requires this object to be formatted is truly hideous, even with +NORMALIZE_WHITESPACE.
assert peripheral.metadata.as_json() == {'interface': {'members': {'csr_bus': {'type': 'interface', 'members': {'addr': {'type': 'port', 'name': 'csr_bus__addr', 'dir': 'in', 'width': 16, 'signed': False, 'init': '0'}, 'w_en': {'type': 'port', 'name': 'csr_bus__w_en', 'dir': 'in', 'width': 1, 'signed': False, 'init': '0'}, 'w_data': {'type': 'port', 'name': 'csr_bus__w_data', 'dir': 'in', 'width': 32, 'signed': False, 'init': '0'}, 'r_en': {'type': 'port', 'name': 'csr_bus__r_en', 'dir': 'in', 'width': 1, 'signed': False, 'init': '0'}, 'r_data': {'type': 'port', 'name': 'csr_bus__r_data', 'dir': 'out', 'width': 32, 'signed': False, 'init': '0'}}, 'annotations': {'https://amaranth-lang.org/schema/example/0/csr-layout.json': {'registers': {'control': 0, 'status': 4, 'data': 8}}}}}, 'annotations': {}}}Identifying schemas
An :class:Annotation schema must have a "$id" property, whose value is a URL that serves as its globally unique identifier. The suggested format of this URL is:
.. code::
://<domain>/schema//<version>/.jsonwhere:
* ``<domain>`` is a domain name registered to the person or entity defining the annotation;
* ```` is the name of the Python package providing the :class:`Annotation` subclass;
* ``<version>`` is the version of that package;
* ```` is a non-empty string specific to the annotation... note::
Annotations used in the Amaranth project packages are published under https://amaranth-lang.org/schema/ according to this URL format, and are covered by the usual compatibility commitment.
Other projects that define additional Amaranth annotations are encouraged, but not required, to make their schemas publicly accessible; the only requirement is for the URL to be globally unique.Reference
.. autoexception:: InvalidSchema
.. autoexception:: InvalidAnnotation
.. autoclass:: Annotation
:no-members:
:members: validate, origin, as_json
.. automethod:: __init_subclass__()
.. autoattribute:: schema
:annotation: = { "$id": "...", ... }Stdlib Stream (`docs/stdlib/stream.rst`)
Data streams
.. py:module:: amaranth.lib.stream
The :mod:amaranth.lib.stream module provides a mechanism for unidirectional exchange of arbitrary data between modules.
Introduction
One of the most common flow control mechanisms is ready/valid handshaking, where a producer pushes data to a consumer whenever it becomes available, and the consumer signals to the producer whether it can accept more data. In Amaranth, this mechanism is implemented using an :ref:interface <wiring> with three members:
- :py:
payload(driven by the producer), containing the data; - :py:
valid(driven by the producer), indicating that data is currently available in :py:payload; - :py:
ready(driven by the consumer), indicating that data is accepted if available.
This module provides such an interface, :class:stream.Interface , and defines the exact rules governing the flow of data through it.
.. _stream-rules:
Data transfer rules
The producer and the consumer must be synchronized: they must belong to the same :ref:clock domain <lang-clockdomains>, and any :ref:control flow modifiers <lang-controlinserter> must be applied to both, in the same order.
Data flows through a stream according to the following four rules:
- On each cycle where both :py:
validand :py:readyare asserted, a transfer is performed: the contents ofpayloadare conveyed from the producer to the consumer. - Once the producer asserts :py:
valid, it must not deassert :py:validor change the contents ofpayloaduntil a transfer is performed. - The producer must not wait for :py:
readyto be asserted before asserting :py:valid: any form of feedback from :py:readythat causes :py:validto become asserted is prohibited. - The consumer may assert or deassert :py:
readyat any time, including via combinational feedback from :py:valid.
Some producers and consumers may be designed without support for backpressure. Such producers must tie :py:ready to :py:Const(1) by specifying :py:always_ready=True when constructing a stream, and consumers may (but are not required to) do the same. Similarly, some producers and consumers may be designed such that a payload is provided or must be provided on each cycle. Such consumers must tie :py:valid to :py:Const(1) by specifying :py:always_valid=True when constructing a stream, and producers may (but are not required to) do the same.
If these control signals are tied to :py:Const(1), then the :func:wiring.connect <.lib.wiring.connect> function ensures that only compatible streams are connected together. For example, if the producer does not support backpressure (:py:ready tied to :py:Const(1)), it can only be connected to consumers that do not require backpressure. However, consumers that do not require backpressure can be connected to producers with or without support for backpressure. The :py:valid control signal is treated similarly.
These rules ensure that producers and consumers that are developed independently can be safely used together, without unduly restricting the application-specific conditions that determine assertion of :py:valid and :py:ready.
Examples
The following examples demonstrate the use of streams for a data processing pipeline that receives serial data input from an external device, transforms it by negating the 2's complement value, and transmits it to another external device whenever requested. Similar pipelines, albeit more complex, are widely used in :abbr:DSP (digital signal processing) applications.
The use of a unified data transfer mechanism enables uniform testing of individual units, and makes it possible to add a queue to the pipeline using only two additional connections.
.. testsetup::
from amaranth import *.. testcode::
from amaranth.lib import stream, wiring
from amaranth.lib.wiring import In, OutThe pipeline is tested using the :doc:built-in simulator </simulator> and the two helper functions defined below:
.. testcode::
from amaranth.sim import Simulator, Period
async def stream_get(ctx, stream):
ctx.set(stream.ready, 1)
payload, = await ctx.tick().sample(stream.payload).until(stream.valid)
ctx.set(stream.ready, 0)
return payload
async def stream_put(ctx, stream, payload):
ctx.set(stream.valid, 1)
ctx.set(stream.payload, payload)
await ctx.tick().until(stream.ready)
ctx.set(stream.valid, 0).. note::
"Minimal streams" as defined in `RFC 61`_ do not provide built-in helper functions for testing pending further work on the clock domain system. They will be provided in a later release. For the time being, you can copy the helper functions above to test your designs that use streams.Serial receiver
+++++++++++++++
The serial receiver captures the serial output of an external device and converts it to a stream of words. While the ssel signal is high, each low-to-high transition on the sclk input captures the value of the sdat signal; eight consecutive captured bits are assembled into a word (:abbr:MSB (most significant bit) first) and pushed into the pipeline for processing. If the ssel signal is low, no data transmission occurs and the transmitter and the receiver are instead synchronized with each other.
In this example, the external device does not provide a way to pause data transmission. If the pipeline isn't ready to accept the next payload, it is necessary to discard data at some point; here, it is done in the serial receiver.
.. testcode::
class SerialReceiver(wiring.Component):
ssel: In(1)
sclk: In(1)
sdat: In(1)
stream: Out(stream.Signature(signed(8)))
def elaborate(self, platform):
m = Module()
# Detect edges on the `sclk` input:
sclk_reg = Signal()
sclk_edge = ~sclk_reg & self.sclk
m.d.sync += sclk_reg.eq(self.sclk)
# Capture `sdat` and bits into payloads:
count = Signal(range(8))
data = Signal(8)
done = Signal()
with m.If(~self.ssel):
m.d.sync += count.eq(0)
with m.Elif(sclk_edge):
m.d.sync += count.eq(count + 1)
m.d.sync += data.eq(Cat(self.sdat, data))
m.d.sync += done.eq(count == 7)
# Push assembled payloads into the pipeline:
with m.If(done & (~self.stream.valid | self.stream.ready)):
m.d.sync += self.stream.payload.eq(data)
m.d.sync += self.stream.valid.eq(1)
m.d.sync += done.eq(0)
with m.Elif(self.stream.ready):
m.d.sync += self.stream.valid.eq(0)
# Payload is discarded if `done & self.stream.valid & ~self.stream.ready`.
return m.. testcode::
def test_serial_receiver():
dut = SerialReceiver()
async def testbench_input(ctx):
await ctx.tick()
ctx.set(dut.ssel, 1)
await ctx.tick()
for bit in [1, 0, 1, 0, 0, 1, 1, 1]:
ctx.set(dut.sdat, bit)
ctx.set(dut.sclk, 0)
await ctx.tick()
ctx.set(dut.sclk, 1)
await ctx.tick()
ctx.set(dut.ssel, 0)
await ctx.tick()
async def testbench_output(ctx):
expected_word = 0b10100111
payload = await stream_get(ctx, dut.stream)
assert (payload & 0xff) == (expected_word & 0xff), \
f"{payload & 0xff:08b} != {expected_word & 0xff:08b} (expected)"
sim = Simulator(dut)
sim.add_clock(Period(MHz=1))
sim.add_testbench(testbench_input)
sim.add_testbench(testbench_output)
with sim.write_vcd("stream_serial_receiver.vcd"):
sim.run().. testcode::
:hide:
test_serial_receiver()The serial protocol recognized by the receiver is illustrated with the following diagram (corresponding to stream_serial_receiver.vcd):
.. wavedrom:: stream/serial_receiver
{
signal: [
{ name: "clk", wave: "lpppppppppppppppppppp" },
{},
[
"serial",
{ name: "ssel", wave: "01................0.." },
{ name: "sclk", wave: "0..101010101010101..." },
{ name: "sdat", wave: "0.=.=.=.=.=.=.=.=....", data: ["1", "0", "1", "0", "0", "1", "1", "1"] },
],
{},
[
"stream",
{ name: "payload", wave: "=..................=.", data: ["00", "A7"] },
{ name: "valid", wave: "0..................10" },
{ name: "ready", wave: "1...................0" },
]
]
}Serial transmitter
++++++++++++++++++
The serial transmitter accepts a stream of words and provides it to the serial input of an external device whenever requested. Its serial interface is the same as that of the serial receiver, with the exception that the sclk and sdat signals are outputs. The ssel signal remains an input; the external device uses it for flow control.
.. testcode::
class SerialTransmitter(wiring.Component):
ssel: In(1)
sclk: Out(1)
sdat: Out(1)
stream: In(stream.Signature(signed(8)))
def elaborate(self, platform):
m = Module()
count = Signal(range(9))
data = Signal(8)
with m.If(~self.ssel):
m.d.sync += count.eq(0)
m.d.sync += self.sclk.eq(1)
with m.Elif(count != 0):
m.d.comb += self.stream.ready.eq(0)
m.d.sync += self.sclk.eq(~self.sclk)
with m.If(self.sclk):
m.d.sync += data.eq(Cat(0, data))
m.d.sync += self.sdat.eq(data[-1])
with m.Else():
m.d.sync += count.eq(count - 1)
with m.Else():
m.d.comb += self.stream.ready.eq(1)
with m.If(self.stream.valid):
m.d.sync += count.eq(8)
m.d.sync += data.eq(self.stream.payload)
return m.. testcode::
def test_serial_transmitter():
dut = SerialTransmitter()
async def testbench_input(ctx):
await stream_put(ctx, dut.stream, 0b10100111)
async def testbench_output(ctx):
await ctx.tick()
ctx.set(dut.ssel, 1)
for index, expected_bit in enumerate([1, 0, 1, 0, 0, 1, 1, 1]):
_, sdat = await ctx.posedge(dut.sclk).sample(dut.sdat)
assert sdat == expected_bit, \
f"bit {index}: {sdat} != {expected_bit} (expected)"
ctx.set(dut.ssel, 0)
await ctx.tick()
sim = Simulator(dut)
sim.add_clock(Period(MHz=1))
sim.add_testbench(testbench_input)
sim.add_testbench(testbench_output)
with sim.write_vcd("stream_serial_transmitter.vcd"):
sim.run().. testcode::
:hide:
test_serial_transmitter()Value negator
+++++++++++++
The value negator accepts a stream of words, negates the 2's complement value of these words, and provides the result as a stream of words again. In a practical :abbr:DSP application, this unit could be replaced with, for example, a :abbr:FIR (finite impulse response) filter.
.. testcode::
class ValueNegator(wiring.Component):
i_stream: In(stream.Signature(signed(8)))
o_stream: Out(stream.Signature(signed(8)))
def elaborate(self, platform):
m = Module()
with m.If(self.i_stream.valid & (~self.o_stream.valid | self.o_stream.ready)):
m.d.comb += self.i_stream.ready.eq(1)
m.d.sync += self.o_stream.payload.eq(-self.i_stream.payload)
m.d.sync += self.o_stream.valid.eq(1)
with m.Elif(self.o_stream.ready):
m.d.sync += self.o_stream.valid.eq(0)
return m.. testcode::
def test_value_negator():
dut = ValueNegator()
async def testbench_input(ctx):
await stream_put(ctx, dut.i_stream, 1)
await stream_put(ctx, dut.i_stream, 17)
async def testbench_output(ctx):
assert await stream_get(ctx, dut.o_stream) == -1
assert await stream_get(ctx, dut.o_stream) == -17
sim = Simulator(dut)
sim.add_clock(Period(MHz=1))
sim.add_testbench(testbench_input)
sim.add_testbench(testbench_output)
with sim.write_vcd("stream_value_negator.vcd"):
sim.run().. testcode::
:hide:
test_value_negator()Complete pipeline
+++++++++++++++++
The complete pipeline consists of a serial receiver, a value negator, a FIFO queue, and a serial transmitter connected in series. Without queueing, any momentary mismatch between the rate at which the serial data is produced and consumed would result in data loss. A FIFO queue from the :mod:.lib.fifo standard library module is used to avoid this problem.
.. testcode::
from amaranth.lib.fifo import SyncFIFOBuffered
class ExamplePipeline(wiring.Component):
i_ssel: In(1)
i_sclk: In(1)
i_sdat: In(1)
o_ssel: In(1)
o_sclk: Out(1)
o_sdat: Out(1)
def elaborate(self, platform):
m = Module()
# Create and connect serial receiver:
m.submodules.receiver = receiver = SerialReceiver()
m.d.comb += [
receiver.ssel.eq(self.i_ssel),
receiver.sclk.eq(self.i_sclk),
receiver.sdat.eq(self.i_sdat),
]
# Create and connect value negator:
m.submodules.negator = negator = ValueNegator()
wiring.connect(m, receiver=receiver.stream, negator=negator.i_stream)
# Create and connect FIFO queue:
m.submodules.queue = queue = SyncFIFOBuffered(width=8, depth=16)
wiring.connect(m, negator=negator.o_stream, queue=queue.w_stream)
# Create and connect serial transmitter:
m.submodules.transmitter = transmitter = SerialTransmitter()
wiring.connect(m, queue=queue.r_stream, transmitter=transmitter.stream)
# Connect outputs:
m.d.comb += [
transmitter.ssel.eq(self.o_ssel),
self.o_sclk.eq(transmitter.sclk),
self.o_sdat.eq(transmitter.sdat),
]
return m.. testcode::
def test_example_pipeline():
dut = ExamplePipeline()
async def testbench_input(ctx):
for value in [1, 17]:
ctx.set(dut.i_ssel, 1)
for bit in reversed(range(8)):
ctx.set(dut.i_sclk, 0)
ctx.set(dut.i_sdat, bool(value & (1 << bit)))
await ctx.tick()
ctx.set(dut.i_sclk, 1)
await ctx.tick()
await ctx.tick()
ctx.set(dut.i_ssel, 0)
ctx.set(dut.i_sclk, 0)
await ctx.tick()
async def testbench_output(ctx):
await ctx.tick()
ctx.set(dut.o_ssel, 1)
for index, expected_value in enumerate([-1, -17]):
value = 0
for _ in range(8):
_, sdat = await ctx.posedge(dut.o_sclk).sample(dut.o_sdat)
value = (value << 1) | sdat
assert value == (expected_value & 0xff), \
f"word {index}: {value:08b} != {expected_value & 0xff:08b} (expected)"
await ctx.tick()
ctx.set(dut.o_ssel, 0)
sim = Simulator(dut)
sim.add_clock(Period(MHz=1))
sim.add_testbench(testbench_input)
sim.add_testbench(testbench_output)
with sim.write_vcd("stream_example_pipeline.vcd"):
sim.run().. testcode::
:hide:
test_example_pipeline()This data processing pipeline overlaps reception and transmission of serial data, with only a few cycles of latency between the completion of reception and the beginning of transmission of the processed data:
.. image:: _images/stream_pipeline.png
Implementing such an efficient pipeline can be difficult without the use of appropriate abstractions. The use of streams allows the designer to focus on the data processing and simplifies testing by ensuring that the interaction of the individual units is standard and well-defined.
Reference
Components that communicate using streams must not only use a :class:stream.Interface , but also follow the :ref:data transfer rules <stream-rules>.
.. autoclass:: Signature
.. autoclass:: Interface
Stdlib Wiring (`docs/stdlib/wiring.rst`)
.. _wiring:
Interfaces and connections
##########################
.. py:module:: amaranth.lib.wiring
The :mod:amaranth.lib.wiring module provides a way to declare the interfaces between design components and connect them to each other in a reliable and convenient way.
.. testsetup::
from amaranth import *
.. _wiring-introduction:
Introduction
Overview
++++++++
This module provides four related facilities:
- Description and construction of interface objects via :class:
Flow(:data:Inand :data:Out), :class:Member, and :class:Signature, as well as the associated container class :class:SignatureMembers. These classes provide the syntax used in defining components, and are also useful for introspection. - Flipping of signatures and interface objects via :class:
FlippedSignatureand :class:FlippedInterface, as well as the associated container class :class:FlippedSignatureMembers. This facility reduces boilerplate by adapting existing signatures and interface objects: the flip operation changes the :data:Indata flow of a member to :data:Outand vice versa. - Connecting interface objects together via :func:
connect. The :func:connectfunction ensures that the provided interface objects can be connected to each other, and adds the necessary :py:.eq()statements to a :class:Module. - Defining reusable, self-contained components via :class:
Component. Components are :class:Elaboratableobjects that interact with the rest of the design through an interface specified by their signature.
To use this module, add the following imports to the beginning of the file:
.. testcode::
from amaranth.lib import wiring
from amaranth.lib.wiring import In, Out
The :ref:"Motivation" <wiring-intro1> and :ref:"Reusable interfaces" <wiring-intro2> sections describe concepts that are essential for using this module and writing idiomatic Amaranth code. The sections after describe advanced use cases that are only relevant for more complex code.
.. _wiring-intro1:
Motivation
++++++++++
Consider a reusable counter with an enable input, configurable limit, and an overflow flag. Using only the core Amaranth language, it could be implemented as:
.. testcode::
class BasicCounter(Elaboratable):
def __init__(self):
self.en = Signal()
self.count = Signal(8)
self.limit = Signal.like(self.count)
self.overflow = Signal()
def elaborate(self, platform):
m = Module()
with m.If(self.en):
m.d.sync += self.overflow.eq(0)
with m.If(self.count == self.limit):
m.d.sync += self.overflow.eq(1)
m.d.sync += self.count.eq(0)
with m.Else():
m.d.sync += self.count.eq(self.count + 1)
return mNothing in this implementation indicates the directions of its ports (:py:en, :py:count, :py:limit, and :py:overflow) in relation to other parts of the design. To understand whether the value of a port is expected to be provided externally or generated internally, it is first necessary to read the body of the :py:elaborate method. If the port is not used within that method in a particular elaboratable, it is not possible to determine its direction, or whether it is even meant to be connected.
The :mod:amaranth.lib.wiring module provides a solution for this problem: components. A component is an elaboratable that declares the shapes and directions of its ports in its signature. The example above can be rewritten to use the :class:Component base class (which itself inherits from :class:Elaboratable) to be:
.. testcode::
class ComponentCounter(wiring.Component):
en: In(1)
count: Out(8)
limit: In(8)
overflow: Out(1)
def elaborate(self, platform):
m = Module()
with m.If(self.en):
m.d.sync += self.overflow.eq(0)
with m.If(self.count == self.limit):
m.d.sync += self.overflow.eq(1)
m.d.sync += self.count.eq(0)
with m.Else():
m.d.sync += self.count.eq(self.count + 1)
return mThe code in the constructor creating the signals of the counter's interface one by one is now gone, replaced with the :term:variable annotations declaring the counter's interface. The inherited constructor, :meth:Component.__init__, creates the same attributes with the same values as before, and the :py:elaborate method is unchanged.
The major difference between the two examples is that the :py:ComponentCounter provides unambiguous answers to two questions that previously required examining the :py:elaborate method:
- Which of the Python object's attributes are ports that are intended to be connected to the rest of the design.
- What is the direction of the flow of information through the port.
This information, aside from being clear from the source code, can now be retrieved from the :py:.signature attribute, which contains an instance of the :class:Signature class:
.. doctest::
>>> ComponentCounter().signature
Signature({'en': In(1), 'count': Out(8), 'limit': In(8), 'overflow': Out(1)})The :ref:shapes <lang-shapes> of the ports need not be static. The :py:ComponentCounter can be made generic, with its range specified when it is constructed, by creating the signature explicitly in its constructor:
.. testcode::
class GenericCounter(wiring.Component):
def __init__(self, width):
super().__init__({
"en": In(1),
"count": Out(width),
"limit": In(width),
"overflow": Out(1)
})
# The implementation of the `elaborate` method is the same.
elaborate = ComponentCounter.elaborate.. doctest::
>>> GenericCounter(16).signature
Signature({'en': In(1), 'count': Out(16), 'limit': In(16), 'overflow': Out(1)})Instances of the :class:ComponentCounter and :class:GenericCounter class are two examples of interface objects. An interface object is a Python object of any type whose :py:signature attribute contains a :class:Signature with which the interface object is compliant (as determined by the :meth:is_compliant <Signature.is_compliant> method of the signature).
The next section introduces the concepts of directionality and connection, and discusses interface objects in more detail.
.. _wiring-intro2:
Reusable interfaces
+++++++++++++++++++
Consider a more complex example where two components are communicating with a stream that is using ready/valid signaling, where the :py:valid signal indicates that the value of :py:data provided by the source is meaningful, and the :py:ready signal indicates that the sink has consumed the data word:
.. testcode::
class DataProducer(wiring.Component):
en: In(1)
data: Out(8)
valid: Out(1)
ready: In(1)
def elaborate(self, platform): ...
class DataConsumer(wiring.Component):
data: In(8)
valid: In(1)
ready: Out(1)
# ... other ports...
def elaborate(self, platform): ...Data would be transferred between these components by assigning the outputs to the inputs elsewhere in the design:
.. testcode::
m = Module()
m.submodules.producer = producer = DataProducer()
m.submodules.consumer = consumer = DataConsumer()
...
m.d.comb += [
consumer.data.eq(producer.data),
consumer.valid.eq(producer.valid),
producer.ready.eq(consumer.ready),
]Although this example is short, it is already repetitive and redundant. The ports on the producer and the consumer, which must match each other for the connection to be made correctly, are declared twice; and the connection itself is made in an error-prone manual way even though the signatures include all of the information required to create it.
The signature of a stream could be defined in a generic way:
.. testcode::
class SimpleStreamSignature(wiring.Signature):
def __init__(self, data_shape):
super().__init__({
"data": Out(data_shape),
"valid": Out(1),
"ready": In(1)
})
def __eq__(self, other):
return self.members == other.members.. doctest::
>>> SimpleStreamSignature(8).members
SignatureMembers({'data': Out(8), 'valid': Out(1), 'ready': In(1)})A definition like this is usable, depending on the data flow direction of the members, only in the producer (as in the code above) or only in the consumer. To resolve this problem, this module introduces flipping: an operation that reverses the data flow direction of the members of a signature or an interface object while leaving everything else about the object intact. In Amaranth, the (non-flipped) signature definition always declares the data flow directions appropriate for a bus initiator, stream source, controller, and so on. A bus target, stream sink, peripheral, and so on would reuse the source definition by flipping it.
A signature is flipped by calling :meth:sig.flip() <Signature.flip>, and an interface object is flipped by calling :func:flipped(intf) <flipped>. These calls return instances of the :class:FlippedSignature and :class:FlippedInterface classes, respectively, which use metaprogramming to wrap another object, changing only the data flow directions of its members and forwarding all other method calls and attribute accesses to the wrapped object.
The example above can be rewritten to use this definition of a stream signature as:
.. testcode::
class StreamProducer(wiring.Component):
en: In(1)
source: Out(SimpleStreamSignature(8))
def elaborate(self, platform): ...
class StreamConsumer(wiring.Component):
sink: Out(SimpleStreamSignature(8).flip())
def elaborate(self, platform): ...
m = Module()
m.submodules.producer = producer = StreamProducer()
m.submodules.consumer = consumer = StreamConsumer()The producer and the consumer reuse the same signature, relying on flipping to make the port directions complementary:
.. doctest::
>>> producer.source.signature.members
SignatureMembers({'data': Out(8), 'valid': Out(1), 'ready': In(1)})
>>> producer.source.signature.members['data']
Out(8)
>>> consumer.sink.signature.members
SignatureMembers({'data': Out(8), 'valid': Out(1), 'ready': In(1)}).flip()
>>> consumer.sink.signature.members['data']
In(8)In the :py:StreamConsumer definition above, the :py:sink member has its direction flipped explicitly because the sink is a stream input; this is the case for every interface due to how port directions are defined. Since this operation is so ubiquitous, it is also performed when :py:In(...) is used with a signature rather than a shape. The :py:StreamConsumer definition above should normally be written as:
.. testcode::
class StreamConsumerUsingIn(wiring.Component):
sink: In(SimpleStreamSignature(8))
def elaborate(self, platform): ...The data flow directions of the ports are identical between the two definitions:
.. doctest::
>>> consumer.sink.signature.members == StreamConsumerUsingIn().sink.signature.members
TrueIf signatures are nested within each other multiple levels deep, the final port direction is determined by how many nested :py:In(...) members there are. For each :py:In(...) signature wrapping a port, the data flow direction of the port is flipped once:
.. doctest::
>>> sig = wiring.Signature({"port": Out(1)})
>>> sig.members["port"]
Out(1)
>>> in1 = wiring.Signature({"sig": In(sig)})
>>> in1.members["sig"].signature.members["port"]
In(1)
>>> in2 = wiring.Signature({"sig": In(in1)})
>>> in2.members["sig"].signature.members["sig"].signature.members["port"]
Out(1)Going back to the stream example, the producer and the consumer now communicate with one another using the same set of ports with identical shapes and complementary directions (the auxiliary :py:en port being outside of the stream signature), and can be connected using the :func:connect function:
.. testcode::
wiring.connect(m, producer.source, consumer.sink)This function examines the signatures of the two provided interface objects, ensuring that they are exactly complementary, and then adds combinational :py:.eq() statements to the module for each of the port pairs to form the connection. Aside from the connectability check, the single line above is equivalent to:
.. testcode::
m.d.comb += [
consumer.sink.data.eq(producer.source.data),
consumer.sink.valid.eq(producer.source.valid),
producer.source.ready.eq(consumer.sink.ready),
]Even on the simple example of a stream signature it is clear how using the :func:connect function results in more concise, readable, and robust code. The difference is proportionally more pronounced with more complex signatures. When a signature is being refactored, no changes to the code that uses :func:connect is required.
This explanation concludes the essential knowledge necessary for using this module and writing idiomatic Amaranth code.
.. _wiring-forwarding:
Forwarding interior interfaces
++++++++++++++++++++++++++++++
Consider a case where a component includes another component as a part of its implementation, and where it is necessary to forward the ports of the inner component, that is, expose them within the outer component's signature. To use the :py:SimpleStreamSignature definition above in an example:
.. testcode::
class DataProcessorImplementation(wiring.Component):
source: Out(SimpleStreamSignature(8))
def elaborate(self, platform): ...
class DataProcessorWrapper(wiring.Component):
source: Out(SimpleStreamSignature(8))
def elaborate(self, platform):
m = Module()
m.submodules.impl = impl = DataProcessorImplementation()
m.d.comb += [
self.source.data.eq(impl.source.data),
self.source.valid.eq(impl.source.valid),
impl.source.ready.eq(self.source.ready),
]
return mBecause forwarding the ports requires assigning an output to an output and an input to an input, the :func:connect function, which connects outputs to inputs and vice versa, cannot be used---at least not directly. The :func:connect function is designed to cover the usual case of connecting the interfaces of modules from outside those modules. In order to connect an interface from inside a module, it is necessary to flip that interface first using the :func:flipped function. The :py:DataProcessorWrapper should instead be implemented as:
.. testcode::
class DataProcessorWrapper(wiring.Component):
source: Out(SimpleStreamSignature(8))
def elaborate(self, platform):
m = Module()
m.submodules.impl = impl = DataProcessorImplementation()
wiring.connect(m, wiring.flipped(self.source), impl.source)
return mIn some cases, both of the two interfaces provided to :func:connect must be flipped. For example, the correct way to implement a component that forwards an input interface to an output interface with no processing is:
.. testcode::
class DataForwarder(wiring.Component):
sink: In(SimpleStreamSignature(8))
source: Out(SimpleStreamSignature(8))
def elaborate(self, platform):
m = Module()
wiring.connect(m, wiring.flipped(self.sink), wiring.flipped(self.source))
return m.. warning::
It is important to wrap an interface with the :func:`flipped` function whenever it is being connected from inside the module. If the :py:`elaborate` function above had made a connection using :py:`wiring.connect(m, self.sink, self.source)`, it would not work correctly. No diagnostic is emitted in this case... _wiring-constant-inputs:
Constant inputs
+++++++++++++++
Sometimes, a component must conform to a particular signature, but some of the input ports required by the signature must have a fixed value at all times. This module addresses this case by allowing both :class:Signal and :class:Const objects to be used to implement port members:
.. testcode::
class ProducerRequiringReady(wiring.Component):
source: Out(SimpleStreamSignature(8))
def __init__(self):
super().__init__()
self.source.ready = Const(1)
def elaborate(self, platform): ...
class ConsumerAlwaysReady(wiring.Component):
sink: In(SimpleStreamSignature(8))
def __init__(self):
super().__init__()
self.sink.ready = Const(1)
def elaborate(self, platform): ...
class ConsumerPossiblyUnready(wiring.Component):
sink: In(SimpleStreamSignature(8))
def elaborate(self, platform): ..... doctest::
>>> SimpleStreamSignature(8).is_compliant(ProducerRequiringReady().source)
True
>>> SimpleStreamSignature(8).flip().is_compliant(ConsumerAlwaysReady().sink)
TrueHowever, the :func:connect function considers a constant input to be connectable only to a constant output with the same value:
.. doctest::
>>> wiring.connect(m, ProducerRequiringReady().source, ConsumerAlwaysReady().sink)
>>> wiring.connect(m, ProducerRequiringReady().source, ConsumerPossiblyUnready().sink)
Traceback (most recent call last):
...
amaranth.lib.wiring.ConnectionError: Cannot connect to the input member 'arg0.ready' that has a constant value 1This feature reduces the proliferation of similar but subtly incompatible interfaces that are semantically similar, only differing in the presence or absence of optional control or status signals.
.. _wiring-adapting-interfaces:
Adapting interfaces
+++++++++++++++++++
Sometimes, a design requires an interface with a particular signature to be used, but the only implementation available is either a component with an incompatible signature or an elaboratable with no signature at all. If this problem cannot be resolved by other means, interface adaptation can be used, where the existing signals are placed into a new interface with the appropriate signature. For example:
.. testcode::
class LegacyAXIDataProducer(Elaboratable):
def __init__(self):
self.adata = Signal(8)
self.avalid = Signal()
self.aready = Signal()
def elaborate(self, platform): ...
class ModernDataConsumer(wiring.Component):
sink: In(SimpleStreamSignature(8))
data_producer = LegacyAXIDataProducer()
data_consumer = ModernDataConsumer()
adapted_data_source = SimpleStreamSignature(8).create()
adapted_data_source.data = data_producer.adata
adapted_data_source.valid = data_producer.avalid
adapted_data_source.ready = data_producer.aready
m = Module()
wiring.connect(m, adapted_data_source, data_consumer.sink)When creating an adapted interface, use the :meth:create <Signature.create> method of the signature that is required elsewhere in the design.
.. _wiring-customizing:
Customizing signatures and interfaces
+++++++++++++++++++++++++++++++++++++
The :mod:amaranth.lib.wiring module encourages creation of reusable building blocks. In the examples above, a custom signature, :py:SimpleStreamSignature, was introduced to illustrate the essential concepts necessary to use this module. While sufficient for that goal, it does not demonstrate the full capabilities provided by the module.
Consider a simple System-on-Chip memory bus with a configurable address width. In an application like that, additional properties and methods could be usefully defined both on the signature (for example, properties to retrieve the parameters of the interface) and on the created interface object (for example, methods to examine the control and status signals). These can be defined as follows:
.. testcode::
from amaranth.lib import enum
class TransferType(enum.Enum, shape=1):
Write = 0
Read = 1
class SimpleBusSignature(wiring.Signature):
def __init__(self, addr_width=32):
self._addr_width = addr_width
super().__init__({
"en": Out(1),
"rw": Out(TransferType),
"addr": Out(self._addr_width),
"r_data": In(32),
"w_data": Out(32),
})
@property
def addr_width(self):
return self._addr_width
def __eq__(self, other):
return isinstance(other, SimpleBusSignature) and self.addr_width == other.addr_width
def __repr__(self):
return f"SimpleBusSignature({self.addr_width})"
def create(self, *, path=None, src_loc_at=0):
return SimpleBusInterface(self, path=path, src_loc_at=1 + src_loc_at)
class SimpleBusInterface(wiring.PureInterface):
def is_read_xfer(self):
return self.en & (self.rw == TransferType.Read)
def is_write_xfer(self):
return self.en & (self.rw == TransferType.Write)This example demonstrates several important principles of use:
- Defining additional properties for a custom signature. The :class:
Signatureobjects are mutable in a restricted way, and can be frozen with the :meth:freeze <Signature.freeze>method. In almost all cases, the newly defined properties must be immutable, as shown above. - Defining a signature-specific :py:
__eq__method. While anonymous (created from a dictionary of members) instances of :class:Signaturecompare structurally, instances of :class:Signature-derived classes compare by identity unless the equality operator is overridden. In almost all cases, the equality operator should compare the parameters of the signatures rather than their structures. - Defining a signature-specific :py:
__repr__method. Similarly to :py:__eq__, the default implementation for :class:Signature-derived classes uses the signature's identity. In almost all cases, the representation conversion operator should return an expression that constructs an equivalent signature. - Defining a signature-specific :py:
createmethod. The default implementation used in anonymous signatures, :meth:Signature.create, returns a new instance of :class:PureInterface. Whenever the custom signature has a corresponding custom interface object class, this method should return a new instance of that class. It should not have any required arguments beyond the ones that :meth:Signature.createhas (required parameters should be provided when creating the signature and not the interface), but may take additional optional arguments, forwarding them to the interface object constructor.
.. doctest::
>>> sig32 = SimpleBusSignature(); sig32
SimpleBusSignature(32)
>>> sig24 = SimpleBusSignature(24); sig24
SimpleBusSignature(24)
>>> sig24.addr_width
24
>>> sig24 == SimpleBusSignature(24)
True
>>> bus = sig24.create(); bus
<SimpleBusInterface: SimpleBusSignature(24), en=(sig bus__en), rw=EnumView(TransferType, (sig bus__rw)), addr=(sig bus__addr), r_data=(sig bus__r_data), w_data=(sig bus__w_data)>
>>> bus.is_read_xfer()
(& (sig bus__en) (== (sig bus__rw) (const 1'd1)))The custom properties defined for both the signature and the interface object can be used on the flipped signature and the flipped interface in the usual way:
.. doctest::
>>> sig32.flip().addr_width
32
>>> wiring.flipped(bus).is_read_xfer()
(& (sig bus__en) (== (sig bus__rw) (const 1'd1))).. note::
Unusually for Python, when the implementation of a property or method is invoked through a flipped object, the :py:`self` argument receives the flipped object that has the type :class:`FlippedSignature` or :class:`FlippedInterface`. This wrapper object proxies all attribute accesses and method calls to the original signature or interface, the only change being that of the data flow directions. See the documentation for these classes for a more detailed explanation... warning::
While the wrapper object forwards attribute accesses and method calls, it does not currently proxy special methods such as :py:`__getitem__` or :py:`__add__` that are rarely, if ever, used with interface objects. This limitation may be lifted in the future... _wiring-path:
Paths
+++++
Whenever an operation in this module needs to refer to the interior of an object, it accepts or produces a path: a tuple of strings and integers denoting the attribute names and indexes through which an interior value can be extracted. For example, the path :py:("buses", 0, "cyc") into the object :py:obj corresponds to the Python expression :py:obj.buses[0].cyc.
When they appear in diagnostics, paths are printed as the corresponding Python expression.
Signatures
.. autoclass:: Flow()
:no-members:
.. autoattribute:: Out
:no-value:
.. autoattribute:: In
:no-value:
.. automethod:: flip
.. automethod:: call
.. autodata:: Out
.. autodata:: In
.. autoclass:: Member(flow, description, *, init=None)
.. autoexception:: SignatureError
.. autoclass:: SignatureMembers
.. autoclass:: FlippedSignatureMembers
:no-members:
.. automethod:: flip
.. autoclass:: Signature
.. autoclass:: FlippedSignature(unflipped)
:no-members:
.. automethod:: flip
.. automethod:: getattr
.. automethod:: setattr
.. automethod:: delattr
.. autoclass:: SignatureMeta
Interfaces
.. autoclass:: PureInterface
.. autoclass:: FlippedInterface(unflipped)
.. autofunction:: flipped
Making connections
.. autoexception:: ConnectionError
.. autofunction:: connect
Components
.. _JSON Schema: https://json-schema.org
.. autoclass:: Component
Component metadata
.. autoexception:: InvalidMetadata
.. autoclass:: ComponentMetadata
:no-members:
:members: validate, origin, as_json
.. autoattribute:: schema
:annotation: = { "$id": "https://amaranth-lang.org/schema/amaranth/0.5/component.json", ... }
Stdlib Crc Catalog (`docs/stdlib/crc/catalog.rst`)
Algorithm catalog
#################
.. automodule:: amaranth.lib.crc.catalog
--- METRICS ---
- Files Extracted: 31
- Estimated Token Budget: ~67297 tokens
- Canonical Reference: https://codewiki.google/github.com/amaranth-lang/amaranth