229 lines
7.5 KiB
Python
229 lines
7.5 KiB
Python
#!/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 FT601 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 struct
|
|
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, FT601Connection
|
|
|
|
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: FT601Connection, adc_dump_path: str = 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:
|
|
if not self.conn.open():
|
|
log.error("Failed to open FT601 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:
|
|
# In mock mode, generate synthetic ADC data
|
|
if self.conn._mock:
|
|
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 FT601 hardware (default: mock)")
|
|
parser.add_argument("--device", type=int, default=0,
|
|
help="FT601 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 = FT601Connection(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()
|