Skip to content
Open-source embedded library

Stepper Motor Controlin portable C

Stepper-C turns phase-current measurements and motion commands into PWM duty cycles for two-phase stepper motors: closed-loop microstepping current control on any MCU, with no heap, no RTOS and code that passes MISRA C:2012.

  • No heap
    No RTOS
  • MISRA C:2012
    0 violations
  • 100 % line &
    branch coverage
  • Apache-2.0
    open source
A controller board with the Stepper-C chip and its power transistors drives a stepper motor; the two phase currents are drawn as sine waves.
The problem

Smooth, quiet and exact, on a small MCU

A stepper driver chip hides the current control in silicon. When you build the power stage yourself (an MCU with gate drivers, a three-leg ESC board, a custom H-bridge) the firmware has to regulate two phase currents at the PWM rate, keep the position exact over millions of microsteps, stay inside the voltage the bridge can apply and switch off within one period on a fault, often on a core without an FPU.

Currents that follow the angle at 20 kHz

One PI controller per phase with gains derived from R, L and one bandwidth value, back-EMF feed-forward and anti-windup. On the motor model a 2 A step settles in 0.40 ms with 1.1 % overshoot.

  • PI + feed-forward
  • Anti-windup
  • Bus compensation

Tight embedded budgets

432 B of RAM per motor, 216 B worst-case stack in the PWM interrupt, 8.8 KiB of flash on Cortex-M4. A fixed-point backend for cores without FPU; no allocation, no globals, no OS.

  • Q15 or float
  • Static memory
  • Bare metal & RTOS

Code you can certify

Zero MISRA C:2012 violations, a documented deviation register, 100 % line and branch coverage in both numeric backends, bounded execution time and static analysis in CI.

  • MISRA C
  • Cppcheck
  • CodeQL
What you get

One library from motion command to PWM

15 public functions, five modules (control, stall detection, motion, power stage, numeric backend), two power stage topologies and two numeric backends, verified in closed loop on a motor model.

15public functions
432 BRAM per motor
216 Bstack in the PWM interrupt
0MISRA C:2012 violations

Quick start

A motor running in one file

Two callbacks connect the library to the power stage: one writes the PWM compare values, one switches the gate drivers. The PWM/ADC interrupt passes the raw ADC readings to the fast loop; the main loop runs the slow loop once per millisecond and gives the commands.

#include "sstep.h"

static struct sstep motor;

static int pwm_write(void* context, const uint16_t compare[SSTEP_LEGS_MAX])
{
    (void)context;
    board_pwm_set(compare);
    return 0;
}

static int output_enable(void* context, bool enable)
{
    (void)context;
    board_gates_enable(enable);
    return 0;
}

/* 17HS19-2004S1 on a B-G431B-ESC1: 24 V, 20 kHz PWM, 16 microsteps */
static const struct sstep_config motor_config = {
    .motor = {.r_mohm = 1600U, .l_uh = 3000U, .i_rated_ma = 2000U,
              .ke_uv_s_per_rad = 208600U, .full_steps_per_rev = 200U},
    .stage = {.topology = SSTEP_TOPOLOGY_SHARED_LEG, .vbus_nominal_mv = 24000U,
              .vbus_min_mv = 10000U, .vbus_max_mv = 28000U, .i_full_scale_ma = 4000U,
              .i_trip_ma = 3500U, .pwm_period_ticks = 4250U, .pwm_freq_hz = 20000U,
              .adc_i_zero = 2048U, .adc_i_span = 2047, .adc_vbus_mv_full = 34320U,
              .adc_full = 4095U},
    .ctrl = {.drive_mode = SSTEP_DRIVE_CURRENT, .fast_loop_hz = 20000U,
             .slow_loop_hz = 1000U, .microsteps = 16U, .current_bw_hz = 1000U,
             .i_run_ma = 1500U, .i_hold_ma = 500U, .hold_delay_ms = 500U,
             .v_max = 16000U, .accel = 64000U},
    .hal = {.pwm_write = pwm_write, .output_enable = output_enable},
};

/* PWM/ADC interrupt, 20 kHz */
void ADC1_2_IRQHandler(void);
void ADC1_2_IRQHandler(void)
{
    const struct sstep_sample sample = {board_adc_phase_a(), board_adc_phase_b(),
                                        board_adc_vbus()};
    (void)sstep_fast_loop(&motor, &sample);
}

int main(void)
{
    board_init();
    if ((sstep_init(&motor, &motor_config) != SSTEP_OK) || (sstep_enable(&motor) != SSTEP_OK))
    {
        for (;;)
        {
        }
    }
    (void)sstep_move_to(&motor, 3200); /* one revolution */
    for (;;)
    {
        if (board_tick_1ms())
        {
            (void)sstep_slow_loop(&motor);
        }
    }
}

The board_… functions stand for your timer, ADC and GPIO code; the ADC scaling values above must be measured on your board (Configuration).

Find your way

If you want to… Read
Understand what the library does and does not do Introduction
Build it, run the tests, add it to your project Getting Started
Understand the loops, states and memory How It Works
Configure a motor and command motion Using the Library
Understand the current control, microstepping and limits Motor Control
Connect it to timers, ADC and interrupts Integration Guide
Look up a function API Reference
Read everything offline or print it PDF user manual
SystemFromScratch®

Part of an open-source embedded portfolio

Stepper-C is developed by Anton Vynohradov under SystemFromScratch® — embedded software and hardware built from the first schematic to certification.

SystemFromScratch® is a registered trademark. STM32 is a registered trademark of STMicroelectronics International N.V.; Stepper-C is not affiliated with or endorsed by STMicroelectronics.