"""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.")