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

> Master FastAccelStepper for ESP32 stepper control. Learn non-blocking movement, sensorless homing, endstop handling, and step-loss recovery for robust force-feedback applications.

- Repository: [chrgri/diy-sim-racing-ffb-pedal](https://github.com/chrgri/diy-sim-racing-ffb-pedal)
- Tags: how-to-guide
- Published: 2026-02-27

---

**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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/StepperWithLimits.cpp) (lines 47-58), the `stepperEngine()` function creates and configures the engine at 4 kHz:

```cpp
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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/StepperWithLimits.cpp) (lines 73-80):

```cpp
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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/Main.h). The constant `MAXIMUM_STEPPER_SPEED` (approximately 250,000 steps/second) is converted to timer ticks and applied to the stepper:

```cpp
// 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%):

```cpp
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:

```cpp
// 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:

```cpp
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):

```cpp
// 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:

```cpp
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:

```cpp
// 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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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`](https://github.com/chrgri/diy-sim-racing-ffb-pedal/blob/main/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.