Add STM32 tuning integration and rewrite README with step-by-step guide

- stm32_link.py: synchronous serial interface to STM32 debug protocol
  (ping, telemetry read/avg, param read/write, frame parser)
- tuner.py: automated tuning combining HIOKI + STM32 measurements
  (param sweep, deadtime optimization, multi-point sweep, CSV/plot output)
- CLI commands: stm32-read, stm32-write, tune-param, tune-deadtime
- README: complete step-by-step guide covering setup, sweeps, analysis,
  tuning, shade profiles, debug console, and parameter reference

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 16:52:09 +07:00
parent e7a23a3c7e
commit 1c41910c1e
4 changed files with 1297 additions and 95 deletions

View File

@@ -550,6 +550,124 @@ def cmd_safe_off(bench: MPPTTestbench, _args: argparse.Namespace) -> None:
print("Done.")
# ── Tuning commands (instruments + STM32) ────────────────────────────
def cmd_stm32_read(bench: MPPTTestbench, args: argparse.Namespace) -> None:
"""Read all STM32 parameters and telemetry."""
from testbench.stm32_link import STM32Link
with STM32Link(args.stm32_port, args.stm32_baud) as link:
if not link.ping():
print("STM32 not responding to ping!")
return
print("STM32 connected.\n")
# Read params
params = link.read_all_params()
if params:
print("Parameters:")
for name, val in sorted(params.items()):
print(f" {name:<20s} = {val}")
print()
# Read telemetry
t = link.read_telemetry_avg(n=20)
if t:
print("Telemetry (20-sample avg):")
print(f" Vin = {t.vin_V:8.2f} V")
print(f" Vout = {t.vout_V:8.2f} V")
print(f" Iin = {t.iin_A:8.2f} A")
print(f" Iout = {t.iout_A:8.2f} A")
print(f" Pin = {t.power_in_W:8.2f} W")
print(f" Pout = {t.power_out_W:8.2f} W")
print(f" EFF = {t.efficiency:8.1f} %")
print(f" Vfly = {t.vfly/1000:8.2f} V")
print(f" Temp = {t.etemp:8.1f} °C")
def cmd_stm32_write(bench: MPPTTestbench, args: argparse.Namespace) -> None:
"""Write a parameter to the STM32."""
from testbench.stm32_link import STM32Link
with STM32Link(args.stm32_port, args.stm32_baud) as link:
if not link.ping():
print("STM32 not responding to ping!")
return
ack = link.write_param(args.param, args.value)
print(f"{args.param} = {args.value} {'ACK' if ack else 'NO ACK'}")
def cmd_tune_param(bench: MPPTTestbench, args: argparse.Namespace) -> None:
"""Sweep an STM32 parameter while measuring efficiency."""
from testbench.stm32_link import STM32Link
from testbench.tuner import Tuner
with STM32Link(args.stm32_port, args.stm32_baud) as link:
if not link.ping():
print("STM32 not responding!")
return
tuner = Tuner(bench, link, settle_time=args.settle)
results = tuner.sweep_param(
param_name=args.param,
start=args.start,
stop=args.stop,
step=args.step,
voltage=args.voltage,
current_limit=args.current_limit,
load_mode=args.load_mode,
load_value=args.load_value,
settle_time=args.settle,
)
tuner.print_results(results)
if args.output:
tuner.write_csv(results, args.output)
if not args.no_plot and results:
tuner.plot_sweep(results, show=True)
def cmd_tune_deadtime(bench: MPPTTestbench, args: argparse.Namespace) -> None:
"""Optimize deadtime for each current bracket."""
from testbench.stm32_link import STM32Link
from testbench.tuner import Tuner
with STM32Link(args.stm32_port, args.stm32_baud) as link:
if not link.ping():
print("STM32 not responding!")
return
tuner = Tuner(bench, link, settle_time=args.settle)
load_values = None
if args.load_values:
load_values = [float(x) for x in args.load_values.split(",")]
results = tuner.tune_deadtime(
dt_start=args.dt_start,
dt_stop=args.dt_stop,
dt_step=args.dt_step,
voltage=args.voltage,
current_limit=args.current_limit,
load_mode=args.load_mode,
load_values=load_values,
settle_time=args.settle,
)
if args.apply:
tuner.apply_best_deadtimes(results)
if args.output:
all_pts = []
for pts in results.values():
all_pts.extend(pts)
tuner.write_csv(all_pts, args.output)
# ── Offline plot command (no instruments needed) ─────────────────────
@@ -726,6 +844,9 @@ examples:
%(prog)s safe-off
%(prog)s plot-sweep sweep_vi_20260312_151212.csv
%(prog)s plot-sweep sweep_vi_20260312_151212.csv --no-show -o plots/
%(prog)s stm32-read --stm32-port COM28
%(prog)s tune-param --stm32-port COM28 --param dt_10_20A --start 14 --stop 40 --step 1 --voltage 60 --current-limit 20 --load-mode CP --load-value 300
%(prog)s tune-deadtime --stm32-port COM28 --voltage 60 --current-limit 20 --load-mode CP --apply
""",
)
@@ -750,6 +871,14 @@ examples:
"--timeout", type=int, default=5000,
help="VISA timeout in ms (default: 5000)",
)
parser.add_argument(
"--stm32-port", default="COM28",
help="STM32 debug serial port (default: COM28)",
)
parser.add_argument(
"--stm32-baud", type=int, default=460800,
help="STM32 debug baud rate (default: 460800)",
)
sub = parser.add_subparsers(dest="command", required=True)
@@ -845,6 +974,41 @@ examples:
# safe-off
sub.add_parser("safe-off", help="Emergency: turn off load and supply")
# stm32-read
sub.add_parser("stm32-read", help="Read STM32 parameters and telemetry")
# stm32-write
p_sw = sub.add_parser("stm32-write", help="Write a parameter to the STM32")
p_sw.add_argument("--param", required=True, help="Parameter name")
p_sw.add_argument("--value", type=float, required=True, help="Value to write")
# tune-param
p_tp = sub.add_parser("tune-param", help="Sweep an STM32 parameter while measuring efficiency")
p_tp.add_argument("--param", required=True, help="Parameter name (e.g. dt_10_20A, vfly_kp)")
p_tp.add_argument("--start", type=float, required=True, help="Start value")
p_tp.add_argument("--stop", type=float, required=True, help="Stop value")
p_tp.add_argument("--step", type=float, required=True, help="Step size")
p_tp.add_argument("--voltage", type=float, required=True, help="Supply voltage (V)")
p_tp.add_argument("--current-limit", type=float, required=True, help="Supply current limit (A)")
p_tp.add_argument("--load-mode", choices=["CC", "CP"], default="CP", help="Load mode")
p_tp.add_argument("--load-value", type=float, default=200.0, help="Load setpoint (A or W)")
p_tp.add_argument("--settle", type=float, default=3.0, help="Settle time per step (s)")
p_tp.add_argument("--no-plot", action="store_true", help="Skip plot")
p_tp.add_argument("-o", "--output", help="CSV output file")
# tune-deadtime
p_td = sub.add_parser("tune-deadtime", help="Optimize deadtime for each current bracket")
p_td.add_argument("--dt-start", type=int, default=14, help="Min deadtime ticks (default: 14)")
p_td.add_argument("--dt-stop", type=int, default=50, help="Max deadtime ticks (default: 50)")
p_td.add_argument("--dt-step", type=int, default=1, help="Deadtime step (default: 1)")
p_td.add_argument("--voltage", type=float, default=60.0, help="Supply voltage (V)")
p_td.add_argument("--current-limit", type=float, default=20.0, help="Supply current limit (A)")
p_td.add_argument("--load-mode", choices=["CC", "CP"], default="CP", help="Load mode")
p_td.add_argument("--load-values", help="Comma-separated load values per bracket (e.g. 20,50,100,250,400,600)")
p_td.add_argument("--settle", type=float, default=3.0, help="Settle time per step (s)")
p_td.add_argument("--apply", action="store_true", help="Apply best deadtimes after sweep")
p_td.add_argument("-o", "--output", help="CSV output file")
# plot-sweep (offline, no instruments)
p_plot = sub.add_parser("plot-sweep", help="Plot efficiency analysis from sweep CSV (no instruments needed)")
p_plot.add_argument("csv", help="Sweep CSV file to plot")
@@ -872,6 +1036,10 @@ examples:
"supply": cmd_supply,
"load": cmd_load,
"safe-off": cmd_safe_off,
"stm32-read": cmd_stm32_read,
"stm32-write": cmd_stm32_write,
"tune-param": cmd_tune_param,
"tune-deadtime": cmd_tune_deadtime,
}
bench = connect_bench(args)