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();
}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 plentyMoves
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 zeroPosition 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.
See the implementation status for the full matrix.
API reference
hal_status_t core_stepper_init(core_stepper_t * h, uint8_t step_pad, uint8_t dir_pad);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);void core_stepper_set_enable_pad(core_stepper_t * h, uint8_t en_pad, bool active_low);void core_stepper_enable(core_stepper_t * h, bool on);void core_stepper_set_polarity(core_stepper_t * h, bool step_active_low, bool dir_invert);void core_stepper_set_pulse_us(core_stepper_t * h, uint32_t pulse_us);void core_stepper_set_speed(core_stepper_t * h, uint32_t steps_per_s);void core_stepper_set_accel(core_stepper_t * h, uint32_t steps_per_s2);void core_stepper_set_min_speed(core_stepper_t * h, uint32_t steps_per_s);void core_stepper_move(core_stepper_t * h, int32_t steps);void core_stepper_move_to(core_stepper_t * h, int32_t position);void core_stepper_run(core_stepper_t * h, int32_t steps_per_s);void core_stepper_stop(core_stepper_t * h);void core_stepper_halt(core_stepper_t * h);bool core_stepper_busy(const core_stepper_t * h);core_stepper_state_t core_stepper_state(const core_stepper_t * h);int32_t core_stepper_position(const core_stepper_t * h);void core_stepper_set_position(core_stepper_t * h, int32_t position);int32_t core_stepper_target(const core_stepper_t * h);int32_t core_stepper_speed(const core_stepper_t * h);uint32_t core_stepper_stop_distance(const core_stepper_t * h, uint32_t steps_per_s);Generated from core_stepper.h — tiles@8e95159.

