Added full design created with Claude

This commit is contained in:
Dennis Brentjes
2026-06-13 18:35:38 +02:00
parent 57b5b471b8
commit 8d0ab1d948
30 changed files with 7424 additions and 395 deletions
+227
View File
@@ -0,0 +1,227 @@
"""StatusPanel — 5-LED / 3-button bring-up panel (sync domain).
A development/diagnostics front panel for the iCEbreaker LED+button PMOD. It
turns the device's internal liveness signals into something you can watch on a
real GameCube during bring-up, and gives three buttons for manual control.
LEDs (logical, active-high; set `led_active_low=True` if the board sinks current)
led[0] heartbeat — ~12 Hz blink: clock alive, bitstream loaded
led[1] exi_active — stretched `cs_active`: the GC is talking on EXI
led[2] rx_act — stretched `rx_pulse`: a packet arrived from the net
led[3] tx_act — stretched `tx_pulse`: a packet went out
led[4] ready — `ready` level (e.g. ethernet init complete)
Buttons (raw pin level; `btn_active_low=True` for the usual pull-up wiring)
btn[0] eth_rst — while held, drive `eth_rst_n` low (reset the ethernet chip)
btn[1] reinit — on press, emit a one-cycle `reinit` pulse (force re-init)
btn[2] freeze — toggle: latch the rx/tx activity LEDs so a single one-shot
blink sticks until you unfreeze (catch a lone packet)
Single-cycle events (`rx_pulse`/`tx_pulse`) are stretched to ~`stretch_cycles`
so the eye can see them; `cs_active` is a level that is re-triggered while high.
Buttons are debounced (`debounce_cycles` stable samples) — same idea as
`rebbarb/debouncer.py`, inlined here to keep this module self-contained.
"""
from amaranth import *
__all__ = ["StatusPanel"]
class StatusPanel(Elaboratable):
def __init__(self, hb_bit=23, stretch_cycles=1_440_000,
debounce_cycles=240_000, led_active_low=False,
btn_active_low=True):
# hb_bit: heartbeat = bit `hb_bit` of a free-running counter
# (24 MHz / 2**23 ≈ 1.4 Hz). stretch_cycles ≈ 60 ms at 24 MHz.
self._hb_bit = hb_bit
self._stretch = stretch_cycles
self._deb = debounce_cycles
self._led_inv = led_active_low
self._btn_inv = btn_active_low
# Status inputs (sync domain)
self.cs_active = Signal() # level: EXI transaction in progress
self.rx_pulse = Signal() # 1-cycle: frame received
self.tx_pulse = Signal() # 1-cycle: frame sent
self.ready = Signal() # level: ethernet ready
# Raw button inputs (from pins)
self.btn = Signal(3)
# Outputs
self.led = Signal(5)
self.eth_rst_n = Signal(init=1) # btn0 held → 0
self.reinit = Signal() # btn1 press → 1-cycle pulse
def elaborate(self, platform):
m = Module()
# ── Heartbeat ────────────────────────────────────────────────────
hb = Signal(self._hb_bit + 1)
m.d.sync += hb.eq(hb + 1)
heartbeat = hb[self._hb_bit]
# ── Button conditioning (normalise polarity → debounce) ──────────
braw = Signal(3)
m.d.comb += braw.eq(self.btn ^ C(0b111 if self._btn_inv else 0, 3))
bdeb = Signal(3)
for i in range(3):
cnt = Signal(range(self._deb + 1), name=f"deb_cnt{i}")
with m.If(braw[i] == bdeb[i]):
m.d.sync += cnt.eq(0) # stable: hold
with m.Else():
m.d.sync += cnt.eq(cnt + 1) # changing: count stable samples
with m.If(cnt == self._deb - 1):
m.d.sync += [bdeb[i].eq(braw[i]), cnt.eq(0)]
# btn0: hold → ethernet reset asserted (active-low output)
m.d.comb += self.eth_rst_n.eq(~bdeb[0])
# btn1: rising edge → reinit pulse
b1_prev = Signal()
m.d.sync += b1_prev.eq(bdeb[1])
m.d.comb += self.reinit.eq(bdeb[1] & ~b1_prev)
# btn2: rising edge toggles freeze
b2_prev = Signal()
freeze = Signal()
m.d.sync += b2_prev.eq(bdeb[2])
with m.If(bdeb[2] & ~b2_prev):
m.d.sync += freeze.eq(~freeze)
# ── Activity stretchers (rx/tx), sticky while frozen ─────────────
def stretch(pulse, name):
cnt = Signal(range(self._stretch + 1), name=f"{name}_cnt")
sticky = Signal(name=f"{name}_sticky")
with m.If(pulse):
m.d.sync += cnt.eq(self._stretch)
with m.If(freeze):
m.d.sync += sticky.eq(1) # latch a one-shot when frozen
with m.Elif(cnt != 0):
m.d.sync += cnt.eq(cnt - 1)
with m.If(~freeze):
m.d.sync += sticky.eq(0) # clear sticky when unfrozen
return (cnt != 0) | sticky
rx_led = stretch(self.rx_pulse, "rx")
tx_led = stretch(self.tx_pulse, "tx")
# ── cs_active: level → stretched so brief transactions are visible ─
cs_cnt = Signal(range(self._stretch + 1))
with m.If(self.cs_active):
m.d.sync += cs_cnt.eq(self._stretch)
with m.Elif(cs_cnt != 0):
m.d.sync += cs_cnt.eq(cs_cnt - 1)
cs_led = cs_cnt != 0
leds = Cat(heartbeat, cs_led, rx_led, tx_led, self.ready)
m.d.comb += self.led.eq(leds ^ C(0b11111 if self._led_inv else 0, 5))
return m
# ── Testbench ─────────────────────────────────────────────────────────────
if __name__ == "__main__":
import sys
from amaranth.sim import Simulator, Period
# Tiny parameters so the timed behaviours are observable in a short sim.
dut = StatusPanel(hb_bit=3, stretch_cycles=8, debounce_cycles=3)
errors = []
async def settle(ctx, n=1):
await ctx.tick("sync").repeat(n)
async def testbench(ctx):
ctx.set(dut.btn, 0b111) # active-low idle (no press)
await settle(ctx, 4)
# T1: heartbeat toggles (bit 3 of the counter flips every 8 cycles)
h0 = ctx.get(dut.led) & 1
await settle(ctx, 8)
h1 = ctx.get(dut.led) & 1
if h0 == h1:
errors.append("T1 heartbeat did not toggle over 8 cycles")
print(f"T1 heartbeat toggled: {h0} -> {h1}")
# T2: rx pulse lights led[2] and it stretches, then clears
ctx.set(dut.rx_pulse, 1)
await settle(ctx, 1)
ctx.set(dut.rx_pulse, 0)
await settle(ctx, 1)
on = (ctx.get(dut.led) >> 2) & 1
if not on:
errors.append("T2 rx LED not lit after pulse")
await settle(ctx, 12) # > stretch_cycles
off = (ctx.get(dut.led) >> 2) & 1
if off:
errors.append("T2 rx LED did not clear after stretch")
print(f"T2 rx LED: on={on} then off={not off}")
# T3: ready level drives led[4]
ctx.set(dut.ready, 1)
await settle(ctx, 1)
if not ((ctx.get(dut.led) >> 4) & 1):
errors.append("T3 ready LED not lit")
ctx.set(dut.ready, 0)
print("T3 ready LED follows level")
# T4: btn0 held (active-low → drive 0) asserts eth_rst_n low after debounce
ctx.set(dut.btn, 0b110) # btn0 pressed
await settle(ctx, 6) # > debounce
if ctx.get(dut.eth_rst_n) != 0:
errors.append("T4 eth_rst_n not asserted while btn0 held")
ctx.set(dut.btn, 0b111) # release
await settle(ctx, 6)
if ctx.get(dut.eth_rst_n) != 1:
errors.append("T4 eth_rst_n not released")
print("T4 btn0 → eth_rst_n hold/release ok")
# T5: btn1 press emits exactly one reinit pulse
pulses = 0
ctx.set(dut.btn, 0b101) # btn1 pressed
for _ in range(10):
await settle(ctx, 1)
pulses += (ctx.get(dut.reinit) & 1)
ctx.set(dut.btn, 0b111)
await settle(ctx, 6)
if pulses != 1:
errors.append(f"T5 reinit pulses: got {pulses}, want 1")
print(f"T5 btn1 → reinit pulses={pulses}")
# T6: freeze (btn2) makes a single rx pulse stick
ctx.set(dut.btn, 0b011) # btn2 press → toggle freeze on
await settle(ctx, 6)
ctx.set(dut.btn, 0b111)
await settle(ctx, 2)
ctx.set(dut.rx_pulse, 1) # one-shot while frozen
await settle(ctx, 1)
ctx.set(dut.rx_pulse, 0)
await settle(ctx, 20) # well past stretch
stuck = (ctx.get(dut.led) >> 2) & 1
if not stuck:
errors.append("T6 frozen rx LED did not stick")
ctx.set(dut.btn, 0b011) # toggle freeze off
await settle(ctx, 6)
ctx.set(dut.btn, 0b111)
await settle(ctx, 2)
cleared = ((ctx.get(dut.led) >> 2) & 1) == 0
if not cleared:
errors.append("T6 rx LED did not clear after unfreeze")
print(f"T6 freeze: stuck={stuck} cleared_after_unfreeze={cleared}")
sim = Simulator(dut)
sim.add_clock(Period(MHz=24), domain="sync")
sim.add_testbench(testbench)
sim.run()
if errors:
print("\nFAILURES:")
for e in errors:
print(" ", e)
sys.exit(1)
else:
print("\nAll tests passed.")