bba_top: document the integration, and retract a comment claiming an open bug
778 -> 837 lines, 25% -> 17% comment-only. The module docstring now carries
what was scattered or missing: a data-flow diagram, all four clock domains with
what each hosts, and the three remaining crossings with their primitives.
The worst thing in this file was a testbench comment asserting a bug that no
longer exists:
"at 27 MHz reads currently come back shifted right by one bit
(04 02 02 00 -> 02 01 01 00, NWAYS 0x17 -> 0x0B), which is an open bug."
That is the MISO latency solved by the full-cycle-early launch. EXI_HALF=1
passes, and has for some time. A stale comment claiming a KNOWN FAILURE is
worse than a verbose one: it tells the next reader the suite's hardest case is
expected to be wrong.
Other stale text: the class docstring documented only the W5500 pins while
W5100 is the default back-end; "Pulse synchronizer connections" and
"reg exi->sync FIFO read side" described clock domains that were merged; the
testbench header claimed "all three clock domains" and a "capture domain"; and
the RX loop's model was called a W5500 when it is the W5100 bus model.
Structure: elaborate() is now _clock_domains, _submodules, _wire_pins,
_wire_datapath and _wire_bringup, with the submodules passed as a bundle.
Also documented, from an AST audit of the tree: w5500_clk/mosi/cs_n are
written and read by nothing, because synth.py declares only W5100 pins. They
are not dead -- they are the alternate back-end's interface, exercised by
w5500_spi_master's own testbench -- so the docstring now says that wiring them
up is a synth.py change rather than leaving a reader to wonder.
MEASURED AND REJECTED: the FFSynchronizer on cs_active into the status panel
is NOT a clock-domain crossing, since ExiCapture already synchronises CS, so I
removed it as redundant. Building it showed why it should stay -- area went
4305 -> 4330 LC, because the panel's combinational cone then reaches back to
ExiCapture's own synchroniser. `sync` is the binding timing domain here, so
25 LC is not worth two flip-flops. Reverted, and the comment now explains
what those registers actually buy instead of mislabelling them as a CDC.
VERIFIED EQUIVALENT: synthesised cell counts identical to the original in
every category (5971 cells; 134 SB_DFF, 3382 SB_LUT4, 669 SB_CARRY, 70
SB_RAM40_4K), so this commit changes no hardware at all. Full module suite
passes and bba_top T9 is 6/6 at EXI_HALF=1, 2 and 8.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+263
-204
@@ -1,18 +1,47 @@
|
|||||||
"""BBATop — top-level elaboratable for the GC BBA FPGA replacement.
|
"""BBATop — the whole adapter: GameCube EXI on one side, ethernet on the other.
|
||||||
|
|
||||||
|
Data flow
|
||||||
|
---------
|
||||||
|
GC ──EXI──> ExiCapture ──bytes──> BBARegisterFile ─┬─> TXFrameDrain ──> eth
|
||||||
|
│
|
||||||
|
GC <─EXI─── ExiCapture <─bytes─── BBARegisterFile <─┴── SPRAMArbiter <─┐
|
||||||
|
│
|
||||||
|
RXFrameAssembler <──frames── ethernet master <─┘
|
||||||
|
|
||||||
|
The FPGA holds no network stack. The GC's own TCP/IP runs on its CPU and this
|
||||||
|
is a MAC bridge: register reads and writes on one side, whole frames on the
|
||||||
|
other, with the RX ring in SPRAM between them.
|
||||||
|
|
||||||
Clock domains
|
Clock domains
|
||||||
-------------
|
-------------
|
||||||
sync : 24 MHz, from the iCE40UP5K internal SB_HFOSC (÷2, CLKHF_DIV=0b01).
|
sync 24 MHz, from the internal SB_HFOSC (÷2, CLKHF_DIV=0b01).
|
||||||
Everything except the EXI bit engine. No PLL, no crystal.
|
EVERYTHING except the EXI bit engine: register file, SPRAM
|
||||||
exiclk : the GC's EXI clock pin itself (~27 MHz), posedge — created inside
|
arbiter, RX/TX engines, ethernet master, bring-up peripherals.
|
||||||
exiclk_n : the same pin, negedge ................ `ExiCapture`.
|
exiclk the GC's EXI clock pin itself, ~27 MHz, posedge. Hosts only
|
||||||
exififo_* the same pin, posedge, reset-less — the byte-FIFO sides.
|
the SPI bit engine, and is async-reset by CS.
|
||||||
|
exififo_w the same pin, posedge, reset-less -- the byte FIFOs' sides.
|
||||||
|
exififo_r
|
||||||
|
|
||||||
Submodule instantiation and signal wiring
|
All four are created inside `ExiCapture`, which owns the pin-clocked front
|
||||||
-----------------------------------------
|
end; this module creates only `sync`. There is no PLL, no external crystal
|
||||||
See CLAUDE.md "Module Breakdown" and "CDC Signal Inventory" for the full list.
|
and no oversampling domain: bit-level timing across a CDC boundary is what
|
||||||
|
made MISO arrive two bits late on hardware, so the engine is clocked by the
|
||||||
|
pin and the only crossings left are byte-level.
|
||||||
|
|
||||||
|
Crossings
|
||||||
|
---------
|
||||||
|
Just three, all with a full byte time (~296 ns at 27 MHz) of slack:
|
||||||
|
|
||||||
|
RX byte stream exififo_w → sync AsyncFIFO, 8-bit, depth 4
|
||||||
|
TX byte stream sync → exififo_r AsyncFIFO, 8-bit, depth 2
|
||||||
|
cs_active EXI pin → sync FFSynchronizer, 2 stages
|
||||||
|
|
||||||
|
Everything that once crossed a separate `exi` domain is now same-domain: those
|
||||||
|
FIFOs became `SyncFIFO`s and the pulse synchronisers became plain wires.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from amaranth import *
|
from amaranth import *
|
||||||
|
|
||||||
from exi_bba.exi_capture import ExiCapture
|
from exi_bba.exi_capture import ExiCapture
|
||||||
@@ -32,20 +61,48 @@ __all__ = ["BBATop"]
|
|||||||
|
|
||||||
|
|
||||||
class BBATop(Elaboratable):
|
class BBATop(Elaboratable):
|
||||||
"""Top-level module. Wires all submodules and defines clock domains.
|
"""Top-level module: creates `sync`, builds every submodule and wires them.
|
||||||
|
|
||||||
External ports (exposed for platform or testbench connection)
|
GC side (EXI, SPI Mode 3)
|
||||||
-------------------------------------------------------------
|
-------------------------
|
||||||
EXI / GC interface (SPI Mode 3)
|
exi_clk, exi_mosi, exi_cs_n
|
||||||
exi_clk / exi_mosi / exi_cs_n : inputs from GC
|
Inputs from the GC. `exi_clk` becomes a clock and `exi_cs_n` an async
|
||||||
exi_miso : output to GC
|
reset inside `ExiCapture`.
|
||||||
int_n : interrupt output (active low)
|
exi_miso
|
||||||
|
Serial output to the GC, driven from the fabric. The synthesised
|
||||||
|
build does not use this -- see `exi_miso_next` -- but the testbench
|
||||||
|
reads it, and `spi_mode3_slave` T6 proves the two agree.
|
||||||
|
exi_miso_next
|
||||||
|
The same bit one edge early, for an SB_IO output register in the IO
|
||||||
|
tile. This is what `synth.py` actually drives the pad from.
|
||||||
|
int_n
|
||||||
|
Interrupt to the GC, active low.
|
||||||
|
|
||||||
W5500 SPI interface (SPI Mode 0)
|
Ethernet side -- ONE of these two exists, chosen by `eth=`
|
||||||
w5500_clk / w5500_mosi / w5500_cs_n : outputs to W5500
|
----------------------------------------------------------
|
||||||
w5500_miso : input from W5500
|
eth="w5100" (default): indirect parallel bus, reaches the ~27 Mbit/s EXI
|
||||||
w5500_int_n : W5500 interrupt (input, active low)
|
ceiling.
|
||||||
w5500_rst_n : W5500 hardware reset (output, active low)
|
w5100_addr A[1:0]; the board ties A[14:2] low
|
||||||
|
w5100_data_o, w5100_data_oe, w5100_data_i FPGA side of a bidirectional
|
||||||
|
D[7:0], a tristate SB_IO at
|
||||||
|
the platform level
|
||||||
|
w5100_cs_n, w5100_rd_n, w5100_wr_n bus strobes
|
||||||
|
w5100_int_n, w5100_rst_n
|
||||||
|
|
||||||
|
eth="w5500": SPI Mode 0, ~12 Mbit/s -- below the EXI ceiling, so it is the
|
||||||
|
alternate rather than the default.
|
||||||
|
w5500_clk, w5500_mosi, w5500_cs_n, w5500_miso
|
||||||
|
w5500_int_n, w5500_rst_n
|
||||||
|
|
||||||
|
⚠️ `synth.py` only declares W5100 pins, so the W5500 outputs reach no pad
|
||||||
|
in a real build. They are the module's interface, exercised by the
|
||||||
|
`w5500_spi_master` testbench; wiring them up is a synth.py change.
|
||||||
|
|
||||||
|
Optional bring-up peripherals
|
||||||
|
-----------------------------
|
||||||
|
panel_led, panel_btn with status_panel=True; see `StatusPanel`
|
||||||
|
uart_tx, uart_rx with uart_console=True or uart_shell=True. Both
|
||||||
|
share the one UART, so only one may be enabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, eth="w5100", reset_cycles=2_000_000,
|
def __init__(self, eth="w5100", reset_cycles=2_000_000,
|
||||||
@@ -55,38 +112,32 @@ class BBATop(Elaboratable):
|
|||||||
udp_dst_ip="192.168.1.100",
|
udp_dst_ip="192.168.1.100",
|
||||||
udp_src_port=40000, udp_dst_port=6464,
|
udp_src_port=40000, udp_dst_port=6464,
|
||||||
phase_meter=False):
|
phase_meter=False):
|
||||||
# Ethernet back-end: "w5100" (indirect parallel bus, reaches the EXI
|
# Both masters expose an identical tx/rx/init/par interface, so only
|
||||||
# ceiling) or "w5500" (SPI, ~12 Mbit/s). Both expose the identical
|
# the physical pins below differ.
|
||||||
# tx/rx/init/par interface, so only the physical pins differ.
|
|
||||||
self._eth = eth
|
self._eth = eth
|
||||||
self._phase_meter = phase_meter # diagnostic, see SPIMode3Slave
|
self._phase_meter = phase_meter # diagnostic; see SPIMode3Slave
|
||||||
# MR-reset settle wait passed to the ethernet master (~1 ms on hardware;
|
# MR-reset settle wait handed to the ethernet master (~1 ms of cycles
|
||||||
# the testbench overrides with a small value for fast simulation).
|
# on hardware; the testbench passes a tiny value to keep sim short).
|
||||||
self._reset_cycles = reset_cycles
|
self._reset_cycles = reset_cycles
|
||||||
# Optional bring-up status panel (drives onboard LEDs/button on the
|
|
||||||
# iCEbreaker — see synth.py). panel_led bit order matches StatusPanel.
|
|
||||||
self._status_panel = status_panel
|
self._status_panel = status_panel
|
||||||
# Optional UART debug console (8N1 115200, sync domain).
|
|
||||||
# uart_tx → FT2232H Channel B (net UART_RXD, pin 19);
|
|
||||||
# uart_rx ← FT2232H Channel B (net UART_TXD, pin 18) — net names are
|
|
||||||
# FTDI-perspective, so this is not a naming mismatch, see synth.py.
|
|
||||||
self._uart_console = uart_console
|
self._uart_console = uart_console
|
||||||
# Optional interactive UART command shell (bring-up UDP test on the
|
# Interactive shell driving a UDP test on a second ethernet socket,
|
||||||
# W5100's socket 1). Shares the UART pins with the console, so only one
|
# which needs the W5100's multi-socket support.
|
||||||
# may be enabled; the UDP path needs the W5100 back-end. See uart_shell.
|
|
||||||
self._uart_shell = uart_shell
|
self._uart_shell = uart_shell
|
||||||
|
|
||||||
if uart_console and uart_shell:
|
if uart_console and uart_shell:
|
||||||
raise ValueError("uart_console and uart_shell share the UART pins; "
|
raise ValueError("uart_console and uart_shell share the UART pins; "
|
||||||
"enable only one")
|
"enable only one")
|
||||||
if uart_shell and eth != "w5100":
|
if uart_shell and eth != "w5100":
|
||||||
raise ValueError("uart_shell UDP test requires eth='w5100'")
|
raise ValueError("uart_shell UDP test requires eth='w5100'")
|
||||||
|
|
||||||
self._udp_cfg = dict(udp_socket=udp_socket, src_ip=udp_src_ip,
|
self._udp_cfg = dict(udp_socket=udp_socket, src_ip=udp_src_ip,
|
||||||
subnet=udp_subnet, gateway=udp_gateway,
|
subnet=udp_subnet, gateway=udp_gateway,
|
||||||
dst_ip=udp_dst_ip, src_port=udp_src_port,
|
dst_ip=udp_dst_ip, src_port=udp_src_port,
|
||||||
dst_port=udp_dst_port)
|
dst_port=udp_dst_port)
|
||||||
|
|
||||||
# EXI (GC side)
|
# EXI (GC side)
|
||||||
self.exi_miso_next = Signal(init=1) # for an SB_IO output register
|
self.exi_miso_next = Signal(init=1)
|
||||||
self.exi_clk = Signal(init=1)
|
self.exi_clk = Signal(init=1)
|
||||||
self.exi_mosi = Signal()
|
self.exi_mosi = Signal()
|
||||||
self.exi_cs_n = Signal(init=1)
|
self.exi_cs_n = Signal(init=1)
|
||||||
@@ -94,7 +145,6 @@ class BBATop(Elaboratable):
|
|||||||
self.int_n = Signal(init=1)
|
self.int_n = Signal(init=1)
|
||||||
|
|
||||||
if eth == "w5500":
|
if eth == "w5500":
|
||||||
# W5500 SPI
|
|
||||||
self.w5500_clk = Signal()
|
self.w5500_clk = Signal()
|
||||||
self.w5500_mosi = Signal()
|
self.w5500_mosi = Signal()
|
||||||
self.w5500_miso = Signal()
|
self.w5500_miso = Signal()
|
||||||
@@ -102,10 +152,6 @@ class BBATop(Elaboratable):
|
|||||||
self.w5500_int_n = Signal(init=1)
|
self.w5500_int_n = Signal(init=1)
|
||||||
self.w5500_rst_n = Signal(init=1)
|
self.w5500_rst_n = Signal(init=1)
|
||||||
else:
|
else:
|
||||||
# W5100 indirect parallel bus. data_o/data_oe/data_i are the FPGA
|
|
||||||
# side of a bidirectional D[7:0] (wrapped in a tristate SB_IO at the
|
|
||||||
# platform level); a board ties the upper address lines to 0 so only
|
|
||||||
# A[1:0] are wired.
|
|
||||||
self.w5100_addr = Signal(2)
|
self.w5100_addr = Signal(2)
|
||||||
self.w5100_data_o = Signal(8)
|
self.w5100_data_o = Signal(8)
|
||||||
self.w5100_data_oe = Signal()
|
self.w5100_data_oe = Signal()
|
||||||
@@ -117,52 +163,54 @@ class BBATop(Elaboratable):
|
|||||||
self.w5100_rst_n = Signal(init=1)
|
self.w5100_rst_n = Signal(init=1)
|
||||||
|
|
||||||
if status_panel:
|
if status_panel:
|
||||||
self.panel_led = Signal(5) # to onboard LEDs (see StatusPanel)
|
self.panel_led = Signal(5)
|
||||||
self.panel_btn = Signal(3) # from onboard button(s)
|
self.panel_btn = Signal(3)
|
||||||
|
|
||||||
if uart_console or uart_shell:
|
if uart_console or uart_shell:
|
||||||
self.uart_tx = Signal(init=1) # FPGA → PC (FT2232H Channel B)
|
# Net names on the board are from the FT2232H's perspective, so
|
||||||
|
# the FPGA's TX pin carries the net called UART_RXD. See synth.py.
|
||||||
|
self.uart_tx = Signal(init=1) # FPGA → PC
|
||||||
self.uart_rx = Signal(init=1) # PC → FPGA
|
self.uart_rx = Signal(init=1) # PC → FPGA
|
||||||
|
|
||||||
def elaborate(self, platform):
|
def elaborate(self, platform):
|
||||||
m = Module()
|
m = Module()
|
||||||
|
self._clock_domains(m, platform)
|
||||||
|
mods = self._submodules(m)
|
||||||
|
self._wire_pins(m, mods)
|
||||||
|
self._wire_datapath(m, mods)
|
||||||
|
self._wire_bringup(m, mods)
|
||||||
|
return m
|
||||||
|
|
||||||
# ── Clock domain generation ───────────────────────────────────────
|
def _clock_domains(self, m, platform):
|
||||||
# ONE generated domain, one physical source:
|
"""Create `sync` from the internal oscillator.
|
||||||
# sync @ 24 MHz (internal SB_HFOSC ÷2) — register file, SPRAM,
|
|
||||||
# RX/TX engines, ethernet master. Everything that is not the EXI
|
|
||||||
# bit engine lives here.
|
|
||||||
#
|
|
||||||
# The EXI front-end is NOT clocked from here: `ExiCapture` builds its
|
|
||||||
# own exiclk/exiclk_n domains directly from the GC's EXI clock pin.
|
|
||||||
# That is what removed the old 54 MHz PLL-fed `capture` domain, which
|
|
||||||
# oversampled the EXI clock 2× and put BIT-level timing across a CDC
|
|
||||||
# boundary (~55 ns of synchroniser latency against a 37 ns bit) — the
|
|
||||||
# cause of the `0x17` -> `05 C0` MISO shift on hardware. With the
|
|
||||||
# engine clocked by the pin, MISO is clock-to-Q and the only crossings
|
|
||||||
# left are byte-level FIFOs with a full byte-time of slack.
|
|
||||||
#
|
|
||||||
# Consequence for timing closure: `capture_clk` is GONE as a
|
|
||||||
# constraint. It was the binding path for this whole design and the
|
|
||||||
# reason builds only closed on a minority of seeds.
|
|
||||||
if platform is not None:
|
|
||||||
# sync @ 24 MHz from the internal oscillator. No PLL and no
|
|
||||||
# external crystal are needed any more, so the 12 MHz input (pin
|
|
||||||
# 35) and the PLL hard macro are both free.
|
|
||||||
# The register-file / SPRAM / ethernet logic is routing-bound at
|
|
||||||
# ~33–44 MHz on the UP5K; 24 MHz closes with large margin. The byte
|
|
||||||
# rate (27 MHz EXI ÷ 8 ≈ 3.4 MHz) leaves ~7 sync cycles per byte.
|
|
||||||
m.domains += ClockDomain("sync")
|
|
||||||
m.submodules.hfosc = Instance(
|
|
||||||
"SB_HFOSC",
|
|
||||||
p_CLKHF_DIV = "0b01", # 48 ÷ 2 → 24 MHz
|
|
||||||
i_CLKHFEN = Const(1, 1),
|
|
||||||
i_CLKHFPU = Const(1, 1),
|
|
||||||
o_CLKHF = ClockSignal("sync"),
|
|
||||||
)
|
|
||||||
# (simulation: the test harness drives the EXI pins and `sync` directly)
|
|
||||||
|
|
||||||
# ── Submodules ────────────────────────────────────────────────────
|
This module generates ONE domain from ONE physical source. The EXI
|
||||||
|
front end is deliberately not clocked from here: `ExiCapture` builds
|
||||||
|
its domains straight off the GC's clock pin, which is what removed the
|
||||||
|
old PLL-fed 54 MHz oversampling domain -- that domain was the binding
|
||||||
|
timing path for the entire design and the reason builds closed on only
|
||||||
|
a minority of seeds.
|
||||||
|
|
||||||
|
24 MHz is chosen with margin: the register file, SPRAM and ethernet
|
||||||
|
logic are routing-bound around 33-44 MHz on this part, and the EXI byte
|
||||||
|
rate (27 MHz / 8 ≈ 3.4 MHz) leaves roughly seven sync cycles per byte.
|
||||||
|
|
||||||
|
In simulation the harness drives `sync` and the EXI pins itself, so
|
||||||
|
nothing is instantiated.
|
||||||
|
"""
|
||||||
|
if platform is None:
|
||||||
|
return
|
||||||
|
m.domains += ClockDomain("sync")
|
||||||
|
m.submodules.hfosc = Instance(
|
||||||
|
"SB_HFOSC",
|
||||||
|
p_CLKHF_DIV = "0b01", # 48 ÷ 2 → 24 MHz
|
||||||
|
i_CLKHFEN = Const(1, 1),
|
||||||
|
i_CLKHFPU = Const(1, 1),
|
||||||
|
o_CLKHF = ClockSignal("sync"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _submodules(self, m):
|
||||||
|
"""Build every submodule and register it. Returns them as a bundle."""
|
||||||
cap = ExiCapture(phase_meter=self._phase_meter)
|
cap = ExiCapture(phase_meter=self._phase_meter)
|
||||||
reg = BBARegisterFile()
|
reg = BBARegisterFile()
|
||||||
arb = SPRAMArbiter()
|
arb = SPRAMArbiter()
|
||||||
@@ -181,39 +229,29 @@ class BBATop(Elaboratable):
|
|||||||
m.submodules.drain = drain
|
m.submodules.drain = drain
|
||||||
m.submodules.eth = eth
|
m.submodules.eth = eth
|
||||||
|
|
||||||
# ── External pin connections ──────────────────────────────────────
|
return SimpleNamespace(cap=cap, reg=reg, arb=arb, asm=asm,
|
||||||
|
drain=drain, eth=eth)
|
||||||
|
|
||||||
|
def _wire_pins(self, m, mods):
|
||||||
|
"""Connect the external pins to the front end and the ethernet chip."""
|
||||||
|
cap, eth = mods.cap, mods.eth
|
||||||
|
|
||||||
m.d.comb += [
|
m.d.comb += [
|
||||||
# EXI inputs (to capture-domain front-end)
|
cap.spi_clk .eq(self.exi_clk), # becomes the exiclk domain
|
||||||
cap.spi_clk .eq(self.exi_clk), # drives the exiclk domains
|
|
||||||
cap.spi_mosi.eq(self.exi_mosi),
|
cap.spi_mosi.eq(self.exi_mosi),
|
||||||
cap.spi_cs_n.eq(self.exi_cs_n),
|
cap.spi_cs_n.eq(self.exi_cs_n), # becomes its async reset
|
||||||
# EXI outputs
|
self.exi_miso .eq(cap.spi_miso),
|
||||||
self.exi_miso.eq(cap.spi_miso),
|
|
||||||
self.exi_miso_next.eq(cap.miso_next),
|
self.exi_miso_next.eq(cap.miso_next),
|
||||||
# Ring-overflow protection: the assembler needs the GC's read
|
self.int_n .eq(mods.reg.exi_int_n),
|
||||||
# pointer to know what it must not overwrite, and reports a
|
|
||||||
# dropped frame back as RBFI (IR[7]).
|
|
||||||
asm.rrp.eq(reg.rrp),
|
|
||||||
# The GC's own ring geometry (BP / RHBP). Previously hardcoded in
|
|
||||||
# the assembler, so a driver configuring a smaller RX ring was
|
|
||||||
# ignored and we kept writing pages it would never read. The
|
|
||||||
# assembler sanity-checks these and falls back to pages 1-15 if
|
|
||||||
# they are unusable -- see docs/bba_compat_todo.md item 2.
|
|
||||||
asm.bp .eq(reg.bp),
|
|
||||||
asm.rhbp.eq(reg.rhbp),
|
|
||||||
asm.ncra_rst.eq(reg.ncra_rst_o),
|
|
||||||
reg.rx_overflow.eq(asm.rx_overflow),
|
|
||||||
self.int_n .eq(reg.exi_int_n),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Ethernet back-end physical pins
|
|
||||||
if self._eth == "w5500":
|
if self._eth == "w5500":
|
||||||
m.d.comb += [
|
m.d.comb += [
|
||||||
self.w5500_clk .eq(eth.spi_clk),
|
self.w5500_clk .eq(eth.spi_clk),
|
||||||
self.w5500_mosi.eq(eth.spi_mosi),
|
self.w5500_mosi .eq(eth.spi_mosi),
|
||||||
self.w5500_cs_n.eq(eth.spi_cs_n),
|
self.w5500_cs_n .eq(eth.spi_cs_n),
|
||||||
eth.spi_miso .eq(self.w5500_miso),
|
eth.spi_miso .eq(self.w5500_miso),
|
||||||
eth.w5500_int_n.eq(self.w5500_int_n),
|
eth.w5500_int_n .eq(self.w5500_int_n),
|
||||||
self.w5500_rst_n.eq(eth.w5500_rst_n),
|
self.w5500_rst_n.eq(eth.w5500_rst_n),
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
@@ -229,33 +267,45 @@ class BBATop(Elaboratable):
|
|||||||
self.w5100_rst_n .eq(eth.w5100_rst_n),
|
self.w5100_rst_n .eq(eth.w5100_rst_n),
|
||||||
]
|
]
|
||||||
|
|
||||||
# ── ExiCapture byte stream ↔ BBARegisterFile (exi domain) ────────
|
def _wire_datapath(self, m, mods):
|
||||||
|
"""Wire the EXI byte streams, SPRAM, and both frame directions.
|
||||||
|
|
||||||
|
Everything below `ExiCapture` is one `sync` domain, so these are plain
|
||||||
|
combinational connections and same-domain FIFO sides -- no CDC.
|
||||||
|
"""
|
||||||
|
cap, reg, arb = mods.cap, mods.reg, mods.arb
|
||||||
|
asm, drain, eth = mods.asm, mods.drain, mods.eth
|
||||||
|
|
||||||
|
# ExiCapture byte streams ↔ register file
|
||||||
m.d.comb += [
|
m.d.comb += [
|
||||||
reg.rx_data .eq(cap.rx_data),
|
reg.rx_data.eq(cap.rx_data),
|
||||||
reg.rx_rdy .eq(cap.rx_rdy),
|
reg.rx_rdy .eq(cap.rx_rdy),
|
||||||
cap.rx_en .eq(reg.rx_en),
|
cap.rx_en .eq(reg.rx_en),
|
||||||
|
|
||||||
cap.tx_data .eq(reg.tx_data),
|
cap.tx_data.eq(reg.tx_data),
|
||||||
cap.tx_en .eq(reg.tx_en),
|
cap.tx_en .eq(reg.tx_en),
|
||||||
reg.tx_rdy .eq(cap.tx_rdy),
|
reg.tx_rdy .eq(cap.tx_rdy),
|
||||||
|
|
||||||
reg.cs_active.eq(cap.cs_active), # transaction-active (for DMA reads)
|
# Transaction-in-progress level: a register-space read at or above
|
||||||
|
# 0x100 streams from SPRAM until CS rises, so the register file
|
||||||
|
# needs to see the end of the transaction.
|
||||||
|
reg.cs_active.eq(cap.cs_active),
|
||||||
]
|
]
|
||||||
|
|
||||||
# ── BBARegisterFile ↔ SPRAMArbiter (sync domain FIFO sides) ──────
|
# Register file ↔ SPRAM arbiter. Requests come through a SyncFIFO;
|
||||||
# SPRAM request: reg exi→sync FIFO read side → arb
|
# responses are COMBINATIONAL and must stay that way -- see the note
|
||||||
|
# on BBARegisterFile._read_mux.
|
||||||
m.d.comb += [
|
m.d.comb += [
|
||||||
arb.exi_req_addr .eq(reg.spram_req_r_data),
|
arb.exi_req_addr .eq(reg.spram_req_r_data),
|
||||||
arb.exi_req_valid.eq(reg.spram_req_r_rdy),
|
arb.exi_req_valid .eq(reg.spram_req_r_rdy),
|
||||||
reg.spram_req_r_en.eq(arb.exi_req_ready),
|
reg.spram_req_r_en.eq(arb.exi_req_ready),
|
||||||
]
|
|
||||||
# SPRAM response: arb result → reg sync→exi FIFO write side
|
|
||||||
m.d.comb += [
|
|
||||||
reg.spram_rsp_data .eq(arb.exi_rsp_data),
|
reg.spram_rsp_data .eq(arb.exi_rsp_data),
|
||||||
reg.spram_rsp_valid.eq(arb.exi_rsp_valid),
|
reg.spram_rsp_valid.eq(arb.exi_rsp_valid),
|
||||||
]
|
]
|
||||||
|
|
||||||
# ── BBARegisterFile ↔ TXFrameDrain (sync domain FIFO sides) ──────
|
# TX: register file → drain → ethernet. tx_bytes carries the frame and
|
||||||
|
# tx_ctrl its length, so the drain can start before the frame ends.
|
||||||
m.d.comb += [
|
m.d.comb += [
|
||||||
drain.tx_bytes_r_data.eq(reg.tx_bytes_r_data),
|
drain.tx_bytes_r_data.eq(reg.tx_bytes_r_data),
|
||||||
drain.tx_bytes_r_rdy .eq(reg.tx_bytes_r_rdy),
|
drain.tx_bytes_r_rdy .eq(reg.tx_bytes_r_rdy),
|
||||||
@@ -264,70 +314,74 @@ class BBATop(Elaboratable):
|
|||||||
drain.tx_ctrl_r_data.eq(reg.tx_ctrl_r_data),
|
drain.tx_ctrl_r_data.eq(reg.tx_ctrl_r_data),
|
||||||
drain.tx_ctrl_r_rdy .eq(reg.tx_ctrl_r_rdy),
|
drain.tx_ctrl_r_rdy .eq(reg.tx_ctrl_r_rdy),
|
||||||
reg.tx_ctrl_r_en .eq(drain.tx_ctrl_r_en),
|
reg.tx_ctrl_r_en .eq(drain.tx_ctrl_r_en),
|
||||||
]
|
|
||||||
|
|
||||||
# ── TXFrameDrain ↔ ethernet master (sync domain) ──────────────────
|
eth.tx_data .eq(drain.tx_data),
|
||||||
m.d.comb += [
|
eth.tx_valid .eq(drain.tx_valid),
|
||||||
eth.tx_data .eq(drain.tx_data),
|
|
||||||
eth.tx_valid.eq(drain.tx_valid),
|
|
||||||
drain.tx_ready.eq(eth.tx_ready),
|
drain.tx_ready.eq(eth.tx_ready),
|
||||||
eth.tx_sof .eq(drain.tx_sof),
|
eth.tx_sof .eq(drain.tx_sof),
|
||||||
eth.tx_eof .eq(drain.tx_eof),
|
eth.tx_eof .eq(drain.tx_eof),
|
||||||
]
|
]
|
||||||
|
|
||||||
# ── ethernet master → RXFrameAssembler (sync domain) ─────────────
|
# RX: ethernet → assembler → SPRAM ring, then the pointer back to the
|
||||||
|
# register file so the GC can see the new frame.
|
||||||
m.d.comb += [
|
m.d.comb += [
|
||||||
asm.rx_data .eq(eth.rx_data),
|
asm.rx_data .eq(eth.rx_data),
|
||||||
asm.rx_valid.eq(eth.rx_valid),
|
asm.rx_valid.eq(eth.rx_valid),
|
||||||
eth.rx_ready.eq(asm.rx_ready),
|
eth.rx_ready.eq(asm.rx_ready),
|
||||||
asm.rx_sof .eq(eth.rx_sof),
|
asm.rx_sof .eq(eth.rx_sof),
|
||||||
asm.rx_eof .eq(eth.rx_eof),
|
asm.rx_eof .eq(eth.rx_eof),
|
||||||
]
|
|
||||||
|
|
||||||
# ── RXFrameAssembler → SPRAMArbiter (ETH write, sync domain) ─────
|
|
||||||
m.d.comb += [
|
|
||||||
arb.eth_wr_addr .eq(asm.eth_wr_addr),
|
arb.eth_wr_addr .eq(asm.eth_wr_addr),
|
||||||
arb.eth_wr_data .eq(asm.eth_wr_data),
|
arb.eth_wr_data .eq(asm.eth_wr_data),
|
||||||
arb.eth_wr_valid.eq(asm.eth_wr_valid),
|
arb.eth_wr_valid.eq(asm.eth_wr_valid),
|
||||||
asm.eth_wr_ready.eq(arb.eth_wr_ready),
|
asm.eth_wr_ready.eq(arb.eth_wr_ready),
|
||||||
]
|
|
||||||
|
|
||||||
# ── RXFrameAssembler → BBARegisterFile (rx_wptr FIFO write side) ─
|
|
||||||
m.d.comb += [
|
|
||||||
reg.rx_wptr_w_data.eq(asm.rx_wptr_w_data),
|
reg.rx_wptr_w_data.eq(asm.rx_wptr_w_data),
|
||||||
reg.rx_wptr_w_en .eq(asm.rx_wptr_w_en),
|
reg.rx_wptr_w_en .eq(asm.rx_wptr_w_en),
|
||||||
asm.rx_wptr_w_rdy .eq(reg.rx_wptr_w_rdy),
|
asm.rx_wptr_w_rdy .eq(reg.rx_wptr_w_rdy),
|
||||||
]
|
]
|
||||||
|
|
||||||
# ── Pulse synchronizer connections ────────────────────────────────
|
# Interrupt sources, and the ring state the assembler needs from the
|
||||||
|
# GC: its read pointer (so unread pages are not overwritten) and the
|
||||||
|
# ring geometry it asked for.
|
||||||
m.d.comb += [
|
m.d.comb += [
|
||||||
# RX irq: sync → exi (RXFrameAssembler → reg → PS → exi domain)
|
|
||||||
reg.rx_irq_i.eq(asm.rx_irq),
|
reg.rx_irq_i.eq(asm.rx_irq),
|
||||||
# TX irq: sync → exi
|
|
||||||
reg.tx_irq_i.eq(drain.tx_irq),
|
reg.tx_irq_i.eq(drain.tx_irq),
|
||||||
# MAC address (PAR0–5) → SHAR. exi and sync share the HFOSC net,
|
reg.rx_overflow.eq(asm.rx_overflow),
|
||||||
# and par is quasi-static (sampled by the master at init_req).
|
|
||||||
|
asm.rrp .eq(reg.rrp),
|
||||||
|
asm.bp .eq(reg.bp),
|
||||||
|
asm.rhbp.eq(reg.rhbp),
|
||||||
|
asm.ncra_rst.eq(reg.ncra_rst_o),
|
||||||
|
|
||||||
|
# The MAC the GC holds in PAR0-5 becomes the ethernet chip's own
|
||||||
|
# source address. Quasi-static: the master samples it at init.
|
||||||
eth.par.eq(reg.par),
|
eth.par.eq(reg.par),
|
||||||
|
|
||||||
|
# RX runs only once the GC sets NCRA's SR (start-receive) bit.
|
||||||
|
asm.rx_enabled.eq(reg.ncra_sr),
|
||||||
]
|
]
|
||||||
|
|
||||||
# ── RX enabled gate (NCRA SR / start-receive bit) ─────────────────
|
# NCRB's PR bit asks for promiscuous receive. Low -- the normal case
|
||||||
# The RX ring-buffer path is active only after the GC sets NCRA[3].
|
# -- leaves the chip filtering to own-MAC plus broadcast, which is what
|
||||||
m.d.comb += asm.rx_enabled.eq(reg.ncra_sr)
|
# the real BBA does; running unfiltered livelocked the GC's own
|
||||||
# NCRB.PR (bit 0) -> MACRAW MAC filter. Low means the ethernet chip
|
# interrupt handler. See the S0_MR note in w5100_parallel_master.
|
||||||
# delivers own-MAC + broadcast only, matching the real BBA; the GC can
|
|
||||||
# ask for promiscuous by setting PR. See the S0_MODE comment in
|
|
||||||
# w5100_parallel_master.py -- running unfiltered hung Swiss.
|
|
||||||
if self._eth == "w5100":
|
if self._eth == "w5100":
|
||||||
m.d.comb += eth.promisc.eq(reg.ncrb[NCRB_PR])
|
m.d.comb += mods.eth.promisc.eq(reg.ncrb[NCRB_PR])
|
||||||
|
|
||||||
# ── Optional bring-up peripherals (sync domain) ───────────────────
|
def _wire_bringup(self, m, mods):
|
||||||
# Build init_req as an OR of all reinit sources (NCRA pulse plus any
|
"""Optional status panel, UART console and UART shell.
|
||||||
# manual re-init from the status panel and/or the UART 'r' command).
|
|
||||||
# "ready" is latched high by eth.init_done and cleared by any init_req.
|
|
||||||
# It is computed only when at least one peripheral needs it.
|
|
||||||
|
|
||||||
init_req = reg.ncra_rst_o # base: GC-issued NCRA reset
|
`init_req` to the ethernet master is the OR of every re-init source:
|
||||||
|
the GC's NCRA reset, plus any manual request from the panel button or
|
||||||
|
the console's 'r' command. `ready` is latched by `init_done` and
|
||||||
|
cleared by any new request, and is only built when something displays
|
||||||
|
it.
|
||||||
|
"""
|
||||||
|
cap, reg, asm, drain, eth = (mods.cap, mods.reg, mods.asm,
|
||||||
|
mods.drain, mods.eth)
|
||||||
|
|
||||||
|
init_req = reg.ncra_rst_o # the GC's own reset
|
||||||
need_ready = self._status_panel or self._uart_console
|
need_ready = self._status_panel or self._uart_console
|
||||||
if need_ready:
|
if need_ready:
|
||||||
ready = Signal()
|
ready = Signal()
|
||||||
@@ -337,11 +391,18 @@ class BBATop(Elaboratable):
|
|||||||
m.submodules.panel = panel
|
m.submodules.panel = panel
|
||||||
init_req = init_req | panel.reinit
|
init_req = init_req | panel.reinit
|
||||||
|
|
||||||
# cs_active lives in the exi domain; bring it to sync for the LED.
|
# NOT a clock-domain crossing, despite the primitive:
|
||||||
|
# `cap.cs_active` is already a sync-domain signal, because
|
||||||
|
# ExiCapture synchronises CS itself. What these two registers buy
|
||||||
|
# is a short input path into the panel's logic, and that is why
|
||||||
|
# they stay -- deleting them cost 25 LC (4305 -> 4330) when
|
||||||
|
# measured, since the panel's combinational cone then reaches back
|
||||||
|
# to ExiCapture's own synchroniser. `sync` is the binding timing
|
||||||
|
# domain in this design, so that is not a trade worth making for
|
||||||
|
# two flip-flops.
|
||||||
cs_a_sync = Signal()
|
cs_a_sync = Signal()
|
||||||
m.submodules.panel_cs = FFSynchronizer(
|
m.submodules.panel_cs = FFSynchronizer(
|
||||||
cap.cs_active, cs_a_sync, o_domain="sync")
|
cap.cs_active, cs_a_sync, o_domain="sync")
|
||||||
|
|
||||||
m.d.comb += [
|
m.d.comb += [
|
||||||
panel.cs_active.eq(cs_a_sync),
|
panel.cs_active.eq(cs_a_sync),
|
||||||
panel.rx_pulse .eq(asm.rx_irq),
|
panel.rx_pulse .eq(asm.rx_irq),
|
||||||
@@ -366,29 +427,31 @@ class BBATop(Elaboratable):
|
|||||||
]
|
]
|
||||||
|
|
||||||
if self._uart_shell:
|
if self._uart_shell:
|
||||||
# Interactive command shell driving the W5100 socket-1 UDP test.
|
# `eth` here is the W5100 master, built with enable_udp_test so it
|
||||||
# `eth` is the W5100 master built with enable_udp_test above.
|
# carries the extra socket the shell drives. The UDP test is the
|
||||||
|
# lowest-priority branch of that master's bus arbiter, so a GC
|
||||||
|
# frame always preempts it.
|
||||||
shell = UARTShell()
|
shell = UARTShell()
|
||||||
m.submodules.shell = shell
|
m.submodules.shell = shell
|
||||||
m.d.comb += [
|
m.d.comb += [
|
||||||
self.uart_tx .eq(shell.uart_tx),
|
self.uart_tx .eq(shell.uart_tx),
|
||||||
shell.uart_rx .eq(self.uart_rx),
|
shell.uart_rx.eq(self.uart_rx),
|
||||||
# send
|
|
||||||
eth.udp_send_req .eq(shell.udp_send_req),
|
eth.udp_send_req .eq(shell.udp_send_req),
|
||||||
eth.udp_dst_ip .eq(shell.udp_dst_ip),
|
eth.udp_dst_ip .eq(shell.udp_dst_ip),
|
||||||
eth.udp_pl_data .eq(shell.udp_pl_data),
|
eth.udp_pl_data .eq(shell.udp_pl_data),
|
||||||
eth.udp_pl_valid .eq(shell.udp_pl_valid),
|
eth.udp_pl_valid .eq(shell.udp_pl_valid),
|
||||||
eth.udp_pl_last .eq(shell.udp_pl_last),
|
eth.udp_pl_last .eq(shell.udp_pl_last),
|
||||||
shell.udp_pl_ready .eq(eth.udp_pl_ready),
|
shell.udp_pl_ready .eq(eth.udp_pl_ready),
|
||||||
shell.udp_test_busy .eq(eth.udp_test_busy),
|
shell.udp_test_busy.eq(eth.udp_test_busy),
|
||||||
# receive (reply)
|
|
||||||
eth.udp_rx_req .eq(shell.udp_rx_req),
|
eth.udp_rx_req .eq(shell.udp_rx_req),
|
||||||
shell.udp_rx_busy .eq(eth.udp_rx_busy),
|
shell.udp_rx_busy .eq(eth.udp_rx_busy),
|
||||||
shell.udp_rx_none .eq(eth.udp_rx_none),
|
shell.udp_rx_none .eq(eth.udp_rx_none),
|
||||||
shell.udp_rx_data .eq(eth.udp_rx_data),
|
shell.udp_rx_data .eq(eth.udp_rx_data),
|
||||||
shell.udp_rx_valid .eq(eth.udp_rx_valid),
|
shell.udp_rx_valid.eq(eth.udp_rx_valid),
|
||||||
shell.udp_rx_eof .eq(eth.udp_rx_eof),
|
shell.udp_rx_eof .eq(eth.udp_rx_eof),
|
||||||
eth.udp_rx_ready .eq(shell.udp_rx_ready),
|
eth.udp_rx_ready .eq(shell.udp_rx_ready),
|
||||||
]
|
]
|
||||||
|
|
||||||
if need_ready:
|
if need_ready:
|
||||||
@@ -399,13 +462,11 @@ class BBATop(Elaboratable):
|
|||||||
|
|
||||||
m.d.comb += eth.init_req.eq(init_req)
|
m.d.comb += eth.init_req.eq(init_req)
|
||||||
|
|
||||||
return m
|
|
||||||
|
|
||||||
|
|
||||||
# ── Integration testbench ─────────────────────────────────────────────────
|
# ── Integration testbench ─────────────────────────────────────────────────
|
||||||
# Drives real EXI Mode-3 transactions on the GC-facing pins and checks the
|
# Drives real EXI Mode-3 transactions on the GC-facing pins and checks what
|
||||||
# response — exercising the full chain ExiCapture (capture domain) ↔ byte FIFOs
|
# comes back, exercising the whole chain: the pin-clocked bit engine, the two
|
||||||
# ↔ BBARegisterFile (exi domain) ↔ sync modules, across all three clock domains.
|
# byte FIFOs that cross into `sync`, and every sync-domain module behind them.
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import os
|
import os
|
||||||
@@ -416,38 +477,36 @@ if __name__ == "__main__":
|
|||||||
status_panel=True) # also exercise the panel wiring
|
status_panel=True) # also exercise the panel wiring
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
# Capture ticks per SPI half-period -> EXI rate = 54 MHz / (2 * HALF).
|
# Delay units per SPI half-period -> EXI rate = 54 MHz / (2 * HALF).
|
||||||
# HALF=8 3.4 MHz fast regression default
|
# HALF=8 3.4 MHz fast regression default
|
||||||
# HALF=2 13.5 MHz
|
# HALF=2 13.5 MHz
|
||||||
# HALF=1 27 MHz THE HARDWARE RATE
|
# HALF=1 27 MHz THE HARDWARE RATE
|
||||||
# ⚠️ The default is 8, which clocks EXI EIGHT TIMES SLOWER than the real
|
# ⚠️ The default is 8, which clocks EXI EIGHT TIMES SLOWER than the real
|
||||||
# GC. At that rate the whole CDC response latency hides inside a single
|
# GC. At that rate the whole response latency hides inside a single bit
|
||||||
# bit time, so this suite passed green while hardware failed outright --
|
# time, so this suite can pass green while hardware fails outright -- that
|
||||||
# that is how both the header-format and pop-threshold bugs reached a
|
# is how both the header-format and pop-threshold bugs reached a console.
|
||||||
# console. Run `EXI_HALF=1 python -m exi_bba.bba_top` before trusting any
|
# Run `EXI_HALF=1 python -m exi_bba.bba_top` before trusting any
|
||||||
# timing-related change; at 27 MHz reads currently come back shifted right
|
# timing-related change. All three rates pass today.
|
||||||
# by one bit (04 02 02 00 -> 02 01 01 00, NWAYS 0x17 -> 0x0B), which is an
|
|
||||||
# open bug.
|
|
||||||
HALF = int(os.environ.get("EXI_HALF", "8"))
|
HALF = int(os.environ.get("EXI_HALF", "8"))
|
||||||
# The bit engine is now clocked by the EXI pin itself, so there is no
|
# The bit engine is clocked by the EXI pin, so there is no faster domain
|
||||||
# `capture` domain to count ticks against. Drive the SPI clock with real
|
# to count ticks against. The SPI clock is driven with real delays
|
||||||
# delays: one half-period = HALF * (1/54 MHz), preserving the old meaning
|
# instead: one half-period is HALF * (1/54 MHz), which keeps the rates in
|
||||||
# of HALF so the rates in the table above still hold.
|
# the table above meaning what they say.
|
||||||
HP = Period(MHz=54) # ONE capture-equivalent tick; delays are HP * n
|
HP = Period(MHz=54) # the unit of delay; half-periods are HP * HALF
|
||||||
# Gap between transactions (CS high). cs_active reaches the exi domain
|
# Gap between transactions (CS high). cs_active reaches `sync` through a
|
||||||
# through a 2-stage FFSynchronizer at 24 MHz, so CS must stay high for
|
# 2-stage FFSynchronizer at 24 MHz, so CS must stay high for ~3 sync
|
||||||
# ~3 exi cycles or the register file never sees the transaction end and
|
# cycles or the register file never sees the transaction end and swallows
|
||||||
# swallows the next one's header as data. At EXI_HALF=1 one HP is only
|
# the next one's header as data. At EXI_HALF=1 one HP is only
|
||||||
# 18.5 ns, far too short -- a real host leaves microseconds between
|
# 18.5 ns, far too short -- a real host leaves microseconds between
|
||||||
# exi_deselect() and the next exi_select().
|
# exi_deselect() and the next exi_select().
|
||||||
CS_GAP = Period(MHz=24) * 6
|
CS_GAP = Period(MHz=24) * 6
|
||||||
|
|
||||||
async def spi_byte(ctx, mosi_val):
|
async def spi_byte(ctx, mosi_val):
|
||||||
"""Drive one EXI Mode-3 byte; return the assembled MISO byte."""
|
"""Drive one EXI Mode-3 byte; return the assembled MISO byte."""
|
||||||
# Real Mode 3 master (CPOL=1, CPHA=1), which is what the GameCube is:
|
# A real Mode 3 master (CPOL=1, CPHA=1), which is what the GameCube
|
||||||
# drive MOSI on the FALLING edge, sample MISO on the RISING edge.
|
# is: drive MOSI on the FALLING edge, and sample MISO in the clock-LOW
|
||||||
# The old version drove MOSI then fell -- a Mode 2 master -- which is
|
# phase. A master that parks MOSI early cannot tell Mode 2 from
|
||||||
# what the Pi probe did and why it never exposed the mismatch.
|
# Mode 3, which is how that mix-up once reached hardware.
|
||||||
miso = 0
|
miso = 0
|
||||||
for bit in range(7, -1, -1):
|
for bit in range(7, -1, -1):
|
||||||
ctx.set(dut.exi_clk, 0) # falling: master drives
|
ctx.set(dut.exi_clk, 0) # falling: master drives
|
||||||
@@ -621,7 +680,7 @@ if __name__ == "__main__":
|
|||||||
await ctx.delay(HP * (HALF))
|
await ctx.delay(HP * (HALF))
|
||||||
|
|
||||||
# ── T6: FULL ETHERNET→SPRAM→GC LOOP ──────────────────────────────
|
# ── T6: FULL ETHERNET→SPRAM→GC LOOP ──────────────────────────────
|
||||||
# A frame arrives from the network (W5500 model) → W5500 master reads it
|
# A frame arrives from the network (W5100 bus model) → the master reads it
|
||||||
# → RXFrameAssembler writes it to the SPRAM ring → GC reads RWP then
|
# → RXFrameAssembler writes it to the SPRAM ring → GC reads RWP then
|
||||||
# DMA-reads the descriptor+frame back. Exercises the entire RX path.
|
# DMA-reads the descriptor+frame back. Exercises the entire RX path.
|
||||||
# The W5100 needs its init sequence (which sets MR.AI / opens socket 0)
|
# The W5100 needs its init sequence (which sets MR.AI / opens socket 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user