简介:STM32驱动编码器+步进电机是一份基于STM32F103ZET6的嵌入式运动控制示例工程,适合正在学习电机控制、定时器PWM与编码器反馈的开发者。资源以Keil工程形式提供,包含完整C/H源码、编译生成的O/HEX/AXF等固件文件以及工程配置文件,共206个文件,压缩包约4.96MB,清晰展现了定时器3输出PWM控制步进电机、定时器4输入捕获读取增量编码器、USART串口反馈数据的实现框架。目前已有7335人学习下载,属于实用型基础Demo。通过这份资源可系统理解步进电机脉冲频率与方向控制、编码器A/B相脉冲捕获与速度计算,并可直接在开发板上验证或参照移植,为后续运动控制项目提供可复用的代码骨架和调试经验。
1. 项目概述与方案选型
1.1 这个项目到底在做什么
先说清楚这套组合的本质:STM32负责“发号施令”和“听反馈”,步进电机负责“干粗活”——转轴、走位置,编码器负责“打小报告”——把电机实际转到的位置告诉单片机。没有编码器的时候,步进电机是开环的,你发1000个脉冲它理论上转1000步,但实际可能因为负载太大、加速度太猛或者共振丢了几步,走了990步,你自己还不知道。加上编码器之后,系统就能发现误差并把误差补回来,这就是闭环的基本思路。
这个项目适合谁?一种是做毕业设计、电赛、机器人项目的学生,另一种是玩3D打印机、小型雕刻机、云台、机械臂的业余爱好者。凡是需要“转得准”“停得稳”的场景,几乎都绕不开这套组合。
1.2 三种常见架构怎么选
我见过很多人一上来就问“STM32怎么直接驱动步进电机”,这里得先泼一盆冷水:STM32的IO口和定时器输出的是3.3V的逻辑信号,电流只有几毫安,根本带不动步进电机的线圈。步进电机需要的电流动辄几百毫安甚至几安培,必须通过驱动芯片来放大。所以实际架构通常分三种:
| 架构类型 | 反馈方式 | 特点 | 典型应用 |
|---|---|---|---|
| 纯开环 | 无反馈 | 成本最低,可能丢步 | 玩具、对精度要求低的简易设备 |
| 半闭环 | 编码器装在电机轴上 | 补偿电机自身丢步,无法补偿传动间隙 | 3D打印机、小型雕刻机 |
| 全闭环 | 编码器装在负载末端 | 精度最高,能补偿丝杠间隙、皮带打滑 | 高端雕刻机、机械臂、CNC |
做这个项目时,我推荐从半闭环入手,原因很实在:编码器直接装在电机后轴上,安装简单、调试容易、对机械结构改动最小。等你把半闭环玩明白了,再去动全闭环也没有多难,区别只是把编码器换个位置、PID参数重新整定一遍而已。
拿我之前做过的一台小型雕刻机来说,用的是42步进电机加增量式编码器半闭环结构,X轴丝杠导程5mm,编码器500线,理论上2500个计数对应1mm,实测跑100mm的行程误差能控制在0.03mm以内。够用了吧?对绝大多数DIY场景来说,完全够用。
2. 硬件设计与器件选型
2.1 编码器选型:增量式还是绝对式
编码器从原理上分两大类:增量式和绝对式。
增量式编码器输出A、B两路正交方波信号(有些还有Z相零位信号),特点是结构简单、便宜、接口通用,但断电后位置信息丢失,上电需要回零操作。绝对式编码器直接输出一个代表绝对位置的数据(SPI、I2C或串口),上电就知道当前位置,但价格贵不少。像热词里提到的AS5047P、MT6701都属于磁编码器,是绝对式的,用SPI或者I2C接口读取。
对这个项目,我强烈建议先用增量式编码器,原因有三个:
- 接口简单,用STM32的定时器编码器模式就能硬件解码,不占用CPU
- 资料多,网上随便一搜就有大量例程和原理讲解
- 便宜,一个欧姆龙E6B2-CWZ6C级别的编码器也就几十块钱,国产替代更便宜
需要注意,增量编码器还有一个关键参数叫线数(PPR),常见的有100线、200线、360线、500线、1000线。线数越高,分辨率越高,但电气信号频率也越高,对单片机性能和布线抗干扰的要求也随之提高。对步进电机控制来说,200到500线的编码器是性价比最高区间,再高就有点浪费了。
2.2 步进电机驱动芯片:A4988、DRV8825还是TMC系列
驱动芯片决定了电机跑得稳不稳、吵不吵、发热大不大。目前市面主流是这三种,我简单对比一下:
| 芯片型号 | 最大电流 | 细分支持 | 静音表现 | 是否带诊断输出 | 价格区间 |
|---|---|---|---|---|---|
| A4988 | 2A(需加散热) | 最高16细分 | 一般,有噪音 | 无 | 便宜 |
| DRV8825 | 2.5A | 最高32细分 | 一般 | 无 | 便宜 |
| TMC2209 | 2A(峰值2.8A) | 最高256细分 | 非常安静 | 有DIAG引脚 | 中等 |
| TMC5160 | 大电流 | 最高256细分 | 非常安静 | 有 | 贵 |
我的建议是:预算允许就上TMC2209,现在市面上成熟模块也就十几二十块钱,静音效果是质的飞跃。调试阶段你会频繁让电机启停、正反转,用A4988那个刺耳的“吱吱”声能让人心烦意乱,换成TMC2209世界立刻清净。
细分参数也得说清楚。所谓细分,就是把一个基本步进角进一步细分。比如1.8度步距角的电机,16细分就把一步分成16个微步,脉冲分辨率从每圈200步变成每圈3200步。细分提高之后,低速时的振动和噪音明显下降,但细分不是越高越好,256细分时扭矩会明显下降,而且对脉冲频率要求很高,很多单片机在高细分下反而跑不出高速。
2.3 供电与接线:最容易翻车的地方
先说结论:驱动板逻辑电源用STM32的3.3V或5V都行,电机电源必须独立供电。42步进电机额定电压一般是2.4V左右、电流1.5A左右,但你给驱动板的电机电源电压加高到12V甚至24V反而更好,因为驱动芯片是斩波恒流工作,更高的电压能更快地“冲”起线圈电流,高速性能会更好。
我踩过的坑是共地问题。驱动板的GND必须和STM32的GND连在一起,否则STM32输出的脉冲信号没有参考点,驱动板收到的全是垃圾信号,电机要么不动、要么乱动。这个坑很隐蔽,因为线看起来接得都对,就是没共地。另外电机电源的地也会流过大电流,注意线径不要太细,至少用0.5平方以上的线,否则压降大、发热严重。
接线方面还有一个细节很多人不知道:脉冲信号线尽量远离电机线。电机线里的电流是脉动的,会通过电磁感应耦合到脉冲线上,轻则抖动,重则直接丢步。我习惯用屏蔽线做脉冲和方向信号的连接,屏蔽层单端接地,效果很明显。
3. STM32软件实现:核心代码与配置
3.1 定时器PWM:控制速度和方向的基础
步进电机驱动芯片一般有三个关键输入:STEP(脉冲)、DIR(方向)、EN(使能)。每给STEP一个上升沿,电机就走一个脉冲对应的角度;DIR的高低电平决定正传还是反转。
STM32用定时器的PWM输出模式来产生STEP信号,就是把定时器配置成输出比较模式,让某个通道持续输出方波,改变占空比和频率就能控制脉冲的速度和数量。我用的是STM32F103C8T6,经典款,CubeMX配置起来很快。
关键代码是定时器初始化:
// 定时器2通道1,用于输出脉冲到驱动板STEP引脚 // 配置为PWM模式1,脉冲频率可动态调整 TIM_HandleTypeDef htim2; TIM_OC_InitTypeDef sConfigOC; void Step_Init(void) { __HAL_RCC_TIM2_CLK_ENABLE(); htim2.Instance = TIM2; htim2.Init.Prescaler = 72 - 1; // 72MHz分频到1MHz htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 1000 - 1; // 默认1kHz脉冲频率 htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim2); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 500; // 50%占空比 HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); } // 动态设置脉冲频率:freq = 定时器时钟 / (prescaler * period) void Step_SetFreq(uint32_t freq) { __HAL_TIM_SET_PRESCALER(&htim2, 72 - 1); __HAL_TIM_SET_AUTORELOAD(&htim2, 1000000UL / freq - 1); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, (1000000UL / freq) / 2); }注意这里为什么占空比设成50%?因为大多数驱动芯片只关心STEP引脚的上升沿,占空比其实无所谓,但是50%占空比能保证脉冲宽度足够宽,芯片肯定能检测到,而且对高速脉冲来说更稳定。
3.2 正交编码器接口:硬件解码,省心又稳定
增量式编码器的A、B两相信号接STM32定时器的两个输入通道,配置为编码器模式之后,定时器的计数器会自动根据A、B的相位关系加减,完全不需要中断介入,单片机只需要定时去读一下寄存器就行。这就相当于把最麻烦的计数工作交给了硬件外设,CPU可以去忙别的事。
我用的是TIM4的CH1和CH2接编码器的A、B相。CubeMX配置方法如下:
- 将PA6、PA7配置为TIM4_CH1、TIM4_CH2的复用功能
- TIM4的Combined Channels选择Encoder Mode TI1 and TI2
- 设置Counter Period为最大值65535
初始化代码:
TIM_Encoder_InitTypeDef sEncoderConfig; TIM_MasterConfigTypeDef sMasterConfig; TIM_IC_InitTypeDef sICConfig; void Encoder_Init(void) { __HAL_RCC_TIM4_CLK_ENABLE(); htim4.Instance = TIM4; htim4.Init.Prescaler = 0; htim4.Init.CounterMode = TIM_COUNTERMODE_UP; htim4.Init.Period = 65535; // 16位自动重装 htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; sEncoderConfig.EncoderMode = TIM_ENCODERMODE_TI12; // 双边沿计数,分辨率x4 sEncoderConfig.IC1Polarity = TIM_ICPOLARITY_RISING; sEncoderConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC1Prescaler = TIM_ICPSC_DIV1; sEncoderConfig.IC1Filter = 10; // 输入滤波,抗干扰很有用 sEncoderConfig.IC2Polarity = TIM_ICPOLARITY_RISING; sEncoderConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI; sEncoderConfig.IC2Prescaler = TIM_ICPSC_DIV1; sEncoderConfig.IC2Filter = 10; HAL_TIM_Encoder_Init(&htim4, &sEncoderConfig); HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL); }IC1Filter设成10,这个特别重要。编码器信号在实际环境里不是完美的方波,会有毛刺和抖动,如果不加滤波,计数器会出现多余计数。滤波值不是越大越好,太大会滤掉真实的快速变化信号,导致高速时计数丢失。我用下来10这个值在大多数非极端环境下都很稳。
3.3 位置读取与方向判断
编码器数据最简单的方式就是直接读htim4.Instance->CNT寄存器。但要注意一个问题:TIM4是16位定时器,计数器计到65535会回绕到0,这会导致正向和反向计数时数据不连续。解决方法是自己做一个32位的扩展计数:
volatile int32_t Encoder_Position = 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { // 这句话是重点:定时器更新中断会触发通道捕获回调,在这里做溢出补偿 if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_CLEARED) { if (__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim4)) { Encoder_Position -= 65536; } else { Encoder_Position += 65536; } } }实际使用还有一个更省事的方法:不要让计数器回绕。你把Period设成一个很大的值(32位定时器用F103的高级定时器TIM1/8时是32位,或者直接用TIM2/TIM5这种32位定时器),使用过程中只要位置不超过范围内,直接读值就行。F103的TIM2和TIM5都是32位定时器,把编码器接在这两个定时器上,基本不用担心回绕问题,省了很多麻烦。
读取到的计数值和实际物理位移之间的换算关系:
实际位移(mm) = 计数值 / (编码器线数 * 4) * 丝杠导程(mm)这个公式里的*4就是四倍频。500线的编码器,A/B两相在双边沿采样时每圈输出2000个计数。配合5mm导程的丝杠,每个计数对应2.5微米,分辨率已经完全够用了。
4. 位置闭环控制实现
4.1 为什么需要闭环
最初跑开环的时候,我发现一个现象:电机加速太猛或者负载突然变大的情况下,真的是会丢步的。好家伙,定位就错了,这在雕刻或3D打印场景下就是废品一件。加了编码器后,通过比较目标位置和实际位置的差值来动态调整脉冲频率,就能做到精确的位置控制。
控制结构其实不复杂:目标位置和编码器测量位置做差,得到误差,误差经过PID运算生成速度指令,速度指令再换算成脉冲频率输出。
4.2 PID控制算法与代码实现
位置环用PID中的P和I就够了,D项在位置环里通常会放大噪声,除非你的编码器信号特别干净,否则我不建议加。核心代码:
float Kp = 2.5; // 比例系数 float Ki = 0.05; // 积分系数 float Kd = 0; // 微分系数,位置环一般不启用 int32_t target_pos = 50000; // 目标位置 int32_t actual_pos = 0; // 实际位置(从编码器读取) int32_t last_error = 0; float integral = 0; void Position_Control_PID(void) { int32_t error = target_pos - actual_pos; integral += error; if (integral > 10000) integral = 10000; // 积分限幅,防过冲 if (integral < -10000) integral = -10000; float output = Kp * error + Ki * integral; // 输出限幅,防止脉冲频率过高或过低 if (output > 50000) output = 50000; if (output < -50000) output = -50000; if (output > 0) { DIR_Set(GPIO_PIN_SET); // 正转 Step_SetFreq((uint32_t)output); } else if (output < 0) { DIR_Set(GPIO_PIN_RESET); // 反转 Step_SetFreq((uint32_t)(-output)); } else { Step_SetFreq(0); // 停止 } }一个关键细节:当误差很小时,脉冲频率也会变得很低,低到一定程度后步进电机会开始明显一顿一顿地走,甚至干脆不动,这种情况就出现了极限环振荡——电机在目标位置周围来回微动,发出高频的声音。解决方法是加一个死区,当误差绝对值小于某个阈值(比如3个计数)时,直接停止输出:
if (abs(error) < 3) { Step_SetFreq(0); integral = 0; return; }4.3 PID参数整定的实操方法
调PID参数是一个很“玄学”但又有章法的过程,我只分享最实用的方法:
先整定P。把I和D都设成0,P从很小的值开始,观察电机运行情况。P太小,电机响应慢,定位时间很长;P太大,电机会来回振荡。找一个刚好开始临界振荡的位置,把P乘以0.6左右,这就是一个不错的初始P值。
再整定I。加上I之后,观察静态误差能不能消除。I太大,系统会在目标位置附近来回跳动;I太小,消除静差的速度很慢。我习惯用“先加误差,观察回落速度”的方法,如果回落太慢就增加I,如果出现明显过冲就把I减半重试。
最后说一个容易被忽视的点:PID的控制周期和脉冲频率之间的配合。我当时是在定时器中断里每10ms执行一次Position_Control_PID(),如果执行太频繁,PID输出变化太快,会导致电机因为加速度突变而丢步。太慢了又会反应迟钝。10ms是我在实际调试中得到的比较靠谱的周期。
5. 常见问题排查与避坑指南
5.1 编码器数据异常跳变
最常见的编码器问题是读数忽大忽小、非预期跳变。我查过的问题基本是这几类:
- 共地问题:编码器的GND和STM32的GND没有连好,信号参考点不稳定。排查方法很简单,用万用表量一下两个地之间的电压差,正常情况应该是0V或接近0V。
- 信号毛刺:编码器线太长或者离电机线太近,导致信号上叠加干扰。解决方法是加输入滤波,就是我前面说的
IC1Filter = 10,或者接一个100nF的电容到GND,效果很明显。 - 上拉电阻缺失:很多编码器输出是开集电极的,需要加上拉电阻才能输出正常的高电平。一般是接2.2kΩ到10kΩ到3.3V或5V,具体看编码器手册。
5.2 电机丢步和失步
电机丢步的原因很杂,我整理过一个排查清单,按优先级从高到低排列:
| 排查项 | 可能原因 | 解决办法 |
|---|---|---|
| 加速度过大 | 启动时脉冲频率跳变太快,电机惯性跟不上 | 加梯形加减速算法,让频率平滑变化 |
| 电流不足 | 驱动板输出电流设太低 | 调整驱动板的电流调节电位器到电机额定值附近 |
| 速度过高 | 超过电机的最大响应频率 | 降低最高速度实测看是否改善 |
| 细分设置不合理 | 高细分下扭矩下降 | 减少细分或加大电流 |
| 电源压降 | 电机高速时电压被拉低 | 换更大功率的电源或加大电源电容 |
这里我要特别提一下加减速。步进电机不像直流电机那样可以瞬间转到目标速度,它必须有一个平滑的加速过程。我试过直接从静止状态给电机发50000Hz的脉冲,结果电机直接失步、发出刺耳的啸叫。后来在项目中加入梯形加减速:先以较低的初始频率启动,然后每步增加一点频率,直到目标速度,减速阶段再反过来。这个大神级的细节,直接决定你的系统能不能在高速下稳定运行。
5.3 调试工具与开发环境的坑
热词里出现的“error: no stm32 target found”我也不幸遇到过。这个问题90%以上的原因是接线问题或者芯片没有被识别。排查步骤:先确认ST-Link和板子之间SWDIO、SWCLK、GND、3V3四根线有没有接对,然后到IDE里重新扫描设备。如果还不行,试试按住板子上的复位键点击下载,在松手瞬间开始烧录,这招对某些国产板子特别管用。
开发环境方面,如果是新手建议用Keil MDK加STM32CubeMX的组合,配置代码自动生成,少用手写寄存器,不容易出错。用VSCode + EIDE插件也是一条路,个人习惯问题。CH340或CP2102驱动装不上这种问题,基本都是系统驱动签名问题,去芯片原厂官网下最新驱动,别用第三方驱动管家类的软件。
5.4 一劳永逸的排查技巧
最后分享一个我一直用的排查技巧:把编码器的读数实时打印到串口,让电机低速手动转动,同时观察串口数据。如果读数稳定变化且方向正确,说明编码器链路没问题;如果读数异常,用逻辑分析仪抓A/B相信号,一眼就能看出波形质量。这个逻辑分析仪是嵌入式调试神器,一个几十块的USB逻辑分析仪就能搞掂很多疑难杂症。
如果你用的是TMC2209这类UART模式的驱动芯片,还可以通过读取芯片的GSTAT寄存器来确认有没有发生过欠压、过温等故障。这个寄存器能告知你很多看起来“莫名其妙”的电机问题背后的真正原因。
6. 项目扩展方向
整个系统跑通之后,后续能扩展的方向其实很多:
- 把控制逻辑改成串口协议控制,让上位机通过发送指令来控制位置
- 加一个限位开关,实现上电自动回零
- 把PID参数放到Flash里,通过串口命令在线调整,这样调参就不用反复重新烧录了
- 甚至可以直接换成CAN总线驱动模块,做一个多轴的联动控制
我个人在实际操作中最大的体会是:编码器和步进电机这套组合,真正困难的地方不在硬件接线,也不在代码实现,而在于你肯花多少时间在调试和参数整定上。原理图随便找个例程就能画,代码用CubeMX一键生成,但想让系统跑得又稳又准,还是得老老实实一遍一遍测试、记录、调整。这一步过去了,你收获的不只是一个能用的控制系统,还有一套排查问题的思路,这个思路比代码本身值钱得多。
本文还有配套的精品资源,点击获取