228 lines
9.1 KiB
Python
228 lines
9.1 KiB
Python
"""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.")
|