add group test pfc protection

This commit is contained in:
2026-08-07 11:59:12 +08:00
parent 1f11c0b832
commit 1b6db67805
+341
View File
@@ -0,0 +1,341 @@
import csv
import time
from dataclasses import dataclass
from pathlib import Path
import pytest
from common.config_loader import load_jig_registers
from common.error_codes import RpuErrorCode, TpuErrorCode
from drivers.eload import Eload
from drivers.jig import Jig, Position
from drivers.rpu import Rpu
from drivers.tpu import Tpu
RESULTS_DIR = Path(__file__).resolve().parent.parent / "results"
# Vac_in (AC source input) is set manually by the test operator before running
# this file - not controlled by the script.
POSITION = Position(0, 0, 40)
VELOAD = 48.0
IOUT_REF = 30.0
DEFAULT_PFC_CURRENT_MAX = 30.0
DEFAULT_PFC_VDC_BUS_MIN = 350.0
DEFAULT_PFC_VDC_BUS_MAX = 425.0
FAULT_PFC_CURRENT_MAX = 8.0
FAULT_PFC_VDC_BUS_MAX = 395.0
FAULT_PFC_VDC_BUS_MIN = 445.0
AC_VOLTAGE_CALI_INCREASE_PCT = 0.10
EXPECTED_TPU_OUT_OVERCURRENT = TpuErrorCode.PFC_IL_OVER_CURRENT
EXPECTED_RPU_OUT_OVERCURRENT = RpuErrorCode.TPU_PFC_FAILURE
EXPECTED_TPU_DC_OVERVOLTAGE = TpuErrorCode.PFC_DC_OVER_VOLTAGE
EXPECTED_RPU_DC_OVERVOLTAGE = RpuErrorCode.BLE_UART_LOSS
EXPECTED_TPU_DC_UNDERVOLTAGE = TpuErrorCode.PFC_DC_UNDER_VOLTAGE
EXPECTED_RPU_DC_UNDERVOLTAGE = 0
EXPECTED_TPU_START_TIMEOUT = TpuErrorCode.PFC_START_TIMEOUT
EXPECTED_RPU_START_TIMEOUT = 0
START_CHARGING_WAIT_S = 1
CHARGING_RAMP_WAIT_S = 20 # wait for Iout to ramp up to the 30A target before recording
STOP_CHARGING_SETTLE_WAIT_S = 3
CONFIG_SETTLE_WAIT_S = 5 # settle wait for cases that don't need charging enabled
STABILITY_SAMPLE_DURATION_S = 5.0
STABILITY_SAMPLE_INTERVAL_S = 0.2
STABILITY_FIELDS = ["PFC_Vout", "PFC_iL_peak"]
def _hex32(value):
return f"0x{value:08X}"
def _hex16(value):
return f"0x{value:04X}"
def _error_ok(actual, actual_shadow, expected):
return actual == expected or actual_shadow == expected
@dataclass
class PfcBaseline:
"""Shared connections + helpers for every PROT-PFC test case in this file."""
jig: Jig
tpu: Tpu
rpu: Rpu
eload: Eload
writer: object
csv_file: object
def sample_min_max(self, labels, duration=STABILITY_SAMPLE_DURATION_S, interval=STABILITY_SAMPLE_INTERVAL_S):
"""Poll `labels` every `interval` seconds for `duration` seconds and
return {label_min, label_max} - same pattern as vac_coil_estimation.py's
sample_stability()."""
samples = {label: [] for label in labels}
start = time.time()
while time.time() - start < duration:
for label in labels:
samples[label].append(self.tpu.read(label))
time.sleep(interval)
stats = {}
for label, vals in samples.items():
stats[f"{label}_min"] = min(vals)
stats[f"{label}_max"] = max(vals)
return stats
def log_row(self, phase, setting_label, setting_value, stability_fields=STABILITY_FIELDS, pass_fail=""):
# The 5s sampling window IS the "record" step for this row. Only the
# fields relevant to this test case are sampled - others stay blank.
stats = self.sample_min_max(stability_fields)
self.writer.writerow([
phase, setting_label, setting_value,
stats.get("PFC_Vout_min", ""), stats.get("PFC_Vout_max", ""),
stats.get("PFC_iL_peak_min", ""), stats.get("PFC_iL_peak_max", ""),
self.tpu.read("RPU_output_voltage"), self.tpu.read("RPU_output_current"),
self.tpu.read("PFC_state"), self.tpu.read("Main_state"), self.tpu.read("Main_pre_state"),
_hex32(self.tpu.read_error_code()), _hex32(self.tpu.read_shadow_error_code()),
_hex16(self.tpu.read("Issue code")),
_hex32(self.rpu.read_error_code()), _hex32(self.rpu.read_shadow_error_code()),
pass_fail,
])
self.csv_file.flush()
def start_charging(self):
self.rpu.set_charging(True)
time.sleep(START_CHARGING_WAIT_S)
self.rpu.set_runtime_voltage(VELOAD)
self.rpu.set_runtime_current(IOUT_REF)
time.sleep(CHARGING_RAMP_WAIT_S)
def stop_charging_and_wait_config_writable(self):
# PFC_Current_max (and other "Debug" dataset config) is only committed
# by the firmware while TPU is NOT actively charging (TPU_RUN_INV is
# excluded from the config-writable states).
self.rpu.set_charging(False)
time.sleep(STOP_CHARGING_SETTLE_WAIT_S)
self.tpu.wait_until_config_writable()
def restore_config(self, label, value):
self.stop_charging_and_wait_config_writable()
self.tpu.write(label, value)
self.tpu.rewrite_config()
@pytest.fixture(scope="module")
def pfc_baseline(jig_port, tpu_port, rpu_port, eload_resource):
jig_registers = load_jig_registers()
jig = Jig(jig_port, jig_registers)
tpu = Tpu(tpu_port)
rpu = Rpu(rpu_port)
eload = Eload(eload_resource)
RESULTS_DIR.mkdir(exist_ok=True)
csv_path = RESULTS_DIR / f"prot_pfc_{time.strftime('%Y%m%d_%H%M%S')}.csv"
csv_file = open(csv_path, "w", newline="", encoding="utf-8")
writer = csv.writer(csv_file)
writer.writerow([
"phase", "setting_label", "setting_value", "pfc_vout_min", "pfc_vout_max",
"pfc_il_peak_min", "pfc_il_peak_max", "rpu_output_voltage", "rpu_output_current",
"pfc_state", "main_state", "main_pre_state", "tpu_error_code", "tpu_shadow_error_code",
"tpu_issue_code", "rpu_error_code", "rpu_shadow_error_code", "pass_fail",
])
baseline = PfcBaseline(jig=jig, tpu=tpu, rpu=rpu, eload=eload, writer=writer, csv_file=csv_file)
try:
rpu.set_charging(False)
jig.move_to_position(POSITION)
eload.set_remote()
eload.set_mode_cv()
eload.set_voltage(VELOAD)
eload.output_on()
pfc_current_max = tpu.read("PFC_Current_max")
pfc_vdc_bus_min = tpu.read("PFC_Vdc_bus_min")
pfc_vdc_bus_max = tpu.read("PFC_Vdc_bus_max")
assert pfc_current_max == pytest.approx(DEFAULT_PFC_CURRENT_MAX, abs=0.5), \
f"PFC_Current_max not at default: {pfc_current_max}"
assert pfc_vdc_bus_min == pytest.approx(DEFAULT_PFC_VDC_BUS_MIN, abs=1), \
f"PFC_Vdc_bus_min not at default: {pfc_vdc_bus_min}"
assert pfc_vdc_bus_max == pytest.approx(DEFAULT_PFC_VDC_BUS_MAX, abs=1), \
f"PFC_Vdc_bus_max not at default: {pfc_vdc_bus_max}"
# Enable charging, wait for Iout to ramp up to 30A, then record for 5s
# (both PFC_Vout and PFC_iL_peak are relevant as a healthy reference).
baseline.start_charging()
baseline.log_row("baseline", "PFC_Imax", pfc_current_max)
yield baseline
finally:
rpu.set_charging(False)
eload.output_off()
eload.set_local()
csv_file.close()
jig.close()
tpu.close()
rpu.close()
eload.disconnect()
def test_pfc_out_overcurrent(pfc_baseline):
tpu = pfc_baseline.tpu
rpu = pfc_baseline.rpu
try:
pfc_baseline.stop_charging_and_wait_config_writable()
tpu.write("PFC_Current_max", FAULT_PFC_CURRENT_MAX)
tpu.rewrite_config()
# This case needs charging active to push real output current above
# the lowered threshold - wait for ramp-up same as baseline.
pfc_baseline.start_charging()
tpu_error = tpu.read_error_code()
tpu_shadow_error = tpu.read_shadow_error_code()
rpu_error = rpu.read_error_code()
rpu_shadow_error = rpu.read_shadow_error_code()
tpu_error_ok = _error_ok(tpu_error, tpu_shadow_error, EXPECTED_TPU_OUT_OVERCURRENT)
rpu_error_ok = _error_ok(rpu_error, rpu_shadow_error, EXPECTED_RPU_OUT_OVERCURRENT)
pass_fail = "PASS" if (tpu_error_ok and rpu_error_ok) else "FAIL"
# Current-protection case - only PFC_iL_peak is relevant, PFC_Vout stays blank.
pfc_baseline.log_row("pfc_out_overcurrent", "PFC_Imax", FAULT_PFC_CURRENT_MAX,
stability_fields=["PFC_iL_peak"], pass_fail=pass_fail)
assert tpu_error_ok, (
f"TPU error code mismatch: got {_hex32(tpu_error)}/{_hex32(tpu_shadow_error)}, "
f"expected {_hex32(EXPECTED_TPU_OUT_OVERCURRENT)}"
)
assert rpu_error_ok, (
f"RPU error code mismatch: got {_hex32(rpu_error)}/{_hex32(rpu_shadow_error)}, "
f"expected {_hex32(EXPECTED_RPU_OUT_OVERCURRENT)}"
)
finally:
pfc_baseline.restore_config("PFC_Current_max", DEFAULT_PFC_CURRENT_MAX)
def test_pfc_dc_overvoltage(pfc_baseline):
tpu = pfc_baseline.tpu
rpu = pfc_baseline.rpu
try:
pfc_baseline.stop_charging_and_wait_config_writable()
tpu.write("PFC_Vdc_bus_max", FAULT_PFC_VDC_BUS_MAX)
tpu.rewrite_config()
# No charging needed - the DC bus is already regulated by PFC before
# RPU enables charging, just settle then sample.
time.sleep(CONFIG_SETTLE_WAIT_S)
tpu_error = tpu.read_error_code()
tpu_shadow_error = tpu.read_shadow_error_code()
rpu_error = rpu.read_error_code()
rpu_shadow_error = rpu.read_shadow_error_code()
tpu_error_ok = _error_ok(tpu_error, tpu_shadow_error, EXPECTED_TPU_DC_OVERVOLTAGE)
rpu_error_ok = _error_ok(rpu_error, rpu_shadow_error, EXPECTED_RPU_DC_OVERVOLTAGE)
pass_fail = "PASS" if (tpu_error_ok and rpu_error_ok) else "FAIL"
pfc_baseline.log_row("pfc_dc_overvoltage", "Vdc_bus_max", FAULT_PFC_VDC_BUS_MAX,
stability_fields=["PFC_Vout"], pass_fail=pass_fail)
assert tpu_error_ok, (
f"TPU error code mismatch: got {_hex32(tpu_error)}/{_hex32(tpu_shadow_error)}, "
f"expected {_hex32(EXPECTED_TPU_DC_OVERVOLTAGE)}"
)
assert rpu_error_ok, (
f"RPU error code mismatch: got {_hex32(rpu_error)}/{_hex32(rpu_shadow_error)}, "
f"expected {_hex32(EXPECTED_RPU_DC_OVERVOLTAGE)}"
)
finally:
pfc_baseline.restore_config("PFC_Vdc_bus_max", DEFAULT_PFC_VDC_BUS_MAX)
def test_pfc_dc_undervoltage(pfc_baseline):
tpu = pfc_baseline.tpu
rpu = pfc_baseline.rpu
try:
pfc_baseline.stop_charging_and_wait_config_writable()
tpu.write("PFC_Vdc_bus_min", FAULT_PFC_VDC_BUS_MIN)
tpu.rewrite_config()
time.sleep(CONFIG_SETTLE_WAIT_S)
tpu_error = tpu.read_error_code()
tpu_shadow_error = tpu.read_shadow_error_code()
rpu_error = rpu.read_error_code()
rpu_shadow_error = rpu.read_shadow_error_code()
tpu_error_ok = _error_ok(tpu_error, tpu_shadow_error, EXPECTED_TPU_DC_UNDERVOLTAGE)
rpu_error_ok = _error_ok(rpu_error, rpu_shadow_error, EXPECTED_RPU_DC_UNDERVOLTAGE)
pass_fail = "PASS" if (tpu_error_ok and rpu_error_ok) else "FAIL"
pfc_baseline.log_row("pfc_dc_undervoltage", "Vdc_bus_min", FAULT_PFC_VDC_BUS_MIN,
stability_fields=["PFC_Vout"], pass_fail=pass_fail)
assert tpu_error_ok, (
f"TPU error code mismatch: got {_hex32(tpu_error)}/{_hex32(tpu_shadow_error)}, "
f"expected {_hex32(EXPECTED_TPU_DC_UNDERVOLTAGE)}"
)
assert rpu_error_ok, (
f"RPU error code mismatch: got {_hex32(rpu_error)}/{_hex32(rpu_shadow_error)}, "
f"expected {_hex32(EXPECTED_RPU_DC_UNDERVOLTAGE)}"
)
finally:
pfc_baseline.restore_config("PFC_Vdc_bus_min", DEFAULT_PFC_VDC_BUS_MIN)
def test_pfc_start_timeout(pfc_baseline):
tpu = pfc_baseline.tpu
rpu = pfc_baseline.rpu
# PFC_AC_Voltage_Cali_A default varies per unit - read the current value
# first (before the try block) so `finally` can always restore it exactly,
# even if something below fails before the fault value is written.
pfc_baseline.stop_charging_and_wait_config_writable()
default_cali_a = tpu.read("PFC_AC_Voltage_Cali_A")
try:
fault_cali_a = default_cali_a * (1 + AC_VOLTAGE_CALI_INCREASE_PCT)
tpu.write("PFC_AC_Voltage_Cali_A", fault_cali_a)
tpu.rewrite_config()
time.sleep(CONFIG_SETTLE_WAIT_S)
tpu_error = tpu.read_error_code()
tpu_shadow_error = tpu.read_shadow_error_code()
rpu_error = rpu.read_error_code()
rpu_shadow_error = rpu.read_shadow_error_code()
tpu_error_ok = _error_ok(tpu_error, tpu_shadow_error, EXPECTED_TPU_START_TIMEOUT)
rpu_error_ok = _error_ok(rpu_error, rpu_shadow_error, EXPECTED_RPU_START_TIMEOUT)
pass_fail = "PASS" if (tpu_error_ok and rpu_error_ok) else "FAIL"
pfc_baseline.log_row("pfc_start_timeout", "PFC_AC_Voltage_Cali_A", fault_cali_a,
stability_fields=["PFC_Vout"], pass_fail=pass_fail)
assert tpu_error_ok, (
f"TPU error code mismatch: got {_hex32(tpu_error)}/{_hex32(tpu_shadow_error)}, "
f"expected {_hex32(EXPECTED_TPU_START_TIMEOUT)}"
)
assert rpu_error_ok, (
f"RPU error code mismatch: got {_hex32(rpu_error)}/{_hex32(rpu_shadow_error)}, "
f"expected {_hex32(EXPECTED_RPU_START_TIMEOUT)}"
)
finally:
pfc_baseline.restore_config("PFC_AC_Voltage_Cali_A", default_cali_a)