如果第一次在 STM32F103 上做 CoreXY 机构的步进电机运动控制,你很可能也会经历我最初踩过的那个坑:接好驱动板,写好两路电机各自的转动测试代码,高高兴兴跑起来,结果发现龙门架的运动方向完全不符合预期。翻来覆去检查接线、检查方向引脚配置,一切都对,但机构就是不走直线。问题的根源往往不在电机的转动逻辑,而在运动学换算这一层。这次要聊的 Vibecode 项目,核心就是基于 STM32F103 驱动 CoreXY 机构,它真正值得拆开讲的地方不是“怎么让电机转”,而是“怎么让两个固定电机协同输出,把龙门架送到你想要的坐标位置”。
先说一个我自己的判断:这类项目的复杂度不在电机驱动本身,而在两件事上——一是把目标坐标换算成两台电机的步进量,也就是运动学解算;二是用稳定可靠的脉冲时序把步进命令执行出去,也就是定时器、PWM、DMA 和中断的配合。如果只追求电机能转,GPIO 翻转就够;但如果你想得到一个平滑、不丢步、可复用的运动控制流程,就必须把机械联动和脉冲调度一起纳入设计。这也是 Vibecode 这一类项目真正的学习价值所在。
1. 先看懂 CoreXY:不是两个电机各转各的
1.1 两个电机固定在机架上,龙门架靠皮带联动
CoreXY 是一种常见于高速 3D 打印机、激光雕刻机、写字绘图机的二维运动机构。它的显著特点是:两个步进电机固定安装在机架上,不随龙门架移动。X 方向和 Y 方向的运动,是通过同步带的环形绕法和两个电机的旋转组合共同实现的。
在典型的水平布局里,电机 A 和电机 B 的转动会同时影响龙门架的 X、Y 位移。假设两台电机都朝某一方向旋转,龙门架可能沿着 X 方向移动;而如果两台电机一正一反旋转,龙门架就沿着 Y 方向移动。如果两台电机的速度比例不同,龙门架就会走出斜线甚至曲线。
也就是说,电机转动量和龙门架位移之间不是一对一的轴映射关系,而是一个线性组合。这是 CoreXY 和传统十字滑台之间最本质的差别。
很多现成的步进电机控制例程,默认都是“一个电机管一根轴”,比如串口收到 X 指令就让 X 电机走一定步数,收到 Y 指令就让 Y 电机走一定步数。这种模型放在十字滑台上没问题,因为 X 电机确实只带动 X 方向的负载。但放在 CoreXY 机构上,直接套用这种思路就会出错:你给 A 电机发了 100 步,给 B 电机发了 0 步,龙门架不会只走 X 方向,而是会走一个斜向的合成位移。
更麻烦的是,如果两台电机输出脉冲时没有协调好速度比例,皮带会被撕扯,机构会抖动,甚至出现皮带跳齿。所以,在写运动控制代码之前,必须先搞清楚你实际机构的皮带绕法,然后把运动学公式确定下来。这一步不是可选项,而是整个项目的地基。
1.2 为什么“两轴独立”的思路会直接翻车
假设你有现成的两轴独立控制代码,直接把 CoreXY 的两台电机接到 X 和 Y 两个轴上,会出现什么现象?
- 你想让龙门架沿 X 方向移动 100mm,于是让 X 电机走对应步数,Y 电机不动。结果龙门架既没有沿 X 走,也没有沿 Y 走,而是沿斜线移动。
- 你想走一个矩形轨迹,第一段沿 X、第二段沿 Y,结果实际轨迹变成一个平行四边形。
- 更隐蔽的问题是:即使你手动修正了方向,只要两路脉冲的时序没有对齐,龙门架运动过程中就会出现速度抖动,表面上看是直线,放大看其实是锯齿。
这些现象共同指向一个结论:CoreXY 的控制固件必须内建运动学换算层,而不是直接把外部坐标指令当作电机步进指令。换算层放在哪里、怎么实现、怎么测试,正是这类项目最核心的工程内容。
2. 用 STM32F103 做运动控制核心,先想清楚三件事
2.1 F103 为什么适合这个任务
STM32F103 主频 72MHz,Flash 从 64KB 到 512KB 不等,RAM 从 20KB 到 64KB。从算力角度看,它并不算强,但步进电机运动控制恰恰不是高算力任务。步进电机的本质是“按固定频率产生脉冲”,而不是像伺服电机那样做高带宽闭环调节。真正消耗 CPU 的,是运动曲线的实时计算、脉冲频率的精确切换和多轴联动时的同步管理。
F103 在这个场景里的角色更像一个“脉冲调度器”:根据目标坐标、速度和加速度,计算每一小段时间内应该输出多少个脉冲、频率是多少,然后在正确的时间点上更新定时器参数,让硬件自动输出 STEP 脉冲。
从外设资源上看,F103 也比较合适:
- 多个通用定时器可以分别产生多路 PWM 脉冲。
- DMA 控制器能够在 CPU 不介入的情况下更新定时器的频率参数。
- 足够的 GPIO 用于方向、使能、限位和状态指示。
如果你的目标只是驱动常见的 42 步进电机或 NEMA17 电机,搭配 A4988、DRV8825、TMC2208 这类驱动器,F103 的性能余量是足够的。这个前提下,学习成本低、资料多、调试工具齐全的优势就体现出来了。
2.2 驱动器接口:STEP、DIR、EN 和电平匹配
步进电机不能直接接 MCU 的 GPIO,必须经由驱动器。常见的步进驱动器模块,输入侧通常有这几根关键信号线:
- STEP:脉冲输入,每来一个上升沿,驱动器就让电机前进一步。
- DIR:方向电平,高/低电平代表两个旋转方向。
- EN:使能信号,一般低电平有效,控制电机是否通电锁止。
这里有一个非常容易被忽略的问题:MCU 的 GPIO 是 3.3V 电平,而不少步进驱动器模块工作在 5V 逻辑电平下。虽然 A4988、DRV8825 这类模块的 STEP 输入在很多情况下可以识别 3.3V 高电平,但这不是一个可以拍胸脯保证的事。如果信号电平处于临界状态,就会出现“有时候转、有时候不转”的随机问题,排查起来非常费时间。
最稳妥的做法有两种:一是确认驱动器模块明确支持 3.3V 逻辑输入;二是在 STEP、DIR、EN 信号线上加电平转换电路,比如用 74LVC245、ULN2003 或者简单分压处理。这个细节看起来小,但实际上会影响整个项目的稳定性。另外还要注意共地:MCU 的 GND 和驱动器的 GND 必须可靠连在一起,否则电平完全没有参考,脉冲信号会乱掉。
2.3 最小系统的关键点和引脚规划
如果你是购买现成的 STM32F103 开发板,可以跳过最小系统这一步。但如果你打算自己画板,把核心板和控制板做在一起,那么一个可用的最小系统至少需要:
- 3.3V 电源,通常从 5V 用 AMS1117 稳压得到,注意去耦电容要靠近芯片电源引脚。
- 8MHz 外部晶振,配合内部 PLL 倍频到 72MHz。
- 复位电路、BOOT 引脚配置和 SWD 下载调试接口。
关于晶振,有一个常见的选择:直接用内部 RC 振荡器,省掉外部晶振。这在小规模验证、功能原型阶段可以接受,但如果你的目标是稳定输出步进脉冲,我建议还是使用外部晶振。原因很简单:脉冲频率的精度直接由定时器的时钟精度决定,内部 RC 振荡器的精度通常在 ±1% 到 ±2% 左右,温度漂移也更大。在运动中,1% 的频率偏差体现在速度上可能不明显,但如果你之后要做长时间、高精度的轨迹控制,这个偏差会累积成位置误差。
引脚规划上,至少需要:
- 电机 A 的 STEP:接在某个定时器通道上,例如 TIM2_CH1(PA0)。
- 电机 A 的 DIR:任意普通 GPIO。
- 电机 B 的 STEP:接在另一个定时器通道上,例如 TIM3_CH1(PA6)。
- 电机 B 的 DIR:任意普通 GPIO。
- 每轴至少一个限位开关,接带外部中断的 GPIO,并加上拉电阻。
这里特别提醒:STEP 引脚一定不要接在普通中断频繁、或者和串口共用的引脚上。STEP 信号是运动中产生频率最高的信号,任何不稳定的因素都会被放大。
3. 脉冲生成:运动控制真正吃性能的地方
3.1 用定时器 PWM 输出 STEP,而不是 GPIO 翻转
最原始的步进电机控制方式,是在一个定时器中断里翻转 GPIO。这种方式在每秒几百个脉冲的低频场景下没有问题,但一旦频率上到几千甚至上万,问题就来了:
- 中断执行时间不稳定,脉冲间隔抖动明显。
- 两路电机同时运动时,CPU 难以保证两路脉冲的严格同步。
- 频率提高后,中断来不及及时响应会导致步数计数错误。
所以在实际项目中,我更推荐让定时器硬件直接产生 STEP 脉冲。做法是把 STEP 引脚配置为定时器通道的 PWM 输出,然后通过修改定时器的周期(ARR)来控制脉冲频率。定时器一旦启动,脉冲就由硬件自动产生,CPU 完全不用管每一拍脉冲的实现细节。
这样做还有一个额外好处:频率切换非常自然。你只需要在需要变速的时刻更新 ARR 的值,定时器会在下一个周期自动按新频率工作。如果配合 DMA,频率更新可以批量完成,进一步降低 CPU 负担。
3.2 频率、细分、速度之间的换算
步进电机的步距角通常是 1.8°,也就是 200 步/圈。驱动器的细分可以放大这个值,比如 1/16 细分时,每圈需要 3200 个脉冲。
脉冲频率和电机转速之间的关系是:
电机转速(圈/秒) = 脉冲频率(Hz) / (200 × 细分)如果不考虑任何机械传动损耗,电机的转动会通过同步带轮转化为龙门架的直线位移。假设同步带轮的齿距是 2mm,带轮 20 齿,那么带轮转一圈,皮带前进 40mm。于是:
龙门架移动 1mm 需要的脉冲数 = (200 × 细分) / 40这个值就是通常说的steps_per_mm。它取决于电机的步距角、驱动器的细分倍数、带轮齿数和同步带节距。在 CoreXY 机构中,由于两个电机同时参与运动,这个参数还要结合运动学公式来确定实际意义。换句话说,steps_per_mm不是一个凭空设置的参数,而是由整套机械传动链决定的一个物理常数。
实际调试中,很少有人能在第一次就把这个值算得很准。正常的做法是先用估算值让机构移动一段已知距离,然后用尺子测量实际位移,再反推修正steps_per_mm。这个“测量-修正”的过程可能要重复两三次才能收敛。
3.3 进阶:用 DMA 批量更新频率曲线
梯形加减速过程中,脉冲频率需要持续变化。如果匀速段是 10000Hz,加减速段每毫秒都要改一次频率,那么在一个 0.5 秒的加速段里,CPU 要更新 500 次定时器寄存器。如果每一步频率更新都靠中断完成,中断开销叠加起来还是可观的。
更高效的做法是用 DMA。你可以把加减速曲线预先计算成一张频率表,表中每个元素对应一个时间片内的 ARR 值。然后让定时器的更新事件触发 DMA,DMA 自动把频率表里的值逐个写入 ARR。整段加速过程,CPU 只需做一次 DMA 配置,后面就由硬件自动完成。
在 F103 上,这个方案是可行的,而且算是比较经典的用法。需要注意的关键点有三个:
- DMA 通道的优先级要合理设置,避免和其他外设抢占总线导致频率更新延迟。
- 频率表的长度和加减速的时间片大小要匹配,时间片太短会导致表过长,太长会让速度曲线看起来粗糙。
- DMA 传输完成中断要用来判断整段速度曲线是否执行完毕,而不是每一拍都进中断。
如果你还在用“中断里翻转 GPIO”的写法,可以先试着切换到“定时器 PWM 输出 + 中断里改 ARR”的第二阶段,等调通之后再考虑“PWM + DMA”的第三阶段。一步一步来,每个阶段都能让你更清楚地理解步进控制的时序逻辑。
4. CoreXY 运动学:把目标坐标变成电机脉冲
4.1 逆向运动学与正向运动学
运动学要解决的核心问题,是在“龙门架坐标系”和“电机坐标系”之间做换算。
逆向运动学回答的是:如果我想让龙门架移动到坐标(X, Y),两台电机各需要走多少步?在常见的 CoreXY 标准绕线下,这个关系可以写成:
MotorA = X + Y MotorB = X - Y这里 X、Y 是龙门架在各自轴向上的位移量,MotorA、MotorB 是两台电机的步进量。注意,X 和 Y 的具体方向定义、以及 MotorB 前面的正负号,取决于同步带的实际绕法和电机安装方向。不同的机械结构,符号可能变成:
MotorA = X - Y MotorB = X + Y或者:
MotorA = -X - Y MotorB = -X + Y所以我不建议死记某一个公式。更可靠的做法是:先按照你机构的设计意图选一个默认公式,然后在样机上手动推动龙门架,观察两台电机轴分别朝哪个方向旋转,再回到公式里校准符号。
正向运动学反过来,从两台电机的步进量推算龙门架的位置:
X = (MotorA + MotorB) / 2 Y = (MotorA - MotorB) / 2这个换算在调试和位置监控时很有用,但需要注意:在步进电机的开环控制体系里,正向运动学计算出来的位置是“理论位置”,不是“实测位置”。如果发生了丢步,理论位置和实际位置就会产生偏差,而这个偏差开环系统自己是发现不了的。
4.2 固件里的实现结构
在固件里,运动学换算不建议放在每发一个脉冲就执行的路径上,那样计算开销太大。更合理的做法是分层处理:
- 上层接收目标坐标,单位可以是毫米,也可以直接是步数。
- 先把毫米坐标转换成 X 轴和 Y 轴各自的步数,这一步要用到
steps_per_mm。 - 再通过逆向运动学,把 X/Y 步数换算成 MotorA 和 MotorB 的目标步数。
- 根据目标步数和当前位置步数,计算出两台电机各自的方向和脉冲总数。
- 启动定时器,按预定的加减速曲线发送脉冲。
用一段示意性的 C 代码来表达这个过程,大概是这样的:
typedef struct { int32_t current_steps; // 当前绝对步数 int32_t target_steps; // 目标绝对步数 uint8_t direction; // 方向 } MotorState; void corexy_move_to(int32_t target_x_steps, int32_t target_y_steps, MotorState *motor_a, MotorState *motor_b) { int32_t a_steps = target_x_steps + target_y_steps; int32_t b_steps = target_x_steps - target_y_steps; motor_a->direction = (a_steps >= motor_a->current_steps) ? 1 : 0; motor_a->target_steps = a_steps; motor_b->direction = (b_steps >= motor_b->current_steps) ? 1 : 0; motor_b->target_steps = b_steps; }这段代码只是示意,真正的实现还要考虑方向引脚的物理极性、加速减速、限位以及异常中断。但核心思路是清晰的:坐标换算在指令层完成,脉冲生成在定时器层完成,两者之间通过一个“目标步数”的接口连接。
4.3 点到点运动和连续轨迹插补
如果你只需要做点到点定位,比如激光雕刻机从当前点移动到下一个加工点,那么核心任务相对简单:算好目标步数,按“加速→匀速→减速”的曲线发脉冲,结束后停下来。两点之间的实际路径是一条直线,因为运动过程中 X 和 Y 的合成速度比例是恒定的。
但如果你要做画图、写字、激光打标这类连续轨迹操作,情况就不一样了。你需要把整条轨迹拆成很多小线段,逐段执行,并且尽量让段与段之间的速度连续,避免频繁启停。这就是插补的范畴。
在 F103 上做两轴直线插补,常用的是时间分割法:每个很短的周期(比如 1ms)内,根据当前速度和加速度计算这一周期内应该前进的距离,然后分解到 X 和 Y 方向,再转换成两台电机的脉冲增量。由于 CoreXY 是联动机构,这个计算过程发生在运动学换算之后,本质上还是在做脉冲分配。
插补是比点到点高一个台阶的内容。如果你第一次接触 CoreXY,我建议先把点到点移动做扎实,确认方向、步数、加减速都正确,再考虑连续轨迹。否则一旦轨迹出现问题,你很难分辨是运动学错了、脉冲时序不对,还是插补算法本身有 bug。
5. 加减速策略:速度曲线才是稳定的关键
5.1 梯形加减速为什么是起步标配
步进电机不能直接以目标速度启动。原因是电机转矩在高速段会明显下降,如果启动瞬间频率过高,电机会因为跟不上脉冲指令而丢步。丢步一旦发生,位置误差就产生了,而且在开环系统里无法自动修正。
所以运动控制必须引入加减速。最基础的模型是梯形速度曲线,分为三个阶段:
- 加速段:脉冲频率从启动频率线性上升到目标频率。
- 匀速段:保持目标频率稳定运行。
- 减速段:脉冲频率线性下降到停止频率,然后停止。
为什么要设置启动频率?因为在低频段,步进电机的转矩输出才足够大。经验上,可靠启动频率通常在 500Hz 到 2kHz 之间,具体取决于电机参数、驱动电压和负载惯量。如果你在调试中遇到“电机刚起步就卡住”,第一件事不是调大目标速度,而是检查启动频率是否太高,或者加速度是否太大。
5.2 丢步的排查顺序
丢步是步进电机运动控制里最让人头疼的异常。它最直接的表现是“位置不对了”,但原因可能有好几个层面:
- 加速度太大,电机转子的转动惯量跟不上脉冲序列。
- 目标速度超过电机在负载条件下的失步转矩对应的最大速度。
- 脉冲频率抖动,导致电机振动和瞬时失步。
- 驱动器电流设置不当,或者电流衰减模式与电机不匹配。
- 机械问题:皮带过松、滑块阻力大、导轨有异物。
- 电源电压不足,驱动器输入端电压跌落,导致电流输出不稳定。
排查丢步问题时,我建议按从物理到软件的次序来:
- 先手动推动龙门架,感受机械阻力是否均匀。如果有卡顿,先解决机械问题。
- 再检查驱动器的电流设置、细分倍数和供电电压。
- 然后降低加速度,看丢步是否消失。
- 如果还有问题,再降低最大速度。
- 最后才考虑固件里的脉冲生成是否有缺陷。
这个顺序很重要。很多人一遇到丢步就怀疑代码,结果把驱动代码改了一遍又一遍,最后发现是皮带太松或者电源功率不够。先物理、后电气、再软件,能省下大量时间。
5.3 参数调整的实操路径
加减速参数没有一套公式可以适用于所有机构,因为负载惯量、皮带张力、导轨阻力各不相同。但可以按下面的路径逐步逼近:
- 先把加速度设成一个保守值,比如 500 steps/s²。
- 设一条短距离直线运动,比如 50mm,逐步提高目标速度。
- 找到在“不丢步”前提下能稳定达到的最大速度。
- 固定这个速度,再逐步提高加速度,直到出现丢步,然后往回退 20% 左右。
- 每次修改参数后,在机构上做标记,跑固定距离,确认没有累积偏差。
在实际固件里,梯形加减速需要频繁更新定时器的 ARR 值。一个常见的做法是:在每个速度更新周期内,根据当前速度、目标速度和加速度步长,计算新的频率并写入定时器。还要注意一个特殊情况:如果移动距离很短,可能还没加速到目标速度就得开始减速,这时候速度曲线是三角形。判断方法也很简单:计算加速距离加上减速距离,如果超过总距离,就不要试图跑到最高速度,直接采用三角形曲线。
6. 从“能转”到“稳定用”:调试路径和工程化补全
6.1 四条验证台阶
拿到板子和机械结构之后,不建议直接烧录完整固件。我习惯把验证过程拆成四步,每步都有明确的通过标准:
第一步,验证单电机。只接电机 A,发固定数量脉冲,确认方向、电流、速度正常。然后单独测电机 B。
第二步,验证双电机同步。给两台电机发相同频率、相同方向的脉冲,龙门架应该沿一个轴向移动。再改变其中一台的方向,确认另一个轴向的运动。
第三步,验证运动学逆解。给定目标坐标(X, Y),先用上位机或手算得到 MotorA 和 MotorB 的目标步数,再让固件执行,对比实际落点。
第四步,验证加减速和连续轨迹。从短距离直线开始,再到斜线、矩形、圆,逐步提高速度。
每一步都不要跳过。很多看似“神秘”的运动轨迹错误,其实只要在第二步多花十分钟就能定位出来。
6.2 常见问题排查链路
调试过程中最常见的问题,可以按下面的链路来排查:
- 电机不转:先用示波器或逻辑分析仪量 STEP 引脚有没有波形。如果没波形,查定时器是否启动、通道配置是否正确;如果有波形但电机不动,查驱动器的 EN 使能、VDD 电源和电机相线。
- 电机转但方向反了:改固件里对应轴的方向极性,或者对调电机相线。注意:如果两台电机的方向都反了,不能简单把两台都调换,要结合运动学公式统一修正。
- 转到一半卡住:先降低加速度,再降低最大速度。如果降参数后仍然丢步,重点检查机械阻力和驱动器供电。
- 运动方向不对:回到运动学公式,确认 MotorA/MotorB 的符号和你实际机构的皮带绕法一致。
- 速度不均匀、有抖动:检查定时器配置和频率更新时间是否稳定,确认是不是中断频繁导致的脉冲间隔抖动。
- 一启动电机,MCU 就复位:大概率是电源问题。电机启动瞬间电流大,会把系统电压拉低。重点检查 3.3V 稳压器是否余量不足,驱动器的供电是否和 MCU 供电隔离,共地参考是否干净。
6.3 长期运行还要补哪些能力
如果这个项目的目标不只是学习,而是要稳定运行在某种设备里,那么除了基本运动控制,还要补几块“非运动”但决定工程可用性的能力:
- 上电回零:用限位开关建立零点坐标,否则每次上电都不知道龙门架在哪。
- 软件限位和硬件限位双重保护:防止运动越过机械边界。
- 异常处理:读取驱动器的 FAULT 信号、检测限位触发、处理通讯超时。
- 参数持久化:把速度、加速度、细分、
steps_per_mm等参数保存到 Flash,掉电不丢失。 - 日志记录:把运动指令、错误码和时间戳输出到串口,方便后期分析。
- 上位机协议:通过串口或 CAN 接收 G 代码或者自定义指令,把坐标指令解析成运动任务。
这些能力看似和“步进电机控制”没有直接关系,但恰恰是项目从原型走向产品的分水岭。还有一个常见的架构问题:要不要上 FreeRTOS?
我的建议是,如果项目比较简单,可以不用 RTOS,直接在定时器中断和主循环里完成任务调度。如果功能模块比较多,比如同时要处理串口协议、按键显示、运动控制和限位监控,那么可以引入 RTOS 来组织任务。但有一个原则要守住:步进脉冲的生成和运动曲线的实时更新,必须放在定时器中断或 DMA 处理里,不能放在普通 RTOS 任务中。RTOS 任务的调度延迟和优先级切换,无法保证脉冲时序的确定性。
另外一个经常被问到的点是低功耗。对于需要电池供电或是桌面型便携设备,可以考虑 STM32F103 的 STOP 模式来降低待机功耗。但要注意:停机模式下,定时器和 DMA 都会停止工作,运动控制必须显式暂停并保存位置状态,唤醒后重新初始化定时器。如果你只是做固定供电的设备,这个需求可以放一放。
回到最开始说的那个判断:CoreXY 项目的难度在运动学换算和脉冲时序调度,不在“电机转动”本身。当你把坐标变换、加减速曲线、定时器 PWM 和 DMA 这些模块理解透了,你会发现这套方法不只是适用于 CoreXY,也适用于三轴打印机、激光雕刻、绘图仪,甚至更复杂的多轴联动设备。一个机构的硬件决定了它的极限,而控制软件决定了它能不能稳定地接近这个极限。Vibecode 项目的价值