smoke_test.py
1 #!/usr/bin/env python3 2 """ 3 AERIS-10 Board Bring-Up Smoke Test — Host-Side Script 4 ====================================================== 5 Sends opcode 0x30 to trigger the FPGA self-test, then reads back 6 the results via opcode 0x31. Decodes per-subsystem PASS/FAIL and 7 optionally captures raw ADC samples for offline analysis. 8 9 Usage: 10 python smoke_test.py # Mock mode (no hardware) 11 python smoke_test.py --live # Real FT2232H hardware 12 python smoke_test.py --live --adc-dump adc_raw.npy # Capture ADC data 13 14 Self-Test Subsystems: 15 Bit 0: BRAM write/read pattern (walking 1s) 16 Bit 1: CIC integrator arithmetic 17 Bit 2: FFT butterfly arithmetic 18 Bit 3: Saturating add (MTI-style) 19 Bit 4: ADC raw data capture (256 samples) 20 21 Exit codes: 22 0 = all tests passed 23 1 = one or more tests failed 24 2 = communication error / timeout 25 """ 26 27 import sys 28 import os 29 import time 30 import struct 31 import argparse 32 import logging 33 34 import numpy as np 35 36 # Add parent directory for radar_protocol import 37 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) 38 from radar_protocol import RadarProtocol, FT2232HConnection 39 40 logging.basicConfig( 41 level=logging.INFO, 42 format="%(asctime)s [%(levelname)s] %(message)s", 43 datefmt="%H:%M:%S", 44 ) 45 log = logging.getLogger("smoke_test") 46 47 # Self-test opcodes (must match radar_system_top.v command decode) 48 OPCODE_SELF_TEST_TRIGGER = 0x30 49 OPCODE_SELF_TEST_RESULT = 0x31 50 51 # Result packet format (sent by FPGA after self-test completes): 52 # The self-test result is reported via the status readback mechanism. 53 # When the host sends opcode 0x31, the FPGA responds with a status packet 54 # containing the self-test results in the first status word. 55 # 56 # For mock mode, we simulate this directly. 57 58 TEST_NAMES = { 59 0: "BRAM Write/Read Pattern", 60 1: "CIC Integrator Arithmetic", 61 2: "FFT Butterfly Arithmetic", 62 3: "Saturating Add (MTI)", 63 4: "ADC Raw Data Capture", 64 } 65 66 67 class SmokeTest: 68 """Host-side smoke test controller.""" 69 70 def __init__(self, connection: FT2232HConnection, adc_dump_path: str = None): 71 self.conn = connection 72 self.adc_dump_path = adc_dump_path 73 self._adc_samples = [] 74 75 def run(self) -> bool: 76 """ 77 Execute the full smoke test sequence. 78 Returns True if all tests pass, False otherwise. 79 """ 80 log.info("=" * 60) 81 log.info(" AERIS-10 Board Bring-Up Smoke Test") 82 log.info("=" * 60) 83 log.info("") 84 85 # Step 1: Connect 86 if not self.conn.is_open: 87 if not self.conn.open(): 88 log.error("Failed to open FT2232H connection") 89 return False 90 91 # Step 2: Send self-test trigger (opcode 0x30) 92 log.info("Sending self-test trigger (opcode 0x30)...") 93 cmd = RadarProtocol.build_command(OPCODE_SELF_TEST_TRIGGER, 1) 94 if not self.conn.write(cmd): 95 log.error("Failed to send trigger command") 96 return False 97 98 # Step 3: Wait for completion and read results 99 log.info("Waiting for self-test completion...") 100 result = self._wait_for_result(timeout_s=5.0) 101 102 if result is None: 103 log.error("Timeout waiting for self-test results") 104 return False 105 106 # Step 4: Decode results 107 result_flags, result_detail = result 108 all_pass = self._decode_results(result_flags, result_detail) 109 110 # Step 5: ADC data dump (if requested and test 4 passed) 111 if self.adc_dump_path and (result_flags & 0x10): 112 self._save_adc_dump() 113 114 # Step 6: Summary 115 log.info("") 116 log.info("=" * 60) 117 if all_pass: 118 log.info(" SMOKE TEST: ALL PASS") 119 else: 120 log.info(" SMOKE TEST: FAILED") 121 log.info("=" * 60) 122 123 return all_pass 124 125 def _wait_for_result(self, timeout_s: float): 126 """ 127 Poll for self-test result. 128 Returns (result_flags, result_detail) or None on timeout. 129 """ 130 if self.conn._mock: 131 # Mock: simulate successful self-test after a short delay 132 time.sleep(0.2) 133 return (0x1F, 0x00) # All 5 tests pass 134 135 deadline = time.time() + timeout_s 136 while time.time() < deadline: 137 # Request result readback (opcode 0x31) 138 cmd = RadarProtocol.build_command(OPCODE_SELF_TEST_RESULT, 0) 139 self.conn.write(cmd) 140 time.sleep(0.1) 141 142 # Read response 143 raw = self.conn.read(256) 144 if raw is None: 145 continue 146 147 # Look for status packet (0xBB header) 148 packets = RadarProtocol.find_packet_boundaries(raw) 149 for start, end, ptype in packets: 150 if ptype == "status": 151 status = RadarProtocol.parse_status_packet(raw[start:end]) 152 if status is not None: 153 # Self-test results encoded in status fields 154 # (This is a simplification — in production, the FPGA 155 # would have a dedicated self-test result packet type) 156 result_flags = status.cfar_threshold & 0x1F 157 result_detail = (status.cfar_threshold >> 8) & 0xFF 158 return (result_flags, result_detail) 159 160 time.sleep(0.1) 161 162 return None 163 164 def _decode_results(self, flags: int, detail: int) -> bool: 165 """Decode and display per-test results. Returns True if all pass.""" 166 log.info("") 167 log.info("Self-Test Results:") 168 log.info("-" * 40) 169 170 all_pass = True 171 for bit, name in TEST_NAMES.items(): 172 passed = bool(flags & (1 << bit)) 173 status = "PASS" if passed else "FAIL" 174 marker = "✓" if passed else "✗" 175 log.info(f" {marker} Test {bit}: {name:30s} [{status}]") 176 if not passed: 177 all_pass = False 178 179 log.info("-" * 40) 180 log.info(f" Result flags: 0b{flags:05b}") 181 log.info(f" Detail byte: 0x{detail:02X}") 182 183 if detail == 0xAD: 184 log.warning(" Detail 0xAD = ADC timeout (no ADC data received)") 185 elif detail != 0x00: 186 log.info(f" Detail indicates first BRAM fail at addr[3:0] = {detail & 0x0F}") 187 188 return all_pass 189 190 def _save_adc_dump(self): 191 """Save captured ADC samples to numpy file.""" 192 if not self._adc_samples: 193 # In mock mode, generate synthetic ADC data 194 if self.conn._mock: 195 self._adc_samples = list(np.random.randint(0, 65536, 256, dtype=np.uint16)) 196 197 if self._adc_samples: 198 arr = np.array(self._adc_samples, dtype=np.uint16) 199 np.save(self.adc_dump_path, arr) 200 log.info(f"ADC raw data saved: {self.adc_dump_path} ({len(arr)} samples)") 201 else: 202 log.warning("No ADC samples captured for dump") 203 204 205 def main(): 206 parser = argparse.ArgumentParser(description="AERIS-10 Board Smoke Test") 207 parser.add_argument("--live", action="store_true", 208 help="Use real FT2232H hardware (default: mock)") 209 parser.add_argument("--device", type=int, default=0, 210 help="FT2232H device index") 211 parser.add_argument("--adc-dump", type=str, default=None, 212 help="Save raw ADC samples to .npy file") 213 args = parser.parse_args() 214 215 mock_mode = not args.live 216 conn = FT2232HConnection(mock=mock_mode) 217 218 tester = SmokeTest(conn, adc_dump_path=args.adc_dump) 219 success = tester.run() 220 221 if conn.is_open: 222 conn.close() 223 224 sys.exit(0 if success else 1) 225 226 227 if __name__ == "__main__": 228 main()