223 lines
7.7 KiB
Python
223 lines
7.7 KiB
Python
"""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.")
|