"""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 — ~1–2 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.")