Added full design created with Claude
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
"""EEPROM model — exi domain.
|
||||
|
||||
Emulates the MX98730EC's 93C46 serial EEPROM.
|
||||
|
||||
93C46 protocol (Microwire, bit-bang)
|
||||
-------------------------------------
|
||||
CS=1 activates the device.
|
||||
Data clocked on rising SK edge, 9-bit header then data:
|
||||
Bit 0: start (always 1)
|
||||
Bit 1: opcode MSB } READ = 10
|
||||
Bit 2: opcode LSB }
|
||||
Bits 3–8: 6-bit address (MSB first)
|
||||
|
||||
After the 9th rising SK the DO line presents the MSB of the 16-bit word.
|
||||
Each subsequent rising SK advances one bit (MSB→LSB).
|
||||
|
||||
Shift register `shift_in` convention
|
||||
--------------------------------------
|
||||
`Cat(di_s, shift_in[:-1])` places di_s at bit 0 and shifts existing bits up.
|
||||
After N edges:
|
||||
shift_in[N-1] = first bit received (start)
|
||||
shift_in[0] = last bit received so far
|
||||
|
||||
At bit_ctr==8 (after 8 edges, receiving 9th on di_s):
|
||||
shift_in[7] = start (bit 0)
|
||||
shift_in[6] = opcode MSB (bit 1)
|
||||
shift_in[5] = opcode LSB (bit 2)
|
||||
shift_in[4:0] = addr[5:1] (bits 3–7, MSB first→LSB first in register)
|
||||
di_s = addr[0] (bit 8)
|
||||
|
||||
opcode = Cat(shift_in[5], shift_in[6]) → 0b10 = READ
|
||||
address = Cat(di_s, shift_in[0:5]) → addr[0..5]
|
||||
|
||||
EEPROM content (64 × 16-bit words)
|
||||
-------------------------------------
|
||||
Words 0–2 hold the source MAC address (Nintendo OUI 00:09:BF:AA:BB:CC).
|
||||
The GC BBA driver reads words 0–3 then copies to PAR0–5.
|
||||
"""
|
||||
|
||||
from amaranth import *
|
||||
from amaranth.lib.cdc import FFSynchronizer
|
||||
|
||||
__all__ = ["EEPROMModel"]
|
||||
|
||||
_EEPROM_WORDS = [
|
||||
0x0009, # word 0: PAR0=0x00, PAR1=0x09
|
||||
0xBFAA, # word 1: PAR2=0xBF, PAR3=0xAA
|
||||
0xBBCC, # word 2: PAR4=0xBB, PAR5=0xCC
|
||||
0x0000, # word 3: checksum placeholder
|
||||
]
|
||||
_EEPROM_WORDS += [0x0000] * (64 - len(_EEPROM_WORDS))
|
||||
|
||||
_OP_READ = 0b10 # opcode for READ
|
||||
|
||||
|
||||
class EEPROMModel(Elaboratable):
|
||||
"""93C46 serial EEPROM model in the exi domain (read-only).
|
||||
|
||||
Ports
|
||||
-----
|
||||
sk / cs / di : bit-bang inputs (raw async; synchronized internally)
|
||||
do : serial data output
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.sk = Signal()
|
||||
self.cs = Signal()
|
||||
self.di = Signal()
|
||||
self.do = Signal()
|
||||
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
|
||||
words = Array([Signal(16, init=v, name=f"e{i}") for i, v in enumerate(_EEPROM_WORDS)])
|
||||
|
||||
# ── Input synchronization (async → exi, 2 stages) ────────────────
|
||||
sk_s = Signal()
|
||||
cs_s = Signal()
|
||||
di_s = Signal()
|
||||
m.submodules.sync_sk = FFSynchronizer(self.sk, sk_s, o_domain="exi")
|
||||
m.submodules.sync_cs = FFSynchronizer(self.cs, cs_s, o_domain="exi")
|
||||
m.submodules.sync_di = FFSynchronizer(self.di, di_s, o_domain="exi")
|
||||
|
||||
sk_prev = Signal()
|
||||
m.d.exi += sk_prev.eq(sk_s)
|
||||
rising_sk = Signal()
|
||||
m.d.comb += rising_sk.eq(sk_s & ~sk_prev)
|
||||
|
||||
# ── State ─────────────────────────────────────────────────────────
|
||||
shift_in = Signal(9)
|
||||
bit_ctr = Signal(4) # 0..8 during header receive
|
||||
|
||||
shift_out = Signal(16) # data word being shifted out MSB-first
|
||||
out_ctr = Signal(4) # 0..15, counts bits shifted out
|
||||
in_read = Signal() # 1 while outputting a word
|
||||
|
||||
# DO is combinatorial: MSB of shift_out while in read-out phase
|
||||
m.d.comb += self.do.eq(Mux(in_read, shift_out[15], 0))
|
||||
|
||||
with m.If(~cs_s):
|
||||
m.d.exi += bit_ctr.eq(0)
|
||||
m.d.exi += in_read.eq(0)
|
||||
m.d.exi += out_ctr.eq(0)
|
||||
|
||||
with m.Elif(rising_sk):
|
||||
with m.If(in_read):
|
||||
# Shift out next bit (MSB first: left shift, zero into LSB)
|
||||
m.d.exi += shift_out.eq(Cat(0, shift_out[:-1]))
|
||||
with m.If(out_ctr == 15):
|
||||
m.d.exi += in_read.eq(0)
|
||||
m.d.exi += out_ctr.eq(0)
|
||||
with m.Else():
|
||||
m.d.exi += out_ctr.eq(out_ctr + 1)
|
||||
|
||||
with m.Else():
|
||||
# Shift di_s in at bit 0 (existing bits move up)
|
||||
m.d.exi += shift_in.eq(Cat(di_s, shift_in[:-1]))
|
||||
m.d.exi += bit_ctr.eq(bit_ctr + 1)
|
||||
|
||||
with m.If(bit_ctr == 8):
|
||||
# 9th bit (di_s = addr[0]) arrives.
|
||||
# shift_in[7] = start, [6]=op_MSB, [5]=op_LSB, [4:0]=addr[5:1]
|
||||
op = Cat(shift_in[5], shift_in[6]) # 0b10 for READ
|
||||
adr = Cat(di_s, shift_in[0:5]) # addr[0..5]
|
||||
with m.If(op == _OP_READ):
|
||||
m.d.exi += shift_out.eq(words[adr])
|
||||
m.d.exi += in_read.eq(1)
|
||||
m.d.exi += out_ctr.eq(0)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
# ── Testbench ─────────────────────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from amaranth.sim import Simulator, Period
|
||||
|
||||
dut = EEPROMModel()
|
||||
errors = []
|
||||
|
||||
HALF = 6 # exi-domain ticks per SK half-period (much longer than sync latency)
|
||||
|
||||
async def eeprom_read(ctx, addr):
|
||||
"""93C46 READ at 6-bit address; returns 16-bit word.
|
||||
|
||||
DO is read BEFORE each rising SK edge, since in_read=1 causes
|
||||
shift_out[15] to be valid between edges. After 16 reads the full
|
||||
16-bit word is assembled MSB-first.
|
||||
"""
|
||||
ctx.set(dut.cs, 1)
|
||||
ctx.set(dut.sk, 0)
|
||||
await ctx.tick("exi").repeat(HALF)
|
||||
|
||||
# Transmit 9 bits: start(1) + opcode READ(10) + addr[5:0] MSB-first
|
||||
bits = [1, 1, 0]
|
||||
for a in range(5, -1, -1):
|
||||
bits.append((addr >> a) & 1)
|
||||
|
||||
for bit in bits:
|
||||
ctx.set(dut.di, bit)
|
||||
ctx.set(dut.sk, 1) # rising edge: DUT latches bit
|
||||
await ctx.tick("exi").repeat(HALF)
|
||||
ctx.set(dut.sk, 0)
|
||||
await ctx.tick("exi").repeat(HALF)
|
||||
|
||||
# After 9th falling SK: in_read=1, shift_out=word[addr], do=MSB.
|
||||
# Read DO before each rising edge (it is valid in the LOW phase).
|
||||
result = 0
|
||||
for _ in range(16):
|
||||
result = (result << 1) | ctx.get(dut.do) # sample before rising SK
|
||||
ctx.set(dut.sk, 1)
|
||||
await ctx.tick("exi").repeat(HALF)
|
||||
ctx.set(dut.sk, 0)
|
||||
await ctx.tick("exi").repeat(HALF)
|
||||
|
||||
ctx.set(dut.cs, 0)
|
||||
await ctx.tick("exi").repeat(HALF)
|
||||
return result
|
||||
|
||||
async def testbench(ctx):
|
||||
await ctx.tick("exi").repeat(4)
|
||||
ctx.set(dut.cs, 0)
|
||||
ctx.set(dut.sk, 0)
|
||||
ctx.set(dut.di, 0)
|
||||
await ctx.tick("exi").repeat(4)
|
||||
|
||||
w0 = await eeprom_read(ctx, 0)
|
||||
print(f"T1 word 0 = 0x{w0:04X} (expected 0x0009)")
|
||||
if w0 != 0x0009:
|
||||
errors.append(f"T1: word 0 = 0x{w0:04X}, expected 0x0009")
|
||||
|
||||
w1 = await eeprom_read(ctx, 1)
|
||||
print(f"T2 word 1 = 0x{w1:04X} (expected 0xBFAA)")
|
||||
if w1 != 0xBFAA:
|
||||
errors.append(f"T2: word 1 = 0x{w1:04X}, expected 0xBFAA")
|
||||
|
||||
w2 = await eeprom_read(ctx, 2)
|
||||
print(f"T3 word 2 = 0x{w2:04X} (expected 0xBBCC)")
|
||||
if w2 != 0xBBCC:
|
||||
errors.append(f"T3: word 2 = 0x{w2:04X}, expected 0xBBCC")
|
||||
|
||||
# T4: word 3 → 0x0000
|
||||
w3 = await eeprom_read(ctx, 3)
|
||||
print(f"T4 word 3 = 0x{w3:04X} (expected 0x0000)")
|
||||
if w3 != 0x0000:
|
||||
errors.append(f"T4: word 3 = 0x{w3:04X}, expected 0x0000")
|
||||
|
||||
sim = Simulator(dut)
|
||||
sim.add_clock(Period(MHz=24), domain="exi")
|
||||
sim.add_testbench(testbench)
|
||||
|
||||
with sim.write_vcd("EEPROMModel.vcd"):
|
||||
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