Stator Resistance (Rs) Calculation¶
Purpose¶
This document describes, in implementation detail, how the Motor Health
Check firmware state machine (DESC_081, Software Architecture)
measures the apparent line-to-line 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,
rs_ramp_measure_step 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 line-to-line resistance of
each injection path (Ruv, Rvw, Rwu), (b) detect open windings,
(c) detect resistance imbalance across phases, (d) optionally flag deviation
from a user-supplied nominal resistance, and (e) hand a known total-loop
resistance to the subsequent LDQ_TEST (Ls) measurement, which reuses it
directly (see Stator Inductance (Ls) Calculation).
Important
The values stored in r_u_ohm / r_v_ohm / r_w_ohm are not
per-phase winding resistances. With a star-connected motor, driving one
phase’s high side while the other two phases’ low sides conduct in
parallel makes the measured loop a line-to-line resistance — the GUI
labels these results Ruv, Rvw and Rwu accordingly (driven
phase U → Ruv, V → Rvw, W → Rwu).
Measurement principle — ramped DC current injection¶
The motor is stationary (no PWM active) when the test begins. For each phase
in turn, the firmware applies a 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(), which now takes the duty cycle as a parameter so
the calling state machine can ramp it step by step:
static void rs_apply_injection(uint8_t drive_phase, uint8_t duty_pct)
{
PWMC_SwitchOffPWM(pwmcHandle[M1]);
PWMC_TurnOnLowSides(pwmcHandle[M1], 0u); /* all CCRs=0, MOE enabled */
uint32_t ticks = (uint32_t)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), which is what makes the result a
line-to-line value rather than a per-phase one.
Ramping the duty up to the current limit¶
Rather than injecting at a single fixed duty, the firmware ramps the duty
cycle up step by step until the measured current reaches a configurable
target, g_rs_ilimit_a — this lets the test push up to roughly 1.5 A of
injection current (instead of the few hundred mA a fixed low duty would
produce) while still starting gently. The ramp and the final-reading
calculation are both implemented in a shared helper, rs_ramp_measure_step():
static bool rs_ramp_measure_step(HC_Handle_t *hc, uint8_t drive_phase,
uint8_t settle_sub, uint32_t now,
float i_now, float *r_ohm)
{
hc->rs.acc += (i_now < 0.0f ? -i_now : i_now);
hc->rs.samples++;
if ((now - hc->rs.tick) < RS_MEASURE_MS) { return false; }
float I_avg = hc->rs.acc / (float)hc->rs.samples;
if ((I_avg >= g_rs_ilimit_a) || (hc->rs.duty_pct >= g_rs_duty_max_pct))
{
/* Current reached the limit, or the duty is maxed out and the
* current still never got there — either way this is the final
* reading. A near-zero current naturally produces a very large
* apparent resistance, which the open-circuit threshold catches. */
float vbus = (float)VBS_GetAvBusVoltage_V(&BusVoltageSensor_M1._Super);
float i_safe = (I_avg > 0.001f) ? I_avg : 0.001f;
float r_apparent = vbus * (float)hc->rs.duty_pct / (100.0f * i_safe);
/* Remove the fixed high-side + low-side MOSFET Rds(on) contribution
* baked into every V/I reading along the injection path. */
float r_corrected = r_apparent - RS_SWITCH_RESISTANCE_OHM;
*r_ohm = (r_corrected > 0.0f) ? r_corrected : 0.0f;
return true;
}
/* Headroom remains: step the duty up and settle again before re-measuring */
uint32_t next_duty = (uint32_t)hc->rs.duty_pct + RS_DUTY_RAMP_STEP_PCT;
hc->rs.duty_pct = (uint8_t)((next_duty > g_rs_duty_max_pct) ? g_rs_duty_max_pct : next_duty);
rs_apply_injection(drive_phase, hc->rs.duty_pct);
hc->rs.acc = 0.0f;
hc->rs.samples = 0U;
hc->rs.tick = now;
hc->rs.sub = settle_sub;
return false;
}
In words: every measurement window either (a) finds I_avg has reached
g_rs_ilimit_a — or the duty has already hit its configurable ceiling
g_rs_duty_max_pct — in which case it computes and returns the final
apparent resistance, or (b) finds there is still headroom, in which case it
bumps the duty up by RS_DUTY_RAMP_STEP_PCT, re-applies the injection, and
loops back to the settle sub-state to let the new operating point stabilize
before measuring again.
Because the ramp always terminates at g_rs_duty_max_pct even when the
current limit is never reached, a high-resistance or open winding still
produces a (large) apparent-resistance reading at the maximum allowed duty,
rather than leaving the test “stuck”. This is also why a separate
current-based open-circuit check is unnecessary — see
Open-circuit detection below.
Apparent-resistance formula and the switch-resistance correction¶
The raw apparent resistance is Ohm’s law on the averaged injection:
However, the injection path always includes one high-side and one low-side
MOSFET (each contributing their Rds(on)) in series with the winding(s)
being measured, adding a fixed offset to every apparent V/I reading.
This is corrected by subtracting a fixed define, RS_SWITCH_RESISTANCE_OHM
(0.75 Ω — the measured combined high-side + low-side switch contribution),
clamping the result at zero so that a winding resistance smaller than the
switch contribution cannot read negative:
Applied duty cycle¶
Ramp start:
RS_TEST_DUTY_PCT = 5U(5 %) — the duty each phase’s ramp begins from.Ramp step:
RS_DUTY_RAMP_STEP_PCT = 1U(1 percentage point per step).Ramp ceiling: configurable via
g_rs_duty_max_pct(UART commandRS:MAXDUTY:<pct>), range 1–80 %, defaultRS_DUTY_MAX_PCT_DEFAULT = 80U. The ramp never exceeds this value, whether or not the current limit was reached.Converted to timer compare ticks as
ticks = duty% * PWM_PERIOD_CYCLES / 100and written directly to the active channel’s capture/compare register.
The injected current target is configurable via g_rs_ilimit_a
(UART command RS:ILIMIT:<mA>, range 100–2000 mA i.e. 0.1–2.0 A,
default RS_ILIMIT_DEFAULT_A). The GUI uses g_rs_ilimit_a and the
last-read bus voltage to display the maximum measurable line-to-line
resistance as Rmax = Vdc / Ilimit — a winding whose true resistance
exceeds this value cannot drive enough current to reach the limit even at
maximum duty, and will be reported at (or near) the open-circuit threshold.
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 |
|
On the first repetition ( |
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/nominal-deviation
checks, 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, where “measure” itself contains an inner
ramp loop (re-settling and re-measuring at successively higher duties) and,
on top of that, an outer repetition loop that repeats the whole ramp
g_rs_repeat_count times before the phase’s result is finalized as an
average. The three phases (U → V → W) run back to back, and all three
results are evaluated together once collection is complete.
Repeated measurement and averaging¶
To improve repeatability, each phase’s full ramp-and-measure cycle can be
repeated multiple times and the resulting resistance values averaged. This
is controlled by the global g_rs_repeat_count
(UART command RS:REPEAT:<count>, range 1–25, default
RS_REPEAT_DEFAULT = 1U):
HC_RsTest_tcarries two extra fields for this:rep_idx(the count of repetitions completed so far for the phase in progress) andrep_acc(the running sum of corrected resistance readings for that phase).On entry to a phase’s
SETUPsub-state,rep_accis cleared only on the first repetition (rep_idx == 0); the “Injecting <phase>” log line is likewise only emitted once per phase, so repeated ramps don’t spam the log.Each completed ramp logs an individual reading, e.g.
[RS] U rep 3/10: 480mOhm I:1500mA duty:62%.Once
rep_idxreachesg_rs_repeat_count, the averagerep_acc / rep_idxbecomes the phase’s reported resistance (r_u_ohm/r_v_ohm/r_w_ohm),rep_idxis reset to 0 for the next phase, and a summary line is logged, e.g.[RS] U: avg 478mOhm over 10 reps(or... OPENappended if the averaged value exceeds the open-circuit threshold).
With the default g_rs_repeat_count = 1, the behavior is identical to a
single ramp-and-measure pass per phase.
Timing¶
All RS-test timing constants are defined at the top of
Src/health_check_sm.c:
Because the per-phase cycle now ramps the duty up step by step (each step
re-running settle + measure) and may repeat several times, the overall
RS_TEST duration is variable — it depends on how many ramp steps are
needed to reach g_rs_ilimit_a (or the duty ceiling) and on
g_rs_repeat_count. A rough estimate for one phase, one repetition, with
N ramp steps is N × (g_rs_settle_ms + RS_MEASURE_MS); multiply by
g_rs_repeat_count for the full per-phase time, and by three (plus the
~16 ms baseline calibration) for the whole RS_TEST state.
Open-circuit detection¶
Unlike earlier revisions, open-circuit detection is not based on a
separate current threshold. Because the duty ramp always terminates — either
when the current reaches g_rs_ilimit_a or when g_rs_duty_max_pct is
hit — a winding that cannot draw enough current to reach the limit simply
produces a very large apparent resistance at the final (maximum) duty. This
is naturally caught by comparing the (averaged) result against a single
configurable resistance threshold, g_rs_open_thresh_ohm
(UART command RS:OPENTHR:<mOhm>, default RS_OPEN_THRESH_OHM_DEFAULT
= 50 Ω):
hc->rs.open_u = (hc->rs.r_u_ohm > g_rs_open_thresh_ohm);
If the averaged resistance exceeds
g_rs_open_thresh_ohm: the phase is flagged open (hc->rs.open_u/open_v/open_w = true) and the per-phase summary line carries anOPENsuffix, e.g.[RS] U: avg 9999mOhm over 1 rep OPEN.Otherwise the phase is marked closed (
open_x = false).
An open phase is excluded from the resistance value reported in the imbalance comparison and the nominal-deviation check (see below), since an open winding would trivially dominate either spread calculation.
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 > g_rs_imbalance_thresh);
In words: imbalance is flagged when the spread between the largest and smallest valid phase resistance exceeds the configurable tolerance ``g_rs_imbalance_thresh`` (UART command ``RS:IMBTOL:<pct>``, default ``RS_IMBALANCE_THRESH_DEFAULT`` = 20 %), expressed as a fraction of the smallest valid resistance. Phases that are open are excluded from this comparison.
Nominal-resistance deviation check¶
The user can optionally supply a nominal line-to-line resistance,
g_rs_nominal_ohm (UART command RS:NOMINAL:<mOhm>, default
RS_NOMINAL_OHM_DEFAULT = 0 — disabled), and an allowed deviation ratio,
g_rs_deviation_tol (UART command RS:DEVTOL:<pct>, default
RS_DEVIATION_TOL_DEFAULT = 20 %). When the nominal is non-zero, every
non-open phase’s averaged resistance is compared against it:
if (g_rs_nominal_ohm > 0.001f)
{
/* for each non-open phase: */
float dev = (r_meas - g_rs_nominal_ohm) / g_rs_nominal_ohm;
float dev_abs = (dev < 0.0f) ? -dev : dev;
if (dev_abs > g_rs_deviation_tol)
{
/* SimpleUART_Log: "[RS] <phase>: deviates from nominal by <pct>%
(meas:<mO>mOhm nominal:<mO>mOhm tol:<pct>%)" */
}
}
This check is purely informational — it only emits a log line per phase that
deviates beyond tolerance; it does not affect hc->rs.imbalance, the
open flags, or the pass/fail verdict. Leaving the nominal at 0 (its
default) disables the check entirely.
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.
(The optional nominal-deviation log lines, if any, are emitted just before this verdict and do not themselves affect it.)
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 — labelled there asRuv/Rvw/Rwu(seetools/uart_console/uart_gui.py).
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 line-to-line 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 (includingduty_pct,rep_idx,rep_acc),HC_STATE_RS_TESTenum value, and the configurable-parameter globals:g_rs_duty_pct,g_rs_duty_max_pct,g_rs_settle_ms,g_rs_ilimit_a,g_rs_open_thresh_ohm,g_rs_imbalance_thresh,g_rs_nominal_ohm,g_rs_deviation_tol,g_rs_repeat_count.Src/health_check_sm.c:RS configuration constants and defaults (
RS_TEST_DUTY_PCT…RS_REPEAT_DEFAULT/RS_REPEAT_MIN/RS_REPEAT_MAX), includingRS_SWITCH_RESISTANCE_OHM(the MOSFETRds(on)correction)RS sub-state IDs (
RS_SUB_BASELINE…RS_SUB_DONE)rs_apply_injection()— PWM injection helper (also reused by the Ls test), now parameterized by duty cyclers_ramp_measure_step()— shared ramp/measurement/correction helperhc_state_rs_test()— the RS_TEST state handler / sub-state machine
Src/simple_uart.c— UART commandsRS:DUTY,RS:ILIMIT,RS:MAXDUTY,RS:SETTLE,RS:OPENTHR,RS:IMBTOL,RS:NOMINAL,RS:DEVTOL,RS:REPEAT.tools/uart_console/uart_gui.py— “RS Test Configuration” panel (spinboxes/Set buttons for every configurable parameter above, plus the live “Max measurable Rs (phase-to-phase)” =Vdc / Ilimitreadout) and the “RS Test Results” panel (Ruv/Rvw/Rwulabels).