
Motor Control
- 76 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
motor-control is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- motor-control
- AI & Agent Building
- AI-coding skill
Motor Control by the numbers
- 76 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,442 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill motor-controlAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 76 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Motor Control
Identity
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Motor Control
Patterns
Foc Control
Name
Field Oriented Control (FOC)
Description
Vector control for brushless motors
Critical
Pattern
import numpy as np from dataclasses import dataclass
@dataclass class FOCParams: """FOC controller parameters.""" pole_pairs: int = 7 # Motor pole pairs phase_resistance: float = 0.1 # Ohms phase_inductance: float = 0.001 # Henries flux_linkage: float = 0.01 # Weber
Current loop gains
kp_d: float = 1.0 ki_d: float = 100.0 kp_q: float = 1.0 ki_q: float = 100.0
Velocity loop gains
kp_vel: float = 0.5 ki_vel: float = 10.0
class FOCController: """Field Oriented Control for BLDC/PMSM motors.
Transforms 3-phase currents to rotating d-q frame. D-axis: flux (usually set to 0 for PMSM) Q-axis: torque """
def __init__(self, params: FOCParams, dt: float): self.params = params self.dt = dt
Current PI controllers
self.id_integral = 0.0 self.iq_integral = 0.0
Velocity PI controller
self.vel_integral = 0.0
Limits
self.v_max = 12.0 # Bus voltage self.i_max = 10.0 # Max phase current
def clarke_transform(self, ia: float, ib: float, ic: float) -> tuple: """Clarke transform: 3-phase to alpha-beta (stationary frame).
Args: ia, ib, ic: Phase currents
Returns: (i_alpha, i_beta) """ i_alpha = ia i_beta = (ia + 2 * ib) / np.sqrt(3) return i_alpha, i_beta
def park_transform(self, i_alpha: float, i_beta: float, theta_e: float) -> tuple: """Park transform: alpha-beta to d-q (rotating frame).
Args: i_alpha, i_beta: Stationary frame currents theta_e: Electrical angle (radians)
Returns: (id, iq) """ cos_theta = np.cos(theta_e) sin_theta = np.sin(theta_e)
id = i_alpha cos_theta + i_beta sin_theta iq = -i_alpha sin_theta + i_beta cos_theta
return id, iq
def inverse_park(self, vd: float, vq: float, theta_e: float) -> tuple: """Inverse Park: d-q to alpha-beta.
Args: vd, vq: D-Q frame voltages theta_e: Electrical angle
Returns: (v_alpha, v_beta) """ cos_theta = np.cos(theta_e) sin_theta = np.sin(theta_e)
v_alpha = vd cos_theta - vq sin_theta v_beta = vd sin_theta + vq cos_theta
return v_alpha, v_beta
def space_vector_modulation(self, v_alpha: float, v_beta: float, v_bus: float) -> tuple: """Space Vector PWM: alpha-beta to duty cycles.
Args: v_alpha, v_beta: Alpha-beta voltages v_bus: DC bus voltage
Returns: (duty_a, duty_b, duty_c) in range [0, 1] """
Normalize
v_alpha = v_alpha / v_bus v_beta = v_beta / v_bus
Inverse Clarke
va = v_alpha vb = -0.5 v_alpha + np.sqrt(3)/2 v_beta vc = -0.5 v_alpha - np.sqrt(3)/2 v_beta
Add common-mode (center-aligned PWM)
v_min = min(va, vb, vc) v_max = max(va, vb, vc) v_offset = -(v_max + v_min) / 2
Convert to duty cycle [0, 1]
duty_a = np.clip((va + v_offset + 0.5), 0, 1) duty_b = np.clip((vb + v_offset + 0.5), 0, 1) duty_c = np.clip((vc + v_offset + 0.5), 0, 1)
return duty_a, duty_b, duty_c
def current_loop(self, id_ref: float, iq_ref: float, id_meas: float, iq_meas: float, omega_e: float) -> tuple: """D-Q current PI controllers with decoupling.
Args: id_ref, iq_ref: Reference currents id_meas, iq_meas: Measured currents omega_e: Electrical angular velocity
Returns: (vd, vq) voltage commands """ p = self.params
Current errors
ed = id_ref - id_meas eq = iq_ref - iq_meas
PI control
self.id_integral += ed self.dt self.iq_integral += eq self.dt
Anti-windup
self.id_integral = np.clip(self.id_integral, -self.v_max/p.ki_d, self.v_max/p.ki_d) self.iq_integral = np.clip(self.iq_integral, -self.v_max/p.ki_q, self.v_max/p.ki_q)
vd_pi = p.kp_d ed + p.ki_d self.id_integral vq_pi = p.kp_q eq + p.ki_q self.iq_integral
Decoupling terms
vd_decoup = -omega_e p.phase_inductance iq_meas vq_decoup = omega_e (p.phase_inductance id_meas + p.flux_linkage)
vd = vd_pi + vd_decoup vq = vq_pi + vq_decoup
return vd, vq
def velocity_loop(self, vel_ref: float, vel_meas: float) -> float: """Velocity PI controller.
Args: vel_ref: Reference velocity (rad/s mechanical) vel_meas: Measured velocity
Returns: iq_ref: Q-axis current reference (torque command) """ p = self.params
error = vel_ref - vel_meas self.vel_integral += error * self.dt
Anti-windup
self.vel_integral = np.clip(self.vel_integral, -self.i_max/p.ki_vel, self.i_max/p.ki_vel)
iq_ref = p.kp_vel error + p.ki_vel self.vel_integral return np.clip(iq_ref, -self.i_max, self.i_max)
def update(self, vel_ref: float, theta_m: float, omega_m: float, ia: float, ib: float, ic: float) -> tuple: """Full FOC update cycle.
Args: vel_ref: Velocity reference (rad/s mechanical) theta_m: Rotor mechanical angle omega_m: Rotor mechanical velocity ia, ib, ic: Phase current measurements
Returns: (duty_a, duty_b, duty_c) """ p = self.params
Electrical angle and velocity
theta_e = theta_m p.pole_pairs omega_e = omega_m p.pole_pairs
Clarke and Park transforms
i_alpha, i_beta = self.clarke_transform(ia, ib, ic) id_meas, iq_meas = self.park_transform(i_alpha, i_beta, theta_e)
Velocity loop -> torque command
iq_ref = self.velocity_loop(vel_ref, omega_m) id_ref = 0.0 # Zero d-axis current for PMSM
Current loop
vd, vq = self.current_loop(id_ref, iq_ref, id_meas, iq_meas, omega_e)
Inverse transform
v_alpha, v_beta = self.inverse_park(vd, vq, theta_e)
PWM generation
return self.space_vector_modulation(v_alpha, v_beta, self.v_max)
Why
FOC provides optimal torque per amp, smooth operation, and high efficiency
Stepper Control
Name
Stepper Motor Control
Description
Open-loop and closed-loop stepper control
Pattern
import numpy as np from dataclasses import dataclass from typing import Tuple
@dataclass class StepperParams: steps_per_rev: int = 200 # Full steps per revolution microsteps: int = 16 # Microstepping ratio max_current: float = 2.0 # Amps per phase accel_steps_per_sec2: float = 10000
class StepperController: """Stepper motor controller with acceleration profiling.
Supports:
- Microstepping (sinusoidal current)
- Trapezoidal velocity profile
- Optional closed-loop with encoder
"""
def __init__(self, params: StepperParams, dt: float): self.params = params self.dt = dt
Microstep table (sinusoidal)
self.microstep_table = self._generate_microstep_table()
Motion state
self.position_steps = 0.0 self.velocity_steps = 0.0 self.target_position = 0.0
def _generate_microstep_table(self) -> np.ndarray: """Generate sinusoidal microstep current table.""" n = self.params.microsteps 4 # Full electrical cycle angles = np.linspace(0, 2np.pi, n, endpoint=False) return np.column_stack([np.sin(angles), np.cos(angles)])
def set_target(self, position_steps: float): """Set target position in microsteps.""" self.target_position = position_steps
def update(self) -> Tuple[float, float]: """Update stepper position with acceleration limiting.
Returns: (current_a, current_b): Phase currents """ p = self.params max_accel = p.accel_steps_per_sec2 * self.dt
Position error
error = self.target_position - self.position_steps
Velocity needed to stop at target (trapezoidal)
stopping_distance = self.velocity_steps*2 / (2 p.accel_steps_per_sec2)
if abs(error) <= abs(stopping_distance):
Decelerate
if self.velocity_steps > 0: self.velocity_steps = max(0, self.velocity_steps - max_accel) else: self.velocity_steps = min(0, self.velocity_steps + max_accel) else:
Accelerate toward target
if error > 0: self.velocity_steps = min(self.velocity_steps + max_accel, p.accel_steps_per_sec2) else: self.velocity_steps = max(self.velocity_steps - max_accel, -p.accel_steps_per_sec2)
Update position
self.position_steps += self.velocity_steps * self.dt
Get phase currents from microstep table
table_index = int(self.position_steps) % len(self.microstep_table) currents = self.microstep_table[table_index] * p.max_current
return currents[0], currents[1]
def get_position_rad(self) -> float: """Get position in radians.""" steps_per_rad = (self.params.steps_per_rev self.params.microsteps) / (2 np.pi) return self.position_steps / steps_per_rad
Closed-loop stepper with encoder
class ClosedLoopStepper(StepperController): """Closed-loop stepper using encoder feedback."""
def __init__(self, params: StepperParams, dt: float, encoder_cpr: int): super().__init__(params, dt) self.encoder_cpr = encoder_cpr
Position/velocity loops
self.kp_pos = 10.0 self.kp_vel = 0.1
def update_with_encoder(self, encoder_counts: int) -> Tuple[float, float]: """Update with encoder feedback for anti-stall."""
Calculate actual position
actual_rad = 2 np.pi encoder_counts / self.encoder_cpr
Calculate expected position
expected_rad = self.get_position_rad()
Position error (stall detection)
pos_error = expected_rad - actual_rad
If large error, motor may be stalled
if abs(pos_error) > 0.5: # radians
Reduce velocity, motor is stalling
self.velocity_steps *= 0.5
Normal update
return self.update()
Why
Stepper control with acceleration profiling prevents missed steps
Encoder Interface
Name
Quadrature Encoder Interface
Description
Hardware and software encoder decoding
Pattern
import numpy as np from collections import deque
class QuadratureEncoder: """Quadrature encoder with velocity estimation.
Supports:
- 4x decoding (count on all edges)
- Velocity estimation with filtering
- Index pulse handling
"""
def __init__(self, cpr: int, dt: float): """ Args: cpr: Counts per revolution (after 4x decoding) dt: Sample period """ self.cpr = cpr self.dt = dt
State
self.count = 0 self.prev_count = 0 self.index_count = None # Count at last index pulse
Velocity estimation
self.velocity_filter = deque(maxlen=8)
Previous A/B state for decoding
self.prev_a = 0 self.prev_b = 0
Lookup table for quadrature decoding
[prev_a, prev_b, curr_a, curr_b] -> count delta
QEI_TABLE = { (0, 0, 0, 1): 1, (0, 0, 1, 0): -1, (0, 1, 0, 0): -1, (0, 1, 1, 1): 1, (1, 0, 0, 0): 1, (1, 0, 1, 1): -1, (1, 1, 0, 1): -1, (1, 1, 1, 0): 1, }
def update_hw(self, hw_count: int, index_pulse: bool = False): """Update from hardware counter.
Args: hw_count: Current hardware counter value index_pulse: True if index pulse detected """ self.prev_count = self.count self.count = hw_count
if index_pulse: self.index_count = self.count
def update_sw(self, a: int, b: int): """Software quadrature decoding.
Args: a, b: Current A and B channel states (0 or 1) """ state = (self.prev_a, self.prev_b, a, b) delta = self.QEI_TABLE.get(state, 0)
self.prev_count = self.count self.count += delta self.prev_a = a self.prev_b = b
def get_position_rad(self) -> float: """Get position in radians.""" return 2 np.pi self.count / self.cpr
def get_velocity_rad_s(self) -> float: """Get filtered velocity in rad/s."""
Simple difference
delta_counts = self.count - self.prev_count velocity = 2 np.pi delta_counts / (self.cpr * self.dt)
Moving average filter
self.velocity_filter.append(velocity) return sum(self.velocity_filter) / len(self.velocity_filter)
def get_velocity_rpm(self) -> float: """Get velocity in RPM.""" return self.get_velocity_rad_s() 60 / (2 np.pi)
def reset_to_index(self): """Reset position to last index pulse location.""" if self.index_count is not None: self.count -= self.index_count self.index_count = 0
Embedded C implementation for hardware timer
ENCODER_C = ''' // STM32 Hardware Quadrature Encoder
void encoder_init(TIM_TypeDef* tim, uint32_t cpr) { // Configure timer in encoder mode tim->SMCR = TIM_SMCR_SMS_0 | TIM_SMCR_SMS_1; // Encoder mode 3 (4x) tim->CCMR1 = TIM_CCMR1_CC1S_0 | TIM_CCMR1_CC2S_0; // IC1 on TI1, IC2 on TI2 tim->CCER = 0; // Rising edge, no inversion tim->ARR = cpr - 1; // Auto-reload (wrap at CPR) tim->CNT = 0; tim->CR1 = TIM_CR1_CEN; // Enable }
int32_t encoder_read(TIM_TypeDef* tim) { return (int32_t)tim->CNT; }
float encoder_velocity(TIM_TypeDef* tim, float dt, uint32_t cpr) { static int32_t prev_count = 0; int32_t count = (int32_t)tim->CNT;
// Handle wraparound int32_t delta = count - prev_count; if (delta > (int32_t)(cpr/2)) delta -= cpr; if (delta < -(int32_t)(cpr/2)) delta += cpr;
prev_count = count; return (2.0f 3.14159f delta) / (cpr * dt); } '''
Why
Proper encoder handling is essential for accurate position/velocity feedback
Current Sensing
Name
Current Sensing
Description
Phase current measurement and reconstruction
Pattern
import numpy as np
class CurrentSensor: """Three-phase current sensing with reconstruction.
Supports:
- 3-shunt sensing (all phases)
- 2-shunt sensing (reconstruct third)
- Single-shunt sensing (sample during zero vectors)
"""
def __init__(self, shunt_resistance: float = 0.01, gain: float = 20.0, vref: float = 3.3, adc_bits: int = 12): """ Args: shunt_resistance: Shunt resistor value (Ohms) gain: Current sense amplifier gain vref: ADC reference voltage adc_bits: ADC resolution """ self.shunt_r = shunt_resistance self.gain = gain self.vref = vref self.adc_max = (1 << adc_bits) - 1
Offset calibration (measure at zero current)
self.offset_a = self.adc_max / 2 self.offset_b = self.adc_max / 2 self.offset_c = self.adc_max / 2
def calibrate_offsets(self, adc_a: int, adc_b: int, adc_c: int): """Calibrate zero-current offsets.
Call with motor disconnected or at zero current. """ self.offset_a = adc_a self.offset_b = adc_b self.offset_c = adc_c
def adc_to_current(self, adc_value: int, offset: float) -> float: """Convert ADC reading to current.""" voltage = (adc_value - offset) self.vref / self.adc_max current = voltage / (self.shunt_r self.gain) return current
def read_3_shunt(self, adc_a: int, adc_b: int, adc_c: int) -> tuple: """Read currents from 3-shunt configuration.""" ia = self.adc_to_current(adc_a, self.offset_a) ib = self.adc_to_current(adc_b, self.offset_b) ic = self.adc_to_current(adc_c, self.offset_c) return ia, ib, ic
def read_2_shunt(self, adc_a: int, adc_b: int) -> tuple: """Read currents from 2-shunt, reconstruct third.
Uses Kirchhoff's current law: Ia + Ib + Ic = 0 """ ia = self.adc_to_current(adc_a, self.offset_a) ib = self.adc_to_current(adc_b, self.offset_b) ic = -(ia + ib) # Reconstruct from KCL return ia, ib, ic
def read_1_shunt(self, adc_samples: list, sectors: list) -> tuple: """Read currents from single DC link shunt.
Requires sampling during specific PWM vectors. More complex timing, but saves cost. """
Implementation depends on PWM timing
Sample during two active vectors per period
currents = [0.0, 0.0, 0.0]
for adc_val, sector in zip(adc_samples, sectors): current = self.adc_to_current(adc_val, self.offset_a) if sector == 1: currents[0] = current currents[1] = -current elif sector == 2: currents[1] = current currents[2] = -current
... other sectors
return tuple(currents)
Why
Accurate current sensing is critical for torque control
Anti-Patterns
Open Loop Bldc
Name
Open-Loop BLDC at High Speed
Problem
Open-loop commutation loses sync at high speed/load
Solution
Use sensorless FOC with back-EMF observer or hall sensors
Slow Current Loop
Name
Slow Current Control Loop
Problem
Current loop slower than electrical dynamics causes instability
Solution
Run current loop at 10-20kHz, faster than electrical time constant
No Current Limit
Name
No Current Protection
Problem
Overcurrent damages motor, driver, or battery
Solution
Implement hardware and software current limiting
Ignoring Dead Time
Name
Ignoring Dead Time Effects
Problem
No dead time causes shoot-through, burns driver
Solution
Add dead time (100ns-1us), compensate in control
Motor Control - Sharp Edges
Shoot-Through Destroys Power Stage
Id
shoot-through
Severity
critical
Summary
Both high and low side MOSFETs on = short circuit through driver
Symptoms
- Driver MOSFETs burn immediately
- Large current spike on scope
- Magic smoke
Why
In a half-bridge, if both high-side and low-side transistors conduct simultaneously, you get a direct short from power to ground.
This happens when:
- Missing dead time between switching
- PWM glitches during transitions
- Software bug setting both outputs high
Result: Large current spike, destroyed MOSFETs in microseconds.
Gotcha
// WRONG: No dead time set_pwm_high(1); set_pwm_low(0); // Low might still be on briefly!
// PWM peripheral without dead time TIM1->CCR1 = duty; // No dead time configured
Solution
// Hardware dead time (preferred) // STM32 advanced timer dead time TIM1->BDTR = TIM_BDTR_MOE | (50 << 0); // 50 * Tdts dead time
// Or in HAL TIM_BDTRInitTypeDef bdtr = { .DeadTime = 100, // Dead time value .AutomaticOutput = TIM_AUTOMATICOUTPUT_ENABLE }; HAL_TIMEx_ConfigBreakDeadTime(&htim1, &bdtr);
// Software dead time (if no hardware support) void set_pwm_safe(int phase, float duty) { // Turn off first set_high_side(phase, 0); set_low_side(phase, 0); delay_ns(200); // Dead time // Then turn on desired side if (duty > 0) set_high_side(phase, duty); else set_low_side(phase, -duty); }
ADC Sampling Not Synchronized to PWM
Id
adc-pwm-sync
Severity
critical
Summary
Sampling current during switching gives wrong values
Symptoms
- Current readings are noisy
- Control loop oscillates
- Readings vary with duty cycle
Why
During PWM switching, current is changing rapidly (di/dt from inductance). Also, switching noise couples into measurements.
You must sample at a specific point in PWM cycle:
- Center of ON-time for low-side sensing
- Center of OFF-time for DC-link sensing
Random sampling gives unusable current values.
Gotcha
// WRONG: Sample whenever while (1) { current = read_adc(); // Could be during switching! foc_update(current); delay_ms(1); }
Solution
// CORRECT: Trigger ADC from PWM timer
// STM32: Trigger ADC from timer TRGO htim1.Init.TRGOSource = TIM_TRGO_UPDATE; // Trigger at counter update
// ADC: External trigger from timer hadc.Init.ExternalTrigConv = ADC_EXTERNALTRIG_T1_TRGO; hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
// Or trigger from CCR4 for precise timing TIM1->CCR4 = TIM1->ARR / 2; // Trigger at PWM center TIM1->CCMR2 |= TIM_CCMR2_OC4M_1; // PWM mode for trigger
// Use DMA for ADC to minimize jitter HAL_ADC_Start_DMA(&hadc, adc_buffer, 3);
// In timer update ISR (or DMA complete) void TIM1_UP_IRQHandler(void) { // ADC values are ready, synchronized to PWM ia = adc_buffer[0]; ib = adc_buffer[1]; foc_update(ia, ib); }
Encoder Noise Causes Position Glitches
Id
encoder-noise
Severity
high
Summary
Electrical noise causes false counts, wrong velocity
Symptoms
- Position jumps randomly
- Velocity spikes to impossible values
- More issues when PWM is active
Why
Encoder signals are vulnerable to noise from:
- Motor PWM switching
- Long cable runs
- Ground loops
- EMI from power stage
A single noise pulse can add or remove a count. At high speeds, can cause motor control instability.
Gotcha
// Just reading encoder without filtering position = TIM2->CNT; velocity = (position - prev_position) / dt; // Wild spikes!
Solution
// 1. Hardware: Use differential encoder signals (RS-422) // Add 100 ohm termination resistors
// 2. Hardware: Add filtering capacitors on encoder inputs // 1nF-10nF between A/B/Z and ground
// 3. Use hardware digital filter (STM32 timers have this) TIM2->CCMR1 |= (0x0F << 4); // Input filter, max filtering
// 4. Software velocity filtering float encoder_velocity_filtered(void) { static float velocity_buffer[8]; static int idx = 0;
int32_t count = TIM2->CNT; static int32_t prev_count = 0;
// Handle wraparound int32_t delta = count - prev_count; if (delta > (CPR/2)) delta -= CPR; if (delta < -(CPR/2)) delta += CPR; prev_count = count;
// Moving average filter velocity_buffer[idx++] = delta / dt; idx &= 7;
float sum = 0; for (int i = 0; i < 8; i++) sum += velocity_buffer[i]; return sum / 8; }
// 5. Sanity check velocity if (fabs(velocity) > MAX_PHYSICALLY_POSSIBLE) { velocity = prev_velocity; // Reject outlier }
FOC Electrical Angle Offset Not Calibrated
Id
foc-angle-offset
Severity
high
Summary
Wrong angle alignment between encoder and motor phases
Symptoms
- Motor vibrates instead of spinning
- Low torque, high current
- Works in one direction, not the other
Why
FOC requires knowing the exact electrical angle of the rotor. The encoder's zero position is arbitrary - it doesn't align with the motor's electrical zero.
If the offset is wrong by 90 degrees, you apply d-axis current instead of q-axis (no torque, lots of heat).
Gotcha
// Using encoder directly without calibration theta_e = encoder_angle * pole_pairs; // Offset is unknown! id_ref = 0; iq_ref = torque_command; // Motor barely moves, gets hot
Solution
// Angle calibration procedure float calibrate_electrical_offset(void) { // 1. Apply d-axis current only // This aligns rotor to electrical zero float vd = 1.0; // Small voltage float vq = 0.0;
// Apply for 1 second (motor will align) for (int i = 0; i < 1000; i++) { float theta = 0; // Force electrical angle to 0 apply_voltage(vd, vq, theta); delay_ms(1); }
// 2. Read encoder position // This is the offset float encoder_angle = get_encoder_angle(); float electrical_offset = -encoder_angle * pole_pairs;
// 3. Save offset save_to_flash(electrical_offset); return electrical_offset; }
// Use calibrated offset in FOC float get_electrical_angle(void) { float encoder_angle = get_encoder_angle(); return (encoder_angle * pole_pairs) + electrical_offset; }
Current Loop Too Slow for Motor Dynamics
Id
current-loop-bandwidth
Severity
high
Summary
Control loop can't track current dynamics, oscillates
Symptoms
- Motor oscillates or vibrates
- Current waveform is distorted
- Worse at higher speeds
Why
Motor electrical time constant: L/R (often 1-10ms) Current loop bandwidth should be faster than this.
Rule of thumb:
- Current loop: 1-2 kHz bandwidth
- Sample rate: 10-20 kHz (10x bandwidth)
If loop is too slow, it can't control the current before the motor's electrical dynamics change.
Gotcha
// WRONG: 1kHz current loop for a motor with 0.5ms time constant void timer_1khz_isr(void) { read_current(); foc_update(); // Too slow! Motor is faster }
Solution
// Motor electrical time constant tau_e = L / R; // e.g., 1mH / 1ohm = 1ms
// Current loop bandwidth: 2-5x faster than 1/tau_e bw_current = 5 / tau_e; // e.g., 5000 rad/s = 800 Hz
// Sample rate: 10x bandwidth f_sample = 10 bw_current / (2pi); // e.g., 8000 Hz
// Configure timer for 10kHz+ PWM and current loop TIM1->PSC = 0; TIM1->ARR = SystemCoreClock / 20000 - 1; // 20kHz center-aligned = 10kHz effective
// Current loop gains (based on plant model) // Plant: G(s) = 1/(Ls + R) // PI controller: C(s) = kp + ki/s // Target closed-loop bandwidth: 1kHz kp = L 2 pi 1000; // L target bandwidth ki = R 2 pi 1000; // R target bandwidth
Regenerative Braking Overvoltage
Id
regenerative-voltage
Severity
high
Summary
Motor acting as generator raises bus voltage beyond safe level
Symptoms
- Bus capacitors fail
- Driver overvoltage protection trips
- Controller resets during braking
Why
When motor decelerates (especially with load), it generates power. This energy flows back to the DC bus, raising voltage.
If bus can't absorb energy (no battery, small capacitors), voltage spikes above driver ratings.
Common in: robots stopping quickly, CNC decel, vehicle braking.
Solution
// 1. Monitor bus voltage float v_bus = read_bus_voltage(); if (v_bus > V_BUS_MAX 0.9) { // Reduce braking torque iq_ref = 0.5; }
// 2. Add brake resistor circuit // When v_bus > threshold, dump energy to resistor void brake_resistor_control(float v_bus) { if (v_bus > V_BRAKE_THRESHOLD) { set_brake_resistor_pwm((v_bus - V_BRAKE_THRESHOLD) * 0.1); } else { set_brake_resistor_pwm(0); } }
// 3. Size bus capacitance appropriately // E = 0.5 C V^2 // For 1J of braking energy with 10V rise from 24V to 34V: // C = 2 * E / (V2^2 - V1^2) = 2 / (34^2 - 24^2) = 3.4mF
// 4. Use regenerative-capable power supply or battery
Motor Control - Validations
PWM Configuration Without Dead Time
Id
no-dead-time
Severity
error
Type
regex
Pattern
- TIM\d+->CCR\d+.=(?!.BDTR|dead)
- set_pwm.high.low(?!.*dead|delay)
Message
PWM configuration should include dead time to prevent shoot-through.
Fix Action
Configure BDTR register or add software dead time
Applies To
- */.c
- */.cpp
ADC Current Sampling Not Synchronized to PWM
Id
unsync-adc-sampling
Severity
warning
Type
regex
Pattern
- read_adc.current(?!.timer|trigger|sync)
- adc_read.phase(?!.pwm|ccr)
Message
Current sensing should be triggered from PWM timer for accurate readings.
Fix Action
Configure ADC external trigger from timer TRGO or CCRx
Applies To
- */.c
- */.cpp
Motor Control Without Current Limiting
Id
no-current-limit
Severity
warning
Type
regex
Pattern
- class.FOC.:(?![\s\S]{0,500}(i_max|current_limit|overcurrent))
- def foc_update(?![\s\S]{0,300}(max|limit|clip))
Message
Motor control should include current limiting to protect motor and driver.
Fix Action
Add current limits: iq_ref = np.clip(iq_ref, -I_MAX, I_MAX)
Applies To
- */.py
- */.c
Hardcoded Motor Pole Pairs
Id
hardcoded-pole-pairs
Severity
info
Type
regex
Pattern
- pole_pairs\s=\s\d+\s*(?!#|//)
- \\s7\s;.electrical|electrical.\\s*7
Message
Motor pole pairs should be configurable, not hardcoded.
Fix Action
Load from configuration or motor parameter struct
Applies To
- */.py
- */.c
Encoder Reading Without Noise Filtering
Id
missing-encoder-filter
Severity
info
Type
regex
Pattern
- velocity\s=.count.-.prev.*(?!filter|average|median)
- encoder_velocity(?!.*filter)
Message
Encoder velocity should be filtered to reject noise spikes.
Fix Action
Add moving average or median filter on velocity
Applies To
- */.py
- */.c
Blocking Operations in Motor Control ISR
Id
blocking-motor-isr
Severity
error
Type
regex
Pattern
- void.IRQ.motor.\{[^}](printf|delay|wait)
- motor.callback.\{[^}]*(print|sleep)
Message
Motor control ISR should be fast with no blocking operations.
Fix Action
Move logging/delays outside ISR, minimize ISR work
Applies To
- */.c
- */.cpp
Current Sensor Without Offset Calibration
Id
no-offset-calibration
Severity
warning
Type
regex
Pattern
- adc.current.(?!offset|calibrat)
- read_current(?![\s\S]{0,200}offset)
Message
Current sensors need offset calibration at zero current.
Fix Action
Calibrate offsets at startup with motor disconnected or no current
Applies To
- */.py
- */.c
FOC Without Electrical Angle Calibration
Id
no-angle-offset
Severity
warning
Type
regex
Pattern
- theta_e\s=.encoder.\.pole_pairs(?!.offset)
- electrical_angle.=.position.\(?!.*calibrat)
Message
FOC requires calibrating encoder to motor electrical zero.
Fix Action
Implement angle calibration routine: apply d-axis current, read encoder
Applies To
- */.py
- */.c
Motor Control Without Regeneration Protection
Id
missing-regeneration-protection
Severity
info
Type
regex
Pattern
- class.Motor.:(?![\s\S]{0,800}(v_bus|regen|brake_resistor))
Message
Consider regenerative braking protection for fast deceleration.
Fix Action
Monitor bus voltage and reduce braking torque or engage brake resistor
Applies To
- */.py
- */.c
FOC Loop at Low Frequency
Id
slow-foc-loop
Severity
warning
Type
regex
Pattern
- foc_update.delay.([5-9]\d|[1-9]\d{2,})
- motor.loop.sleep.*0\.0[1-9]
Message
FOC current loop should run at 10kHz+ for good performance.
Fix Action
Use timer interrupt at 10-20kHz for FOC loop
Applies To
- */.py
- */.c
Inverse Park Applied Before Forward Park
Id
inverse-park-before-park
Severity
error
Type
regex
Pattern
- inverse_park.\n.park_transform|v_alpha.*i_alpha
Message
FOC sequence: Clarke -> Park -> Control -> Inverse Park -> SVPWM
Fix Action
Apply transforms in correct order
Applies To
- */.py