简介:面向使用STM32F0系列单片机做步进电机控制的嵌入式开发者,该资料围绕定时器PWM翻转模式生成驱动信号,通过修改CCR比较寄存器值调节占空比,并结合ULN2803驱动24BYJ48四相八拍减速电机,涵盖从外设初始化到主循环更新的完整实现思路。资源共108个文件,以C源码、H头文件、工程配置与备份文件、hex固件及汇编启动文件等类型为主,压缩包仅475KB,便于快速下载查阅。目前已有1270人学习下载,适合正在学习定时器PWM输出、电机驱动或STM32F0开发的初学者借鉴。借助其中的Libraries驱动库、drive电机控制代码和Projects工程文件,读者能对照学习定时器通道配置、四路相位相差90°PWM的实现方式,以及通过CCR值精细调整转速与扭矩的方法,为机器人、3D打印机等运动控制场景提供可直接参考的代码组织方式。 很多人第一次接触步进电机控制,第一反应就是用GPIO翻转电平,靠延时函数去凑脉冲。这办法在低速、单轴、负载稳定的场景下确实能跑,但一旦涉及多轴联动、变速运动或者带载启停,CPU稍微被中断打扰一下,脉冲时序就会乱,电机立刻开始抖、丢步甚至啸叫。STM32定时器配合PWM输出,本质上是把最考验时序的部分从CPU手里剥离出来,交给硬件外设去精确执行,这才是工业控制里真正可靠的做法。
这篇文章会用STM32F103系列(标准库和HAL库都会涉及)来讲清楚整个方案:从定时器PWM的原理、步进电机驱动的匹配方式,到Cubemx配置、代码实现,再到多轴扩展和加减速控制。适合正在做3D打印机、雕刻机、机械臂,或者只是想把步进电机跑顺的开发者参考。
1. 整体设计与方案选型
1.1 为什么是“定时器+PWM”而不是“GPIO翻转”
步进电机的本质是“给一个脉冲走一步”。驱动器(比如A4988、DRV8825、TB6600)接收STEP引脚上的脉冲信号,每检测到一个上升沿或下降沿,就会给电机线圈切换一次电流方向,电机就转过一个固定的角度。
所以控制步进电机,最底层的工作就是精确地产生一串脉冲。用GPIO翻转方式,伪代码大概是这样的:
while(1) { GPIO_SetBits(STEP_PORT, STEP_PIN); delay_us(100); GPIO_ResetBits(STEP_PORT, STEP_PIN); delay_us(100); }看着没毛病,但问题在于:这个循环一旦被中断打断,delay就失真了,脉冲之间的间隔就不均匀。步进电机的运转平稳性恰恰就依赖脉冲间隔的一致性,间隔不均匀轻则噪音变大,重则直接丢步。
用定时器PWM就完全不一样了。PWM波形的产生完全由定时器硬件控制,一旦启动,波形就持续输出,不需要CPU参与。CPU只需要在需要改变速度时,修改一下比较寄存器的值即可。脉冲间隔的精度由定时器时钟决定,误差在纳秒级别,根本不会受到中断、任务调度的影响。
1.2 步进电机控制的核心参数与计算
在开始配置定时器之前,先把步进电机的几个关键参数理清楚,因为这些直接决定PWM频率的设定。
- 步距角:常见的是1.8°,也就是电机转一圈需要200个脉冲。
- 驱动器细分:比如8细分,意味着每个脉冲电机只走1.8°/8 = 0.225°。细分可以提升运动平滑度和定位精度。
- 转速:单位rpm(转/分钟)。
- PWM频率:即每秒的脉冲数。
计算公式如下:
[ PWM频率(Hz) = \frac{转速(rpm) \times 步数/圈}{60} ]
举个例子:1.8°步距角、8细分、目标转速120rpm,那么每秒需要的脉冲数就是:
[ \frac{120 \times (200 \times 8)}{60} = 3200Hz ]
这个3200Hz就是定时器PWM的期望输出频率。有了这个数值,再去配置定时器的预分频和自动重载值就非常清晰了。
1.3 驱动电路:MCU不能直接推电机
必须强调一个基础常识:STM32的GPIO输出能力不足以直接驱动步进电机线圈(相电流通常在几百毫安到几安培)。必须通过驱动器芯片或者驱动电路。
常见的搭配:
- A4988 / DRV8825:3D打印机最常见的步进电机驱动模块,支持细分设置,逻辑电压3.3V/5V兼容,适合直接接STM32。
- TB6600:工业级驱动器,电流大,适合42/57步进电机,需要外部供电(如24V)。
- ULN2003:玩法级驱动板(5线四相步进电机),适合教学演示,不推荐用于实际负载场景。
如果你的项目用的是带驱动器的电机(比如42步进电机+TB6600),那么MCU只需要输出PWM脉冲(STEP)、方向信号(DIR)和使能信号(ENA)。硬件连接很简单:
| 驱动器引脚 | 功能 | 接STM32 |
|---|---|---|
| PUL/STEP | 脉冲输入 | 定时器PWM输出引脚 |
| DIR | 方向控制 | 任意GPIO |
| ENA | 使能/失能 | 任意GPIO(可接GND常使能) |
2. 定时器PWM输出的原理与配置要点
2.1 定时器输出PWM的内部机制
STM32的定时器PWM输出,原理说白了就是一个计数器不停从0数到ARR(自动重载值),同时在CCR(捕获比较寄存器)里设定一个阈值。当计数值小于CCR时,输出高电平;当计数值大于等于CCR时,输出低电平。
把这个过程具象化一下,参考这个配置过程:
- 预分频器PSC:对定时器时钟进行分频,决定计数器的计数频率。
- 自动重载值ARR:决定PWM的周期。ARR越大,频率越低。
- 比较值CCR:决定占空比。CCR = ARR / 2时,占空比为50%。
对于步进电机来说,占空比通常不用太纠结,50%即可。因为驱动器关心的是脉冲沿,不是电平持续时间。只要脉冲宽度满足驱动器的最小要求(一般几微秒),基本都能正常工作。
2.2 PWM频率的计算与参数选择
假设系统时钟72MHz,定时器挂在APB1上,F103的APB1定时器时钟通常是72MHz(看具体时钟树配置)。
目标PWM频率3200Hz,那么可以这样配置参数:
- 先确定预分频PSC,让计数频率不至于太高。比如PSC = 71,那么计数器时钟 = 72MHz / 72 = 1MHz。
- 然后ARR = 1MHz / 3200Hz = 312.5,取整为312。
- 实际频率 = 1MHz / 313 ≈ 3194.9Hz,误差约0.16%,对步进电机来说完全可以接受。
实际项目里,如果希望频率精确,可以反过来调整PSC值,让计数器时钟能整除目标频率。比如1MHz无法整除3200,那就把目标时钟重新选为3.2MHz(PSC = 21.5,不行),或者用72MHz直接计数,ARR = 22500,这样频率就是精确的3200Hz。核心思路是先确定PSC,再凑ARR,两者配合找到最接近的目标频率。
这就引出了一个问题:实际项目中转速会经常变化,难道每次都去改PSC和ARR吗?不用。正常做法是固定PSC,只动态修改ARR——修改ARR会同时改变频率,这个在下一个章节详细说。
2.3 PWM输出引脚配置与重映射注意
STM32的定时器PWM输出,并不是随便哪个引脚都能用的。每个定时器的PWM通道固定映射到特定的引脚。比如F103C8T6上的TIM3_CH1默认在PA6,TIM2_CH1默认在PA0。
但在一些引脚被占用的场景下,可以使用**引脚重映射(AFIO)**功能。比如把TIM3_CH1从PA6重映射到PB4。CubeMX里很好操作:在Pinout视图里,选中PA6后,GPIO设置里找到TIM3_CH1的Alternate Function,然后到System View里启用AFIO,再选择Remap选项即可。
实际项目中要注意的是:
- 重映射后,原有的默认引脚功能会失效,确认目标引脚没有其他用途(例如I2C、SPI、USART)。
- 使用重映射功能,记得在代码里开启AFIO时钟:
__HAL_AFIO_REMAP_TIM3_ENABLE(); - 如果你用的是高密度型号(比如F103ZET6),引脚资源丰富,尽量少重映射,避免不必要的麻烦。
3. 实际操作:Cubemx配置+HAL库代码实现
3.1 用CubeMX快速配置一个PWM输出通道
我用的是STM32CubeMX + HAL库,操作流程如下,假设芯片是STM32F103C8T6,目标电机转速150rpm,步距角1.8°,细分4。
第一步:配置时钟树。
- HSE选择外部晶振(或者用内部8MHz也行,只是精度略差)。
- PLL倍频到72MHz,APB1分频器设置成/2,但要注意,STM32F103的APB1定时器时钟在APB1分频不为1时,是APB1*2,所以定时器时钟还是72MHz。
第二步:选择定时器。
- 在左侧Categories里选择Timers,选择TIM2(或任意你想要的定时器)。
- 在TIM2的Mode中,选择Channel1为PWM Generation CH1。
- 配置参数如下(对应150rpm,4细分):
[ 步数/圈 = 200 \times 4 = 800 ] [ PWM频率 = \frac{150 \times 800}{60} = 2000Hz ]
PSC = 71,计数器时钟 = 1MHz;ARR = 500,PWM频率 = 1MHz / 500 = 2000Hz,完全命中。
第三步:配置GPIO。
- 定时器通道引脚自动被分配到PA0(TIM2_CH1)。
- DIR引脚随便选一个空闲引脚,比如PA4,配置为GPIO_Output,初始电平LOW。
第四步:生成代码。
CubeMX生成的初始化代码已经包含了MX_TIM2_Init() 和 MX_GPIO_Init(),需要自己在用户代码区补充启动PWM的函数。
3.2 HAL库启动PWM的关键代码解析
生成的代码结构里,有一个核心的点需要在while(1)之前完成:启动PWM输出。代码如下:
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);这一行执行之后,PA0口就会持续输出频率2000Hz、占空比50%的PWM波形。电机就会以150rpm的速度持续转动。
如果需要改变转速,只需要修改ARR(或CCR,但实际项目里通常同时修改ARR和CCR保持占空比稳定):
// 设定目标转速,单位rpm void set_motor_speed(TIM_HandleTypeDef *htim, uint32_t channel, uint16_t arr_value, uint16_t ccr_value) { __HAL_TIM_SET_AUTORELOAD(htim, arr_value); __HAL_TIM_SET_COMPARE(htim, channel, ccr_value); }比如想让转速降为75rpm,那么PWM频率应为1000Hz,ARR=1000,CCR可以取ARR/2=500:
__HAL_TIM_SET_AUTORELOAD(&htim2, 1000); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 500);3.3 加减速控制与多轴扩展思路
直接从0rpm瞬间跳到150rpm,电机会因为惯性过大而丢步。实际项目中需要加减速控制。最简单的办法是梯形加减速:
// 梯形加减速简化示例 uint16_t current_arr = 5000; // 起始ARR,对应低速 uint16_t target_arr = 500; // 目标ARR,对应高速 while (current_arr > target_arr) { current_arr -= 10; // 每次减小ARR,即频率增加 if (current_arr < target_arr) current_arr = target_arr; __HAL_TIM_SET_AUTORELOAD(&htim2, current_arr); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, current_arr / 2); delay_ms(1); // 每步间隔,实际需要按电机惯量调整 }加速的每一步间隔(delay_ms参数)和ARR变化量(每次减小的值)就是“加速度曲线”的体现。这些参数需要实际调试,间隔太短容易丢步,太长则加减速过程冗长。
多轴联动(比如雕刻机的X/Y/Z三轴)思路完全一样:每个轴分配一个定时器通道,分别启动各自的PWM输出。在一定脉冲数范围内,三轴同时输出,就能走出直线插补的效果。更复杂的S形加减速、规划器,就属于更深入的内容了,但底层仍然是“定时器PWM输出+动态修改频率”。
3.4 标准库的PWM实现差异
很多老项目还在用标准库(SPL),这里简要提一下,方便有需要的读者对照。标准库的初始化逻辑和HAL库是一样的,区别只是寄存器操作的封装方式:
// 标准库初始化PWM的结构体 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 配置时基 TIM_TimeBaseStructure.TIM_Prescaler = 71; TIM_TimeBaseStructure.TIM_Period = 500; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); // 配置PWM模式 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 250; // CCR值 TIM_OC1Init(TIM2, &TIM_OCInitStructure); // 启动PWM TIM_Cmd(TIM2, ENABLE); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);核心逻辑完全一致:预分频、周期、比较值、PWM模式。用标准库还是HAL库,不影响对原理的理解。
4. 调试经验与常见问题排查
4.1 电机不转但PWM有波形,查这四个地方
这是刚上手时最常碰到的情况。明明用示波器能看到PA0口有脉冲波形,电机就是纹丝不动。按优先级排查:
- 检查驱动器的供电是否接好。A4988/TB6600这类的驱动器需要外部电机电源(12V/24V),MCU的PWM只是信号,不带功率。电机电源没接或者电压不足,电机自然不转。
- 检查STEP引脚接线是否确实接到了定时器输出引脚。特别是用CubeMX配置时,确认生成的引脚是不是你想接的那一个(比如TIM2_CH1默认在PA0,但你可能从别的例程里习惯性接了PA6)。
- 检查驱动器的ENABLE引脚状态。很多驱动模块的ENA引脚默认状态下可能已经是“失能”状态,需要通过GPIO拉低/拉高解除(具体看模块丝印)。
- 检查DIR引脚的电平。方向信号如果没接悬空,有少数模块会进入奇怪的内部状态。最好给DIR一个确定电平。
4.2 启动瞬间抖动但不动,加速太快了
这个现象特别典型:上电后给个速度,电机“咔咔”响但不转,或者只在原地颤动。
这个现象通常是PWM频率和电机负载不匹配导致的。如果电机额定电流、电压都正常,那么问题基本锁定在“初始频率太高,转矩不足,导致转子无法响应第一个脉冲序列”。
针对这个问题的解决方案很明确:
- 降低起始频率(增大初始ARR值),比如从500Hz起步,而不是直接2000Hz。
- 如果必须快速启动,检查驱动器的电流设置拨码(比如A4988的Vref电位器),电流设置太低会导致低速扭矩不足。
4.3 丢步、噪声大,排查清单
调试现场遇到过各式各样的问题,整理了一个排查清单,覆盖了实际项目中我会优先检查的几项:
| 现象 | 检查点 | 解决方法 |
|---|---|---|
| 高速丢掉脉冲 | PWM频率超过驱动器最大响应频率 | 降低最高速度或换更高性能驱动器 |
| 中低速共振震动大 | 步进电机存在共振区间 | 避开共振区,或使用更高细分 |
| 运行时异响 | 驱动器电流设置偏大/偏小 | 调整驱动电流到电机额定值 |
| 突然原地振动不转 | 某一相线接触不良 | 检查电机相线是否牢固 |
| 方向控制失效 | 驱动器DIR接入引脚GPIO模式 | 确保DIR引脚配置为Output并且复用没冲突 |
4.4 占空比能不能是100%
标题热搜里有一条是“stm32f103芯片定时器输出占空比到不了100”,这里说清楚:对于步进电机驱动,占空比50%就足够好,不需要追求100%。虽然标准PWM模式里,CCR设为ARR时(或ARR+1)可以输出100%占空比,但这在步进驱动里没有实际意义——驱动器需要的只是脉冲边沿,持续高电平反而会导致电机丢步。
这个坑我在早期调试时踩过,当时想通过把占空比拉到最大来试探电机极限,结果电机直接失步,而且声音非常尖锐。如果一定要输出100%占空比(比如某些L298驱动板的特殊模式),用PWM2模式和CCR=ARR是可行的,但不要以为这是常规操作。
4.5 引脚不够用怎么办
如果是F103C8T6这种小封装,引脚紧张是常态。可以考虑三个方向:
- 用重映射功能释放默认占用的引脚,比如把TIM3从PA6/PA7重映射到PB4/PB5。
- 用74HC595或PCF8574这类IO扩展芯片来管理DIR和ENA,PWM通道仍然走定时器硬件引脚。
- 换更大封装的F103ZET6,引脚多一倍,做雕刻机控制器时基本不用操心IO不够的问题。
4.6 关于“error: no stm32 target found”这类连接问题
调试时偶尔会有调试器连接不上的报错。排查顺序:确认SWDIO/SWCLK接线是否松动、确认目标板供电是否正常、确认有没有其他软件占用调试器端口、确认复位引脚电平是否合理。大多数情况是接线问题,不是配置问题,不用紧张。
5. 写在最后:一些个人经验分享
这项目做下来,踩过的坑不少。早期为了赶进度,省掉加减速直接全速跑,结果电机疯狂丢步,定位全乱,排查了半天才意识到是加速度太陡。后来学乖了,无论电机大小,凡是涉及移动平台,无条件做加减速,即使做得很粗糙也比没有强。
另一个让我印象很深的是振动的干扰。有个项目里PWM线从驱动器旁边穿过,整个电机无缘无故乱跳,最后发现是线材干扰导致信号毛刺,换屏蔽线之后彻底解决。这类问题是示波器不一定能立刻看出来的,现场排查经验很关键。
最后想说,整个方案的可复用性非常高:定时器PWM驱动步进电机这招,从F103到F407,从标准库到HAL库,从入门小车到桌面雕刻机,思路一脉相承。把原理吃透了,换芯片、换驱动器、换电机,都只是换个参数重新配置而已。这个基础打牢,后面做更复杂的联动控制、闭环控制,都会顺畅很多。
本文还有配套的精品资源,点击获取