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 RL circuit that starts at zero current, the current rises exponentially toward its steady-state value:

\[i(t) = I_{ss} \left(1 - e^{-t/\tau}\right), \qquad \tau = \frac{L}{R}, \qquad I_{ss} = \frac{V}{R}\]

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

LS_SUB_BASELINE

Calls PhaseCurrent_Calibrate() to re-capture the zero-current ADC offset (PWM is already off, left that way by RS_SUB_DONE). Logs [LS] Calibrating current baseline... / [LS] Baseline captured.

1

LS_SUB_SETUP_U

Logs [LS] Measuring U..., calls ls_measure_phase(0, hc->rs.r_u_ohm), stores the result in hc->ls.l_u_H, logs [LS] U: <uH> uH, advances to LS_SUB_SETUP_V.

2

LS_SUB_SETUP_V

Same sequence driving phase V: ls_measure_phase(1, hc->rs.r_v_ohm)hc->ls.l_v_H. Advances to LS_SUB_SETUP_W.

3

LS_SUB_SETUP_W

Same sequence driving phase W: ls_measure_phase(2, hc->rs.r_w_ohm)hc->ls.l_w_H. Advances to LS_SUB_DONE.

4

LS_SUB_DONE

Computes the imbalance flag, logs the LS: summary line and the pass/fail verdict, then transitions the top-level state machine to HC_STATE_BUILD_TEMPORARY_MOTOR_MODEL.

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:

  1. Guard against an unusable resistance. If r_eff_ohm < 0.001 Ω (i.e. the RS test found this phase open or otherwise failed), return 0.0f immediately — no measurement is attempted.

  2. 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”.

  3. Apply the step injection with rs_apply_injection(drive_phase) (same helper as the RS test — drive phase at g_rs_duty_pct duty, the other two phases’ low sides on as the return path) and record the injection-start timestamp t0 = DWT->CYCCNT.

  4. 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) / SystemCoreClock seconds — 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 buffers i_buf / t_buf.

    ls_get_phase_current() simply maps drive_phase (0/1/2) onto PhaseCurrent_Read().ia_A / ib_A / ic_A.

  5. 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.

  6. Sanity-check the capture. If fewer than LS_SS_TAIL + 2 (8) valid samples were collected, return 0.0f (capture failed/timed out).

  7. 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_ss from the measured waveform itself — rather than computing it as V_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.

  8. 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 below I_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_ss falls 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) -ratio is 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 τ.

  9. Validate the fit. If Σ tₙ² < 1×10⁻²⁰ (degenerate/empty fit) or Σ tₙ·yₙ 0 (wrong-signed slope — current did not rise as expected), return 0.0f.

  10. Compute the loop inductance L_eff = τ · R_eff and 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

r_eff_ohm < 0.001 Ω

Phase open / RS measurement invalid; injection current would be unmeasurably small or undefined

Capture loop times out repeatedly, yielding

fewer than LS_SS_TAIL + 2 (8) samples

ADC ISR not posting samples / open winding

i_ss < 0.01 A

Steady-state current too small to be meaningful (open circuit / no real injection)

Σ tₙ² < 1×10⁻²⁰ or Σ tₙ·yₙ 0

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

LS_NUM_SAMPLES

24

ADC samples captured per phase, timed with the DWT cycle counter — spans roughly 800 µs at the 30 kHz PWM/ADC update rate (PWM_FREQUENCY in Inc/drive_parameters.h)

LS_SS_TAIL

6

Number of trailing samples averaged to estimate the steady-state current I_ss

LS_IMBALANCE_THRESH

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_pct used 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 the LDQ_TEST state.

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):

  1. 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 the RS: line.

  2. 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_ohm as-is as the loop resistance R in τ = 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 in hc_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() detects r_eff_ohm < 0.001 Ω and returns 0 immediately, 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.hHC_LsTest_t result structure, HC_STATE_LDQ_TEST enum value.

  • Src/health_check_sm.c:

    • LS configuration constants (LS_NUM_SAMPLES, LS_SS_TAIL, LS_IMBALANCE_THRESH)

    • LS sub-state IDs (LS_SUB_BASELINELS_SUB_DONE)

    • ls_get_phase_current() — phase-current accessor by index

    • ls_measure_phase() — injection, capture, curve fit, and per-phase inductance computation

    • hc_state_ldq_test() — the LDQ_TEST state handler / sub-state machine

    • rs_apply_injection() — shared injection helper (see Stator Resistance (Rs) Calculation)

  • Inc/drive_parameters.hPWM_FREQUENCY (30 kHz ADC/PWM update rate referenced in the sample-count timing estimate).