diff --git a/exi_bba/bba_register_file.py b/exi_bba/bba_register_file.py index dc659b9..d1b2b63 100644 --- a/exi_bba/bba_register_file.py +++ b/exi_bba/bba_register_file.py @@ -202,11 +202,24 @@ class BBARegisterFile(Elaboratable): m.d.comb += self.ncra_sr.eq(r_ncra[3]) # start-receive bit # ── Transaction state ──────────────────────────────────────────── - hdr0 = Signal(8) - addr = Signal(13) + # Real GC<->BBA header (see docs/exi_protocol_real.md), verified + # against libogc and Swiss. Byte 0 bit7 selects the address space and + # bit6 is the write flag; there is NO length field -- the data phase + # runs until CS. + # command (bit7=0): [reg&0x3F, 00, 00, data] one 4-byte transfer + # register (bit7=1): [0x80|w<<6, ah, al, 00] + data phase until CS + addr = Signal(16) is_write = Signal() - xfer_len = Signal(2) # 0=1B … 3=4B - byte_ctr = Signal(2) + cmd_space = Signal() # this transaction is a command access + cmd_reg = Signal(6) + byte_ctr = Signal(3) # response-byte index (device-ID sequence) + cmd_wr = Signal() # command-space write pending + prefetched = Signal() # first response byte already pushed + cmd_wdata = Signal(8) + # EXI-side command registers. Swiss uses exactly two: 0x02 is the EXI + # interrupt mask and 0x03 the EXI interrupt status (write-1-to-clear). + r_cmd_imr = Signal(8) + r_cmd_ir = Signal(8) tx_frame_len = Signal(16) # TX trigger bookkeeping. `twd_armed` fires one cycle AFTER TWD_HI is # written, so tx_frame_len has both halves by then; `tx_queued` records @@ -226,7 +239,7 @@ class BBARegisterFile(Elaboratable): # sp_addr — next SPRAM byte address to request (auto-increments) # outstanding — SPRAM requests issued but whose responses are not yet # popped (bounds prefetch and is drained at end) - sp_addr = Signal(13) + sp_addr = Signal(16) outstanding = Signal(4) SP_LIMIT = 4 # max prefetch depth in flight @@ -234,7 +247,7 @@ class BBARegisterFile(Elaboratable): # pointer (set to the base in HEADER1, incremented per byte). Keeping # it registered keeps the 13-bit adder off the combinational path that # feeds the read-response mux → tx_fifo write data. - eff_addr = Signal(13) + eff_addr = Signal(16) rd_sel = eff_addr[0:8] # ── Combinational read-response value (non-SPRAM) ──────────────── @@ -259,11 +272,17 @@ class BBARegisterFile(Elaboratable): with m.Case(0): m.d.comb += devid.eq(0x04) with m.Case(1): m.d.comb += devid.eq(0x02) with m.Case(2): m.d.comb += devid.eq(0x02) - with m.Case(3): m.d.comb += devid.eq(0x00) + with m.Default(): m.d.comb += devid.eq(0x00) + + cmd_rdval = Signal(8) + with m.Switch(cmd_reg): + with m.Case(0x02): m.d.comb += cmd_rdval.eq(r_cmd_imr) + with m.Case(0x03): m.d.comb += cmd_rdval.eq(r_cmd_ir) + with m.Default(): m.d.comb += cmd_rdval.eq(0) rd_val = Signal(8) # response for the current non-SPRAM read byte - with m.If((addr == 0) & id_phase): - m.d.comb += rd_val.eq(devid) + with m.If(cmd_space): + m.d.comb += rd_val.eq(cmd_rdval) with m.Else(): m.d.comb += rd_val.eq(reg_rdval) @@ -292,35 +311,132 @@ class BBARegisterFile(Elaboratable): with m.FSM(domain="exi", name="exi_fsm"): with m.State("HEADER0"): + # Byte 0 selects the space and direction. The REAL protocol + # (docs/exi_protocol_real.md) has no length field: the GC + # clocks as many data bytes as it wants and ends with CS. with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) - m.d.exi += hdr0.eq(self.rx_data) - m.next = "HEADER1" + m.d.exi += [is_write.eq(self.rx_data[6]), prefetched.eq(0), + byte_ctr.eq(0), sp_req.eq(0), outstanding.eq(0)] + with m.If(self.rx_data[7]): # register space + m.d.exi += cmd_space.eq(0) + m.next = "RHDR_HI" + with m.Else(): # command space + m.d.exi += [cmd_space.eq(1), + cmd_reg.eq(self.rx_data[0:6]), + addr.eq(self.rx_data[0:6]), + eff_addr.eq(self.rx_data[0:6])] + # EXI_GetID (byte 0 == 0x00) gives only ONE header + # byte of warning before the GC starts clocking the + # reply, so push the first ID byte the instant byte 0 + # identifies the query -- the earliest point possible. + with m.If((self.rx_data == 0x00) & id_phase + & self.tx_rdy): + m.d.comb += [self.tx_data.eq(0x04), self.tx_en.eq(1)] + m.d.exi += byte_ctr.eq(1) + m.next = "CHDR_1" - with m.State("HEADER1"): + # ── register space: [0x80|w<<6, ah, al, 0x00] ────────────────── + with m.State("RHDR_HI"): with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) - new_addr = Cat(self.rx_data[2:8], hdr0[0:7]) # 13-bit addr - new_len = self.rx_data[0:2] - new_write = hdr0[7] + m.d.exi += addr[8:16].eq(self.rx_data) + m.next = "RHDR_LO" - m.d.exi += addr.eq(new_addr) - m.d.exi += eff_addr.eq(new_addr) # running pointer init - m.d.exi += xfer_len.eq(new_len) - m.d.exi += is_write.eq(new_write) - m.d.exi += byte_ctr.eq(0) - m.d.exi += sp_req.eq(0) - m.d.exi += drain_ctr.eq(0) + with m.State("RHDR_LO"): + # The address completes HERE, one whole byte before the data + # phase. That trailing 0x00 byte is the device's prefetch + # window -- the old 2-byte-plus-length format spent those bits + # on the length field and left zero slack, which is why the + # first on-console read came back 22 bit-times late. Kick the + # fetch off now so the answer is ready for the first data slot. + with m.If(self.rx_rdy): + m.d.comb += self.rx_en.eq(1) + full = Cat(self.rx_data, addr[8:16]) # 16-bit address + m.d.exi += [addr.eq(full), eff_addr.eq(full), + sp_addr.eq(full)] + with m.If(~is_write & (full >= 0x100) & spram_req.w_rdy): + m.d.exi += [spram_req.w_data.eq(full), + spram_req.w_en.eq(1), + sp_addr.eq(full + 1), + outstanding.eq(1)] + # MISO is driven LIVE from the tx FIFO head, so a byte + # pushed now would show up during the pad slot rather than + # the first data slot. Park a filler byte there -- MISO is + # don't-care for the whole header -- so the real first + # response byte lands one slot later, where it belongs. + with m.If(~is_write & self.tx_rdy): + m.d.comb += [self.tx_data.eq(0x00), self.tx_en.eq(1)] + m.next = "RHDR_PAD" - with m.If(new_write): + with m.State("RHDR_PAD"): + # Trailing header byte. For a register read, push the first + # response byte during this byte-time so it has crossed the + # tx CDC before the GC clocks data slot 0. + with m.If(self.rx_rdy): + m.d.comb += self.rx_en.eq(1) + with m.If(is_write): m.next = "WRITE" - with m.Elif(new_addr >= 0x100): - # SPRAM region: stream until CS deasserts (DMA-capable). - m.d.exi += sp_addr.eq(new_addr) - m.d.exi += outstanding.eq(0) + with m.Elif(addr >= 0x100): m.next = "SPRAM_STREAM" with m.Else(): m.next = "REG_READ" + with m.Elif(~is_write & (addr < 0x100) & self.tx_rdy & ~prefetched): + # Prefetch the FIRST response byte (once!) while the pad + # byte is still on the wire, so it has crossed the tx CDC + # before the GC clocks data slot 0. Must be one-shot: this + # branch is re-evaluated every cycle the pad byte has not + # arrived, and repeating it would consume the register + # stream early (symptom: reads shifted by a byte). + m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)] + m.d.exi += [eff_addr.eq(eff_addr + 1), prefetched.eq(1)] + + # ── command space: [reg, 00, 00, data] in ONE 4-byte transfer ── + with m.State("CHDR_1"): + with m.If(self.rx_rdy): + m.d.comb += self.rx_en.eq(1) + # EXI_GetID is its own shape: the GC writes 0x0000 (two + # bytes, so it lands here as command reg 0) and then clocks + # FOUR read bytes. Hand it the ID stream rather than the + # single-byte command response. + with m.If(~is_write & (cmd_reg == 0) & id_phase): + # byte_ctr is NOT reset here: HEADER0 already pushed + # ID byte 0 and advanced it. Resetting would restart + # the sequence and duplicate the first byte. + m.next = "DEVID" + with m.Else(): + m.next = "CHDR_2" + with m.Elif(~is_write & (cmd_reg == 0) & id_phase & self.tx_rdy): + # Device-ID query: keep feeding the ID while byte 1 is + # still on the wire so the tx FIFO stays ahead of the GC. + # NOT one-shot -- the FIFO is only 2 deep, and stopping + # after one byte leaves a hole in the next slot. + m.d.comb += [self.tx_data.eq(devid), self.tx_en.eq(1)] + m.d.exi += byte_ctr.eq(byte_ctr + 1) + with m.Elif(~is_write & self.tx_rdy & ~prefetched): + # Ordinary command read: response lands in byte slot 3. + m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)] + m.d.exi += prefetched.eq(1) + + with m.State("CHDR_2"): + with m.If(self.rx_rdy): + m.d.comb += self.rx_en.eq(1) + m.next = "CHDR_3" + + with m.State("CHDR_3"): + with m.If(self.rx_rdy): + m.d.comb += self.rx_en.eq(1) + with m.If(is_write): + m.d.exi += cmd_wr.eq(1) # applied below + m.d.exi += cmd_wdata.eq(self.rx_data) + m.next = "TRAILER" + + with m.State("TRAILER"): + # Soak up anything else the GC clocks, then wait for CS to rise. + with m.If(self.rx_rdy): + m.d.comb += self.rx_en.eq(1) + with m.If(~self.cs_active): + m.next = "HEADER0" with m.State("WRITE"): # Consume xfer_len+1 data bytes, writing the register file. @@ -374,32 +490,32 @@ class BBARegisterFile(Elaboratable): m.d.exi += tx_bytes.w_en.eq(1) # All other addresses silently ignored - with m.If(byte_ctr == xfer_len): - m.next = "HEADER0" - with m.Else(): - m.d.exi += byte_ctr.eq(byte_ctr + 1) - m.d.exi += eff_addr.eq(eff_addr + 1) + # No length field in the real protocol: consume data + # bytes until CS rises. + m.d.exi += eff_addr.eq(eff_addr + 1) + with m.If(~self.cs_active): + m.next = "HEADER0" with m.State("REG_READ"): - # Register / device-ID read (addr < 0x100): value available - # immediately, bounded by the header's xfer_len (≤4 bytes). - with m.If(self.tx_rdy): - m.d.comb += self.tx_data.eq(rd_val) - m.d.comb += self.tx_en.eq(1) - with m.If(byte_ctr == xfer_len): - m.next = "READ_DRAIN" - with m.Else(): - m.d.exi += byte_ctr.eq(byte_ctr + 1) - m.d.exi += eff_addr.eq(eff_addr + 1) + # Register read (addr < 0x100): stream until CS, like the SPRAM + # path. The real protocol has no length field. + with m.If(self.tx_rdy & self.cs_active): + m.d.comb += [self.tx_data.eq(rd_val), self.tx_en.eq(1)] + m.d.exi += eff_addr.eq(eff_addr + 1) + with m.If(self.rx_rdy): + m.d.comb += self.rx_en.eq(1) # drain dummies + with m.If(~self.cs_active): + m.next = "HEADER0" - with m.State("READ_DRAIN"): - # Discard the xfer_len+1 dummy bytes the GC clocks while reading. + with m.State("DEVID"): + # Stream 04 02 02 00 until CS rises. + with m.If(self.tx_rdy & self.cs_active): + m.d.comb += [self.tx_data.eq(devid), self.tx_en.eq(1)] + m.d.exi += byte_ctr.eq(byte_ctr + 1) with m.If(self.rx_rdy): m.d.comb += self.rx_en.eq(1) - with m.If(drain_ctr == xfer_len): - m.next = "HEADER0" - with m.Else(): - m.d.exi += drain_ctr.eq(drain_ctr + 1) + with m.If(~self.cs_active): + m.next = "HEADER0" with m.State("SPRAM_STREAM"): # Stream SPRAM bytes until CS deasserts — handles both ≤4-byte @@ -441,6 +557,13 @@ class BBARegisterFile(Elaboratable): with m.If((outstanding == 0) & ~self.rx_rdy & ~spram_rsp.r_rdy): m.next = "HEADER0" + # ── Command-space write, applied the cycle after CHDR_3 ────────── + with m.If(cmd_wr): + m.d.exi += cmd_wr.eq(0) + with m.Switch(cmd_reg): + with m.Case(0x02): m.d.exi += r_cmd_imr.eq(cmd_wdata) + with m.Case(0x03): m.d.exi += r_cmd_ir.eq(r_cmd_ir & ~cmd_wdata) + # ── Interrupt output ───────────────────────────────────────────── m.d.exi += self.exi_int_n.eq(~(r_ir & r_imr).any()) @@ -481,45 +604,81 @@ if __name__ == "__main__": # Drive the byte-stream interface directly (the SPI bit cadence and FIFOs # live in ExiCapture; here we model the byte producer/consumer). + # Responses can be pushed WHILE the header is still being clocked -- the + # device prefetches during the header's turnaround byte, and the device-ID + # reply starts from header byte 0. So every helper that advances time has + # to record tx pushes, or the early ones are silently lost and reads look + # shifted. tx_log is that shared record; collect_tx drains it first. + tx_log = [] + + def sample_tx(ctx): + if ctx.get(reg.tx_en): + tx_log.append(ctx.get(reg.tx_data)) + async def push_rx(ctx, b): """Present one received byte and wait for the register file to pop it.""" ctx.set(reg.rx_data, b) ctx.set(reg.rx_rdy, 1) while True: en = ctx.get(reg.rx_en) + sample_tx(ctx) await ctx.tick("exi") if en: break ctx.set(reg.rx_rdy, 0) async def collect_tx(ctx, n): - """Collect n response bytes pushed by the register file (bounded).""" + """Collect n response bytes, including any pushed during the header.""" out = [] + while tx_log and len(out) < n: + out.append(tx_log.pop(0)) for _ in range(3000): - if ctx.get(reg.tx_en): - out.append(ctx.get(reg.tx_data)) if len(out) >= n: break + if ctx.get(reg.tx_en): + out.append(ctx.get(reg.tx_data)) await ctx.tick("exi") return out + # Real protocol (docs/exi_protocol_real.md): register space is a 4-byte + # header [0x80|w<<6, ah, al, 0x00] followed by a data phase that runs + # until CS rises -- there is no length field. async def exi_read(ctx, addr, length=1): - hdr0 = (addr >> 6) & 0x7F - hdr1 = ((addr & 0x3F) << 2) | (length - 1) - await push_rx(ctx, hdr0) - await push_rx(ctx, hdr1) - result = await collect_tx(ctx, length) # READ pushes `length` bytes - for _ in range(length): # READ_DRAIN dummies - await push_rx(ctx, 0x00) + tx_log.clear() + ctx.set(reg.cs_active, 1) + await push_rx(ctx, 0x80) + await push_rx(ctx, (addr >> 8) & 0xFF) + await push_rx(ctx, addr & 0xFF) + await push_rx(ctx, 0x00) # turnaround / prefetch window + # The device parks a filler byte in the pad slot (MISO is don't-care + # for the whole header) so the first REAL response byte lands in data + # slot 0. This bench counts pushes rather than wire slots, so drop it. + result = (await collect_tx(ctx, length + 1))[1:] + ctx.set(reg.cs_active, 0) + await ctx.tick("exi").repeat(6) return result async def exi_write(ctx, addr, data): - hdr0 = 0x80 | ((addr >> 6) & 0x7F) - hdr1 = ((addr & 0x3F) << 2) | (len(data) - 1) - await push_rx(ctx, hdr0) - await push_rx(ctx, hdr1) + ctx.set(reg.cs_active, 1) + await push_rx(ctx, 0xC0) + await push_rx(ctx, (addr >> 8) & 0xFF) + await push_rx(ctx, addr & 0xFF) + await push_rx(ctx, 0x00) for b in data: await push_rx(ctx, b) + ctx.set(reg.cs_active, 0) + await ctx.tick("exi").repeat(6) + + async def exi_devid(ctx): + """EXI_GetID: write 0x0000 (2 bytes), then clock 4 read bytes.""" + tx_log.clear() + ctx.set(reg.cs_active, 1) + await push_rx(ctx, 0x00) # command space, reg 0, read + await push_rx(ctx, 0x00) + result = await collect_tx(ctx, 4) + ctx.set(reg.cs_active, 0) + await ctx.tick("exi").repeat(6) + return result # SPRAM contents the streaming-read test reads back (byte i = 0xA0+i). spram_mem = {0x100 + i: (0xA0 + i) & 0xFF for i in range(64)} @@ -555,7 +714,7 @@ if __name__ == "__main__": await ctx.tick("exi").repeat(8) # T1: Device ID (addr=0, 4-byte read) - result = await exi_read(ctx, 0x0000, length=4) + result = await exi_devid(ctx) if result != _DEVICE_ID: errors.append(f"T1 device ID: expected {_DEVICE_ID}, got {result}") print(f"T1 device ID: {[f'0x{b:02X}' for b in result]}") @@ -602,12 +761,16 @@ if __name__ == "__main__": errors.append(f"T5 INT_N after clear: expected 1, got {int_n}") print(f"T5 INT_N after IR clear: {int_n} (want 1)") - # T6: streaming SPRAM read (DMA) — read N>4 bytes from 0x100 by holding - # cs_active and clocking past the header's 4-byte length field. + # T6: streaming SPRAM read (DMA) — the real protocol has no length + # field, so the data phase simply runs for as long as CS is held. + # Swiss reads the packet buffer exactly this way (bba_ins -> DMA). N = 12 + tx_log.clear() ctx.set(reg.cs_active, 1) - await push_rx(ctx, 0x04) # hdr0 → addr[12:6]; addr 0x100, read - await push_rx(ctx, 0x00) # hdr1 → addr[5:0]=0, len field ignored + await push_rx(ctx, 0x80) # register space, read + await push_rx(ctx, 0x01) # addr[15:8] -> 0x0100 + await push_rx(ctx, 0x00) # addr[7:0] + await push_rx(ctx, 0x00) # turnaround byte got = [] for _ in range(5000): if ctx.get(reg.tx_en): diff --git a/exi_bba/bba_top.py b/exi_bba/bba_top.py index 4f8d748..0a9de7c 100644 --- a/exi_bba/bba_top.py +++ b/exi_bba/bba_top.py @@ -420,17 +420,16 @@ if __name__ == "__main__": return miso async def exi_read(ctx, addr, length): - """EXI immediate read: 2-byte header, clock-idle gap, then `length` bytes.""" - hdr0 = (addr >> 6) & 0x7F - # The header length field is only 2 bits ([1:0]); mask it so a long - # (DMA) read doesn't overflow length-1 into the addr[5:0] bits. For - # SPRAM reads the field is ignored anyway — the stream runs until CS. - hdr1 = ((addr & 0x3F) << 2) | ((length - 1) & 0x3) + """Register-space read, REAL protocol (docs/exi_protocol_real.md): + 4-byte header [0x80, ah, al, 0x00] then a data phase that runs until + CS. The trailing 0x00 is the device's prefetch window.""" ctx.set(dut.exi_cs_n, 0) ctx.set(dut.exi_clk, 1) await ctx.tick("capture").repeat(HALF) - await spi_byte(ctx, hdr0) - await spi_byte(ctx, hdr1) + await spi_byte(ctx, 0x80) + await spi_byte(ctx, (addr >> 8) & 0xFF) + await spi_byte(ctx, addr & 0xFF) + await spi_byte(ctx, 0x00) # EXI_Imm clock-idle gap: the core decodes the header and prefetches # responses into the tx FIFO before the GC clocks the data phase. await ctx.tick("capture").repeat(HALF * 12) @@ -440,19 +439,32 @@ if __name__ == "__main__": return result async def exi_write(ctx, addr, data): - """EXI immediate write: 2-byte header then the data bytes.""" - hdr0 = 0x80 | ((addr >> 6) & 0x7F) - hdr1 = ((addr & 0x3F) << 2) | (len(data) - 1) + """Register-space write: 4-byte header [0xC0, ah, al, 0x00] + data.""" ctx.set(dut.exi_cs_n, 0) ctx.set(dut.exi_clk, 1) await ctx.tick("capture").repeat(HALF) - await spi_byte(ctx, hdr0) - await spi_byte(ctx, hdr1) + await spi_byte(ctx, 0xC0) + await spi_byte(ctx, (addr >> 8) & 0xFF) + await spi_byte(ctx, addr & 0xFF) + await spi_byte(ctx, 0x00) for b in data: await spi_byte(ctx, b) ctx.set(dut.exi_cs_n, 1) await ctx.tick("capture").repeat(HALF) + async def exi_devid(ctx): + """EXI_GetID: write 0x0000 (two bytes) then clock four read bytes.""" + ctx.set(dut.exi_cs_n, 0) + ctx.set(dut.exi_clk, 1) + await ctx.tick("capture").repeat(HALF) + await spi_byte(ctx, 0x00) + await spi_byte(ctx, 0x00) + await ctx.tick("capture").repeat(HALF * 12) + out = [await spi_byte(ctx, 0x00) for _ in range(4)] + ctx.set(dut.exi_cs_n, 1) + await ctx.tick("capture").repeat(HALF) + return out + # ── W5100 indirect-bus slave model (drives w5100_data_i) ───────────── # Pre-loads a known MACRAW packet in the RX buffer so we can verify the full # ethernet→SPRAM→GC path. Same protocol as the W5100ParallelMaster bench. @@ -522,7 +534,7 @@ if __name__ == "__main__": await ctx.tick("capture").repeat(20) # T1: device ID — read 4 bytes from addr 0 → 0x04 0x02 0x02 0x00 - dev = await exi_read(ctx, 0x0000, 4) + dev = await exi_devid(ctx) print(f"T1 device ID: {[f'0x{b:02X}' for b in dev]}") if dev != [0x04, 0x02, 0x02, 0x00]: errors.append(f"T1 device ID: got {dev}") diff --git a/exi_bba/exi_j4.py b/exi_bba/exi_j4.py index a0cca41..2276d00 100644 --- a/exi_bba/exi_j4.py +++ b/exi_bba/exi_j4.py @@ -89,27 +89,39 @@ if __name__ == "__main__": return miso async def exi_read(ctx, pads, addr, length): - """EXI immediate read on `pads`: header, clock-idle gap, data phase.""" + """Register-space read, REAL protocol: 4-byte header then data.""" clk, mosi, cs_n = pads - hdr0 = (addr >> 6) & 0x7F - hdr1 = ((addr & 0x3F) << 2) | ((length - 1) & 0x3) ctx.set(cs_n, 0) ctx.set(clk, 1) await ctx.tick("capture").repeat(HALF) - await spi_byte(ctx, pads, hdr0) - await spi_byte(ctx, pads, hdr1) - await ctx.tick("capture").repeat(HALF * 12) # header-decode gap + await spi_byte(ctx, pads, 0x80) + await spi_byte(ctx, pads, (addr >> 8) & 0xFF) + await spi_byte(ctx, pads, addr & 0xFF) + await spi_byte(ctx, pads, 0x00) # turnaround byte result = [await spi_byte(ctx, pads, 0x00) for _ in range(length)] ctx.set(cs_n, 1) await ctx.tick("capture").repeat(HALF) return result + async def exi_devid(ctx, pads): + """EXI_GetID: write 0x0000 (2 bytes), clock 4 read bytes.""" + clk, mosi, cs_n = pads + ctx.set(cs_n, 0) + ctx.set(clk, 1) + await ctx.tick("capture").repeat(HALF) + await spi_byte(ctx, pads, 0x00) + await spi_byte(ctx, pads, 0x00) + out = [await spi_byte(ctx, pads, 0x00) for _ in range(4)] + ctx.set(cs_n, 1) + await ctx.tick("capture").repeat(HALF) + return out + async def testbench(ctx): await ctx.tick("capture").repeat(20) # T1: enumerate over the SP1 pads while J4 is unplugged (idle high). unplug(ctx, top.j4) - dev = await exi_read(ctx, top.sp1, 0x0000, 4) + dev = await exi_devid(ctx, top.sp1) print(f"T1 enumerate via SP1 (J4 idle): {[f'0x{b:02X}' for b in dev]}") if dev != DEVICE_ID: errors.append(f"T1 SP1 device ID: got {dev}") @@ -117,7 +129,7 @@ if __name__ == "__main__": # T2: enumerate over the J4 pads while SP1 is unplugged (idle high). unplug(ctx, top.sp1) - dev = await exi_read(ctx, top.j4, 0x0000, 4) + dev = await exi_devid(ctx, top.j4) print(f"T2 enumerate via J4 (SP1 idle): {[f'0x{b:02X}' for b in dev]}") if dev != DEVICE_ID: errors.append(f"T2 J4 device ID: got {dev}") @@ -125,7 +137,7 @@ if __name__ == "__main__": # T3: back to SP1, to prove using J4 left no sticky state behind. unplug(ctx, top.j4) - dev = await exi_read(ctx, top.sp1, 0x0000, 4) + dev = await exi_devid(ctx, top.sp1) print(f"T3 enumerate via SP1 again: {[f'0x{b:02X}' for b in dev]}") if dev != DEVICE_ID: errors.append(f"T3 SP1 re-enumerate: got {dev}") diff --git a/gc_bba_test/source/bba.c b/gc_bba_test/source/bba.c index 28972b9..a0c5373 100644 --- a/gc_bba_test/source/bba.c +++ b/gc_bba_test/source/bba.c @@ -16,18 +16,13 @@ static void gap(void) while (ticks_to_microsecs(diff_ticks(t0, gettime())) < bba_gap_us) ; } -/* Build the 2-byte EXI transaction header. - * byte0: [7]=write [6:0]=addr[12:6] - * byte1: [7:2]=addr[5:0] [1:0]=len-1 - * Returned packed big-endian in a u16 so EXI_Imm(...,2,...) clocks byte0 first - * (the GameCube is big-endian, so the high byte goes out first). */ -static u16 hdr(u16 addr, u32 len, int write) -{ - u8 b0 = (write ? 0x80 : 0x00) | ((addr >> 6) & 0x7F); - u8 b1 = (u8)(((addr & 0x3F) << 2) | ((len - 1) & 0x03)); - return ((u16)b0 << 8) | b1; -} - +/* Real GC<->BBA protocol (docs/exi_protocol_real.md), verified against libogc + * and Swiss. Register space is a 4-byte header followed by a data phase that + * runs until CS -- there is no length field. + * read [0x80, addr>>8, addr&0xFF, 0x00] + N bytes + * write [0xC0, addr>>8, addr&0xFF, 0x00] + N bytes + * The trailing 0x00 is the device's prefetch window: the address completes a + * whole byte before the data phase so the register file can fetch in time. */ static int begin(void) { int tries; @@ -53,10 +48,10 @@ static void end(void) EXI_Unlock(BBA_EXI_CHAN); } -static int put_hdr(u16 addr, u32 len, int write) +static int put_hdr(u16 addr, int write) { - u16 h = hdr(addr, len, write); - if (EXI_Imm(BBA_EXI_CHAN, &h, 2, EXI_WRITE, NULL) <= 0) return -3; + u32 h = ((u32)(write ? 0xC0 : 0x80) << 24) | ((u32)addr << 8); + if (EXI_Imm(BBA_EXI_CHAN, &h, 4, EXI_WRITE, NULL) <= 0) return -3; if (EXI_Sync(BBA_EXI_CHAN) <= 0) return -4; return 0; } @@ -64,12 +59,9 @@ static int put_hdr(u16 addr, u32 len, int write) int bba_read(u16 addr, void *buf, u32 len) { int rc; - if (len < 1 || len > 4) return -10; + if (len < 1) return -10; if ((rc = begin()) < 0) return rc; - if ((rc = put_hdr(addr, len, 0)) < 0) { end(); return rc; } - /* Hold the clock idle while the device decodes the header and prefetches - * the response into its TX FIFO. libogc's inter-call overhead alone is - * NOT enough at full EXI speed -- see bba_gap_us. */ + if ((rc = put_hdr(addr, 0)) < 0) { end(); return rc; } gap(); memset(buf, 0, len); if (EXI_ImmEx(BBA_EXI_CHAN, buf, len, EXI_READ) <= 0) { end(); return -5; } @@ -80,16 +72,30 @@ int bba_read(u16 addr, void *buf, u32 len) int bba_write(u16 addr, const void *buf, u32 len) { int rc; - u8 tmp[4]; - if (len < 1 || len > 4) return -10; + u8 tmp[64]; + if (len < 1 || len > sizeof(tmp)) return -10; memcpy(tmp, buf, len); if ((rc = begin()) < 0) return rc; - if ((rc = put_hdr(addr, len, 1)) < 0) { end(); return rc; } + if ((rc = put_hdr(addr, 1)) < 0) { end(); return rc; } if (EXI_ImmEx(BBA_EXI_CHAN, tmp, len, EXI_WRITE) <= 0) { end(); return -6; } end(); return 0; } +int bba_devid(u8 id[4]) +{ + int rc; + u16 h = 0x0000; /* command space, reg 0, read */ + if ((rc = begin()) < 0) return rc; + if (EXI_Imm(BBA_EXI_CHAN, &h, 2, EXI_WRITE, NULL) <= 0) { end(); return -3; } + if (EXI_Sync(BBA_EXI_CHAN) <= 0) { end(); return -4; } + gap(); + memset(id, 0, 4); + if (EXI_ImmEx(BBA_EXI_CHAN, id, 4, EXI_READ) <= 0) { end(); return -5; } + end(); + return 0; +} + int bba_r8(u16 addr) { u8 v; @@ -104,18 +110,9 @@ int bba_w8(u16 addr, u8 val) int bba_stream(u16 addr, void *buf, u32 len) { - int rc; - if (len < 1) return -10; - if ((rc = begin()) < 0) return rc; - /* Length field is capped at 2 bits; the device ignores it for addresses - * >= 0x0100 and streams until CS rises, so ask for 4 and clock as many - * bytes as we actually want. */ - if ((rc = put_hdr(addr, 4, 0)) < 0) { end(); return rc; } - gap(); - memset(buf, 0, len); - if (EXI_ImmEx(BBA_EXI_CHAN, buf, len, EXI_READ) <= 0) { end(); return -5; } - end(); - return 0; + /* Identical to bba_read now: the real protocol always streams until CS, + * so there is no separate "DMA" header form. */ + return bba_read(addr, buf, len); } int bba_tx_bytes(const u8 *buf, u32 len) diff --git a/gc_bba_test/source/bba.h b/gc_bba_test/source/bba.h index 187677c..ca37712 100644 --- a/gc_bba_test/source/bba.h +++ b/gc_bba_test/source/bba.h @@ -59,15 +59,19 @@ extern u32 bba_gap_us; /* All return 0 on success, negative on an EXI-layer failure. */ -/* Immediate read of `len` (1..4) bytes from `addr`. - * Clocks EXACTLY `len` bytes: over-clocking a register read leaves stale - * prefetched bytes queued in the gateware and desynchronises the NEXT - * transaction (hardware-confirmed 2026-09-30). Never pad this. */ +/* REGISTER-space read: 4-byte header [0x80, ah, al, 0x00] then `len` bytes. + * The real protocol has NO length field -- the data phase runs until CS, so + * `len` is unbounded. The trailing 0x00 header byte is the device's prefetch + * window. See docs/exi_protocol_real.md. */ int bba_read(u16 addr, void *buf, u32 len); -/* Immediate write of `len` (1..4) bytes to `addr`. */ +/* REGISTER-space write: 4-byte header [0xC0, ah, al, 0x00] then `len` bytes. */ int bba_write(u16 addr, const void *buf, u32 len); +/* EXI_GetID: write 0x0000 (two bytes) then read 4. Returns the device ID, + * which is a ONE-SHOT -- the first NCRA reset clears it until power cycle. */ +int bba_devid(u8 id[4]); + /* Convenience single-byte helpers. bba_r8 returns the byte, or <0 on error. */ int bba_r8(u16 addr); int bba_w8(u16 addr, u8 val); diff --git a/gc_bba_test/source/tests.c b/gc_bba_test/source/tests.c index 4ddfebd..ac1ec5e 100644 --- a/gc_bba_test/source/tests.c +++ b/gc_bba_test/source/tests.c @@ -19,7 +19,7 @@ static int t_devid(char *m, size_t n) snprintf(m, n, "skipped: one-shot, needs a console power cycle"); return 2; } - if ((rc = bba_read(0x0000, b, 4)) < 0) { snprintf(m, n, "EXI error %d", rc); return 1; } + if ((rc = bba_devid(b)) < 0) { snprintf(m, n, "EXI error %d", rc); return 1; } snprintf(m, n, "%02X %02X %02X %02X (want 04 02 02 00)", b[0], b[1], b[2], b[3]); return (b[0]==0x04 && b[1]==0x02 && b[2]==0x02 && b[3]==0x00) ? 0 : 1; }