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:
|
Phase |
Timer channel driven |
|---|---|---|
0 |
U |
TIM1 CH1 ( |
1 |
V |
TIM1 CH2 ( |
2 |
W |
TIM1 CH3 ( |
This produces an average injected voltage
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:
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 globalg_rs_duty_pct.Range: 1–30 %, configurable at runtime via the UART command
RS:DUTY:<pct>before the health check is started (seeInc/health_check_sm.hcomment ong_rs_duty_pct).Converted to timer compare ticks as
ticks = duty% * PWM_PERIOD_CYCLES / 100and 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 |
|
Calls |
1 |
|
Logs the injected duty, calls |
2 |
|
Waits until |
3 |
|
Each scheduler tick: reads |
4–6 |
|
Identical sequence, driving phase V ( |
7–9 |
|
Identical sequence, driving phase W ( |
10 |
|
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 |
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 |
|---|---|---|
|
5 % |
Default DC-injection duty cycle |
|
80 ms |
Dwell time after switching the injection path,
before sampling begins — allows the winding’s
|
|
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 |
|
0.03 A |
Minimum average current to consider the winding electrically connected (30 mA) |
|
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 to0.0f, and[RS] <phase>: OPEN CIRCUITis 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):
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.
A machine-parsable result line, with resistances expressed in integer milliohms (to avoid the
%flimitation of the newlib-nanoprintfused 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 (seetools/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.h—HC_RsTest_tresult structure,HC_STATE_RS_TESTenum value,g_rs_duty_pctdeclaration.Src/health_check_sm.c:RS configuration constants (
RS_TEST_DUTY_PCT…RS_IMBALANCE_THRESH)RS sub-state IDs (
RS_SUB_BASELINE…RS_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