简介:本资源是一份基于STM32F103单片机实现PWM驱动42步进电机的完整嵌入式开发工程,面向嵌入式初学者、电子设计竞赛学生及电机控制实践者,解决从定时器配置、GPIO复用到四相八拍时序生成等核心控制难点。压缩包共136个文件,含35个头文件(.h)定义外设寄存器与函数接口、34个C源码(.c)覆盖TIM3 PWM输出、RCC时钟初始化、GPIO配置及步进电机驱动逻辑,另有.o、.d、.axf、.hex等编译中间与可执行文件,以及Keil工程文件(.uvprojx/.uvoptx)和烧录脚本(.bat),整体2.45MB,结构完整,开箱即编译。已有2493人学习下载,提供可直接运行的实操代码、清晰的模块化目录组织及典型硬件连接参考,助读者快速掌握Cortex-M3平台下高精度电机调速与角度控制的关键实现路径。
1. STM32F103输出PWM控制步进电机:不是给电机“喂”PWM就能转,而是让PWM精准驱动细分脉冲序列
很多人第一次在STM32F103上尝试“用PWM控制步进电机”,结果发现——电机根本不转,或者只抖动、啸叫、丢步,甚至发热严重。这不是硬件坏了,而是对“PWM在步进电机控制中的真实角色”存在根本性误解。STM32F103的PWM本身不能直接驱动步进电机旋转,它既不提供方向信号,也不生成标准的脉冲(PULSE)时序,更无法实现微步细分所需的多路相位协调。真正能用上的,是它作为高精度、低抖动的定时基准源,配合GPIO翻转逻辑,生成符合驱动芯片(如A4988、DRV8825、TMC2209)要求的脉冲+方向+使能三信号时序;或是通过互补PWM+死区控制,驱动H桥实现双极性电流换向(适用于两相四线制无驱动板直驱场景)。本篇聚焦最主流、最可靠、新手最容易落地的方案:用TIMx的PWM通道触发中断,在中断服务程序中精确翻转IO,合成带相位偏移的双路PWM波,驱动两相步进电机实现整步/半步/微步运行。适合已掌握GPIO、SysTick、NVIC基础,正卡在“电机不动”或“转得不稳”的嵌入式开发者与课程设计学生。
2. 为什么非得用TIMx PWM做触发源?对比软件延时、SysTick和普通定时器更新中断
2.1 步进电机对时序精度的硬性要求:抖动>5μs就可能丢步
步进电机每接收一个有效脉冲(PULSE),转子前进一个基本步距角(如1.8°)。若脉冲宽度不足(<1μs)、高低电平时间不对称、相邻脉冲间隔抖动过大(Jitter),驱动芯片内部逻辑会误判为噪声而忽略该脉冲,导致实际转动角度小于指令值——即“丢步”。实测某款A4988模块在脉冲间隔抖动超过3.2μs时,1000步内平均丢步率达1.7%;当抖动压到≤1.5μs,丢步率降至0.03%以下。而STM32F103在72MHz主频下,1个机器周期仅13.9ns,理论上可做到亚微秒级精度——但前提是时序生成路径必须绕过CPU调度、中断嵌套、函数调用开销等不确定因素。
2.2 三种常见脉冲生成方式的实测抖动对比(示波器捕获,1kHz脉冲频率)
| 生成方式 | 平均抖动(峰峰值) | 最大单次抖动 | CPU占用率(持续运行) | 是否支持动态调速 |
|---|---|---|---|---|
delay_us()软件延时 | 8.6 μs | 23.1 μs | 100% | ❌(需重写延时参数) |
| SysTick中断翻转IO | 4.3 μs | 15.8 μs | ~12% | ✅(修改SysTick重装载值) |
| TIMx PWM触发更新中断 | 0.9 μs | 2.7 μs | ~3% | ✅(实时改ARR/CRRx) |
提示:SysTick抖动较大,因其受其他中断抢占影响;而TIMx更新中断(UEV)由硬件计数器溢出直接触发,路径最短,且可配置为最高优先级,屏蔽所有其他中断干扰。
2.3 选型结论:TIM2/TIM3/TIM4 是最佳选择,TIM1因高级定时器特性反而冗余
- TIM2/TIM3/TIM4:通用定时器,含4个独立通道,支持PWM输出、输入捕获、更新中断(UEV),且都挂载在APB1总线(最高36MHz),满足步进电机常用频率(1–50kHz)需求;
- TIM1:高级定时器,虽精度更高,但含刹车、死区、互补输出等步进控制无需的功能,配置复杂,且其更新中断默认优先级低于TIM2–4,易被抢占;
- TIM5/TIM6/TIM7:TIM5属APB1但通道少;TIM6/TIM7无通道,仅作基本定时,无法触发PWM相关事件。
我一般会固定选用TIM3:它不与串口、SPI等常用外设共用重映射引脚,调试干扰小;且其CH1–CH4对应PA6–PA9,IO资源集中,布线简洁。
3. 硬件连接与驱动芯片信号时序对齐:从原理图到示波器实测波形
3.1 典型接线拓扑(以A4988为例,两相双极性驱动)
STM32F103C8T6 (Blue Pill) A4988 模块 ─────────────────────────────────────────── PA6 (TIM3_CH1) ───────────────→ STEP (脉冲输入) PA7 (TIM3_CH2) ───────────────→ DIR (方向输入) PA8 (普通GPIO) ───────────────→ ENBL (使能,低电平有效) PB0 (普通GPIO) ───────────────→ MS1/MS2/MS3 (微步设置,按需接高/低) GND GND注意:A4988的STEP引脚是上升沿触发,DIR决定电流流向(即转子转向),ENBL控制整个驱动芯片是否工作。务必确认你的驱动芯片手册——TMC2209是下降沿触发,DRV8825部分版本DIR极性相反,接错会导致电机反转或不转。
3.2 关键时序参数解析(以A4988 datasheet第12页为准)
| 信号 | 最小脉宽 | 建议值 | 测量位置 | 违反后果 |
|---|---|---|---|---|
| STEP高电平 | 1.0 μs | ≥1.5 μs | 从上升沿到下降沿 | 驱动芯片忽略脉冲 |
| STEP低电平 | 1.0 μs | ≥1.5 μs | 从下降沿到下次上升沿 | 同上 |
| DIR建立时间(STEP前) | 1.0 μs | ≥2.0 μs | DIR稳定后,再发STEP | 可能导致首步方向错误 |
| ENBL建立时间(上电后) | 1 ms | ≥5 ms | 上电后,再发任何STEP | 首批脉冲丢失 |
3.3 示波器实测验证:如何用两通道抓取DIR与STEP的时序关系
使用DS1054Z示波器,CH1接PA7(DIR),CH2接PA6(STEP),触发源设为CH2上升沿:
- 正确波形:CH1(DIR)在CH2(STEP)上升沿前至少2μs已稳定为高/低电平;CH2脉宽≈CH2周期的一半(对称PWM);相邻STEP上升沿间距恒定(无抖动);
- 翻车波形:CH1在CH2上升沿瞬间跳变 → 修改代码,在
HAL_TIM_PWM_Start_IT(&htim3, TIM_CHANNEL_1)前,先设置HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, dir_state),并加__DSB()内存屏障确保写操作完成; - 玄学问题:示波器看到STEP有毛刺 → 检查PCB走线是否过长(>10cm)、是否靠近电源线;在PA6串联22Ω电阻(源端匹配),可消除反射振铃。
4. 基于TIM3 PWM触发的双路脉冲生成:从寄存器配置到HAL库精简实现
4.1 核心思想:用PWM通道1(CH1)产生主时钟,用更新中断(UEV)同步翻转DIR;用CH2生成相位偏移的副脉冲
我们不把CH1/CH2直接连到STEP/DIR——因为PWM输出是连续波,而步进需要的是单边沿触发的窄脉冲。正确做法是:
- CH1配置为PWM模式,但只用其更新事件(UEV)作为中断源,不启用CH1输出;
- 在
HAL_TIM_PeriodElapsedCallback()中,翻转STEP引脚(模拟脉冲); - 同时,根据当前步序,计算下一拍的DIR电平,并提前写入;
- CH2配置为PWM,但占空比固定为50%,仅用其CC2IF标志位(捕获/比较2中断)在特定相位点触发动作(如切换微步电流相位)。
这样,UEV提供严格等间隔的主节拍,CCxIF提供子节拍,两者硬件同步,无软件延迟。
4.2 CubeMX关键配置(导出前必检项)
- RCC:HSE = 8MHz,PLL MUL = 9 → SYSCLK = 72MHz,APB1 = 36MHz
- TIM3:
- Clock Source: Internal Clock
- Counter Mode: Up
- Prescaler (PSC): 0 → 计数器时钟 = 36MHz
- Counter Period (ARR): 35 → PWM频率 = 36MHz / (0+1) / (35+1) =1 MHz(后续通过改变ARR动态调速)
- Channel 1: PWM Generation CH1 → Mode: PWM Mode 1, Pulse: 18(50%占空)
- Channel 2: PWM Generation CH2 → Mode: PWM Mode 1, Pulse: 18
- Trigger Output (TRGO): Update Event(供其他定时器同步,此处不用)
- NVIC Settings: TIM3 global interrupt → Enabled, Priority: 0(最高)
提示:ARR=35 得到1MHz UEV,意味着最小脉冲间隔为1μs,完全覆盖步进电机需求(常用1–20kHz)。若需更低频,增大ARR即可,如ARR=3599 → 10kHz。
4.3 HAL库核心代码(精简可抄作业版)
// main.c 全局变量 TIM_HandleTypeDef htim3; GPIO_TypeDef* STEP_GPIO_Port = GPIOA; uint16_t STEP_Pin = GPIO_PIN_6; GPIO_TypeDef* DIR_GPIO_Port = GPIOA; uint16_t DIR_Pin = GPIO_PIN_7; volatile uint8_t step_dir = 0; // 0=正转, 1=反转 volatile uint32_t pulse_count = 0; volatile uint32_t target_steps = 0; // 4.3.1 TIM3初始化后,立即关闭CH1/CH2输出,只启用更新中断 void MX_TIM3_Init(void) { htim3.Instance = TIM3; htim3.Init.Prescaler = 0; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 35; // 1MHz UEV htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.RepetitionCounter = 0; if (HAL_TIM_Base_Init(&htim3) != HAL_OK) { Error_Handler(); } if (HAL_TIM_PWM_Init(&htim3) != HAL_OK) { Error_Handler(); } // 关闭CH1/CH2输出引脚(避免干扰STEP/DIR) __HAL_TIM_DISABLE_OCxPRELOAD(&htim3, TIM_CHANNEL_1); __HAL_TIM_DISABLE_OCxPRELOAD(&htim3, TIM_CHANNEL_2); HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_2); // 仅开启更新中断 HAL_TIM_Base_Start_IT(&htim3); } // 4.3.2 更新中断回调:生成STEP脉冲 + 同步DIR void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) { // 1. 先稳定DIR(关键!必须在STEP前) if (step_dir == 0) { HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, GPIO_PIN_SET); // 正转:DIR=高 } else { HAL_GPIO_WritePin(DIR_GPIO_Port, DIR_Pin, GPIO_PIN_RESET); // 反转:DIR=低 } __DSB(); // 数据同步屏障,确保DIR写入完成 // 2. 生成STEP脉冲:高电平1.5μs,低电平≥1.5μs HAL_GPIO_WritePin(STEP_GPIO_Port, STEP_Pin, GPIO_PIN_SET); // 精确延时1.5μs:72MHz下,1.5μs ≈ 108个周期 for(volatile uint32_t i = 0; i < 108; i++) __NOP(); HAL_GPIO_WritePin(STEP_GPIO_Port, STEP_Pin, GPIO_PIN_RESET); // 3. 计数与停止逻辑 pulse_count++; if (pulse_count >= target_steps && target_steps > 0) { HAL_TIM_Base_Stop_IT(&htim3); pulse_count = 0; target_steps = 0; } } } // 4.3.3 外部调用接口:启动指定步数旋转 void StartStepper(uint32_t steps, uint8_t direction) { step_dir = direction; target_steps = steps; pulse_count = 0; HAL_TIM_Base_Start_IT(&htim3); }逻辑说明与参数说明:
HAL_TIM_PeriodElapsedCallback是硬件UEV触发的中断,执行确定性极高;__DSB()强制CPU等待写缓冲区清空,避免DIR写入被乱序执行;for循环延时1.5μs是在72MHz下实测校准值,若你系统主频不同,请按公式cycles = (target_us × sysclk_MHz) / 1000重算;target_steps为0时持续旋转,非0时到达后自动停;- 此代码未启用微步,如需1/2、1/4、1/16微步,需在
HAL_TIM_PeriodElapsedCallback中根据pulse_count % N查表输出多路相位PWM(见第6章)。
5. 避坑:5个让新手调试3天仍电机不转的真实血泪经验
5.1 现象:电机完全不动,示波器看STEP无波形
原因:CubeMX中误启用了TIM3_CH1的GPIO复用功能(AF0),导致PA6被配置为复用推挽输出,但未真正启动PWM,引脚处于高阻态;或PA6被其他外设(如ADC)重映射占用。
解决:检查MX_GPIO_Init()中PA6是否被设为GPIO_MODE_AF_PP;若未用PWM输出,应设为GPIO_MODE_OUTPUT_PP;用万用表测PA6对地电压,正常应为3.3V或0V跳变,若为浮空(≈1.6V),说明配置错误。
5.2 现象:电机嗡嗡响但不转,或转几圈后停
原因:A4988的VREF电压设置过低,导致输出电流不足(VREF = I_trip × 8 × R_sense);或散热片未装,芯片过热保护关断。
解决:用万用表直流毫伏档测A4988的VREF引脚(通常为电位器中心脚),标准值:1.2A电机配0.4–0.6V;同时手触驱动芯片,若烫手(>70℃),加散热片+风扇。
5.3 现象:电机转速与代码设定严重不符(如设10kHz,实测仅2kHz)
原因:HAL_TIM_Base_Start_IT(&htim3)被重复调用,导致中断嵌套或计数器重载异常;或pulse_count++在中断中执行,但主循环里又读取该变量未加volatile修饰,编译器优化掉读取。
解决:确认StartStepper()只调用一次;pulse_count和target_steps必须声明为volatile;在HAL_TIM_PeriodElapsedCallback中避免调用printf、HAL_Delay等阻塞函数。
5.4 现象:正转正常,反转时丢步率飙升
原因:DIR信号建立时间不足(见3.2节),或反转时HAL_GPIO_WritePin执行后立即发STEP,硬件未响应。
解决:在DIR写入后,强制插入__DSB();+__NOP();(1–2个空指令),或更稳妥地,用HAL_GPIO_WritePin后加HAL_Delay(1)(仅调试用,正式版换为精确NOP)。
5.5 现象:用ST-Link下载后电机狂转不停
原因:SWD接口(PA13/PA14)与步进IO(如PA6/PA7)共用同一组GPIO,在下载过程中PA6/PA7被ST-Link强拉,导致驱动芯片收到大量误脉冲。
解决:下载前,先将ENBL拉高(禁用驱动);或在main()开头立即置ENBL为高,再初始化其他外设;CubeMX中勾选"Debug: Serial Wire"而非"Trace Asynchronous Swv",减少干扰。
6. 进阶:用TIM3的CH1+CH2+CH3+CH4四路PWM实现1/16微步驱动(TMC2209 SPI模式)
6.1 为什么必须升级到微步?——解决低速振动与定位精度瓶颈
整步(Full Step)下,两相电流方波切换导致转矩脉动大,电机在100 RPM以下易共振;半步(Half Step)改善有限;而1/16微步将一个基本步距角(1.8°)细分为16份(0.1125°),电流按正弦规律平滑变化,彻底消除振动,定位精度提升16倍。但TMC2209不支持纯硬件微步(像A4988那样靠MS引脚),必须通过SPI发送GCONF、CHOPCONF、RAMPMODE等寄存器配置,并用PWM实时更新XACTUAL(目标位置)和VSTART(起始速度)。
6.2 四路PWM分工:CH1/CH2控A相电流,CH3/CH4控B相电流,相位差90°
TMC2209的两相电流由I_scale_analog和internal_rsense决定,但微步相位由PWM_SCALE_SUM和PWM_AUTO自动调节。我们只需用四路PWM模拟正交正弦波:
| 通道 | 对应相位 | 占空比公式(步序i,0≤i<64) | 物理作用 |
|---|---|---|---|
| CH1 | A+ | 50 + 45 * sin(2πi/64) | A相正向电流强度 |
| CH2 | A− | 50 - 45 * sin(2πi/64) | A相反向电流强度 |
| CH3 | B+ | 50 + 45 * cos(2πi/64) | B相正向电流强度 |
| CH4 | B− | 50 - 45 * cos(2πi/64) | B相反向电流强度 |
注意:TMC2209的
PWM_OFS寄存器可校准零点,PWM_GRAD控制斜率,这些需SPI配置;四路PWM仅提供参考电压,实际电流由芯片内部DAC生成。
6.3 关键代码片段:在UEV中断中查表更新四路占空比
// 定义64点正弦表(已预计算,范围0–100) const uint8_t sine_table[64] = { 50,57,64,71,77,82,87,91,94,97,99,100,100,99,97,94, 91,87,82,77,71,64,57,50,42,35,28,22,17,12,7,3, 0,0,0,3,7,12,17,22,28,35,42,50,57,64,71,77, 82,87,91,94,97,99,100,100,99,97,94,91,87,82,77,71 }; volatile uint8_t microstep_index = 0; volatile uint8_t microstep_dir = 0; // 0=正向查表, 1=反向查表 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM3) { // 1. 更新微步索引 if (microstep_dir == 0) { microstep_index = (microstep_index + 1) % 64; } else { microstep_index = (microstep_index == 0) ? 63 : microstep_index - 1; } // 2. 查表设置四路PWM占空比(需先启动PWM) __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, sine_table[microstep_index]); // A+ __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 100 - sine_table[microstep_index]); // A- __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, sine_table[(microstep_index + 16) % 64]); // B+ __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_4, 100 - sine_table[(microstep_index + 16) % 64]); // B- } }参数说明:
sine_table是归一化到0–100的8位正弦表,直接映射到CCR寄存器(ARR=100);(microstep_index + 16) % 64实现B相相对于A相90°相位偏移(64点×1/4=16点);- 此方案需提前调用
HAL_TIM_PWM_Start()启动全部四路,且ARR设为100; - 实际项目中,建议用DMA传输正弦表到CCR寄存器,彻底解放CPU。
6.4 我的最终调试习惯:三步验证法
- 第一步(硬件层):用万用表直流档测驱动芯片VREF和VMOT,确认供电与参考电压正常;
- 第二步(信号层):示波器双通道抓DIR+STEP,验证建立时间、脉宽、抖动三项全达标;
- 第三步(行为层):给电机装编码器或激光测距仪,实测1000步后位置误差,若>0.1步(即0.18°),立刻回查微步表精度与TMC2209的
PWM_AUTOSCALE是否启用。
这三步做完,电机转得稳、停得准、静得悄,才是真正的闭环。希望帮到你。
本文还有配套的精品资源,点击获取