Files
2026-06-13 18:35:38 +02:00

534 lines
24 KiB
Python
Raw Permalink 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.
"""BBATop — top-level elaboratable for the GC BBA FPGA replacement.
Clock domains
-------------
capture : 54 MHz, from 12 MHz crystal via SB_PLL40_PAD (DIVR=0 DIVF=71 DIVQ=4)
exi/sync : 24 MHz, from the iCE40UP5K internal SB_HFOSC (÷2, CLKHF_DIV=0b01)
Submodule instantiation and signal wiring
-----------------------------------------
See CLAUDE.md "Module Breakdown" and "CDC Signal Inventory" for the full list.
"""
from amaranth import *
from exi_bba.exi_capture import ExiCapture
from exi_bba.bba_register_file import BBARegisterFile
from exi_bba.spram_arbiter import SPRAMArbiter
from exi_bba.rx_frame_assembler import RXFrameAssembler
from exi_bba.tx_frame_drain import TXFrameDrain
from exi_bba.w5500_spi_master import W5500SPIMaster
from exi_bba.w5100_parallel_master import W5100ParallelMaster
from exi_bba.status_panel import StatusPanel
from amaranth.lib.cdc import FFSynchronizer
__all__ = ["BBATop"]
class BBATop(Elaboratable):
"""Top-level module. Wires all submodules and defines clock domains.
External ports (exposed for platform or testbench connection)
-------------------------------------------------------------
EXI / GC interface (SPI Mode 3)
exi_clk / exi_mosi / exi_cs_n : inputs from GC
exi_miso : output to GC
int_n : interrupt output (active low)
W5500 SPI interface (SPI Mode 0)
w5500_clk / w5500_mosi / w5500_cs_n : outputs to W5500
w5500_miso : input from W5500
w5500_int_n : W5500 interrupt (input, active low)
w5500_rst_n : W5500 hardware reset (output, active low)
"""
def __init__(self, eth="w5100", reset_cycles=24000, status_panel=False):
# Ethernet back-end: "w5100" (indirect parallel bus, reaches the EXI
# ceiling) or "w5500" (SPI, ~12 Mbit/s). Both expose the identical
# tx/rx/init/par interface, so only the physical pins differ.
self._eth = eth
# MR-reset settle wait passed to the ethernet master (~1 ms on hardware;
# the testbench overrides with a small value for fast simulation).
self._reset_cycles = reset_cycles
# Optional bring-up status panel (drives onboard LEDs/button on the
# iCEbreaker — see synth.py). panel_led bit order matches StatusPanel.
self._status_panel = status_panel
# EXI (GC side)
self.exi_clk = Signal(init=1)
self.exi_mosi = Signal()
self.exi_cs_n = Signal(init=1)
self.exi_miso = Signal()
self.int_n = Signal(init=1)
if eth == "w5500":
# W5500 SPI
self.w5500_clk = Signal()
self.w5500_mosi = Signal()
self.w5500_miso = Signal()
self.w5500_cs_n = Signal(init=1)
self.w5500_int_n = Signal(init=1)
self.w5500_rst_n = Signal(init=1)
else:
# W5100 indirect parallel bus. data_o/data_oe/data_i are the FPGA
# side of a bidirectional D[7:0] (wrapped in a tristate SB_IO at the
# platform level); a board ties the upper address lines to 0 so only
# A[1:0] are wired.
self.w5100_addr = Signal(2)
self.w5100_data_o = Signal(8)
self.w5100_data_oe = Signal()
self.w5100_data_i = Signal(8)
self.w5100_cs_n = Signal(init=1)
self.w5100_rd_n = Signal(init=1)
self.w5100_wr_n = Signal(init=1)
self.w5100_int_n = Signal(init=1)
self.w5100_rst_n = Signal(init=1)
if status_panel:
self.panel_led = Signal(5) # to onboard LEDs (see StatusPanel)
self.panel_btn = Signal(3) # from onboard button(s)
def elaborate(self, platform):
m = Module()
# ── Clock domain generation ───────────────────────────────────────
# Three domains, two physical sources (1 PLL + 1 internal HFOSC):
# capture @ 54 MHz (PLL) — SPI bit engine only; oversamples the
# 27 MHz EXI clock 2× (robust Mode-3).
# exi @ 24 MHz (HFOSC) — register file / transaction FSM.
# sync @ 24 MHz (HFOSC) — SPRAM, RX/TX engines, ethernet master.
# exi and sync share the HFOSC net (frequency- and phase-matched); the
# AsyncFIFOs between them are still valid CDC and keep the module
# boundaries clean. Only the tiny capture front-end needs the fast
# clock — which is why 27 MHz-EXI / OG performance is reachable on the
# iCE40UP5K even though the register-file logic tops out ~44 MHz.
if platform is not None:
# capture @ 54 MHz: icepll -i 12 -o 54 → DIVR=0 DIVF=71 DIVQ=4.
# 54 MHz = 2× the 27 MHz EXI clock — the minimum oversampling that
# cleanly implements SPI Mode 3. The isolated SPI bit engine closes
# ~91 MHz on this device; the byte-FIFO read path brings the
# integrated capture domain to ~62 MHz, so 54 closes with margin.
m.domains += ClockDomain("capture")
platform.lookup(platform.default_clk).attrs["GLOBAL"] = False
m.submodules.pll = Instance(
"SB_PLL40_PAD",
p_FEEDBACK_PATH = "SIMPLE",
p_DIVR = 0,
p_DIVF = 71,
p_DIVQ = 4,
p_FILTER_RANGE = 1,
i_PACKAGEPIN = platform.request("clk12", dir="-").io,
i_RESETB = Const(1, 1),
i_BYPASS = Const(0, 1),
o_PLLOUTGLOBAL = ClockSignal("capture"),
)
# exi & sync @ 24 MHz: one SB_HFOSC (÷2) drives both slow domains.
# The bulky register-file / SPRAM / W5500 logic is routing-bound at
# ~3344 MHz on the UP5K; 24 MHz closes with large margin. The byte
# rate (27 MHz EXI ÷ 8 ≈ 3.4 MHz) leaves ~7 slow cycles per byte.
m.domains += ClockDomain("exi")
m.domains += ClockDomain("sync")
m.submodules.hfosc = Instance(
"SB_HFOSC",
p_CLKHF_DIV = "0b01", # 48 ÷ 2 → 24 MHz
i_CLKHFEN = Const(1, 1),
i_CLKHFPU = Const(1, 1),
o_CLKHF = ClockSignal("exi"),
)
m.d.comb += ClockSignal("sync").eq(ClockSignal("exi"))
# (simulation: test harness provides capture/exi/sync clocks via add_clock)
# ── Submodules ────────────────────────────────────────────────────
cap = ExiCapture() # SPI bit engine (capture) + byte FIFOs
reg = BBARegisterFile()
arb = SPRAMArbiter()
asm = RXFrameAssembler()
drain = TXFrameDrain()
eth = (W5500SPIMaster(reset_cycles=self._reset_cycles)
if self._eth == "w5500"
else W5100ParallelMaster(reset_cycles=self._reset_cycles))
m.submodules.cap = cap
m.submodules.reg = reg
m.submodules.arb = arb
m.submodules.asm = asm
m.submodules.drain = drain
m.submodules.eth = eth
# ── External pin connections ──────────────────────────────────────
m.d.comb += [
# EXI inputs (to capture-domain front-end)
cap.spi_clk .eq(self.exi_clk),
cap.spi_mosi.eq(self.exi_mosi),
cap.spi_cs_n.eq(self.exi_cs_n),
# EXI outputs
self.exi_miso.eq(cap.spi_miso),
self.int_n .eq(reg.exi_int_n),
]
# Ethernet back-end physical pins
if self._eth == "w5500":
m.d.comb += [
self.w5500_clk .eq(eth.spi_clk),
self.w5500_mosi.eq(eth.spi_mosi),
self.w5500_cs_n.eq(eth.spi_cs_n),
eth.spi_miso .eq(self.w5500_miso),
eth.w5500_int_n.eq(self.w5500_int_n),
self.w5500_rst_n.eq(eth.w5500_rst_n),
]
else:
m.d.comb += [
self.w5100_addr .eq(eth.bus_addr),
self.w5100_data_o .eq(eth.bus_data_o),
self.w5100_data_oe.eq(eth.bus_data_oe),
eth.bus_data_i .eq(self.w5100_data_i),
self.w5100_cs_n .eq(eth.cs_n),
self.w5100_rd_n .eq(eth.rd_n),
self.w5100_wr_n .eq(eth.wr_n),
eth.w5100_int_n .eq(self.w5100_int_n),
self.w5100_rst_n .eq(eth.w5100_rst_n),
]
# ── ExiCapture byte stream ↔ BBARegisterFile (exi domain) ────────
m.d.comb += [
reg.rx_data .eq(cap.rx_data),
reg.rx_rdy .eq(cap.rx_rdy),
cap.rx_en .eq(reg.rx_en),
cap.tx_data .eq(reg.tx_data),
cap.tx_en .eq(reg.tx_en),
reg.tx_rdy .eq(cap.tx_rdy),
reg.cs_active.eq(cap.cs_active), # transaction-active (for DMA reads)
]
# ── BBARegisterFile ↔ SPRAMArbiter (sync domain FIFO sides) ──────
# SPRAM request: reg exi→sync FIFO read side → arb
m.d.comb += [
arb.exi_req_addr .eq(reg.spram_req_r_data),
arb.exi_req_valid.eq(reg.spram_req_r_rdy),
reg.spram_req_r_en.eq(arb.exi_req_ready),
]
# SPRAM response: arb result → reg sync→exi FIFO write side
m.d.comb += [
reg.spram_rsp_w_data.eq(arb.exi_rsp_data),
reg.spram_rsp_w_en .eq(arb.exi_rsp_valid),
# arb does not need w_rdy feedback (spram_rsp FIFO is deeper than latency)
]
# ── BBARegisterFile ↔ TXFrameDrain (sync domain FIFO sides) ──────
m.d.comb += [
drain.tx_bytes_r_data.eq(reg.tx_bytes_r_data),
drain.tx_bytes_r_rdy .eq(reg.tx_bytes_r_rdy),
reg.tx_bytes_r_en .eq(drain.tx_bytes_r_en),
drain.tx_ctrl_r_data.eq(reg.tx_ctrl_r_data),
drain.tx_ctrl_r_rdy .eq(reg.tx_ctrl_r_rdy),
reg.tx_ctrl_r_en .eq(drain.tx_ctrl_r_en),
]
# ── TXFrameDrain ↔ ethernet master (sync domain) ──────────────────
m.d.comb += [
eth.tx_data .eq(drain.tx_data),
eth.tx_valid.eq(drain.tx_valid),
drain.tx_ready.eq(eth.tx_ready),
eth.tx_sof .eq(drain.tx_sof),
eth.tx_eof .eq(drain.tx_eof),
]
# ── ethernet master → RXFrameAssembler (sync domain) ─────────────
m.d.comb += [
asm.rx_data .eq(eth.rx_data),
asm.rx_valid.eq(eth.rx_valid),
eth.rx_ready.eq(asm.rx_ready),
asm.rx_sof .eq(eth.rx_sof),
asm.rx_eof .eq(eth.rx_eof),
]
# ── RXFrameAssembler → SPRAMArbiter (ETH write, sync domain) ─────
m.d.comb += [
arb.eth_wr_addr .eq(asm.eth_wr_addr),
arb.eth_wr_data .eq(asm.eth_wr_data),
arb.eth_wr_valid.eq(asm.eth_wr_valid),
asm.eth_wr_ready.eq(arb.eth_wr_ready),
]
# ── RXFrameAssembler → BBARegisterFile (rx_wptr FIFO write side) ─
m.d.comb += [
reg.rx_wptr_w_data.eq(asm.rx_wptr_w_data),
reg.rx_wptr_w_en .eq(asm.rx_wptr_w_en),
asm.rx_wptr_w_rdy .eq(reg.rx_wptr_w_rdy),
]
# ── Pulse synchronizer connections ────────────────────────────────
m.d.comb += [
# RX irq: sync → exi (RXFrameAssembler → reg → PS → exi domain)
reg.rx_irq_i.eq(asm.rx_irq),
# TX irq: sync → exi
reg.tx_irq_i.eq(drain.tx_irq),
# MAC address (PAR05) → SHAR. exi and sync share the HFOSC net,
# and par is quasi-static (sampled by the master at init_req).
eth.par.eq(reg.par),
]
# ── RX enabled gate (NCRA SR / start-receive bit) ─────────────────
# The RX ring-buffer path is active only after the GC sets NCRA[3].
m.d.comb += asm.rx_enabled.eq(reg.ncra_sr)
# ── Optional bring-up status panel (sync domain) ──────────────────
# init_req = NCRA reset (exi→sync PS), OR'd with the panel's manual
# re-init button when the panel is present.
if self._status_panel:
panel = StatusPanel()
m.submodules.panel = panel
# cs_active lives in the exi domain; bring it to sync for the LED.
cs_a_sync = Signal()
m.submodules.panel_cs = FFSynchronizer(
cap.cs_active, cs_a_sync, o_domain="sync")
# "ready" = ethernet init complete (latched until the next init).
ready = Signal()
with m.If(eth.init_done):
m.d.sync += ready.eq(1)
with m.Elif(reg.ncra_rst_o | panel.reinit):
m.d.sync += ready.eq(0)
m.d.comb += [
panel.cs_active.eq(cs_a_sync),
panel.rx_pulse .eq(asm.rx_irq),
panel.tx_pulse .eq(drain.tx_irq),
panel.ready .eq(ready),
panel.btn .eq(self.panel_btn),
self.panel_led .eq(panel.led),
eth.init_req .eq(reg.ncra_rst_o | panel.reinit),
]
else:
m.d.comb += eth.init_req.eq(reg.ncra_rst_o)
return m
# ── Integration testbench ─────────────────────────────────────────────────
# Drives real EXI Mode-3 transactions on the GC-facing pins and checks the
# response — exercising the full chain ExiCapture (capture domain) ↔ byte FIFOs
# ↔ BBARegisterFile (exi domain) ↔ sync modules, across all three clock domains.
if __name__ == "__main__":
import sys
from amaranth.sim import Simulator, Period
dut = BBATop(eth="w5100", reset_cycles=20, # small reset wait for sim
status_panel=True) # also exercise the panel wiring
errors = []
HALF = 8 # capture ticks per SPI half-period (well-oversampled)
async def spi_byte(ctx, mosi_val):
"""Drive one EXI Mode-3 byte; return the assembled MISO byte."""
miso = 0
for bit in range(7, -1, -1):
ctx.set(dut.exi_mosi, (mosi_val >> bit) & 1)
ctx.set(dut.exi_clk, 0) # falling: slave samples MOSI
await ctx.tick("capture").repeat(HALF)
miso = (miso << 1) | ctx.get(dut.exi_miso)
ctx.set(dut.exi_clk, 1) # rising
await ctx.tick("capture").repeat(HALF)
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)
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)
# 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)
result = [await spi_byte(ctx, 0x00) for _ in range(length)]
ctx.set(dut.exi_cs_n, 1)
await ctx.tick("capture").repeat(HALF)
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)
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)
for b in data:
await spi_byte(ctx, b)
ctx.set(dut.exi_cs_n, 1)
await ctx.tick("capture").repeat(HALF)
# ── 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.
RX_FRAME = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04]
_W_RX_BASE = 0x6000
_W_S0_CR = 0x0401
_W_S0_RX_RSR = 0x0426
_W_S0_RX_RD = 0x0428
_W_CR_RECV = 0x40
_A_MR, _A_AR0, _A_AR1, _A_DR = 0b00, 0b01, 0b10, 0b11
def w5100_preload():
plen = len(RX_FRAME) + 2 # MACRAW length includes its header
mem = {}
for i, b in enumerate([(plen >> 8) & 0xFF, plen & 0xFF] + RX_FRAME):
mem[_W_RX_BASE + i] = b
mem[_W_S0_RX_RSR], mem[_W_S0_RX_RSR + 1] = (plen >> 8) & 0xFF, plen & 0xFF
mem[_W_S0_RX_RD], mem[_W_S0_RX_RD + 1] = 0, 0
return mem
w5100_mem = w5100_preload()
async def w5100_model(ctx):
idm_ar = 0
mr = 0
prev_cs = prev_rd = prev_wr = 1
async for vals in ctx.tick("sync").sample(
dut.w5100_cs_n, dut.w5100_rd_n, dut.w5100_wr_n,
dut.w5100_addr, dut.w5100_data_o):
cs, rd, wr, a, do = vals[-5:]
ai = (mr >> 1) & 1
if cs == 0 and rd == 0: # drive read data
if a == _A_MR:
val = mr
elif a == _A_AR0:
val = (idm_ar >> 8) & 0xFF
elif a == _A_AR1:
val = idm_ar & 0xFF
else:
val = w5100_mem.get(idm_ar, 0)
ctx.set(dut.w5100_data_i, val)
if cs == 0 and prev_wr == 0 and wr == 1: # latch write on /WR rising
if a == _A_MR:
mr = do
elif a == _A_AR0:
idm_ar = (idm_ar & 0x00FF) | (do << 8)
elif a == _A_AR1:
idm_ar = (idm_ar & 0xFF00) | do
else:
w5100_mem[idm_ar] = do
if idm_ar == _W_S0_CR and do == _W_CR_RECV:
w5100_mem[_W_S0_RX_RSR] = 0
w5100_mem[_W_S0_RX_RSR + 1] = 0
if ai:
idm_ar = (idm_ar + 1) & 0xFFFF
if cs == 0 and prev_rd == 0 and rd == 1 and a == _A_DR and ai:
idm_ar = (idm_ar + 1) & 0xFFFF
prev_cs, prev_rd, prev_wr = cs, rd, wr
async def testbench(ctx):
ctx.set(dut.exi_clk, 1)
ctx.set(dut.exi_cs_n, 1)
ctx.set(dut.panel_btn, 0b111) # all buttons released (active-low idle)
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)
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}")
await ctx.tick("capture").repeat(HALF)
# T2: write PAR03, read them back through the full chain
await exi_write(ctx, 0x20, [0xDE, 0xAD, 0xBE, 0xEF])
await ctx.tick("capture").repeat(HALF * 4)
par = await exi_read(ctx, 0x20, 4)
print(f"T2 PAR0-3 readback: {[f'0x{b:02X}' for b in par]}")
if par != [0xDE, 0xAD, 0xBE, 0xEF]:
errors.append(f"T2 PAR readback: got {par}")
await ctx.tick("capture").repeat(HALF)
# T3: NWAYS must read back the hardcoded 0x17 (link-up sentinel)
nways = await exi_read(ctx, 0x31, 1)
print(f"T3 NWAYS: 0x{nways[0]:02X} (want 0x17)")
if nways != [0x17]:
errors.append(f"T3 NWAYS: got {nways}")
await ctx.tick("capture").repeat(HALF)
# T4: DMA-style SPRAM read — clock 8 data bytes (past the 4-byte header
# limit) within one CS. Exercises the integrated streaming path:
# ExiCapture(cs_active) → register file SPRAM_STREAM → SPRAMArbiter →
# real SPRAM → MISO, plus the SPRAM_END cleanup. SPRAM is uninitialised
# here, so we check the stream completes (8 bytes, no underrun/hang)
# rather than specific data.
dma = await exi_read(ctx, 0x0100, 8)
print(f"T4 DMA read (8B from 0x100): {[f'0x{b:02X}' for b in dma]}")
if len(dma) != 8:
errors.append(f"T4 DMA read length: got {len(dma)}")
await ctx.tick("capture").repeat(HALF)
# T5: a register read after the streaming read confirms the FSM cleaned
# up (SPRAM_END → HEADER0) and the device is responsive again.
nways2 = await exi_read(ctx, 0x31, 1)
print(f"T5 NWAYS after DMA: 0x{nways2[0]:02X} (want 0x17)")
if nways2 != [0x17]:
errors.append(f"T5 NWAYS after DMA read: got {nways2}")
await ctx.tick("capture").repeat(HALF)
# ── T6: FULL ETHERNET→SPRAM→GC LOOP ──────────────────────────────
# A frame arrives from the network (W5500 model) → W5500 master reads it
# → RXFrameAssembler writes it to the SPRAM ring → GC reads RWP then
# DMA-reads the descriptor+frame back. Exercises the entire RX path.
# The W5100 needs its init sequence (which sets MR.AI / opens socket 0)
# before multi-byte bus accesses work — trigger it via NCRA reset, as
# the real GC driver does, and let it run before enabling RX.
await exi_write(ctx, 0x00, [0x01]) # NCRA reset → init_req pulse
await ctx.tick("capture").repeat(2000) # let W5100 init run
await exi_write(ctx, 0x00, [0x08]) # NCRA SR bit → enable RX
await ctx.tick("capture").repeat(HALF * 2)
ctx.set(dut.w5100_int_n, 0) # W5100: a packet was received
await ctx.tick("capture").repeat(4000) # let the W5100 RX + SPRAM write run
ctx.set(dut.w5100_int_n, 1)
await ctx.tick("capture").repeat(HALF * 2)
rwp = await exi_read(ctx, 0x16, 1) # RX write pointer (page)
total_len = len(RX_FRAME) + 4
got = await exi_read(ctx, 0x0100, total_len) # descriptor + frame
want = [0x00, 0x00, (total_len >> 8) & 0xFF, total_len & 0xFF] + RX_FRAME
print(f"T6 RWP=0x{rwp[0]:02X} (want 0x02)")
print(f"T6 SPRAM[0x100]: {[f'0x{b:02X}' for b in got]}")
print(f"T6 expected : {[f'0x{b:02X}' for b in want]}")
if rwp != [0x02]:
errors.append(f"T6 RWP: got {rwp}, want [0x02]")
if got != want:
errors.append(f"T6 RX frame mismatch:\n got {got}\n want {want}")
# T7: status-panel integration — after all the EXI traffic above, the
# EXI-activity LED (panel led[1] = stretched cs_active) must be lit,
# proving cap.cs_active → FFSync → StatusPanel → LED is wired end-to-end.
leds = ctx.get(dut.panel_led)
if not (leds >> 1) & 1:
errors.append(f"T7 panel: EXI-activity LED not lit (led=0b{leds:05b})")
print(f"T7 panel led=0b{leds:05b} (bit1=EXI activity, expect 1)")
sim = Simulator(dut)
sim.add_clock(Period(MHz=54), domain="capture")
sim.add_clock(Period(MHz=24), domain="exi")
sim.add_clock(Period(MHz=24), domain="sync")
sim.add_testbench(testbench)
sim.add_process(w5100_model)
sim.run()
if errors:
print("\nFAILURES:")
for e in errors:
print(" ", e)
sys.exit(1)
else:
print("\nAll BBATop integration tests passed.")