Skip to content

Using the Library

This chapter shows how to bring a motor up with Stepper-C: fill the configuration, start the driver, command motion, read the status and handle faults. The hardware side (timer, ADC, interrupts) is in the Integration Guide; the meaning of every configuration field is in Configuration.

Setting up a driver

One struct sstep controls one motor. Allocate it statically (or in any memory that lives as long as the motor runs), fill a struct sstep_config and call sstep_init():

static struct sstep motor;

int motor_start(const struct sstep_config* cfg)
{
    int result = sstep_init(&motor, cfg);
    if (result == SSTEP_OK)
    {
        result = sstep_enable(&motor);
    }
    return result;
}

sstep_init() checks every field, derives the controller gains, the ADC scaling, the trip levels and the motion constants, and leaves the outputs off (SSTEP_STATE_DISABLED). It does not call any callback. Start the PWM timer and the ADC before sstep_enable(): the fast loop must already run when the outputs are switched on.

sstep_enable() calls output_enable(true) and holds the current position with the run current (the hold current follows after hold_delay_ms). The rotor jumps to the nearest position of the commanded electrical angle when the current appears: up to two full steps on the first enable after power-up.

Commanding motion

All motion commands take positions in microsteps and return immediately; the slow loop executes them. They are accepted in SSTEP_STATE_HOLDING and SSTEP_STATE_MOVING.

Command Effect
sstep_move_to(&motor, 3200) Move to the absolute position 3200 with the trapezoidal profile
sstep_move_by(&motor, -800) Move 800 microsteps back from the current target
sstep_run(&motor, 6400) Accelerate to 6400 microsteps/s and keep running (negative: other direction)
sstep_stop(&motor) Decelerate to standstill and hold
sstep_set_position(&motor, 0) Call the current position 0 (homing); only at standstill, the rotor does not move
sstep_set_motion_limits(&motor, v_max, accel, decel) New limits, used from the next slow period
sstep_set_current(&motor, run_ma, hold_ma) New run and hold currents

A command replaces the running one without a velocity step: a new target during a move, a run during a move, a reversal during a run. sstep_move_by() adds to the target, so several relative moves given in a row add up.

Wait for a move with the event callback or by polling the status:

static volatile bool move_done;

static void on_event(void* context, enum sstep_event event)
{
    (void)context;
    if (event == SSTEP_EVENT_MOVE_DONE)
    {
        move_done = true;
    }
}

bool motor_is_idle(void)
{
    struct sstep_status st;
    return (sstep_get_status(&motor, &st) == SSTEP_OK) && (st.state == SSTEP_STATE_HOLDING);
}

Reading the status

sstep_get_status() fills a snapshot in engineering units:

Field Unit Meaning
state – SSTEP_STATE_DISABLED, …_HOLDING, …_MOVING, …_FAULT
faults flags Active SSTEP_FAULT_… flags
position microsteps Commanded position (rounded)
target microsteps Target of the current move
velocity microsteps/s Commanded velocity
i_a_ma, i_b_ma mA Last measured phase currents
vbus_mv mV Last measured bus voltage
emf_d_mv, emf_q_mv mV Back-EMF estimate along and 90° ahead of the commanded current (stall detection)
rotor_velocity microsteps/s Rotor velocity estimated from the back-EMF (stall detection)
load_angle_cdeg 0.01° Estimated load angle in motion (stall detection)
stall_level_pct % Stall detector level; the fault comes at 100

Position and velocity are commanded values. There is no encoder: with stall detection the library reports a rotor that stops following the command (next section); without it a lost step goes unnoticed. Either way, keep the load angle reserve with enough current and a sensible acceleration (Motor Control).

Detecting a stall

Stall detection estimates the back-EMF of the motor in the fast loop and raises SSTEP_FAULT_STALL when the rotor stops in motion, falls out of synchronism, or turns while the command is at rest (for example a load that the hold current cannot hold). It is off unless stall_time_ms is set, and needs current drive and ke_uv_s_per_rad:

static struct sstep_config with_stall_detection(struct sstep_config cfg)
{
    cfg.ctrl.stall_time_ms = 50U;     /* report after 50 ms */
    cfg.ctrl.stall_filter_us = 1000U; /* back-EMF filter */
    cfg.ctrl.stall_speed = 1600U;     /* 0.5 rev/s at 16 microsteps: slip threshold */
    cfg.ctrl.stall_sync_pct = 50U;    /* in motion: back-EMF >= 50 % of Ke * speed */
    return cfg;
}

The fault is handled like the others (next section): the outputs are off and the commanded position no longer matches the rotor, so home the axis again after sstep_clear_faults(). Below stall_speed only a rotor faster than the command is detected; run homing moves against a hard stop faster than stall_speed.

While tuning, log the estimates from the slow-loop context and look at the margin: stall_level_pct should stay well below 100 in every normal operating point, and load_angle_cdeg shows how much torque reserve is left.

void log_stall_margin(void)
{
    struct sstep_status st;
    if (sstep_get_status(&motor, &st) == SSTEP_OK)
    {
        board_log_stall(st.velocity, st.rotor_velocity, st.load_angle_cdeg, st.stall_level_pct);
    }
}

The algorithm, the choice of the four values and the measured results are in Stall detection.

Handling faults

A fault switches the outputs off at once, sets SSTEP_STATE_FAULT and calls on_fault() with the new flags. Commands then return SSTEP_ERR_STATE.

Flag Raised by Typical cause
SSTEP_FAULT_OVERCURRENT Fast loop, current drive only Short circuit, wrong adc_i_span sign, gains far too high
SSTEP_FAULT_OVERVOLTAGE Fast loop Regenerated energy when braking a load; supply too high
SSTEP_FAULT_UNDERVOLTAGE Fast loop Supply off or sagging; vbus_min_mv too close to nominal
SSTEP_FAULT_HAL Any function that calls a callback pwm_write() or output_enable() returned a negative value
SSTEP_FAULT_EXTERNAL (or any flag you choose) sstep_report_fault() Gate driver fault pin, over-temperature
SSTEP_FAULT_STALL Slow loop, stall detection on; fast loop, with SSTEP_FAULT_OVERCURRENT, when the rotor is torn away faster than stall_time_ms Load above the torque, hard stop, rotor slipping at the hold current

Report a gate driver fault from its interrupt:

void EXTI15_10_IRQHandler(void);
void EXTI15_10_IRQHandler(void)
{
    if (board_driver_fault())
    {
        (void)sstep_report_fault(&motor, SSTEP_FAULT_EXTERNAL);
    }
}

To recover, remove the cause, then:

if ((sstep_clear_faults(&motor) == SSTEP_OK) && (sstep_enable(&motor) == SSTEP_OK))
{
    board_led(false);
}

sstep_clear_faults() returns to SSTEP_STATE_DISABLED; the position is kept, so the motor continues from where it was commanded (it may have been moved by hand meanwhile).

Several motors

Each motor has its own instance and its own callbacks; use context to tell the callbacks which hardware they drive:

static struct sstep axes[2];
static const uint32_t axis_ids[2] = {0U, 1U};

static int axis_pwm_write(void* context, const uint16_t compare[SSTEP_LEGS_MAX])
{
    board_pwm_set_axis(*(const uint32_t*)context, compare);
    return 0;
}

with .hal.context = (void*)&axis_ids[k] in the configuration of axis k. The fast loops of several axes may run in one interrupt one after the other, or in separate interrupts.

Return values

Code Value Returned when
SSTEP_OK 0 Success
SSTEP_ERR_PARAM −1 NULL pointer, a value outside its range, |velocity| > v_max, a relative move that overflows
SSTEP_ERR_CONFIG −2 Configuration fields that do not fit together, currents above the trip or rated current
SSTEP_ERR_STATE −3 Any function but sstep_init() on an instance without a successful sstep_init(); a motion command while disabled or faulted; sstep_set_position() while moving; sstep_enable() while faulted
SSTEP_ERR_UNSUPPORTED −4 A topology that is not compiled in

sstep_fast_loop() returns SSTEP_ERR_PARAM only for NULL arguments and only with SSTEP_CFG_ARG_CHECKS = 1; its faults are reported through the state and on_fault().

Common mistakes

Symptom Likely cause
SSTEP_FAULT_OVERCURRENT right after sstep_enable() adc_i_span has the wrong sign (inverting amplifier) or adc_i_zero is wrong: the controller sees the current in the wrong direction and drives it up
SSTEP_FAULT_UNDERVOLTAGE right after sstep_enable() The fast loop ran before the first ADC conversion (vbus = 0), or adc_vbus_mv_full is wrong
The motor hums but does not turn Phases swapped or one phase reversed; current_bw_hz too low; v_max above what the supply can drive
The motor turns the wrong way Swap the two wires of one phase, or negate positions in the application
Loses steps at speed Not enough voltage for the speed (Voltage limits); shared-leg stage at its limit; acceleration too high for the load
Moves are jerky, about once per millisecond sstep_slow_loop() is not called at slow_loop_hz, or fast_loop_hz does not match the real interrupt rate
Current settles at the wrong value i_full_scale_ma / adc_i_span do not match the shunt and amplifier
SSTEP_FAULT_STALL at enable or at the run → hold change, rotor at rest Observer offset above \(e\) times the slip threshold (wrong r_mohm, adc_i_zero): check emf_d_mv at rest, raise stall_speed
SSTEP_FAULT_STALL at speed although the motor runs Load close to the pull-out torque, or a load angle swinging beyond about 120° near the voltage limit: check load_angle_cdeg, lower stall_sync_pct for a heavy load (not below the inductance error, Choosing the parameters), lower the speed or raise the current
sstep_init() returns SSTEP_ERR_CONFIG Check the combinations: trip ≤ full scale, run current ≤ trip and rated, bandwidth ≤ fast_loop_hz / 4, fast_loop_hz a multiple of slow_loop_hz