#!/usr/bin/env python3 """ AERIS-10 Board Bring-Up Smoke Test — Host-Side Script ====================================================== Sends opcode 0x30 to trigger the FPGA self-test, then reads back the results via opcode 0x31. Decodes per-subsystem PASS/FAIL and optionally captures raw ADC samples for offline analysis. Usage: python smoke_test.py # Mock mode (no hardware) python smoke_test.py --live # Real FT2232H hardware python smoke_test.py --live --adc-dump adc_raw.npy # Capture ADC data Self-Test Subsystems: Bit 0: BRAM write/read pattern (walking 1s) Bit 1: CIC integrator arithmetic Bit 2: FFT butterfly arithmetic Bit 3: Saturating add (MTI-style) Bit 4: ADC raw data capture (256 samples) Exit codes: 0 = all tests passed 1 = one or more tests failed 2 = communication error / timeout """ import sys import os import time import argparse import logging import numpy as np # Add parent directory for radar_protocol import sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from radar_protocol import RadarProtocol, FT2232HConnection logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S", ) log = logging.getLogger("smoke_test") # Self-test opcodes (must match radar_system_top.v command decode) OPCODE_SELF_TEST_TRIGGER = 0x30 OPCODE_SELF_TEST_RESULT = 0x31 # Result packet format (sent by FPGA after self-test completes): # The self-test result is reported via the status readback mechanism. # When the host sends opcode 0x31, the FPGA responds with a status packet # containing the self-test results in the first status word. # # For mock mode, we simulate this directly. TEST_NAMES = { 0: "BRAM Write/Read Pattern", 1: "CIC Integrator Arithmetic", 2: "FFT Butterfly Arithmetic", 3: "Saturating Add (MTI)", 4: "ADC Raw Data Capture", } class SmokeTest: """Host-side smoke test controller.""" def __init__(self, connection: FT2232HConnection, adc_dump_path: str | None = None): self.conn = connection self.adc_dump_path = adc_dump_path self._adc_samples = [] def run(self) -> bool: """ Execute the full smoke test sequence. Returns True if all tests pass, False otherwise. """ log.info("=" * 60) log.info(" AERIS-10 Board Bring-Up Smoke Test") log.info("=" * 60) log.info("") # Step 1: Connect if not self.conn.is_open and not self.conn.open(): log.error("Failed to open FT2232H connection") return False # Step 2: Send self-test trigger (opcode 0x30) log.info("Sending self-test trigger (opcode 0x30)...") cmd = RadarProtocol.build_command(OPCODE_SELF_TEST_TRIGGER, 1) if not self.conn.write(cmd): log.error("Failed to send trigger command") return False # Step 3: Wait for completion and read results log.info("Waiting for self-test completion...") result = self._wait_for_result(timeout_s=5.0) if result is None: log.error("Timeout waiting for self-test results") return False # Step 4: Decode results result_flags, result_detail = result all_pass = self._decode_results(result_flags, result_detail) # Step 5: ADC data dump (if requested and test 4 passed) if self.adc_dump_path and (result_flags & 0x10): self._save_adc_dump() # Step 6: Summary log.info("") log.info("=" * 60) if all_pass: log.info(" SMOKE TEST: ALL PASS") else: log.info(" SMOKE TEST: FAILED") log.info("=" * 60) return all_pass def _wait_for_result(self, timeout_s: float): """ Poll for self-test result. Returns (result_flags, result_detail) or None on timeout. """ if self.conn._mock: # Mock: simulate successful self-test after a short delay time.sleep(0.2) return (0x1F, 0x00) # All 5 tests pass deadline = time.time() + timeout_s while time.time() < deadline: # Request result readback (opcode 0x31) cmd = RadarProtocol.build_command(OPCODE_SELF_TEST_RESULT, 0) self.conn.write(cmd) time.sleep(0.1) # Read response raw = self.conn.read(256) if raw is None: continue # Look for status packet (0xBB header) packets = RadarProtocol.find_packet_boundaries(raw) for start, end, ptype in packets: if ptype == "status": status = RadarProtocol.parse_status_packet(raw[start:end]) if status is not None: # Self-test results encoded in status fields # (This is a simplification — in production, the FPGA # would have a dedicated self-test result packet type) result_flags = status.cfar_threshold & 0x1F result_detail = (status.cfar_threshold >> 8) & 0xFF return (result_flags, result_detail) time.sleep(0.1) return None def _decode_results(self, flags: int, detail: int) -> bool: """Decode and display per-test results. Returns True if all pass.""" log.info("") log.info("Self-Test Results:") log.info("-" * 40) all_pass = True for bit, name in TEST_NAMES.items(): passed = bool(flags & (1 << bit)) status = "PASS" if passed else "FAIL" marker = "✓" if passed else "✗" log.info(f" {marker} Test {bit}: {name:30s} [{status}]") if not passed: all_pass = False log.info("-" * 40) log.info(f" Result flags: 0b{flags:05b}") log.info(f" Detail byte: 0x{detail:02X}") if detail == 0xAD: log.warning(" Detail 0xAD = ADC timeout (no ADC data received)") elif detail != 0x00: log.info(f" Detail indicates first BRAM fail at addr[3:0] = {detail & 0x0F}") return all_pass def _save_adc_dump(self): """Save captured ADC samples to numpy file.""" if not self._adc_samples and self.conn._mock: # In mock mode, generate synthetic ADC data self._adc_samples = list(np.random.randint(0, 65536, 256, dtype=np.uint16)) if self._adc_samples: arr = np.array(self._adc_samples, dtype=np.uint16) np.save(self.adc_dump_path, arr) log.info(f"ADC raw data saved: {self.adc_dump_path} ({len(arr)} samples)") else: log.warning("No ADC samples captured for dump") def main(): parser = argparse.ArgumentParser(description="AERIS-10 Board Smoke Test") parser.add_argument("--live", action="store_true", help="Use real FT2232H hardware (default: mock)") parser.add_argument("--device", type=int, default=0, help="FT2232H device index") parser.add_argument("--adc-dump", type=str, default=None, help="Save raw ADC samples to .npy file") args = parser.parse_args() mock_mode = not args.live conn = FT2232HConnection(mock=mock_mode) tester = SmokeTest(conn, adc_dump_path=args.adc_dump) success = tester.run() if conn.is_open: conn.close() sys.exit(0 if success else 1) if __name__ == "__main__": main()