7587636b48
bba_devid fails on a console with 00 04 02 02 -- the ID correct but one byte
late -- and it fails in its REAL shape, with the 16 us gap and exactly four
bytes clocked, so this is not the t_raw_id probe artefact. The register suite
passes, so register-space reads are fine; only command space is affected.
Cause is latency, not phase. A command-space header is two bytes, so the
reply must be in the tx FIFO by the LAST RISING EDGE of the header transfer:
one byte time, 296 ns at 27 MHz. The round trip is rx AsyncFIFO (~125 ns) +
a sync cycle + tx AsyncFIFO (~110 ns) = 280-320 ns, marginal by construction.
HEADER0 already pushed in the same cycle it consumed byte 0, so the FSM side
was already as early as it can be.
The inter-transfer gap cannot rescue it: the load happens on that last rising
edge and the clock is PAUSED afterwards, so no edge exists during the gap for
anything to update. That is why bba_devid's 16 us gap makes no difference.
Pre-pushing the ID at CS-fall does not work either -- the engine's
unconditional per-byte pops would eat it, and the header length is not known
until byte 0 has been decoded, which is the latency in question.
So take the round trip out of the path. The device ID is a hardwired constant
and belongs to the physical interface rather than to the register file, and
byte 0 == 0x00 identifies the query exactly (bit 7 clear = command space, bit
6 clear = read, reg 0). SPIMode3Slave now recognises it and answers
04 02 02 00 from the EXI clock domain, with no CDC at all. The only thing it
needs from sync is id_phase, a single bit that changes once, crossed with an
FFSynchronizer.
The register file's ID path is REMOVED rather than kept as a fallback: left
in, it would push bytes that a 2-byte header has too few pops to drain, which
is exactly how an earlier version made the NEXT enumeration come back a slot
late.
Tests follow the responsibility:
- spi_mode3_slave T7: the query returns 04 02 02 00 with the tx FIFO
deliberately feeding 0x00, so only the fast path can be answering.
- spi_mode3_slave T8: and it is GATED -- with id_arm low, 0x5A comes
through from the FIFO. Without this the fast path would hijack address 0
forever and the GC could never read NCRA back after its own reset.
- bba_register_file T1: retargeted to assert the FSM pushes NOTHING for an
ID query, which is the property that stops stale bytes corrupting the
following transaction.
Note what these tests do NOT establish: bba_top already passed the device ID
at EXI_HALF=1 before this change, because simulation cannot reproduce a
296 ns window miss -- the sync domain's latency relative to the EXI clock in
sim does not match silicon closely enough. T7/T8 prove the fast path works;
the evidence that it is NECESSARY is the hardware result plus the paused-clock
argument above.
--seeds 8: clk 28.4-31.0 MHz, EXI 56.7-64.4 MHz, 8/8 PASS. Best seed 2
(clk 30.98, EXI 59.47), flashed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
419 lines
19 KiB
Python
419 lines
19 KiB
Python
"""SPI Mode 3 byte-oriented slave for the EXI bus — CLOCKED BY EXI CLK.
|
|
|
|
CPOL=1, CPHA=1 (real Mode 3): CLK idles HIGH, the master CHANGES MOSI on the
|
|
falling edge and both ends SAMPLE on the rising edge.
|
|
|
|
Architecture
|
|
------------
|
|
The engine is clocked by the EXI clock itself rather than oversampling it from
|
|
a faster domain:
|
|
|
|
exiclk posedge of EXI CLK — sample MOSI, assemble RX bytes, AND
|
|
launch MISO one bit early (see the TX
|
|
note in elaborate)
|
|
CS_N asynchronous reset
|
|
|
|
Why, and what it fixes (hardware 2026-10-03)
|
|
--------------------------------------------
|
|
The previous version oversampled EXI CLK from a 54 MHz `capture` domain, which
|
|
put BIT-level timing across a clock-domain boundary: the engine's view of the
|
|
clock lagged the pin by 2 stages of `FFSynchronizer` plus a register, ~3 ticks
|
|
≈ 55 ns. One EXI bit at 27 MHz is 37 ns, so MISO came out ~2 bits late and the
|
|
master read `0x17` as `05 C0` (= `0x17 >> 2`). Register reads were correct up
|
|
to 4 MHz and failed from 8 MHz — the sweep read `ok=[1,1,1,0,0,0]`.
|
|
|
|
No amount of oversampling fixes that: at 54 MHz there are only 2 ticks per bit,
|
|
and the design cannot close the 81 MHz that 3x would need. Clocking the engine
|
|
from the EXI pin makes MISO clock-to-Q instead, and moves the ONLY remaining
|
|
crossing to the byte-level AsyncFIFOs, which have a whole byte-time (~296 ns at
|
|
27 MHz) of slack instead of a fraction of a bit.
|
|
|
|
EXI CLK (pin 44) sits at die (6,0) on the 5k-sg48, which is global buffer input
|
|
net 6 — so it can drive a real clock network with no board change.
|
|
|
|
The domains are created by the PARENT (`ExiCapture`) so the byte FIFOs can name
|
|
them; this module only uses `m.d.exiclk`.
|
|
"""
|
|
|
|
from amaranth import *
|
|
|
|
__all__ = ["SPIMode3Slave"]
|
|
|
|
|
|
class SPIMode3Slave(Elaboratable):
|
|
"""Byte-oriented SPI Mode 3 slave, clocked by the EXI clock.
|
|
|
|
Ports
|
|
-----
|
|
spi_clk / spi_mosi / spi_cs_n : raw inputs from the GC. spi_clk drives the
|
|
exiclk domain and spi_cs_n its async reset (both wired by
|
|
the parent); they are still listed here so the module is self-describing
|
|
and so spi_cs_n can gate MISO combinationally.
|
|
spi_miso : output to the GC; idles HIGH while CS is deasserted.
|
|
rx_byte : last complete received byte (valid while rx_valid is high)
|
|
rx_valid : 1-cycle pulse in `exiclk` when rx_byte holds a new byte
|
|
tx_byte : byte to transmit; sampled at each byte boundary
|
|
tx_load : 1-cycle pulse asking upstream for the next byte, one per byte
|
|
boundary. No header-length or slot-threshold logic: see the TX comment
|
|
in elaborate() for why unconditional pops are both sufficient and
|
|
necessary.
|
|
"""
|
|
|
|
def __init__(self, phase_meter=False):
|
|
# DIAGNOSTIC: when set, every byte the engine transmits is the constant
|
|
# 0x80 instead of the FIFO head. The load/shift/mux/output path is
|
|
# otherwise untouched, so the register file and both byte FIFOs drop
|
|
# out of the picture and what the host reads back is purely the
|
|
# engine's byte PHASE: 0x80 = aligned, 0x40 = 1 bit late, 0x20 = 2 bits
|
|
# late, and so on. Not a functional build.
|
|
self._phase_meter = phase_meter
|
|
|
|
self.spi_clk = Signal(init=1) # idles HIGH
|
|
self.spi_mosi = Signal()
|
|
self.spi_cs_n = Signal(init=1) # active LOW
|
|
|
|
self.spi_miso = Signal(init=1)
|
|
# The SAME bit as spi_miso, but combinational from the PRE-edge state,
|
|
# so it can feed an SB_IO output register clocked on the falling edge.
|
|
# Driving MISO from a flop inside the IO block removes the fabric
|
|
# routing between the shift register and the pad, which is what the
|
|
# 27 MHz GameCube read fails on -- see the note in synth.py.
|
|
self.miso_next = Signal(init=1)
|
|
|
|
# Asserted while the device-ID query is still answerable (the register
|
|
# file's id_phase, synchronised into this domain by the parent).
|
|
self.id_arm = Signal()
|
|
|
|
self.rx_byte = Signal(8)
|
|
self.rx_valid = Signal()
|
|
self.tx_byte = Signal(8)
|
|
self.tx_load = Signal()
|
|
|
|
def elaborate(self, platform):
|
|
m = Module()
|
|
|
|
# ── RX: sample MOSI on the RISING edge (Mode 3 samples on edge 2) ──
|
|
rx_shift = Signal(8)
|
|
rx_bit = Signal(3)
|
|
|
|
nxt = Cat(self.spi_mosi, rx_shift[:-1]) # MSB-first: shift left, new bit at LSB
|
|
m.d.exiclk += rx_shift.eq(nxt)
|
|
m.d.exiclk += rx_bit.eq(rx_bit + 1)
|
|
|
|
# rx_byte/rx_valid are COMBINATIONAL, not registered. The clock stops
|
|
# and CS rises immediately after a transaction's final bit, so a
|
|
# registered pulse would need one more clock edge that never arrives
|
|
# and the LAST byte of every transaction would be silently lost
|
|
# (symptom: a 4-byte write landed as DE AD BE 00, and a 34-byte TX
|
|
# stream as 15 bytes). Driving them combinationally makes the FIFO
|
|
# capture the byte on the very edge that completes it.
|
|
m.d.comb += self.rx_byte .eq(nxt)
|
|
m.d.comb += self.rx_valid.eq(rx_bit == 7)
|
|
|
|
# ── Device-ID fast path, answered HERE and not by the register file ──
|
|
# A command-space header is only TWO bytes, so a reply that travels
|
|
# rx AsyncFIFO -> sync FSM -> tx AsyncFIFO has one byte time (296 ns
|
|
# at 27 MHz) to make it back. That round trip costs ~280-320 ns, so
|
|
# it is marginal by construction, and measured on a console it LOST:
|
|
# the ID came back one byte late (00 04 02 02). The inter-transfer
|
|
# gap cannot rescue it either -- the reply must be in hand by the last
|
|
# rising edge of the header transfer, and the clock is paused after
|
|
# that edge, so nothing can update during the gap.
|
|
#
|
|
# The device ID is a CONSTANT and belongs to the physical interface
|
|
# rather than to the register file, so answer it from this domain and
|
|
# skip both crossings entirely. byte 0 == 0x00 identifies the query
|
|
# exactly: bit 7 clear = command space, bit 6 clear = read, reg = 0.
|
|
byte_idx = Signal(3) # bytes completed this transaction (saturating)
|
|
id_mode = Signal()
|
|
with m.If(rx_bit == 7):
|
|
with m.If(byte_idx != 7):
|
|
m.d.exiclk += byte_idx.eq(byte_idx + 1)
|
|
with m.If((byte_idx == 0) & (nxt == 0x00) & self.id_arm):
|
|
m.d.exiclk += id_mode.eq(1)
|
|
|
|
# Byte N of the reply is loaded at the end of header byte N, so the
|
|
# four ID bytes are selected by byte_idx 1..4.
|
|
id_byte = Signal(8)
|
|
with m.Switch(byte_idx):
|
|
with m.Case(1): m.d.comb += id_byte.eq(0x04)
|
|
with m.Case(2): m.d.comb += id_byte.eq(0x02)
|
|
with m.Case(3): m.d.comb += id_byte.eq(0x02)
|
|
with m.Default(): m.d.comb += id_byte.eq(0x00)
|
|
id_sel = Signal()
|
|
m.d.comb += id_sel.eq(id_mode & (byte_idx >= 1) & (byte_idx <= 4))
|
|
|
|
# ── TX: launch MISO a FULL CYCLE EARLY, on the RISING edge ────────
|
|
# The host samples MISO on the rising edge. The obvious thing is to
|
|
# drive it on the preceding FALLING edge, which gives the output path
|
|
# a HALF period -- 18.5 ns at 27 MHz. That is not enough on this
|
|
# part, and the shortfall is irreducible:
|
|
#
|
|
# MISO changes at (falling edge AT THE CONNECTOR)
|
|
# + input pad delay
|
|
# + GLOBAL CLOCK NETWORK INSERTION <-- the big one
|
|
# + IOB clock-to-Q
|
|
# + output pad delay
|
|
# ... and the host still needs its own setup time on top.
|
|
#
|
|
# Measured on a GameCube 2026-10-03 with PHASE_METER=1 (the engine
|
|
# drives a constant 0x80 per byte, so the host reads the engine's byte
|
|
# phase directly, with the register file and both byte FIFOs out of the
|
|
# path). Per-speed dump, first two slots per EXI speed index:
|
|
#
|
|
# 0:8080 1:8080 2:8080 3:8080 4:8080 5:C040
|
|
#
|
|
# Perfect to 16 MHz, one bit late at 27 MHz. An 8-slot dump at 27 MHz
|
|
# read C0 40 40 40 C0 40 40 40 -- a UNIFORM one-bit lag (ones at bit
|
|
# 1, 9, 17, 25, 33, ...), with extra highs at bits 0 and 32 where
|
|
# libogc splits the read into two 4-byte transfers and MISO tri-states
|
|
# to its pull-up in the gap.
|
|
#
|
|
# Crucially, ~9 ns was removed from the fabric output path first (the
|
|
# EXI_J4 pad merge, then the CS mux, then the whole fabric route by
|
|
# moving the output into an SB_IO register) and the result stayed BYTE
|
|
# IDENTICAL. Nine nanoseconds out of an 18.5 ns budget cannot change
|
|
# nothing if propagation delay were the whole story, so the remaining
|
|
# cost is the part no RTL change can touch: getting the clock in and
|
|
# the data back out of the chip.
|
|
#
|
|
# So stop shrinking the delay and DOUBLE THE BUDGET. Present the bit
|
|
# the host will sample at rising edge N on rising edge N-1, giving the
|
|
# whole path a FULL period (37 ns at 27 MHz) instead of half. The
|
|
# output then changes shortly AFTER the edge on which the host samples
|
|
# the previous bit, which is safe in both directions: the insertion
|
|
# delay that caused the problem now covers the host's hold time, and
|
|
# a full period covers its setup.
|
|
#
|
|
# `tx_out` is therefore clocked on the RISING edge and loads at
|
|
# rx_bit == 7 -- one bit-time earlier than a falling-edge shifter
|
|
# loading at rx_bit == 0. init=0xFF parks MISO high under CS reset.
|
|
# This also removes `tx_hold` and the whole exiclk_n domain: the
|
|
# engine now has ONE clock edge and ONE phase reference.
|
|
tx_out = Signal(8, init=0xFF)
|
|
tx_src = Signal(8) # the byte loaded at the next byte boundary
|
|
m.d.comb += tx_src.eq(
|
|
Mux(id_sel, id_byte,
|
|
0x80 if self._phase_meter else self.tx_byte))
|
|
m.d.comb += self.tx_load.eq(rx_bit == 7)
|
|
with m.If(rx_bit == 7):
|
|
m.d.exiclk += tx_out.eq(tx_src)
|
|
with m.Else():
|
|
m.d.exiclk += tx_out.eq(Cat(0, tx_out[:-1]))
|
|
|
|
m.d.comb += self.spi_miso.eq(tx_out[7])
|
|
# What tx_out[7] WILL be after the next RISING edge -- this is what the
|
|
# SB_IO output register captures. Mirrors the load/shift choice above
|
|
# exactly; T6 proves the two agree bit for bit.
|
|
m.d.comb += self.miso_next.eq(Mux(rx_bit == 7, tx_src[7], tx_out[6]))
|
|
|
|
return m
|
|
|
|
|
|
# ── Testbench ─────────────────────────────────────────────────────────────
|
|
|
|
class _Harness(Elaboratable):
|
|
"""Wraps the slave with the pin-driven domain its parent normally
|
|
creates, so the engine can be exercised standalone."""
|
|
|
|
def __init__(self):
|
|
self.dut = SPIMode3Slave()
|
|
|
|
def elaborate(self, platform):
|
|
m = Module()
|
|
m.domains.exiclk = cd_p = ClockDomain(async_reset=True)
|
|
d = self.dut
|
|
m.d.comb += [
|
|
cd_p.clk.eq(d.spi_clk), cd_p.rst.eq(d.spi_cs_n),
|
|
]
|
|
m.submodules.dut = d
|
|
return m
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
from amaranth.sim import Simulator, Period
|
|
|
|
HP = Period(MHz=27) / 2 # one EXI half-period at the hardware rate
|
|
top = _Harness()
|
|
dut = top.dut
|
|
|
|
failures = []
|
|
|
|
def check(name, got, want):
|
|
ok = got == want
|
|
print(f" {'ok ' if ok else 'FAIL'} {name}: got {got}, want {want}")
|
|
if not ok:
|
|
failures.append(name)
|
|
|
|
# A STRICT Mode 3 master. Unlike a convenience model that parks MOSI long
|
|
# before the edge, this one changes MOSI exactly ON the falling edge and
|
|
# samples MISO in the CLK-low phase. That is what makes the edge
|
|
# convention observable: a slave that sampled MOSI on the falling edge
|
|
# would capture the previous bit, and one that drove MISO on the rising
|
|
# edge would come out a bit late. Both bugs shipped to hardware against a
|
|
# lax master that could not see them.
|
|
async def spi_byte(ctx, mosi_val, probe_rx=False):
|
|
miso = 0
|
|
rx = None
|
|
for bit in range(7, -1, -1):
|
|
ctx.set(dut.spi_clk, 0) # falling edge
|
|
ctx.set(dut.spi_mosi, (mosi_val >> bit) & 1)
|
|
await ctx.delay(HP)
|
|
miso = (miso << 1) | ctx.get(dut.spi_miso) # sample in CLK-low
|
|
if probe_rx and bit == 0:
|
|
rx = (ctx.get(dut.rx_valid), ctx.get(dut.rx_byte))
|
|
ctx.set(dut.spi_clk, 1) # rising edge: slave samples
|
|
await ctx.delay(HP)
|
|
return (miso, rx) if probe_rx else miso
|
|
|
|
async def cs_low(ctx):
|
|
ctx.set(dut.spi_clk, 1)
|
|
ctx.set(dut.spi_cs_n, 0)
|
|
await ctx.delay(HP)
|
|
|
|
async def cs_high(ctx):
|
|
ctx.set(dut.spi_clk, 1)
|
|
await ctx.delay(HP)
|
|
ctx.set(dut.spi_cs_n, 1)
|
|
await ctx.delay(HP * 4)
|
|
|
|
async def tb(ctx):
|
|
ctx.set(dut.spi_cs_n, 1)
|
|
ctx.set(dut.spi_clk, 1)
|
|
ctx.set(dut.tx_byte, 0)
|
|
await ctx.delay(HP * 4)
|
|
|
|
# T1 — RX samples MOSI on the RISING edge, MSB first.
|
|
# rx_byte/rx_valid are combinational and peak during the final bit,
|
|
# which is the instant the parent's FIFO captures them.
|
|
await cs_low(ctx)
|
|
seen = []
|
|
for b in (0x80, 0x00, 0x31, 0x00):
|
|
_, (valid, byte) = await spi_byte(ctx, b, probe_rx=True)
|
|
seen.append((valid, byte))
|
|
await cs_high(ctx)
|
|
check("T1 rx bytes", [b for _, b in seen], [0x80, 0x00, 0x31, 0x00])
|
|
check("T1 rx_valid", [v for v, _ in seen], [1, 1, 1, 1])
|
|
|
|
# T2 — TX: the byte latched at the end of slot N appears in slot N+1,
|
|
# MSB first. Slot 0 is the reset value, before any load.
|
|
await cs_low(ctx)
|
|
ctx.set(dut.tx_byte, 0xA5)
|
|
s0 = await spi_byte(ctx, 0x00)
|
|
ctx.set(dut.tx_byte, 0x3C)
|
|
s1 = await spi_byte(ctx, 0x00)
|
|
ctx.set(dut.tx_byte, 0x00)
|
|
s2 = await spi_byte(ctx, 0x00)
|
|
await cs_high(ctx)
|
|
# Slot 0 is the header byte: the host ignores MISO there and the
|
|
# engine has not loaded anything yet, so it reads the CS-reset parking
|
|
# value (tx_out init=0xFF, which is what keeps MISO high while
|
|
# deselected). The bits the host actually uses start at slot 1.
|
|
check("T2 miso slot0 (pre-load)", hex(s0), hex(0xFF))
|
|
check("T2 miso slot1", hex(s1), hex(0xA5))
|
|
check("T2 miso slot2", hex(s2), hex(0x3C))
|
|
|
|
# T3 — tx_load pulses exactly once per byte, during the final bit.
|
|
await cs_low(ctx)
|
|
loads = 0
|
|
for _ in range(3):
|
|
for bit in range(8):
|
|
ctx.set(dut.spi_clk, 0)
|
|
await ctx.delay(HP)
|
|
loads += ctx.get(dut.tx_load)
|
|
ctx.set(dut.spi_clk, 1)
|
|
await ctx.delay(HP)
|
|
await cs_high(ctx)
|
|
check("T3 tx_load pulses", loads, 3)
|
|
|
|
# T4 — MISO releases HIGH when CS is deasserted, so it never fights
|
|
# another EXI device sharing the bus.
|
|
check("T4 miso idles high", ctx.get(dut.spi_miso), 1)
|
|
|
|
# T5 — a transaction that BEGINS ON A RISING EDGE. The host asserts
|
|
# CS while CLK is already low, so the first edge the device sees is a
|
|
# rise and the transaction carries one fewer FALLING edge than rising.
|
|
# A TX phase counted on falling edges slips one bit here and never
|
|
# recovers; a phase taken from rx_bit cannot.
|
|
#
|
|
# THIS IS THE HARDWARE BUG. On a GameCube the phase-meter build read
|
|
# 0x20 (= 0x80 >> 2) in every slot, identically at 1 MHz and 27 MHz.
|
|
# Nothing in the old testbench started a transaction this way -- every
|
|
# case asserted CS with CLK high -- so the falling and rising counts
|
|
# always matched and the slip was structurally invisible, exactly as
|
|
# the Mode 2/3 mix-up was invisible to a master that parked MOSI early.
|
|
ctx.set(dut.spi_clk, 0) # CLK already LOW ...
|
|
ctx.set(dut.spi_cs_n, 0) # ... when CS asserts
|
|
await ctx.delay(HP)
|
|
ctx.set(dut.tx_byte, 0xA5)
|
|
await spi_byte(ctx, 0x00) # slot 0: header, MISO don't-care
|
|
ctx.set(dut.tx_byte, 0x3C)
|
|
s1 = await spi_byte(ctx, 0x00)
|
|
s2 = await spi_byte(ctx, 0x00)
|
|
await cs_high(ctx)
|
|
check("T5 rising-edge start, slot1", hex(s1), hex(0xA5))
|
|
check("T5 rising-edge start, slot2", hex(s2), hex(0x3C))
|
|
|
|
# T6 — miso_next must equal what spi_miso BECOMES on the next RISING
|
|
# edge. The synthesised build drives the pad from an SB_IO register
|
|
# clocked on that edge (synth.py), so if this ever diverges the
|
|
# hardware and the simulation stop agreeing about MISO -- which is
|
|
# precisely the class of bug that has cost this design the most time.
|
|
ctx.set(dut.spi_clk, 1)
|
|
ctx.set(dut.spi_cs_n, 0)
|
|
await ctx.delay(HP)
|
|
ctx.set(dut.tx_byte, 0x6B)
|
|
mismatch = 0
|
|
for _ in range(3):
|
|
for _bit in range(8):
|
|
ctx.set(dut.spi_clk, 0)
|
|
await ctx.delay(HP)
|
|
predicted = ctx.get(dut.miso_next) # before the RISING edge
|
|
ctx.set(dut.spi_clk, 1)
|
|
await ctx.delay(HP)
|
|
if ctx.get(dut.spi_miso) != predicted:
|
|
mismatch += 1
|
|
await cs_high(ctx)
|
|
check("T6 miso_next predicts spi_miso", mismatch, 0)
|
|
|
|
# T7 — the device-ID fast path. A command-space query (byte 0 = 0x00,
|
|
# byte 1 = 0x00) must be answered 04 02 02 00 straight from this
|
|
# domain, with the tx FIFO empty: the register file cannot make the
|
|
# round trip inside a 2-byte header's one byte time, which on a
|
|
# console returned the ID one byte late (00 04 02 02).
|
|
ctx.set(dut.id_arm, 1)
|
|
ctx.set(dut.tx_byte, 0x00) # FIFO deliberately contributes nothing
|
|
await cs_low(ctx)
|
|
await spi_byte(ctx, 0x00) # header byte 0 identifies the query
|
|
await spi_byte(ctx, 0x00) # header byte 1
|
|
got = [await spi_byte(ctx, 0x00) for _ in range(4)]
|
|
await cs_high(ctx)
|
|
check("T7 device ID", [hex(b) for b in got],
|
|
[hex(x) for x in (0x04, 0x02, 0x02, 0x00)])
|
|
|
|
# T8 — and it must be GATED. Once the GC's first NCRA reset clears
|
|
# id_phase, address 0 reads back NCRA, so the fast path must stop
|
|
# hijacking the reply and let the FIFO through.
|
|
ctx.set(dut.id_arm, 0)
|
|
ctx.set(dut.tx_byte, 0x5A)
|
|
await cs_low(ctx)
|
|
await spi_byte(ctx, 0x00)
|
|
await spi_byte(ctx, 0x00)
|
|
got2 = [await spi_byte(ctx, 0x00) for _ in range(2)]
|
|
await cs_high(ctx)
|
|
check("T8 fast path gated by id_arm", [hex(b) for b in got2],
|
|
[hex(0x5A), hex(0x5A)])
|
|
|
|
sim = Simulator(top)
|
|
sim.add_testbench(tb)
|
|
with sim.write_vcd("SPIMode3Slave.vcd"):
|
|
sim.run()
|
|
|
|
if failures:
|
|
print("\nFAILURES:")
|
|
for f in failures:
|
|
print(" -", f)
|
|
sys.exit(1)
|
|
print("\nAll tests passed.")
|