Stator Resistance (Rs) Calculation

Purpose

This document describes, in implementation detail, how the Motor Health Check firmware state machine (DESC_081, Software Architecture) measures per-phase stator winding resistance during the RS_TEST state (HC_STATE_RS_TEST). The implementation lives in ../../Src/health_check_sm.c (functions rs_apply_injection and hc_state_rs_test), with the result structure HC_RsTest_t declared in Inc/health_check_sm.h.

The RS test runs immediately after PASSIVE_MOTOR_CHECK and before LDQ_TEST in the nominal health-check sequence:

IDLE → HARDWARE_SELF_CHECK → PASSIVE_MOTOR_CHECK → RS_TEST → LDQ_TEST → ...

Its purposes are to (a) characterize the apparent series resistance of each phase’s injection path, (b) detect open windings, (c) detect resistance imbalance across phases, and (d) hand a known total-loop resistance to the subsequent LDQ_TEST (Ls) measurement, which reuses it directly (see Stator Inductance (Ls) Calculation).

Measurement principle — DC current injection

The motor is stationary (no PWM active) when the test begins. For each phase in turn, the firmware applies a small constant PWM duty cycle to that phase’s high-side leg while the other two phases’ low-side switches are turned on, forming a return path through the windings. This is implemented by rs_apply_injection():

PWMC_SwitchOffPWM(pwmcHandle[M1]);
PWMC_TurnOnLowSides(pwmcHandle[M1], 0u);  /* all CCRs=0, MOE enabled */

uint32_t ticks = (uint32_t)g_rs_duty_pct * (uint32_t)PWM_PERIOD_CYCLES / 100U;
/* set CCRx of the drive phase (TIM1 CH1=U, CH2=V, CH3=W) to `ticks`,
   with preload disable → set compare → preload enable, so the new
   duty applies immediately rather than at the next update event */

The drive-phase mapping is:

drive_phase

Phase

Timer channel driven

0

U

TIM1 CH1 (LL_TIM_OC_SetCompareCH1)

1

V

TIM1 CH2 (LL_TIM_OC_SetCompareCH2)

2

W

TIM1 CH3 (LL_TIM_OC_SetCompareCH3)

This produces an average injected voltage

\[V_{inj} = V_{bus} \times \frac{duty\,\%}{100}\]

across a series circuit consisting of the driven phase’s winding resistance and the parallel combination of the other two phases’ winding resistances (R_drive + R_return1 R_return2). The resulting average current I_avg is measured with the offset-corrected 3-shunt phase-current sensor (PhaseCurrent_Read(), see Src/phase_current.c), and the apparent resistance is computed as:

\[R_{apparent} = \frac{V_{inj}}{I_{avg}} = \frac{V_{bus} \times duty\,\%}{100 \times I_{avg}}\]

This is exactly what the code computes for each phase, e.g. for phase U:

float vbus     = (float)VBS_GetAvBusVoltage_V(&BusVoltageSensor_M1._Super);
hc->rs.r_u_ohm = vbus * (float)g_rs_duty_pct / (100.0f * I_avg);

Important

The value stored in r_u_ohm / r_v_ohm / r_w_ohm is not a pure per-phase winding resistance. It is the total series resistance of that phase’s injection path — drive-phase resistance plus the parallel combination of the other two phases. This is intentional: the LDQ_TEST state reuses this exact value as the loop resistance R_eff for its RL time-constant calculation (see Stator Inductance (Ls) Calculation), since both tests excite the same circuit topology.

Applied duty cycle

  • Default duty: RS_TEST_DUTY_PCT = 5U (5 %), held in the global g_rs_duty_pct.

  • Range: 1–30 %, configurable at runtime via the UART command RS:DUTY:<pct> before the health check is started (see Inc/health_check_sm.h comment on g_rs_duty_pct).

  • Converted to timer compare ticks as ticks = duty% * PWM_PERIOD_CYCLES / 100 and written directly to the active channel’s capture/compare register.

The duty is intentionally low: it is just large enough to produce a measurable current (above the open-circuit threshold, see below) without producing significant torque or heating while the rotor is stationary.

Sub-state machine

RS_TEST is internally a sequential sub-state machine, tracked in hc->rs.sub (HC_RsTest_t.sub) and driven once per call to hc_state_rs_test(). The sub-states run in this fixed order:

ID

Sub-state

Action

0

RS_SUB_BASELINE

Calls PhaseCurrent_Calibrate() (PWM still off) to capture the zero-current ADC offset over 16 samples spaced 1 ms apart, so that all subsequent current readings are bias-corrected. Logs [RS] Calibrating current baseline... / [RS] Baseline captured.

1

RS_SUB_SETUP_U

Logs the injected duty, calls rs_apply_injection(0) (drive U), latches hc->rs.tick = now and advances to the settle sub-state.

2

RS_SUB_SETTLE_U

Waits until (now - tick) >= RS_SETTLE_MS (80 ms), then resets the accumulator/sample counter and advances to the measure sub-state.

3

RS_SUB_MEASURE_U

Each scheduler tick: reads Ia, accumulates |Ia| and increments the sample count. Once (now - tick) >= RS_MEASURE_MS (40 ms), computes I_avg = acc / samples, classifies the phase as open or computes r_u_ohm (see formulas above), logs the result, and moves on to RS_SUB_SETUP_V.

4–6

RS_SUB_SETUP_V / _SETTLE_V / _MEASURE_V

Identical sequence, driving phase V (rs_apply_injection(1), channel CH2) and measuring Ib; produces r_v_ohm. Advances to RS_SUB_SETUP_W.

7–9

RS_SUB_SETUP_W / _SETTLE_W / _MEASURE_W

Identical sequence, driving phase W (rs_apply_injection(2), channel CH3) and measuring Ic; produces r_w_ohm. Advances to RS_SUB_DONE.

10

RS_SUB_DONE

Switches PWM off, evaluates open-circuit/imbalance flags, logs the per-phase summary line and the pass/fail verdict, then transitions the top-level state machine to HC_STATE_LDQ_TEST.

Each phase therefore goes through the same three-step cycle: setup → settle → measure, driven on three consecutive timer channels (U → V → W), and all three results are evaluated together once collection is complete.

Timing

All RS-test timing constants are defined at the top of Src/health_check_sm.c:

Constant

Value

Purpose

RS_TEST_DUTY_PCT

5 %

Default DC-injection duty cycle

RS_SETTLE_MS

80 ms

Dwell time after switching the injection path, before sampling begins — allows the winding’s L/R electrical transient to die out so the measured current reflects the DC steady state

RS_MEASURE_MS

40 ms

Width of the current-averaging window; samples are accumulated on every scheduler tick that the sub-state machine is invoked during this window

RS_OPEN_THRESH_A

0.03 A

Minimum average current to consider the winding electrically connected (30 mA)

RS_IMBALANCE_THRESH

0.20 (20 %)

Maximum allowed spread between the largest and smallest valid phase resistance, relative to the smallest

Putting the per-phase cycle together, each phase requires RS_SETTLE_MS + RS_MEASURE_MS = 120 ms of injection plus the (negligible) setup tick, and the three phases run back to back. The baseline calibration adds roughly CAL_SAMPLES (16) × 1 ms 16 ms. The whole RS_TEST state therefore completes in approximately:

16 ms (baseline)  +  3 × 120 ms (settle+measure)  ≈  376 ms

(plus negligible per-tick scheduling overhead and the final evaluation step).

Open-circuit detection

After the 40 ms measurement window for a phase, the average current I_avg is compared against RS_OPEN_THRESH_A (30 mA):

  • If I_avg < RS_OPEN_THRESH_A: the phase is flagged open (hc->rs.open_u / open_v / open_w = true), its resistance is forced to 0.0f, and [RS] <phase>: OPEN CIRCUIT is logged. An open phase is excluded from the resistance value reported and from the imbalance comparison (see below).

  • Otherwise the phase is marked closed (open_x = false) and its apparent resistance is computed and logged as [RS] <phase>: <mOhm> mOhm  I:<mA> mA.

This threshold (30 mA, with a 5 % duty on a ~12–24 V bus) is comfortably above ADC/offset noise while remaining far below the current that a healthy low-resistance winding would draw, so a genuinely open winding is reliably distinguished from a connected one.

Imbalance detection

Once all three phases have been measured (entering RS_SUB_DONE), the firmware scans the three results and keeps only the valid ones — i.e. those that are not flagged open and whose resistance is greater than 0.001 Ω (a sanity floor that also screens out residual zero values from phases that failed to produce a usable measurement):

float r_min = 1.0e9f, r_max = 0.0f;
bool  any   = false;
/* for each non-open phase with r > 0.001 Ω: track r_min / r_max, any = true */

hc->rs.imbalance = any && (r_min > 0.001f) &&
                   ((r_max - r_min) / r_min > RS_IMBALANCE_THRESH);

In words: imbalance is flagged when the spread between the largest and smallest valid phase resistance exceeds 20 % of the smallest valid resistance. Phases that are open are excluded from this comparison (an open winding is already reported via its own OPEN_x flag and would trivially dominate any spread calculation).

Overall verdict and reporting

When RS_SUB_DONE runs, the PWM is switched off (PWMC_SwitchOffPWM) and the firmware emits, over the UART log (SimpleUART_Log):

  1. A pass/fail line:

    • [RS] All phases OK  PASS — when no phase is open and no imbalance was detected.

    • [RS] FAIL see RS: line for details — otherwise.

  2. A machine-parsable result line, with resistances expressed in integer milliohms (to avoid the %f limitation of the newlib-nano printf used on this target):

    RS:U:<mO> V:<mO> W:<mO> mOhm[ OPEN_U][ OPEN_V][ OPEN_W][ IMBALANCE]
    

    The desktop GUI parses this exact RS: line to populate its report (see tools/uart_gui).

State transition

Regardless of the pass/fail verdict, RS_SUB_DONE unconditionally transitions the top-level state machine to HC_STATE_LDQ_TEST. The RS test does not raise a critical fault (hc->fault_active) on its own — open-circuit and imbalance conditions are surfaced only through the logged flags and the RS: report line, to be consolidated later in REPORT_GENERATION (DESC_085, currently a placeholder).

Relationship to the Ls (LDQ) test

The per-phase resistances computed here (hc->rs.r_u_ohm / r_v_ohm / r_w_ohm) are passed directly into ls_measure_phase() as the r_eff_ohm argument when LDQ_TEST runs immediately afterwards:

/* 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. */
hc->ls.l_u_H = ls_measure_phase(0U, hc->rs.r_u_ohm);

This works because rs_apply_injection() excites exactly the same circuit topology in both tests (driven phase in series with the parallel combination of the other two), so the loop resistance measured by the RS test is the correct R to use in the Ls test’s τ = L / R relationship. A phase whose RS measurement failed (open circuit, r_eff_ohm < 0.001 Ω) causes ls_measure_phase() to return 0 immediately — the Ls measurement for that phase is skipped in all but name. See Stator Inductance (Ls) Calculation for the full Ls procedure.

Source reference

  • Inc/health_check_sm.hHC_RsTest_t result structure, HC_STATE_RS_TEST enum value, g_rs_duty_pct declaration.

  • Src/health_check_sm.c:

    • RS configuration constants (RS_TEST_DUTY_PCTRS_IMBALANCE_THRESH)

    • RS sub-state IDs (RS_SUB_BASELINERS_SUB_DONE)

    • rs_apply_injection() — PWM injection helper (also reused by the Ls test)

    • hc_state_rs_test() — the RS_TEST state handler / sub-state machine