Motor Overdrive Compensation Research¶
Date: January 8, 2025 Project: EMDR Bilateral Stimulation Device Author: Claude Code (Sonnet 4.5) Purpose: Research motor startup compensation techniques for perceptually uniform haptic stimulation
Executive Summary¶
This document investigates overdrive compensation strategies for ERM (Eccentric Rotating Mass) motors to address the perceptual intensity mismatch during motor startup. The goal is to overdrive the motor on startup such that the user perceives consistent intensity throughout the pulse, accounting for the motor's mechanical inertia lag.
Key Findings¶
- Generalized Overdrive Formula Exists: Commercial haptic drivers (TI DRV2605L, DRV2605) implement standardized overdrive algorithms using motor datasheet parameters
- BEMF-Based Approach is Superior: Real-time back-EMF feedback provides actual motor speed and enables closed-loop compensation
- Alternative Methods Available: Sensorless observers (EKF, Luenberger) can estimate speed from electrical measurements alone
Recommendation¶
Pursue BEMF-based closed-loop overdrive when new boards arrive with OUTA connected. This provides optimal performance while leveraging existing hardware investment. Use generalized time-constant approach as interim solution.
1. Problem Statement¶
1.1 Motor Specifications (Zard Zoop ERM)¶
Manufacturer: Zard Zoop Model: DC 3V 12000RPM Flat Coin Vibration Motor (φ10×3mm) Source: Amazon product specifications (no formal datasheet available)
Published Specifications¶
| Parameter | Value | Notes |
|---|---|---|
| Rated Voltage | 3.0V | - |
| Operating Range | 2.7V - 3.3V | Compatible with LiPo nominal voltage |
| Rated Speed | 12,000 RPM | No-load condition |
| Rated Current | 90mA | At 3.0V no-load |
| Dimensions | φ10mm × 3mm | Flat coin-type form factor |
| Weight | <1g (~0.9g) | Estimated from bulk packaging |
| Body Material | Stainless steel | High-strength construction |
| Temperature Range | -20°C to 70°C | (-4°F to 158°F) |
| Humidity Tolerance | 15%RH to 90%RH | - |
Derived/Estimated Motor Constants¶
Note: These constants are estimated based on typical ERM motor characteristics and the published specifications. Actual values should be validated through measurement.
| Constant | Symbol | Estimated Value | Derivation Method |
|---|---|---|---|
| Coil Resistance | R | 33Ω | V_rated / I_rated = 3.0V / 90mA |
| Back-EMF Constant | Ke | 0.000239 V·s/rad | Ke ≈ (V_rated - I_rated×R) / ω_rated |
| Torque Constant | Kt | 0.000239 N·m/A | Kt ≈ Ke (for SI units) |
| Mechanical Time Constant | τ_mech | 30-50ms | Typical for 10mm coin ERM motors |
| Rotor Inertia | J | ~1×10⁻⁸ kg·m² | Estimated from motor size/mass |
| Coil Inductance | L | ~100µH | Typical for small DC motors |
Calculation Notes:
-
Angular velocity at rated speed:
-
Back-EMF at no-load (estimated):
Note: This unexpectedly low value suggests the published current may be peak or the motor has nonlinear characteristics. Real measurement via ADC is strongly recommended. -
Motor voltage constant (Kv):
This should be calibrated via actual BEMF measurement per Section 5.3.
Measurement Recommendations¶
Given the lack of formal datasheet, the following measurements are recommended before implementing overdrive:
- BEMF Measurement: Use existing GPIO0/1 ADC to measure back-EMF at 3.0V steady-state
- Startup Time Measurement: Oscilloscope or BEMF sampling to determine actual τ_mech
- Stall Current Measurement: Measure locked-rotor current to validate coil resistance
- Thermal Testing: Verify overdrive doesn't exceed temperature limits during 20+ minute sessions
Missing from Specification: - ❌ Torque curve (stall torque, no-load speed curve) - ❌ Electrical time constant (L/R ratio) - ❌ Eccentric mass weight/offset radius - ❌ Maximum operating voltage (datasheet-validated) - ❌ Motor efficiency characteristics
Hardware Constraints: - Supply Voltage: 3.3V nominal (LiPo battery) - Cannot Overvolt: No boost converter, limited to supply voltage - Current BEMF Connection: IN1 (H-bridge input) - INCORRECT, causes inaccurate readings - New Board BEMF Connection: OUTA (H-bridge output) - CORRECT, cleaner signal
Overdrive Strategy (PWM-Based): Since actual overvoltage is not possible with 3.3V supply, "overdrive" is achieved via PWM duty cycle manipulation: - Sustain Phase: User-desired PWM duty cycle (e.g., 70% = 2.31V average, or 30% for gentle) - Startup Overdrive: Sustain duty × overdrive ratio (e.g., 1.4× boost), capped at 100% max - Overdrive Duration: 3 × τ_mech ≈ 90-150ms (estimated) - Example: 60% sustain → 84% overdrive (1.4× boost); 30% sustain → 42% overdrive (1.4× boost) - Validation Required: Test thermal performance during 20+ minute sessions
1.2 The Motor Startup Sag Phenomenon¶
ERM motors exhibit significant mechanical inertia during startup: - Typical startup time: 50-100ms to reach rated speed - Time constant: ERMs have mechanical time constant τ_mech = (R×J)/(Ke×Kt) - 63.2% speed reached: After 1 time constant (τ_mech) - 99% speed reached: After 5 time constants (5×τ_mech)
User Perception Challenge¶
For short pulses (125ms active, 375ms coast at 0.5-2 Hz): - Motor may not reach full speed before coasting begins - User perceives weaker vibration at pulse start - Longer pulses mask this effect (motor reaches steady-state) - Shorter pulses exacerbate the problem (never reach full speed)
Design Goal¶
Apply higher voltage (overdrive) during startup such that: 1. Motor reaches perceptually "full intensity" faster 2. User doesn't perceive startup as stronger than sustained vibration 3. Overdrive duration/magnitude scales with pulse duration 4. Approach works reliably across motor manufacturing tolerances
2. Approach 1: Generalized ERM Motor Formulas¶
2.1 Industry Standard Overdrive Technique¶
Modern haptic drivers (TI DRV2605L, Renesas GreenPak) implement a standardized approach:
Algorithm: 1. Apply higher drive voltage/PWM at startup to accelerate motor quickly 2. Monitor motor acceleration (via back-EMF or time estimate) 3. When motor reaches 90% of rated amplitude, reduce to sustain level 4. Sustain at normal level for remainder of pulse
Register-Based Configuration (DRV2605L example with boost converter):
// These drivers can generate voltages above supply via boost converter
// DRV2605L example: 3V motor with 3.6V overdrive capability
// Rated Voltage register (0x16)
rated_voltage_reg = (V_rated × 255) / 5.44V
// Overdrive Clamp register (0x17)
overdrive_clamp_reg = (V_max × 255) / 5.6V
// Example: 3V rated, 3.6V max motor
rated_voltage_reg = (3.0 × 255) / 5.44 = 142
overdrive_clamp_reg = (3.6 × 255) / 5.6 = 164
Important: Our design does NOT have a boost converter, so we achieve overdrive via PWM duty cycle instead (see Section 2.3).
2.2 Mechanical Time Constant Approach¶
Calculate motor time constant from datasheet:
Where: - R = armature resistance (Ω) - from datasheet - J = rotor inertia (kg·m²) - from datasheet or estimate from mass - Ke = back-EMF constant (V/rad/s) - derivable from no-load speed - Kt = torque constant (N·m/A) - often equal to Ke for SI units
Time-based overdrive strategy:
Overdrive Duration = 3 × τ_mech // Reaches ~95% speed
Overdrive Ratio = 1.3-1.5× (30-50% boost above sustain, tuned empirically)
Overdrive PWM = sustain_pwm × overdrive_ratio (capped at 100% duty maximum)
Sustain PWM = User-desired intensity (10-100% duty cycle range)
Example calculations: - Sustain 80% → Overdrive = min(80% × 1.4, 100%) = 100% (capped) - Sustain 60% → Overdrive = 60% × 1.4 = 84% - Sustain 30% → Overdrive = 30% × 1.4 = 42%
2.3 Pulse-Duration-Dependent Overdrive (PWM-Based)¶
Observation from research: - Short pulses require higher overdrive (motor never reaches steady-state) - Long pulses require less overdrive (motor stabilizes naturally)
Our Implementation Constraint: Since we're limited to 3.3V supply (no boost converter), "overdrive" means: - Boosted PWM duty during startup (relative to desired intensity) - Sustain PWM duty during rest of pulse (user's desired intensity)
Proposed logarithmic scaling (PWM duty cycle):
// For pulse duration t_pulse and motor time constant τ_mech:
// PWM range: 0-1023 for 10-bit LEDC timer
#define PWM_MAX_DUTY 1023 // 100% duty = 3.3V average (hardware limit)
#define OVERDRIVE_RATIO_BASE 1.4f // 40% boost (tune empirically)
// sustain_duty is the user's desired intensity (from Mode 5 BLE settings, for example)
if (t_pulse < 3 × τ_mech) {
// Short pulse - high overdrive needed
overdrive_duration = 0.6 × t_pulse
overdrive_duty = min(sustain_duty × OVERDRIVE_RATIO_BASE, PWM_MAX_DUTY)
} else if (t_pulse < 10 × τ_mech) {
// Medium pulse - moderate overdrive
overdrive_duration = 3 × τ_mech
overdrive_duty = min(sustain_duty × OVERDRIVE_RATIO_BASE, PWM_MAX_DUTY)
} else {
// Long pulse - minimal overdrive (reduce boost ratio)
overdrive_duration = 2 × τ_mech
overdrive_duty = min(sustain_duty × 1.2f, PWM_MAX_DUTY) // Only 20% boost
}
Note: Overdrive duty is relative to sustain duty, ensuring perceptually uniform intensity regardless of desired vibration strength.
2.4 Limitations of Open-Loop Approach¶
❌ Motor manufacturing variation: Ke, Kt, R vary ±10-15% unit-to-unit ❌ Temperature effects: Resistance increases with heat, changing time constant ❌ Wear over time: Bearing friction increases, slowing startup ❌ Load variation: Eccentric mass position affects startup torque ❌ Battery voltage sag: Lower supply voltage extends startup time
Verdict: Generalized formulas provide 80% solution but lack runtime adaptation.
3. Approach 2: BEMF-Based Closed-Loop Compensation¶
3.1 Back-EMF Fundamentals¶
Physical Principle:
Where: - V_BEMF = back electromotive force (opposing voltage generated by motor) - RPM = revolutions per minute - Kv = motor voltage constant (fixed per motor design)
Practical Measurement:
3.2 BEMF Measurement on ESP32-C6¶
Current Hardware (INCORRECT connection): - GPIO0/GPIO1 connected to IN1 (H-bridge input) via voltage divider - Problem: Adapted from TI reference design with dedicated IC, wrong net in KiCad schematic - Impact: Inaccurate BEMF readings due to measuring control signal instead of motor output - ADC measures 0-3.3V (voltage divider to handle motor swing) - Sampling during PWM off-time intended to capture pure back-EMF
New Hardware (CORRECT connection on new boards): - GPIO0/GPIO1 connected to OUTA (H-bridge output before motor inductance) - Cleaner signal (measures actual motor terminal voltage, less PWM ripple) - Faster settling time for accurate measurement - Proper BEMF measurement during motor coast periods
Measurement Algorithm:
// During PWM off-time (motor coasting)
esp_err_t measure_bemf(uint32_t *rpm_out) {
// Wait for RC settling (5τ = 550µs, use 1ms for safety margin)
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms delay
// Read ADC
int adc_reading = adc1_get_raw(ADC_BEMF_CHANNEL);
// Convert to voltage (accounting for voltage divider)
float v_bemf = (adc_reading / 4095.0f) * 3.3f * 2.0f - 3.3f;
// Calculate RPM using motor constant
float rpm = v_bemf / MOTOR_KV_CONSTANT;
*rpm_out = (uint32_t)rpm;
return ESP_OK;
}
3.3 Closed-Loop Overdrive Algorithm¶
Real-time adaptive compensation:
typedef struct {
uint32_t target_rpm; // Desired steady-state speed
uint32_t overdrive_pwm; // Maximum PWM duty cycle
uint32_t sustain_pwm; // Steady-state PWM duty cycle
uint32_t measured_rpm; // Current speed from BEMF
} overdrive_state_t;
void adaptive_overdrive_step(overdrive_state_t *state) {
// Measure current motor speed
measure_bemf(&state->measured_rpm);
// State machine for overdrive control
float speed_ratio = (float)state->measured_rpm / state->target_rpm;
if (speed_ratio < 0.9f) {
// Still accelerating - apply overdrive
motor_set_pwm(state->overdrive_pwm);
} else {
// Reached 90% speed - switch to sustain
motor_set_pwm(state->sustain_pwm);
}
}
Advantages: ✅ Adapts to motor manufacturing variation ✅ Compensates for battery voltage changes ✅ Accounts for temperature effects ✅ Detects motor wear over device lifetime ✅ Guarantees perceptual consistency
Disadvantages: ❌ Requires hardware connection (OUTA to ADC) ❌ Adds ~1ms measurement latency per sample (0.55ms RC settling + safety margin) ❌ Interrupts PWM drive during measurement (brief coast period required)
3.4 BEMF-Derived Motor Constants¶
One-time calibration procedure:
// Run motor at known voltage, measure BEMF at steady-state
float calibrate_motor_kv(void) {
motor_set_voltage(3.0f); // Apply rated voltage
vTaskDelay(pdMS_TO_TICKS(500)); // Wait for steady-state
uint32_t rpm;
measure_bemf(&rpm);
float kv = rpm / 3.0f; // RPM per volt
ESP_LOGI(TAG, "Motor Kv calibrated: %.1f RPM/V", kv);
return kv;
}
Store calibrated Kv in NVS (non-volatile storage) for runtime use.
4. Approach 3: Alternative Sensorless Speed Estimation¶
4.1 Overview of Observer-Based Methods¶
When direct BEMF measurement is unavailable, sensorless observers estimate motor speed from electrical measurements (voltage, current) alone.
Common Techniques: 1. Extended Kalman Filter (EKF) - Probabilistic state estimation 2. Luenberger Observer - Deterministic state feedback 3. Model Reference Adaptive System (MRAS) - Adaptive control theory 4. Sliding Mode Observer (SMO) - Robust nonlinear control
4.2 Extended Kalman Filter (EKF) for ERM Motors¶
State-space motor model:
State vector: x = [ω, i]^T (angular velocity, current)
ω_dot = (Kt × i - B × ω) / J
i_dot = (V_supply - R × i - Ke × ω) / L
Where: - ω = angular velocity (rad/s) - i = armature current (A) - Kt = torque constant (N·m/A) - Ke = back-EMF constant (V·s/rad) - B = damping coefficient (N·m·s/rad) - J = rotor inertia (kg·m²) - R = armature resistance (Ω) - L = armature inductance (H)
EKF Implementation: - Prediction step: Estimate next state using motor dynamics - Update step: Correct estimate using measured current - Output: Real-time speed estimate without direct speed sensor
Embedded Implementation Challenges: ❌ Requires current sensing - need shunt resistor + op-amp or Hall sensor ❌ Matrix operations - computationally intensive for ESP32-C6 ❌ Parameter tuning - Q and R matrices require expert knowledge ❌ Model accuracy - ERM motors have nonlinear friction, hard to model
4.3 Simpler Current-Based Estimation¶
Empirical relationship:
Lookup Table Approach: 1. Characterize motor offline: Measure RPM vs. (V, I) grid 2. Store 2D lookup table in flash memory 3. At runtime: Measure V_supply and I_motor, interpolate RPM
Advantages: ✅ No complex math (just table lookup + interpolation) ✅ Accounts for nonlinear motor behavior ✅ Runs fast on embedded systems
Disadvantages: ❌ Still requires current sensing hardware ❌ Table storage overhead (~1-2KB flash) ❌ Doesn't adapt to motor wear over time
4.4 Verdict on Sensorless Methods¶
For this application: Sensorless observers are overkill.
Reasoning: 1. We already have BEMF hardware (GPIO0/1 ADC inputs) 2. OUTA connection coming on new boards provides cleaner signal 3. Current sensing adds hardware complexity we don't need 4. ERM motor dynamics are simple enough for direct BEMF measurement 5. EKF computational cost not justified when direct measurement available
Recommendation: Skip sensorless observers, use BEMF directly.
5. Implementation Recommendations¶
5.1 Near-Term: Time-Constant-Based Overdrive (Before New Boards)¶
Use time-based PWM duty modulation:
// Motor parameters (estimated from specs and measurement)
#define MOTOR_SUPPLY_VOLTAGE_MV 3300 // LiPo nominal voltage
#define MOTOR_TAU_MECH_MS 50 // Estimated mechanical time constant
#define PWM_MAX_DUTY 1023 // 10-bit LEDC: 100% duty
#define OVERDRIVE_RATIO 1.4f // 40% boost above sustain (tune empirically)
// Overdrive calculation
typedef struct {
uint32_t pulse_duration_ms;
uint32_t overdrive_duration_ms;
uint32_t overdrive_duty; // Sustain × ratio, capped at max
uint32_t sustain_duty; // User's desired intensity
} overdrive_params_t;
void calculate_overdrive_params(uint32_t pulse_ms, uint32_t sustain_duty, overdrive_params_t *out) {
out->sustain_duty = sustain_duty; // User's desired intensity (from Mode 5, etc.)
// Calculate overdrive duty as relative boost
uint32_t overdrive_candidate = (uint32_t)(sustain_duty * OVERDRIVE_RATIO);
out->overdrive_duty = (overdrive_candidate > PWM_MAX_DUTY) ? PWM_MAX_DUTY : overdrive_candidate;
// Pulse-duration-dependent overdrive timing
if (pulse_ms < (3 * MOTOR_TAU_MECH_MS)) {
// Short pulse - overdrive for 60% of total pulse
out->overdrive_duration_ms = (pulse_ms * 6) / 10;
} else if (pulse_ms < (10 * MOTOR_TAU_MECH_MS)) {
// Medium pulse - overdrive for 3 time constants
out->overdrive_duration_ms = 3 * MOTOR_TAU_MECH_MS;
} else {
// Long pulse - minimal overdrive (use lower boost ratio)
uint32_t overdrive_long = (uint32_t)(sustain_duty * 1.2f); // Only 20% boost
out->overdrive_duty = (overdrive_long > PWM_MAX_DUTY) ? PWM_MAX_DUTY : overdrive_long;
out->overdrive_duration_ms = 2 * MOTOR_TAU_MECH_MS;
}
ESP_LOGI(TAG, "Overdrive params: pulse=%ums, overdrive=%ums @ %u/1023 (%.1f%%), sustain @ %u/1023 (%.1f%%)",
pulse_ms, out->overdrive_duration_ms, out->overdrive_duty,
(out->overdrive_duty * 100.0f) / PWM_MAX_DUTY,
out->sustain_duty, (out->sustain_duty * 100.0f) / PWM_MAX_DUTY);
}
Key Changes: 1. sustain_duty is now a function parameter (user's desired intensity, not hardcoded) 2. overdrive_duty is calculated as sustain × ratio, enabling both uniformity and emphasis 3. Capped at PWM_MAX_DUTY to respect hardware limits 4. OVERDRIVE_RATIO should be user-configurable via BLE GATT characteristic (see BLE Integration below) 5. Works for any intensity: 10% sustain → 14% overdrive (1.4× ratio); 80% sustain → 100% overdrive (capped)
BLE GATT Integration (Mode 5 Extension):
// Add new GATT characteristic for overdrive ratio
#define GATT_CHAR_OVERDRIVE_RATIO_UUID 0x000C // Example UUID
// Store as uint16_t: ratio × 100 (e.g., 1.4× = 140)
static uint16_t overdrive_ratio_percent = 140; // Default 1.4× (40% boost)
// GATT write handler
static int gatt_svr_chr_write_overdrive_ratio(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg) {
uint16_t new_ratio;
ble_hs_mbuf_to_flat(ctxt->om, &new_ratio, sizeof(new_ratio), NULL);
// Validate range: 100 (1.0×) to 200 (2.0×)
if (new_ratio < 100 || new_ratio > 200) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
overdrive_ratio_percent = new_ratio;
ESP_LOGI(TAG, "Overdrive ratio updated: %.2f×", new_ratio / 100.0f);
return 0;
}
// Use in motor_task
float get_overdrive_ratio(void) {
return overdrive_ratio_percent / 100.0f; // Convert back to float
}
User Experience: - PWA/App slider: "Startup Emphasis: Smooth (1.0×) ... Moderate (1.4×) ... Pronounced (2.0×)" - Real-time adjustment: User can change during session and feel difference - Stored in NVS: Device remembers user preference between sessions
Integration with existing motor_task:
case MOTOR_STATE_FORWARD_ACTIVE: {
// Get user's desired intensity from Mode 5 settings (or current mode)
uint32_t desired_intensity_duty = get_current_mode_intensity(); // e.g., 70% = 717/1023
overdrive_params_t overdrive;
calculate_overdrive_params(active_time_ms, desired_intensity_duty, &overdrive);
// Apply overdrive phase
motor_set_duty(overdrive.overdrive_duty);
if (delay_with_mode_check(overdrive.overdrive_duration_ms)) break;
// Switch to sustain phase
motor_set_duty(overdrive.sustain_duty);
uint32_t sustain_ms = active_time_ms - overdrive.overdrive_duration_ms;
if (delay_with_mode_check(sustain_ms)) break;
// Coast phase
motor_coast();
state = MOTOR_STATE_FORWARD_COAST_REMAINING;
break;
}
Note: The get_current_mode_intensity() function would return the user's desired PWM duty cycle from:
- Mode 1-4: Hardcoded intensity (e.g., 70% duty)
- Mode 5: Custom intensity from BLE GATT characteristic (adjustable 10-100%)
Expected Results: - 15-20% faster perceived startup (subjective) - More consistent intensity across pulse durations - No hardware changes required
Limitations: - Not adaptive to motor variation or wear - May need manual tuning per motor batch
5.2 Long-Term: BEMF-Based Closed-Loop (After New Boards)¶
Hardware Prerequisites: - ✅ GPIO0/1 ADC inputs (already connected) - ⏳ OUTA connection on new PCB (pending board arrival) - ✅ Voltage divider circuit (already implemented)
Software Architecture:
// BEMF measurement with OUTA signal
#define BEMF_SETTLING_TIME_MS 1 // RC settling (5τ = 550µs, rounded up for safety)
#define BEMF_TARGET_RPM_THRESHOLD 0.9f // Switch at 90% of target
typedef enum {
OVERDRIVE_PHASE_STARTUP, // Applying 100% PWM duty (max acceleration)
OVERDRIVE_PHASE_SUSTAIN, // At lower PWM duty (normal intensity)
OVERDRIVE_PHASE_COAST // Motor coasting
} overdrive_phase_t;
typedef struct {
overdrive_phase_t phase;
uint32_t target_rpm;
uint32_t current_rpm;
uint32_t overdrive_pwm;
uint32_t sustain_pwm;
uint64_t phase_start_us;
} bemf_overdrive_state_t;
esp_err_t bemf_measure_rpm(uint32_t *rpm_out) {
// Temporarily disable PWM to measure pure BEMF
motor_coast();
vTaskDelay(pdMS_TO_TICKS(BEMF_SETTLING_TIME_MS));
// Read ADC
int adc_raw = adc1_get_raw(ADC1_CHANNEL_0);
// Convert to voltage (voltage divider: V_motor = 2 × V_adc - 3.3V)
float v_adc = (adc_raw / 4095.0f) * 3.3f;
float v_bemf = (v_adc * 2.0f) - 3.3f;
// Convert to RPM using motor Kv constant (stored in NVS from calibration)
float kv = get_motor_kv_from_nvs();
*rpm_out = (uint32_t)(fabsf(v_bemf) * kv);
return ESP_OK;
}
void bemf_overdrive_control(bemf_overdrive_state_t *state) {
// Measure current motor speed
if (bemf_measure_rpm(&state->current_rpm) != ESP_OK) {
ESP_LOGW(TAG, "BEMF measurement failed, using time-based fallback");
return; // Fall back to time-based overdrive
}
// Calculate speed ratio
float speed_ratio = (float)state->current_rpm / state->target_rpm;
// State machine logic
switch (state->phase) {
case OVERDRIVE_PHASE_STARTUP:
if (speed_ratio >= BEMF_TARGET_RPM_THRESHOLD) {
// Reached target speed - switch to sustain
ESP_LOGI(TAG, "BEMF: Reached %.0f%% speed (%lu RPM), switching to sustain",
speed_ratio * 100, state->current_rpm);
motor_set_duty(state->sustain_pwm);
state->phase = OVERDRIVE_PHASE_SUSTAIN;
state->phase_start_us = esp_timer_get_time();
} else {
// Still accelerating - maintain overdrive
motor_set_duty(state->overdrive_pwm);
}
break;
case OVERDRIVE_PHASE_SUSTAIN:
// Maintain steady-state speed
motor_set_duty(state->sustain_pwm);
break;
case OVERDRIVE_PHASE_COAST:
motor_coast();
break;
}
}
Integration Example:
case MOTOR_STATE_FORWARD_ACTIVE: {
// Get user's desired intensity from current mode
uint32_t desired_intensity_duty = get_current_mode_intensity(); // e.g., 60% = 614/1023
// Calculate overdrive as relative boost
uint32_t overdrive_candidate = (uint32_t)(desired_intensity_duty * OVERDRIVE_RATIO);
uint32_t overdrive_pwm = (overdrive_candidate > PWM_MAX_DUTY) ? PWM_MAX_DUTY : overdrive_candidate;
bemf_overdrive_state_t overdrive = {
.phase = OVERDRIVE_PHASE_STARTUP,
.target_rpm = calculate_target_rpm(active_time_ms, desired_intensity_duty),
.overdrive_pwm = overdrive_pwm, // e.g., 60% × 1.4 = 84% duty
.sustain_pwm = desired_intensity_duty, // User's desired intensity
.phase_start_us = esp_timer_get_time()
};
motor_set_duty(overdrive.overdrive_pwm);
while ((esp_timer_get_time() - overdrive.phase_start_us) < (active_time_ms * 1000)) {
bemf_overdrive_control(&overdrive); // Takes ~1.1ms (coast + measure + decide)
// Check for interrupts (mode change, shutdown)
// Control loop rate: check BEMF every 5ms (each check takes ~1.1ms)
if (delay_with_mode_check(5)) { // 5ms interval between checks
motor_coast();
break;
}
}
state = MOTOR_STATE_FORWARD_COAST_REMAINING;
break;
}
Expected Results: - ✅ Perceptually uniform intensity across all pulse durations - ✅ Automatic adaptation to motor variation (±15% tolerance) - ✅ Compensation for battery voltage sag during session - ✅ Detection of motor degradation over device lifetime
Measurement Overhead: - Per-sample latency: 1ms settling + ADC read (~0.1ms) = ~1.1ms per sample - Control loop rate: Check BEMF every 5-10ms (gives motor time to respond to PWM changes) - Samples per pulse: 125ms pulse ÷ 5-10ms interval = 12-25 samples possible - Total measurement time: 12-25 samples × 1.1ms = ~13-28ms spent measuring per pulse - Active control time: Remaining ~97-112ms is motor actually running
5.3 Calibration Workflow¶
One-time motor characterization (stored in NVS):
void calibrate_motor_constants(void) {
ESP_LOGI(TAG, "=== Motor Calibration Mode ===");
// 1. Measure Kv constant at known PWM duty
// Use 100% PWM duty (3.3V nominal supply)
ESP_LOGI(TAG, "Applying 100%% PWM (3.3V nominal)...");
motor_set_duty(PWM_MAX_DUTY); // 1023 = 100% duty
vTaskDelay(pdMS_TO_TICKS(500)); // Wait for steady-state
uint32_t rpm_at_full_duty;
bemf_measure_rpm(&rpm_at_full_duty);
// Calculate Kv based on 3.3V supply (measure actual battery voltage for precision)
float battery_voltage = measure_battery_voltage(); // e.g., 3.28V actual
float kv = rpm_at_full_duty / battery_voltage;
ESP_LOGI(TAG, "Motor Kv: %.1f RPM/V (measured at %.2fV)", kv, battery_voltage);
// 2. Measure time constant (startup transient)
ESP_LOGI(TAG, "Measuring startup time constant...");
motor_coast();
vTaskDelay(pdMS_TO_TICKS(1000)); // Full stop
uint64_t start_us = esp_timer_get_time();
motor_set_duty(PWM_MAX_DUTY); // Apply 100% duty
uint32_t rpm;
uint32_t tau_mech_ms = 0;
while (1) {
vTaskDelay(pdMS_TO_TICKS(10));
bemf_measure_rpm(&rpm);
if (rpm >= (rpm_at_full_duty * 0.632f)) { // 63.2% = 1 time constant
tau_mech_ms = (esp_timer_get_time() - start_us) / 1000;
break;
}
}
ESP_LOGI(TAG, "Mechanical time constant: %lu ms", tau_mech_ms);
// 3. Store to NVS
nvs_handle_t nvs;
nvs_open("motor_cal", NVS_READWRITE, &nvs);
nvs_set_blob(nvs, "kv", &kv, sizeof(float));
nvs_set_u32(nvs, "tau_mech_ms", tau_mech_ms);
nvs_commit(nvs);
nvs_close(nvs);
ESP_LOGI(TAG, "Calibration complete and saved to NVS");
}
Note: The Kv constant should account for actual battery voltage, which varies from 2.7V (depleted) to 4.2V (freshly charged). For most accurate results, calibrate at nominal 3.7V (mid-discharge).
6. Perceptual Intensity Matching Strategy¶
6.1 The Challenge: Uniformity vs. Therapeutic Efficacy¶
Two competing design goals:
Goal 1: Perceptual Uniformity (Smooth Ride)¶
- Overdrive ratio matched so startup feels identical to sustain
- User perceives continuous, smooth vibration throughout pulse
- Assumption: Uniform intensity = better therapeutic experience
- Approach: Tune overdrive ratio (1.2-1.4×) for perceptual matching
Goal 2: Therapeutic Efficacy (Pronounced Bilateral Alternation)¶
- Overdrive ratio creates noticeable "punch" at startup (but not jarring)
- User clearly perceives bilateral alternation (left-right distinction)
- Hypothesis: More pronounced alternation may improve therapeutic outcomes
- Approach: Higher overdrive ratio (1.5-2.0×) for clearer bilateral cues
Current Research Gap: Most ERM motor EMDR research uses simple on/off control with global intensity. We don't have evidence about whether perceptually uniform stimulation or pronounced bilateral alternation produces better therapeutic outcomes.
Frequency-Dependent Consideration: - Low frequency (0.5 Hz, long delays): Stronger punch may help user perceive alternation - High frequency (2 Hz, short delays): Smoother transitions may feel less jarring
Human haptic perception is nonlinear: - Weber's Law: ΔI/I = constant (just-noticeable difference) - Perceived intensity follows power law: P = k × I^n (where n ≈ 0.5-0.7 for vibration)
Implication: - Doubling motor speed does NOT double perceived intensity - Overdrive design must account for both psychophysical scaling AND therapeutic goals
6.2 Dual Calibration Approach¶
Since we don't know which approach is therapeutically superior, the device should support both via user-adjustable overdrive ratio:
Approach A: Perceptual Matching (Smooth)¶
- Select desired intensity (e.g., 60% duty = 614/1023)
- Run motor at steady-state sustain PWM duty for 500ms
- User rates perceived intensity on 0-10 scale (baseline = 7)
- Apply overdrive startup (sustain × ratio) with various ratios
- User rates startup intensity on same 0-10 scale
- Adjust overdrive ratio until startup matches baseline rating
- Result: Overdrive ratio 1.2-1.4× (perceptually uniform)
Approach B: Bilateral Emphasis (Pronounced)¶
- Select desired intensity (e.g., 60% duty = 614/1023)
- Run motor at steady-state sustain PWM duty for 500ms
- Apply overdrive startup with higher ratios (1.5-2.0×)
- User evaluates: "Does this feel like a helpful bilateral cue?"
- Adjust ratio until bilateral alternation is clearly perceptible but not jarring
- Result: Overdrive ratio 1.5-2.0× (emphasized bilateral alternation)
BLE GATT Characteristic for Overdrive Ratio¶
Make overdrive ratio user-adjustable via Mode 5 settings: - Range: 1.0× (no overdrive) to 2.0× (100% boost) - Step: 0.1× increments - Default: 1.4× (moderate emphasis) - User can experiment with different ratios to find what works therapeutically
Frequency-Dependent Defaults:
float get_default_overdrive_ratio(float frequency_hz) {
if (frequency_hz < 1.0f) {
// Low frequency - stronger punch helps perception
return 1.6f; // 60% boost
} else {
// High frequency - smoother transitions
return 1.3f; // 30% boost
}
}
Note: This becomes a research tool - users can report which overdrive ratios produce better therapeutic outcomes at different frequencies.
Automated calibration (sweeping overdrive ratio):
typedef struct {
float overdrive_ratio_start; // e.g., 1.1 (10% boost)
float overdrive_ratio_end; // e.g., 1.6 (60% boost)
float overdrive_ratio_step; // e.g., 0.1
uint32_t overdrive_duration_ms; // Fixed duration for this test
} perceptual_calibration_t;
void run_perceptual_calibration(perceptual_calibration_t *cal, uint32_t sustain_duty) {
for (float ratio = cal->overdrive_ratio_start;
ratio <= cal->overdrive_ratio_end;
ratio += cal->overdrive_ratio_step) {
uint32_t overdrive_candidate = (uint32_t)(sustain_duty * ratio);
uint32_t overdrive_duty = (overdrive_candidate > PWM_MAX_DUTY) ? PWM_MAX_DUTY : overdrive_candidate;
ESP_LOGI(TAG, "Test overdrive ratio: %.2f (sustain=%lu/1023, overdrive=%lu/1023, duration=%lums)",
ratio, sustain_duty, overdrive_duty, cal->overdrive_duration_ms);
// Apply overdrive phase
motor_set_duty(overdrive_duty); // e.g., 60% × 1.4 = 84%
vTaskDelay(pdMS_TO_TICKS(cal->overdrive_duration_ms));
// Switch to sustain phase
motor_set_duty(sustain_duty); // User's desired intensity
vTaskDelay(pdMS_TO_TICKS(500 - cal->overdrive_duration_ms)); // Total 500ms pulse
motor_coast();
vTaskDelay(pdMS_TO_TICKS(1000)); // 1s pause between tests
// User input: "Did this feel uniform?" (button press = yes)
}
}
Usage Example:
// Test at strong intensity (80% duty)
run_perceptual_calibration(&cal, (PWM_MAX_DUTY * 80) / 100);
// Test at gentle intensity (30% duty)
run_perceptual_calibration(&cal, (PWM_MAX_DUTY * 30) / 100);
6.3 Logarithmic Scaling for Short Pulses¶
Hypothesis: Overdrive duration should scale logarithmically with pulse duration deficit.
Since we're limited to 100% PWM duty maximum, we adjust overdrive duration based on pulse length:
uint32_t calculate_overdrive_duration_ms(uint32_t pulse_ms, uint32_t tau_mech_ms) {
// Deficit = how far are we from "long enough to reach steady-state"
float deficit_ratio = (5.0f * tau_mech_ms) / pulse_ms;
if (deficit_ratio <= 1.0f) {
// Pulse is long enough - minimal overdrive (2 time constants)
return 2 * tau_mech_ms;
} else {
// Pulse is short - logarithmic scaling of overdrive duration
// For very short pulses, use larger percentage of total pulse time
float log_deficit = log10f(deficit_ratio);
// Scale from 40% to 70% of pulse duration based on deficit
float overdrive_fraction = 0.4f + (log_deficit * 0.3f);
// Cap at 70% of pulse to leave time for sustain phase
if (overdrive_fraction > 0.7f) overdrive_fraction = 0.7f;
return (uint32_t)(pulse_ms * overdrive_fraction);
}
}
// Example usage:
uint32_t pulse_ms = 125;
uint32_t tau_mech_ms = 50;
uint32_t overdrive_duration = calculate_overdrive_duration_ms(pulse_ms, tau_mech_ms);
// Result: 5×50=250, 250/125=2.0, log10(2)=0.301,
// overdrive_fraction = 0.4 + 0.301*0.3 = 0.49 (49%)
// overdrive_duration = 125 * 0.49 = 61ms overdrive, 64ms sustain
Rationale: - Long pulses (≥250ms): Motor reaches steady-state naturally, minimal overdrive needed - Medium pulses (125ms): ~50% overdrive duration gets motor up to speed - Short pulses (<100ms): ~60-70% overdrive duration compensates for never reaching steady-state
7. Comparison Matrix¶
| Criterion | Time-Constant Formula | BEMF Closed-Loop | Sensorless Observer |
|---|---|---|---|
| Accuracy | ⭐⭐⭐ (±15% variation) | ⭐⭐⭐⭐⭐ (±2% variation) | ⭐⭐⭐⭐ (±5% variation) |
| Hardware Required | None (current setup) | OUTA ADC connection | Current sensor + ADC |
| Computational Cost | ⭐⭐⭐⭐⭐ (trivial) | ⭐⭐⭐⭐ (moderate) | ⭐⭐ (matrix operations) |
| Development Time | 1-2 days | 1 week | 2-3 weeks |
| Adaptation to Wear | ❌ No | ✅ Yes | ⚠️ If model updated |
| Tuning Required | Manual (per motor batch) | Automatic | Expert (Kalman tuning) |
| Perceptual Quality | Good (80% solution) | Excellent (95% solution) | Very Good (90% solution) |
| Recommendation | ✅ Use now | ✅ Upgrade later | ❌ Skip |
8. References¶
Industry Standards¶
-
TI DRV2605L Datasheet - Haptic Driver for LRA and ERM with Built-In Library (2023) https://www.ti.com/lit/ds/symlink/drv2605l.pdf
-
Precision Microdrives AB-021 - Measuring RPM from Back EMF https://www.precisionmicrodrives.com/ab-021
-
Precision Microdrives AB-012 - Driving Vibration Motors With Pulse Width Modulation https://www.precisionmicrodrives.com/ab-012
-
Precision Microdrives AB-004 - Understanding ERM Vibration Motor Characteristics https://www.precisionmicrodrives.com/ab-004
Academic Sources¶
-
Immersion Corporation Patent US20130194084A1 - Eccentric Rotating Mass Actuator Optimization for Haptic Effects (2013)
-
"Sensorless Control of Electric Motors with Kalman Filters" - Rigatos & Siano (2011), International Journal of Advanced Robotic Systems
Technical Articles¶
-
"How to improve the startup and stop behavior of ERM and LRA actuators" - Texas Instruments E2E Community (2025)
-
"Electric Servo Motor Equations and Time Constants" - George W. Younkin, P.E., Control Technology Corporation
Motor Control Theory¶
-
"Mechanical Time Constants for Servo Motors" - Parker Hannifin Electromechanical Knowledge Base
-
"Understanding DC Motor Speed" - Automation & Control Engineering Forum
9. Next Steps¶
Immediate (Current Hardware - IN1 Connection)¶
- ✅ Implement time-constant-based overdrive in motor_task.c with configurable ratio
- ✅ Add BLE GATT characteristic: Overdrive ratio (1.0-2.0×, default 1.4×)
- ✅ Subjective testing:
- Test perceptual uniformity (ratio 1.2-1.4×)
- Test bilateral emphasis (ratio 1.5-2.0×)
- Test frequency-dependent effects (0.5 Hz vs 2 Hz)
- ✅ Document baseline: Record perceived intensity AND therapeutic feedback for different ratios
- ⚠️ Skip BEMF measurement: Current IN1 connection provides corrupted signal
After New Boards Arrive (OUTA Connection)¶
- ⏳ Validate circuit analysis with oscilloscope (RC settling time, PWM filtering)
- ⏳ Test BEMF measurement accuracy with OUTA signal (expected ~12,000 RPM at 3.3V)
- ⏳ Implement periodic calibration (every 10s during natural coast periods)
- ⏳ Empirically test stall detection hypothesis (ADC reading during active drive)
- ⏳ Run one-time calibration procedure to store motor Kv constant in NVS
- ⏳ Long-term testing: Verify adaptation to battery sag and motor wear over therapeutic sessions
Documentation & Design Decisions¶
- 📄 Create ADR if BEMF-based periodic calibration proves valuable
- 📄 Archive this document to docs/archive/ if time-based approach is sufficient
- 📄 Update requirements_spec.md with perceptual uniformity and bilateral emphasis requirements
10. Circuit Analysis & BEMF Measurement Strategy¶
10.1 BEMF Sensing Circuit Characteristics¶
Hardware Configuration:
Motor OUTA → R1 (10kΩ) → ADC Input (GPIO0/1) → R2 (10kΩ) → GND
↓
C (22nF) → GND
↓
GPIO Internal Pulldown (100kΩ, optional)
Component Values (Production BOM): - Voltage Divider: 10kΩ / 10kΩ (scales ±3.3V motor swing to 0-3.3V ADC range) - Filter Capacitor: 22nF to ground (chosen for better PWM filtering vs. 10nF) - GPIO Pulldown: 100kΩ internal (when enabled, negligible compared to 10kΩ divider)
Design Note: Original design used 15nF, prototypes built with 12nF. Production BOM standardized on 22nF for improved noise immunity and signal quality.
RC Time Constant Calculation:
R_thevenin = (R1 || R2) = (10kΩ || 10kΩ) = 5kΩ
τ = R_thevenin × C = 5kΩ × 22nF = 110µs
Settling time (5τ for 99.3% accuracy):
t_settle = 5 × 110µs = 550µs ≈ 0.55ms
Key Insight: The settling time is much faster than initially assumed (0.55ms vs. 7.5ms with 100kΩ assumption). This enables frequent BEMF sampling without interrupting therapeutic pulse patterns. The code uses 1ms delays for safety margin.
Low-Pass Filter Cutoff Frequency:
Effect on 25 kHz PWM ripple: - PWM fundamental at 25 kHz is ~17× above cutoff - Attenuation: -20 dB/decade beyond cutoff → ~-25 dB at 25 kHz - Result: PWM ripple reduced by >94%, ADC reads very clean DC average during active drive
10.2 Circuit Behavior in Different H-Bridge States¶
State 1: Active Drive (IN1 XOR IN2 = HIGH)¶
H-Bridge Configuration: - Forward: IN1=HIGH, IN2=LOW → Motor sees +V_supply - Reverse: IN1=LOW, IN2=HIGH → Motor sees -V_supply
ADC Reading During Active Drive:
V_ADC_measured = (V_drive ± V_BEMF) / 2
Where:
- V_drive = PWM average voltage (e.g., 70% duty = 2.31V average)
- V_BEMF = Back-EMF generated by spinning motor (opposes drive voltage)
- Division by 2 from voltage divider
Stall Detection Hypothesis (User's Insight):
When motor is actively driven: - Motor Running: V_ADC < V_drive/2 (BEMF opposes drive, reduces measured voltage) - Motor Stalled: V_ADC ≈ V_drive/2 (no BEMF, only drive voltage present)
Potential Use Case:
// During FORWARD_ACTIVE state, measure ADC while PWM is active
uint32_t adc_during_drive = adc1_get_raw(ADC1_CHANNEL_0);
float v_adc = (adc_during_drive / 4095.0f) * 3.3f;
float v_expected_no_bemf = (current_pwm_duty / 1023.0f) * 3.3f / 2.0f; // Voltage divider
if (v_adc > (v_expected_no_bemf * 0.95f)) {
// ADC reading close to drive voltage → motor might be stalled
ESP_LOGW(TAG, "Possible motor stall detected (ADC=%.2fV, expected <%.2fV)",
v_adc, v_expected_no_bemf * 0.9f);
}
Limitations: - PWM ripple (even with filtering) adds noise to measurement - BEMF magnitude depends on motor speed (hard to distinguish low-speed from stall) - Verdict: Empirical testing required when new boards arrive
State 2: Coast Mode (IN1 = IN2 = LOW)¶
H-Bridge Configuration: - Both low-side FETs OFF → Motor terminals are high-impedance - Motor acts as generator (BEMF appears across terminals)
ADC Reading During Coast:
V_ADC_measured = V_BEMF / 2
Where:
- V_BEMF = Pure back-EMF from motor (no drive voltage present)
- Clean signal after RC settling (550µs)
This is the PRIMARY measurement mode for BEMF-based overdrive control: 1. Set motor to coast (IN1=IN2=LOW) 2. Wait 0.55ms for RC settling (5τ = 550µs) 3. Read ADC to get clean BEMF measurement 4. Calculate motor RPM from BEMF
Current Paths: - Motor → R1 (10kΩ) → ADC input - ADC input → R2 (10kΩ) → GND - ADC input → C (22nF) → GND (filter cap slowly discharges/charges to BEMF voltage) - GPIO pulldown (100kΩ) provides weak additional path to ground (negligible)
Measurement Code (Clean BEMF):
esp_err_t measure_bemf_during_coast(float *rpm_out) {
// Ensure motor is coasting
motor_coast(); // Sets IN1=IN2=LOW
// Wait for RC settling (5τ = 550µs, round up to 1ms for margin)
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms delay (conservative)
// Read ADC
int adc_raw = adc1_get_raw(ADC1_CHANNEL_0);
// Convert to voltage (voltage divider scales ±3.3V to 0-3.3V range)
float v_adc = (adc_raw / 4095.0f) * 3.3f;
float v_bemf = (v_adc * 2.0f) - 3.3f; // Undo voltage divider offset
// Convert to RPM using motor Kv constant (from NVS calibration)
float kv = get_motor_kv_from_nvs(); // e.g., 4000 RPM/V
*rpm_out = fabsf(v_bemf) * kv;
ESP_LOGD(TAG, "BEMF: ADC=%d (%.2fV), V_BEMF=%.2fV, RPM=%.0f",
adc_raw, v_adc, v_bemf, *rpm_out);
return ESP_OK;
}
10.3 Periodic Calibration Strategy (Recommended Approach)¶
Problem with Real-Time BEMF Measurement:
Measuring BEMF during every therapeutic pulse would: - Require interrupting PWM drive for 1ms+ per sample - Add complexity to state machine (additional states for BEMF sampling) - Potentially affect therapeutic pulse pattern integrity - Sample rate limited by RC settling time (can't sample faster than ~1kHz)
Solution: Periodic Calibration During Natural Coast Periods
Instead of real-time measurement, update motor constants periodically:
// Update motor Kv and tau_mech every 10 seconds during natural coast periods
#define CALIBRATION_INTERVAL_MS 10000 // 10 seconds
static uint64_t last_calibration_time_ms = 0;
void periodic_motor_calibration(void) {
uint64_t now_ms = esp_timer_get_time() / 1000;
if ((now_ms - last_calibration_time_ms) < CALIBRATION_INTERVAL_MS) {
return; // Not time yet
}
// Motor is already coasting (natural inter-pulse period)
float rpm;
if (measure_bemf_during_coast(&rpm) == ESP_OK) {
// Update motor constants based on current battery voltage and RPM
float battery_voltage = measure_battery_voltage();
float kv_measured = rpm / battery_voltage;
// Low-pass filter to avoid noise (exponential moving average)
static float kv_filtered = 0;
if (kv_filtered == 0) {
kv_filtered = kv_measured; // Initialize
} else {
kv_filtered = (kv_filtered * 0.9f) + (kv_measured * 0.1f); // 10% new data
}
// Store updated Kv in NVS periodically (every 10 calibrations)
save_motor_kv_to_nvs(kv_filtered);
ESP_LOGI(TAG, "Motor calibration: Kv=%.1f RPM/V (battery=%.2fV)",
kv_filtered, battery_voltage);
}
last_calibration_time_ms = now_ms;
}
Integration with motor_task:
case MOTOR_STATE_FORWARD_COAST_REMAINING:
case MOTOR_STATE_REVERSE_COAST_REMAINING: {
// Natural coast period (375ms typical for 0.5 Hz mode)
// Opportunity for periodic calibration (doesn't interrupt pulse pattern)
periodic_motor_calibration();
// Continue with normal coast delay
if (delay_with_mode_check(coast_remaining_ms)) break;
state = MOTOR_STATE_CHECK_MESSAGES;
break;
}
Benefits of Periodic Calibration: - ✅ No pulse pattern interruption - calibration happens during natural coast periods - ✅ Adapts to battery voltage sag - Kv recalculated every 10 seconds - ✅ Detects motor wear - Long-term Kv drift indicates bearing friction increase - ✅ Simple implementation - No complex state machine changes - ✅ Preserves therapeutic integrity - Active pulse timing unchanged
What Gets Calibrated: 1. Motor Kv constant - RPM per volt (accounts for battery voltage, motor wear) 2. Time constant τ_mech - Can be re-measured during startup transients (Phase 2) 3. Overdrive effectiveness - Compare intended vs. measured RPM to tune ratio
10.4 Testing Plan for New Boards (OUTA Connection)¶
When new boards arrive with BEMF properly connected to OUTA:
Test 1: Verify RC Settling Time¶
Equipment: Oscilloscope (pocket scope)
Procedure: 1. Trigger on motor coast transition (IN1=IN2 transition to LOW) 2. Measure voltage at ADC input (GPIO0) 3. Verify exponential decay/rise to BEMF voltage 4. Confirm 5τ ≈ 550µs settling time
Expected Waveform:
V_ADC
^
| Active Drive (PWM ripple filtered)
| ___/\/\/\/\___
| | \
| | \_____ BEMF steady-state (after 550µs)
| | <-5τ->
+--|---------------|----> Time
Coast starts 550µs
Test 2: Stall Detection Empirical Validation¶
Procedure: 1. Run motor at 70% PWM duty during FORWARD_ACTIVE 2. Measure ADC value during active drive (with 25kHz PWM ripple filtered) 3. Manually stall motor (hold rotor with fingernail) 4. Measure ADC value during stall 5. Compare: Does ADC increase significantly when stalled?
Hypothesis: - Running: V_ADC = (V_drive - V_BEMF) / 2 → Lower reading - Stalled: V_ADC = V_drive / 2 → Higher reading (no BEMF opposition)
If validated, stall detection code:
// During FORWARD_ACTIVE, check for stall every 50ms
if (check_motor_stall_via_adc()) {
ESP_LOGE(TAG, "Motor stall detected! Stopping for safety.");
motor_coast();
state = MOTOR_STATE_SHUTDOWN;
break;
}
Test 3: BEMF Measurement Accuracy¶
Procedure: 1. Run motor at 100% PWM duty (3.3V supply) 2. Wait for steady-state (500ms) 3. Coast motor and measure BEMF after 1ms settling 4. Calculate RPM: RPM = V_BEMF × Kv 5. Compare to expected 12,000 RPM no-load spec 6. Repeat at different battery voltages (3.0V, 3.5V, 4.0V)
Validation: - Expected: ~12,000 RPM at 3.3V supply - Acceptable error: ±10% (manufacturing variation)
Test 4: Voltage Divider Loading Effects¶
Concern: Does 10kΩ/10kΩ voltage divider load the H-bridge output?
H-Bridge Output Impedance: TB6612FNG has <1Ω on-resistance for FETs
Loading Calculation: - Motor load: 33Ω coil resistance - Voltage divider load: 10kΩ || 10kΩ = 5kΩ (when ADC input is high-Z) - Total load: 33Ω || 5kΩ ≈ 33Ω (voltage divider negligible)
Verdict: Voltage divider loading is negligible (<1% effect on motor voltage)
Empirical Test: 1. Measure motor voltage at OUTA with oscilloscope (with voltage divider connected) 2. Disconnect ADC input and measure again 3. Verify <1% voltage difference
10.5 Updated BEMF Measurement Recommendations¶
For Current Boards (IN1 Connection - Inaccurate): - Skip BEMF measurement (signal is corrupted by control input) - Use time-constant-based overdrive with user-adjustable ratio (Phase 1) - Focus on therapeutic feedback to tune overdrive ratio empirically
For New Boards (OUTA Connection - Correct):
Phase 1 (Immediate): Periodic Calibration
1. Measure BEMF during natural coast periods (every 10 seconds)
2. Update motor Kv constant to account for battery voltage and wear
3. Use calibrated Kv to improve time-constant overdrive formula
4. Still use relative overdrive: overdrive_duty = sustain_duty × ratio
Phase 2 (Future): Real-Time Closed-Loop (Optional) 1. Control loop checks BEMF every 5-10ms during pulse (each check takes ~1.1ms) 2. Switch from overdrive to sustain when motor reaches 90% target RPM 3. Provides optimal perceptual uniformity 4. Complexity: Requires careful integration to avoid pulse pattern disruption
Recommendation: Start with Phase 1 periodic calibration, evaluate if Phase 2 is necessary based on therapeutic feedback.
Why Periodic Calibration is Sufficient:
| Aspect | Periodic (every 10s) | Real-Time (check every 5-10ms) |
|---|---|---|
| Measurement frequency | Once per 10 seconds | 12-25 times per 125ms pulse |
| Per-measurement latency | ~1.1ms (same as real-time) | ~1.1ms (same as periodic) |
| Adaptation to battery sag | ✅ Yes (slow drift over minutes) | ✅ Yes (instant response) |
| Motor wear detection | ✅ Yes (slow drift over weeks) | ✅ Yes (instant response) |
| Pulse pattern integrity | ✅ Preserved (measures during coast) | ⚠️ Interrupts active drive 12-25× per pulse |
| Implementation complexity | ⭐⭐ Simple | ⭐⭐⭐⭐ Complex (tight state machine integration) |
| Therapeutic value | ✅ Likely sufficient | ❓ Unknown benefit |
Verdict: Periodic calibration provides 90% of the benefit with 10% of the complexity. Real-time closed-loop is only justified if user testing reveals perceptual inconsistency that periodic calibration can't address.
11. Conclusion¶
Recommended Path Forward¶
Phase 1 (Now): Implement configurable time-constant-based overdrive - Uses current hardware (no changes needed) - Make overdrive ratio user-adjustable (1.0-2.0×) via BLE GATT - Provides foundation for both perceptual uniformity AND bilateral emphasis approaches - Enables user experimentation and therapeutic feedback collection
Phase 2 (After new boards): Upgrade to BEMF closed-loop with configurable ratio - Leverages existing ADC hardware investment with OUTA connection - Maintains user-adjustable overdrive ratio for therapeutic experimentation - Provides adaptive compensation while preserving user control - Superior to sensorless observers (simpler, more accurate)
Phase 3 (Ongoing research): Therapeutic efficacy studies - Collect user feedback on different overdrive ratios - Document frequency-dependent effects (0.5 Hz vs 2 Hz) - Identify patterns: Does perceptual uniformity or bilateral emphasis produce better outcomes? - Publish findings to contribute to EMDR research literature
Dual-Goal Design Philosophy¶
This implementation serves two purposes:
- Engineering Goal: Compensate for motor startup lag to achieve desired intensity profile
- Research Goal: Provide a tool for investigating whether perceptually uniform vs. emphasized bilateral alternation improves therapeutic efficacy
Key Insight: Since existing EMDR research uses simple on/off control, we don't know which approach is therapeutically optimal. By making overdrive ratio user-adjustable, the device becomes a research platform for investigating this question.
Why Not Sensorless Observers?¶
While Extended Kalman Filters and Luenberger observers are powerful for PMSM/BLDC motors without position sensors, they are unnecessary complexity for this application:
- We already have direct BEMF measurement hardware
- Current sensing adds cost and complexity
- EKF tuning requires expert knowledge (Q/R matrices)
- Computational overhead not justified when direct measurement available
Verdict: Direct BEMF measurement with user-configurable overdrive ratio is the optimal approach for this therapeutic research device.
Updated Circuit Analysis Findings¶
Key Discoveries from 10kΩ/10kΩ Voltage Divider + 22nF Filter Analysis:
- RC Settling Time: 550µs (NOT 7.5ms as initially assumed with 100kΩ)
- Much faster than expected, enabling frequent BEMF sampling
- Fast enough to sample during natural coast periods without timing impact
-
Production BOM uses 22nF for improved signal quality vs. 10nF option
-
PWM Ripple Filtering: 25kHz PWM attenuated by >94% (1.45kHz cutoff)
- ADC reads very clean DC average even during active drive
-
Excellent noise immunity for stall detection hypothesis testing
-
Periodic Calibration Recommendation: Update motor Kv every 10 seconds
- Adapts to battery voltage sag (slow drift over minutes)
- Detects motor wear over device lifetime (weeks/months)
- Preserves therapeutic pulse pattern integrity
-
Simpler than real-time closed-loop, likely sufficient for therapeutic use
-
Voltage Divider Loading: Negligible (<1% effect on motor voltage)
- 5kΩ divider load vs. 33Ω motor coil → minimal current draw
- No need for buffer amplifier
Implementation Priority:
| Approach | When | Complexity | Benefit |
|---|---|---|---|
| Time-constant overdrive (configurable ratio) | Now | ⭐⭐ | ✅ Works immediately, user research tool |
| Periodic BEMF calibration (every 10s) | New boards | ⭐⭐⭐ | ✅ Adapts to battery/wear, simple |
| Real-time closed-loop (check every 5-10ms) | If needed | ⭐⭐⭐⭐⭐ | ❓ Unknown therapeutic value |
Note: "Check every 5-10ms" means the control loop samples BEMF at 5-10ms intervals. Each BEMF measurement takes ~1.1ms (RC settling + ADC read).
Document Status: ✅ Complete with Circuit Analysis - Ready for review and ADR consideration Last Updated: January 8, 2025 (added Section 10: Circuit Analysis & BEMF Measurement Strategy) Estimated Implementation Time: 2-3 days (Phase 1), 3-4 days (Periodic Calibration), 1-2 weeks (Real-Time Closed-Loop if needed) Hardware Blocker: BEMF measurement requires new boards with OUTA connection (current IN1 connection is inaccurate)