Stator Resistance (Rs) Calculation ================================== Purpose ------- This document describes, in implementation detail, how the Motor Health Check firmware state machine (DESC_081, :doc:`software_architecture`) measures per-phase stator winding resistance during the ``RS_TEST`` state (``HC_STATE_RS_TEST``). The implementation lives in :doc:`../../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: .. code-block:: text 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 :doc:`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()``: .. code-block:: c 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 .. math:: 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: .. math:: 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: .. code-block:: c 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 :doc:`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:`` 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: .. list-table:: :header-rows: 1 :widths: 8 22 70 * - 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: .. code-block:: text 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] : 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] : mOhm I: 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): .. code-block:: c 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): .. code-block:: text RS:U: V: W: 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: .. code-block:: c /* 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 :doc:`ls_calculation` for the full Ls procedure. Source reference ---------------- * ``Inc/health_check_sm.h`` — ``HC_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_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