diff --git a/docs/manual/src/applets/interface/ethernet_rgmii.rst b/docs/manual/src/applets/interface/ethernet_rgmii.rst new file mode 100644 index 000000000..00a6dbe27 --- /dev/null +++ b/docs/manual/src/applets/interface/ethernet_rgmii.rst @@ -0,0 +1,7 @@ +``ethernet-rgmii`` +================== + +.. _applet.interface.ethernet.rgmii: + +.. autoprogram:: glasgow.applet.interface.ethernet.rgmii:EthernetRGMIIApplet._get_argparser_for_sphinx("ethernet-rgmii") + :prog: glasgow run ethernet-rgmii diff --git a/docs/manual/src/applets/interface/index.rst b/docs/manual/src/applets/interface/index.rst index 0d7626367..ddd4b0f2c 100644 --- a/docs/manual/src/applets/interface/index.rst +++ b/docs/manual/src/applets/interface/index.rst @@ -20,3 +20,5 @@ I/O interfaces jtag_svf sbw_probe swd_probe + ethernet_rgmii + probe_rs diff --git a/software/glasgow/applet/control/mdio/__init__.py b/software/glasgow/applet/control/mdio/__init__.py index 4b6f6fa09..9c90d208c 100644 --- a/software/glasgow/applet/control/mdio/__init__.py +++ b/software/glasgow/applet/control/mdio/__init__.py @@ -96,7 +96,10 @@ class ControlMDIOInterface: def __init__(self, logger: logging.Logger, assembly: AbstractAssembly, *, mdc: GlasgowPin, mdio: GlasgowPin): self._logger = logger - self._level = logging.DEBUG if self._logger.name == __name__ else logging.TRACE + if self._logger.name == __name__ or "interface.ethernet." in self._logger.name: + self._level = logging.DEBUG + else: + self._level = logging.TRACE assembly.use_pulls({mdio: "low"}) ports = assembly.add_port_group(mdc=mdc, mdio=mdio) diff --git a/software/glasgow/applet/interface/ethernet/__init__.py b/software/glasgow/applet/interface/ethernet/__init__.py new file mode 100644 index 000000000..0dbd78ccf --- /dev/null +++ b/software/glasgow/applet/interface/ethernet/__init__.py @@ -0,0 +1,224 @@ +# Ref: IEEE Std 802.3-2018 +# Accession: G00098 + +from typing import BinaryIO +from collections.abc import AsyncIterator +import time +import logging +import asyncio +import argparse + +from amaranth import * +from amaranth.lib import wiring, stream +from amaranth.lib.wiring import In, Out +from amaranth.lib.crc.catalog import CRC32_ETHERNET + +from glasgow.support.logging import dump_hex +from glasgow.support.os_network import OSNetworkInterface +from glasgow.arch.ieee802_3 import * +from glasgow.gateware import cobs, ethernet +from glasgow.protocol import snoop +from glasgow.abstract import AbstractAssembly +from glasgow.applet import GlasgowAppletV2 +from glasgow.applet.control.mdio import ControlMDIOInterface + + +__all__ = ["EthernetComponent", "AbstractEthernetApplet"] + + +class EthernetComponent(wiring.Component): + i_stream: In(stream.Signature(8)) + o_stream: Out(stream.Signature(8)) + + rx_bypass: In(1) + tx_bypass: In(1) + + def __init__(self, driver): + self._driver = driver + + super().__init__() + + def elaborate(self, platform): + m = Module() + + m.submodules.tx_decoder = tx_decoder = cobs.Decoder() + wiring.connect(m, tx_decoder.i, wiring.flipped(self.i_stream)) + + m.submodules.ctrl = ctrl = ethernet.Controller(self._driver) + m.d.comb += ctrl.rx_bypass.eq(self.rx_bypass) + m.d.comb += ctrl.tx_bypass.eq(self.tx_bypass) + wiring.connect(m, ctrl.i, tx_decoder.o) + + m.submodules.rx_encoder = rx_encoder = cobs.Encoder(fifo_depth=2048) + wiring.connect(m, rx_encoder.i, ctrl.o) + + wiring.connect(m, wiring.flipped(self.o_stream), rx_encoder.o) + + return m + + +class AbstractEthernetInterface: + def __init__(self, logger: logging.Logger, assembly: AbstractAssembly, *, + driver: ethernet.AbstractDriver): + self._logger = logger + self._level = logging.DEBUG if self._logger.name.startswith(__name__) else logging.TRACE + + component = assembly.add_submodule(EthernetComponent(driver)) + self._pipe = assembly.add_inout_pipe( + component.o_stream, component.i_stream, + in_fifo_depth=0, out_buffer_size=512 * 128) + self._rx_bypass = assembly.add_rw_register(component.rx_bypass) + self._tx_bypass = assembly.add_rw_register(component.tx_bypass) + + self._snoop: snoop.SnoopWriter | None = None + + def _log(self, message: str, *args): + self._logger.log(self._level, "Ethernet: " + message, *args) + + @property + def snoop_file(self) -> BinaryIO | None: + if self._snoop is not None: + return self._snoop.file + return None + + @snoop_file.setter + def snoop_file(self, snoop_file): + if snoop_file is not None: + self._snoop = snoop.SnoopWriter(snoop_file, + datalink_type=snoop.SnoopDatalinkType.Ethernet) + else: + self._snoop = None + + def _snoop_packet(self, packet): + if self._snoop is not None: + self._snoop.write(snoop.SnoopPacket(packet, timestamp_ns=time.time_ns())) + + async def send(self, packet: bytes | bytearray | memoryview) -> bool: + cobs_packet = cobs.encode(packet) + b"\x00" + if self._pipe.writable is None or len(cobs_packet) <= self._pipe.writable: + self._log("tx data=<%s>", dump_hex(packet)) + self._snoop_packet(packet) + await self._pipe.send(cobs_packet) + await self._pipe.flush(_wait=False) + return True + else: + self._logger.warning("tx drop") + return False + + async def recv(self) -> bytes: + packet = cobs.decode((await self._pipe.recv_until(b"\x00"))[:-1]) + self._log("rx data=<%s> len=%d", dump_hex(packet), len(packet)) + self._snoop_packet(packet) + return packet + + async def iter_recv(self) -> AsyncIterator[bytes]: + while True: + yield await self.recv() + + +class AbstractEthernetApplet(GlasgowAppletV2): + logger = logging.getLogger(__name__) + help = "send and receive Ethernet packets" + description = """ + Communicate with an Ethernet network using a PHY connected via the $PHYIF$ interface. + + The `bridge` operation is supported only on Linux. To create a suitable TAP interface, run: + + :: + + sudo ip tuntap add glasgow0 mode tap user $USER + sudo ip link set glasgow0 up + """ + required_revision = "C0" + eth_iface: AbstractEthernetInterface + mdio_iface: ControlMDIOInterface + + @classmethod + def add_setup_arguments(cls, parser): + parser.add_argument("--snoop", dest="snoop_file", type=argparse.FileType("wb"), + metavar="SNOOP-FILE", help="save packets to a file in RFC 1761 format") + + async def setup(self, args): + self.eth_iface.snoop_file = args.snoop_file + + @classmethod + def add_run_arguments(cls, parser): + p_operation = parser.add_subparsers(dest="operation", metavar="OPERATION", required=True) + + p_bridge = p_operation.add_parser( + "bridge", help="bridge network to the host OS") + p_bridge.add_argument( + "interface", metavar="INTERFACE", nargs="?", type=str, default="glasgow0", + help="forward packets to and from this TAP interface (default: %(default)s)") + + p_loopback = p_operation.add_parser( + "loopback", help="test connection to PHY using near-end loopback") + p_loopback.add_argument( + "--delay", "-d", metavar="DELAY", type=float, default=1.0, + help="wait for DELAY seconds between sending packets (default: %(default)s)") + + async def run(self, args): + if args.operation == "bridge": + os_iface = OSNetworkInterface(args.interface) + + async def forward_rx(): + async for packet in self.eth_iface.iter_recv(): + if len(packet) >= 14: # must be at least ETH_HLEN, or we'll get EINVAL on Linux + await os_iface.send([packet]) + + async def forward_tx(): + while True: + for packet in await os_iface.recv(): + if not await self.eth_iface.send(packet): + break + + async with asyncio.TaskGroup() as group: + group.create_task(forward_rx()) + group.create_task(forward_tx()) + + if args.operation == "loopback": + # Enable near-end loopback. + basic_control = REG_BASIC_CONTROL.from_int( + await self.mdio_iface.c22_read(0, REG_BASIC_CONTROL_addr)) + basic_control.LOOPBACK = 1 + await self.mdio_iface.c22_write(0, REG_BASIC_CONTROL_addr, basic_control.to_int()) + + # Accept all packets, even those with CRC errors. + await self.eth_iface._rx_bypass.set(True) + + count_ok = 0 + count_bad = 0 + count_lost = 0 + try: + packet_data = bytes(range(256)) + packet_fcs = CRC32_ETHERNET().compute(packet_data).to_bytes(4, "little") + packet_full = packet_data + packet_fcs + while True: + await self.eth_iface.send(packet_data) + try: + async with asyncio.timeout(args.delay): + packet_recv = await self.eth_iface.recv() + if packet_recv == packet_full: + self.logger.info("packet ok") + count_ok += 1 + else: + if len(packet_recv) < len(packet_full): + self.logger.warning("packet bad (short)") + elif len(packet_recv) > len(packet_full): + self.logger.warning("packet bad (long)") + elif packet_recv[:len(packet_data)] != packet_data: + self.logger.warning("packet bad (data)") + else: + self.logger.warning("packet bad (crc)") + count_bad += 1 + await asyncio.sleep(args.delay) + except TimeoutError: + self.logger.warning("packet lost") + count_lost += 1 + finally: + count_all = count_ok + count_bad + count_lost + if count_all: + self.logger.info(f"statistics: " + f"ok {count_ok}/{count_all} ({count_ok/count_all*100:.0f}%), " + f"bad {count_bad}/{count_all} ({count_bad/count_all*100:.0f}%), " + f"lost {count_lost}/{count_all} ({count_lost/count_all*100:.0f}%)") diff --git a/software/glasgow/applet/interface/ethernet/rgmii/__init__.py b/software/glasgow/applet/interface/ethernet/rgmii/__init__.py new file mode 100644 index 000000000..f1bea4a84 --- /dev/null +++ b/software/glasgow/applet/interface/ethernet/rgmii/__init__.py @@ -0,0 +1,165 @@ +# Ref: IEEE Std 802.3-2018 +# Accession: G00098 +# Ref: Reduced Gigabit Media Independent Interface (RGMII) Version 1.3 +# Accession: G00099 + +import logging + +from amaranth import * +from amaranth.lib import io + +from glasgow.arch.ieee802_3 import * +from glasgow.gateware.ethernet import AbstractDriver +from glasgow.gateware.iostream import StreamIOBuffer +from glasgow.gateware.ports import PortGroup +from glasgow.gateware.iodelay import IODelay +from glasgow.abstract import AbstractAssembly, GlasgowPin +from glasgow.applet.interface.ethernet import AbstractEthernetInterface, AbstractEthernetApplet +from glasgow.applet.control.mdio import ControlMDIOInterface + + +__all__ = ["EthernetRGMIIDriver", "EthernetRGMIIInterface"] + + +class EthernetRGMIIDriver(AbstractDriver): + def __init__(self, ports, *, rx_delay: int, tx_delay: int): + self._ports = ports + + self._rx_delay = rx_delay + self._tx_delay = tx_delay + + super().__init__() + + def elaborate(self, platform): + m = Module() + + # The RGMII interface is source-synchronous with separate clocks for the receive and + # transmit halves. We use RX_CLK (recovered receive clock) as the driver clock and + # retransmit it as TX_CLK; the driver itself only uses a single `mac` clock domain. + + m.submodules.rx_clk_buffer = rx_clk_buffer = io.Buffer("i", self._ports.rx_clk) + m.submodules.rx_clk_delay = IODelay( + rx_clk_buffer.i, ClockSignal("mac"), length=self._rx_delay) + if platform is not None: + # TODO: does not pass timing at 125 MHz + # platform.add_clock_constraint(phy_clk, 125e6) + platform.add_clock_constraint(rx_clk_buffer.i, 50e6) + + m.submodules.tx_clk_buffer = tx_clk_buffer = io.Buffer("o", self._ports.tx_clk) + m.submodules.tx_clk_delay = IODelay( + ClockSignal("mac"), tx_clk_buffer.o, length=self._tx_delay) + + m.submodules.buffer = buffer = StreamIOBuffer(PortGroup( + tx_ctl =self._ports.tx_ctl .with_direction("o"), + tx_data=self._ports.tx_data.with_direction("o"), + # These are outputs, but configuring them as I/O avoids issues with IOB clock + # constraints on iCE40. + rx_ctl =self._ports.rx_ctl .with_direction("io"), + rx_data=self._ports.rx_data.with_direction("io"), + ), ratio=2, o_domain="mac", i_domain="mac") + m.d.comb += buffer.i.p.port.tx_ctl.oe.eq(Cat(1, 1)) + m.d.comb += buffer.i.p.port.tx_data.oe.eq(Cat(1, 1)) + + tx_offset = Signal() + m.d.mac += buffer.i.p.port.tx_ctl.o.eq( + (self.i.valid & ~self.i.p.end).replicate(2)) + m.d.mac += buffer.i.p.port.tx_data.o.eq( + self.i.p.data.word_select(tx_offset, 4).replicate(2)) + with m.If(self.i.valid): + m.d.mac += tx_offset.eq(tx_offset + 1) + m.d.comb += self.i.ready.eq(tx_offset == 1) + + rx_data = Signal(8) + rx_valid = Signal(2) + # posedge: rx_dv, negedge: rx_dv xor rx_err; ignore rx_err for the time being + m.d.mac += rx_data.eq(Cat(rx_data[4:], buffer.o.p.port.rx_data.i[0])) + m.d.mac += rx_valid.eq(Cat(rx_valid[1:], buffer.o.p.port.rx_ctl.i[0])) + m.d.mac += self.o.p.data.eq(rx_data) + m.d.mac += self.o.p.end.eq(~rx_valid.all()) + + with m.FSM(domain="mac"): + m.d.mac += self.o.valid.eq(~self.o.valid) + + with m.State("10/100-Sync"): + with m.If(rx_valid.all() & (rx_data == 0xd5)): + m.d.mac += self.o.valid.eq(1) + m.next = "10/100-Data" + + with m.State("10/100-Data"): + with m.If(~rx_valid.all()): + m.next = "10/100-Sync" + + return m + + +class EthernetRGMIIInterface(AbstractEthernetInterface): + def __init__(self, logger: logging.Logger, assembly: AbstractAssembly, *, + rx_clk: GlasgowPin, rx_ctl: GlasgowPin, rx_data: GlasgowPin, rx_delay: int, + tx_clk: GlasgowPin, tx_ctl: GlasgowPin, tx_data: GlasgowPin, tx_delay: int): + ports = assembly.add_port_group( + rx_clk=rx_clk, rx_ctl=rx_ctl, rx_data=rx_data, + tx_clk=tx_clk, tx_ctl=tx_ctl, tx_data=tx_data) + super().__init__(logger, assembly, + driver=EthernetRGMIIDriver(ports, rx_delay=rx_delay, tx_delay=tx_delay)) + + +class EthernetRGMIIApplet(AbstractEthernetApplet): + logger = logging.getLogger(__name__) + help = AbstractEthernetApplet.help + " via RGMII" + preview = True + description = AbstractEthernetApplet.description.replace("$PHYIF$", "RGMII") + """ + + RGMII supports three modes: 10/100/1000 Mbps. This applet currently supports only the 100 Mbps + mode (it disables autonegotiation), and extending it to support 1000 Mbps would be nontrivial. + + RGMII requires either the MAC or the PHY to delay the receive and transmit clocks to ensure + sufficient setup and hold time. The default ``--tx-delay`` and ``--rx-delay`` values might be + sufficient; if not, calibrate delays in range of 0..16 until there is no packet corruption or + loss. Delay value of 4 stages corresponds to roughly 2.5 ns on iCE40; for the transmit path + this corresponds to the measured data-to-clock phase offset, while for the receive path this + delay shifts the capture window of FPGA input buffers. Note that the stages to nanoseconds + relationship is not linear. + """ + required_revision = "C0" + + @classmethod + def add_build_arguments(cls, parser, access): + access.add_voltage_argument(parser) + access.add_pins_argument(parser, "rx_clk", default=True, required=True) + access.add_pins_argument(parser, "rx_ctl", default=True, required=True) + access.add_pins_argument(parser, "rx_data", default=True, required=True, width=4) + access.add_pins_argument(parser, "tx_clk", default=True, required=True) + access.add_pins_argument(parser, "tx_ctl", default=True, required=True) + access.add_pins_argument(parser, "tx_data", default=True, required=True, width=4) + access.add_pins_argument(parser, "mdc", default=True, required=True) + access.add_pins_argument(parser, "mdio", default=True, required=True) + + parser.add_argument( + "--rx-delay", metavar="STAGES", type=int, default=0, + help="clock delay for the receive path (default: %(default)s)") + parser.add_argument( + "--tx-delay", metavar="STAGES", type=int, default=4, + help="clock delay for the trasnmit path (default: %(default)s)") + + def build(self, args): + with self.assembly.add_applet(self): + self.assembly.use_voltage(args.voltage) + self.eth_iface = EthernetRGMIIInterface(self.logger, self.assembly, + rx_clk=args.rx_clk, rx_ctl=args.rx_ctl, rx_data=args.rx_data, + tx_clk=args.tx_clk, tx_ctl=args.tx_ctl, tx_data=args.tx_data, + rx_delay=args.rx_delay, tx_delay=args.tx_delay) + self.mdio_iface = ControlMDIOInterface(self.logger, self.assembly, + mdc=args.mdc, mdio=args.mdio) + + async def setup(self, args): + await super().setup(args) + await self.mdio_iface.clock.set_frequency(1e6) + + # Configure for 100 Mbps full duplex, no autonegotiation + await self.mdio_iface.c22_write(0, REG_BASIC_CONTROL_addr, + REG_BASIC_CONTROL(DUPLEXMD=1, SPD_SEL_0=1).to_int()) + + @classmethod + def tests(cls): + from . import test + return test.EthernetRGMIIAppletTestCase diff --git a/software/glasgow/applet/interface/ethernet/rgmii/test.py b/software/glasgow/applet/interface/ethernet/rgmii/test.py new file mode 100644 index 000000000..6316d2240 --- /dev/null +++ b/software/glasgow/applet/interface/ethernet/rgmii/test.py @@ -0,0 +1,35 @@ +from glasgow.applet import GlasgowAppletV2TestCase, synthesis_test, applet_v2_simulation_test + +from . import EthernetRGMIIApplet + + +class EthernetRGMIIAppletTestCase(GlasgowAppletV2TestCase, applet=EthernetRGMIIApplet): + @synthesis_test + def test_build(self): + self.assertBuilds() + + def prepare_loopback(self, assembly): + a0 = assembly.get_pin("A0") + async def tb_phy_clk(ctx): + while True: + ctx.set(a0.i, ~ctx.get(a0.i)) + await ctx.delay(1e-7) + assembly.add_testbench(tb_phy_clk, background=True) + + assembly.connect_pins("A1", "A7") + assembly.connect_pins("A2", "B0") + assembly.connect_pins("A3", "B1") + assembly.connect_pins("A4", "B2") + assembly.connect_pins("A5", "B3") + + @applet_v2_simulation_test(prepare=prepare_loopback) + async def test_loopback(self, applet, ctx): + await applet.eth_iface.send(bytes.fromhex(""" + ffffffffffffccd9ac6b18130806 + 0001080006040001ccd9ac6b1813c0a800d6000000000000c0a8007c + """)) + assert await applet.eth_iface.recv() == bytes.fromhex(""" + ffffffffffffccd9ac6b18130806 + 0001080006040001ccd9ac6b1813c0a800d6000000000000c0a8007c + 000000000000000000000000000000000000 + """) diff --git a/software/glasgow/gateware/ethernet.py b/software/glasgow/gateware/ethernet.py new file mode 100644 index 000000000..cc4145933 --- /dev/null +++ b/software/glasgow/gateware/ethernet.py @@ -0,0 +1,242 @@ +from amaranth import * +from amaranth.lib import data, wiring, stream +from amaranth.lib.wiring import In, Out +from amaranth.lib.crc.catalog import CRC32_ETHERNET + +from glasgow.gateware.stream import AsyncQueue, PacketQueue, PacketExtender +from glasgow.gateware.crc import ChecksumAppender, ChecksumVerifier + + +__all__ = [ + "AbstractDriver", "LoopbackDriver", + "Enframer", "Deframer", "Controller", +] + + +class AbstractDriver(wiring.Component): + def __init__(self, signature={}): + self.cd_mac = ClockDomain() + + super().__init__({ + "i": In(stream.Signature(data.StructLayout({ + "data": 8, + "end": 1, + }))), + "o": Out(stream.Signature(data.StructLayout({ + "data": 8, + "end": 1, + }), always_ready=True)), + **signature + }) + + +class LoopbackDriver(AbstractDriver): + def elaborate(self, platform): + m = Module() + + m.d.comb += self.cd_mac.clk.eq(ClockSignal()) + m.d.comb += self.cd_mac.rst.eq(ResetSignal()) + + wiring.connect(m, wiring.flipped(self.i), wiring.flipped(self.o)) + + return m + + +class Enframer(wiring.Component): + i: In(stream.Signature(data.StructLayout({ + "data": 8, + "first": 1, + "last": 1, + }))) + o: Out(stream.Signature(data.StructLayout({ + "data": 8, + "end": 1, + }))) + + def elaborate(self, platform): + m = Module() + + count = Signal(4) + with m.FSM(): + with m.State("Preamble"): + with m.If(self.i.valid): + m.d.comb += self.o.valid.eq(1) + m.d.comb += self.o.p.data.eq(0x55) + with m.If(self.o.ready): + m.d.sync += count.eq(count + 1) + with m.If(count == 6): + m.d.sync += count.eq(0) + m.next = "Start Delimiter" + + with m.State("Start Delimiter"): + m.d.comb += self.o.valid.eq(1) + m.d.comb += self.o.p.data.eq(0xd5) + with m.If(self.o.ready): + m.d.sync += count.eq(0) + m.next = "Frame Data" + + with m.State("Frame Data"): + m.d.comb += self.o.valid.eq(1) + m.d.comb += self.o.p.data.eq(self.i.p.data) + with m.If(self.o.ready): + m.d.comb += self.i.ready.eq(1) + with m.If(~self.i.valid): + m.next = "Underflow" + with m.Elif(self.i.p.last): + m.next = "Interpacket Gap" + + with m.State(f"Interpacket Gap"): + m.d.comb += self.o.valid.eq(1) + m.d.comb += self.o.p.end.eq(1) + with m.If(self.o.ready): + m.d.sync += count.eq(count + 1) + with m.If(count == 11): + m.d.sync += count.eq(0) + m.next = "Preamble" + + with m.State("Underflow"): + pass # should never happen + + return m + + +class Deframer(wiring.Component): + i: In(stream.Signature(data.StructLayout({ + "data": 8, + "end": 1, + }), always_ready=True)) + o: Out(stream.Signature(data.StructLayout({ + "data": 8, + "first": 1, + "last": 1, + "end": 1, + }))) + + def elaborate(self, platform): + m = Module() + + m.d.comb += self.o.p.data.eq(self.i.p.data) + + with m.FSM(): + with m.State("Idle"): + with m.If(self.i.valid): + with m.If(self.i.p.end): + m.next = "Preamble" + + with m.State("Preamble"): + with m.If(self.i.valid): + with m.If(~self.i.p.end & (self.i.p.data == 0xd5)): + m.d.sync += self.o.p.first.eq(1) + m.next = "Frame Data" + + with m.State("Frame Data"): + with m.If(self.i.valid): + m.d.sync += self.o.p.first.eq(0) + m.d.comb += self.o.p.end.eq(self.i.p.end) + m.d.comb += self.o.valid.eq(1) + with m.If(~self.o.ready): + m.next = "Idle" # discard the rest of the packet + with m.Elif(self.i.p.end): + m.next = "Preamble" + + return m + + +class Controller(wiring.Component): + i: In(stream.Signature(data.StructLayout({ + "data": 8, + "end": 1, + }))) + o: Out(stream.Signature(data.StructLayout({ + "data": 8, + "end": 1, + }))) + + tx_bypass: In(1) + rx_bypass: In(1) + + def __init__(self, driver): + self.driver = driver + + super().__init__() + + def elaborate(self, platform): + m = Module() + + m.domains.mac = self.driver.cd_mac + + m.submodules.tx_cdc_fifo = tx_cdc_fifo = AsyncQueue( + shape=self.i.p.shape(), depth=8, o_domain="mac") + wiring.connect(m, tx_cdc_fifo.i, wiring.flipped(self.i)) + + m.submodules.tx_queue = tx_queue = DomainRenamer("mac")( + PacketQueue(8, data_depth=2048, size_depth=32)) + m.d.comb += [ + tx_queue.i.valid.eq(tx_cdc_fifo.o.valid), + tx_queue.i.p.data.eq(tx_cdc_fifo.o.p.data), + tx_queue.i.p.end.eq(tx_cdc_fifo.o.p.end), + tx_cdc_fifo.o.ready.eq(tx_queue.i.ready), + ] + + m.submodules.tx_padder = tx_padder = DomainRenamer("mac")( + PacketExtender(8, min_length=60, padding=0x00)) + with m.If(~self.tx_bypass): + wiring.connect(m, tx_padder.i, tx_queue.o) + + m.submodules.fcs_appender = fcs_appender = DomainRenamer("mac")( + ChecksumAppender(CRC32_ETHERNET)) + wiring.connect(m, fcs_appender.i, tx_padder.o) + + m.submodules.enframer = enframer = DomainRenamer("mac")(Enframer()) + with m.If(~self.tx_bypass): + wiring.connect(m, enframer.i, fcs_appender.o) + with m.Else(): + wiring.connect(m, enframer.i, tx_queue.o) + + m.submodules.driver = driver = self.driver + wiring.connect(m, driver.i, enframer.o) + + m.submodules.deframer = deframer = DomainRenamer("mac")(Deframer()) + wiring.connect(m, deframer.i, driver.o) + + m.submodules.fcs_verifier = fcs_verifier = DomainRenamer("mac")( + ChecksumVerifier(CRC32_ETHERNET)) + with m.If(~self.rx_bypass): + wiring.connect(m, fcs_verifier.i, deframer.o) + + m.submodules.rx_queue = rx_queue = DomainRenamer("mac")( + PacketQueue(8, data_depth=2048, size_depth=32)) + with m.If(~self.rx_bypass): + m.d.comb += [ + rx_queue.i.valid.eq(fcs_verifier.o.valid), + rx_queue.i.p.data.eq(fcs_verifier.o.p.data), + rx_queue.i.p.first.eq(fcs_verifier.o.p.first), + rx_queue.i.p.last.eq(fcs_verifier.o.p.last), + fcs_verifier.o.ready.eq(rx_queue.i.valid), + ] + with m.Else(): + wiring.connect(m, rx_queue.i, deframer.o) + + m.submodules.rx_cdc_fifo = rx_cdc_fifo = AsyncQueue( + shape=rx_queue.o.p.shape(), depth=8, i_domain="mac") + wiring.connect(m, rx_cdc_fifo.i, rx_queue.o) + + with m.FSM(): + with m.State("Data"): + m.d.comb += [ + self.o.valid.eq(rx_cdc_fifo.o.valid), + self.o.p.data.eq(rx_cdc_fifo.o.p.data), + rx_cdc_fifo.o.ready.eq(self.o.ready), + ] + with m.If(rx_cdc_fifo.o.valid & rx_cdc_fifo.o.ready & rx_cdc_fifo.o.p.last): + m.next = "End" + + with m.State("End"): + m.d.comb += [ + self.o.valid.eq(1), + self.o.p.end.eq(1), + ] + with m.If(self.o.ready): + m.next = "Data" + + return m diff --git a/software/glasgow/gateware/iodelay.py b/software/glasgow/gateware/iodelay.py new file mode 100644 index 000000000..1c6e07a93 --- /dev/null +++ b/software/glasgow/gateware/iodelay.py @@ -0,0 +1,32 @@ +from amaranth import * + + +__all__ = ["IODelay"] + + +class IODelay(Elaboratable): + """Very hacky replacement for proper I/O delay blocks for iCE40. Works surprisingly well.""" + + def __init__(self, i, o, *, length=8): # approx. 5 ns by default + self._i = i + self._o = o + self._length = length + + def elaborate(self, platform): + m = Module() + + i = o = self._i + for n in range(self._length): + o = Signal() + m.submodules[f"stage_{n}"] = Instance("SB_LUT4", + a_keep=1, + p_LUT_INIT=C(0b01, 16), + i_I0=i, + i_I1=C(0), + i_I2=C(0), + i_I3=C(0), + o_O=o) + i = o + m.d.comb += self._o.eq(o) + + return m diff --git a/software/glasgow/gateware/iostream.py b/software/glasgow/gateware/iostream.py index fbed05631..18db99628 100644 --- a/software/glasgow/gateware/iostream.py +++ b/software/glasgow/gateware/iostream.py @@ -49,10 +49,15 @@ def elaborate(self, platform): class StreamIOBuffer(wiring.Component): - def __init__(self, ports, *, ratio, offset, meta_layout): - self._ports = ports - self._ratio = ratio - self._offset = offset + def __init__(self, ports, *, ratio=1, offset=0, meta_layout=0, + i_domain="sync", o_domain="sync"): + assert i_domain == o_domain or meta_layout == 0 + + self._ports = ports + self._ratio = ratio + self._offset = offset + self._i_domain = i_domain + self._o_domain = o_domain super().__init__({ "i": In(stream.Signature(data.StructLayout({ @@ -98,7 +103,13 @@ def elaborate(self, platform): case 2: buffer_cls = SimulatableDDRBuffer for name, port in self._ports: - m.submodules[name] = buffer = buffer_cls(port.direction, port) + i_domain = o_domain = None + if port.direction in (io.Direction.Input, io.Direction.Bidir): + i_domain = self._i_domain + if port.direction in (io.Direction.Output, io.Direction.Bidir): + o_domain = self._o_domain + m.submodules[name] = buffer = buffer_cls(port.direction, port, + i_domain=i_domain, o_domain=o_domain) if port.direction in (io.Direction.Output, io.Direction.Bidir): m.d.comb += buffer.o.eq(self.i.p.port[name].o) m.d.comb += buffer.oe.eq(self.i.p.port[name].oe) @@ -109,7 +120,7 @@ def elaborate(self, platform): case 2, offset if offset % self._ratio == 0: m.d.comb += self.o.p.port[name].i.eq(buffer.i) case 2, offset if offset % self._ratio == 1: - m.d.sync += self.o.p.port[name].i[0].eq(buffer.i[1]) + m.d[self._i_domain] += self.o.p.port[name].i[0].eq(buffer.i[1]) m.d.comb += self.o.p.port[name].i[1].eq(buffer.i[0]) case _, _: raise NotImplementedError( diff --git a/software/glasgow/gateware/stream.py b/software/glasgow/gateware/stream.py index 37d2f5aa7..bd2eac1f7 100644 --- a/software/glasgow/gateware/stream.py +++ b/software/glasgow/gateware/stream.py @@ -1,11 +1,12 @@ from amaranth import * +from amaranth.hdl import ShapeCastable, ValueCastable from amaranth.lib import data, wiring, stream, fifo, memory from amaranth.lib.wiring import In, Out __all__ = [ "stream_put", "stream_get", "stream_assert" - "StreamBuffer", "Queue", "AsyncQueue", "SkidBuffer", + "StreamBuffer", "Queue", "AsyncQueue", "SkidBuffer", "PacketExtender", ] @@ -170,6 +171,7 @@ def __init__(self, data_shape, *, data_depth, size_depth): "data": data_shape, "first": 1, "last": 1, + "end": 1, }))), "o": Out(stream.Signature(data.StructLayout({ "data": data_shape, @@ -196,9 +198,14 @@ def incr(addr, size): write_incr = incr(data_write.addr, self._data_depth) m.d.comb += data_write.data.eq(self.i.p.data) - m.d.comb += size_queue.i.payload.eq(write_count) with m.If(self.i.valid): - with m.If(self.i.p.first & (write_count != 0)): + with m.If(self.i.p.end & (write_count != 0)): + m.d.comb += self.i.ready.eq(size_queue.i.ready) + m.d.comb += size_queue.i.valid.eq(1) + m.d.comb += size_queue.i.payload.eq(write_count - 1) + m.d.sync += write_first.eq(data_write.addr) + m.d.sync += write_count.eq(0) + with m.Elif(self.i.p.first & (write_count != 0)): m.d.sync += data_write.addr.eq(write_first) m.d.sync += write_count.eq(0) with m.Elif(~self.i.p.last | size_queue.i.ready): @@ -207,6 +214,7 @@ def incr(addr, size): m.d.comb += data_write.en.eq(1) with m.If(self.i.p.last): m.d.comb += size_queue.i.valid.eq(1) + m.d.comb += size_queue.i.payload.eq(write_count) m.d.sync += write_first.eq(write_incr) m.d.sync += data_write.addr.eq(write_incr) m.d.sync += write_count.eq(0) @@ -234,3 +242,52 @@ def incr(addr, size): m.d.comb += data_read.addr.eq(read_addr) return m + + +class PacketExtender(wiring.Component): + def __init__(self, data_shape: ShapeCastable, *, min_length: int, padding: ValueCastable): + self._min_length = min_length + self._padding = padding + + super().__init__({ + "i": In(stream.Signature(data.StructLayout({ + "data": data_shape, + "first": 1, + "last": 1, + }))), + "o": Out(stream.Signature(data.StructLayout({ + "data": data_shape, + "first": 1, + "last": 1, + }))), + }) + + def elaborate(self, platform): + m = Module() + + offset = Signal(range(self._min_length + 1)) + + with m.FSM(): + with m.State("Data"): + wiring.connect(m, wiring.flipped(self.o), wiring.flipped(self.i)) + with m.If(self.i.valid & self.i.ready): + with m.If(self.i.p.first): + m.d.sync += offset.eq(0) + with m.Elif(offset < self._min_length): + m.d.sync += offset.eq(offset + 1) + with m.If(self.i.p.last & (offset < self._min_length - 1)): + m.d.comb += self.o.p.last.eq(0) + m.next = "Padding" + + with m.State("Padding"): + m.d.comb += self.o.p.data.eq(self._padding) + m.d.comb += self.o.p.last.eq(offset + 1 == self._min_length - 1) + m.d.comb += self.o.valid.eq(1) + with m.If(self.o.ready): + with m.If(~self.o.p.last): + m.d.sync += offset.eq(offset + 1) + with m.Else(): + m.d.sync += offset.eq(0) + m.next = "Data" + + return m diff --git a/software/glasgow/support/os_network.py b/software/glasgow/support/os_network.py index 22eb3f43e..665dfb1a7 100644 --- a/software/glasgow/support/os_network.py +++ b/software/glasgow/support/os_network.py @@ -45,7 +45,7 @@ async def send(self, packets: list[Buffer]): except BlockingIOError: # write until the buffer is full pass - async def recv(self, *, length=65536) -> list[Buffer]: + async def recv(self, *, length=65536) -> list[bytes]: """"Receive packets. To improve throughput, :meth:`recv` dequeues all available packets. Packets longer than diff --git a/software/pyproject.toml b/software/pyproject.toml index 3ecbad5f2..fba7bdc1d 100644 --- a/software/pyproject.toml +++ b/software/pyproject.toml @@ -92,6 +92,7 @@ selftest = "glasgow.applet.internal.selftest:SelfTestApplet" benchmark = "glasgow.applet.internal.benchmark:BenchmarkApplet" analyzer = "glasgow.applet.interface.analyzer:AnalyzerApplet" +ethernet-rgmii = "glasgow.applet.interface.ethernet.rgmii:EthernetRGMIIApplet" uart = "glasgow.applet.interface.uart:UARTApplet" uart-analyzer = "glasgow.applet.interface.uart_analyzer:UARTAnalyzerApplet" spi-analyzer = "glasgow.applet.interface.spi_analyzer:SPIAnalyzerApplet" diff --git a/software/tests/gateware/test_stream.py b/software/tests/gateware/test_stream.py index fc77b9ad0..c745462f5 100644 --- a/software/tests/gateware/test_stream.py +++ b/software/tests/gateware/test_stream.py @@ -109,3 +109,19 @@ async def o_testbench(ctx): await stream_assert(ctx, dut.o, {"data": 0xfe, "first": 1, "last": 1}) self.run_scenario(dut, i_testbench, o_testbench) + + def test_packet_end(self): + dut = PacketQueue(8, data_depth=8, size_depth=2) + + async def i_testbench(ctx): + await stream_put(ctx, dut.i, {"data": 0x01, "first": 1, "last": 0}) + await stream_put(ctx, dut.i, {"data": 0x02, "first": 0, "last": 0}) + await stream_put(ctx, dut.i, {"end": 1}) + await stream_put(ctx, dut.i, {"data": 0x03, "first": 1, "last": 1}) + + async def o_testbench(ctx): + await stream_assert(ctx, dut.o, {"data": 0x01, "first": 1, "last": 0}) + await stream_assert(ctx, dut.o, {"data": 0x02, "first": 0, "last": 1}) + await stream_assert(ctx, dut.o, {"data": 0x03, "first": 1, "last": 1}) + + self.run_scenario(dut, i_testbench, o_testbench)