Skip to content

Commit c4e07b1

Browse files
whitequarkwanda-phi
andcommitted
applet.interface.ethernet.{rmii,rgmii}: new applets.
Co-authored-by: Wanda <wanda@phinode.net>
1 parent 25db198 commit c4e07b1

6 files changed

Lines changed: 648 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)