Input/output buffers

Theamaranth.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 providescore I/O values that designate connections to external devices, andI/O buffer instances 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. Theamaranth.lib.io module builds on top of it to providelibrary I/O ports that specialize and annotate I/O values, andbuffer components that connect ports to logic.

Note

Unfortunately, the terminology related to I/O has several ambiguities:

Amaranth documentation always uses the least ambiguous form of these terms.

Examples

All of the following examples assume that one of the built-in FPGA platforms is used.

fromamaranth.simimportSimulator,Periodfromamaranth.libimportio,wiring,streamfromamaranth.lib.wiringimportIn,Out

LED output

In this example, a library I/O port for a LED is requested from the platform and driven to blink the LED:

classToplevel(Elaboratable):defelaborate(self,platform):m=Module()delay=Signal(24)state=Signal()withm.If(delay==0):m.d.sync+=delay.eq(~0)m.d.sync+=state.eq(~state)withm.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)returnm

Clock input

In this example, a clock domain is created and driven from an external clock source:

classToplevel(Elaboratable):defelaborate(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)...returnm

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:

classToplevel(Elaboratable):defelaborate(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)withm.If(bus_re.i):m.d.comb+=bus_d.oe.eq(1)m.d.comb+=bus_d.o.eq(data)withm.Elif(bus_we.i):m.d.sync+=data.eq(bus_d.i)returnm

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 theprevious cycle. This is calledpipelining and is typical for clocked buses; seeFFBuffer 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 asource-synchronous interface, a clock signal is generated with the same phase as the DDR data signals associated with it:

classSourceSynchronousOutput(wiring.Component):dout:In(16)defelaborate(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:]),]returnm

This component transmitsdout 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 isedge-aligned, meaning that the data edges exactly coincide with the clock edges.

Simulation

The Amaranth simulator,amaranth.sim, cannot simulatecore I/O values orI/O buffer instances as it only operates on unidirectionally driven two-state wires. This module provides a simulation-only library I/O port,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 aSingleEndedPort,DifferentialPort, or a platform-specific library I/O port, while tests will instantiate the component with aSimulationPort. Tests are able to inject inputs into the component usingsim_port.i, capture the outputs of the component viasim_port.o, and ensure that the component is driving the outputs at the appropriate times usingsim_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:

classOutputSerializer(wiring.Component):data:In(stream.Signature(8))def__init__(self,dclk_port,dout_port):self.dclk_port=dclk_portself.dout_port=dout_portsuper().__init__()defelaborate(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))withm.If(self.data.valid):m.d.sync+=dclk.o.eq(~dclk.o)withm.If(dclk.o):m.d.sync+=index.eq(index+1)withm.If(index==7):m.d.comb+=self.data.ready.eq(1)returnmdeftest_output_serializer():dclk_port=io.SimulationPort("o",1)dout_port=io.SimulationPort("o",1)dut=OutputSerializer(dclk_port,dout_port)asyncdeftestbench_write_data(ctx):ctx.set(dut.data.payload,0xA1)ctx.set(dut.data.valid,1)awaitctx.tick().until(dut.data.ready)ctx.set(dut.data.valid,0)asyncdeftestbench_sample_output(ctx):forbitin[1,0,0,0,0,1,0,1]:_,dout_value=awaitctx.posedge(dut.dclk_port.o).sample(dut.dout_port.o)assertctx.get(dut.dout_port.oe)==1,"DUT is not driving the data output"assertdout_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()

Ports

classamaranth.lib.io.Direction

Represents direction of a library I/O port, or of an I/O buffer component.

Input='i'

Input direction (from outside world to Amaranth design).

Output='o'

Output direction (from Amaranth design to outside world).

Bidir='io'

Bidirectional (can be switched between input and output).

__and__(other)

Narrow the set of possible directions.

  • self&self returnsself.

  • Bidir&other returnsother.

  • Input&Output raisesValueError.

classamaranth.lib.io.PortLike

Represents an abstract library I/O port that can be passed to a buffer.

The port types supported by most platforms areSingleEndedPort andDifferentialPort. Platforms may define additional port types where appropriate.

Note

amaranth.hdl.IOPort is not an instance ofamaranth.lib.io.PortLike.

abstractpropertydirection

Direction of the port.

Return type:

Direction

abstract__len__()

Computes the width of the port.

Returns:

The number of wires (for single-ended library I/O ports) or wire pairs (for differentiallibrary I/O ports) this port consists of.

Return type:

int

abstract__getitem__(key)

Slices the port.

Returns:

A newPortLike instance of the same type asself, containing a selectionof wires of this port according tokey. Its width is the same as the length ofthe slice (ifkey is aslice); or 1 (ifkey is anint).

Return type:

PortLike

abstract__invert__()

Inverts polarity of the port.

Inverting polarity of a library I/O port has the same effect as adding inverters tothei ando members of an I/O buffer component for that port.

Returns:

A newPortLike instance of the same type asself, containing the samewires as this port, but with polarity inverted.

Return type:

PortLike

abstract__add__(other)

Concatenates two library I/O ports of the same type.

The direction of the resulting port is:

  • The same as the direction of both, if the two ports have the same direction.

  • Direction.Input if a bidirectional port is concatenated with an input port.

  • Direction.Output if a bidirectional port is concatenated with an output port.

Returns:

A newtype(self) which contains wires fromself followed by wiresfromother, preserving their polarity inversion.

Return type:

type(self)

Raises:
  • ValueError – If an input port is concatenated with an output port.

  • TypeError – Ifself andother have different types.

classamaranth.lib.io.SingleEndedPort(io,*,invert=False,direction=Direction.Bidir)

Represents a single-ended library I/O port.

Implements thePortLike interface.

Parameters:
  • io (IOValue) – Underlying core I/O value.

  • invert (bool or iterable ofbool) – Polarity inversion. If the value is a simplebool, it specifies inversion forthe entire port. If the value is an iterable ofbool, the iterable must have thesame length as the width ofio, and the inversion is specified for individual wires.

  • direction (Direction orstr) – Set of allowed buffer directions. A string is converted to aDirection first.If equal toInput orOutput, this port can only beused with buffers of matching direction. If equal toBidir, this portcan be used with buffers of any direction.

Attributes:
  • io (IOValue) – Theio parameter.

  • invert (tuple ofbool) – Theinvert parameter, normalized to specify polarity inversion per-wire.

  • direction (Direction) – Thedirection parameter, normalized to theDirection enumeration.

classamaranth.lib.io.DifferentialPort(p,n,*,invert=False,direction=Direction.Bidir)

Represents a differential library I/O port.

Implements thePortLike interface.

Parameters:
  • p (IOValue) – Underlying core I/O value for the true (positive) half of the port.

  • n (IOValue) – Underlying core I/O value for the complement (negative) half of the port.Must have the same width asp.

  • invert (bool or iterable ofbool) – Polarity inversion. If the value is a simplebool, it specifies inversion forthe entire port. If the value is an iterable ofbool, the iterable must have thesame length as the width ofp andn, and the inversion is specified forindividual wires.

  • direction (Direction orstr) – Set of allowed buffer directions. A string is converted to aDirection first.If equal toInput orOutput, this port can only beused with buffers of matching direction. If equal toBidir, this portcan be used with buffers of any direction.

Attributes:
  • p (IOValue) – Thep parameter.

  • n (IOValue) – Then parameter.

  • invert (tuple ofbool) – Theinvert parameter, normalized to specify polarity inversion per-wire.

  • direction (Direction) – Thedirection parameter, normalized to theDirection enumeration.

classamaranth.lib.io.SimulationPort(direction,width,*,invert=False,name=None,src_loc_at=0)

Represents a simulation library I/O port.

Implements thePortLike interface.

Parameters:
  • direction (Direction orstr) – Set of allowed buffer directions. A string is converted to aDirection first.If equal toInput orOutput, this port can only beused with buffers of matching direction. If equal toBidir, this portcan be used with buffers of any direction.

  • width (int) – Width of the port. The width of each of the attributesi,o,oe (wheneverpresent) equalswidth.

  • invert (bool or iterable ofbool) – Polarity inversion. If the value is a simplebool, it specifies inversion forthe entire port. If the value is an iterable ofbool, the iterable must have thesame length as the width ofp andn, and the inversion is specified forindividual wires.

  • name (str orNone) – Name of the port. This name is only used to derive the names of the input, output, andoutput enable signals.

  • src_loc_at (int) –Source location. Used to infername if not specified.

Attributes:
  • i (Signal) – Input signal. Present ifdirectionin(Input,Bidir).

  • o (Signal) – Ouptut signal. Present ifdirectionin(Output,Bidir).

  • oe (Signal) – Output enable signal. Present ifdirectionin(Output,Bidir).

  • invert (tuple ofbool) – Theinvert parameter, normalized to specify polarity inversion per-wire.

  • direction (Direction) – Thedirection parameter, normalized to theDirection enumeration.

Buffers

classamaranth.lib.io.Buffer(direction,port)

A combinational I/O buffer component.

This buffer can be used on any platform; if the platform does not specialize its implementation,anI/O buffer instance is used.

The following diagram defines the timing relationship between the underlying core I/O value(for differential ports, the core I/O value of the true half) and thei,o, andoe members:

{'signal': [{'name': 'clk', 'wave': 'p.....'}, {'name': 'o', 'wave': 'x345x.', 'data': ['A', 'B', 'C']}, {'name': 'oe', 'wave': '01..0.'}, {}, {'name': 'port', 'wave': 'z345z.', 'data': ['A', 'B', 'C']}, {}, {'name': 'i', 'wave': 'x345x.', 'data': ['A', 'B', 'C']}], 'config': {'hscale': 2, 'skin': 'default'}}
Parameters:
  • direction (Direction) – Direction of the buffer.

  • port (PortLike) – Port driven by the buffer.

Raises:

ValueError – Unlessport.directionin(direction,Bidir).

Attributes:

signature (Buffer.Signature) –Signature(direction,len(port)).flip().

Platform overrides

Define theget_io_buffer() platform method to override the implementation ofBuffer, e.g. to instantiate library cells directly.

classSignature(direction,width)

Signature of a combinational I/O buffer.

Parameters:
  • direction (Direction) – Direction of the buffer.

  • width (int) – Width of the buffer.

Members:
  • i (In(width)) – Present ifdirectionin(Input,Bidir).

  • o (Out(width)) – Present ifdirectionin(Output,Bidir).

  • oe (Out(1,init=0)) – Present ifdirectionisBidir.

  • oe (Out(1,init=1)) – Present ifdirectionisOutput.

classamaranth.lib.io.FFBuffer(direction,port,*,i_domain=None,o_domain=None)

A registered I/O buffer component.

This buffer can be used on any platform; if the platform does not specialize its implementation,anI/O buffer instance is used, combined with reset-lessregisters oni,o, andoe members.

The following diagram defines the timing relationship between the underlying core I/O value(for differential ports, the core I/O value of the true half) and thei,o, andoe members:

{'signal': [{'name': 'clk', 'wave': 'p......'}, {'name': 'o', 'wave': 'x345x..', 'data': ['A', 'B', 'C']}, {'name': 'oe', 'wave': '01..0..'}, {}, {'name': 'port', 'wave': 'z.345z.', 'data': ['A', 'B', 'C']}, {}, {'name': 'i', 'wave': 'x..345x', 'data': ['A', 'B', 'C']}], 'config': {'hscale': 2, 'skin': 'default'}}

Warning

On some platforms, this buffer can only be used with rising edge clock domains, and willraise an exception during conversion of the design to a netlist otherwise.

This limitation will be lifted in the future.

Parameters:
  • direction (Direction) – Direction of the buffer.

  • port (PortLike) – Port driven by the buffer.

  • i_domain (str) – Name of the input register’s clock domain. Used whendirectionin(Input,Bidir).Defaults to"sync".

  • o_domain (str) – Name of the output and output enable registers’ clock domain. Used whendirectionin(Output,Bidir). Defaults to"sync".

Attributes:

signature (FFBuffer.Signature) –Signature(direction,len(port)).flip().

Platform overrides

Define theget_io_buffer() platform method to override the implementation ofFFBuffer, e.g. to instantiate library cells directly.

classSignature(direction,width)

Signature of a registered I/O buffer.

Parameters:
  • direction (Direction) – Direction of the buffer.

  • width (int) – Width of the buffer.

Members:
  • i (In(width)) – Present ifdirectionin(Input,Bidir).

  • o (Out(width)) – Present ifdirectionin(Output,Bidir).

  • oe (Out(1,init=0)) – Present ifdirectionisBidir.

  • oe (Out(1,init=1)) – Present ifdirectionisOutput.

classamaranth.lib.io.DDRBuffer(direction,port,*,i_domain=None,o_domain=None)

A double data rate I/O buffer component.

This buffer is only available on platforms that support double data rate I/O.

The following diagram defines the timing relationship between the underlying core I/O value(for differential ports, the core I/O value of the true half) and thei,o, andoe members:

{'head': {'tick': 0}, 'signal': [{'name': 'clk', 'wave': 'p.......'}, {'name': 'o[0]', 'wave': 'x357x...', 'node': '.a', 'data': ['A', 'C', 'E']}, {'name': 'o[1]', 'wave': 'x468x...', 'node': '.b', 'data': ['B', 'D', 'F']}, {'name': 'oe', 'wave': '01..0...'}, {'node': '........R.S', 'period': 0.5}, {'name': 'port', 'wave': 'z...345678z.....', 'node': '....123456', 'data': ['A', 'B', 'C', 'D', 'E', 'F'], 'period': 0.5}, {'node': '..P.Q', 'period': 0.5}, {'name': 'i[0]', 'wave': 'x...468x', 'node': '.....d', 'data': ['B', 'D', 'F']}, {'name': 'i[1]', 'wave': 'x..357x.', 'node': '.....e', 'data': ['A', 'C', 'E']}], 'edge': ['a~1', 'b-~2', 'P+Q t1', '5~-d', '6~e', 'R+S t2'], 'config': {'hscale': 2, 'skin': 'default'}}

The output data (labelleda,b) is input fromo into internal registers atthe beginning of clock cycle 2, and transmitted at points labelled1,2 during the sameclock cycle. The output latencyt1 is defined as the amount of cycles between the time ofcapture ofo and the time of transmission of rising edge data plus one cycle, and is 1for this diagram.

The received data is captured into internal registers during the clock cycle 4 at pointslabelled5,6, and output toi during the next clock cycle (labelledd,e).The input latencyt2 is defined as the amount of cycles between the time of reception ofrising edge data and the time of update ofi, and is 1 for this diagram.

The output enable signal is input fromoe once per cycle and affects the entire cycle itapplies to. Its latency is defined in the same way as the output latency, and is equal tot1.

Warning

Some platforms include additional pipeline registers that may cause latenciest1 andt2to be higher than one cycle. At the moment there is no way to query these latencies.

This limitation will be lifted in the future.

Warning

On all supported platforms, this buffer can only be used with rising edge clock domains,and will raise an exception during conversion of the design to a netlist otherwise.

This limitation may be lifted in the future.

Warning

Double data rate I/O buffers are not compatible withSimulationPort.

This limitation may be lifted in the future.

Parameters:
  • direction (Direction) – Direction of the buffer.

  • port (PortLike) – Port driven by the buffer.

  • i_domain (str) – Name of the input register’s clock domain. Used whendirectionin(Input,Bidir).Defaults to"sync".

  • o_domain (str) – Name of the output and output enable registers’ clock domain. Used whendirectionin(Output,Bidir). Defaults to"sync".

Attributes:

signature (DDRBuffer.Signature) –Signature(direction,len(port)).flip().

Platform overrides

Define theget_io_buffer() platform method to override the implementation ofDDRBuffer, e.g. to instantiate library cells directly.

classSignature(direction,width)

Signature of a double data rate I/O buffer.

Parameters:
  • direction (Direction) – Direction of the buffer.

  • width (int) – Width of the buffer.

Members:
  • i (In(ArrayLayout(width,2))) – Present ifdirectionin(Input,Bidir).

  • o (Out(ArrayLayout(width,2))) – Present ifdirectionin(Output,Bidir).

  • oe (Out(1,init=0)) – Present ifdirectionisBidir.

  • oe (Out(1,init=1)) – Present ifdirectionisOutput.