Service: Electrical Parameters Identification

Field Value
Title Service: Electrical Parameters Identification
Type design
Status draft
Version 0.2.0
Component service-electrical-ident
Date 2026-08-30

IMPORTANT — Implementation-blind document: This document describes behavior, structure, and responsibilities WITHOUT referencing code. No code blocks using programming languages (C++, C, Python, CMake, shell, etc.) are allowed. Use Mermaid diagrams to express behavior instead. Prose descriptions of algorithms are encouraged; source-level details are not.

Diagrams: All visuals must be either a Mermaid fenced code block (```mermaid) or ASCII art inline in the document. External image references using Markdown image syntax are not allowed.


Responsibilities

Is responsible for: - Measuring phase resistance (\(R_s\)) via DC voltage step using ResistanceEstimator - Measuring d-axis inductance (\(L_s\)) via HF sinusoidal injection using SinusoidalInductanceEstimator - Estimating the number of pole pairs by rotating an open-loop voltage vector and comparing electrical angle to mechanical encoder angle - Delivering a combined ResistanceInductanceResult — resistance, inductance, and fit quality — via a single completion callback once both sub-estimators finish - Enforcing that R/L identification and pole-pairs estimation cannot run concurrently - Stopping the inverter cleanly before invoking any completion callback - Delivering every completion from the event loop, never from the sampling interrupt that decides it - Rejecting calibration when fitQuality < 0.5 (incoherent injection response)

Is NOT responsible for: - Persisting identified parameters — the caller decides what to do with results - Encoder zero-offset calibration — that is the Motor Alignment service - Performing any closed-loop current control — all voltage application is open-loop - Running concurrently with the normal FOC loop — the FOC loop must be stopped before either procedure begins


Architecture

The R/L estimation is split across three cooperating components:

Block diagram

ResistanceEstimator and SinusoidalInductanceEstimator are standalone components; the orchestrator calls them sequentially and assembles their results into a single ResistanceInductanceResult. Application code (hardware test terminal, etc.) may also call these components directly.


Component Details

Open-Loop Injection Current Limit

Every procedure in this service drives the bridge open-loop: a fixed duty is applied and held, with no current regulator in the path. The resulting current is set entirely by the applied voltage and the winding impedance, and a low-resistance motor reaches a large current within the settle time. The resistance test in particular holds a DC duty for seconds.

Each procedure therefore registers a phase-current callback that checks every sample against ThreePhaseInverter::MaxCurrentSupported() and aborts immediately if any phase exceeds it — stopping the inverter, cancelling the step timer, and reporting failure through the procedure’s normal failure path. Previously the pole-pairs and resistance procedures registered an empty callback and discarded the samples, leaving hardware comparators (present on some boards, absent on others) as the only backstop. MotorAlignment applies the same check.

The limit is a per-sample abort, not regulation: it stops a runaway, it does not hold a setpoint.

Ending a procedure

A procedure that has finished, failed or been aborted does not reclaim the inverter’s phase-current callback slot. Reassigning that slot from inside its own invocation destroys the closure being executed, and every one of these procedures fails from inside that callback. Instead each procedure is gated on its own pending completion, and a sample that arrives after the procedure ended is discarded before it reaches anything. This matters most for the inductance injection, whose callback writes duty cycles on every sample: ungated, it would re-arm the peripheral that the abort had just stopped.

Abort() is the caller-facing form of the same shutdown. It stops injection, cancels all timers (including the no-sample watchdog), and drops the pending completion without invoking it — the caller that aborted owns the outcome, so a fault raised by the state machine cannot be overwritten by the result of the procedure it interrupted. It covers whichever stage is live: resistance, inductance injection, or the pole-pairs sweep.

The decisions that end a procedure on a sample — the resistance buffer filling, the inductance block completing, a phase current over the limit — are taken in the sampling interrupt, and so is the one thing that must happen at that instant: the injection is stopped there. The completion is not. Computing the result, starting the inductance stage once the resistance is known, and whatever the caller does next — answering over CAN, tracing, starting the next calibration step — is event-loop work. Run from the interrupt, it started the inductance stage from inside the resistance stage’s own sample callback, started and cancelled timers there, and in a full calibration took one execution of 6709 cycles, more than five control periods (software-in-the-loop.md, Part I). So the outcome is queued and delivered from the event loop. Until then the run is still in progress: samples are discarded, the settle and step timers and the no-sample watchdog do nothing, and Abort() drops the queued outcome. The outcome carries the run it belongs to, so one that an abort overtook cannot complete a run started after it. Outcomes decided on the event loop already — the pole-pair count, which the sweep computes on its step timer, and a no-sample failure — complete directly, unless an over-current outcome is already queued, which they then leave to be delivered. The interrupt can also trip while the event loop is between its check and a write to the bridge — the sweep’s next step, or the first duty a procedure applies — and that write would re-arm the bridge the interrupt had just stopped, so each such write is followed by a second check that stops the bridge again.

Every estimator is also RAII-safe: if destroyed while active, the destructor cancels all timers and stops the driver without invoking the completion callback.

ResistanceEstimator

Applies a DC differential voltage across phase A versus phases B and C held at neutral. A TimerSingleShot fires after the configured settle time to allow the current to reach steady state. A 5-sample moving-average filter (using a bounded deque of capacity 5) is applied in-flight to each ADC sample before it is pushed to the measurement buffer (bounded vector, capacity 123).

When the buffer is full, the steady-state current is read from the mean of the last 10 % of the buffer. Resistance is computed as \(V_{step} / I_{ss}\) divided by the winding topology factor. If \(I_{ss} \leq 0\), the result is absent.

No-sample watchdog: a second TimerSingleShot is armed immediately after the DC duty is applied and resets on every ADC callback (during both settle and measurement phases). Default bound: 100 ms (Config::noSampleTimeout). If the ADC produces no callback within that window — whether at startup or after samples stop mid-measurement — the watchdog fires FailMeasurement(), stopping the driver and delivering an absent result. This ensures ADC loss does not leave the motor energised indefinitely regardless of the configured settle time.

RAII cleanup: the destructor cancels all timers and stops the driver if the estimator is destroyed while a measurement is active, so callers do not need to call Abort() explicitly before releasing the object.

State diagram

Internal buffers:

Buffer Capacity Purpose
Moving-avg window 5 samples In-flight noise reduction on ADC input
Measurement store 123 samples Filtered step-response for steady-state read

SinusoidalInductanceEstimator

Injects a sinusoidal voltage on the alpha axis only (beta = 0), keeping net electromagnetic torque at zero so the rotor remains stationary. The injection frequency is snapped to the nearest integer-samples-per-period to align exactly with the Goertzel analysis bin, eliminating spectral leakage.

Two phases run consecutively:

  1. Warmup — voltage is injected but the Goertzel is not fed. Allows the current transient to decay and the steady-state sinusoidal response to establish. Duration: warmupPeriods full injection cycles.

  2. Measurement — the Goertzel accumulates measurementPeriods full cycles of current samples (block size \(N = N_{periods} \times N_{spp}\), bin \(k = N_{periods}\)). When Ready(), the complex result is delay-corrected (rotation by \(e^{+j\omega d T_s}\) to cancel the \(d\)-sample ADC pipeline lag), then converted to \(\text{Im}(Z)\) and finally to per-phase inductance.

The coherence metric fitQuality = 2|I_{Goertzel}|² / (N · \Sigma i²) is computed simultaneously from the accumulated current power. It equals 1.0 for a pure sinusoid at \(f_{inj}\) and falls toward zero with increasing noise or rotor motion.

Working memory (streaming — no sample buffer required):

Variable Size Purpose
Goertzel state 3 float \(s_1\), \(s_2\), sample count
sumSquared 1 float Accumulates \(\Sigma i^2\) for fitQuality
sampleCount 1 int Warmup/measurement gate

PWM bootstrap: the ADC on TI targets is triggered by the PWM peripheral. Because the resistance estimator stops PWM on completion, the inductance estimator writes a zero-voltage vector immediately after registering the ADC callback to restart the PWM-triggered ADC chain before the first sample is expected.

No-sample watchdog: a TimerSingleShot is armed when injection begins and resets on every ADC callback. Default bound: 100 ms (Config::noSampleTimeout). Loss of ADC samples — whether the bootstrap never triggers or samples stop mid-measurement — causes the watchdog to fire FailMeasurement(), stopping the driver and delivering an absent result within the documented bound.

RAII cleanup: the destructor cancels the watchdog timer and stops the driver if the estimator is destroyed while a measurement is active.

State diagram

ElectricalParametersIdentificationImpl (orchestrator)

Sequences ResistanceEstimator then SinusoidalInductanceEstimator and assembles their results into a single ResistanceInductanceResult. If resistance estimation fails (absent result), the L stage is skipped and the combined callback fires immediately with an absent result. If the L stage yields fitQuality < 0.5, the orchestrator treats this as a fault and includes the low-quality result so the caller can make its own policy decision.

State diagram

Concurrent-call invariant: a second call to EstimateResistanceAndInductance while any of the five states above is active causes the new request to be rejected immediately (callback fires with an absent result). The pole-pairs procedure shares this guard: neither procedure may start while the other is active.


Procedure 2 — Pole Pairs Estimation

Starting at electrical angle 0°, the service advances the voltage vector by a small angular increment each settle-timer tick. After each step, the encoder is read and the mechanical displacement is accumulated. Over \(N_{rev}\) full electrical revolutions:

\[ p = \text{round}(2\pi N_{rev} / \Delta\theta_{mech,total}) \]

If the total mechanical rotation is below \(\pi/2\) (rotor not following the field), the result is absent.

Block diagram

Interfaces

Provided

Interface Purpose Contract
EstimateResistanceAndInductance(config, onDone) Sequences DC-step R estimation then sinusoidal L estimation; delivers ResistanceInductanceResult{resistance, inductance, fitQuality} Rejected (immediate empty callback) if pole-pairs procedure is already running; inverter stopped before callback fires; fires exactly once, from the event loop
EstimateNumberOfPolePairs(config, onDone) Sweeps an open-loop rotating vector and delivers optional<size_t> pole pairs Rejected if R/L procedure is already running; inverter stopped before callback fires; fires exactly once, from the event loop

Result Type

ResistanceInductanceResult carries:

Field Type Absent when
resistance optional<Ohm> DC step finds \(I_{ss} = 0\)
inductance optional<MilliHenry> Goertzel yields \(L \leq 0\)
fitQuality float in \([0, 1]\) Always present; 0 on failure

Required

Interface Purpose Contract
ThreePhaseInverter Voltage application and ADC sampling callbacks for both stages Must not be concurrently claimed by any other controller
Encoder Reads mechanical angle during pole-pairs sweep Must be initialised before pole-pairs estimation begins
DC bus voltage (Volts) Normalises applied voltage and interprets current in physical units Must remain stable throughout any active procedure

Online Resistance and Inductance Estimation

In addition to the one-shot calibration procedures above, a continuous online estimator (RealTimeResistanceAndInductanceEstimator) runs alongside the closed-loop speed/position controller to track slow parameter drift. See the state machine design document for when online estimates are applied.

Model

The d-axis voltage equation for a non-salient PMSM is:

\[V_d = R \cdot I_d + L \cdot \left(\frac{dI_d}{dt} - \omega_e \cdot I_q\right)\]

Integrated over one outer-loop period \(T\) it needs no derivative of a sampled current:

\[\overline{V_d} = R\,\overline{I_d} + L\left(\frac{I_{d,end} - I_{d,start}}{T} - \overline{\omega_e I_q}\right)\]

The regressor vector is \(\phi = [\overline{I_d},\ (\Delta I_d / T - \overline{\omega_e I_q})]^T\) in amperes and amperes per millisecond, and the parameter vector is \(\theta = [R,\ L]^T\) in ohms and millihenries. An RLS algorithm with forgetting factor 0.999 updates \(\theta\) once per outer-loop period (1 kHz).

The averages are accumulated by the control interrupt, tick by tick, and published with each window:

Excitation

The product regulates \(I_d\) to zero, so the resistance column is never excited on its own: the recursion drifted, read \(-0.68\ \Omega\) for a \(0.36\ \Omega\) winding, and could not by construction track a warming one. The speed and position cascades therefore add a d-axis square wave to the current reference while the estimator is attached: \(\pm 2.5\,\%\) of the drive’s current limit at 10 Hz. It produces no torque on a non-salient machine and a copper loss proportional to its square. An update is taken only while \(|\overline{I_d}|\) exceeds 50 mA, so an amplitude of zero switches both the injection and resistance tracking off.

Publication

Estimates are published after 200 updates since the last seed, and only when both are finite and positive.

Seeding

When calibration data is loaded (from NVM or after fresh calibration), the online estimator is seeded with the identified values, avoiding a cold-start transient where estimates are physically meaningless.