How to Use the FastAccelStepper Library for Stepper Motor Control and Endstop Handling

The FastAccelStepper library provides non-blocking, high-performance stepper motor control on ESP32 through a singleton engine pattern, while the StepperWithLimits wrapper adds sensorless homing via current detection, mechanical endstop support, and automatic step-loss recovery for force-feedback pedal applications.

The DIY Sim Racing FFB Pedal firmware demonstrates production-grade stepper control by wrapping the FastAccelStepper library in a hardware-abstraction layer. This guide explains how the chrgri/diy-sim-racing-ffb-pedal repository implements precise motion control, sensorless homing, and crash detection using the library's interrupt-driven architecture.

Initializing the FastAccelStepper Engine

The firmware uses a singleton pattern to ensure only one FastAccelStepperEngine instance manages the ESP32's hardware timer. In StepperWithLimits.cpp (lines 47-58), the stepperEngine() function creates and configures the engine at 4 kHz:

FastAccelStepperEngine stepperEngine() {
  static FastAccelStepperEngine myEngine;
  static bool initialized = false;
  if (!initialized) {
    myEngine.init();
    myEngine.task_rate(4);  // 4 kHz update rate
    initialized = true;
  }
  return myEngine;
}

This singleton approach allows multiple StepperWithLimits instances to share the same hardware timer without conflicts.

Configuring the Stepper Motor

The StepperWithLimits constructor connects a stepper to the engine and configures direction inversion and auto-enable functionality. From StepperWithLimits.cpp (lines 73-80):

StepperWithLimits::StepperWithLimits(uint8_t pinStep, uint8_t pinDirection, 
                                     bool invertMotorDir_b, uint32_t stepsPerMotorRev_u32) {
  _pinStep = pinStep;
  _pinDir = pinDirection;
  _invertMotorDir = invertMotorDir_b;
  _stepsPerMotorRev = stepsPerMotorRev_u32;
  
  _stepper = stepperEngine().stepperConnectToPin(_pinStep);
  _stepper->setDirectionPin(_pinDir, _invertMotorDir);
  _stepper->setAutoEnable(true);
}

The setAutoEnable(true) call allows the driver to automatically power the motor only during movement, reducing heat generation during idle periods.

Setting Speed and Acceleration Limits

To prevent mechanical damage, the firmware enforces a maximum speed defined in Main.h. The constant MAXIMUM_STEPPER_SPEED (approximately 250,000 steps/second) is converted to timer ticks and applied to the stepper:

// From Main.h (lines 65-66)
#define MAXIMUM_STEPPER_SPEED 250000  // steps per second

// Applied in StepperWithLimits.cpp
uint32_t maxSpeedInTicks = convertSpeedToTicks(MAXIMUM_STEPPER_SPEED);
_stepper->setSpeedInTicks(maxSpeedInTicks);
_stepper->setAcceleration(MAXIMUM_STEPPER_SPEED * 4);  // 4x speed for aggressive ramping

The convertSpeedToTicks() function translates steps-per-second into the timer tick values required by the FastAccelStepper library's internal pulse queue.

Implementing Sensorless Homing and Endstop Handling

The StepperWithLimits class provides robust limit detection through sensorless homing using servo current monitoring, with optional support for mechanical switches.

Sensorless Homing via Current Detection

The primary homing method findMinMaxSensorless() detects physical stops by monitoring the iSV57 servo's current draw. When the pedal hits a mechanical stop, current rises above STEPPER_WITH_LIMITS_SENSORLESS_CURRENT_THRESHOLD_IN_PERCENT (30%):

void StepperWithLimits::findMinMaxSensorless(const DAP_config_st& dap_config_st) {
  // Move toward minimum at reduced speed for safety
  setSpeed(MAXIMUM_STEPPER_SPEED / 4);
  
  while (abs(getServosCurrent()) < STEPPER_WITH_LIMITS_SENSORLESS_CURRENT_THRESHOLD_IN_PERCENT) {
    moveTo(_currentPosition - 100, false);  // Non-blocking move toward min
    delay(10);
  }
  
  _endstopLimitMin = _currentPosition;
  // Repeat logic for maximum detection...
}

This approach eliminates mechanical limit switches while providing accurate travel calibration.

Mechanical Endstop Support

The class also supports traditional GPIO-based limit switches through optional constructor parameters. When pinMin or pinMax are specified, the firmware configures them as digital inputs:

// Optional GPIO endstop initialization
if (pinMin != 0) {
  pinMode(pinMin, INPUT);
  // Logic would check digitalRead(pinMin) against LIMIT_TRIGGER_VALUE
}

The architecture allows hybrid configurations where mechanical switches serve as safety backups to sensorless detection.

Step-Loss Recovery and Crash Detection

The firmware includes sophisticated error handling to maintain positional accuracy and prevent hardware damage during stalls.

Step-Loss Compensation

When the motor stalls against an endstop, the stepper's internal position counter may drift from the servo's actual position. The correctPos() method calculates this offset and applies compensation:

void StepperWithLimits::correctPos() {
  int32_t servo_offset_compensation_steps_local_i32 = calculateOffset();
  
  if (xSemaphoreTake(semaphore_resetServoPos, portMAX_DELAY) == pdTRUE) {
    stepper_cl->servo_offset_compensation_steps_i32 = servo_offset_compensation_steps_local_i32;
    xSemaphoreGive(semaphore_resetServoPos);
  }
}

This ensures positional accuracy is maintained even after mechanical stalls by storing the offset in servo_offset_compensation_steps_i32.

Crash Detection

The servoCommunicationTask() monitors for crash conditions when the pedal remains stuck at an endstop with high current draw for longer than TIME_SINCE_SERVO_POS_CHANGE_TO_DETECT_CRASH_IN_MS (10 seconds):

// In servoCommunicationTask() (lines 990-1012)
if (timeSinceLastPositionChange > TIME_SINCE_SERVO_POS_CHANGE_TO_DETECT_CRASH_IN_MS) {
  if (abs(getServosCurrent()) > CURRENT_THRESHOLD_CRASH_DETECT) {
    isv57.applyOfsetToZeroPos();  // Adjust zero to relieve pressure
  }
}

This safety feature prevents continuous motor heating and potential damage during extended stall conditions by adjusting the zero position offset.

Thread-Safe Communication

All cross-task operations use FreeRTOS semaphores to prevent race conditions. The correctPos() method demonstrates this pattern when updating the servo offset:

if (xSemaphoreTake(semaphore_resetServoPos, portMAX_DELAY) == pdTRUE) {
  // Critical section: update shared variables
  stepper_cl->servo_offset_compensation_steps_i32 = value;
  xSemaphoreGive(semaphore_resetServoPos);
}

This ensures safe concurrent access between the main loop and the servo communication task running on the ESP32.

Complete Implementation Example

The following excerpt from the firmware demonstrates the typical initialization and usage pattern:

// main.cpp (excerpt)
#include "StepperWithLimits.h"
#include "Main.h"

// pins for the V3 PCB
constexpr uint8_t STEP_PIN = stepPinStepper;   // 23
constexpr uint8_t DIR_PIN  = dirPinStepper;    // 22
constexpr bool   INVERT_DIR = false;
constexpr uint32_t STEPS_PER_REV = 3200;       // matches motor gearing

// Global stepper wrapper
StepperWithLimits pedalStepper(STEP_PIN, DIR_PIN, INVERT_DIR, STEPS_PER_REV);

/* ------------------------------------------------------------------
   1️⃣ Initialise the stepper (done in the constructor)
   ------------------------------------------------------------------ */
// No extra call needed – the constructor already connects to the engine,
 // sets direction pin, enables auto‑enable, and applies speed/acceleration limits.

// ------------------------------------------------------------------
// 2️⃣ Perform a sensorless homing sequence to discover travel limits
// ------------------------------------------------------------------
DAP_config_st cfg;                // configuration struct defined elsewhere
cfg.payLoadPedalConfig_.lengthPedal_travel = 150.0f; // mm, example
cfg.payLoadPedalConfig_.spindlePitch_mmPerRev_u8 = 2; // mm/rev, example
pedalStepper.findMinMaxSensorless(cfg);   // blocks until both ends are found

// ------------------------------------------------------------------
// 3️⃣ Map user‑requested pedal travel (0‑100 %) to step counts
// ------------------------------------------------------------------
uint8_t startPct = 0;   // pedal fully released
uint8_t endPct   = 100; // pedal fully pressed
pedalStepper.updatePedalMinMaxPos(startPct, endPct);

// ------------------------------------------------------------------
// 4️⃣ Move the pedal to a target position (non‑blocking)
// ------------------------------------------------------------------
int32_t targetSteps = pedalStepper.getMinPosition() + 5000; // example offset
pedalStepper.moveTo(targetSteps, false); // false = asynchronous

// ------------------------------------------------------------------
// 5️⃣ Optional: force a safe stop (e.g., on shutdown)
// ------------------------------------------------------------------
pedalStepper.forceStop();

Key API Reference

Method Purpose
StepperWithLimits(uint8_t pinStep, uint8_t pinDirection, bool invertMotorDir_b, uint32_t stepsPerMotorRev) Construct wrapper, attach to FastAccelStepper engine.
findMinMaxSensorless(const DAP_config_st &cfg) Sensorless homing → sets _endstopLimitMin / _endstopLimitMax.
updatePedalMinMaxPos(uint8_t startPct, uint8_t endPct) Translate pedal‑travel percentages into step counts (_posMin, _posMax).
moveTo(int32_t position, bool blocking) Command a move. If blocking==true the call will not return until the movement finishes.
moveSlowlyToPos(int32_t pos) Helper that temporarily reduces speed/acceleration, moves, then restores normal speed.
forceStop() Immediate emergency halt.
setSpeed(uint32_t stepsPerSec) Override the default speed (normally left at MAXIMUM_STEPPER_SPEED).
isAtMinPos() True when the stepper is stationary near the calibrated minimum.
correctPos() Apply step‑loss compensation after a stall.

Summary

  • FastAccelStepperEngine operates as a singleton at 4 kHz to manage hardware timers for all stepper instances in StepperWithLimits.cpp.
  • Sensorless homing detects travel limits by monitoring servo current against the STEPPER_WITH_LIMITS_SENSORLESS_CURRENT_THRESHOLD_IN_PERCENT threshold (30%).
  • Speed limits are enforced via MAXIMUM_STEPPER_SPEED (250,000 steps/s) converted to timer ticks and applied with setSpeedInTicks().
  • Step-loss recovery uses correctPos() to calculate drift between stepper counter and servo position, applying compensation through thread-safe semaphores.
  • Crash detection triggers when stall time exceeds TIME_SINCE_SERVO_POS_CHANGE_TO_DETECT_CRASH_IN_MS (10 s) with high current, adjusting the zero position offset to prevent damage.

Frequently Asked Questions

How does the FastAccelStepper library differ from standard Arduino stepper libraries?

Unlike blocking Arduino stepper libraries, FastAccelStepper uses ESP32 hardware timer interrupts to generate step pulses asynchronously, allowing the main loop to perform other tasks during movement. The library supports multiple steppers from a single engine instance and provides precise acceleration curves without CPU overhead, as implemented in the stepperEngine() singleton pattern.

Can I use mechanical endstops instead of sensorless homing?

Yes. While the default implementation in StepperWithLimits.cpp uses sensorless homing via current detection, the constructor accepts pinMin and pinMax parameters for GPIO endstops. When specified, the firmware configures these pins as digital inputs and can check digitalRead() against LIMIT_TRIGGER_VALUE to detect limits, allowing hybrid approaches where mechanical switches serve as safety backups.

What happens when the stepper loses steps during a stall?

The firmware detects step loss through the correctPos() method, which calculates the offset between the stepper's internal position counter and the servo's absolute position feedback. This difference is stored in servo_offset_compensation_steps_i32 and applied to subsequent moves, ensuring positional accuracy is maintained even after mechanical stalls against endstops.

How do I adjust the maximum speed and acceleration limits?

Modify the MAXIMUM_STEPPER_SPEED constant in Main.h (default 250,000 steps/s). The StepperWithLimits class automatically converts this to timer ticks via convertSpeedToTicks() and applies it using setSpeedInTicks(). Acceleration defaults to four times the maximum speed (MAXIMUM_STEPPER_SPEED * 4) for aggressive ramping, but you can override this via setAcceleration() for specific movement profiles.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →