步进电机模拟器:失步与转子滞后角
看步进电机的转子如何落后于指令磁场,把它推过 90 度电角度,亲眼看清一台机器如何在毫无察觉的情况下丢失位置。
- 类别
- 嵌入式系统
- 时长
- 20–40 分钟
- 平台
- 浏览器 · Arduino · ESP32
01 / 从这里开始
简介
步进电机并不“迈步”——它是拖着转子跟在一个旋转的磁场后面,而两者之间的夹角决定了这段运动能否到位。这个实验台直接对转子的运动方程做积分:其中没有任何代码去检测失步,所以当滞后角一路越过 90° 的失步极限时,电机会自己失去同步,就像台架上的真实电机一样。
Live lab / Open-loop step integrity
Stepper motor simulator
Command a move and watch the rotor trail the field. Push the acceleration, drop the voltage or add load until the lag angle runs past the 90° pull-out limit — and the motor silently stops arriving.
Upper plot: the dashed line is where the driver thinks the axis is, the solid line is where the rotor actually is. Lower plot: the lag angle between them. The shaded band is where a steady load can sit — past ±90 electrical degrees the torque starts falling instead of rising, so there is no equilibrium out there, and a rotor that keeps going reaches 180° and slips a pole.
- Commanded (what the driver counted)
- Actual rotor position
- Where a load can sit (±90°e)
- Sync lost
- Full steps lost
- 0
- Peak lag
- 25°e
- Position error
- 0.000 mm
- Resolution
- 80 steps/mm
- Pull-out torque
- 0.311 N·m
- Corner speed
- 599 rpm
Every action has a button above. If you prefer the keyboard, focus the plots and press P for the next preset, M for the next microstepping setting, N to toggle the acceleration ramp, R to reset and F for full screen; sliders respond to the arrow keys.
控件
实验台在每次输入时都会把整段运动重跑一遍,所以曲线会跟着你拖动的滑块走,而不用等按下“开始”按钮。也没有什么东西需要开始——这段运动是 30 000 个积分步,你的指头还没挪回来它就算完了。
| 控件 | 它改变什么 |
|---|---|
| 预设 | 五种算好的情形,从一次干净的运动到四种不同的失败方式 |
| 步进频率 | 梯形加速斜坡,或者步进频率直接跳到全速 |
| 距离 / 最高速度 / 加速度 | 你所要求的运动 |
| 电源电压 | 驱动器要把电流灌进绕组所需的电压 |
| 限流 | 你用微调电位器设定的值——它就是峰值扭矩 |
| 细分 | 磁场被分得多细,1/1 到 1/16 |
| 机构 | GT2 皮带(40 mm/转)或 T8 丝杠(8 mm/转) |
| 阻力矩 | 电机轴上的摩擦与负载 |
上图是位置:虚线是驱动认为轴所处的位置,实线是转子实际所处的位置。下图是两者之间的滞后角,稳定的 ±90° 区间做了阴影。
原理
步进电机并不“迈步”。驱动器以增量的方式旋转磁场,转子跟随着它,始终落后某个角度——而它产生的扭矩就是这个角度的正弦:
T = T_peak(ω) × sin(δ)
δ 以电角度度量。一台 1.8° 的电机有 50 个转子齿,因此它的磁路每转重复 50 次,一个整步恰好是 90 电角度。这个换算是整个专题里唯一的算术。
在 −90° 到 +90° 之间,电机相当于一根弹簧:落后越多,拉回力就越大。正是这个稳定区让开环步进电机得以工作,任何稳态负载都必须待在里面。越过 90° 之后正弦就翻转了,于是落后越多,产生的恢复扭矩反而越小,这又让它落得更远——到了 180°,扭矩彻底反向,开始朝下一个磁极拉。转子滑到下一个稳定齿,那是一个电周期——四个整步——之外。
这是两个不同的数字,实验台把它们分得很清楚。90° 是平衡终止的地方;短暂冲过它是可以承受的,这正是整步指令——每一步都是一次 90° 的冲击——不会毁掉机器的原因。180° 则是不可返回的临界点。
有三样东西会让滞后角增大:
- **负载。**在稳速下
sin(δ) = 需求 ÷ 可用。负载为可用扭矩的一半时,滞后角停在 30°,还留有 60° 的裕量。 - **加速度。**让转子动起来要从同一份预算里花掉扭矩,所以滞后在斜坡阶段最严重,而不是在高速段。
- 速度,因为
T_peak本身会随电机转速提高而下降。
最后这一条最不显然。绕组是一个电感,电机转得越快,就越难把电流灌进去再逼出来——感抗随电频率上升,而转子自身旋转产生的反电动势又会吃掉电源电压。结果就是一条扭矩曲线:在拐点速度之前是平的,越过之后开始下滑,而这个拐点由电源电压决定。
这就引出了实验台存在的意义——让两条规则变得显而易见:更大的电流买到扭矩,更高的电压买到速度。
算法
转子是直接积分的。这段代码里没有任何东西去检测失步、去插入失步,甚至不知道失步是可能的——它是正弦定律积分之后的自然结果。
// per 20 us tick
const ideal = sampleProfile(plan, time).position; // mm the planner wants
const command = Math.round(ideal * stepsPerMm) / stepsPerMm; // the driver only does whole microsteps
const lag = rotorTeeth * (commandAngle - theta); // electrical radians
const { torque } = peakTorque(setup, omega); // falls with speed
const friction = loadTorque * Math.tanh(omega / 0.5); // smooth Coulomb, one line
const accel = (torque * Math.sin(lag) - friction - damping * omega) / inertia;
theta += omega * dt + 0.5 * accel * dt * dt;
omega += accel * dt;
有两处细节比看上去更重要。
**在转子看到指令之前,指令已经先被量化到细分栅格上了。**那道阶梯就是激励:整步运行时,每一条指令都是在阻尼极轻的共振点上施加的一次 90 电角度冲击,这正是整步电机啸叫而细分电机不叫的原因。
绕组模型决定了扭矩天花板:
const backEmf = kT * speed;
const impedance = Math.hypot(resistance, rotorTeeth * speed * inductance);
const current = Math.min(currentLimit, Math.max(0, supplyVolts - backEmf) / impedance);
const torque = Math.SQRT2 * kT * current;
这是对斩波驱动器的集总稳态近似,而不是逐相仿真。它能复现真实扭矩曲线的形状,两个工程结论都算得对;只有在最高速端偏保守。
指令轨迹来自本站已有的运动曲线规划器,所以“带斜坡”和“不带斜坡”走的是同一条代码路径,只是加速度上限不同——并没有为失败单独写一条分支。
引导实验
每一个实验只隔离一个变量,其他什么都别动。
1. 斜坡就是整台机器。其他都保持默认值,把步进频率在斜坡和直全速之间来回切换。
| 斜坡 | 直全速 | |
|---|---|---|
| 峰值滞后 | 25°e | 1.2 ms 时越过 90°,到 3.0 ms 就彻底没了 |
| 丢失的整步数 | 0 | 280 |
| 轴最终停在哪里 | 60.00 mm | 4.0 mm |
同样的电机、同样的驱动器、同样的电流、同样的目标。唯一的区别是步进频率是否做了斜坡,而这两者之间的差别就是整段运动的差别。
2. 找出起动频率。设为直全速,然后从 100 mm/s 开始逐步提高最高速度。它能工作,能工作,然后在 200 mm/s 附近的某个地方彻底不工作了——而且不是逐渐劣化。那个阈值就是起动频率,它是真实电机上的真实数值。
3. 电压买到速度。加载12 V 电源、快速运行预设:运动会失速。现在把电源电压提到 24,保持限流不动,看着它跑完。然后退回 12 V,改用限流去救——救不回来。越过拐点速度之后,驱动器本来就没能达到设定电流。
4. 细分买不到精度。把其他都固定,把细分从 1/1 调到 1/16,读两个数:
| 细分 | 步长 | 相对路径的最坏偏差 | 负载引起的稳态误差 |
|---|---|---|---|
| 整步 | 0.200 mm | 0.127 mm | 0.049 mm |
| 1/4 | 0.050 mm | 0.061 mm | 0.044 mm |
| 1/16 | 0.0125 mm | 0.045 mm | 0.044 mm |
跟踪精度改善了大约三倍,然后就停住了。负载误差一点没动。整步时它是 0.044 mm,十六细分数时它还是 0.044 mm——区别在于 0.044 mm 是四分之一个整步、同时又是三个半细分步,所以细分做细之后,同一个误差在驱动器所报告的单位下看起来要糟糕得多。
5. 失步。在速度设得不高的情况下,缓慢提高阻力矩。观察下图中的滞后角朝虚线攀升。它会先停在 30°,再到 50°,再到 70°——然后在滑块只加一小格的幅度里,同步就没了。之所以没有预警区,是因为 90° 之后已经不存在可供它稳定的平衡点。
7. 整步就是一次 90° 的冲击。加载整步运行预设,读出峰值滞后:约 102°e。每一步都指令完整的 90°,靠转子的惯性把它带过去。它仍然能到位——什么都没丢——但整段运动都在一个带载电机承受不起的区域里来回摆荡。这同时解释了为什么整步运行很吵,以及为什么它给负载留下的裕量如此之小。
6. 机构会改变问题本身。在其他都不动的情况下,把机构切换到 T8 丝杠。分辨率从 80 步/mm 变成 400 步/mm,同样的最高速度现在需要五倍的步进频率。
你应该观察到什么
- **失败是悬崖,不是斜坡。**没有任何渐进退化。滞后角平滑地增大到 90°,然后整段运动就凭空消失了。这就是步进电机与舵机式闭环之间最本质的区别——后者在负载下只会变慢。
- **机器永远不知道。**虚线的指令线总是精确地停在目标上,因为驱动器数过了它收到的每一个脉冲。只有实线才说真话,而真实机器上没有这条线。
- **负载误差永远存在。**即使在一次完美的运行中,转子也停在驱动器所说的位置之后几十微米处。这不是故障,而是
sin(δ) = 负载 ÷ 扭矩所要求的。 - **丢失的步数永远是四的倍数。**你试试看能不能造出一个不是的数。
硬件落地
| 这个实验台 | 真实轴 |
|---|---|
| 理想的 50 kHz 步进时序 | 单片机的定时器抖动,以及 Arduino Uno 上大约 10–15 kHz 的上限 |
| 一个刚性质量块 | 皮带拉伸、背隙,以及会变形的机架 |
| 恒定的阻力矩 | 沿轴线变化的摩擦,以及线缆拖曳 |
| 集总绕组模型 | 真实的扭矩曲线,带有定位转矩和中频共振 |
| 完美的电流调节 | 限流设得过高时会热降额的驱动器 |
| 永远不会发热 | 每台电机一旦通电就持续消耗 8 W |
最重要的一条区别换个角度看是最后一行:**真实机器不会给你下图。**你看不到滞后角,因此也看不到自己运行在多大的裕量上。实际的替代办法是故意去找失效点——把速度和加速度加到轴开始跳步为止,然后回退 30% 到 50%,就稳定运行在这个位置。
可以从接线与限流实操开始,用尺子标定每毫米步数,然后在绘图机器人制作里把整套东西组装起来。
源代码
在真实机器上,斜坡不是可选项。下面这个阻塞版本就是多数人第一版草图的样子,也正是会发出嗡嗡声的那一个:
// What not to ship: the step rate jumps from zero to full.
void moveBlocking(long steps, unsigned delayUs) {
for (long i = 0; i < steps; i++) {
digitalWrite(STEP_PIN, HIGH);
delayMicroseconds(2);
digitalWrite(STEP_PIN, LOW);
delayMicroseconds(delayUs); // constant => no acceleration at all
}
}
带斜坡的非阻塞版本。这就是实验台规划的同一个梯形曲线,逐步生成,这样单片机上的其他东西都不会被卡住:
#include <AccelStepper.h>
const float STEPS_PER_MM = 80.0; // 20T GT2 pulley at 1/16 stepping
AccelStepper axis(AccelStepper::DRIVER, STEP_PIN, DIR_PIN);
void setup() {
pinMode(EN_PIN, OUTPUT);
digitalWrite(EN_PIN, LOW); // active low: energise
axis.setMaxSpeed(200 * STEPS_PER_MM); // 200 mm/s
axis.setAcceleration(2000 * STEPS_PER_MM);
}
void loop() {
if (axis.distanceToGo() == 0) {
axis.moveTo(axis.currentPosition() == 0 ? lround(60 * STEPS_PER_MM) : 0);
}
axis.run(); // returns immediately — call it constantly
checkEndstops(); // which is only possible because it does
}
setMaxSpeed 和 setAcceleration 只会给你一条梯形曲线,仅此而已——AccelStepper 里没有 S 形曲线的选项。要做跃度受限曲线,就得自己生成设定点,并由定时器喂给 moveTo。
如果你想知道自己的机器是否已经接近极限,便宜的测试方法是重复性测试:
// Drive a long move out and back, N times, then check whether home moved.
// Anything other than a multiple of four full steps is not step loss.
for (int i = 0; i < 20; i++) { moveTo(LONG_MOVE); runToPosition(); moveTo(0); runToPosition(); }
// now re-home and compare the reported position against zero
假设与硬件差异
转子动力学是诚实的:扭矩正比于 sin(δ) 正是混合式步进电机的工作方式,而滑移行为是对它积分的必然结果,而不是写进代码里的某条规则。这些常数描述的是一台 17HS4401 级别的 NEMA 17——1.8°、1.7 A、2.8 Ω、3.2 mH、保持扭矩 0.44 N·m,轴上还带一个折算质量 300 g 的滑座。
模型做了哪些简化:绕组被集总为一个稳态阻抗和反电动势项,而不是逐相仿真,因此扭矩曲线最末端偏保守。定位转矩——你徒手转动一台不通电的步进电机时感觉到的那点齿槽感——没有建模,中频共振没有建模,驱动器的衰减模式行为没有建模,任何热效应也都没有建模。时序是理想的,因此真实单片机的步进抖动不会出现。
这些都不会改变该拧哪个旋钮。但它们全都是应该去验证一台真实机器、而不是直接相信这一页上某个数字的理由。
电路图
这里所建模的物理接线是:电机到驱动器四根线,驱动器到单片机两根线,外加一个人们总是漏掉的电容:
Arduino 5V ──── VDD
Arduino GND ──── GND (logic) ──┬── GND (motor) ──── PSU −
PSU + (12–24 V) ──── VMOT ──┬──┴── 100 µF electrolytic, close to the board
Pin 3 ──── STEP RESET ──┬── SLEEP
Pin 4 ──── DIR MS1, MS2, MS3 ──── 5 V (1/16)
Pin 5 ──── ENABLE (active low)
Coil A ──── 1A, 1B Coil B ──── 2A, 2B
A4988 零件页上有完整的引脚定义、两种采样电阻版本的 Vref 对照表,以及那个电容为什么不能省。
硬件清单
元器件
- 一台双极步进电机——1.8°、每转 200 整步的那种
- 一个带可调限流的电流调节型驱动器
- 12 V 或更高的电机电源,并且记得去测量它
- 一根皮带轴或丝杠轴,因为步/mm 只是答案的一半
探索关联图谱
哪里用到这个模拟器
以这个实验台为基础的项目、学习路线和教程。
继续搭建
下载资源
完成项目时可以参考本页上的这些资料。可下载的项目包只会在其源文件和版本发布之后才会添加。
常见问题
常见问题解答
为什么我的步进电机会丢步?
因为转子落后被指令的磁场太多了。在 90 电角度以内(一个整步的范围),电机会把自己拉回正轨,所以任何稳态负载都必须待在这个范围内。越过 90° 之后,落后得越多,可用的扭矩就越小;到了 180°,扭矩已经反向,转而朝下一个磁极拉,于是转子滑到一个完整电周期之外的下一个稳定点。有三样东西会把它推过这个极限:大于可用扭矩的负载;陡到让转子动起来所需的扭矩超过剩余扭矩的加速度;以及速度,因为电机转得越快,可用扭矩就越低。这个实验台让你可以分别触发每一种情况。
为什么它每次丢的恰好是四步?
因为磁场的分布每四个整步重复一次。转子失去同步时并不会停在某个任意的角度上——它会一直滑,直到卡进下一个稳定齿,那是一个完整电周期之外的位置;一个电周期是 360 电角度,也就是四个整步。实验台从没有强制规定这一点;它只是对正弦扭矩定律做积分,而丢失的步数自然总是四的倍数。如果你的机器偏差的步数不是四个整步的倍数,那么原因就不是失步。
细分会让我的机器更精确吗?
它让机器更平顺,而不是更精确。在固定负载下把细分选择从 1/1 调到 1/16,分别观察两个数字:相对指令路径的偏差下降了大约三倍,因为你不再每次给转子一个 90 电角度的冲击,它也就不再振铃了。而负载引起的稳态误差一点没动——整步时是 0.044 mm,十六细分时还是 0.044 mm。把步长分细,只是让这个误差以步为单位看起来更大了;对误差本身毫无作用。
要解决失速,应该提高电压还是电流?
取决于它在哪里失速,而实验台会把这个区别展示得一清二楚。在拐点速度以下,驱动器能维持你设定的限流,所以扭矩由电流决定——提高限流即可。在拐点速度以上,驱动器已经无法克服感抗和反电动势,把设定的电流灌进绕组,所以扭矩由电源电压决定,提高限流除了让一切更烫之外什么也改变不了。加载 12 V 预设,看着同一段在 12 V 下失败的运动,在不改电流的情况下于 24 V 时顺利完成。
为什么同一段运动加了斜坡能成功,不加就失败?
因为让转子加速到目标速度需要扭矩,而在运动开始的那一瞬间,转子还静止着,被指令的磁场却已经在狂奔。把步进频率控件切换到“直全速”,其他一切不变,滞后角大约在一毫秒内就越过 90°,到三毫秒时同步就彻底丢了:驱动器数完了整段 60 mm 运动的脉冲,而轴只走到 4 mm。这个极限就是起动频率,也就是从静止状态能指令的最高步进频率,而加速斜坡正是让你保持在它之下的手段。
真实的电机也是这样工作的吗?
转子动力学是精确的——扭矩与滞后角的正弦成正比,这正是混合式步进电机的实际工作原理,而滑步行为是对它积分的自然结果。绕组模型是对斩波驱动器的集总稳态近似,而不是逐相仿真,所以它能复现数据手册中扭矩曲线的形状,并给出正确的工程答案,但在高速尾段偏保守。用它来建立直觉、看清哪个旋钮要紧;在为机器选型时,请使用真实的扭矩曲线。
延伸阅读
参考资料
权威资料,帮助你越过本模拟器有限的教学模型,进一步深入学习。