tooling: sync bench + debug console to live STM32 protocol
The mppt-testbench Python tooling had drifted from the flashed firmware
(bundled fw e7a23a3 vs live 1b85532) and could no longer communicate:
- stm32_link.py: CRC8 -> CRC-16/CCITT-FALSE; telemetry 68B -> 78B
(btemp, cmp_outer/inner, iout_slow, vfly_ofs_applied); add PTYPE_INT16
and commands 0x12-0x18; replace PARAMS with the current 37-param map
(single dt_normal, no dt brackets; test_corr/phase_ofs, phase PI,
precharge PI, duty dither; vfly_active 0-3).
- tuner.py: retire per-bracket deadtime; sweep the single dt_normal.
- cli.py: update tune-deadtime, help/examples, btemp readout;
default ports COM11 (load) / COM4 (stm32).
- debug console TUI: sync protocol.py/app.py/status_bar/telemetry_panel
from live (new command keys, link RX/TX/loss stats, single dead-time,
new telemetry fields, param-write auto-retry); add duty_fft.py.
- README: rewrite parameter table, deadtime section, ports, keybindings.
Verified: protocol round-trip self-tests + live `bench stm32-read`
reading all 37 params over COM4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+51
-56
@@ -13,7 +13,7 @@ from pathlib import Path
|
||||
|
||||
from testbench.bench import MPPTTestbench, IDLE_VOLTAGE
|
||||
from testbench.stm32_link import (
|
||||
STM32Link, Telemetry, PARAM_BY_NAME, DT_BRACKETS,
|
||||
STM32Link, Telemetry, PARAM_BY_NAME,
|
||||
)
|
||||
|
||||
|
||||
@@ -199,6 +199,12 @@ class Tuner:
|
||||
|
||||
# ── Deadtime optimization ────────────────────────────────────────
|
||||
|
||||
# Firmware now uses a single global dead-time (`dt_normal`, 0x60) rather
|
||||
# than per-current-bracket values, so we sweep the one parameter — optionally
|
||||
# at several load points to expose any load dependence — and pick the best.
|
||||
|
||||
DT_PARAM = "dt_normal"
|
||||
|
||||
def tune_deadtime(
|
||||
self,
|
||||
dt_start: int = 14,
|
||||
@@ -209,45 +215,37 @@ class Tuner:
|
||||
load_mode: str = "CP",
|
||||
load_values: list[float] | None = None,
|
||||
settle_time: float | None = None,
|
||||
) -> dict[str, list[TunePoint]]:
|
||||
"""Optimize deadtime for each current bracket.
|
||||
) -> list[TunePoint]:
|
||||
"""Optimize the single global dead-time (`dt_normal`).
|
||||
|
||||
For each deadtime bracket, sets a load that puts the converter
|
||||
in that current range, then sweeps deadtime values to find the
|
||||
optimum.
|
||||
Sweeps `dt_normal` from `dt_start` to `dt_stop` at each requested load
|
||||
point and returns a flat list of measurements. The best value can be
|
||||
applied with :meth:`apply_best_deadtime`.
|
||||
|
||||
Args:
|
||||
load_values: Load setpoints to test (one per DT bracket).
|
||||
If None, auto-selects based on bracket midpoints.
|
||||
load_values: Load setpoints to test. If None, a single mid-range
|
||||
load is used.
|
||||
"""
|
||||
settle = settle_time or self.settle_time
|
||||
|
||||
if load_values is None:
|
||||
# Auto-select load values targeting the middle of each bracket
|
||||
# Using CP mode: power ≈ voltage × current
|
||||
load_values = []
|
||||
for _, _, i_lo, i_hi in DT_BRACKETS:
|
||||
mid_i_A = (i_lo + i_hi) / 2 / 1000.0 # mA → A
|
||||
target_power = voltage * mid_i_A * 0.4 # rough vout/vin ratio
|
||||
load_values.append(max(10.0, target_power))
|
||||
if not load_values:
|
||||
load_values = [200.0 if load_mode == "CP" else 5.0]
|
||||
|
||||
unit = "A" if load_mode == "CC" else "W"
|
||||
print("=" * 80)
|
||||
print("DEADTIME OPTIMIZATION")
|
||||
print("DEAD-TIME OPTIMIZATION (dt_normal)")
|
||||
print(f" DT range: {dt_start} → {dt_stop} (step {dt_step})")
|
||||
print(f" V={voltage:.0f}V, I_limit={current_limit:.0f}A, mode={load_mode}")
|
||||
print(f" Loads: {', '.join(f'{lv:.0f}{unit}' for lv in load_values)}")
|
||||
print("=" * 80)
|
||||
|
||||
all_results: dict[str, list[TunePoint]] = {}
|
||||
|
||||
for i, (param_id, param_name, i_lo, i_hi) in enumerate(DT_BRACKETS):
|
||||
load_val = load_values[i] if i < len(load_values) else load_values[-1]
|
||||
unit = "A" if load_mode == "CC" else "W"
|
||||
|
||||
print(f"\n── Bracket: {param_name} ({i_lo/1000:.0f}-{i_hi/1000:.0f}A) "
|
||||
f"@ {load_mode}={load_val:.0f}{unit} ──")
|
||||
all_results: list[TunePoint] = []
|
||||
best_per_load: list[tuple[float, TunePoint]] = []
|
||||
|
||||
for load_val in load_values:
|
||||
print(f"\n── {self.DT_PARAM} @ {load_mode}={load_val:.0f}{unit} ──")
|
||||
results = self.sweep_param(
|
||||
param_name=param_name,
|
||||
param_name=self.DT_PARAM,
|
||||
start=dt_start,
|
||||
stop=dt_stop,
|
||||
step=dt_step,
|
||||
@@ -257,44 +255,41 @@ class Tuner:
|
||||
load_value=load_val,
|
||||
settle_time=settle,
|
||||
)
|
||||
all_results[param_name] = results
|
||||
all_results.extend(results)
|
||||
|
||||
# Find and report best
|
||||
if results:
|
||||
valid = [p for p in results if 0 < p.meter_eff < 110]
|
||||
if valid:
|
||||
best = max(valid, key=lambda p: p.meter_eff)
|
||||
print(f" ★ Best: {param_name}={best.param_value:.0f} → "
|
||||
f"EFF={best.meter_eff:.2f}%")
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 80)
|
||||
print("DEADTIME OPTIMIZATION SUMMARY")
|
||||
print(f"{'Bracket':<15} {'Best DT':>8} {'Efficiency':>12} {'Temp':>8}")
|
||||
print("-" * 45)
|
||||
for param_name, results in all_results.items():
|
||||
valid = [p for p in results if 0 < p.meter_eff < 110]
|
||||
if valid:
|
||||
best = max(valid, key=lambda p: p.meter_eff)
|
||||
print(f"{param_name:<15} {best.param_value:>8.0f} "
|
||||
f"{best.meter_eff:>11.2f}% {best.stm_etemp:>7.0f}°C")
|
||||
else:
|
||||
print(f"{param_name:<15} {'N/A':>8} {'N/A':>12} {'N/A':>8}")
|
||||
best_per_load.append((load_val, best))
|
||||
print(f" ★ Best: {self.DT_PARAM}={best.param_value:.0f} → "
|
||||
f"EFF={best.meter_eff:.2f}%")
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 80)
|
||||
print("DEAD-TIME OPTIMIZATION SUMMARY")
|
||||
print(f"{'Load':<12} {'Best DT':>8} {'Efficiency':>12} {'Temp':>8}")
|
||||
print("-" * 42)
|
||||
for load_val, best in best_per_load:
|
||||
print(f"{load_val:<11.0f}{unit} {best.param_value:>8.0f} "
|
||||
f"{best.meter_eff:>11.2f}% {best.stm_etemp:>7.0f}°C")
|
||||
if not best_per_load:
|
||||
print(" (no valid points)")
|
||||
print("=" * 80)
|
||||
|
||||
return all_results
|
||||
|
||||
def apply_best_deadtimes(self, results: dict[str, list[TunePoint]]):
|
||||
"""Apply the best deadtime from each bracket to the STM32."""
|
||||
print("\nApplying optimal deadtimes:")
|
||||
for param_name, points in results.items():
|
||||
valid = [p for p in points if 0 < p.meter_eff < 110]
|
||||
if valid:
|
||||
best = max(valid, key=lambda p: p.meter_eff)
|
||||
val = int(best.param_value)
|
||||
ack = self.link.write_param(param_name, val)
|
||||
status = "OK" if ack else "NO ACK"
|
||||
print(f" {param_name} = {val} ({status})")
|
||||
def apply_best_deadtime(self, results: list[TunePoint]):
|
||||
"""Apply the single best dead-time (highest efficiency) to the STM32."""
|
||||
valid = [p for p in results if 0 < p.meter_eff < 110]
|
||||
if not valid:
|
||||
print("\nNo valid points — dead-time not applied.")
|
||||
return
|
||||
best = max(valid, key=lambda p: p.meter_eff)
|
||||
val = int(best.param_value)
|
||||
ack = self.link.write_param(self.DT_PARAM, val)
|
||||
status = "OK" if ack else "NO ACK"
|
||||
print(f"\nApplying best dead-time: {self.DT_PARAM} = {val} "
|
||||
f"(EFF={best.meter_eff:.2f}%) ({status})")
|
||||
|
||||
# ── Multi-point sweep ────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user