Skip to content

Title: p_pid_ang_div = 0 in FOC config causes permanent ISR hang (division by zero → infinite loop in utils_norm_angl #930

Open

Description

@Kenaiwolf

Summary

Setting mc_configuration.p_pid_ang_div to 0 (either via a custom firmware default in motor/mcconf_default.h, or live through VESC Tool while the motor is configured as MOTOR_TYPE_FOC) causes a division-by-zero that produces +Inf, which is then fed into utils_norm_angle(). Because that function's loop condition can never become false for a non-finite value, execution hangs permanently inside the FOC ADC interrupt handler. Since this happens inside an ISR that runs at the current-sample rate, the whole MCU effectively freezes — no fault is raised, no reset occurs, and the device stops enumerating over USB/CAN entirely.

Where it happens

mcpwm_foc_adc_int_handler() divides by the field unconditionally when its value falls outside the 0.98–1.02 "treat as 1" pass-through band: mcpwm_foc.c:3881-3884

if (conf_now->p_pid_ang_div > 0.98 && conf_now->p_pid_ang_div < 1.02) {
motor_now->m_pos_pid_now = angle_now;
} else {
const float ang_div_inv = 1.0 / conf_now->p_pid_ang_div;

Full context showing the result feeding into utils_norm_angle(): mcpwm_foc.c:3877-3898

A second, independent division by the same field exists in foc_math.c: foc_math.c:421-422

if (conf_now->p_pid_gain_dec_angle > 0.1) {
float min_error = conf_now->p_pid_gain_dec_angle / conf_now->p_pid_ang_div;

The infinite loop itself — utils_norm_angle() never terminates for +Inf/NaN input: utils_math.h:151-160

static inline void utils_norm_angle(float *angle) {
while (*angle < 0.0) { *angle += 360.0; }
while (*angle > 360.0) { *angle -= 360.0; }
}

No validation exists anywhere in the incoming-config path. The wire deserializer copies the raw float with no bounds check: confgenerator.c:509

And commands_apply_mcconf_hw_limits(), which is the function responsible for sanitizing/clamping risky config fields before they're applied (it already clamps ~a dozen adjacent fields such as l_current_max_scale, foc_overmod_factor, foc_f_zv, l_max_vin, l_max_erpm, etc.), has no such guard for p_pid_ang_div: commands.c:1889-1978

Reproduction

Build/flash firmware with motor_type = MOTOR_TYPE_FOC and p_pid_ang_div = 0.0 (e.g., via a modified motor/mcconf_default.h).
Power on the board. The FOC ADC interrupt handler hits the division-by-zero on the very first current sample and hangs forever — the board never finishes boot, never enumerates over USB.
Alternatively, with a normally-booted board already running MOTOR_TYPE_FOC, send a config write from VESC Tool with p_pid_ang_div set to 0. commands_apply_mcconf_hw_limits() does not reject it, and mc_interface_set_configuration() applies it live — the very next ADC interrupt hangs the device identically, with no reflash needed to trigger it.

Confirmed via GDB/OpenOCD over SWD: the target was found halted permanently inside the hang, with backtrace:

#0 utils_norm_angle (angle=0x200180c4 <m_motor_1+892>) at util/utils_math.h:158
#1 mcpwm_foc_adc_int_handler (...) at motor/mcpwm_foc.c:3891
#2 Vector130 () at .../stm32_dma.c:351

Expected behavior

p_pid_ang_div should never be allowed to reach 0 (or any value close enough to 0 to overflow a float reciprocal) once it reaches these division sites — regardless of whether it originates from a compiled-in default, an EEPROM-stored config, or a live VESC Tool write.

Suggested fix

Add a guard for p_pid_ang_div in commands_apply_mcconf_hw_limits(), consistent with how neighboring fields are already clamped there, e.g. utils_truncate_number_abs(&mcconf->p_pid_ang_div, <minimum_nonzero>) or explicitly forcing it to 1.0 if within some epsilon of 0.
Defensively, utils_norm_angle() could early-return on non-finite input (!isfinite(*angle)), which would prevent this entire class of hang regardless of which caller supplies a bad value in the future.

Stock default for reference

The upstream default for this field is 1.0 (safe/inert value): mcconf_default.h:182-184

#ifndef MCCONF_P_PID_ANG_DIV
#define MCCONF_P_PID_ANG_DIV 1.0 // Divide angle by this value
#endif

The bug only surfaces when this default is overridden (in source or at runtime) with 0, but since nothing in the validation path prevents that, it should be treated as a firmware robustness bug rather than "user error."

Activity

  1. pacatomic commented on Nov 1, 2026

    @pacatomic

    Host-only reproduction and proposed scope, checked against master 3e3792b.

    I compiled a C harness that calls the actual inline utils_norm_angle() from util/utils_math.h (Zig 0.14.1's C compiler, Windows x86-64, -O2 -Wall -Wextra -Werror -std=gnu99). Each isolated input had a 1.5 s process timeout:

    Input Original helper
    370 returns 10
    NaN returns NaN
    +Inf / -Inf timeout
    +1e20f / -1e20f timeout

    Two details matter for the proposed fix:

    • A finite-value guard alone is insufficient: at sufficiently large binary32 magnitudes, adding/subtracting 360 rounds back to the same float. Smaller but large angles can also take an impractical number of iterations.
    • NaN does not enter either comparison loop under normal floating-point semantics; it propagates. Its handling still needs a control-layer policy.

    For helper-level hardening, a candidate is to preserve non-finite inputs for the caller, keep a fast single-turn adjustment for [-360,720), and use fmodf(angle,360.0f) outside that interval. After reduction, add 360 for a negative remainder, then subtract 360 if rounding produced exactly 360. A local candidate passes 21 explicit inputs, 10 nextafterf boundary neighbors and 200,000 deterministic binary32 bit patterns, checking range, modulo agreement and idempotence.

    This is only a termination fix, not a complete motor-safety fix: returning Inf/NaN unchanged must not authorize control output. For configuration validation, commands_apply_mcconf_hw_limits() alone does not cover the boot path: mc_interface_init() reads defaults/persistent configuration directly, while live changes go through mc_interface_set_configuration(). Both division sites also need consideration. Also, utils_truncate_number_abs() caps maximum magnitude; it does not enforce a minimum distance from zero.

    Before a full fix, could we agree on the supported divisor range/sign and the behavior for invalid stored versus live configuration (reject/fault versus substitution)? I would keep that policy separate from the numerical helper hardening and measure ADC-ISR cycles with the project's ARM toolchain.

    AI assistance was used for source analysis, the candidate and host tests. No STM32 build, hardware reproduction, ISR timing measurement or motor validation is claimed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions