Added full design created with Claude
This commit is contained in:
@@ -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 — ~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.")
|
||||
Reference in New Issue
Block a user