BERGSONNE

Stepper (STEP/DIR)

Almost every stepper driver — from a bench-supply DM320T to a DRV8428 on a tile — takes the same two signals: a STEP pulse per microstep and a DIR level. core_stepper generates them from a timer output channel and a pad, with trapezoidal ramps, non-blocking position and velocity moves, and a signed microstep position count.

Overview

Declare the STEP pad as a timer channel in config.json, bind an axis to it and a DIR pad, set a cruise speed and an acceleration, then queue moves. The hardware makes every pulse; one interrupt per step plans the next interval. Your main loop just polls core_stepper_busy() (and feeds the watchdog).

// config.json:  "pads": { "3": "TIM2.2", "9": "GPIO.OUT" }
#include "core_stepper.h"

  core_stepper_t axis;
  core_stepper_init(&axis, 3, 9);          // STEP on pad 3 (TIM2 ch2), DIR on pad 9
  core_stepper_set_speed(&axis, 1600);      // microsteps / s
  core_stepper_set_accel(&axis, 8000);      // microsteps / s²

  core_stepper_move(&axis, 160);            // non-blocking
  while (core_stepper_busy(&axis)) {
      core_watchdog_feed();
  }
Rates are microsteps, not motor steps
The module does not know the driver’s microstep setting or the motor’s step angle. Everything is in pulses: at 1/8 microstepping a 200-step motor is 1600 pulses per revolution, so set_speed(1600) is one revolution per second.

Wiring

STEP must be a pad with a timer output function (a TIMx.y in the tile definition, e.g. Core.ST.L4.1 pad 3 = TIM2.2); DIR and ENABLE are ordinary pads. Both are push-pull. Drivers with opto-isolated inputs (DM320T, DM542T and friends) are common-anode: tie OPTO (or PUL+ and DIR+) to the logic rail and let the pads sink PUL and DIR. Those inputs are specified for 4–5 V; a DM320T steps fine from a 3.3 V Core with 8 µs pulses, but if pulses go missing on another driver put a 74LVC07 or a small FET between pad and input with the + side on 5 V. Logic-level drivers (DRV8428, A4988, Trinamic) connect directly.

The defaults honour the slowest common inputs: STEP idles low and pulses high for 8 µs at the end of each step interval, the low time between pulses is at least 8 µs, and DIR only changes at the start of an interval, at least 8 µs before the next rising edge. Drivers that step on the falling edge, or that read DIR the other way round, are a core_stepper_set_polarity() away; fast inputs can shorten the pulse with core_stepper_set_pulse_us() (which also raises the maximum rate, 1 / (2 · pulse)).

core_stepper_set_enable_pad(&axis, 5, false);   // ENA on pad 5, active high
  core_stepper_enable(&axis, true);
  core_stepper_set_pulse_us(&axis, 2);               // DRV8428: 1 µs is plenty

Moves

move() is relative to the current target, so two quick calls of +160 travel 320. move_to() is absolute. run() holds a signed velocity until told otherwise. All three are non-blocking and retarget a move in flight; a reversal decelerates to the start rate before DIR flips. stop() ramps down, halt() cuts the timer immediately.

core_stepper_run(&axis, 800);               // feed forward at 800 steps/s
  ...
  // stop on the next 160-step boundary ahead of where a ramp-down would land:
  int32_t stop = core_stepper_position(&axis)
               + core_stepper_stop_distance(&axis, core_stepper_speed(&axis));
  core_stepper_move_to(&axis, ((stop + 159) / 160) * 160);

  core_stepper_set_position(&axis, 0);        // homed: redefine here as zero

Position counts every pulse actually issued, in the direction it was issued, so it stays honest across retargets and halts. A halt mid-pulse still counts the step — the driver has already seen the rising edge.

Speed & ramps

The profile is trapezoidal: linear acceleration from the start rate up to the cruise rate, cruise, linear deceleration back to the start rate at the target. Steppers start instantly at a few hundred full steps per second, so a start rate above 1 saves time without losing steps; set it with set_min_speed(). Acceleration 0 disables ramps entirely.

Timing resolution is 1 µs. The step interval and pulse are preloaded one step ahead, so interrupt latency never shortens a pulse or stretches an interval; the ISR only has to finish within one interval. A 32-bit timer (TIM2 on the ST Cores) goes down to 1 step/s, a 16-bit one to about 16 steps/s. The timer must not be shared with a PWM output — the module rewrites its period on every step. At the default 8 µs pulse the ceiling is 62.5 kHz.

Cross-architecture support

The generator is plain timer + GPIO code and builds for every Core. It is verified on Core.ST.L4 driving a StepperOnline DM320T and a NEMA 11 directly from 3.3 V with the default 8 µs pulses; the other Cores are compile-checked.

L0M0+L4M4W5M33H5M33WCH (RISC-V) · Nordic (nRF54) — in development

See the implementation status for the full matrix.

API reference

Lower-level · Tier 1
hal_status_t core_stepper_init(core_stepper_t * h, uint8_t step_pad, uint8_t dir_pad);
Bind a stepper axis to a STEP pad (timer output) and a DIR pad. The timer and channel come from the pad's TIMx.y assignment in config.json, which is also what routes the pin to the timer. Loads conservative defaults: 1000 steps/s cruise, 100 steps/s start, 4000 steps/s^2 ramp, 8 us pulses. The timer is left stopped until a move. Do not share the timer with a PWM output — the module rewrites its period on every step.
hal_status_t core_stepper_init_ch(core_stepper_t * h, TIM_TypeDef * instance, uint8_t channel, uint8_t step_pad, uint8_t dir_pad);
Bind an axis with an explicit timer instance and channel (no coregen lookup). The caller must have configured the STEP pad's alternate function for that channel. Everything else as core_stepper_init().
void core_stepper_set_enable_pad(core_stepper_t * h, uint8_t en_pad, bool active_low);
Add an ENABLE output. The pad is driven to its disabled level immediately; call core_stepper_enable() to power the driver. Pass pad 0 to remove it.
void core_stepper_enable(core_stepper_t * h, bool on);
Drive the ENABLE pad. No effect without core_stepper_set_enable_pad(). Motion is not interlocked: halt before disabling if you care where the rotor ends up.
void core_stepper_set_polarity(core_stepper_t * h, bool step_active_low, bool dir_invert);
Set signal polarities. Call while idle.
void core_stepper_set_pulse_us(core_stepper_t * h, uint32_t pulse_us);
Set the STEP pulse width; the low time between pulses is kept at least as long, which also bounds DIR setup. 8 us (default) satisfies the DM320T's 7.5 us; a DRV8428 is happy with 1 us. The maximum step rate is 1 / (2 * pulse_us).
void core_stepper_set_speed(core_stepper_t * h, uint32_t steps_per_s);
Set the cruise rate for position moves and the cap for run(). Takes effect at the next step; a move in flight ramps to the new value.
void core_stepper_set_accel(core_stepper_t * h, uint32_t steps_per_s2);
Set the acceleration used for every ramp, or 0 to switch instantly between the start rate and full speed (fine for small, unloaded motors at low rates; expect stalls otherwise).
void core_stepper_set_min_speed(core_stepper_t * h, uint32_t steps_per_s);
Set the rate a ramp starts from and stops at. Steppers can start instantly at a few hundred full steps per second; starting from a crawl only wastes time. Clamped to at least 1 (16 on a 16-bit timer).
void core_stepper_move(core_stepper_t * h, int32_t steps);
Move a signed number of microsteps relative to the current target (so two quick calls of +160 travel 320). Retargets a move in flight; a reversal decelerates to the start rate before flipping DIR.
void core_stepper_move_to(core_stepper_t * h, int32_t position);
Move to an absolute microstep position.
void core_stepper_run(core_stepper_t * h, int32_t steps_per_s);
Run continuously at a signed rate, ramping from the current speed. Rate 0 decelerates to a stop (same as core_stepper_stop()). The magnitude is capped at the cruise rate from core_stepper_set_speed().
void core_stepper_stop(core_stepper_t * h);
Decelerate to a stop using the configured acceleration. Position continues to count during the ramp. Idempotent.
void core_stepper_halt(core_stepper_t * h);
Stop instantly: timer off, STEP forced to idle. A pulse whose rising edge the driver has already seen is counted. A loaded motor at speed will lose steps; the position counter reflects the pulses sent.
bool core_stepper_busy(const core_stepper_t * h);
true while pulses are being generated (including a ramp to zero).
core_stepper_state_t core_stepper_state(const core_stepper_t * h);
Current motion state.
int32_t core_stepper_position(const core_stepper_t * h);
Current signed microstep position (pulses actually issued).
void core_stepper_set_position(core_stepper_t * h, int32_t position);
Redefine the current position without moving (homing). Only while idle; the target follows so the next move() is relative to it.
int32_t core_stepper_target(const core_stepper_t * h);
Target position of the current or last move.
int32_t core_stepper_speed(const core_stepper_t * h);
Current signed step rate.
uint32_t core_stepper_stop_distance(const core_stepper_t * h, uint32_t steps_per_s);
Microsteps needed to decelerate from `steps_per_s` to the start rate at the configured acceleration. Useful for planning where a run() will stop, e.g. to land on a pocket boundary.

Generated from core_stepper.htiles@8e95159.