☰
51/STM32步进电机控制实战:驱动选型、脉冲当量与加减速丢步排查
2026/9/29 7:36:34 网站建设 项目流程

大约每隔几天,就会有人在群里发一段十秒钟的视频:步进电机轴头在抖、嗡嗡响,就是不转,配一句"单片机控制步进电机到底哪一步错了"。这个画面我太熟悉了——第一次做的时候我用的是 28BYJ-48 加 ULN2003 驱动板,插错了一相线,从下午三点折腾到天黑,最后发现是排线顺序反了。后来做 42 步进电机,做到要精确走 10000 个脉冲停在导轨中点,再后来接触闭环模组和 FOC 驱动,才慢慢把那些 datasheet 上不会写、教程里也基本不提的东西摸清楚。

这篇内容不打算从"什么是步进电机"讲起,那是课本的活。我想聊的是工程上真正会卡人的几件事:驱动器怎么选档、脉冲当量怎么算、定时器初值怎么推、加减速为什么要写成那个奇怪的样子、丢步了从哪儿开始查。写到这里的代码都是可以直接编译下载跑的,51 和 STM32 各一套。适合三类人看:刚上手单片机做课程设计、毕业设计的学生;做小设备、小工装、自动化夹具的工程师;以及手里有一堆步进电机但一直没跑明白的 DIY 玩家。哪怕你只会点灯,跟着把定时器和 GPIO 这两块吃透,这套东西就能立起来。

1. 先把"能转"和"转得准"分开看:这套系统到底卡在哪

1.1 为什么很少有人用单片机 IO 直接带步进电机

新手最朴素的想法是:单片机一个 IO 输出高低电平,接电机一根线,轮着来不就行了。这个想法在继电器、LED 上成立,在步进电机上会直接把 IO 送走。42 步进电机一相绕组的直流电阻通常在 1~2Ω 量级,标称相电流 1A 到 1.5A,按欧姆定律算,加上 1Ω 左右的绕组,稳态电流就冲着安培去了。而 51 单片机一个 IO 脚的持续灌电流能力也就 10~20mA,STM32 单个 IO 是 20mA 上下(有总量限制),差了整整两个数量级。

除了电流,还有电压问题。步进电机要"转得快",靠的不是给它一个恒定电压,而是靠驱动器用高电压强行把电流推上去,再靠斩波把电流卡在设定值。这个过程里绕组两端会出现远高于电源的反向电压尖峰,裸 IO 面对这种尖峰会直接被击穿。所以从电气上讲,中间那级驱动电路不是可选项,是必需项。

真正需要搞清楚的是:驱动级要做两件事——把弱信号放大成电流,以及把电流的方向按相序来回切换。前者靠功率管,后者靠 H 桥或者多路开关组合。理解了这两件事,后面选驱动器就是选"谁来干这两件事",思路会清楚很多。

1.2 一套完整系统的四个组成部分

我习惯把整套东西拆成四层,排错的时候就按这四层从下往上或者从上往下走,比乱猜快得多。

  • 控制层:51、STM32 或者任何单片机,产出的是脉冲、方向、使能这三类信号,或者四路相序电平。它只管"什么时候发脉冲、发多少个、往哪个方向"。
  • 驱动层:把控制信号翻译成绕组电流。可以是分立 H 桥、达林顿阵列、集成驱动芯片,也可以是独立的驱动器盒子。
  • 执行层:电机本体,包括绕组结构、转子齿数、有没有减速箱、有没有编码器。
  • 反馈层:开环系统里这一层是空的;闭环系统里就是编码器加位置环。

很多人调试失败,是因为把四层混在一起想。比如"电机不转",可能是控制层没发脉冲、驱动层没使能、执行层接线错了、反馈层没到位——四个方向完全不同的原因。习惯性地把系统分层,然后在每层边界上用示波器或者万用表量一下信号在不在,定位速度会快好几倍。这也是我在现场最常用的办法:把手放在驱动器输入端子那里,量 PUL 端有没有跳变,有就往下游查,没有就往上游查,一步砍掉一半可能性。

1.3 步距角、细分、脉冲当量这三个词别搞混

这是新手最容易翻车的地方。步距角是电机本体的物理参数,两相混合式电机最常见的是 1.8°,意思是转子每走一步转过 1.8°,转一圈 200 步。这个数字是刻在转子齿上的,改不了。

细分是驱动器的行为。驱动器内部把一整个步距角再切成若干小份,用两组绕组电流的矢量合成,让转子停在中间位置上。1/8 细分下,一个整步被切成 8 个微步。注意这一步是纯电子行为,电机本体什么都没变。

脉冲当量是系统的参数,等于步距角除以细分数。1.8° 的电机配 1/8 细分,一个脉冲对应 0.225°,转一圈需要 1600 个脉冲。

我见过太多人拿 1.8° 去算丝杆行程,忘了自己设了 1/16 细分,结果机床走的距离差了 16 倍。还有个更隐蔽的:换驱动器的时候顺手把拨码从 1/8 改到 1/16,忘了在程序里改脉冲当量常数,位置就整体偏了一半。所以我的习惯是在代码里把"一圈脉冲数"定义成一个宏,所有运动计算都从它派生,改一处就够。

打个生活化的比方:步距角是楼梯一阶的高度,细分相当于在每阶踏面上贴了一块斜板,把一阶切成几小格。你走一阶还是那一阶,但可以停在这几格的任意一格上。楼梯本身没变,只是停的位置变细了。

2. 驱动方案的档位划分:从 ULN2003 到工业数字驱动器怎么选

2.1 六种常见驱动方案的横向对比

选驱动器不是越贵越好,是要跟电机的绕组结构、电流等级、运动精度要求对上。下面这张表是我这几年实际用过的方案整理,参数取常见值,具体型号查看规格书确认。

方案适配电机细分能力相电流量级单片机接口典型场景
ULN2003 模块五线四相(28BYJ-48)仅整步/半步约 0.2A4 路 IO教学演示、低速机构
L298N 模块两相四线整步/半步1A 上下4 路 IO + PWM老式小车、低成本
DRV8833两相四线靠外部 PWM 近似1.5A/桥4 路 PWM低压小体积、电池供电
A4988 / DRV882542 步进内置 1/16、1/321~2ASTEP/DIR3D 打印、桌面 CNC
工业数字驱动器57、86 机座可达 1/128数安STEP/DIR + 串口工业设备、机床
集成闭环模组42、57内置2~4ASTEP/DIR 或串口不能丢步的场合

这张表里最值得说一句的是 DRV8833。不少人看它有两个 H 桥,就以为能做细分,接上两相四线电机以后发现只能整步走,噪声还大。原因是 DRV8833 内部只做功率放大,没有电流斩波和矢量合成,你给它什么相序它就输出什么。想用它做微步,得自己在单片机里用 PWM 调出两路相位差 90° 的正弦/余弦电流波形,还得加电流采样才能闭环——工作量远超换一颗 A4988。

2.2 什么情况下必须上 STEP/DIR 型驱动器

判断标准很简单:只要你对"走多少个脉冲能对应多长位移"有精确要求,就该用 STEP/DIR 型驱动器。因为这类驱动器把细分数、电流值、衰减模式都在内部处理好了,单片机的任务退化成"发脉冲"和"给方向"两个动作,逻辑极其干净。

反过来,用四路 IO 直接输出相序的做法,只适合两种场合:一是 ULN2003 带 28BYJ-48 这种教学场景;二是你要自己做 FOC 或者研究电机原理,需要直接操控每一相电流。剩下的生产场景,四路 IO 会因为占用引脚多、相序切换时机不好控制、无法细分而变得很难维护。

再补一个实际经验:STEP/DIR 型驱动器的脉冲输入通常是光耦隔离的差分输入,端子写成 PUL+/PUL-、DIR+/DIR-、ENA+/ENA-。这种设计对单片机非常友好,因为信号地和功率地可以分开走,干扰不容易窜回单片机。

2.3 电源、共地与 ENA 极性:最容易被省掉的一环

先说接线方式。这种六端子驱动器有两种典型接法:

共阳接法:PUL+、DIR+、ENA+ 三个正端全部接到 +5V,三个负端分别接单片机 IO。单片机输出低电平时光耦导通,等效于"低电平有效"。

共阴接法:三个负端全部接单片机地,三个正端接单片机 IO,高电平有效。

选哪种?用 51 单片机的话,我强烈建议共阳接法。原因是 51 的 P1、P2、P3 口是弱上拉结构,拉高时输出电流很小(几十微安到一百微安的级别),灌电流能力却不错。共阳接法是让 IO 拉低,走的是灌电流路径,稳得多。用共阴接法的时候你会发现 IO 拉高根本驱动不动光耦,脉冲频率一高就直接不上。

还有一个坑:3.3V 的 STM32 配共阳接法,光耦输入端的正向压降约 1.2V,串的限流电阻如果是按 5V 设计的(比如 270Ω),3.3V 下电流只剩不到 8mA,有些驱动器会认不出来。遇到"接了线但完全没反应",先并联一个 100Ω 电阻试试,或者干脆用 5V 给光耦正端供电。这个细节我在两个项目里踩过,都是折腾半天最后发现是限流电阻的问题。

关于 ENA,很多驱动器默认是使能状态,ENA 悬空就行。有些驱动器的 ENA 逻辑可以通过拨码切换,接反了会出现"一发脉冲电机反而松掉"的诡异现象。排查技巧:先不接 ENA,让驱动器和电机自然处于使能态,只调 PUL 和 DIR,把运动跑通以后再回来处理 ENA。这样能少引入一个变量。

电源这块,控制电源和功率电源要分开走。控制侧用 5V 或 3.3V,功率侧根据电机选 12V、24V 甚至更高。两边的地要在驱动器信号侧单点汇合,不要在多处乱连。另外驱动器电源输入端一定要并一个大电解电容,100μF 到 470μF 都行,作用是在电机减速、换向产生反电动势时给电流一个缓冲,否则电源电压会被瞬间抬高,轻则驱动器报过压,重则炸管。

3. 把脉冲算准:相序、细分与定时器参数的手算过程

3.1 从拍数到相序表:整步、半步、八拍的区别

两相步进电机有两个绕组,标成 A 和 B。每个绕组可以正通电、反通电、不通电。把这些状态按顺序排出来,就是"拍"。常见的有三种:

单四拍:A → B → A' → B'(A' 表示 A 相反向通电),四拍一循环。这种方式同一时刻只有一相通电,力矩最小,但功耗低。

双四拍:AB → A'B → A'B' → AB',两相同时通电。力矩比单四拍大约 40%,是很多驱动器的默认方式。

八拍:A → AB → B → A'B → A' → A'B' → B' → AB',八拍一循环。这就是半步模式,位置分辨率提高一倍。

如果是 28BYJ-48 这种单极五线电机,接线是 A、B、C、D 四相加上公共端,接 ULN2003 的时候顺序千万别凭颜色猜。我见过红、蓝、粉、橙、黄五种颜色的线,不同批次定义还不一样。最靠谱的办法是用万用表电阻档量:公共端和其他四根线的电阻基本相同(几十到一百多欧),四根相线两两之间电阻是公共端阻值的两倍左右。量出来以后再对照顺序排,比看颜色靠谱一百倍。

八拍模式下,28BYJ-48 内部转子一圈是 4096 个半步步,因为内部减速比 1:64。这意味着一圈要发 4096 个脉冲,按 1ms 一个脉冲算,转一圈要 4 秒多,输出轴转速只有 15 rpm 上下。这也是为什么这类电机只能做慢速机构——它本来就不是为速度设计的。

3.2 脉冲当量与转速的换算

把公式写清楚,用的时候直接套:

  • 一圈脉冲数 = 360° ÷ 步距角 × 细分数
  • 转速(rpm)= 脉冲频率(Hz)× 60 ÷ 一圈脉冲数
  • 单脉冲位移 = 丝杆导程 ÷ 一圈脉冲数

以 1.8° 两相电机为例,逐个细分数算下来:

细分数单脉冲角度一圈脉冲数1kHz 脉冲对应转速
11.8°200300 rpm
20.9°400150 rpm
40.45°80075 rpm
80.225°160037.5 rpm
160.1125°320018.75 rpm
320.05625°64009.375 rpm

有了这张表,反过来选细分就容易了。假设你要 300 rpm,用的是 1/8 细分,那么需要的脉冲频率是 300 × 1600 ÷ 60 = 8000 Hz。8000Hz 对单片机来说完全没压力,STM32 用 PWM 直接输出,51 用定时器中断也扛得住(中断周期 62.5μs,12MHz 的 51 一次中断大概 20μs 内能跑完,还有余量)。

但要注意 8000Hz 对应 300 rpm 的机械转速,42 步进电机在这个转速下力矩已经明显下滑了。具体能不能带动负载,还是要实测。我一般会留 30% 的转速余量,先按目标转速的七成去算频率。

3.3 定时器初值的手算过程

先说 51。假设用 12MHz 晶振,工作在 12T 模式,那么一个机器周期是 1μs。定时器 0 用模式 1(16 位定时器),计数范围 0 到 65535。

如果我想让中断每 500μs 触发一次,在中断里翻转 IO,那么 IO 的完整脉冲周期就是 1000μs,也就是 1kHz。初值计算:

计数次数 = 500μs ÷ 1μs = 500 初值 = 65536 - 500 = 65036 = 0xFE0C

所以 TH0 = 0xFE,TL0 = 0x0C。这一步看起来很基础,但我见过有人直接用 65535 - 500 算,差了 1,累积几千个脉冲以后相位就飘了。记住是 65536 减,不是 65535 减。

如果要 2kHz,中断周期变成 250μs,初值 = 65536 - 250 = 65286 = 0xFF06。

STM32 这边用 PWM 输出更省事,公式是:

f_pwm = f_clk ÷ (PSC + 1) ÷ (ARR + 1)

以 STM32F103 的 72MHz 为例,要得到 1kHz:

取 PSC = 71,分频后计数时钟 = 72MHz ÷ 72 = 1MHz ARR + 1 = 1MHz ÷ 1kHz = 1000 ARR = 999

要改速度,只需要改 ARR 值,PSC 不用动。ARR = 499 就是 2kHz,ARR = 1999 就是 500Hz。这个"固定 PSC、只改 ARR"的习惯很重要,因为 PSC 带了缓冲寄存器,中途改会在下一个更新事件才生效,容易出现节奏错乱。

4. 加减速曲线:不丢步的关键在启动那一下

4.1 为什么"线性改周期"不等于"线性加速"

这是我见过的最大误区。很多人做加减速是这样写的:起始 ARR 是 2000,目标 ARR 是 200,加速段一共 200 步,那每一步 ARR 减 9。看起来挺合理,但实际上这个电机的加速度曲线是畸形的。

问题在于频率和周期的关系是倒数。ARR 从 2000 到 200 的过程中,前段频率变化很慢(500Hz 到 555Hz 左右),后段频率变化极快(从 5kHz 冲到 5kHz 之上时,周期的微小变化会导致频率巨大跳变)。如果按"周期线性"来加速,电机在起步段几乎感觉不到加速,到了末段突然被猛推一把,这时候最容易丢步。

正确的做法是让频率随时间线性增长,也就是 f_n = f_0 + a × n。转换成周期就是 T_n = 1 ÷ (f_0 + a × n),是一条双曲线。工程上要在单片机里实时算倒数太费时间,常用三种替代方案:

第一,分段折线逼近。把加速区间分成 8 到 16 段,每段内部用固定的周期增量,段与段之间增量不同,逼近双曲线。计算量小,效果能接受。

第二,预先算好一张周期表放在 Flash 里。上电时或者编译期就算好,运行时只需查表取值。这是最省 CPU 的做法。

第三,用 Austin 那个递推公式近似:c_n = c_{n-1} - 2 × c_{n-1} ÷ (4n + 1)。只需要整数乘除,51 也能跑,精度还不错。

我个人在 51 上一般用查表法,在 STM32 上用 DMA 直接喂表给 ARR 寄存器,连中断都省了。

4.2 梯形加减速的离散实现

梯形加减速就是"加速—匀速—减速"三段。关键在于确定三段的边界条件。给定总步数 N、起始周期 T0、目标周期 Tv、加速段步数 Na,那么:

  • 如果 N ≥ 2 × Na,就是完整的梯形,加速 Na 步、匀速 N - 2×Na 步、减速 Na 步。
  • 如果 N < 2 × Na,就是三角形,实际加速步数取 N ÷ 2,两边对称。

代码里我会维护一个"剩余步数"变量,每次发完脉冲就更新一次周期值。下面这段是 51 上可用的简化版,直接把"周期计数值"当加速量来做线性插值:

#define CNT_SLOW 800 /* 起始半周期计数值,越大越慢 */ #define CNT_FAST 200 /* 目标半周期计数值,越小越快 */ #define ACC_STEPS 300 /* 加速段脉冲数 */ static unsigned int calc_reload(void) { unsigned int cnt; unsigned int d; unsigned int remain; if (g_done < ACC_STEPS) { d = g_done; /* 还在加速段 */ cnt = CNT_SLOW - (unsigned int) ((unsigned long)(CNT_SLOW - CNT_FAST) * d / ACC_STEPS); } else { remain = (g_target > g_done) ? (g_target - g_done) : 0; if (remain < ACC_STEPS) { /* 进入减速段 */ cnt = CNT_SLOW - (unsigned int) ((unsigned long)(CNT_SLOW - CNT_FAST) * remain / ACC_STEPS); } else { /* 匀速段 */ cnt = CNT_FAST; } } return 65536 - cnt; }

这段代码的逻辑是"用剩余步数当作减速的镜像",好处是不用单独维护减速计数器,一份代码管两头。缺点是加速和减速的斜率必须一致。如果实际负载有重力分量(比如垂直升降的丝杆),下行时希望加速慢、减速快,那就得拆成两套参数。

有一点必须提醒:CNT_SLOW 这个值不能随便取大。12MHz 的 51 定时器最大定时是 65536μs,也就是 65ms,半周期超过这个数就装不下了。而且就算装得下,起始频率低于 100Hz 的时候电机会有明显的"咯噔咯噔"感,因为步进电机在极低频下是阶梯式跳动的,不是连续旋转。

4.3 行程太短、步数不够时怎么办

实际项目里经常遇到这种情况:只需要走 50 个脉冲,但加速段本身就要 300 步。这时候不能硬套梯形,会出问题——如果还按 300 步去插值,程序会在第 50 个脉冲时突然从加速中途跳到减速,电机直接被"甩"一下。

处理方式是做一次钳位:

if (g_target < ACC_STEPS * 2) { acc_limit = g_target / 2; if (acc_limit < 4) acc_limit = 4; /* 至少留几个脉冲做缓冲 */ } else { acc_limit = ACC_STEPS; }

也就是说,短行程用三角形速度曲线,加速和减速各占一半。如果行程短到连三角形都放不下(比如只有 3 个脉冲),那就干脆不做加减速,直接按一个中间的固定频率发完。3 个脉冲丢步也没人看得出来。

还有一个经验:短行程运动其实最考验驱动器。因为每次都要经历一次加加速,驱动器如果电流设置偏小,就会在启动瞬间掉步。我之前做一个小夹具,行程只有 0.8mm,反复往返,跑了一晚上以后位置慢慢偏了 0.1mm。后来把驱动器电流从 0.6A 提到 0.9A,问题消失。所以短行程场合,电流该给足就给足,别省。

5. 两套最小可跑代码:51 定时器中断版与 STM32 主从计数版

5.1 51 单片机:定时器翻转 IO 的完整框架

这一套是给 STC89C52、STC15 这类芯片用的,思路是定时器中断里翻转 IO 产生脉冲,同时累加已发脉冲数,走够了就关掉定时器。

#include <reg52.h> sbit PUL = P1^0; /* 脉冲输出,接驱动器 PUL-(共阳接法) */ sbit DIR = P1^1; /* 方向,接 DIR- */ sbit ENA = P1^2; /* 使能,接 ENA- */ #define STEPS_PER_REV 1600 /* 1.8°电机 + 1/8细分 */ volatile unsigned int g_target = 0; /* 本次目标脉冲数 */ volatile unsigned int g_done = 0; /* 已发脉冲数 */ volatile unsigned char g_level = 0; /* 当前脉冲电平 */ volatile unsigned char g_busy = 0; /* 运行标志 */ #define CNT_SLOW 800 #define CNT_FAST 200 #define ACC_STEPS 300 static unsigned int calc_reload(void) { unsigned int cnt; unsigned int d; unsigned int remain; if (g_done < ACC_STEPS) { d = g_done; cnt = CNT_SLOW - (unsigned int) ((unsigned long)(CNT_SLOW - CNT_FAST) * d / ACC_STEPS); } else { remain = (g_target > g_done) ? (g_target - g_done) : 0; if (remain < ACC_STEPS) { cnt = CNT_SLOW - (unsigned int) ((unsigned long)(CNT_SLOW - CNT_FAST) * remain / ACC_STEPS); } else { cnt = CNT_FAST; } } return 65536 - cnt; } void Timer0_Init(void) { TMOD &= 0xF0; TMOD |= 0x01; /* 定时器0,模式1,16位 */ ET0 = 1; EA = 1; TR0 = 0; /* 先不启动 */ } void Timer0_ISR(void) interrupt 1 { unsigned int reload; if (g_level == 0) { PUL = 1; g_level = 1; } else { PUL = 0; g_level = 0; g_done++; if (g_done >= g_target) { TR0 = 0; g_busy = 0; return; } } reload = calc_reload(); TH0 = (unsigned char)(reload >> 8); TL0 = (unsigned char)(reload & 0xFF); } void step_move(unsigned int steps, unsigned char dir) { unsigned int reload; if (g_busy || steps == 0) return; DIR = dir; g_target = steps; g_done = 0; g_level = 0; g_busy = 1; reload = calc_reload(); TH0 = (unsigned char)(reload >> 8); TL0 = (unsigned char)(reload & 0xFF); TR0 = 1; } void main(void) { unsigned int i; ENA = 0; /* 共阳接法下拉低使能 */ Timer0_Init(); while (1) { step_move(STEPS_PER_REV, 1); /* 正转一圈 */ while (g_busy); for (i = 0; i < 60000; i++); /* 粗延时,让机械停一下 */ step_move(STEPS_PER_REV, 0); /* 反转一圈 */ while (g_busy); for (i = 0; i < 60000; i++); } }

这段代码有几个容易被忽略的地方。第一,g_target、g_done、g_busy都加了volatile,因为它们在中断和主循环之间共享,不加的话编译器可能把它们优化到寄存器里,主循环永远读不到更新值。第二,g_done是 16 位变量,主循环里读它的时候可能正好被中断改了一半,严格来说需要关中断保护,但在这个场景下只用于判断g_busy,影响不大;如果用在关键判断上,记得先EA = 0读出来再EA = 1。第三,中断里执行了乘除法,12MHz 的 51 做一次 32 位除法大约要几十微秒,所以脉冲频率上限大概在 5kHz 左右,再高就得改成查表。

5.2 STM32:用主从定时器做硬件级脉冲计数

STM32 的优势在于脉冲可以完全由硬件产生,CPU 只在运动结束时被唤醒一次。思路是:TIM3 输出 PWM 当脉冲源,TIM2 配置成外部触发从模式,把 TIM3 的每次输出上升沿当作计数脉冲。这样 TIM2 的计数器值就等于已发脉冲数,精度是硬件级的。

#include "stm32f10x.h" void Stepper_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 | RCC_APB1Periph_TIM3, ENABLE); /* PA6 -> TIM3_CH1 脉冲输出 */ gpio.GPIO_Pin = GPIO_Pin_6; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); /* PA7 -> DIR, PA4 -> ENA */ gpio.GPIO_Pin = GPIO_Pin_7 | GPIO_Pin_4; gpio.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOA, &gpio); /* TIM3:72MHz / 72 = 1MHz 计数时钟,ARR=999 -> 1kHz */ tb.TIM_Prescaler = 72 - 1; tb.TIM_Period = 1000 - 1; tb.TIM_ClockDivision = TIM_CKD_DIV1; tb.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &tb); oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_Pulse = 500; /* 50% 占空比 */ oc.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &oc); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); /* TIM2:从模式,TIM3 的 OC1REF 每来一次就加一 */ tb.TIM_Prescaler = 0; tb.TIM_Period = 65535; TIM_TimeBaseInit(TIM2, &tb); TIM_SelectInputTrigger(TIM2, TIM_TS_ITR2); /* ITR2 对应 TIM3 */ TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_External1); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_EnableIRQ(TIM2_IRQn); }

这里最容易配错的是TIM_TS_ITR2。STM32F1 系列里,TIM2 的四个内部触发源分别是 ITR0 对应 TIM1、ITR1 对应 TIM8、ITR2 对应 TIM3、ITR3 对应 TIM4。这个映射表在参考手册的"定时器内部触发连接"那张图里,不同系列的对应关系不一样,F4 和 F1 就不完全相同。换芯片的时候一定要重新查这张表,不要照抄代码。我第一次在 F4 上移植这段代码,就是因为 TS 配错,TIM2 一动不动。

除此之外,TIM2 是 16 位计数器,最多只能记 65535 个脉冲。1/8 细分下大约 41 圈。超过这个数就得在更新中断里软件累加,或者在运动开始前把计数器清零、结束后读值。这个限制在做长行程的时候必须考虑。

5.3 封装成"走 N 步"的可复用接口

把代码写成上面的样子,只能算是跑通。要真正用在项目里,我会再包一层状态机:

typedef enum { ST_IDLE = 0, ST_ACC, ST_RUN, ST_DEC, ST_DONE } StepState_t; typedef struct { int32_t target; /* 目标脉冲数,带符号 */ int32_t done; /* 已发脉冲数 */ uint16_t v_start; /* 起始周期计数值 */ uint16_t v_max; /* 最高速周期计数值 */ uint16_t acc_steps; /* 加速段步数 */ uint8_t state; uint8_t dir; } Stepper_t; void Stepper_Tick(Stepper_t *s) { uint16_t arr; int32_t remain; if (s->state == ST_IDLE || s->state == ST_DONE) return; s->done++; if (s->done >= s->target) { s->state = ST_DONE; /* 关闭 PWM 输出 */ TIM_Cmd(TIM3, DISABLE); return; } remain = s->target - s->done; if (s->done < s->acc_steps && remain > s->acc_steps) { s->state = ST_ACC; arr = s->v_start - (uint16_t)((uint32_t)(s->v_start - s->v_max) * s->done / s->acc_steps); } else if (remain <= s->acc_steps) { s->state = ST_DEC; arr = s->v_start - (uint16_t)((uint32_t)(s->v_start - s->v_max) * remain / s->acc_steps); } else { s->state = ST_RUN; arr = s->v_max; } TIM3->ARR = arr; /* 预装载,下个周期生效 */ TIM3->CCR1 = arr >> 1; /* 保持 50% 占空比 */ } void Stepper_Move(Stepper_t *s, int32_t steps, uint8_t dir) { if (steps < 0) { dir = !dir; steps = -steps; } if (steps == 0) return; GPIO_WriteBit(GPIOA, GPIO_Pin_7, dir ? Bit_SET : Bit_RESET); s->target = steps; s->done = 0; s->dir = dir; s->state = ST_ACC; /* 提前把第一次的周期设置好,避免启动时用旧值 */ TIM3->ARR = s->v_start; TIM3->CCR1 = s->v_start >> 1; TIM_Cmd(TIM3, ENABLE); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); }

这样封装之后,上层调用就是Stepper_Move(&motor, 1600, 1)走一圈,然后轮询motor.state == ST_DONE判断是否到位。加减速的所有逻辑都藏在Stepper_Tick里,上层完全不用管。

要注意TIM3->ARR是带预装载的,写进去以后要等当前周期结束才生效。这意味着实际发出的脉冲周期永远比设定值滞后一拍,在加减速末段会有细微偏差。对定位精度要求高的场合,可以在运动结束后再补发几个脉冲做修正,或者干脆把加减速段设计得保守一点,让末段速度慢下来。

6. 丢步、啸叫、发热:一份自查清单和完整排查链路

6.1 从"完全不转"开始的排查顺序

电机不转的时候不要瞎试,按下面的顺序走,每一步都是二选一,最多五步就能定位。

第一步:量脉冲。把示波器或者万用表(频率档)接到驱动器 PUL 输入端子上,看有没有跳变。如果没有,问题在单片机侧,去查定时器有没有启动、IO 配置对不对、共阳接法是不是漏了 5V。如果有跳变,往下走。

第二步:量方向。给 DIR 一个固定电平,看电机转向是否和预期一致。有些驱动器 DIR 极性反了,程序里写 1 实际转反,这时候改程序或者改接线都能解决,但必须确认清楚。

第三步:查接线。两相四线电机有四根线,正确接法是 A+、A- 一对,B+、B- 一对。判断同一相的方法还是万用表:同一相的两根线之间电阻是几十欧到几欧,不同相之间是开路。如果接错成 A+ 和 B+ 一对,现象就是"抖但不转"。

第四步:看使能。有些驱动器不使能的时候绕组完全断电,电机轴是自由状态,用手能轻松转动。如果上电后手能转,说明没使能。检查 ENA 电平和拨码。

第五步:降频。前面四步都过了,还是不走或者只嗡嗡响,把脉冲频率降到 100Hz 试试。如果低频能走,高频不走,那就是加减速没做或者电流不够,属于另一类问题。

我把这几步做成了一张表,现场可以直接对着查:

现象最可能原因验证方法处理
完全不动,无声音无脉冲或未使能频率档量 PUL 端查定时器、查 ENA
抖动、嗡嗡响不转相序接错或一相开路万用表量两两通断重新分相接线
低频能走,高频丢步缺加减速或电流不足手动降频测试加梯形加减速、调大电流
跑一段时间位置偏移累积丢步空载跑 100 圈复测提高电流、降速、检查机械阻力
发脉冲反而松轴ENA 逻辑接反摘掉 ENA 测试改接法或改逻辑
驱动器外壳烫手电流过大、散热差手摸壳温降电流、加散热片、设保持电流

6.2 偶发丢步怎么定位

"偶尔丢一步"比"完全不转"难查十倍,因为它不可复现。我一般用三个办法交叉验证。

第一个办法是标记法。在电机轴和固定支架上各画一条线,跑 100 圈回到原点,看两条线是否重合。如果差了,说明确实丢步。再跑 200 圈,如果偏差是同比例增加,说明是每个循环都丢一点;如果偏差不变,说明是某一次启动时丢的。

第二个办法是空载法。把负载摘掉,空载跑同样的程序。空载不丢、带载丢,那要么是电流不够,要么是机械阻力太大或者有卡点。我遇到过一次是丝杆上有一颗铁屑,每转几圈就卡一下,找了一个多小时。

第三个办法是降速法。把最高速降到原来的 60%,加减速段拉长一倍。如果丢步消失,那基本可以确定是速度规划太激进。这个方法虽然不优雅,但最快能确认边界。

还有一个偏门但很好用的技巧:在运动结束以后,让电机反向慢速走一小段,看看有没有空程。开环步进电机丢步以后不会报警,但反向的时候会先有一段"空转"才咬合。用眼睛看轴、用手摸联轴器,都能感觉到这个空程。这个方法比仪器还直观。

6.3 共振、啸叫与噪声的处理思路

步进电机的啸叫分两类。一类是低频共振,发生在 100Hz 到 300Hz 的脉冲频率区间,表现为电机整体抖动、声音发闷。另一类是高频电磁噪声,来自斩波频率,表现为持续的高频细声。

低频共振的处理办法有三个:一是提高细分数,把脉冲频率推到共振区之外;二是在加减速曲线上"跳过"共振区,让加速段快速通过这一段;三是加装阻尼器或者在联轴器上做弹性处理。我一般先用提高细分,成本最低。原来 1/4 细分跑 200Hz 正好在共振点,改成 1/16 以后同样的机械转速对应 800Hz,直接避开了。

高频电磁噪声基本没办法彻底消除,它是驱动器斩波工作时的正常产物。用静音型驱动芯片(带特殊斩波算法的型号)能明显改善,代价是价格高一些、低速力矩略有下降。做桌面设备、医疗设备这类对噪声敏感的场合值得投入;做工业设备就直接忽略。

还有一种是机械结构的谐振,比如钣金外壳太薄,电机一转整个机器跟着嗡嗡叫。这种不是电气问题,加橡胶垫、加配重、改支架厚度都能解决。我从一堆没用的经验里总结出来一条:先把电气调好,再处理机械噪声。反过来做的话,你可能花了半天加橡胶垫,最后发现根本原因是细分太低。

7. 再往前走一步:编码器闭环、FOC 与集成模组的取舍

7.1 编码器闭环:把"以为走到了"变成"确实走到了"

开环步进电机最根本的问题是它不知道自己走了多少。程序发出去 1600 个脉冲,它可能只走了 1598 步,而且这个差值会累积。闭环就是加一个位置传感器,实时把实际位置读回来。

常用的编码器有两类。一类是磁编码器,比如基于霍尔或者磁阻原理的角度传感器,装在电机轴尾端,输出绝对角度,分辨率 12 到 14 位比较常见。另一类是增量式光电编码器,输出 A/B 正交脉冲加 Z 相,分辨率取决于线数和倍频。

加闭环以后控制结构会变成两层:

  • 位置环:比较目标位置和实际位置,输出速度指令。
  • 速度/电流环:驱动器内部把速度指令转成相电流。

STM32 上做位置环,200Hz 到 1kHz 的更新率就够了,用定时器中断跑一个比例积分控制器,算出下一段的脉冲频率或者直接给驱动器的模拟量输入。如果是用 STEP/DIR 型驱动器,闭环的意义在于"丢步以后能发现",可以在运动结束后比对编码器读数,发现有偏差就补发脉冲。这种做法叫"事后补偿",实现简单,在多数场合够用。

7.2 FOC 驱动步进电机解决了什么问题

传统步进电机是"开环换相",相电流是方波或者接近方波,电流和转子磁场之间没有严格对齐,所以转矩脉动大、噪声大、效率低、高速力矩掉得快。

FOC 的思路完全不同:用电流传感器实时采样两相电流,通过坐标变换把交流量转成旋转坐标系下的直流量,然后分别控制"产生力矩的分量"和"产生磁通的分量",最后再变换回去输出给 H 桥。这个过程需要转子的实时角度,所以必须有编码器。

效果上,FOC 驱动的步进电机有几个明显变化:低速几乎听不到声音,转矩脉动小很多,高速时力矩下降更平缓,而且可以做到电流按需分配,发热明显降低。代价是控制算法复杂、需要电流采样电路和角度传感器、调试参数多。

我的建议是分场景:如果只是做个治具、走个直线,普通 STEP/DIR 驱动器足够了;如果做的是需要静音、需要高效率、需要高速的场合,比如精密仪器、桌面设备的主轴,FOC 值得投入。中间还有个折中方案——用集成度高的闭环模组,把驱动器、编码器和控制算法都封在一个壳子里,单片机只负责发指令。圈里常说的那种集成闭环步进就是这类东西,对于不想碰算法的人来说,是省事的选择。

7.3 集成模组的取舍:省事与可控性之间

集成模组的好处很直接:接线从十几个端子简化成电源加通信线,闭环算法现成,参数通常有上位机软件可视化调。做样机的时候,几天就能把机构跑起来。

但它的代价也要看清楚。第一,控制逻辑被封装了,你想改加减速曲线、想自定义补偿策略,往往只能在厂家给的参数里挑,自由度有限。第二,出问题的时候排查链路变长,你分不清是模组内部的问题还是自己上位机的问题。第三,成本和供货周期受供应商影响。

我自己的做法是:批量生产、需求明确、不需要深度定制的场合,用集成模组;需要自己掌握运动曲线、需要和上位机深度耦合的场合,用分离方案。两者不冲突,同一个项目里也可以混用——主轴用集成模组,辅助轴用驱动器加分立电机。

最后分享一个我做方案时的小习惯:先把运动需求写成一串数字,再选硬件。包括:一圈脉冲数、最高转速、最大加速度、单次行程、连续工作时长、定位精度、允许噪声。把这七个数字列清楚,选驱动器、选电机、选细分、选控制方式,答案基本自己就浮出来了。反过来先买硬件再凑程序,通常会在某个环节卡住,然后推翻重来。

这个内容后面还能继续扩:多轴联动时的插补算法、用内部 Flash 存储的参数表、用 RS485 组网批量下发运动参数,都是可以接着往下做的方向。等你把单轴的脉冲、加减速、闭环都跑顺了,多轴只是把同一套逻辑复制几份,加上一个统一的节拍调度而已——真正难的那部分,其实在第一次把电机转起来的那天就已经跨过去了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询