Skip to content

Commit 752002e

Browse files
committed
applet.interface.ethernet.rgmii: new applet.
1 parent 3485930 commit 752002e

9 files changed

Lines changed: 478 additions & 2 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
``ethernet-rgmii``
2+
==================
3+
4+
.. _applet.interface.ethernet.rgmii:
5+
6+
.. autoprogram:: glasgow.applet.interface.ethernet.rgmii:EthernetRGMIIApplet._get_argparser_for_sphinx("ethernet-rgmii")
7+
:prog: glasgow run ethernet-rgmii

docs/manual/src/applets/interface/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ I/O interfaces
1717
jtag_openocd
1818
jtag_xvc
1919
swd_probe
20+
ethernet_rgmii
2021
probe_rs

software/glasgow/applet/control/mdio/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ class ControlMDIOInterface:
9494
def __init__(self, logger: logging.Logger, assembly: AbstractAssembly, *,
9595
mdc: GlasgowPin, mdio: GlasgowPin):
9696
self._logger = logger
97-
self._level = logging.DEBUG if self._logger.name == __name__ else logging.TRACE
97+
if self._logger.name == __name__ or "interface.ethernet." in self._logger.name:
98+
self._level = logging.DEBUG
99+
else:
100+
self._level = logging.TRACE
98101

99102
assembly.use_pulls({mdio: "low"})
100103
ports = assembly.add_port_group(mdc=mdc, mdio=mdio)
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# Ref: IEEE Std 802.3-2018
2+
# Accession: G00098
3+
4+
from typing import Iterable, AsyncIterator, BinaryIO
5+
import time
6+
import logging
7+
import asyncio
8+
import argparse
9+
10+
from amaranth import *
11+
from amaranth.lib import wiring, stream
12+
from amaranth.lib.wiring import In, Out
13+
from amaranth.lib.crc.catalog import CRC32_ETHERNET
14+
15+
from glasgow.support.logging import dump_hex
16+
from glasgow.support.os_network import OSNetworkInterface
17+
from glasgow.arch.ieee802_3 import *
18+
from glasgow.gateware import cobs, ethernet
19+
from glasgow.protocol import snoop
20+
from glasgow.abstract import AbstractAssembly, GlasgowPin
21+
from glasgow.applet import GlasgowAppletV2
22+
from glasgow.applet.control.mdio import ControlMDIOInterface
23+
24+
25+
__all__ = ["EthernetComponent", "AbstractEthernetApplet"]
26+
27+
28+
class EthernetComponent(wiring.Component):
29+
i_stream: In(stream.Signature(8))
30+
o_stream: Out(stream.Signature(8))
31+
o_flush: Out(1)
32+
33+
rx_bypass: In(1)
34+
tx_bypass: In(1)
35+
36+
def __init__(self, driver):
37+
self._driver = driver
38+
39+
super().__init__()
40+
41+
def elaborate(self, platform):
42+
m = Module()
43+
44+
m.submodules.tx_decoder = tx_decoder = cobs.Decoder()
45+
wiring.connect(m, tx_decoder.i, wiring.flipped(self.i_stream))
46+
47+
m.submodules.ctrl = ctrl = ethernet.Controller(self._driver)
48+
m.d.comb += ctrl.rx_bypass.eq(self.rx_bypass)
49+
m.d.comb += ctrl.tx_bypass.eq(self.tx_bypass)
50+
wiring.connect(m, ctrl.i, tx_decoder.o)
51+
52+
m.submodules.rx_encoder = rx_encoder = cobs.Encoder(fifo_depth=2048)
53+
wiring.connect(m, rx_encoder.i, ctrl.o)
54+
55+
wiring.connect(m, wiring.flipped(self.o_stream), rx_encoder.o)
56+
m.d.comb += self.o_flush.eq(~rx_encoder.o.valid)
57+
58+
return m
59+
60+
61+
class AbstractEthernetInterface:
62+
def __init__(self, logger: logging.Logger, assembly: AbstractAssembly, *,
63+
driver: ethernet.AbstractDriver):
64+
self._logger = logger
65+
self._level = logging.DEBUG if self._logger.name.startswith(__name__) else logging.TRACE
66+
67+
component = assembly.add_submodule(EthernetComponent(driver))
68+
self._pipe = assembly.add_inout_pipe(
69+
component.o_stream, component.i_stream, in_flush=component.o_flush,
70+
in_fifo_depth=0, out_buffer_size=512 * 128)
71+
self._rx_bypass = assembly.add_rw_register(component.rx_bypass)
72+
self._tx_bypass = assembly.add_rw_register(component.tx_bypass)
73+
74+
self._snoop: snoop.SnoopWriter = None
75+
76+
def _log(self, message: str, *args):
77+
self._logger.log(self._level, "Ethernet: " + message, *args)
78+
79+
@property
80+
def snoop_file(self) -> BinaryIO:
81+
if self._snoop is not None:
82+
return self._snoop.file
83+
84+
@snoop_file.setter
85+
def snoop_file(self, snoop_file):
86+
if snoop_file is not None:
87+
self._snoop = snoop.SnoopWriter(snoop_file,
88+
datalink_type=snoop.SnoopDatalinkType.Ethernet)
89+
else:
90+
self._snoop = None
91+
92+
def _snoop_packet(self, packet):
93+
if self._snoop is not None:
94+
self._snoop.write(snoop.SnoopPacket(packet, timestamp_ns=time.time_ns()))
95+
96+
async def send(self, packet: bytes | bytearray | memoryview) -> bool:
97+
cobs_packet = cobs.encode(packet) + b"\x00"
98+
if self._pipe.writable is None or len(cobs_packet) <= self._pipe.writable:
99+
self._log("tx data=<%s>", dump_hex(packet))
100+
self._snoop_packet(packet)
101+
await self._pipe.send(cobs_packet)
102+
await self._pipe.flush(_wait=False)
103+
return True
104+
else:
105+
self._logger.warning("tx drop")
106+
return False
107+
108+
async def recv(self) -> bytes:
109+
packet = cobs.decode((await self._pipe.recv_until(b"\x00"))[:-1])
110+
self._log("rx data=<%s> len=%d", dump_hex(packet), len(packet))
111+
self._snoop_packet(packet)
112+
return packet
113+
114+
async def iter_recv(self) -> AsyncIterator[bytes]:
115+
while True:
116+
yield await self.recv()
117+
118+
119+
class AbstractEthernetApplet(GlasgowAppletV2):
120+
logger = logging.getLogger(__name__)
121+
help = "send and receive Ethernet packets"
122+
description = """
123+
Communicate with an Ethernet network using a PHY connected via the $PHYIF$ interface.
124+
125+
The `bridge` operation is supported only on Linux. To create a suitable TAP interface, run:
126+
127+
::
128+
129+
sudo ip tuntap add glasgow0 mode tap user $USER
130+
sudo ip link set glasgow0 up
131+
"""
132+
required_revision = "C0"
133+
134+
@classmethod
135+
def add_setup_arguments(cls, parser):
136+
parser.add_argument("--snoop", dest="snoop_file", type=argparse.FileType("wb"),
137+
metavar="SNOOP-FILE", help="save packets to a file in RFC 1761 format")
138+
139+
async def setup(self, args):
140+
self.eth_iface.snoop_file = args.snoop_file
141+
142+
@classmethod
143+
def add_run_arguments(cls, parser):
144+
p_operation = parser.add_subparsers(dest="operation", metavar="OPERATION", required=True)
145+
146+
p_bridge = p_operation.add_parser(
147+
"bridge", help="bridge network to the host OS")
148+
p_bridge.add_argument(
149+
"interface", metavar="INTERFACE", nargs="?", type=str, default="glasgow0",
150+
help="forward packets to and from this TAP interface (default: %(default)s)")
151+
152+
p_loopback = p_operation.add_parser(
153+
"loopback", help="test connection to PHY using near-end loopback")
154+
p_loopback.add_argument(
155+
"--delay", "-d", metavar="DELAY", type=float, default=1.0,
156+
help="wait for DELAY seconds between sending packets (default: %(default)s)")
157+
158+
async def run(self, args):
159+
if args.operation == "bridge":
160+
os_iface = OSNetworkInterface(args.interface)
161+
162+
async def forward_rx():
163+
async for packet in self.eth_iface.iter_recv():
164+
if len(packet) >= 14: # must be at least ETH_HLEN, or we'll get EINVAL on Linux
165+
await os_iface.send([packet])
166+
167+
async def forward_tx():
168+
while True:
169+
for packet in await os_iface.recv():
170+
if not await self.eth_iface.send(packet):
171+
break
172+
173+
async with asyncio.TaskGroup() as group:
174+
group.create_task(forward_rx())
175+
group.create_task(forward_tx())
176+
177+
if args.operation == "loopback":
178+
# Enable near-end loopback
179+
basic_control = REG_BASIC_CONTROL.from_int(
180+
await self.mdio_iface.c22_read(0, REG_BASIC_CONTROL_addr))
181+
basic_control.LOOPBACK = 1
182+
await self.mdio_iface.c22_write(0, REG_BASIC_CONTROL_addr, basic_control.to_int())
183+
184+
# Accept all packets, even those with CRC errors
185+
await self.eth_iface._rx_bypass.set(True)
186+
187+
count_ok = 0
188+
count_bad = 0
189+
count_lost = 0
190+
try:
191+
packet_data = bytes(range(256))
192+
packet_fcs = CRC32_ETHERNET().compute(packet_data).to_bytes(4, "little")
193+
packet_full = packet_data + packet_fcs
194+
while True:
195+
await self.eth_iface.send(packet_data)
196+
try:
197+
async with asyncio.timeout(args.delay):
198+
packet_recv = await self.eth_iface.recv()
199+
if packet_recv == packet_full:
200+
self.logger.info("packet ok")
201+
count_ok += 1
202+
else:
203+
if len(packet_recv) < len(packet_full):
204+
self.logger.warning("packet bad (short)")
205+
elif len(packet_recv) > len(packet_full):
206+
self.logger.warning("packet bad (long)")
207+
elif packet_recv[:len(packet_data)] != packet_data:
208+
self.logger.warning("packet bad (data)")
209+
else:
210+
self.logger.warning("packet bad (crc)")
211+
count_bad += 1
212+
await asyncio.sleep(args.delay)
213+
except asyncio.TimeoutError:
214+
self.logger.warning("packet lost")
215+
count_lost += 1
216+
finally:
217+
count_all = count_ok + count_bad + count_lost
218+
if count_all:
219+
self.logger.info(f"statistics: "
220+
f"ok {count_ok}/{count_all} ({count_ok/count_all*100:.0f}%), "
221+
f"bad {count_bad}/{count_all} ({count_bad/count_all*100:.0f}%), "
222+
f"lost {count_lost}/{count_all} ({count_lost/count_all*100:.0f}%)")

0 commit comments

Comments
 (0)