import csv import time from dataclasses import dataclass from pathlib import Path import pytest from common.config_loader import load_jig_registers from drivers.jig import Jig, Position from drivers.rpu import Rpu from drivers.tpu import Tpu RESULTS_DIR = Path(__file__).resolve().parent.parent / "results" DEFAULT_POSITION = Position(0, 0, 0) CLOSE_BORDER_POSITION = Position(0, 0, 15) FAR_BORDER_POSITION = Position(30, 10, 60) SWEEP_POSITIONS = [ Position(0, 0, 5), Position(0, 0, 10), Position(0, 0, 15), Position(0, 0, 20), Position(30, 10, 15), Position(60, 10, 15), Position(80, 10, 15), Position(90, 10, 15), Position(20, 10, 50), Position(30, 10, 50), Position(0, 0, 55), Position(10, 10, 55), Position(30, 10, 55), Position(30, 10, 60), Position(30, 10, 65), Position(0, 0, 80), ] DEFAULT_MOVE_WAIT_S = 5 POSITION_MOVE_WAIT_S = 5 START_CHARGING_WAIT_S = 1 RAMP_UP_WAIT_S = 8 RAMP_DOWN_WAIT_S = 3 FREQ_CAL_TIMEOUT_S = 30 FREQ_CAL_POLL_INTERVAL_S = 1 TARGET_CURRENT_A = 30.0 ALIGNMENT_RAMP_MA_PER_SEC = 20000 # "BLE Conn mode" (TPU: Debug dataset addr 5, RPU: sec_flash addr 142) selects # which frequency-calibration handshake the firmware expects # (global_variables.h: typedef enum _BLE_MODE { BLE_DEFAULT, BLE_RECONN, ... }). BLE_MODE_DEFAULT = 0 BLE_MODE_RECONN = 1 # TPU firmware resets PASSWORD_LO/HI back to 0 as soon as it recognizes the # combined password (main.c: check_debug_test_mode() does # `*main_app.pops->pass_code = 0` right after matching) - verifying by # read-back would race against that reset, so these writes use verify=False. TPU_DEBUG_PASSWORD_LO = 0xBEEF TPU_DEBUG_PASSWORD_HI = 0xFEED RPU_DEBUG_PASSWORD_LO = 0xDAAD RPU_DEBUG_PASSWORD_HI = 0xCBAD RPU_NORMAL_PASSWORD_LO = 0xFEED RPU_NORMAL_PASSWORD_HI = 0xCEE5 @dataclass class AlignmentContext: jig: Jig tpu: Tpu rpu: Rpu @pytest.fixture(scope="module") def alignment(jig_port, tpu_port, rpu_port): jig_registers = load_jig_registers() jig = Jig(jig_port, jig_registers) tpu = Tpu(tpu_port, extra_datasets=["frequency_calibration"]) rpu = Rpu(rpu_port) ctx = AlignmentContext(jig=jig, tpu=tpu, rpu=rpu) rpu.set_charging(False) rpu.wait_until_config_writable() # Alignment sweep needs Iout to ramp up fast enough to settle within the # per-position wait window - bump both ramp rates for the duration of # this file, then restore the original values on teardown. default_ramp_control = rpu.read("Ramp_control_mA_per_sec") default_ramp_slow = rpu.read("Ramp_slow_mA_per_sec") rpu.write("Ramp_control_mA_per_sec", ALIGNMENT_RAMP_MA_PER_SEC) rpu.write("Ramp_slow_mA_per_sec", ALIGNMENT_RAMP_MA_PER_SEC) rpu.rewrite_config() try: yield ctx finally: rpu.set_charging(False) jig.move_to_position(DEFAULT_POSITION, wait_time=DEFAULT_MOVE_WAIT_S) rpu.set_charging(False) rpu.wait_until_config_writable() rpu.write("Ramp_control_mA_per_sec", default_ramp_control) rpu.write("Ramp_slow_mA_per_sec", default_ramp_slow) rpu.rewrite_config() jig.close() tpu.close() rpu.close() def _wait_for_freq_cal_done(tpu, position_name, timeout_s=FREQ_CAL_TIMEOUT_S): deadline = time.time() + timeout_s while time.time() < deadline: if tpu.read("FREQ_CAL_DONE"): error_code = tpu.read("FREQ_CAL_ERROR_CODE") assert error_code == 0, f"[{position_name}] frequency calibration failed, error code {error_code}" return time.sleep(FREQ_CAL_POLL_INTERVAL_S) pytest.fail(f"[{position_name}] frequency calibration timed out after {timeout_s}s") def _run_freq_cal_step(tpu, cmd, position_name, ble_mode): if ble_mode == BLE_MODE_DEFAULT: # Method 1 (BLE mode 0): TPU and RPU acknowledge each other directly # over the wireless link - no host-driven ACK handshake needed. tpu.write("FREQ_CAL_CMD", cmd) else: # Method 2 (BLE mode 1): host must drive the FREQ_CAL_CMD_ACK # handshake itself (self-clearing coil, confirmed in # freq_calibration.c: "*freq_cal.cal_modbus_ack = false"). tpu.write("FREQ_CAL_CMD_ACK", True, verify=False) time.sleep(0.1) tpu.write("FREQ_CAL_CMD", cmd) time.sleep(0.1) tpu.write("FREQ_CAL_CMD_ACK", True, verify=False) time.sleep(0.1) _wait_for_freq_cal_done(tpu, position_name) def test_frequency_calibration(alignment): jig = alignment.jig tpu = alignment.tpu rpu = alignment.rpu tpu_ble_mode = tpu.read("BLE Conn mode") rpu_ble_mode = rpu.read("BLE Conn mode") if tpu_ble_mode != rpu_ble_mode: pytest.fail( f"TPU/RPU BLE Conn mode mismatch (TPU={tpu_ble_mode}, RPU={rpu_ble_mode}) - " f"both must be the same mode (0=BLE_DEFAULT or 1=BLE_RECONN) to calibrate" ) ble_mode = tpu_ble_mode rpu.set_charging(False) jig.move_to_position(DEFAULT_POSITION, wait_time=DEFAULT_MOVE_WAIT_S) if ble_mode == BLE_MODE_RECONN: # Enter RPU debug / force-frequency-calibration mode (Method 2 only). rpu.write("PASSWORD_LO", RPU_DEBUG_PASSWORD_LO) rpu.write("PASSWORD_HI", RPU_DEBUG_PASSWORD_HI) time.sleep(0.1) rpu.write("FORCE_FREQ_CAL", True) time.sleep(0.1) try: jig.move_to_position(CLOSE_BORDER_POSITION, wait_time=POSITION_MOVE_WAIT_S) _run_freq_cal_step(tpu, 1, "close-border", ble_mode) jig.move_to_position(FAR_BORDER_POSITION, wait_time=POSITION_MOVE_WAIT_S) _run_freq_cal_step(tpu, 2, "far-border", ble_mode) finally: if ble_mode == BLE_MODE_RECONN: rpu.write("FORCE_FREQ_CAL", False) time.sleep(0.1) rpu.write("PASSWORD_LO", RPU_NORMAL_PASSWORD_LO) rpu.write("PASSWORD_HI", RPU_NORMAL_PASSWORD_HI) time.sleep(0.1) freq_low = tpu.read("FREQ_CAL_RESULT_LOW") freq_high = tpu.read("FREQ_CAL_RESULT_HIGH") # Commit the calibrated frequencies: TPU needs its own debug-mode # password first (combined 0xFEEDBEEF -> LO=0xBEEF, HI=0xFEED). tpu.write("PASSWORD_LO", TPU_DEBUG_PASSWORD_LO, verify=False) tpu.write("PASSWORD_HI", TPU_DEBUG_PASSWORD_HI, verify=False) time.sleep(0.1) tpu.write("Coupling_freq_1", freq_low) tpu.write("Coupling_freq_2", freq_high) time.sleep(0.1) tpu.rewrite_config() jig.move_to_position(DEFAULT_POSITION, wait_time=DEFAULT_MOVE_WAIT_S) print(f"Frequency calibration done: f1 (close-border) = {freq_low:.2f} kHz, " f"f2 (far-border) = {freq_high:.2f} kHz") def test_alignment_sweep(alignment): jig = alignment.jig tpu = alignment.tpu rpu = alignment.rpu RESULTS_DIR.mkdir(exist_ok=True) csv_path = RESULTS_DIR / f"alignment_sweep_{time.strftime('%Y%m%d_%H%M%S')}.csv" rpu.set_charging(False) jig.move_to_position(DEFAULT_POSITION, wait_time=DEFAULT_MOVE_WAIT_S) try: with open(csv_path, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow([ "position_index", "x", "y", "z", "iref", "pfc_vout", "pfc_il_peak", "inv_iout_peak", "coupling_phase_1", "coupling_phase_2", "rpu_pi_i_out", "coil_vac_out", "coil_vac_out_est", "tpu_uart_lost_rate", "tpu_ble_rssi", "tpu_shadow_error_code", "rpu_output_current", "rpu_dc_batt_voltage", "rpu_ble_uart_lost_rate", "rpu_ble_rssi", "rpu_shadow_error_code", ]) for index, pos in enumerate(SWEEP_POSITIONS, start=1): jig.move_to_position(pos, wait_time=POSITION_MOVE_WAIT_S) rpu.set_charging(True) time.sleep(START_CHARGING_WAIT_S) rpu.set_runtime_current(TARGET_CURRENT_A) time.sleep(RAMP_UP_WAIT_S) actual_pos = jig.get_current_position() writer.writerow([ index, actual_pos.x, actual_pos.y, actual_pos.z, TARGET_CURRENT_A, tpu.read("PFC_Vout"), tpu.read("PFC_iL_peak"), tpu.read("Inv_Iout_peak"), tpu.read("coupling phase 1") / 10.0, tpu.read("coupling phase 2") / 10.0, tpu.read("RPU_PI_I_OUT") / 100.0, tpu.read("Coil_VAC_out"), tpu.read("Coil_VAC_out_est"), tpu.read("UART_Lost_rate"), tpu.read("BLE_RSSI"), hex(tpu.read_shadow_error_code()), rpu.read("Output_current_Q128") / rpu.Q128_DIVISOR, rpu.read("DC_Batt_voltage_Q128") / rpu.Q128_DIVISOR, rpu.read("BLE_UART_Lost_rate"), rpu.read("BLE_RSSI"), hex(rpu.read_shadow_error_code()), ]) f.flush() rpu.set_charging(False) time.sleep(RAMP_DOWN_WAIT_S) finally: rpu.set_charging(False) jig.move_to_position(DEFAULT_POSITION, wait_time=DEFAULT_MOVE_WAIT_S) print(f"Saved to {csv_path}")