Files
rebbarb/exi_bba/eeprom_model.py
T
2026-06-13 18:35:38 +02:00

223 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 38: 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 37, 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 02 hold the source MAC address (Nintendo OUI 00:09:BF:AA:BB:CC).
The GC BBA driver reads words 03 then copies to PAR05.
"""
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.")