Files
rebbarb/exi_bba/status_panel.py
T
2026-06-13 18:35:38 +02:00

228 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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.")