From 9d7c350c21fcd6eed21b22b08553bede9a980db8 Mon Sep 17 00:00:00 2001 From: vanminhpe Date: Fri, 7 Aug 2026 14:19:43 +0800 Subject: [PATCH] feat: add ALIGNMENT group tests (frequency calibration + position sweep) --- tests/test_alignment.py | 241 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 tests/test_alignment.py diff --git a/tests/test_alignment.py b/tests/test_alignment.py new file mode 100644 index 0000000..8ee76f3 --- /dev/null +++ b/tests/test_alignment.py @@ -0,0 +1,241 @@ +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}")