一句话结论:步进电机的转速不能突变——启动频率要按「转矩-频率曲线」来,所以必须做加减速规划;短距离还得用三角形而非梯形。
一、完整工程下载
压缩包内含全部源码、platformio.ini、Makefile、README.md,解压即用,不需要额外配置。
下载 stepper-accel.zip (9.1 KB,共 7 个文件)
.gitignore
Makefile
README.md
include/
stepper.h
platformio.ini
src/
stepper.c
test/
test_stepper.c步进电机丢步的三个真实原因
- 启动太快:静止启动时能带动的最高频率叫「启动频率」(pull-in rate),
通常只有运行频率的 1/5~1/10。直接以高速启动 → 转子跟不上磁场 → 丢步或啸叫。
- 急停过冲:高速运行时突然停止,转子带着惯性冲过去,也会丢步。
- 加减速斜率超过转矩能力:加速度
a对应转矩T = J·a,
超过电机的动态转矩就必然丢步。
所以核心就一件事:让速度平滑地变。
梯形加减速(Trapezoidal)
速度
^ _______________
| / \
| / \
| / \
+----+---------------------+----> 时间
加速 匀速 减速规划只需要三个量:
v_max:最大速度(步/秒)a:加速度(步/秒²)S:总步数
关键公式:能刹住的距离(从当前速度 v 到 0 需要多少步):
steps_to_stop = v² / (2a)只要 剩余步数 >= steps_to_stop 就还能加速,否则开始减速。判据用代码写就是:
v_target = sqrt(2 * a * steps_remaining); /* 刹不住就降速 */
if (v_target > v_max) v_target = v_max;这一行同时处理了三种情况:长距离(能到 v_max,梯形)、 短距离(到不了 v_max,三角形)、减速段(自然收敛到 0)。
不需要预先算"加速多少步、匀速多少步、减速多少步",也就不会在边界上出错。 这是工业里最常用的增量式规划写法。
步域而非时域
注意上面是在步域里算的,不是时域。每一步更新一次速度, 下一步的定时器周期就是 1/v 秒。好处是不依赖定时器精度、也不怕中断抖动累积误差—— 因为步数永远是对的,速度只会略有波动。
S 曲线(Jerk 限制)
梯形加减速的加速度是突变的,机械上表现为"咯"的一下。 把加速度也做成连续变化的(限制加加速度 jerk)就得到 S 曲线:
速度
^ ____________
| / \
| / \
| / \
+--/--------------------------\--> 时间
^ jerk 段,加速度线性上升代码上就是在梯形的基础上多一层:
/* 目标加速度仍按梯形算,但实际加速度一步步逼近它,每步最多变 j_max/v */
a_actual += clamp(a_target - a_actual, -j_max/v, +j_max/v);
v += a_actual / v; /* dt = 1/v */代价是规划时间变长(同样的行程 S 曲线比梯形慢约 5~15%), 换来的是几乎零振动——3D 打印机、雕刻机、云台都得用它。
工程内容
stepper.c/h:梯形 + S 曲线两种规划器,统一接口
stepper_next_interval_us() 返回「下一步与下一步之间要等多少微秒」
test/test_stepper.c:在电脑上把步全部"走"完,输出真实的速度曲线表,
并验证:步数精确、峰值速度不超限、加速度有界、短距离自动降级成三角形
接到 STM32 上(关键 20 行)
static stepper_t axis;
void TIM3_IRQHandler(void) { /* 步进脉冲定时器 */
if (TIM3->SR & TIM_SR_UIF) {
TIM3->SR = ~TIM_SR_UIF;
STEP_PIN_TOGGLE(); /* 产生一个脉冲(或用 PWM 单脉冲模式) */
uint32_t us = stepper_next_interval_us(&axis);
if (us == 0) {
TIM3->CR1 &= ~TIM_CR1_CEN; /* 走完了,关定时器 */
} else {
TIM3->ARR = us - 1; /* 1MHz 计数 -> ARR 就是微秒 */
}
}
}调试要点
- 先测出你电机的真实曲线:从 100 步/秒 开始每档加 100,
找到不丢步的最大启动频率和最大运行频率,这两个数是所有参数的基础
- 丢步怎么判断:装一个限位开关,走 1000 步回到起点,
不一致就是丢步了;或加编码器闭环(本站有编码器测速一文)
- 细分(microstepping)不是万能:细分提高的是平滑度,
但细分后每步转矩下降,高速时反而更容易丢步
- 加减速参数保守一点:
a取理论值的 60% 是工程常态 - 驱动器要设对电流:电流太小没力、太大发热丢步(失步),
按电机额定电流的 70% 起步
进阶方向
- 用 S 型曲线的解析式(五段式,含恒 jerk 段)做更精确的规划
- 加 闭环步进:编码器反馈 + 位置环,丢步自动补偿
- 多轴直线插补(Bresenham 直线算法 + 速度合成),做简易 CNC
- 与 G 代码解析器结合,就是一个自制的写字机/雕刻机固件
完整代码
Makefile
CC ?= gcc
CFLAGS ?= -std=c99 -Wall -Wextra -O2 -Iinclude
LDLIBS ?= -lm
SRC = src/stepper.c
TEST = test/test_stepper.c
ifeq ($(OS),Windows_NT)
EXT = .exe
endif
BIN = build/test$(EXT)
all: run
$(BIN): $(SRC) $(TEST)
@mkdir -p build
$(CC) $(CFLAGS) $(SRC) $(TEST) -o $(BIN) $(LDLIBS)
run: $(BIN)
@$(BIN)
clean:
rm -rf build
.PHONY: all run cleaninclude/stepper.h
/**
* stepper.h - 步进电机速度规划(梯形 / S 曲线)
*
* 用法:
* stepper_move(&ax, 2000); // 走 2000 步
* while ((us = stepper_next_interval_us(&ax))) { // 每次定时器中断调一次
* toggle_step_pin();
* set_timer_delay_us(us);
* }
*
* 设计要点:规划在「步域」里做,每一步更新一次速度,
* 定时器周期 = 1/v。这样步数永远精确,中断抖动不会累积成误差。
*/
#ifndef STEPPER_H
#define STEPPER_H
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
SP_IDLE = 0,
SP_ACCEL,
SP_CRUISE,
SP_DECEL,
SP_DONE
} step_phase_t;
typedef struct {
/* ---- 配置 ---- */
float v_max; /* 最大速度,步/秒 */
float a_max; /* 最大加速度,步/秒² */
float j_max; /* 最大加加速度,步/秒³;<=0 表示用梯形(不做 jerk 限制) */
float v_start; /* 起始速度(不能为 0,否则第一步的时间是无穷大) */
/* ---- 运行时 ---- */
float v; /* 当前速度(步/秒) */
float a; /* 当前加速度(仅 S 曲线用) */
uint32_t total; /* 本次规划的总步数 */
uint32_t done; /* 已走步数 */
int8_t dir; /* +1 / -1 */
step_phase_t phase;
} stepper_t;
void stepper_init(stepper_t *s, float v_max, float a_max, float j_max, float v_start);
/** 规划一次运动(会清零进度)。steps 可为负表示反向 */
void stepper_move(stepper_t *s, int32_t steps);
/** 立即停止(不清规划,仅把剩余步数改为「必须以当前速度刹停」) */
void stepper_stop_soft(stepper_t *s);
/**
* 走下一步并返回「下一步之前要等多少微秒」
* @return 0 表示运动结束
*/
uint32_t stepper_next_interval_us(stepper_t *s);
#ifdef __cplusplus
}
#endif
#endif /* STEPPER_H */platformio.ini
[platformio]
default_envs = bluepill
[env:bluepill]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = stlink
monitor_speed = 115200
build_flags =
-Wall
-Wextra
-Isrc
lib_ldf_mode = deep+src/stepper.c
#include "stepper.h"
#include <math.h>
#ifndef STEPPER_EPS
#define STEPPER_EPS 1e-6f
#endif
void stepper_init(stepper_t *s, float v_max, float a_max, float j_max, float v_start)
{
s->v_max = (v_max > 0.0f) ? v_max : 1000.0f;
s->a_max = (a_max > 0.0f) ? a_max : 10000.0f;
s->j_max = j_max;
s->v_start = (v_start > 1.0f) ? v_start : 1.0f;
if (s->v_start > s->v_max) {
s->v_start = s->v_max;
}
s->v = s->v_start;
s->a = 0.0f;
s->total = 0;
s->done = 0;
s->dir = 1;
s->phase = SP_IDLE;
}
void stepper_move(stepper_t *s, int32_t steps)
{
s->dir = (steps >= 0) ? 1 : -1;
s->total = (uint32_t)((steps >= 0) ? steps : -steps);
s->done = 0;
s->v = s->v_start;
s->a = 0.0f;
s->phase = (s->total == 0u) ? SP_DONE : SP_ACCEL;
}
void stepper_stop_soft(stepper_t *s)
{
if (s->phase == SP_IDLE || s->phase == SP_DONE) {
return;
}
{
/* 把「剩余行程」改写成「需要的刹停距离」,规划器会自动进入减速段 */
float stop_steps = (s->v * s->v) / (2.0f * s->a_max);
s->total = s->done + (uint32_t)(stop_steps + 1.0f);
}
}
static float clampf(float v, float lo, float hi)
{
if (v > hi) {
return hi;
}
if (v < lo) {
return lo;
}
return v;
}
uint32_t stepper_next_interval_us(stepper_t *s)
{
float remaining;
float v_target;
float a_req;
float a_stop;
float v2;
float dt;
if (s->phase == SP_DONE || s->phase == SP_IDLE) {
return 0;
}
/* 本步的间隔由「进入本步时的速度」决定:先取间隔,再更新速度 */
dt = 1.0f / s->v;
s->done++;
if (s->done >= s->total) {
s->phase = SP_DONE;
return (uint32_t)(dt * 1e6f);
}
remaining = (float)(s->total - s->done);
/* 刹车用的加速度要取保守值:S 曲线下加速度本身还要花时间反向,
* 用 a_max 直接算刹停距离会冲过目标点(实测踩到的坑) */
a_stop = (s->j_max > 0.0f) ? (s->a_max * 0.7f) : s->a_max;
/* 目标速度:既要刹得住,又不超过 v_max */
v_target = sqrtf(2.0f * a_stop * remaining);
if (v_target > s->v_max) {
v_target = s->v_max;
}
/* 步域里速度递推是 v² += 2a,所以「一步内达到 v_target」需要的加速度是:
* a_req = (v_target² - v²) / 2
* 这个式子是整个规划器唯一需要记住的东西——梯形只是把它截断到 ±a_max。 */
a_req = (v_target * v_target - s->v * s->v) * 0.5f;
if (a_req > s->a_max) {
a_req = s->a_max;
} else if (a_req < -s->a_max) {
a_req = -s->a_max;
}
if (s->j_max > 0.0f) {
/* S 曲线:限制加速度每步的变化量(jerk),让加速度也是连续变化的 */
float da_max = s->j_max / s->v; /* jerk 上限 × dt(1/v) */
float da = a_req - s->a;
if (da > da_max) {
da = da_max;
} else if (da < -da_max) {
da = -da_max;
}
s->a += da;
} else {
/* 梯形:加速度直接等于所需值(即速度斜率恒定) */
s->a = a_req;
}
/* 更新速度 */
v2 = s->v * s->v + 2.0f * s->a;
if (v2 < s->v_start * s->v_start) {
v2 = s->v_start * s->v_start; /* 不允许低于起始速度,否则间隔会无限大 */
}
s->v = clampf(sqrtf(v2), s->v_start, s->v_max);
/* 阶段标记(只用于观察) */
if (s->v >= s->v_max - 0.01f) {
s->phase = SP_CRUISE;
} else if (a_req < 0.0f) {
s->phase = SP_DECEL;
} else {
s->phase = SP_ACCEL;
}
return (uint32_t)(dt * 1e6f);
}test/test_stepper.c
/**
* 主机端测试:把步"走"完,打印真实速度曲线并校验各项约束
* gcc -std=c99 -Wall -Wextra -Iinclude src/stepper.c test/test_stepper.c -o build/test -lm
*/
#include <math.h>
#include <stdio.h>
#include "stepper.h"
#define V_MAX 2000.0f
#define A_MAX 20000.0f
#define J_MAX_S 200000.0f
typedef struct {
uint32_t steps;
double time_s;
float peak_v;
float peak_a;
float max_dv; /* 相邻两步速度最大跳变(用来判断 S 曲线是否真的更平滑) */
float min_v;
} result_t;
static result_t run_move(float v_max, float a_max, float j_max, int32_t steps,
int samples)
{
stepper_t sp;
result_t r = {0, 0.0, 0.0f, 0.0f, 0.0f, 1e9f};
float prev_v;
uint32_t n = 0;
uint32_t total = (uint32_t)((steps < 0) ? -steps : steps);
stepper_init(&sp, v_max, a_max, j_max, 100.0f);
stepper_move(&sp, steps);
prev_v = sp.v;
if (samples > 0) {
printf(" 步号 速度(步/秒) 加速度 间隔(us) 阶段(1加速2匀速3减速)\n");
}
for (;;) {
uint32_t us = stepper_next_interval_us(&sp);
if (us == 0u) {
break;
}
r.time_s += (double)us / 1e6;
if (sp.v > r.peak_v) {
r.peak_v = sp.v;
}
if (fabsf(sp.a) > r.peak_a) {
r.peak_a = fabsf(sp.a);
}
if (sp.v < r.min_v) {
r.min_v = sp.v;
}
{
float dv = fabsf(sp.v - prev_v);
if (dv > r.max_dv && sp.phase != SP_DONE) {
r.max_dv = dv;
}
prev_v = sp.v;
}
n++;
if (samples > 0) {
/* 加速段(前 120 步)密采样,匀速段稀疏,末尾减速段再密采样 */
int show = (n <= 120u && (n % 20u) == 0u) ||
((n % 300u) == 0u) ||
(total > n && (total - n) < 14u);
if (show) {
printf(" %6u %11.1f %10.1f %10u %d\n",
n, sp.v, sp.a, us, (int)sp.phase);
}
}
if (n > 1000000u) {
printf(" !! 步数异常,可能死循环\n");
break;
}
}
r.steps = n;
return r;
}
int main(void)
{
printf("===== 步进电机速度规划测试(v_max=%.0f 步/秒, a_max=%.0f 步/秒²)=====\n\n",
V_MAX, A_MAX);
printf("[1] 长距离 2000 步 —— 梯形曲线\n");
{
result_t r = run_move(V_MAX, A_MAX, 0.0f, 2000, 1);
/* 理论时间:加速到 v_max 需要 v/a = 0.1s,走 v²/2a = 100 步 */
double t_acc = V_MAX / A_MAX;
double s_acc = V_MAX * V_MAX / (2.0 * A_MAX);
double t_total = 2.0 * t_acc + (2000.0 - 2.0 * s_acc) / V_MAX;
printf(" 实际步数=%u 用时=%.4f s 峰值速度=%.1f 理论用时=%.4f s 误差=%.2f%%\n",
r.steps, r.time_s, r.peak_v, t_total,
100.0 * (r.time_s - t_total) / t_total);
printf(" 相邻步最大速度跳变=%.1f 步/秒(梯形曲线,跳变集中在段边界)\n", r.max_dv);
}
printf("\n[2] 短距离 100 步 —— 自动降级为三角形(到不了 v_max)\n");
{
result_t r = run_move(V_MAX, A_MAX, 0.0f, 100, 0);
printf(" 实际步数=%u 用时=%.4f s 峰值速度=%.1f(v_max=%.0f,未触及)\n",
r.steps, r.time_s, r.peak_v, V_MAX);
printf(" 理论峰值 = sqrt(a·S) = %.1f\n", sqrt((double)A_MAX * 100.0));
}
printf("\n[3] 同样 2000 步,改用 S 曲线(jerk 限制 %.0f)\n", J_MAX_S);
{
result_t rt = run_move(V_MAX, A_MAX, 0.0f, 2000, 0);
result_t rs = run_move(V_MAX, A_MAX, J_MAX_S, 2000, 0);
printf(" 梯形:用时 %.4f s 峰值加速度 %.0f 相邻步速度最大跳变 %.1f\n",
rt.time_s, rt.peak_a, rt.max_dv);
printf(" S曲线:用时 %.4f s 峰值加速度 %.0f 相邻步速度最大跳变 %.1f\n",
rs.time_s, rs.peak_a, rs.max_dv);
printf(" -> S 曲线慢 %.1f%%,但速度变化平滑得多(跳变降到 %.0f%%)\n",
100.0 * (rs.time_s - rt.time_s) / rt.time_s,
100.0 * rs.max_dv / rt.max_dv);
}
printf("\n[S 曲线速度曲线抽样(每 150 步)]\n");
{
stepper_t sp;
int n = 0;
stepper_init(&sp, V_MAX, A_MAX, J_MAX_S, 100.0f);
stepper_move(&sp, 2000);
printf(" 步号 速度 加速度\n");
while (stepper_next_interval_us(&sp) != 0u) {
n++;
if (n % 150 == 0) {
printf(" %6d %8.1f %9.1f\n", n, sp.v, sp.a);
}
if (n > 100000) {
break;
}
}
}
printf("\n[4] 反向运动 500 步\n");
{
result_t r = run_move(V_MAX, A_MAX, 0.0f, -500, 0);
printf(" 实际步数=%u(取绝对值)用时=%.4f s 峰值速度=%.1f\n",
r.steps, r.time_s, r.peak_v);
}
printf("\n[5] 急停:先加速到最高速,再调用 soft stop\n");
{
stepper_t sp;
uint32_t n = 0;
float v_at_stop = 0.0f;
stepper_init(&sp, V_MAX, A_MAX, 0.0f, 100.0f);
stepper_move(&sp, 100000); /* 距离足够长 */
while (stepper_next_interval_us(&sp) != 0u) {
n++;
if (n == 300u) { /* 第 300 步时按急停 */
v_at_stop = sp.v;
stepper_stop_soft(&sp);
}
if (n > 100000u) {
break;
}
}
printf(" 急停时速度=%.1f 步/秒,总步数=%u(含减速段),末速度=%.1f\n",
v_at_stop, n, sp.v);
printf(" 理论刹停步数 = v²/(2a) = %.0f\n", (double)v_at_stop * v_at_stop / (2.0 * A_MAX));
}
printf("\n[约束校验]\n");
{
result_t r = run_move(V_MAX, A_MAX, 0.0f, 3999, 0);
printf(" 步数精确:%s(%u == 3999)\n", (r.steps == 3999u) ? "是" : "否", r.steps);
printf(" 速度不超限:%s(峰值 %.1f <= %.0f)\n",
(r.peak_v <= V_MAX + 1.0f) ? "是" : "否", r.peak_v, V_MAX);
printf(" 最低速度不低于 v_start:%s(%.1f >= 100)\n",
(r.min_v >= 99.0f) ? "是" : "否", r.min_v);
}
printf("\n===== 以上曲线全部由本机 gcc 实编译实运行产生 =====\n");
return 0;
}实测输出
下面这段输出是把上面的核心算法用 本机 gcc 真编译、真运行得到的(不含任何硬件依赖):
===== 步进电机速度规划测试(v_max=2000 步/秒, a_max=20000 步/秒²)=====
[1] 长距离 2000 步 —— 梯形曲线
步号 速度(步/秒) 加速度 间隔(us) 阶段(1加速2匀速3减速)
20 900.0 20000.0 1139 1
40 1268.9 20000.0 798 1
60 1552.4 20000.0 649 1
80 1791.6 20000.0 561 1
100 2000.0 14999.4 501 2
120 2000.0 0.0 500 2
300 2000.0 0.0 500 2
600 2000.0 0.0 500 2
900 2000.0 0.0 500 2
1200 2000.0 0.0 500 2
1500 2000.0 0.0 500 2
1800 2000.0 0.0 500 2
1987 721.1 -20000.0 1336 3
1988 692.8 -20000.0 1386 3
1989 663.3 -20000.0 1443 3
1990 632.5 -20000.0 1507 3
1991 600.0 -20000.0 1581 3
1992 565.7 -20000.0 1666 3
1993 529.2 -20000.0 1767 3
1994 489.9 -20000.0 1889 3
1995 447.2 -20000.0 2041 3
1996 400.0 -20000.0 2236 3
1997 346.4 -20000.0 2499 3
1998 282.8 -20000.0 2886 3
1999 200.0 -20000.0 3535 3
实际步数=2000 用时=1.0939 s 峰值速度=2000.0 理论用时=1.1000 s 误差=-0.55%
相邻步最大速度跳变=123.6 步/秒(梯形曲线,跳变集中在段边界)
[2] 短距离 100 步 —— 自动降级为三角形(到不了 v_max)
实际步数=100 用时=0.1354 s 峰值速度=1414.2(v_max=2000,未触及)
理论峰值 = sqrt(a·S) = 1414.2
[3] 同样 2000 步,改用 S 曲线(jerk 限制 200000)
梯形:用时 1.0939 s 峰值加速度 20000 相邻步速度最大跳变 123.6
S曲线:用时 1.0970 s 峰值加速度 20000 相邻步速度最大跳变 32.0
-> S 曲线慢 0.3%,但速度变化平滑得多(跳变降到 26%)
[S 曲线速度曲线抽样(每 150 步)]
步号 速度 加速度
150 2000.0 16199.8
300 2000.0 1199.8
450 2000.0 0.0
600 2000.0 0.0
750 2000.0 0.0
900 2000.0 0.0
1050 2000.0 0.0
1200 2000.0 0.0
1350 2000.0 0.0
1500 2000.0 0.0
1650 2000.0 0.0
1800 2000.0 0.0
1950 1763.1 -9677.6
[4] 反向运动 500 步
实际步数=500(取绝对值)用时=0.3439 s 峰值速度=2000.0
[5] 急停:先加速到最高速,再调用 soft stop
急停时速度=2000.0 步/秒,总步数=401(含减速段),末速度=200.0
理论刹停步数 = v²/(2a) = 100
[约束校验]
步数精确:是(3999 == 3999)
速度不超限:是(峰值 2000.0 <= 2000)
最低速度不低于 v_start:是(200.0 >= 100)
===== 以上曲线全部由本机 gcc 实编译实运行产生 =====