Stator Inductance (Ls) Calculation¶
Purpose¶
This document describes, in implementation detail, how the Motor Health
Check firmware state machine (DESC_081, Software Architecture)
measures per-phase stator inductance during the LDQ_TEST state
(HC_STATE_LDQ_TEST). The implementation lives in
Src/health_check_sm.c (helpers ls_get_phase_current and
ls_measure_phase, state handler hc_state_ldq_test), with the result
structure HC_LsTest_t declared in Inc/health_check_sm.h.
The Ls test runs immediately after RS_TEST and before
BUILD_TEMPORARY_MOTOR_MODEL in the nominal health-check sequence:
... → PASSIVE_MOTOR_CHECK → RS_TEST → LDQ_TEST → BUILD_TEMPORARY_MOTOR_MODEL → ...
It depends directly on the results of RS_TEST (see
Stator Resistance (Rs) Calculation): the per-phase loop resistance computed there is
reused, unmodified, as the R in this test’s L = τ·R calculation.
Measurement principle — RL step-response curve fit¶
When a constant voltage step V is applied to a series R–L
circuit that starts at zero current, the current rises exponentially toward
its steady-state value:
The firmware re-uses exactly the same injection circuit as the RS test
(drive one phase, low-side return through the other two —
rs_apply_injection()), but instead of waiting for steady state and
averaging, it captures the rising transient at high time-resolution and
fits it to the model above to extract the time constant τ. Knowing the
loop resistance R (from the RS test), the loop inductance follows as
L_eff = τ·R, and the physical per-phase inductance is then derived from
L_eff (see Per-phase conversion below).
This curve-fitting approach is used — rather than, say, measuring ripple amplitude at steady state — because it does not depend on knowing the exact applied voltage (which is distorted at low duty cycles by inverter dead time); it only needs the shape of the current rise and the already-known loop resistance.
Sub-state machine¶
LDQ_TEST is internally a sequential sub-state machine, tracked in
hc->ls.sub (HC_LsTest_t.sub). Unlike the RS test (which spreads its
settle/measure phases across many scheduler ticks), each Ls sub-state
performs its entire measurement synchronously inside a single call to
ls_measure_phase() — that function busy-waits for, and collects, the
whole current-rise capture before returning.
ID |
Sub-state |
Action |
|---|---|---|
0 |
|
Calls |
1 |
|
Logs |
2 |
|
Same sequence driving phase V:
|
3 |
|
Same sequence driving phase W:
|
4 |
|
Computes the imbalance flag, logs the |
ls_measure_phase() step by step¶
The function signature is:
static float ls_measure_phase(uint8_t drive_phase, float r_eff_ohm)
where drive_phase selects U/V/W (0/1/2, same convention as
rs_apply_injection) and r_eff_ohm must be the total loop
resistance for that phase’s injection path, as produced by the RS test
(hc->rs.r_u_ohm / r_v_ohm / r_w_ohm). The procedure is:
Guard against an unusable resistance. If
r_eff_ohm < 0.001 Ω(i.e. the RS test found this phase open or otherwise failed), return0.0fimmediately — no measurement is attempted.Enable the DWT cycle counter for microsecond-resolution timestamps that are independent of the PWM/ADC update rate:
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
This avoids the bias that affected an earlier implementation, where sample timing was inferred from the PWM period and each phase picked up a different “PWM-trough phase offset”.
Apply the step injection with
rs_apply_injection(drive_phase)(same helper as the RS test — drive phase atg_rs_duty_pctduty, the other two phases’ low sides on as the return path) and record the injection-start timestampt0 = DWT->CYCCNT.Capture the rising transient. Loop
LS_NUM_SAMPLES(24) times:Busy-wait for the MCSDK ISR to post a new ADC sample, by polling
ls_get_phase_current(drive_phase)until its value changes from the previous reading (bounded by a 200 000-iteration timeout per sample; on timeout the capture loop is aborted early with whatever samples it has so far).Compute the elapsed time since injection start directly from the cycle counter:
t = (DWT->CYCCNT - t0) / SystemCoreClockseconds — a precise, drift-free measure that does not assume anything about the PWM rate.Store the rectified current magnitude
|i|(the injected current can read negative depending on which shunt/phase is driven) and the timestamp into parallel buffersi_buf/t_buf.
ls_get_phase_current()simply mapsdrive_phase(0/1/2) ontoPhaseCurrent_Read().ia_A/ib_A/ic_A.Switch the PWM off (
PWMC_SwitchOffPWM) as soon as the capture loop ends — the injection is no longer needed once the transient has been recorded.Sanity-check the capture. If fewer than
LS_SS_TAIL + 2(8) valid samples were collected, return0.0f(capture failed/timed out).Estimate the steady-state current ``I_ss`` by averaging the last
LS_SS_TAIL(6) samples — the “settled tail” of the waveform:float i_ss = 0.0f; for (k = ncoll - LS_SS_TAIL; k < ncoll; k++) { i_ss += i_buf[k]; } i_ss /= (float)LS_SS_TAIL; if (i_ss < 0.01f) { return 0.0f; }
Estimating
I_ssfrom the measured waveform itself — rather than computing it asV_bus·duty / R— sidesteps the inverter dead-time distortion that makes the commanded voltage an unreliable predictor of the actual applied voltage at low duty cycles. A result below 10 mA is treated as an invalid/open-circuit measurement.Linearize and fit. Rearranging the step-response equation:
\[\ln\!\left(1 - \frac{i(t)}{I_{ss}}\right) = -\frac{t}{\tau}\]gives a straight line through the origin with slope
-1/τ. The code performs an origin-forced linear least-squares fit — i.e. it assumes the line passes through(0, 0)(true by construction:i(0) = 0) and solves only for the slope:\[\min_{\tau} \sum_n \left(y_n + \frac{t_n}{\tau}\right)^2 \;\;\Longrightarrow\;\; \frac{1}{\tau} = \frac{-\sum_n t_n y_n}{\sum_n t_n^2} \;\;\Longrightarrow\;\; \tau = \frac{-\sum_n t_n^2}{\sum_n t_n y_n}\]where
y_n = ln(1 - i_n/I_ss)(always negative for a rising current belowI_ss). In code:for (k = 0; k < ncoll - LS_SS_TAIL; k++) { float ratio = i_buf[k] / i_ss; if (ratio < 0.10f || ratio > 0.90f) { continue; } /* clean region only */ float y = logf(1.0f - ratio); sum_ty += t_buf[k] * y; sum_tt += t_buf[k] * t_buf[k]; } tau = -sum_tt / sum_ty;
Only samples whose
ratio = i/I_ssfalls strictly inside[0.10, 0.90]are included in the fit:Below 10 % the signal is dominated by current-sensor noise and the ADC offset-calibration residual, and
ln(1 - ratio) ≈ -ratiois close to zero — a poor, noise-sensitive contributor to the fit.Above 90 % the argument of the logarithm approaches zero, so
ln(1 - ratio)approaches-∞— a singularity that would let a single near-steady-state sample dominate (and destabilize) the fit.
Restricting the fit to the clean
10–90 %transient region yields a numerically well-conditioned, noise-robust estimate ofτ.Validate the fit. If
Σ tₙ² < 1×10⁻²⁰(degenerate/empty fit) orΣ tₙ·yₙ ≥ 0(wrong-signed slope — current did not rise as expected), return0.0f.Compute the loop inductance
L_eff = τ · R_effand convert to a per-phase value (see next section), which is the function’s return value.
Per-phase conversion¶
The quantity τ·R_eff is the effective loop inductance of the
excited circuit (driven phase in series with the parallel combination of
the other two phases — the same topology the RS test measured resistance
for). For a balanced, star-connected three-phase motor, the code converts
this loop value to a single phase’s self-inductance with a fixed factor:
float l_eff = tau * r_eff_ohm;
return (2.0f / 3.0f) * l_eff; /* per-phase Ls for a balanced star motor */
i.e. \(L_{phase} = \tfrac{2}{3} L_{eff}\). This mirrors the analogous
R_{phase} relationship used implicitly by the RS test (the loop value
equals 1.5× a single balanced phase value, so a phase value is
\(\tfrac{2}{3}\) of the loop value), keeping the Rs and Ls
characterizations dimensionally consistent with each other.
Returns 0 on failure¶
ls_measure_phase() returns 0.0f — meaning “no usable measurement” —
in every one of these cases:
Condition |
Meaning |
|---|---|
|
Phase open / RS measurement invalid; injection current would be unmeasurably small or undefined |
Capture loop times out repeatedly, yielding |
|
fewer than |
ADC ISR not posting samples / open winding |
|
Steady-state current too small to be meaningful (open circuit / no real injection) |
|
Degenerate or wrong-signed fit — no clean rising transient was captured in the 10–90 % window |
A 0 result therefore propagates cleanly into hc->ls.l_x_H and is
visible in both the per-phase log line ([LS] X: 0uH) and the final
LS: report line, without needing a separate error code.
Timing¶
All Ls-test timing/configuration constants are defined at the top of
Src/health_check_sm.c:
Constant |
Value |
Purpose |
|---|---|---|
|
24 |
ADC samples captured per phase, timed with the
DWT cycle counter — spans roughly 800 µs at the
30 kHz PWM/ADC update rate ( |
|
6 |
Number of trailing samples averaged to estimate
the steady-state current |
|
0.15 (15 %) |
Maximum allowed spread between the largest and smallest per-phase inductance, relative to the smallest |
Additional timing characteristics worth noting:
The per-sample busy-wait timeout is 200 000 loop iterations — large enough to comfortably span one PWM/ADC period at the system clock rate while still bounding the worst case if the ADC ISR stalls.
The injected duty cycle is the same
g_rs_duty_pctused by the RS test (default 5 %, see Stator Resistance (Rs) Calculation); the Ls test does not apply its own separate duty.Each phase’s measurement runs to completion synchronously inside
ls_measure_phase()— there is no settle/measure split across scheduler ticks as in the RS test. The capture window itself (LS_NUM_SAMPLES× ADC period ≈ 800 µs) plus injection setup and PWM shutdown dominate the per-phase duration; three phases plus the baseline calibration run back-to-back within theLDQ_TESTstate.
Imbalance detection¶
Once all three phases have been measured (entering LS_SUB_DONE), the
firmware compares the three resulting inductances directly (no
open/closed filtering is performed here — a phase whose RS measurement
failed will already have produced l_x_H = 0, which naturally dominates
the spread calculation and triggers the imbalance flag):
float l_min = hc->ls.l_u_H, l_max = hc->ls.l_u_H;
/* l_min/l_max updated against l_v_H and l_w_H */
hc->ls.imbalance = (l_min > 1.0e-6f) &&
((l_max - l_min) / l_min > LS_IMBALANCE_THRESH);
In words: imbalance is flagged when the spread between the largest and
smallest per-phase inductance exceeds 15 % of the smallest, provided the
smallest is non-trivially above zero (> 1 µH, screening out the
degenerate all-zero case where every measurement failed).
Overall verdict and reporting¶
LS_SUB_DONE emits, over the UART log (SimpleUART_Log):
A machine-parsable result line, with inductances expressed in integer microhenries:
LS:U:<uH> V:<uH> W:<uH> uH[ IMBALANCE]
parsed by the desktop GUI (
tools/uart_gui) in the same way as theRS:line.A pass/fail line:
[LS] All phases OK PASS— when no inductance imbalance was detected.[LS] FAIL — inductance imbalance detected— otherwise.
State transition¶
LS_SUB_DONE unconditionally transitions the top-level state machine to
HC_STATE_BUILD_TEMPORARY_MOTOR_MODEL (DESC_083, currently a placeholder
that is intended to build a conservative motor model from the Rs and Ls
results). As with the RS test, the Ls test does not raise a critical fault
on imbalance or measurement failure — these are surfaced only through the
logged flags and the LS: report line, to be consolidated later in
REPORT_GENERATION.
Dependency on the Rs test¶
The Ls test cannot run meaningfully without valid RS results:
It reuses
hc->rs.r_u_ohm/r_v_ohm/r_w_ohmas-is as the loop resistanceRinτ = L/R— these already represent the total series resistance of the same injection-path topology (R_drive + R_return1 ∥ R_return2), so no further combination is needed (see the comment inhc_state_ldq_test(): “r_u_ohm from the Rs test already equals R_U + R_V||R_W — the total series resistance for this injection path. Do not re-add terms.”).If a phase’s RS measurement failed (open circuit →
r_x_ohm == 0),ls_measure_phase()detectsr_eff_ohm < 0.001 Ωand returns0immediately, without attempting an injection on that phase.
See Stator Resistance (Rs) Calculation for the full RS procedure and the precise meaning of the resistance values it produces.
Source reference¶
Inc/health_check_sm.h—HC_LsTest_tresult structure,HC_STATE_LDQ_TESTenum value.Src/health_check_sm.c:LS configuration constants (
LS_NUM_SAMPLES,LS_SS_TAIL,LS_IMBALANCE_THRESH)LS sub-state IDs (
LS_SUB_BASELINE…LS_SUB_DONE)ls_get_phase_current()— phase-current accessor by indexls_measure_phase()— injection, capture, curve fit, and per-phase inductance computationhc_state_ldq_test()— the LDQ_TEST state handler / sub-state machiners_apply_injection()— shared injection helper (see Stator Resistance (Rs) Calculation)
Inc/drive_parameters.h—PWM_FREQUENCY(30 kHz ADC/PWM update rate referenced in the sample-count timing estimate).